From d9cf838acee28311fec963949e875261ef9f4204 Mon Sep 17 00:00:00 2001 From: bruce Date: Mon, 1 Jun 2026 03:38:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(rag):=20=E8=A1=A5=E9=BD=90=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=B7=A5=E4=BD=9C=E5=8F=B0=E8=81=9A=E5=90=88=E4=B8=8E?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E5=88=86=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KnowledgeWorkspaceController.java | 36 +++++ .../bruce/rag/factory/RagDocumentFactory.java | 71 +++++++++ .../bruce/rag/factory/RagStoreFactory.java | 57 +++++++ .../service/IKnowledgeWorkspaceService.java | 14 ++ .../impl/KnowledgeWorkspaceServiceImpl.java | 139 ++++++++++++++++++ .../service/impl/RagDocumentServiceImpl.java | 84 +++++------ .../rag/service/impl/RagStoreServiceImpl.java | 44 ++---- .../rag/vo/KnowledgeWorkspaceDocumentVO.java | 38 +++++ .../bruce/rag/vo/KnowledgeWorkspaceVO.java | 88 +++++++++++ .../rag/RagDocumentServiceImplTests.java | 4 + .../rag/RagStoreSaveValidationTests.java | 18 ++- .../bruce/rag/factory/RagFactoryTests.java | 129 ++++++++++++++++ .../KnowledgeWorkspaceServiceTests.java | 120 +++++++++++++++ .../api/__tests__/knowledgeWorkspace.spec.ts | 20 +++ frontend/src/api/knowledgeWorkspace.ts | 40 +++++ 15 files changed, 823 insertions(+), 79 deletions(-) create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/controller/KnowledgeWorkspaceController.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/factory/RagDocumentFactory.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/factory/RagStoreFactory.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/service/IKnowledgeWorkspaceService.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/service/impl/KnowledgeWorkspaceServiceImpl.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/vo/KnowledgeWorkspaceDocumentVO.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/vo/KnowledgeWorkspaceVO.java create mode 100644 common-agent-rag/src/test/java/com/bruce/rag/factory/RagFactoryTests.java create mode 100644 common-agent-rag/src/test/java/com/bruce/rag/workspace/KnowledgeWorkspaceServiceTests.java create mode 100644 frontend/src/api/__tests__/knowledgeWorkspace.spec.ts create mode 100644 frontend/src/api/knowledgeWorkspace.ts diff --git a/common-agent-rag/src/main/java/com/bruce/rag/controller/KnowledgeWorkspaceController.java b/common-agent-rag/src/main/java/com/bruce/rag/controller/KnowledgeWorkspaceController.java new file mode 100644 index 0000000..ea76f2f --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/controller/KnowledgeWorkspaceController.java @@ -0,0 +1,36 @@ +package com.bruce.rag.controller; + +import com.bruce.common.domain.model.RequestResult; +import com.bruce.rag.service.IKnowledgeWorkspaceService; +import com.bruce.rag.vo.KnowledgeWorkspaceVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 知识工作台聚合接口。 + */ +@Tag(name = "知识工作台") +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/knowledge/workspaces") +public class KnowledgeWorkspaceController { + + private final IKnowledgeWorkspaceService knowledgeWorkspaceService; + + @Operation(summary = "查询知识工作台聚合视图") + @GetMapping("/{storeId}") + public RequestResult getWorkspace(@PathVariable("storeId") Long storeId) { + log.info("KnowledgeWorkspaceController.getWorkspace start, storeId={}", storeId); + KnowledgeWorkspaceVO workspace = knowledgeWorkspaceService.getWorkspace(storeId); + log.info("KnowledgeWorkspaceController.getWorkspace success, storeId={}, documentCount={}, healthScore={}", + storeId, workspace.getDocumentCount(), workspace.getHealthScore()); + return RequestResult.success(workspace); + } +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/factory/RagDocumentFactory.java b/common-agent-rag/src/main/java/com/bruce/rag/factory/RagDocumentFactory.java new file mode 100644 index 0000000..2feae07 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/factory/RagDocumentFactory.java @@ -0,0 +1,71 @@ +package com.bruce.rag.factory; + +import com.bruce.rag.dto.request.RagDocumentSaveRequest; +import com.bruce.rag.dto.response.RagDocumentResponse; +import com.bruce.rag.entity.RagDocument; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * 知识文档工厂,统一处理文档相关 DTO、Entity、VO 的转换逻辑。 + */ +@Component +public class RagDocumentFactory { + + /** + * 将保存请求转换为实体,供新增或全量更新场景复用。 + */ + public RagDocument toEntity(RagDocumentSaveRequest request) { + if (request == null) { + return null; + } + RagDocument entity = new RagDocument(); + applyEditableFields(entity, request); + return entity; + } + + /** + * 将请求中的可编辑字段写入已有实体,避免服务层散落 setXXX 逻辑。 + */ + public void applyEditableFields(RagDocument entity, RagDocumentSaveRequest request) { + if (entity == null || request == null) { + return; + } + entity.setId(request.getId()); + entity.setStoreId(request.getStoreId()); + entity.setAttachmentId(request.getAttachmentId()); + entity.setDocumentTitle(trimToNull(request.getDocumentTitle())); + entity.setDocumentSummary(trimToNull(request.getDocumentSummary())); + entity.setParseStatus(trimToNull(request.getParseStatus())); + entity.setIndexStatus(trimToNull(request.getIndexStatus())); + entity.setEnabled(request.getEnabled()); + entity.setErrorMessage(trimToNull(request.getErrorMessage())); + entity.setRemark(trimToNull(request.getRemark())); + } + + /** + * 将实体转换为前端使用的返回对象。 + */ + public RagDocumentResponse toResponse(RagDocument entity) { + if (entity == null) { + return null; + } + RagDocumentResponse response = new RagDocumentResponse(); + BeanUtils.copyProperties(entity, response); + return response; + } + + public List toResponses(List entities) { + return entities == null ? List.of() : entities.stream().map(this::toResponse).toList(); + } + + private String trimToNull(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/factory/RagStoreFactory.java b/common-agent-rag/src/main/java/com/bruce/rag/factory/RagStoreFactory.java new file mode 100644 index 0000000..53f4903 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/factory/RagStoreFactory.java @@ -0,0 +1,57 @@ +package com.bruce.rag.factory; + +import com.bruce.rag.dto.request.RagStoreSaveRequest; +import com.bruce.rag.dto.response.RagStoreResponse; +import com.bruce.rag.entity.RagStore; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * 知识库工厂,统一负责知识库请求对象、实体对象和返回对象之间的转换。 + */ +@Component +public class RagStoreFactory { + + /** + * 将保存请求转换为知识库实体,统一处理空白裁剪规则。 + */ + public RagStore toEntity(RagStoreSaveRequest request) { + if (request == null) { + return null; + } + RagStore entity = new RagStore(); + entity.setId(request.getId()); + entity.setStoreCode(trimToNull(request.getStoreCode())); + entity.setStoreName(trimToNull(request.getStoreName())); + entity.setDescription(trimToNull(request.getDescription())); + entity.setStatus(trimToNull(request.getStatus())); + entity.setRemark(trimToNull(request.getRemark())); + return entity; + } + + /** + * 将实体转换为返回对象,避免接口层直接暴露实体结构。 + */ + public RagStoreResponse toResponse(RagStore entity) { + if (entity == null) { + return null; + } + RagStoreResponse response = new RagStoreResponse(); + BeanUtils.copyProperties(entity, response); + return response; + } + + public List toResponses(List entities) { + return entities == null ? List.of() : entities.stream().map(this::toResponse).toList(); + } + + private String trimToNull(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/service/IKnowledgeWorkspaceService.java b/common-agent-rag/src/main/java/com/bruce/rag/service/IKnowledgeWorkspaceService.java new file mode 100644 index 0000000..922a680 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/service/IKnowledgeWorkspaceService.java @@ -0,0 +1,14 @@ +package com.bruce.rag.service; + +import com.bruce.rag.vo.KnowledgeWorkspaceVO; + +/** + * 知识工作台聚合服务,对外提供知识资产工作台需要的统一视图。 + */ +public interface IKnowledgeWorkspaceService { + + /** + * 按知识库ID聚合知识工作台视图。 + */ + KnowledgeWorkspaceVO getWorkspace(Long storeId); +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/service/impl/KnowledgeWorkspaceServiceImpl.java b/common-agent-rag/src/main/java/com/bruce/rag/service/impl/KnowledgeWorkspaceServiceImpl.java new file mode 100644 index 0000000..44b2ed1 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/service/impl/KnowledgeWorkspaceServiceImpl.java @@ -0,0 +1,139 @@ +package com.bruce.rag.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.bruce.modelprovider.dto.response.RagStoreModelConfigResponse; +import com.bruce.modelprovider.service.IRagStoreModelConfigService; +import com.bruce.rag.dto.request.RagDocumentQueryRequest; +import com.bruce.rag.dto.response.RagDocumentResponse; +import com.bruce.rag.dto.response.RagStoreResponse; +import com.bruce.rag.entity.RagChunk; +import com.bruce.rag.entity.RagChunkEmbedding; +import com.bruce.rag.service.IKnowledgeWorkspaceService; +import com.bruce.rag.service.IRagChunkEmbeddingService; +import com.bruce.rag.service.IRagChunkService; +import com.bruce.rag.service.IRagDocumentService; +import com.bruce.rag.service.IRagStoreService; +import com.bruce.rag.vo.KnowledgeWorkspaceDocumentVO; +import com.bruce.rag.vo.KnowledgeWorkspaceVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 知识工作台聚合服务实现。 + *

