diff --git a/src/main/java/cn/yinlihupo/controller/ai/AiChatController.java b/src/main/java/cn/yinlihupo/controller/ai/AiChatController.java index 8e401e2..4b34572 100644 --- a/src/main/java/cn/yinlihupo/controller/ai/AiChatController.java +++ b/src/main/java/cn/yinlihupo/controller/ai/AiChatController.java @@ -25,7 +25,7 @@ import java.util.UUID; */ @Slf4j @RestController -@RequestMapping("/ai/chat") +@RequestMapping("/api/v1/ai/chat") @RequiredArgsConstructor @Tag(name = "AI对话", description = "AI智能问答相关接口") public class AiChatController { diff --git a/src/main/java/cn/yinlihupo/controller/ai/AiKnowledgeBaseController.java b/src/main/java/cn/yinlihupo/controller/ai/AiKnowledgeBaseController.java index 8cb1b90..d207ed2 100644 --- a/src/main/java/cn/yinlihupo/controller/ai/AiKnowledgeBaseController.java +++ b/src/main/java/cn/yinlihupo/controller/ai/AiKnowledgeBaseController.java @@ -3,6 +3,7 @@ package cn.yinlihupo.controller.ai; import cn.yinlihupo.common.core.BaseResponse; import cn.yinlihupo.common.util.ResultUtils; import cn.yinlihupo.common.util.SecurityUtils; +import cn.yinlihupo.domain.vo.DocumentChunkVO; import cn.yinlihupo.domain.vo.KbDocumentVO; import cn.yinlihupo.service.ai.AiKnowledgeBaseService; import io.swagger.v3.oas.annotations.Operation; @@ -20,7 +21,7 @@ import java.util.List; */ @Slf4j @RestController -@RequestMapping("/ai/kb") +@RequestMapping("/api/v1/ai/kb") @RequiredArgsConstructor @Tag(name = "AI知识库", description = "AI知识库文档管理相关接口") public class AiKnowledgeBaseController { @@ -134,4 +135,53 @@ public class AiKnowledgeBaseController { return ResultUtils.error("重新索引失败: " + e.getMessage()); } } + + /** + * 获取文档分片列表 + * + * @param docId 文档UUID + * @return 分片列表 + */ + @GetMapping("/document/{docId}/chunks") + @Operation(summary = "获取文档分片列表", description = "获取指定文档的所有分片信息") + public BaseResponse> getDocumentChunks(@PathVariable String docId) { + Long userId = SecurityUtils.getCurrentUserId(); + if (userId == null) { + return ResultUtils.error("用户未登录"); + } + + try { + List chunks = knowledgeBaseService.getDocumentChunks(docId); + return ResultUtils.success("查询成功", chunks); + } catch (Exception e) { + log.error("获取文档分片失败: {}", e.getMessage(), e); + return ResultUtils.error("获取文档分片失败: " + e.getMessage()); + } + } + + /** + * 获取分片详情 + * + * @param chunkId 分片ID + * @return 分片详情 + */ + @GetMapping("/chunk/{chunkId}") + @Operation(summary = "获取分片详情", description = "获取指定分片的详细信息") + public BaseResponse getChunkDetail(@PathVariable String chunkId) { + Long userId = SecurityUtils.getCurrentUserId(); + if (userId == null) { + return ResultUtils.error("用户未登录"); + } + + try { + DocumentChunkVO chunk = knowledgeBaseService.getChunkDetail(chunkId); + if (chunk == null) { + return ResultUtils.error("分片不存在"); + } + return ResultUtils.success("查询成功", chunk); + } catch (Exception e) { + log.error("获取分片详情失败: {}", e.getMessage(), e); + return ResultUtils.error("获取分片详情失败: " + e.getMessage()); + } + } } diff --git a/src/main/java/cn/yinlihupo/domain/dto/ChatRequest.java b/src/main/java/cn/yinlihupo/domain/dto/ChatRequest.java index 6a56b9b..4fb2e31 100644 --- a/src/main/java/cn/yinlihupo/domain/dto/ChatRequest.java +++ b/src/main/java/cn/yinlihupo/domain/dto/ChatRequest.java @@ -2,8 +2,6 @@ package cn.yinlihupo.domain.dto; import lombok.Data; -import java.util.UUID; - /** * AI对话请求DTO */ @@ -13,7 +11,7 @@ public class ChatRequest { /** * 会话ID(为空则新建会话) */ - private UUID sessionId; + private String sessionId; /** * 项目ID(必填) diff --git a/src/main/java/cn/yinlihupo/domain/vo/DocumentChunkVO.java b/src/main/java/cn/yinlihupo/domain/vo/DocumentChunkVO.java new file mode 100644 index 0000000..f9c50ac --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/vo/DocumentChunkVO.java @@ -0,0 +1,55 @@ +package cn.yinlihupo.domain.vo; + +import lombok.Data; + +/** + * 文档分片VO + */ +@Data +public class DocumentChunkVO { + + /** + * 分片ID + */ + private String id; + + /** + * 原始文档ID + */ + private String docId; + + /** + * 分片内容 + */ + private String content; + + /** + * 分片序号 + */ + private Integer chunkIndex; + + /** + * 总分片数 + */ + private Integer chunkTotal; + + /** + * 文档标题 + */ + private String title; + + /** + * 文档类型 + */ + private String docType; + + /** + * 来源类型 + */ + private String sourceType; + + /** + * 状态 + */ + private String status; +} diff --git a/src/main/java/cn/yinlihupo/mapper/AiDocumentMapper.java b/src/main/java/cn/yinlihupo/mapper/AiDocumentMapper.java index ba7ef16..e242c61 100644 --- a/src/main/java/cn/yinlihupo/mapper/AiDocumentMapper.java +++ b/src/main/java/cn/yinlihupo/mapper/AiDocumentMapper.java @@ -1,6 +1,7 @@ package cn.yinlihupo.mapper; import cn.yinlihupo.domain.entity.AiDocument; +import cn.yinlihupo.domain.vo.DocumentChunkVO; import cn.yinlihupo.domain.vo.KbDocumentVO; import cn.yinlihupo.domain.vo.ReferencedDocVO; import com.baomidou.mybatisplus.core.mapper.BaseMapper; @@ -88,4 +89,21 @@ public interface AiDocumentMapper extends BaseMapper { * @return 影响行数 */ int incrementQueryCount(@Param("id") String id); + + /** + * 查询文档分片列表 + * 通过 metadata 中的 doc_id 字段查询关联的分片 + * + * @param docId 文档ID + * @return 分片列表 + */ + List selectDocumentChunks(@Param("docId") String docId); + + /** + * 查询文档分片详情 + * + * @param chunkId 分片ID + * @return 分片详情 + */ + DocumentChunkVO selectChunkById(@Param("chunkId") String chunkId); } diff --git a/src/main/java/cn/yinlihupo/service/ai/AiKnowledgeBaseService.java b/src/main/java/cn/yinlihupo/service/ai/AiKnowledgeBaseService.java index 9fd48bf..58bee93 100644 --- a/src/main/java/cn/yinlihupo/service/ai/AiKnowledgeBaseService.java +++ b/src/main/java/cn/yinlihupo/service/ai/AiKnowledgeBaseService.java @@ -1,5 +1,6 @@ package cn.yinlihupo.service.ai; +import cn.yinlihupo.domain.vo.DocumentChunkVO; import cn.yinlihupo.domain.vo.KbDocumentVO; import org.springframework.web.multipart.MultipartFile; @@ -57,4 +58,20 @@ public interface AiKnowledgeBaseService { * @param docId 文档ID */ void processDocumentAsync(String docId); + + /** + * 获取文档分片列表 + * + * @param docId 文档ID + * @return 分片列表 + */ + List getDocumentChunks(String docId); + + /** + * 获取分片详情 + * + * @param chunkId 分片ID + * @return 分片详情 + */ + DocumentChunkVO getChunkDetail(String chunkId); } diff --git a/src/main/java/cn/yinlihupo/service/ai/impl/AiChatServiceImpl.java b/src/main/java/cn/yinlihupo/service/ai/impl/AiChatServiceImpl.java index 3c467cd..c1353c8 100644 --- a/src/main/java/cn/yinlihupo/service/ai/impl/AiChatServiceImpl.java +++ b/src/main/java/cn/yinlihupo/service/ai/impl/AiChatServiceImpl.java @@ -67,24 +67,24 @@ public class AiChatServiceImpl implements AiChatService { @Override public void streamChat(ChatRequest request, Long userId, SseEmitter emitter) { long startTime = System.currentTimeMillis(); - UUID sessionId = request.getSessionId(); + String sessionId = request.getSessionId(); boolean isNewSession = (sessionId == null); try { // 1. 获取或创建会话 if (isNewSession) { - sessionId = UUID.randomUUID(); + sessionId = UUID.randomUUID().toString(); String title = generateSessionTitle(request.getMessage()); createSession(userId, request.getProjectId(), request.getTimelineNodeId(), request.getMessage(), title); } else { // 验证会话权限 - if (!hasSessionAccess(sessionId, userId)) { + if (!hasSessionAccess(UUID.fromString(sessionId), userId)) { sendError(emitter, "无权访问该会话"); return; } } - final UUID finalSessionId = sessionId; + final String finalSessionId = sessionId; // 发送开始消息 sendEvent(emitter, "start", Map.of( @@ -107,7 +107,7 @@ public class AiChatServiceImpl implements AiChatService { // 4. 构建Prompt String systemPrompt = buildSystemPrompt(request.getProjectId(), retrievedDocs); - List messages = buildMessages(finalSessionId, request.getContextWindow(), + List messages = buildMessages(UUID.fromString(finalSessionId) , request.getContextWindow(), systemPrompt, request.getMessage()); // 5. 流式调用LLM @@ -331,15 +331,15 @@ public class AiChatServiceImpl implements AiChatService { /** * 保存消息 */ - private Long saveMessage(UUID sessionId, Long userId, Long projectId, + private Long saveMessage(String sessionId, Long userId, Long projectId, Long timelineNodeId, String role, String content, String referencedDocIds) { // 获取当前最大序号 - Integer maxIndex = chatHistoryMapper.selectMaxMessageIndex(sessionId); + Integer maxIndex = chatHistoryMapper.selectMaxMessageIndex(UUID.fromString(sessionId)); int nextIndex = (maxIndex != null ? maxIndex : 0) + 1; AiChatMessage message = new AiChatMessage(); - message.setSessionId(sessionId); + message.setSessionId(UUID.fromString(sessionId)); message.setUserId(userId); message.setProjectId(projectId); message.setTimelineNodeId(timelineNodeId); diff --git a/src/main/java/cn/yinlihupo/service/ai/impl/AiKnowledgeBaseServiceImpl.java b/src/main/java/cn/yinlihupo/service/ai/impl/AiKnowledgeBaseServiceImpl.java index 97b140c..46b5f0c 100644 --- a/src/main/java/cn/yinlihupo/service/ai/impl/AiKnowledgeBaseServiceImpl.java +++ b/src/main/java/cn/yinlihupo/service/ai/impl/AiKnowledgeBaseServiceImpl.java @@ -1,6 +1,7 @@ package cn.yinlihupo.service.ai.impl; import cn.yinlihupo.domain.entity.AiDocument; +import cn.yinlihupo.domain.vo.DocumentChunkVO; import cn.yinlihupo.domain.vo.KbDocumentVO; import cn.yinlihupo.mapper.AiDocumentMapper; import cn.yinlihupo.service.ai.AiKnowledgeBaseService; @@ -209,4 +210,14 @@ public class AiKnowledgeBaseServiceImpl implements AiKnowledgeBaseService { vo.setCreateTime(doc.getCreateTime()); return vo; } + + @Override + public List getDocumentChunks(String docId) { + return documentMapper.selectDocumentChunks(docId); + } + + @Override + public DocumentChunkVO getChunkDetail(String chunkId) { + return documentMapper.selectChunkById(chunkId); + } } diff --git a/src/main/java/cn/yinlihupo/service/ai/rag/DocumentProcessor.java b/src/main/java/cn/yinlihupo/service/ai/rag/DocumentProcessor.java index edb67f9..cf5107b 100644 --- a/src/main/java/cn/yinlihupo/service/ai/rag/DocumentProcessor.java +++ b/src/main/java/cn/yinlihupo/service/ai/rag/DocumentProcessor.java @@ -14,6 +14,7 @@ import org.springframework.stereotype.Component; import java.io.InputStream; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; @@ -165,44 +166,51 @@ public class DocumentProcessor { /** * 存储切片到向量库 + * 每个分片都是独立记录,包含完整的项目属性用于检索 * - * @param parentDoc 父文档 - * @param chunks 切片列表 + * @param doc 文档实体(包含项目属性) + * @param chunks 切片列表 */ - private void storeChunks(AiDocument parentDoc, List chunks) { - String parentId = parentDoc.getId(); + private void storeChunks(AiDocument doc, List chunks) { + String docId = doc.getId(); for (int i = 0; i < chunks.size(); i++) { String chunkContent = chunks.get(i); // 使用UUID生成唯一的chunk ID,确保格式正确 String chunkId = UUID.randomUUID().toString(); - // 创建向量文档 - Document vectorDoc = new Document( - chunkId, - chunkContent, - Map.of( - "project_id", parentDoc.getProjectId() != null ? parentDoc.getProjectId().toString() : "", - "timeline_node_id", parentDoc.getTimelineNodeId() != null ? parentDoc.getTimelineNodeId().toString() : "", - "chunk_index", i, - "chunk_total", chunks.size(), - "chunk_parent_id", parentId, - "title", parentDoc.getTitle() != null ? parentDoc.getTitle() : "", - "source_type", parentDoc.getSourceType() != null ? parentDoc.getSourceType() : "", - "status", "active" - ) - ); + // 创建向量文档,每个分片都包含完整的项目属性 + Map metadata = new HashMap<>(); + // 项目关联属性(用于检索过滤) + metadata.put("project_id", doc.getProjectId() != null ? doc.getProjectId().toString() : ""); + metadata.put("timeline_node_id", doc.getTimelineNodeId() != null ? doc.getTimelineNodeId().toString() : ""); + metadata.put("kb_id", doc.getKbId() != null ? doc.getKbId().toString() : ""); + // 文档来源信息 + metadata.put("source_type", doc.getSourceType() != null ? doc.getSourceType() : ""); + metadata.put("source_id", doc.getSourceId() != null ? doc.getSourceId().toString() : ""); + // 文档信息 + metadata.put("doc_id", docId); // 原始文档ID,用于关联查询 + metadata.put("title", doc.getTitle() != null ? doc.getTitle() : ""); + metadata.put("doc_type", doc.getDocType() != null ? doc.getDocType() : ""); + metadata.put("file_type", doc.getFileType() != null ? doc.getFileType() : ""); + // 分片信息 + metadata.put("chunk_index", i); + metadata.put("chunk_total", chunks.size()); + // 状态 + metadata.put("status", "active"); + + Document vectorDoc = new Document(chunkId, chunkContent, metadata); // 存储到向量库 vectorStore.add(List.of(vectorDoc)); - // 如果是第一个切片,更新父文档内容 - if (i == 0) { - parentDoc.setContent(chunkContent); - documentMapper.updateById(parentDoc); - } + log.debug("存储切片: {}/{}, docId: {}, chunkId: {}", i + 1, chunks.size(), docId, chunkId); + } - log.debug("存储切片: {}/{}, parentId: {}, chunkId: {}", i + 1, chunks.size(), parentId, chunkId); + // 更新文档内容为第一个分片(用于预览) + if (!chunks.isEmpty()) { + doc.setContent(chunks.get(0)); + documentMapper.updateById(doc); } } diff --git a/src/main/resources/mapper/AiDocumentMapper.xml b/src/main/resources/mapper/AiDocumentMapper.xml index 3f4d597..bc9f23e 100644 --- a/src/main/resources/mapper/AiDocumentMapper.xml +++ b/src/main/resources/mapper/AiDocumentMapper.xml @@ -2,7 +2,7 @@ - + @@ -58,11 +58,11 @@ AND deleted = 0 - + @@ -98,4 +98,40 @@ WHERE id = #{id} + + + + + +