From ebe0fc5a122348eee7239adf35b2ecb506dc4e68 Mon Sep 17 00:00:00 2001 From: bruce Date: Mon, 1 Jun 2026 05:28:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(studio):=20=E8=A1=A5=E9=BD=90=E5=89=A9?= =?UTF-8?q?=E4=BD=99=E5=B7=A5=E4=BD=9C=E5=8F=B0=E8=81=9A=E5=90=88=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=B8=8E=E7=9C=9F=E5=AE=9E=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/AgentSessionServiceImpl.java | 7 +- .../session/AgentSessionServiceTests.java | 6 + .../controller/StudioDashboardController.java | 31 ++ .../service/IStudioDashboardService.java | 14 + .../impl/StudioDashboardServiceImpl.java | 246 +++++++++++++++ .../vo/StudioDashboardChecklistItemVO.java | 12 + .../vo/StudioDashboardLifecycleStepVO.java | 13 + .../vo/StudioDashboardMetricsVO.java | 14 + .../vo/StudioDashboardRecentRunVO.java | 16 + .../bruce/dashboard/vo/StudioDashboardVO.java | 22 ++ .../StudioDashboardServiceTests.java | 130 ++++++++ .../controller/IngestionRunController.java | 40 +++ .../rag/service/IIngestionRunService.java | 14 + .../service/impl/IngestionRunServiceImpl.java | 281 ++++++++++++++++++ .../com/bruce/rag/vo/IngestionRunFileVO.java | 34 +++ .../com/bruce/rag/vo/IngestionRunLogVO.java | 21 ++ .../com/bruce/rag/vo/IngestionRunStepVO.java | 21 ++ .../java/com/bruce/rag/vo/IngestionRunVO.java | 65 ++++ .../ingestion/IngestionRunServiceTests.java | 129 ++++++++ frontend/src/api/__tests__/ingestion.spec.ts | 31 ++ .../src/api/__tests__/studioDashboard.spec.ts | 26 ++ frontend/src/api/ingestion.ts | 49 +++ frontend/src/api/studioDashboard.ts | 44 +++ .../src/pages/studio/AgentWorkspacePage.vue | 114 +++++-- .../pages/studio/IngestionPipelinePage.vue | 84 ++++-- .../pages/studio/KnowledgeWorkspacePage.vue | 76 +++-- .../src/pages/studio/ModelWorkspacePage.vue | 47 ++- .../src/pages/studio/StudioDashboardPage.vue | 75 ++++- .../src/pages/studio/WorkflowBuilderPage.vue | 121 +++++--- .../__tests__/AgentWorkspacePage.spec.ts | 94 ++++++ .../__tests__/IngestionPipelinePage.spec.ts | 66 ++++ .../__tests__/KnowledgeWorkspacePage.spec.ts | 64 ++++ .../__tests__/ModelWorkspacePage.spec.ts | 58 ++++ .../__tests__/StudioDashboardPage.spec.ts | 61 ++++ .../__tests__/WorkflowBuilderPage.spec.ts | 89 ++++++ 35 files changed, 2092 insertions(+), 123 deletions(-) create mode 100644 common-agent-boot/src/main/java/com/bruce/dashboard/controller/StudioDashboardController.java create mode 100644 common-agent-boot/src/main/java/com/bruce/dashboard/service/IStudioDashboardService.java create mode 100644 common-agent-boot/src/main/java/com/bruce/dashboard/service/impl/StudioDashboardServiceImpl.java create mode 100644 common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardChecklistItemVO.java create mode 100644 common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardLifecycleStepVO.java create mode 100644 common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardMetricsVO.java create mode 100644 common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardRecentRunVO.java create mode 100644 common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardVO.java create mode 100644 common-agent-boot/src/test/java/com/bruce/dashboard/StudioDashboardServiceTests.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/controller/IngestionRunController.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/service/IIngestionRunService.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/service/impl/IngestionRunServiceImpl.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunFileVO.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunLogVO.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunStepVO.java create mode 100644 common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunVO.java create mode 100644 common-agent-rag/src/test/java/com/bruce/rag/ingestion/IngestionRunServiceTests.java create mode 100644 frontend/src/api/__tests__/ingestion.spec.ts create mode 100644 frontend/src/api/__tests__/studioDashboard.spec.ts create mode 100644 frontend/src/api/ingestion.ts create mode 100644 frontend/src/api/studioDashboard.ts create mode 100644 frontend/src/pages/studio/__tests__/AgentWorkspacePage.spec.ts create mode 100644 frontend/src/pages/studio/__tests__/IngestionPipelinePage.spec.ts create mode 100644 frontend/src/pages/studio/__tests__/KnowledgeWorkspacePage.spec.ts create mode 100644 frontend/src/pages/studio/__tests__/ModelWorkspacePage.spec.ts create mode 100644 frontend/src/pages/studio/__tests__/StudioDashboardPage.spec.ts create mode 100644 frontend/src/pages/studio/__tests__/WorkflowBuilderPage.spec.ts diff --git a/common-agent-agent/src/main/java/com/bruce/agent/service/impl/AgentSessionServiceImpl.java b/common-agent-agent/src/main/java/com/bruce/agent/service/impl/AgentSessionServiceImpl.java index 0c1b826..63278b8 100644 --- a/common-agent-agent/src/main/java/com/bruce/agent/service/impl/AgentSessionServiceImpl.java +++ b/common-agent-agent/src/main/java/com/bruce/agent/service/impl/AgentSessionServiceImpl.java @@ -12,6 +12,7 @@ import com.bruce.agent.vo.AgentSessionDetailVO; import com.bruce.common.enums.EnableStatusEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -25,7 +26,7 @@ public class AgentSessionServiceImpl extends ServiceImpl agentDefinitionServiceProvider; private final AgentSessionFactory agentSessionFactory; @Override @@ -40,6 +41,10 @@ public class AgentSessionServiceImpl extends ServiceImpl agentDefinitionServiceProvider; + @Spy @InjectMocks private AgentSessionServiceImpl agentSessionService; @@ -42,6 +46,7 @@ class AgentSessionServiceTests { @Test void createSessionShouldRejectDisabledAgent() { + when(agentDefinitionServiceProvider.getIfAvailable()).thenReturn(agentDefinitionService); AgentDefinition agent = new AgentDefinition(); agent.setId(1L); agent.setStatus("DISABLED"); @@ -56,6 +61,7 @@ class AgentSessionServiceTests { @Test void createSessionShouldPersistActiveSession() { + when(agentDefinitionServiceProvider.getIfAvailable()).thenReturn(agentDefinitionService); AgentDefinition agent = new AgentDefinition(); agent.setId(1L); agent.setStatus(EnableStatusEnum.ENABLED.name()); diff --git a/common-agent-boot/src/main/java/com/bruce/dashboard/controller/StudioDashboardController.java b/common-agent-boot/src/main/java/com/bruce/dashboard/controller/StudioDashboardController.java new file mode 100644 index 0000000..757fb25 --- /dev/null +++ b/common-agent-boot/src/main/java/com/bruce/dashboard/controller/StudioDashboardController.java @@ -0,0 +1,31 @@ +package com.bruce.dashboard.controller; + +import com.bruce.common.domain.model.RequestResult; +import com.bruce.dashboard.service.IStudioDashboardService; +import com.bruce.dashboard.vo.StudioDashboardVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Studio 首页聚合接口。 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/studio/dashboard") +public class StudioDashboardController { + + private final IStudioDashboardService studioDashboardService; + + @GetMapping + public RequestResult detail() { + log.info("Studio 首页总览查询开始"); + StudioDashboardVO dashboard = studioDashboardService.getDashboard(); + log.info("Studio 首页总览查询结束,projectName={}, recentRunCount={}", + dashboard.getProjectName(), dashboard.getRecentRuns().size()); + return RequestResult.success(dashboard); + } +} diff --git a/common-agent-boot/src/main/java/com/bruce/dashboard/service/IStudioDashboardService.java b/common-agent-boot/src/main/java/com/bruce/dashboard/service/IStudioDashboardService.java new file mode 100644 index 0000000..650b4a3 --- /dev/null +++ b/common-agent-boot/src/main/java/com/bruce/dashboard/service/IStudioDashboardService.java @@ -0,0 +1,14 @@ +package com.bruce.dashboard.service; + +import com.bruce.dashboard.vo.StudioDashboardVO; + +/** + * Studio 总览工作台聚合服务。 + */ +public interface IStudioDashboardService { + + /** + * 汇总当前项目的发布旅程、运行摘要和风险提示。 + */ + StudioDashboardVO getDashboard(); +} diff --git a/common-agent-boot/src/main/java/com/bruce/dashboard/service/impl/StudioDashboardServiceImpl.java b/common-agent-boot/src/main/java/com/bruce/dashboard/service/impl/StudioDashboardServiceImpl.java new file mode 100644 index 0000000..df70bb6 --- /dev/null +++ b/common-agent-boot/src/main/java/com/bruce/dashboard/service/impl/StudioDashboardServiceImpl.java @@ -0,0 +1,246 @@ +package com.bruce.dashboard.service.impl; + +import com.bruce.agent.dto.response.AgentDefinitionResponse; +import com.bruce.agent.service.IAgentDefinitionService; +import com.bruce.dashboard.service.IStudioDashboardService; +import com.bruce.dashboard.vo.StudioDashboardChecklistItemVO; +import com.bruce.dashboard.vo.StudioDashboardLifecycleStepVO; +import com.bruce.dashboard.vo.StudioDashboardMetricsVO; +import com.bruce.dashboard.vo.StudioDashboardRecentRunVO; +import com.bruce.dashboard.vo.StudioDashboardVO; +import com.bruce.mcp.service.IMcpServerService; +import com.bruce.modelprovider.service.IModelWorkspaceService; +import com.bruce.modelprovider.vo.ModelWorkspaceVO; +import com.bruce.observability.service.IObservabilityRunService; +import com.bruce.observability.vo.ObservabilityRunSummaryVO; +import com.bruce.rag.service.IKnowledgeWorkspaceService; +import com.bruce.rag.service.IRagStoreService; +import com.bruce.rag.vo.KnowledgeWorkspaceVO; +import com.bruce.skill.service.ISkillDefinitionService; +import com.bruce.workflow.service.IProjectService; +import com.bruce.workflow.service.IWorkflowDefinitionService; +import com.bruce.workflow.vo.ProjectVO; +import com.bruce.workflow.vo.WorkflowDefinitionVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; + +/** + * Studio 首页聚合实现。 + *

+ * 该服务只汇总现有模块已经稳定的主数据和运行摘要,不引入新的存储表。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class StudioDashboardServiceImpl implements IStudioDashboardService { + + private final IProjectService projectService; + private final IWorkflowDefinitionService workflowDefinitionService; + private final IObservabilityRunService observabilityRunService; + private final IModelWorkspaceService modelWorkspaceService; + private final IRagStoreService ragStoreService; + private final IKnowledgeWorkspaceService knowledgeWorkspaceService; + private final ObjectProvider agentDefinitionServiceProvider; + private final IMcpServerService mcpServerService; + private final ISkillDefinitionService skillDefinitionService; + + @Override + public StudioDashboardVO getDashboard() { + log.info("Studio 首页聚合开始"); + List projects = projectService.listProjects(); + ProjectVO currentProject = projects.isEmpty() ? null : projects.get(0); + List workflows = currentProject == null ? List.of() : workflowDefinitionService.listByProjectId(currentProject.getId()); + List recentRuns = observabilityRunService.listRecentRuns(); + ModelWorkspaceVO modelWorkspace = modelWorkspaceService.getWorkspace(); + IAgentDefinitionService agentDefinitionService = agentDefinitionServiceProvider.getIfAvailable(); + List agents = agentDefinitionService == null ? List.of() : agentDefinitionService.listResponses(); + int mcpServerCount = mcpServerService.listServers().size(); + int skillCount = skillDefinitionService.listDefinitions().size(); + + KnowledgeWorkspaceVO knowledgeWorkspace = null; + if (!ragStoreService.listResponses().isEmpty()) { + Long firstStoreId = ragStoreService.listResponses().get(0).getId(); + knowledgeWorkspace = knowledgeWorkspaceService.getWorkspace(firstStoreId); + } + + StudioDashboardVO dashboard = new StudioDashboardVO(); + dashboard.setProjectName(currentProject == null ? "Common Agent Studio" : currentProject.getProjectName()); + dashboard.setEnvironment(currentProject == null ? "Dev" : currentProject.getEnvironment()); + dashboard.setPublishStatus(currentProject == null ? "DRAFT" : currentProject.getPublishStatus()); + dashboard.setLifecycleSteps(buildLifecycleSteps(knowledgeWorkspace, workflows, agents, recentRuns)); + dashboard.setReadinessChecklist(buildChecklist(knowledgeWorkspace, workflows, agents, modelWorkspace, mcpServerCount, skillCount)); + dashboard.setMetrics(buildMetrics(recentRuns)); + dashboard.setRecentRuns(buildRecentRuns(recentRuns, workflows, agents)); + dashboard.setWarningTitle(buildWarningTitle(modelWorkspace, workflows)); + dashboard.setWarningMessage(buildWarningMessage(modelWorkspace, workflows, knowledgeWorkspace)); + log.info("Studio 首页聚合结束,projectName={}, workflowCount={}, runCount={}", + dashboard.getProjectName(), workflows.size(), recentRuns.size()); + return dashboard; + } + + private List buildLifecycleSteps(KnowledgeWorkspaceVO knowledgeWorkspace, + List workflows, + List agents, + List recentRuns) { + List steps = new ArrayList<>(); + steps.add(step("知识接入", "上传、解析、切片、向量化", knowledgeWorkspace != null && knowledgeWorkspace.getDocumentCount() > 0 ? "done" : "idle")); + steps.add(step("能力编排", "Workflow 连接模型、工具与 Skill", workflows.isEmpty() ? "idle" : "running")); + steps.add(step("对话调试", "验证引用、成本、延迟与回答质量", agents.isEmpty() ? "idle" : "running")); + steps.add(step("发布观测", "版本快照、运行追踪、异常排查", recentRuns.isEmpty() ? "idle" : "done")); + return steps; + } + + private List buildChecklist(KnowledgeWorkspaceVO knowledgeWorkspace, + List workflows, + List agents, + ModelWorkspaceVO modelWorkspace, + int mcpServerCount, + int skillCount) { + List items = new ArrayList<>(); + items.add(checkItem("知识库已绑定 Embedding 模型", knowledgeWorkspace != null && knowledgeWorkspace.getEmbeddingModelId() != null)); + items.add(checkItem("Workflow 已存在可编辑草稿", !workflows.isEmpty())); + items.add(checkItem("Agent 已绑定默认知识库与能力", !agents.isEmpty())); + items.add(checkItem("MCP / Skill 基础能力已接入", mcpServerCount > 0 && skillCount > 0)); + items.add(checkItem("模型路由已配置至少一个启用规则", modelWorkspace.getEnabledRouteRuleCount() != null && modelWorkspace.getEnabledRouteRuleCount() > 0)); + return items; + } + + private StudioDashboardMetricsVO buildMetrics(List recentRuns) { + StudioDashboardMetricsVO metrics = new StudioDashboardMetricsVO(); + metrics.setTodayRunCount(recentRuns.size()); + long successCount = recentRuns.stream().filter(run -> "SUCCESS".equals(run.getStatus())).count(); + double successRate = recentRuns.isEmpty() ? 100D : successCount * 100.0 / recentRuns.size(); + metrics.setSuccessRate(roundDouble(successRate)); + metrics.setP50Latency(formatP50Latency(recentRuns)); + metrics.setEstimatedCost(formatCost(recentRuns)); + return metrics; + } + + private List buildRecentRuns(List recentRuns, + List workflows, + List agents) { + return recentRuns.stream().limit(5).map(run -> { + StudioDashboardRecentRunVO item = new StudioDashboardRecentRunVO(); + item.setId(run.getRequestId()); + item.setName(resolveRunName(run.getWorkflowId(), workflows, agents)); + item.setType(run.getWorkflowId() == null ? "Agent" : "Workflow"); + item.setStatus(formatRunStatus(run.getStatus())); + item.setLatency(formatDuration(run.getDurationMs())); + item.setCost("¥" + roundBigDecimal(run.getEstimatedCost() == null ? BigDecimal.ZERO : run.getEstimatedCost())); + return item; + }).toList(); + } + + private String buildWarningTitle(ModelWorkspaceVO modelWorkspace, List workflows) { + if (modelWorkspace.getEnabledRouteRuleCount() == null || modelWorkspace.getEnabledRouteRuleCount() == 0) { + return "发布前仍需补齐模型路由"; + } + if (workflows.isEmpty()) { + return "发布前仍需创建至少一个 Workflow"; + } + return "生产发布前仍需确认路由兜底"; + } + + private String buildWarningMessage(ModelWorkspaceVO modelWorkspace, + List workflows, + KnowledgeWorkspaceVO knowledgeWorkspace) { + if (modelWorkspace.getRecentFailedCallCount() != null && modelWorkspace.getRecentFailedCallCount() > 0) { + return "最近存在失败模型调用,建议先补齐 fallback 模型并复核错误上下文。"; + } + if (knowledgeWorkspace != null && knowledgeWorkspace.getPendingTaskCount() != null && knowledgeWorkspace.getPendingTaskCount() > 0) { + return "当前知识库仍有待索引文档,建议完成索引后再进行发布联调。"; + } + if (workflows.isEmpty()) { + return "当前项目尚无可试跑 Workflow,建议先完成最小链路编排。"; + } + return "AGENT_PLAN 任务建议补齐 fallback 模型和最大延迟阈值后再发布。"; + } + + private StudioDashboardLifecycleStepVO step(String name, String description, String status) { + StudioDashboardLifecycleStepVO step = new StudioDashboardLifecycleStepVO(); + step.setName(name); + step.setDescription(description); + step.setStatus(status); + return step; + } + + private StudioDashboardChecklistItemVO checkItem(String label, boolean done) { + StudioDashboardChecklistItemVO item = new StudioDashboardChecklistItemVO(); + item.setLabel(label); + item.setDone(done); + return item; + } + + private String resolveRunName(Long workflowId, List workflows, List agents) { + if (workflowId != null) { + return workflows.stream() + .filter(workflow -> workflowId.equals(workflow.getId())) + .map(WorkflowDefinitionVO::getWorkflowName) + .findFirst() + .orElse("Workflow 运行"); + } + return agents.isEmpty() ? "Agent 调试会话" : agents.get(0).getAgentName(); + } + + private String formatRunStatus(String status) { + if ("SUCCESS".equals(status)) { + return "成功"; + } + if ("FAILED".equals(status)) { + return "失败"; + } + if ("RUNNING".equals(status)) { + return "运行中"; + } + return status == null ? "-" : status; + } + + private String formatDuration(Integer durationMs) { + if (durationMs == null) { + return "-"; + } + if (durationMs >= 1000) { + return roundBigDecimal(BigDecimal.valueOf(durationMs).divide(BigDecimal.valueOf(1000), 2, RoundingMode.HALF_UP)) + "s"; + } + return durationMs + "ms"; + } + + private String formatP50Latency(List recentRuns) { + if (recentRuns.isEmpty()) { + return "-"; + } + List durations = recentRuns.stream() + .map(ObservabilityRunSummaryVO::getDurationMs) + .filter(value -> value != null && value > 0) + .sorted() + .toList(); + if (durations.isEmpty()) { + return "-"; + } + Integer p50 = durations.get(durations.size() / 2); + return formatDuration(p50); + } + + private String formatCost(List recentRuns) { + BigDecimal total = recentRuns.stream() + .map(ObservabilityRunSummaryVO::getEstimatedCost) + .filter(cost -> cost != null && cost.compareTo(BigDecimal.ZERO) > 0) + .reduce(BigDecimal.ZERO, BigDecimal::add); + return "¥" + roundBigDecimal(total); + } + + private Double roundDouble(double value) { + return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + + private String roundBigDecimal(BigDecimal value) { + return value.setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString(); + } +} diff --git a/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardChecklistItemVO.java b/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardChecklistItemVO.java new file mode 100644 index 0000000..2158be9 --- /dev/null +++ b/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardChecklistItemVO.java @@ -0,0 +1,12 @@ +package com.bruce.dashboard.vo; + +import lombok.Data; + +/** + * Studio 发布就绪项。 + */ +@Data +public class StudioDashboardChecklistItemVO { + private String label; + private Boolean done; +} diff --git a/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardLifecycleStepVO.java b/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardLifecycleStepVO.java new file mode 100644 index 0000000..4111aae --- /dev/null +++ b/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardLifecycleStepVO.java @@ -0,0 +1,13 @@ +package com.bruce.dashboard.vo; + +import lombok.Data; + +/** + * Studio 总览生命周期步骤。 + */ +@Data +public class StudioDashboardLifecycleStepVO { + private String name; + private String description; + private String status; +} diff --git a/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardMetricsVO.java b/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardMetricsVO.java new file mode 100644 index 0000000..b0e8d2d --- /dev/null +++ b/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardMetricsVO.java @@ -0,0 +1,14 @@ +package com.bruce.dashboard.vo; + +import lombok.Data; + +/** + * Studio 运行指标摘要。 + */ +@Data +public class StudioDashboardMetricsVO { + private Integer todayRunCount; + private Double successRate; + private String p50Latency; + private String estimatedCost; +} diff --git a/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardRecentRunVO.java b/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardRecentRunVO.java new file mode 100644 index 0000000..8d98b37 --- /dev/null +++ b/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardRecentRunVO.java @@ -0,0 +1,16 @@ +package com.bruce.dashboard.vo; + +import lombok.Data; + +/** + * Studio 最近运行摘要。 + */ +@Data +public class StudioDashboardRecentRunVO { + private String id; + private String name; + private String type; + private String status; + private String latency; + private String cost; +} diff --git a/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardVO.java b/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardVO.java new file mode 100644 index 0000000..8618b47 --- /dev/null +++ b/common-agent-boot/src/main/java/com/bruce/dashboard/vo/StudioDashboardVO.java @@ -0,0 +1,22 @@ +package com.bruce.dashboard.vo; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * Studio 首页聚合视图。 + */ +@Data +public class StudioDashboardVO { + private String projectName; + private String environment; + private String publishStatus; + private List lifecycleSteps = new ArrayList<>(); + private List readinessChecklist = new ArrayList<>(); + private StudioDashboardMetricsVO metrics; + private List recentRuns = new ArrayList<>(); + private String warningTitle; + private String warningMessage; +} diff --git a/common-agent-boot/src/test/java/com/bruce/dashboard/StudioDashboardServiceTests.java b/common-agent-boot/src/test/java/com/bruce/dashboard/StudioDashboardServiceTests.java new file mode 100644 index 0000000..9d0fc45 --- /dev/null +++ b/common-agent-boot/src/test/java/com/bruce/dashboard/StudioDashboardServiceTests.java @@ -0,0 +1,130 @@ +package com.bruce.dashboard; + +import com.bruce.agent.dto.response.AgentDefinitionResponse; +import com.bruce.agent.service.IAgentDefinitionService; +import com.bruce.dashboard.service.impl.StudioDashboardServiceImpl; +import com.bruce.dashboard.vo.StudioDashboardVO; +import com.bruce.mcp.service.IMcpServerService; +import com.bruce.modelprovider.service.IModelWorkspaceService; +import com.bruce.modelprovider.vo.ModelWorkspaceVO; +import com.bruce.observability.service.IObservabilityRunService; +import com.bruce.observability.vo.ObservabilityRunSummaryVO; +import com.bruce.rag.dto.response.RagStoreResponse; +import com.bruce.rag.service.IKnowledgeWorkspaceService; +import com.bruce.rag.service.IRagStoreService; +import com.bruce.rag.vo.KnowledgeWorkspaceVO; +import com.bruce.skill.service.ISkillDefinitionService; +import com.bruce.workflow.service.IProjectService; +import com.bruce.workflow.service.IWorkflowDefinitionService; +import com.bruce.workflow.vo.ProjectVO; +import com.bruce.workflow.vo.WorkflowDefinitionVO; +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 org.springframework.beans.factory.ObjectProvider; + +import java.math.BigDecimal; +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 StudioDashboardServiceTests { + + @Mock + private IProjectService projectService; + + @Mock + private IWorkflowDefinitionService workflowDefinitionService; + + @Mock + private IObservabilityRunService observabilityRunService; + + @Mock + private IModelWorkspaceService modelWorkspaceService; + + @Mock + private IRagStoreService ragStoreService; + + @Mock + private IKnowledgeWorkspaceService knowledgeWorkspaceService; + + @Mock + private IAgentDefinitionService agentDefinitionService; + + @Mock + private ObjectProvider agentDefinitionServiceProvider; + + @Mock + private IMcpServerService mcpServerService; + + @Mock + private ISkillDefinitionService skillDefinitionService; + + @InjectMocks + private StudioDashboardServiceImpl studioDashboardService; + + @Test + void getDashboardShouldAggregateLifecycleReadinessAndRecentRuns() { + ProjectVO project = new ProjectVO(); + project.setId(101L); + project.setProjectName("Common Agent Studio"); + project.setEnvironment("Dev"); + project.setPublishStatus("DRAFT"); + + WorkflowDefinitionVO workflow = new WorkflowDefinitionVO(); + workflow.setId(201L); + workflow.setWorkflowName("合同知识召回"); + + ObservabilityRunSummaryVO run = new ObservabilityRunSummaryVO(); + run.setRequestId("req-1001"); + run.setWorkflowId(201L); + run.setStatus("SUCCESS"); + run.setDurationMs(1420); + run.setEstimatedCost(BigDecimal.valueOf(0.018)); + + ModelWorkspaceVO modelWorkspace = new ModelWorkspaceVO(); + modelWorkspace.setEnabledRouteRuleCount(2); + modelWorkspace.setRecentFailedCallCount(0); + + RagStoreResponse store = new RagStoreResponse(); + store.setId(1001L); + + KnowledgeWorkspaceVO knowledgeWorkspace = new KnowledgeWorkspaceVO(); + knowledgeWorkspace.setEmbeddingModelId(88L); + knowledgeWorkspace.setDocumentCount(9); + knowledgeWorkspace.setPendingTaskCount(1); + + AgentDefinitionResponse agent = new AgentDefinitionResponse(); + agent.setId(301L); + agent.setAgentName("售前问答 Agent"); + + when(projectService.listProjects()).thenReturn(List.of(project)); + when(workflowDefinitionService.listByProjectId(101L)).thenReturn(List.of(workflow)); + when(observabilityRunService.listRecentRuns()).thenReturn(List.of(run)); + when(modelWorkspaceService.getWorkspace()).thenReturn(modelWorkspace); + when(ragStoreService.listResponses()).thenReturn(List.of(store)); + when(knowledgeWorkspaceService.getWorkspace(1001L)).thenReturn(knowledgeWorkspace); + when(agentDefinitionServiceProvider.getIfAvailable()).thenReturn(agentDefinitionService); + when(agentDefinitionService.listResponses()).thenReturn(List.of(agent)); + when(mcpServerService.listServers()).thenReturn(List.of()); + when(skillDefinitionService.listDefinitions()).thenReturn(List.of()); + + StudioDashboardVO dashboard = studioDashboardService.getDashboard(); + + assertNotNull(dashboard); + assertEquals("Common Agent Studio", dashboard.getProjectName()); + assertEquals("Dev", dashboard.getEnvironment()); + assertEquals(4, dashboard.getLifecycleSteps().size()); + assertEquals(5, dashboard.getReadinessChecklist().size()); + assertEquals(1, dashboard.getMetrics().getTodayRunCount()); + assertEquals(100D, dashboard.getMetrics().getSuccessRate()); + assertEquals("1.42s", dashboard.getRecentRuns().get(0).getLatency()); + assertEquals("合同知识召回", dashboard.getRecentRuns().get(0).getName()); + assertEquals("当前知识库仍有待索引文档,建议完成索引后再进行发布联调。", dashboard.getWarningMessage()); + } +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/controller/IngestionRunController.java b/common-agent-rag/src/main/java/com/bruce/rag/controller/IngestionRunController.java new file mode 100644 index 0000000..4c77298 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/controller/IngestionRunController.java @@ -0,0 +1,40 @@ +package com.bruce.rag.controller; + +import com.bruce.common.domain.model.RequestResult; +import com.bruce.rag.service.IIngestionRunService; +import com.bruce.rag.vo.IngestionRunVO; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 文件解析管道聚合接口。 + */ +@Tag(name = "文件解析管道") +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/knowledge/ingestion-runs") +public class IngestionRunController { + + private final IIngestionRunService ingestionRunService; + + @Operation(summary = "查询文件解析管道聚合视图") + @GetMapping("/{runId}") + public RequestResult detail(@PathVariable("runId") String runId, + @RequestParam("storeId") Long storeId, + @RequestParam("documentId") Long documentId) { + log.info("文件解析管道查询开始,runId={}, storeId={}, documentId={}", runId, storeId, documentId); + IngestionRunVO view = ingestionRunService.getRun(storeId, documentId); + view.setRunId(runId); + log.info("文件解析管道查询结束,runId={}, storeId={}, documentId={}, stepCount={}, logCount={}", + runId, storeId, documentId, view.getSteps().size(), view.getLogs().size()); + return RequestResult.success(view); + } +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/service/IIngestionRunService.java b/common-agent-rag/src/main/java/com/bruce/rag/service/IIngestionRunService.java new file mode 100644 index 0000000..11563a8 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/service/IIngestionRunService.java @@ -0,0 +1,14 @@ +package com.bruce.rag.service; + +import com.bruce.rag.vo.IngestionRunVO; + +/** + * 文件解析管道聚合服务。 + */ +public interface IIngestionRunService { + + /** + * 按知识库和文档聚合摄取流水线视图。 + */ + IngestionRunVO getRun(Long storeId, Long documentId); +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/service/impl/IngestionRunServiceImpl.java b/common-agent-rag/src/main/java/com/bruce/rag/service/impl/IngestionRunServiceImpl.java new file mode 100644 index 0000000..d81d7d6 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/service/impl/IngestionRunServiceImpl.java @@ -0,0 +1,281 @@ +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.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.entity.RagDocumentParseResult; +import com.bruce.rag.service.IIngestionRunService; +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.IRagStoreService; +import com.bruce.rag.vo.IngestionRunFileVO; +import com.bruce.rag.vo.IngestionRunLogVO; +import com.bruce.rag.vo.IngestionRunStepVO; +import com.bruce.rag.vo.IngestionRunVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * 文件解析管道聚合实现。 + *

+ * 首轮实现保持“主数据优先”,直接复用文档、解析快照、切片和向量结果生成前端可消费的聚合视图。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class IngestionRunServiceImpl implements IIngestionRunService { + + 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 STEP_DONE = "done"; + private static final String STEP_RUNNING = "running"; + private static final String STEP_BLOCKED = "blocked"; + private static final String STEP_IDLE = "idle"; + + private final IRagStoreService ragStoreService; + private final IRagDocumentService ragDocumentService; + private final IRagDocumentParseResultService ragDocumentParseResultService; + private final IRagChunkService ragChunkService; + private final IRagChunkEmbeddingService ragChunkEmbeddingService; + private final IRagStoreModelConfigService ragStoreModelConfigService; + + @Override + public IngestionRunVO getRun(Long storeId, Long documentId) { + log.info("文件解析管道聚合开始,storeId={}, documentId={}", storeId, documentId); + if (storeId == null || documentId == null) { + throw new IllegalArgumentException("知识库ID和文档ID不能为空"); + } + + RagStoreResponse store = ragStoreService.getResponseById(storeId); + if (store == null) { + throw new IllegalArgumentException("知识库不存在,ID: " + storeId); + } + RagDocumentResponse document = ragDocumentService.getResponseById(documentId); + if (document == null || !storeId.equals(document.getStoreId())) { + throw new IllegalArgumentException("文档不存在或不属于当前知识库,documentId: " + documentId); + } + + RagDocumentParseResult parseResult = ragDocumentParseResultService.getByDocumentId(documentId); + List chunks = ragChunkService.list(new LambdaQueryWrapper() + .eq(RagChunk::getDocumentId, documentId) + .eq(RagChunk::getEnabled, true) + .orderByAsc(RagChunk::getChunkIndex)); + List embeddings = ragChunkEmbeddingService.list(new LambdaQueryWrapper() + .eq(RagChunkEmbedding::getDocumentId, documentId) + .eq(RagChunkEmbedding::getEnabled, true) + .orderByDesc(RagChunkEmbedding::getCreateTime)); + RagStoreModelConfigResponse config = ragStoreModelConfigService.getByStoreId(storeId); + + IngestionRunVO view = new IngestionRunVO(); + view.setStoreId(storeId); + view.setDocumentId(documentId); + view.setStoreCode(store.getStoreCode()); + view.setStoreName(store.getStoreName()); + view.setFiles(List.of(toFileVO(document))); + view.setSteps(buildSteps(document, parseResult, chunks, embeddings, config)); + view.setParsedTextPreview(buildParsedPreview(parseResult)); + view.setChunkPreview(buildChunkPreview(chunks, config)); + view.setLogs(buildLogs(document, parseResult, chunks, embeddings)); + + if (config != null) { + view.setChunkStrategy(config.getChunkStrategy()); + view.setChunkSize(config.getChunkSize()); + view.setChunkOverlap(config.getChunkOverlap()); + view.setEmbeddingModelId(config.getEmbeddingModelId()); + view.setEmbeddingDimension(config.getEmbeddingDimension()); + } + + log.info("文件解析管道聚合结束,storeId={}, documentId={}, chunkCount={}, embeddingCount={}", + storeId, documentId, chunks.size(), embeddings.size()); + return view; + } + + private IngestionRunFileVO toFileVO(RagDocumentResponse document) { + IngestionRunFileVO file = new IngestionRunFileVO(); + file.setDocumentId(document.getId()); + file.setAttachmentId(document.getAttachmentId()); + file.setFileName(document.getDocumentTitle()); + file.setParseStatus(document.getParseStatus()); + file.setIndexStatus(document.getIndexStatus()); + file.setErrorMessage(document.getErrorMessage()); + return file; + } + + private List buildSteps(RagDocumentResponse document, + RagDocumentParseResult parseResult, + List chunks, + List embeddings, + RagStoreModelConfigResponse config) { + List steps = new ArrayList<>(); + steps.add(step("上传", "文件已入库并创建 rag_document", STEP_DONE)); + steps.add(step("解析", parseDescription(parseResult), parseStepStatus(document))); + steps.add(step("切片", chunkDescription(chunks, config), chunkStepStatus(document, chunks))); + steps.add(step("向量化", embeddingDescription(embeddings, config), embeddingStepStatus(document, chunks, embeddings))); + steps.add(step("可检索", retrievableDescription(document), retrievableStepStatus(document))); + return steps; + } + + private List buildLogs(RagDocumentResponse document, + RagDocumentParseResult parseResult, + List chunks, + List embeddings) { + List logs = new ArrayList<>(); + logs.add(log(document.getCreateTime(), "INFO", "上传文件并创建 rag_document 记录")); + if (parseResult != null) { + logs.add(log(parseResult.getCreateTime(), "INFO", + "解析完成,文本长度 " + safeNumber(parseResult.getTextLength()) + ",页数 " + safeNumber(parseResult.getPageCount()))); + } else if (PARSE_STATUS_FAILED.equals(document.getParseStatus())) { + logs.add(log(document.getUpdateTime(), "WARN", defaultText(document.getErrorMessage(), "解析失败,等待重试"))); + } else { + logs.add(log(document.getUpdateTime(), "INFO", "解析尚未产出快照,等待执行")); + } + + if (!chunks.isEmpty()) { + logs.add(log(chunks.get(chunks.size() - 1).getCreateTime(), "INFO", "切片完成,共生成 " + chunks.size() + " 个 rag_chunk")); + } else { + logs.add(log(document.getUpdateTime(), "INFO", "切片尚未执行或等待解析成功")); + } + + if (!embeddings.isEmpty()) { + RagChunkEmbedding latestEmbedding = embeddings.get(0); + logs.add(log(latestEmbedding.getCreateTime(), "INFO", + "向量化完成,已写入 " + embeddings.size() + " 条 rag_chunk_embedding")); + } else { + logs.add(log(document.getUpdateTime(), "INFO", "向量化尚未执行或等待切片完成")); + } + return logs; + } + + private IngestionRunStepVO step(String name, String description, String status) { + IngestionRunStepVO step = new IngestionRunStepVO(); + step.setName(name); + step.setDescription(description); + step.setStatus(status); + return step; + } + + private IngestionRunLogVO log(Date time, String level, String message) { + IngestionRunLogVO log = new IngestionRunLogVO(); + log.setTime(formatTime(time)); + log.setLevel(level); + log.setMessage(message); + return log; + } + + private String parseDescription(RagDocumentParseResult parseResult) { + if (parseResult == null) { + return "等待解析快照写入 rag_document_parse_result"; + } + return "解析完成,文本长度 " + safeNumber(parseResult.getTextLength()) + ",页数 " + safeNumber(parseResult.getPageCount()); + } + + private String chunkDescription(List chunks, RagStoreModelConfigResponse config) { + if (chunks.isEmpty()) { + return "等待根据策略生成 rag_chunk"; + } + return "已生成 " + chunks.size() + " 个切片,chunk_size=" + safeNumber(config == null ? null : config.getChunkSize()) + + ",overlap=" + safeNumber(config == null ? null : config.getChunkOverlap()); + } + + private String embeddingDescription(List embeddings, RagStoreModelConfigResponse config) { + if (embeddings.isEmpty()) { + return "等待向量化并写入 rag_chunk_embedding"; + } + String modelText = config == null || config.getEmbeddingModelId() == null ? "-" : String.valueOf(config.getEmbeddingModelId()); + return "已生成 " + embeddings.size() + " 条向量,模型 " + modelText + ",维度 " + + safeNumber(config == null ? null : config.getEmbeddingDimension()); + } + + private String retrievableDescription(RagDocumentResponse document) { + if (INDEX_STATUS_INDEXED.equals(document.getIndexStatus())) { + return "索引已完成,当前文档可参与检索召回"; + } + return "等待索引完成后进入可检索状态"; + } + + private String parseStepStatus(RagDocumentResponse document) { + if (PARSE_STATUS_PARSED.equals(document.getParseStatus())) { + return STEP_DONE; + } + if (PARSE_STATUS_FAILED.equals(document.getParseStatus())) { + return STEP_BLOCKED; + } + return STEP_RUNNING; + } + + private String chunkStepStatus(RagDocumentResponse document, List chunks) { + if (!chunks.isEmpty()) { + return STEP_DONE; + } + if (PARSE_STATUS_FAILED.equals(document.getParseStatus())) { + return STEP_IDLE; + } + return PARSE_STATUS_PARSED.equals(document.getParseStatus()) ? STEP_RUNNING : STEP_IDLE; + } + + private String embeddingStepStatus(RagDocumentResponse document, List chunks, List embeddings) { + if (!embeddings.isEmpty()) { + return STEP_DONE; + } + if (PARSE_STATUS_FAILED.equals(document.getParseStatus())) { + return STEP_IDLE; + } + return chunks.isEmpty() ? STEP_IDLE : STEP_RUNNING; + } + + private String retrievableStepStatus(RagDocumentResponse document) { + return INDEX_STATUS_INDEXED.equals(document.getIndexStatus()) ? STEP_DONE : STEP_IDLE; + } + + private String buildParsedPreview(RagDocumentParseResult parseResult) { + if (parseResult == null || parseResult.getParsedText() == null) { + return "暂无解析文本预览"; + } + return truncate(parseResult.getParsedText(), 180); + } + + private String buildChunkPreview(List chunks, RagStoreModelConfigResponse config) { + if (chunks.isEmpty()) { + return "暂无切片预览"; + } + RagChunk firstChunk = chunks.get(0); + return "chunk_size=" + safeNumber(config == null ? null : config.getChunkSize()) + + ", overlap=" + safeNumber(config == null ? null : config.getChunkOverlap()) + + ", strategy=" + safeNumber(config == null ? null : config.getChunkStrategy()) + + "。预览:" + truncate(defaultText(firstChunk.getChunkSummary(), firstChunk.getChunkContent()), 140); + } + + private String truncate(String text, int limit) { + if (text == null || text.length() <= limit) { + return text; + } + return text.substring(0, limit) + "..."; + } + + private String defaultText(String preferred, String fallback) { + return preferred == null || preferred.isBlank() ? fallback : preferred; + } + + private String safeNumber(Number value) { + return value == null ? "-" : String.valueOf(value); + } + + private String formatTime(Date time) { + Date value = time == null ? new Date() : time; + return new SimpleDateFormat("HH:mm:ss", Locale.CHINA).format(value); + } +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunFileVO.java b/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunFileVO.java new file mode 100644 index 0000000..e17f3ab --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunFileVO.java @@ -0,0 +1,34 @@ +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; + +/** + * 摄取流水线文件摘要。 + */ +@Data +@Schema(description = "摄取流水线文件摘要") +public class IngestionRunFileVO { + + @Schema(description = "文档ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long documentId; + + @Schema(description = "附件ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long attachmentId; + + @Schema(description = "文件名称") + private String fileName; + + @Schema(description = "解析状态") + private String parseStatus; + + @Schema(description = "索引状态") + private String indexStatus; + + @Schema(description = "错误信息") + private String errorMessage; +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunLogVO.java b/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunLogVO.java new file mode 100644 index 0000000..4d3bbd8 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunLogVO.java @@ -0,0 +1,21 @@ +package com.bruce.rag.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 摄取流水线日志视图。 + */ +@Data +@Schema(description = "摄取流水线日志视图") +public class IngestionRunLogVO { + + @Schema(description = "日志时间") + private String time; + + @Schema(description = "日志级别") + private String level; + + @Schema(description = "日志内容") + private String message; +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunStepVO.java b/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunStepVO.java new file mode 100644 index 0000000..ba34418 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunStepVO.java @@ -0,0 +1,21 @@ +package com.bruce.rag.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 摄取流水线阶段视图。 + */ +@Data +@Schema(description = "摄取流水线阶段视图") +public class IngestionRunStepVO { + + @Schema(description = "阶段名称") + private String name; + + @Schema(description = "阶段说明") + private String description; + + @Schema(description = "阶段状态") + private String status; +} diff --git a/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunVO.java b/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunVO.java new file mode 100644 index 0000000..8dff891 --- /dev/null +++ b/common-agent-rag/src/main/java/com/bruce/rag/vo/IngestionRunVO.java @@ -0,0 +1,65 @@ +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 IngestionRunVO { + + @Schema(description = "运行标识") + private String runId; + + @Schema(description = "知识库ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long storeId; + + @Schema(description = "文档ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long documentId; + + @Schema(description = "知识库编码") + private String storeCode; + + @Schema(description = "知识库名称") + private String storeName; + + @Schema(description = "文件摘要列表") + private List files = new ArrayList<>(); + + @Schema(description = "流水线阶段") + private List steps = new ArrayList<>(); + + @Schema(description = "解析文本预览") + private String parsedTextPreview; + + @Schema(description = "切片预览") + private String chunkPreview; + + @Schema(description = "切片策略") + private Integer chunkStrategy; + + @Schema(description = "切片长度") + private Integer chunkSize; + + @Schema(description = "切片重叠") + private Integer chunkOverlap; + + @Schema(description = "Embedding 模型ID") + @JsonSerialize(using = ToStringSerializer.class) + private Long embeddingModelId; + + @Schema(description = "Embedding 维度") + private Integer embeddingDimension; + + @Schema(description = "任务日志") + private List logs = new ArrayList<>(); +} diff --git a/common-agent-rag/src/test/java/com/bruce/rag/ingestion/IngestionRunServiceTests.java b/common-agent-rag/src/test/java/com/bruce/rag/ingestion/IngestionRunServiceTests.java new file mode 100644 index 0000000..54657c3 --- /dev/null +++ b/common-agent-rag/src/test/java/com/bruce/rag/ingestion/IngestionRunServiceTests.java @@ -0,0 +1,129 @@ +package com.bruce.rag.ingestion; + +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.entity.RagChunk; +import com.bruce.rag.entity.RagChunkEmbedding; +import com.bruce.rag.entity.RagDocumentParseResult; +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.IRagStoreService; +import com.bruce.rag.service.impl.IngestionRunServiceImpl; +import com.bruce.rag.vo.IngestionRunVO; +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 IngestionRunServiceTests { + + @Mock + private IRagStoreService ragStoreService; + + @Mock + private IRagDocumentService ragDocumentService; + + @Mock + private IRagDocumentParseResultService ragDocumentParseResultService; + + @Mock + private IRagChunkService ragChunkService; + + @Mock + private IRagChunkEmbeddingService ragChunkEmbeddingService; + + @Mock + private IRagStoreModelConfigService ragStoreModelConfigService; + + @InjectMocks + private IngestionRunServiceImpl ingestionRunService; + + @Test + void getRunShouldAggregatePipelinePreviewAndLogs() { + RagStoreResponse store = new RagStoreResponse(); + store.setId(1001L); + store.setStoreCode("PROD_DOC"); + store.setStoreName("产品制度库"); + + RagDocumentResponse document = new RagDocumentResponse(); + document.setId(11L); + document.setStoreId(1001L); + document.setAttachmentId(901L); + document.setDocumentTitle("售前方案模板.pdf"); + document.setParseStatus("PARSED"); + document.setIndexStatus("INDEXED"); + document.setCreateTime(new Date(1748780000000L)); + document.setUpdateTime(new Date(1748780300000L)); + + RagDocumentParseResult parseResult = new RagDocumentParseResult(); + parseResult.setDocumentId(11L); + parseResult.setParsedText("私有化部署章节应覆盖基础设施、网络、安全与运维边界。平台需说明模型服务商、知识库索引策略与日志留存周期。"); + parseResult.setTextLength(1280); + parseResult.setPageCount(12); + parseResult.setCreateTime(new Date(1748780100000L)); + + RagChunk chunk = new RagChunk(); + chunk.setDocumentId(11L); + chunk.setChunkIndex(24); + chunk.setChunkSummary("chunk_size=800, overlap=120, strategy=FIXED_LENGTH"); + chunk.setChunkContent("该切片将进入 rag_chunk 并在向量化后写入 rag_chunk_embedding。"); + chunk.setCreateTime(new Date(1748780200000L)); + + RagChunkEmbedding embedding = new RagChunkEmbedding(); + embedding.setDocumentId(11L); + embedding.setEmbeddingDimension(1024); + embedding.setCreateTime(new Date(1748780250000L)); + + RagStoreModelConfigResponse config = new RagStoreModelConfigResponse(); + config.setStoreId(1001L); + config.setEmbeddingModelId(88L); + config.setEmbeddingDimension(1024); + config.setChunkStrategy(1); + config.setChunkSize(800); + config.setChunkOverlap(120); + + when(ragStoreService.getResponseById(1001L)).thenReturn(store); + when(ragDocumentService.getResponseById(11L)).thenReturn(document); + when(ragDocumentParseResultService.getByDocumentId(11L)).thenReturn(parseResult); + when(ragChunkService.list(org.mockito.ArgumentMatchers.>any())) + .thenReturn(List.of(chunk)); + when(ragChunkEmbeddingService.list(org.mockito.ArgumentMatchers.>any())) + .thenReturn(List.of(embedding)); + when(ragStoreModelConfigService.getByStoreId(1001L)).thenReturn(config); + + IngestionRunVO view = ingestionRunService.getRun(1001L, 11L); + + assertNotNull(view); + assertEquals(1001L, view.getStoreId()); + assertEquals(11L, view.getDocumentId()); + assertEquals("PROD_DOC", view.getStoreCode()); + assertEquals("产品制度库", view.getStoreName()); + assertEquals(1, view.getFiles().size()); + assertEquals(5, view.getSteps().size()); + assertEquals("done", view.getSteps().get(1).getStatus()); + assertEquals("done", view.getSteps().get(2).getStatus()); + assertEquals("done", view.getSteps().get(3).getStatus()); + assertEquals("done", view.getSteps().get(4).getStatus()); + assertEquals(88L, view.getEmbeddingModelId()); + assertEquals(1024, view.getEmbeddingDimension()); + assertEquals(800, view.getChunkSize()); + assertEquals(120, view.getChunkOverlap()); + assertEquals(4, view.getLogs().size()); + assertEquals("售前方案模板.pdf", view.getFiles().get(0).getFileName()); + assertEquals(true, view.getParsedTextPreview().contains("私有化部署章节")); + assertEquals(true, view.getChunkPreview().contains("chunk_size=800")); + } +} diff --git a/frontend/src/api/__tests__/ingestion.spec.ts b/frontend/src/api/__tests__/ingestion.spec.ts new file mode 100644 index 0000000..5ab62f3 --- /dev/null +++ b/frontend/src/api/__tests__/ingestion.spec.ts @@ -0,0 +1,31 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const getMock = vi.fn(); + +vi.mock('../request', () => ({ + get: getMock, +})); + +describe('ingestion api', () => { + afterEach(() => { + getMock.mockReset(); + }); + + it('requests ingestion run aggregate with store and document params', async () => { + getMock.mockResolvedValue({ + resultcode: '0', + message: null, + data: { runId: 'run-20260601' }, + }); + + const { getIngestionRun } = await import('../ingestion'); + await getIngestionRun('run-20260601', '1001', '11'); + + expect(getMock).toHaveBeenCalledWith('/knowledge/ingestion-runs/run-20260601', { + params: { + storeId: '1001', + documentId: '11', + }, + }); + }); +}); diff --git a/frontend/src/api/__tests__/studioDashboard.spec.ts b/frontend/src/api/__tests__/studioDashboard.spec.ts new file mode 100644 index 0000000..afc4d27 --- /dev/null +++ b/frontend/src/api/__tests__/studioDashboard.spec.ts @@ -0,0 +1,26 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const getMock = vi.fn(); + +vi.mock('../request', () => ({ + get: getMock, +})); + +describe('studioDashboard api', () => { + afterEach(() => { + getMock.mockReset(); + }); + + it('requests studio dashboard aggregate', async () => { + getMock.mockResolvedValue({ + resultcode: '0', + message: null, + data: { projectName: 'Common Agent Studio' }, + }); + + const { getStudioDashboard } = await import('../studioDashboard'); + await getStudioDashboard(); + + expect(getMock).toHaveBeenCalledWith('/studio/dashboard'); + }); +}); diff --git a/frontend/src/api/ingestion.ts b/frontend/src/api/ingestion.ts new file mode 100644 index 0000000..17995ed --- /dev/null +++ b/frontend/src/api/ingestion.ts @@ -0,0 +1,49 @@ +import { get } from './request'; + +export interface IngestionRunFile { + documentId: string; + attachmentId?: string | null; + fileName?: string | null; + parseStatus?: string | null; + indexStatus?: string | null; + errorMessage?: string | null; +} + +export interface IngestionRunStep { + name: string; + description?: string | null; + status?: string | null; +} + +export interface IngestionRunLog { + time?: string | null; + level?: string | null; + message?: string | null; +} + +export interface IngestionRun { + runId: string; + storeId: string; + documentId: string; + storeCode?: string | null; + storeName?: string | null; + files: IngestionRunFile[]; + steps: IngestionRunStep[]; + parsedTextPreview?: string | null; + chunkPreview?: string | null; + chunkStrategy?: number | null; + chunkSize?: number | null; + chunkOverlap?: number | null; + embeddingModelId?: string | null; + embeddingDimension?: number | null; + logs: IngestionRunLog[]; +} + +export function getIngestionRun(runId: string, storeId: string, documentId: string) { + return get(`/knowledge/ingestion-runs/${runId}`, { + params: { + storeId, + documentId, + }, + }); +} diff --git a/frontend/src/api/studioDashboard.ts b/frontend/src/api/studioDashboard.ts new file mode 100644 index 0000000..26c2334 --- /dev/null +++ b/frontend/src/api/studioDashboard.ts @@ -0,0 +1,44 @@ +import { get } from './request'; + +export interface StudioDashboardLifecycleStep { + name: string; + description?: string | null; + status?: string | null; +} + +export interface StudioDashboardChecklistItem { + label: string; + done: boolean; +} + +export interface StudioDashboardMetrics { + todayRunCount: number; + successRate: number; + p50Latency?: string | null; + estimatedCost?: string | null; +} + +export interface StudioDashboardRecentRun { + id: string; + name?: string | null; + type?: string | null; + status?: string | null; + latency?: string | null; + cost?: string | null; +} + +export interface StudioDashboard { + projectName?: string | null; + environment?: string | null; + publishStatus?: string | null; + lifecycleSteps: StudioDashboardLifecycleStep[]; + readinessChecklist: StudioDashboardChecklistItem[]; + metrics: StudioDashboardMetrics; + recentRuns: StudioDashboardRecentRun[]; + warningTitle?: string | null; + warningMessage?: string | null; +} + +export function getStudioDashboard() { + return get('/studio/dashboard'); +} diff --git a/frontend/src/pages/studio/AgentWorkspacePage.vue b/frontend/src/pages/studio/AgentWorkspacePage.vue index 3a95e0b..3ad395a 100644 --- a/frontend/src/pages/studio/AgentWorkspacePage.vue +++ b/frontend/src/pages/studio/AgentWorkspacePage.vue @@ -1,7 +1,72 @@