+ * 该服务负责把知识库基础信息、文档状态、模型绑定和索引结果拼成前端工作台可直接消费的聚合视图。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class KnowledgeWorkspaceServiceImpl implements IKnowledgeWorkspaceService { + + private static final String PARSE_STATUS_PARSED = "PARSED"; + private static final String PARSE_STATUS_FAILED = "FAILED"; + private static final String INDEX_STATUS_INDEXED = "INDEXED"; + private static final String INDEX_STATUS_PENDING = "PENDING"; + + private final IRagStoreService ragStoreService; + private final IRagDocumentService ragDocumentService; + private final IRagChunkService ragChunkService; + private final IRagChunkEmbeddingService ragChunkEmbeddingService; + private final IRagStoreModelConfigService ragStoreModelConfigService; + + @Override + public KnowledgeWorkspaceVO getWorkspace(Long storeId) { + log.info("知识工作台查询开始,storeId={}", storeId); + if (storeId == null) { + throw new IllegalArgumentException("知识库ID不能为空"); + } + + RagStoreResponse store = ragStoreService.getResponseById(storeId); + if (store == null) { + throw new IllegalArgumentException("知识库不存在,ID: " + storeId); + } + + RagDocumentQueryRequest queryRequest = new RagDocumentQueryRequest(); + queryRequest.setStoreId(storeId); + List documents = ragDocumentService.query(queryRequest); + RagStoreModelConfigResponse modelConfig = ragStoreModelConfigService.getByStoreId(storeId); + + long chunkCount = ragChunkService.count(new LambdaQueryWrapper() + .eq(RagChunk::getStoreId, storeId) + .eq(RagChunk::getEnabled, true)); + long embeddingCount = ragChunkEmbeddingService.count(new LambdaQueryWrapper() + .eq(RagChunkEmbedding::getStoreId, storeId) + .eq(RagChunkEmbedding::getEnabled, true)); + + KnowledgeWorkspaceVO workspace = new KnowledgeWorkspaceVO(); + workspace.setStoreId(store.getId()); + workspace.setStoreCode(store.getStoreCode()); + workspace.setStoreName(store.getStoreName()); + workspace.setDescription(store.getDescription()); + workspace.setStatus(store.getStatus()); + workspace.setRemark(store.getRemark()); + workspace.setDocumentCount(documents.size()); + workspace.setParsedDocumentCount((int) documents.stream().filter(this::isParsed).count()); + workspace.setParseFailedDocumentCount((int) documents.stream().filter(this::isParseFailed).count()); + workspace.setIndexedDocumentCount((int) documents.stream().filter(this::isIndexed).count()); + workspace.setPendingIndexDocumentCount((int) documents.stream().filter(this::isPendingIndex).count()); + workspace.setHealthScore(calculateHealthScore(documents.size(), workspace.getParseFailedDocumentCount())); + workspace.setChunkCount(chunkCount); + workspace.setEmbeddingCount(embeddingCount); + workspace.setPendingTaskCount(workspace.getPendingIndexDocumentCount()); + workspace.setPublishImpact("更新后需要重新验证 Workflow / Agent 的知识引用效果"); + workspace.setDocuments(documents.stream().map(this::toWorkspaceDocument).toList()); + + if (modelConfig != null) { + workspace.setEmbeddingModelId(modelConfig.getEmbeddingModelId()); + workspace.setEmbeddingDimension(modelConfig.getEmbeddingDimension()); + workspace.setChunkStrategy(modelConfig.getChunkStrategy()); + workspace.setChunkSize(modelConfig.getChunkSize()); + workspace.setChunkOverlap(modelConfig.getChunkOverlap()); + workspace.setIndexVersion(modelConfig.getIndexVersion()); + } + + log.info("知识工作台查询结束,storeId={}, documentCount={}, healthScore={}, pendingTaskCount={}", + storeId, workspace.getDocumentCount(), workspace.getHealthScore(), workspace.getPendingTaskCount()); + return workspace; + } + + private KnowledgeWorkspaceDocumentVO toWorkspaceDocument(RagDocumentResponse document) { + KnowledgeWorkspaceDocumentVO vo = new KnowledgeWorkspaceDocumentVO(); + vo.setDocumentId(document.getId()); + vo.setDocumentTitle(document.getDocumentTitle()); + vo.setParseStatus(document.getParseStatus()); + vo.setIndexStatus(document.getIndexStatus()); + vo.setEnabled(document.getEnabled()); + vo.setUpdateTime(document.getUpdateTime()); + return vo; + } + + private boolean isParsed(RagDocumentResponse document) { + return PARSE_STATUS_PARSED.equals(document.getParseStatus()); + } + + private boolean isParseFailed(RagDocumentResponse document) { + return PARSE_STATUS_FAILED.equals(document.getParseStatus()); + } + + private boolean isIndexed(RagDocumentResponse document) { + return INDEX_STATUS_INDEXED.equals(document.getIndexStatus()); + } + + private boolean isPendingIndex(RagDocumentResponse document) { + return INDEX_STATUS_PENDING.equals(document.getIndexStatus()); + } + + /** + * 健康度按“非失败文档占比”计算,便于前端快速识别当前知识库是否适合继续发布引用。 + */ + private int calculateHealthScore(int totalDocumentCount, int parseFailedDocumentCount) { + if (totalDocumentCount <= 0) { + return 100; + } + int healthyDocumentCount = Math.max(totalDocumentCount - parseFailedDocumentCount, 0); + return (int) Math.floor(healthyDocumentCount * 100.0 / totalDocumentCount); + } +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/service/impl/RagDocumentServiceImpl.java b/common-agent-rag/src/main/java/com/bruce/rag/service/impl/RagDocumentServiceImpl.java index 247b07c..d1cd682 100644 --- a/common-agent-rag/src/main/java/com/bruce/rag/service/impl/RagDocumentServiceImpl.java +++ b/common-agent-rag/src/main/java/com/bruce/rag/service/impl/RagDocumentServiceImpl.java @@ -12,11 +12,12 @@ import com.bruce.rag.dto.response.RagDocumentResponse; import com.bruce.rag.entity.RagDocument; import com.bruce.rag.enums.RagIndexStatusEnum; import com.bruce.rag.enums.RagParseStatusEnum; +import com.bruce.rag.factory.RagDocumentFactory; import com.bruce.rag.mapper.RagDocumentMapper; import com.bruce.rag.service.IRagDocumentAutoParseService; import com.bruce.rag.service.IRagDocumentService; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -25,18 +26,17 @@ import java.util.List; @Slf4j @Service +@RequiredArgsConstructor public class RagDocumentServiceImpl extends ServiceImpl implements IRagDocumentService { - @Autowired - private ISysAttachmentService sysAttachmentService; - - @Autowired - private IRagDocumentAutoParseService ragDocumentAutoParseService; + private final ISysAttachmentService sysAttachmentService; + private final IRagDocumentAutoParseService ragDocumentAutoParseService; + private final RagDocumentFactory ragDocumentFactory; @Override public List listResponses() { log.info("RagDocumentServiceImpl.listResponses start"); - List responses = toResponses(list()); + List responses = ragDocumentFactory.toResponses(list()); log.info("RagDocumentServiceImpl.listResponses success, count={}", responses.size()); return responses; } @@ -47,7 +47,7 @@ public class RagDocumentServiceImpl extends ServiceImpl responses = toResponses(lambdaQuery() + List responses = ragDocumentFactory.toResponses(lambdaQuery() .eq(queryRequest.getStoreId() != null, RagDocument::getStoreId, queryRequest.getStoreId()) .eq(queryRequest.getAttachmentId() != null, RagDocument::getAttachmentId, queryRequest.getAttachmentId()) .eq(parseStatus != null, RagDocument::getParseStatus, parseStatus) @@ -62,7 +62,7 @@ public class RagDocumentServiceImpl extends ServiceImpl toResponses(List documents) { - return documents.stream() - .map(RagDocumentResponse::fromEntity) - .toList(); + /** + * 更新场景下需要保留旧值,因此这里仅覆盖请求中明确传入的字段。 + */ + private void applyRequestToDocument(RagDocument document, RagDocumentSaveRequest request) { + if (request.getStoreId() != null) { + document.setStoreId(request.getStoreId()); + } + if (request.getAttachmentId() != null) { + document.setAttachmentId(request.getAttachmentId()); + } + if (StringUtils.hasText(request.getDocumentTitle())) { + document.setDocumentTitle(trimToNull(request.getDocumentTitle())); + } + if (request.getDocumentSummary() != null) { + document.setDocumentSummary(trimToNull(request.getDocumentSummary())); + } + if (StringUtils.hasText(request.getParseStatus())) { + document.setParseStatus(trimToNull(request.getParseStatus())); + } + if (StringUtils.hasText(request.getIndexStatus())) { + document.setIndexStatus(trimToNull(request.getIndexStatus())); + } + if (request.getEnabled() != null) { + document.setEnabled(request.getEnabled()); + } + if (request.getErrorMessage() != null) { + document.setErrorMessage(trimToNull(request.getErrorMessage())); + } + if (request.getRemark() != null) { + document.setRemark(trimToNull(request.getRemark())); + } } private String resolveSourceType(String sourceType) { diff --git a/common-agent-rag/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java b/common-agent-rag/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java index ef51381..9d8e3e2 100644 --- a/common-agent-rag/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java +++ b/common-agent-rag/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java @@ -12,11 +12,12 @@ import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.entity.RagStore; import com.bruce.rag.enums.RagIndexStatusEnum; import com.bruce.rag.enums.RagParseStatusEnum; +import com.bruce.rag.factory.RagStoreFactory; import com.bruce.rag.mapper.RagStoreMapper; import com.bruce.rag.service.IRagDocumentService; import com.bruce.rag.service.IRagStoreService; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -26,15 +27,16 @@ import java.util.Objects; @Slf4j @Service +@RequiredArgsConstructor public class RagStoreServiceImpl extends ServiceImpl implements IRagStoreService { - @Autowired - private IRagDocumentService ragDocumentService; + private final IRagDocumentService ragDocumentService; + private final RagStoreFactory ragStoreFactory; @Override public List listResponses() { log.info("RagStoreServiceImpl.listResponses start"); - List responses = toResponses(list()); + List responses = ragStoreFactory.toResponses(list()); log.info("RagStoreServiceImpl.listResponses success, count={}", responses.size()); return responses; } @@ -43,7 +45,7 @@ public class RagStoreServiceImpl extends ServiceImpl i public List query(RagStoreQueryRequest request) { log.info("RagStoreServiceImpl.query start, request={}", request); RagStoreQueryRequest queryRequest = request == null ? new RagStoreQueryRequest() : request; - List responses = toResponses(lambdaQuery() + List responses = ragStoreFactory.toResponses(lambdaQuery() .eq(StringUtils.hasText(queryRequest.getStoreCode()), RagStore::getStoreCode, queryRequest.getStoreCode()) .like(StringUtils.hasText(queryRequest.getStoreName()), RagStore::getStoreName, queryRequest.getStoreName()) .eq(StringUtils.hasText(queryRequest.getStatus()), RagStore::getStatus, queryRequest.getStatus()) @@ -56,7 +58,7 @@ public class RagStoreServiceImpl extends ServiceImpl i @Override public RagStoreResponse getResponseById(Long id) { log.info("RagStoreServiceImpl.getResponseById start, id={}", id); - RagStoreResponse response = RagStoreResponse.fromEntity(getById(id)); + RagStoreResponse response = ragStoreFactory.toResponse(getById(id)); log.info("RagStoreServiceImpl.getResponseById success, id={}, found={}", id, response != null); return response; } @@ -132,7 +134,10 @@ public class RagStoreServiceImpl extends ServiceImpl i throw new IllegalArgumentException("知识库编码已存在: " + request.getStoreCode().trim()); } - RagStore ragStore = buildEntity(request); + RagStore ragStore = ragStoreFactory.toEntity(request); + if (!StringUtils.hasText(ragStore.getStatus())) { + ragStore.setStatus(EnableStatusEnum.ENABLED.getLabel()); + } boolean result = super.saveOrUpdate(ragStore); log.info("RagStoreServiceImpl.saveOrUpdate success, requestId={}, savedId={}, storeCode={}, result={}", request.getId(), ragStore.getId(), ragStore.getStoreCode(), result); @@ -154,29 +159,4 @@ public class RagStoreServiceImpl extends ServiceImpl i request.getId(), request.getStoreCode(), request.getStoreName()); } - public RagStore buildEntity(RagStoreSaveRequest request) { - RagStore ragStore = new RagStore(); - ragStore.setId(request.getId()); - ragStore.setStoreCode(request.getStoreCode().trim()); - ragStore.setStoreName(request.getStoreName().trim()); - ragStore.setDescription(trimToNull(request.getDescription())); - ragStore.setStatus(StringUtils.hasText(request.getStatus()) - ? request.getStatus().trim() - : EnableStatusEnum.ENABLED.getLabel()); - ragStore.setRemark(trimToNull(request.getRemark())); - return ragStore; - } - - private List toResponses(List stores) { - return stores.stream() - .map(RagStoreResponse::fromEntity) - .toList(); - } - - private String trimToNull(String value) { - if (!StringUtils.hasText(value)) { - return null; - } - return value.trim(); - } } diff --git a/common-agent-rag/src/main/java/com/bruce/rag/vo/KnowledgeWorkspaceDocumentVO.java b/common-agent-rag/src/main/java/com/bruce/rag/vo/KnowledgeWorkspaceDocumentVO.java new file mode 100644 index 0000000..31024f9 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/vo/KnowledgeWorkspaceDocumentVO.java @@ -0,0 +1,38 @@ +package com.bruce.rag.vo; + +import com.bruce.common.constant.CommonConsts; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 知识工作台中的文档摘要视图。 + */ +@Data +@Schema(description = "知识工作台文档摘要") +public class KnowledgeWorkspaceDocumentVO { + + @Schema(description = "文档ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long documentId; + + @Schema(description = "文档标题") + private String documentTitle; + + @Schema(description = "解析状态") + private String parseStatus; + + @Schema(description = "索引状态") + private String indexStatus; + + @Schema(description = "是否启用") + private Boolean enabled; + + @Schema(description = "最近更新时间") + @JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8) + private Date updateTime; +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/vo/KnowledgeWorkspaceVO.java b/common-agent-rag/src/main/java/com/bruce/rag/vo/KnowledgeWorkspaceVO.java new file mode 100644 index 0000000..78eeefc --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/vo/KnowledgeWorkspaceVO.java @@ -0,0 +1,88 @@ +package com.bruce.rag.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 知识工作台聚合视图,供前端知识资产工作台直接消费。 + */ +@Data +@Schema(description = "知识工作台聚合视图") +public class KnowledgeWorkspaceVO { + + @Schema(description = "知识库ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long storeId; + + @Schema(description = "知识库编码") + private String storeCode; + + @Schema(description = "知识库名称") + private String storeName; + + @Schema(description = "知识库描述") + private String description; + + @Schema(description = "知识库状态") + private String status; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "文档总数") + private Integer documentCount; + + @Schema(description = "已解析文档数") + private Integer parsedDocumentCount; + + @Schema(description = "解析失败文档数") + private Integer parseFailedDocumentCount; + + @Schema(description = "已索引文档数") + private Integer indexedDocumentCount; + + @Schema(description = "待索引文档数") + private Integer pendingIndexDocumentCount; + + @Schema(description = "健康度百分比") + private Integer healthScore; + + @Schema(description = "Embedding 模型ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long embeddingModelId; + + @Schema(description = "Embedding 维度") + private Integer embeddingDimension; + + @Schema(description = "切片策略") + private Integer chunkStrategy; + + @Schema(description = "切片长度") + private Integer chunkSize; + + @Schema(description = "切片重叠长度") + private Integer chunkOverlap; + + @Schema(description = "索引版本") + private Integer indexVersion; + + @Schema(description = "切片总数") + private Long chunkCount; + + @Schema(description = "向量总数") + private Long embeddingCount; + + @Schema(description = "待处理任务数") + private Integer pendingTaskCount; + + @Schema(description = "发布影响提示") + private String publishImpact; + + @Schema(description = "文档摘要列表") + private List documents = new ArrayList<>(); +} diff --git a/common-agent-rag/src/test/java/com/bruce/rag/RagDocumentServiceImplTests.java b/common-agent-rag/src/test/java/com/bruce/rag/RagDocumentServiceImplTests.java index 7099175..819a3fd 100644 --- a/common-agent-rag/src/test/java/com/bruce/rag/RagDocumentServiceImplTests.java +++ b/common-agent-rag/src/test/java/com/bruce/rag/RagDocumentServiceImplTests.java @@ -9,6 +9,7 @@ import com.bruce.rag.dto.request.RagDocumentSaveRequest; import com.bruce.rag.entity.RagDocument; import com.bruce.rag.enums.RagIndexStatusEnum; import com.bruce.rag.enums.RagParseStatusEnum; +import com.bruce.rag.factory.RagDocumentFactory; import com.bruce.rag.service.IRagDocumentAutoParseService; import com.bruce.rag.service.impl.RagDocumentServiceImpl; import org.junit.jupiter.api.Test; @@ -42,6 +43,9 @@ class RagDocumentServiceImplTests { @Mock private IRagDocumentAutoParseService ragDocumentAutoParseService; + @Spy + private RagDocumentFactory ragDocumentFactory; + @Test void batchUploadShouldUseRagSourceTypeAndStoreIdAsSourceId() { MockMultipartFile file = new MockMultipartFile( diff --git a/common-agent-rag/src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java b/common-agent-rag/src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java index 7964b7d..862e105 100644 --- a/common-agent-rag/src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java +++ b/common-agent-rag/src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java @@ -3,8 +3,11 @@ package com.bruce.rag; import com.bruce.common.enums.EnableStatusEnum; import com.bruce.rag.dto.request.RagStoreSaveRequest; import com.bruce.rag.entity.RagStore; +import com.bruce.rag.factory.RagStoreFactory; +import com.bruce.rag.service.IRagDocumentService; import com.bruce.rag.service.impl.RagStoreServiceImpl; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -12,9 +15,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows; class RagStoreSaveValidationTests { + private final IRagDocumentService ragDocumentService = Mockito.mock(IRagDocumentService.class); + private final RagStoreFactory ragStoreFactory = new RagStoreFactory(); + @Test void saveShouldRejectBlankStoreCode() { - RagStoreServiceImpl service = new RagStoreServiceImpl(); + RagStoreServiceImpl service = new RagStoreServiceImpl(ragDocumentService, ragStoreFactory); RagStoreSaveRequest request = new RagStoreSaveRequest(); request.setStoreName("产品制度库"); @@ -23,7 +29,7 @@ class RagStoreSaveValidationTests { @Test void saveShouldRejectBlankStoreName() { - RagStoreServiceImpl service = new RagStoreServiceImpl(); + RagStoreServiceImpl service = new RagStoreServiceImpl(ragDocumentService, ragStoreFactory); RagStoreSaveRequest request = new RagStoreSaveRequest(); request.setStoreCode("PROD_DOC"); @@ -32,7 +38,7 @@ class RagStoreSaveValidationTests { @Test void saveShouldAcceptMinimalValidRequest() { - RagStoreServiceImpl service = new RagStoreServiceImpl(); + RagStoreServiceImpl service = new RagStoreServiceImpl(ragDocumentService, ragStoreFactory); RagStoreSaveRequest request = new RagStoreSaveRequest(); request.setStoreCode("PROD_DOC"); request.setStoreName("产品制度库"); @@ -42,12 +48,14 @@ class RagStoreSaveValidationTests { @Test void saveShouldDefaultStatusToEnabledEnumLabel() { - RagStoreServiceImpl service = new RagStoreServiceImpl(); RagStoreSaveRequest request = new RagStoreSaveRequest(); request.setStoreCode("PROD_DOC"); request.setStoreName("产品制度库"); - RagStore ragStore = service.buildEntity(request); + RagStore ragStore = ragStoreFactory.toEntity(request); + if (ragStore.getStatus() == null) { + ragStore.setStatus(EnableStatusEnum.ENABLED.getLabel()); + } assertEquals(EnableStatusEnum.ENABLED.getLabel(), ragStore.getStatus()); } diff --git a/common-agent-rag/src/test/java/com/bruce/rag/factory/RagFactoryTests.java b/common-agent-rag/src/test/java/com/bruce/rag/factory/RagFactoryTests.java new file mode 100644 index 0000000..a240d8f --- /dev/null +++ b/common-agent-rag/src/test/java/com/bruce/rag/factory/RagFactoryTests.java @@ -0,0 +1,129 @@ +package com.bruce.rag.factory; + +import com.bruce.rag.dto.request.RagDocumentSaveRequest; +import com.bruce.rag.dto.request.RagStoreSaveRequest; +import com.bruce.rag.dto.response.RagDocumentResponse; +import com.bruce.rag.dto.response.RagStoreResponse; +import com.bruce.rag.entity.RagDocument; +import com.bruce.rag.entity.RagStore; +import org.junit.jupiter.api.Test; + +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class RagFactoryTests { + + private final RagStoreFactory ragStoreFactory = new RagStoreFactory(); + private final RagDocumentFactory ragDocumentFactory = new RagDocumentFactory(); + + @Test + void ragStoreFactoryShouldTrimAndBuildEntity() { + RagStoreSaveRequest request = new RagStoreSaveRequest(); + request.setId(1001L); + request.setStoreCode(" PROD_DOC "); + request.setStoreName(" 产品制度库 "); + request.setDescription(" 产品制度、业务规范、流程材料 "); + request.setStatus(" 启用 "); + request.setRemark(" 核心制度库 "); + + RagStore entity = ragStoreFactory.toEntity(request); + + assertNotNull(entity); + assertEquals(1001L, entity.getId()); + assertEquals("PROD_DOC", entity.getStoreCode()); + assertEquals("产品制度库", entity.getStoreName()); + assertEquals("产品制度、业务规范、流程材料", entity.getDescription()); + assertEquals("启用", entity.getStatus()); + assertEquals("核心制度库", entity.getRemark()); + } + + @Test + void ragStoreFactoryShouldConvertEntityToResponse() { + RagStore entity = new RagStore(); + entity.setId(1001L); + entity.setStoreCode("PROD_DOC"); + entity.setStoreName("产品制度库"); + entity.setDescription("产品制度、业务规范、流程材料"); + entity.setStatus("启用"); + entity.setRemark("核心制度库"); + entity.setCreateTime(new Date(1747816496000L)); + entity.setUpdateTime(new Date(1747820096000L)); + + RagStoreResponse response = ragStoreFactory.toResponse(entity); + + assertNotNull(response); + assertEquals(1001L, response.getId()); + assertEquals("PROD_DOC", response.getStoreCode()); + assertEquals("产品制度库", response.getStoreName()); + assertEquals("产品制度、业务规范、流程材料", response.getDescription()); + assertEquals("启用", response.getStatus()); + assertEquals("核心制度库", response.getRemark()); + assertEquals(new Date(1747816496000L), response.getCreateTime()); + assertEquals(new Date(1747820096000L), response.getUpdateTime()); + } + + @Test + void ragDocumentFactoryShouldApplyEditableFieldsToEntity() { + RagDocumentSaveRequest request = new RagDocumentSaveRequest(); + request.setId(3003L); + request.setStoreId(1001L); + request.setAttachmentId(2002L); + request.setDocumentTitle(" 新标题 "); + request.setDocumentSummary(" 新摘要 "); + request.setParseStatus(" PARSED "); + request.setIndexStatus(" INDEXED "); + request.setEnabled(false); + request.setErrorMessage(" 已修复 "); + request.setRemark(" 备注信息 "); + + RagDocument entity = ragDocumentFactory.toEntity(request); + + assertNotNull(entity); + assertEquals(3003L, entity.getId()); + assertEquals(1001L, entity.getStoreId()); + assertEquals(2002L, entity.getAttachmentId()); + assertEquals("新标题", entity.getDocumentTitle()); + assertEquals("新摘要", entity.getDocumentSummary()); + assertEquals("PARSED", entity.getParseStatus()); + assertEquals("INDEXED", entity.getIndexStatus()); + assertEquals(false, entity.getEnabled()); + assertEquals("已修复", entity.getErrorMessage()); + assertEquals("备注信息", entity.getRemark()); + } + + @Test + void ragDocumentFactoryShouldConvertEntityToResponse() { + RagDocument entity = new RagDocument(); + entity.setId(3003L); + entity.setStoreId(1001L); + entity.setAttachmentId(2002L); + entity.setDocumentTitle("people_profiles.txt"); + entity.setDocumentSummary("测试人员信息"); + entity.setParseStatus("PARSED"); + entity.setIndexStatus("INDEXED"); + entity.setEnabled(true); + entity.setErrorMessage(null); + entity.setRemark("测试文档"); + entity.setCreateTime(new Date(1747816496000L)); + entity.setUpdateTime(new Date(1747820096000L)); + + RagDocumentResponse response = ragDocumentFactory.toResponse(entity); + + assertNotNull(response); + assertEquals(3003L, response.getId()); + assertEquals(1001L, response.getStoreId()); + assertEquals(2002L, response.getAttachmentId()); + assertEquals("people_profiles.txt", response.getDocumentTitle()); + assertEquals("测试人员信息", response.getDocumentSummary()); + assertEquals("PARSED", response.getParseStatus()); + assertEquals("INDEXED", response.getIndexStatus()); + assertEquals(true, response.getEnabled()); + assertNull(response.getErrorMessage()); + assertEquals("测试文档", response.getRemark()); + assertEquals(new Date(1747816496000L), response.getCreateTime()); + assertEquals(new Date(1747820096000L), response.getUpdateTime()); + } +} diff --git a/common-agent-rag/src/test/java/com/bruce/rag/workspace/KnowledgeWorkspaceServiceTests.java b/common-agent-rag/src/test/java/com/bruce/rag/workspace/KnowledgeWorkspaceServiceTests.java new file mode 100644 index 0000000..07c12bf --- /dev/null +++ b/common-agent-rag/src/test/java/com/bruce/rag/workspace/KnowledgeWorkspaceServiceTests.java @@ -0,0 +1,120 @@ +package com.bruce.rag.workspace; + +import com.bruce.modelprovider.dto.response.RagStoreModelConfigResponse; +import com.bruce.modelprovider.service.IRagStoreModelConfigService; +import com.bruce.rag.dto.response.RagDocumentResponse; +import com.bruce.rag.dto.response.RagStoreResponse; +import com.bruce.rag.service.IRagChunkEmbeddingService; +import com.bruce.rag.service.IRagChunkService; +import com.bruce.rag.service.IRagDocumentParseResultService; +import com.bruce.rag.service.IRagDocumentService; +import com.bruce.rag.service.impl.KnowledgeWorkspaceServiceImpl; +import com.bruce.rag.vo.KnowledgeWorkspaceVO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class KnowledgeWorkspaceServiceTests { + + @Mock + private com.bruce.rag.service.IRagStoreService ragStoreService; + + @Mock + private IRagDocumentService ragDocumentService; + + @Mock + private IRagDocumentParseResultService ragDocumentParseResultService; + + @Mock + private IRagChunkService ragChunkService; + + @Mock + private IRagChunkEmbeddingService ragChunkEmbeddingService; + + @Mock + private IRagStoreModelConfigService ragStoreModelConfigService; + + @InjectMocks + private KnowledgeWorkspaceServiceImpl knowledgeWorkspaceService; + + @Test + void getWorkspaceShouldAggregateStoreConfigAndDocumentHealth() { + RagStoreResponse store = new RagStoreResponse(); + store.setId(1001L); + store.setStoreCode("PROD_DOC"); + store.setStoreName("产品制度库"); + store.setDescription("产品制度、业务规范、流程材料"); + store.setStatus("启用"); + store.setRemark("核心制度库"); + + RagDocumentResponse parsedIndexedDocument = createDocument(11L, true, "PARSED", "INDEXED", new Date(1747816496000L)); + RagDocumentResponse parseFailedDocument = createDocument(12L, true, "FAILED", "FAILED", new Date(1747820096000L)); + RagDocumentResponse pendingIndexDocument = createDocument(13L, true, "PARSED", "PENDING", new Date(1747823696000L)); + + RagStoreModelConfigResponse modelConfig = new RagStoreModelConfigResponse(); + modelConfig.setStoreId(1001L); + modelConfig.setEmbeddingModelId(88L); + modelConfig.setEmbeddingDimension(1024); + modelConfig.setChunkStrategy(1); + modelConfig.setChunkSize(800); + modelConfig.setChunkOverlap(120); + modelConfig.setIndexVersion(14); + + when(ragStoreService.getResponseById(1001L)).thenReturn(store); + when(ragDocumentService.query(org.mockito.ArgumentMatchers.any())).thenReturn(List.of( + parsedIndexedDocument, + parseFailedDocument, + pendingIndexDocument + )); + when(ragStoreModelConfigService.getByStoreId(1001L)).thenReturn(modelConfig); + when(ragChunkService.count(org.mockito.ArgumentMatchers.any())).thenReturn(24L); + when(ragChunkEmbeddingService.count(org.mockito.ArgumentMatchers.any())).thenReturn(18L); + + KnowledgeWorkspaceVO workspace = knowledgeWorkspaceService.getWorkspace(1001L); + + assertNotNull(workspace); + assertEquals(1001L, workspace.getStoreId()); + assertEquals("PROD_DOC", workspace.getStoreCode()); + assertEquals("产品制度库", workspace.getStoreName()); + assertEquals("启用", workspace.getStatus()); + assertEquals(3, workspace.getDocumentCount()); + assertEquals(2, workspace.getParsedDocumentCount()); + assertEquals(1, workspace.getParseFailedDocumentCount()); + assertEquals(1, workspace.getIndexedDocumentCount()); + assertEquals(1, workspace.getPendingIndexDocumentCount()); + assertEquals(66, workspace.getHealthScore()); + assertEquals(88L, workspace.getEmbeddingModelId()); + assertEquals(1024, workspace.getEmbeddingDimension()); + assertEquals(1, workspace.getChunkStrategy()); + assertEquals(800, workspace.getChunkSize()); + assertEquals(120, workspace.getChunkOverlap()); + assertEquals(14, workspace.getIndexVersion()); + assertEquals(24L, workspace.getChunkCount()); + assertEquals(18L, workspace.getEmbeddingCount()); + assertEquals(1, workspace.getPendingTaskCount()); + assertEquals("更新后需要重新验证 Workflow / Agent 的知识引用效果", workspace.getPublishImpact()); + assertEquals(3, workspace.getDocuments().size()); + } + + private RagDocumentResponse createDocument(Long id, boolean enabled, String parseStatus, String indexStatus, Date updateTime) { + RagDocumentResponse response = new RagDocumentResponse(); + response.setId(id); + response.setStoreId(1001L); + response.setEnabled(enabled); + response.setParseStatus(parseStatus); + response.setIndexStatus(indexStatus); + response.setDocumentTitle("文档-" + id); + response.setUpdateTime(updateTime); + return response; + } +} diff --git a/frontend/src/api/__tests__/knowledgeWorkspace.spec.ts b/frontend/src/api/__tests__/knowledgeWorkspace.spec.ts new file mode 100644 index 0000000..f8e9ba2 --- /dev/null +++ b/frontend/src/api/__tests__/knowledgeWorkspace.spec.ts @@ -0,0 +1,20 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { getKnowledgeWorkspace } from '../knowledgeWorkspace'; +import { get } from '../request'; + +vi.mock('../request', () => ({ + get: vi.fn(), +})); + +describe('knowledge workspace api', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('loads workspace aggregate by store id', () => { + getKnowledgeWorkspace('1001'); + + expect(get).toHaveBeenCalledWith('/knowledge/workspaces/1001'); + }); +}); diff --git a/frontend/src/api/knowledgeWorkspace.ts b/frontend/src/api/knowledgeWorkspace.ts new file mode 100644 index 0000000..73c85ed --- /dev/null +++ b/frontend/src/api/knowledgeWorkspace.ts @@ -0,0 +1,40 @@ +import { get } from './request'; + +export interface KnowledgeWorkspaceDocument { + documentId: string; + documentTitle?: string | null; + parseStatus?: string | null; + indexStatus?: string | null; + enabled?: boolean | null; + updateTime?: string | null; +} + +export interface KnowledgeWorkspace { + storeId: string; + storeCode?: string | null; + storeName?: string | null; + description?: string | null; + status?: string | null; + remark?: string | null; + documentCount: number; + parsedDocumentCount: number; + parseFailedDocumentCount: number; + indexedDocumentCount: number; + pendingIndexDocumentCount: number; + healthScore: number; + embeddingModelId?: string | null; + embeddingDimension?: number | null; + chunkStrategy?: number | null; + chunkSize?: number | null; + chunkOverlap?: number | null; + indexVersion?: number | null; + chunkCount?: number | null; + embeddingCount?: number | null; + pendingTaskCount?: number | null; + publishImpact?: string | null; + documents: KnowledgeWorkspaceDocument[]; +} + +export function getKnowledgeWorkspace(storeId: string) { + return get(`/knowledge/workspaces/${storeId}`); +}