feat(studio): 补齐剩余工作台聚合接口与真实对接
This commit is contained in:
@@ -12,6 +12,7 @@ import com.bruce.agent.vo.AgentSessionDetailVO;
|
|||||||
import com.bruce.common.enums.EnableStatusEnum;
|
import com.bruce.common.enums.EnableStatusEnum;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ public class AgentSessionServiceImpl extends ServiceImpl<AgentSessionMapper, Age
|
|||||||
private static final String SESSION_STATUS_ACTIVE = "ACTIVE";
|
private static final String SESSION_STATUS_ACTIVE = "ACTIVE";
|
||||||
private static final String SESSION_STATUS_CLOSED = "CLOSED";
|
private static final String SESSION_STATUS_CLOSED = "CLOSED";
|
||||||
|
|
||||||
private final IAgentDefinitionService agentDefinitionService;
|
private final ObjectProvider<IAgentDefinitionService> agentDefinitionServiceProvider;
|
||||||
private final AgentSessionFactory agentSessionFactory;
|
private final AgentSessionFactory agentSessionFactory;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -40,6 +41,10 @@ public class AgentSessionServiceImpl extends ServiceImpl<AgentSessionMapper, Age
|
|||||||
@Override
|
@Override
|
||||||
public AgentSession createSessionEntity(AgentSessionCreateDTO request) {
|
public AgentSession createSessionEntity(AgentSessionCreateDTO request) {
|
||||||
validateCreateRequest(request);
|
validateCreateRequest(request);
|
||||||
|
IAgentDefinitionService agentDefinitionService = agentDefinitionServiceProvider.getIfAvailable();
|
||||||
|
if (agentDefinitionService == null) {
|
||||||
|
throw new IllegalStateException("Agent定义服务未就绪,暂无法创建会话");
|
||||||
|
}
|
||||||
AgentDefinition agent = agentDefinitionService.getById(request.getAgentId());
|
AgentDefinition agent = agentDefinitionService.getById(request.getAgentId());
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
throw new IllegalArgumentException("Agent不存在,ID: " + request.getAgentId());
|
throw new IllegalArgumentException("Agent不存在,ID: " + request.getAgentId());
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.mockito.InjectMocks;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Spy;
|
import org.mockito.Spy;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
@@ -32,6 +33,9 @@ class AgentSessionServiceTests {
|
|||||||
@Mock
|
@Mock
|
||||||
private IAgentDefinitionService agentDefinitionService;
|
private IAgentDefinitionService agentDefinitionService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ObjectProvider<IAgentDefinitionService> agentDefinitionServiceProvider;
|
||||||
|
|
||||||
@Spy
|
@Spy
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private AgentSessionServiceImpl agentSessionService;
|
private AgentSessionServiceImpl agentSessionService;
|
||||||
@@ -42,6 +46,7 @@ class AgentSessionServiceTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createSessionShouldRejectDisabledAgent() {
|
void createSessionShouldRejectDisabledAgent() {
|
||||||
|
when(agentDefinitionServiceProvider.getIfAvailable()).thenReturn(agentDefinitionService);
|
||||||
AgentDefinition agent = new AgentDefinition();
|
AgentDefinition agent = new AgentDefinition();
|
||||||
agent.setId(1L);
|
agent.setId(1L);
|
||||||
agent.setStatus("DISABLED");
|
agent.setStatus("DISABLED");
|
||||||
@@ -56,6 +61,7 @@ class AgentSessionServiceTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createSessionShouldPersistActiveSession() {
|
void createSessionShouldPersistActiveSession() {
|
||||||
|
when(agentDefinitionServiceProvider.getIfAvailable()).thenReturn(agentDefinitionService);
|
||||||
AgentDefinition agent = new AgentDefinition();
|
AgentDefinition agent = new AgentDefinition();
|
||||||
agent.setId(1L);
|
agent.setId(1L);
|
||||||
agent.setStatus(EnableStatusEnum.ENABLED.name());
|
agent.setStatus(EnableStatusEnum.ENABLED.name());
|
||||||
|
|||||||
@@ -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<StudioDashboardVO> detail() {
|
||||||
|
log.info("Studio 首页总览查询开始");
|
||||||
|
StudioDashboardVO dashboard = studioDashboardService.getDashboard();
|
||||||
|
log.info("Studio 首页总览查询结束,projectName={}, recentRunCount={}",
|
||||||
|
dashboard.getProjectName(), dashboard.getRecentRuns().size());
|
||||||
|
return RequestResult.success(dashboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.bruce.dashboard.service;
|
||||||
|
|
||||||
|
import com.bruce.dashboard.vo.StudioDashboardVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Studio 总览工作台聚合服务。
|
||||||
|
*/
|
||||||
|
public interface IStudioDashboardService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总当前项目的发布旅程、运行摘要和风险提示。
|
||||||
|
*/
|
||||||
|
StudioDashboardVO getDashboard();
|
||||||
|
}
|
||||||
@@ -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 首页聚合实现。
|
||||||
|
* <p>
|
||||||
|
* 该服务只汇总现有模块已经稳定的主数据和运行摘要,不引入新的存储表。
|
||||||
|
*/
|
||||||
|
@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<IAgentDefinitionService> agentDefinitionServiceProvider;
|
||||||
|
private final IMcpServerService mcpServerService;
|
||||||
|
private final ISkillDefinitionService skillDefinitionService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StudioDashboardVO getDashboard() {
|
||||||
|
log.info("Studio 首页聚合开始");
|
||||||
|
List<ProjectVO> projects = projectService.listProjects();
|
||||||
|
ProjectVO currentProject = projects.isEmpty() ? null : projects.get(0);
|
||||||
|
List<WorkflowDefinitionVO> workflows = currentProject == null ? List.of() : workflowDefinitionService.listByProjectId(currentProject.getId());
|
||||||
|
List<ObservabilityRunSummaryVO> recentRuns = observabilityRunService.listRecentRuns();
|
||||||
|
ModelWorkspaceVO modelWorkspace = modelWorkspaceService.getWorkspace();
|
||||||
|
IAgentDefinitionService agentDefinitionService = agentDefinitionServiceProvider.getIfAvailable();
|
||||||
|
List<AgentDefinitionResponse> 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<StudioDashboardLifecycleStepVO> buildLifecycleSteps(KnowledgeWorkspaceVO knowledgeWorkspace,
|
||||||
|
List<WorkflowDefinitionVO> workflows,
|
||||||
|
List<AgentDefinitionResponse> agents,
|
||||||
|
List<ObservabilityRunSummaryVO> recentRuns) {
|
||||||
|
List<StudioDashboardLifecycleStepVO> 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<StudioDashboardChecklistItemVO> buildChecklist(KnowledgeWorkspaceVO knowledgeWorkspace,
|
||||||
|
List<WorkflowDefinitionVO> workflows,
|
||||||
|
List<AgentDefinitionResponse> agents,
|
||||||
|
ModelWorkspaceVO modelWorkspace,
|
||||||
|
int mcpServerCount,
|
||||||
|
int skillCount) {
|
||||||
|
List<StudioDashboardChecklistItemVO> 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<ObservabilityRunSummaryVO> 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<StudioDashboardRecentRunVO> buildRecentRuns(List<ObservabilityRunSummaryVO> recentRuns,
|
||||||
|
List<WorkflowDefinitionVO> workflows,
|
||||||
|
List<AgentDefinitionResponse> 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<WorkflowDefinitionVO> workflows) {
|
||||||
|
if (modelWorkspace.getEnabledRouteRuleCount() == null || modelWorkspace.getEnabledRouteRuleCount() == 0) {
|
||||||
|
return "发布前仍需补齐模型路由";
|
||||||
|
}
|
||||||
|
if (workflows.isEmpty()) {
|
||||||
|
return "发布前仍需创建至少一个 Workflow";
|
||||||
|
}
|
||||||
|
return "生产发布前仍需确认路由兜底";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildWarningMessage(ModelWorkspaceVO modelWorkspace,
|
||||||
|
List<WorkflowDefinitionVO> 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<WorkflowDefinitionVO> workflows, List<AgentDefinitionResponse> 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<ObservabilityRunSummaryVO> recentRuns) {
|
||||||
|
if (recentRuns.isEmpty()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
List<Integer> 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<ObservabilityRunSummaryVO> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.bruce.dashboard.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Studio 发布就绪项。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class StudioDashboardChecklistItemVO {
|
||||||
|
private String label;
|
||||||
|
private Boolean done;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<StudioDashboardLifecycleStepVO> lifecycleSteps = new ArrayList<>();
|
||||||
|
private List<StudioDashboardChecklistItemVO> readinessChecklist = new ArrayList<>();
|
||||||
|
private StudioDashboardMetricsVO metrics;
|
||||||
|
private List<StudioDashboardRecentRunVO> recentRuns = new ArrayList<>();
|
||||||
|
private String warningTitle;
|
||||||
|
private String warningMessage;
|
||||||
|
}
|
||||||
@@ -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<IAgentDefinitionService> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<IngestionRunVO> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.bruce.rag.service;
|
||||||
|
|
||||||
|
import com.bruce.rag.vo.IngestionRunVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件解析管道聚合服务。
|
||||||
|
*/
|
||||||
|
public interface IIngestionRunService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按知识库和文档聚合摄取流水线视图。
|
||||||
|
*/
|
||||||
|
IngestionRunVO getRun(Long storeId, Long documentId);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件解析管道聚合实现。
|
||||||
|
* <p>
|
||||||
|
* 首轮实现保持“主数据优先”,直接复用文档、解析快照、切片和向量结果生成前端可消费的聚合视图。
|
||||||
|
*/
|
||||||
|
@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<RagChunk> chunks = ragChunkService.list(new LambdaQueryWrapper<RagChunk>()
|
||||||
|
.eq(RagChunk::getDocumentId, documentId)
|
||||||
|
.eq(RagChunk::getEnabled, true)
|
||||||
|
.orderByAsc(RagChunk::getChunkIndex));
|
||||||
|
List<RagChunkEmbedding> embeddings = ragChunkEmbeddingService.list(new LambdaQueryWrapper<RagChunkEmbedding>()
|
||||||
|
.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<IngestionRunStepVO> buildSteps(RagDocumentResponse document,
|
||||||
|
RagDocumentParseResult parseResult,
|
||||||
|
List<RagChunk> chunks,
|
||||||
|
List<RagChunkEmbedding> embeddings,
|
||||||
|
RagStoreModelConfigResponse config) {
|
||||||
|
List<IngestionRunStepVO> 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<IngestionRunLogVO> buildLogs(RagDocumentResponse document,
|
||||||
|
RagDocumentParseResult parseResult,
|
||||||
|
List<RagChunk> chunks,
|
||||||
|
List<RagChunkEmbedding> embeddings) {
|
||||||
|
List<IngestionRunLogVO> 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<RagChunk> 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<RagChunkEmbedding> 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<RagChunk> 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<RagChunk> chunks, List<RagChunkEmbedding> 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<RagChunk> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<IngestionRunFileVO> files = new ArrayList<>();
|
||||||
|
|
||||||
|
@Schema(description = "流水线阶段")
|
||||||
|
private List<IngestionRunStepVO> 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<IngestionRunLogVO> logs = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -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.<com.baomidou.mybatisplus.core.conditions.Wrapper<RagChunk>>any()))
|
||||||
|
.thenReturn(List.of(chunk));
|
||||||
|
when(ragChunkEmbeddingService.list(org.mockito.ArgumentMatchers.<com.baomidou.mybatisplus.core.conditions.Wrapper<RagChunkEmbedding>>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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
31
frontend/src/api/__tests__/ingestion.spec.ts
Normal file
31
frontend/src/api/__tests__/ingestion.spec.ts
Normal file
@@ -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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
26
frontend/src/api/__tests__/studioDashboard.spec.ts
Normal file
26
frontend/src/api/__tests__/studioDashboard.spec.ts
Normal file
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
49
frontend/src/api/ingestion.ts
Normal file
49
frontend/src/api/ingestion.ts
Normal file
@@ -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<IngestionRun>(`/knowledge/ingestion-runs/${runId}`, {
|
||||||
|
params: {
|
||||||
|
storeId,
|
||||||
|
documentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
44
frontend/src/api/studioDashboard.ts
Normal file
44
frontend/src/api/studioDashboard.ts
Normal file
@@ -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<StudioDashboard>('/studio/dashboard');
|
||||||
|
}
|
||||||
@@ -1,7 +1,72 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { ChatDotRound, Coin, Timer } from '@element-plus/icons-vue';
|
import { ChatDotRound, Coin, Timer } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
import { chatMessages, citations, traceSteps } from '@/data/studioMock';
|
import type { AgentMessageRecord, AgentWorkspace } from '@/api/agent';
|
||||||
|
import { appendAgentMessage, getAgentWorkspace } from '@/api/agent';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const agentId = ref('1001');
|
||||||
|
const workspace = ref<AgentWorkspace | null>(null);
|
||||||
|
const draftMessage = ref('');
|
||||||
|
|
||||||
|
const citations = computed(() => {
|
||||||
|
return (workspace.value?.messages ?? [])
|
||||||
|
.filter((message) => message.citationJson?.includes('chunkId'))
|
||||||
|
.map((message, index) => ({
|
||||||
|
key: `${message.id || index}`,
|
||||||
|
title: `引用 ${index + 1}`,
|
||||||
|
text: message.citationJson ?? '',
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const latestAssistantMessage = computed(() => {
|
||||||
|
const assistantMessages = (workspace.value?.messages ?? []).filter((item) => item.role === 'assistant');
|
||||||
|
return assistantMessages.length > 0 ? assistantMessages[assistantMessages.length - 1] : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
function roleLabel(role: AgentMessageRecord['role']) {
|
||||||
|
if (role === 'user') {
|
||||||
|
return '用户';
|
||||||
|
}
|
||||||
|
if (role === 'assistant') {
|
||||||
|
return 'Agent';
|
||||||
|
}
|
||||||
|
return '系统';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWorkspace() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getAgentWorkspace(agentId.value);
|
||||||
|
workspace.value = response.data;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessage() {
|
||||||
|
if (!workspace.value?.sessionId || !draftMessage.value.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await appendAgentMessage(workspace.value.sessionId, {
|
||||||
|
role: 'user',
|
||||||
|
content: draftMessage.value.trim(),
|
||||||
|
});
|
||||||
|
ElMessage.success('调试消息已写入会话');
|
||||||
|
draftMessage.value = '';
|
||||||
|
await loadWorkspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCost(tokens?: number) {
|
||||||
|
if (!tokens) {
|
||||||
|
return '¥0.000';
|
||||||
|
}
|
||||||
|
return `¥${(tokens / 100000).toFixed(3)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadWorkspace);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -11,38 +76,47 @@ import { chatMessages, citations, traceSteps } from '@/data/studioMock';
|
|||||||
<p class="studio-kicker">AgentWorkspaceView</p>
|
<p class="studio-kicker">AgentWorkspaceView</p>
|
||||||
<h1>Agent 对话调试</h1>
|
<h1>Agent 对话调试</h1>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary">发布 Agent</el-button>
|
<el-button type="primary">{{ workspace?.status || 'Draft' }}</el-button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="agent-layout">
|
<div class="agent-layout" v-loading="loading">
|
||||||
<section class="studio-panel chat-panel">
|
<section class="studio-panel chat-panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>售前问答 Agent</h2>
|
<h2>{{ workspace?.agentName || 'Agent 工作台' }}</h2>
|
||||||
<span>POST /api/agents/1001/runs</span>
|
<span>GET /api/agent-sessions/workspace</span>
|
||||||
</div>
|
</div>
|
||||||
<el-tag>Draft</el-tag>
|
<el-tag>{{ workspace?.sessionStatus || workspace?.status || 'DRAFT' }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-list">
|
<div class="message-list">
|
||||||
<article v-for="message in chatMessages" :key="message.content" :class="message.role">
|
<article
|
||||||
<strong>{{ message.role === 'user' ? '用户' : 'Agent' }}</strong>
|
v-for="message in workspace?.messages ?? []"
|
||||||
|
:key="message.id || message.content"
|
||||||
|
:class="message.role"
|
||||||
|
:data-test="`agent-message-${message.role}`"
|
||||||
|
>
|
||||||
|
<strong>{{ roleLabel(message.role) }}</strong>
|
||||||
<p>{{ message.content }}</p>
|
<p>{{ message.content }}</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-composer">
|
<div class="chat-composer">
|
||||||
<span>输入调试问题,运行会写入 agent_session / agent_message 草案</span>
|
<span>输入调试问题,写入当前 agent_session / agent_message</span>
|
||||||
<el-button type="primary"><el-icon><ChatDotRound /></el-icon> 发送</el-button>
|
<input v-model="draftMessage" data-test="agent-message-input" type="text" />
|
||||||
|
<el-button data-test="agent-send-message" type="primary" @click="sendMessage">
|
||||||
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
|
发送
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside class="studio-panel citation-panel">
|
<aside class="studio-panel citation-panel">
|
||||||
<div class="panel-heading compact">
|
<div class="panel-heading compact">
|
||||||
<h2>引用切片</h2>
|
<h2>引用切片</h2>
|
||||||
<span>3 个来源</span>
|
<span>{{ workspace?.citationCount || 0 }} 个来源</span>
|
||||||
</div>
|
</div>
|
||||||
<article v-for="citation in citations" :key="citation.title" class="citation-card">
|
<article v-for="citation in citations" :key="citation.key" class="citation-card" data-test="agent-citation-card">
|
||||||
<strong>{{ citation.title }}</strong>
|
<strong>{{ citation.title }}</strong>
|
||||||
<el-tag type="success">score {{ citation.score }}</el-tag>
|
<el-tag type="success">引用</el-tag>
|
||||||
<p>{{ citation.text }}</p>
|
<p>{{ citation.text }}</p>
|
||||||
</article>
|
</article>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -50,17 +124,17 @@ import { chatMessages, citations, traceSteps } from '@/data/studioMock';
|
|||||||
<aside class="studio-panel run-inspector">
|
<aside class="studio-panel run-inspector">
|
||||||
<div class="panel-heading compact">
|
<div class="panel-heading compact">
|
||||||
<h2>运行追踪</h2>
|
<h2>运行追踪</h2>
|
||||||
<span>modelRequestId: f4215d</span>
|
<span data-test="agent-latest-request-id">requestId: {{ workspace?.latestRequestId || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-mini">
|
<div class="metric-mini">
|
||||||
<span><el-icon><Timer /></el-icon> 1.42s</span>
|
<span><el-icon><Timer /></el-icon> {{ workspace?.sessionStatus || '-' }}</span>
|
||||||
<span><el-icon><Coin /></el-icon> ¥0.018</span>
|
<span><el-icon><Coin /></el-icon> {{ formatCost(workspace?.totalTokens) }}</span>
|
||||||
<span>1,248 tokens</span>
|
<span>{{ workspace?.totalTokens || 0 }} tokens</span>
|
||||||
</div>
|
</div>
|
||||||
<ol class="log-list">
|
<ol class="log-list">
|
||||||
<li v-for="step in traceSteps" :key="step.node">
|
<li v-if="latestAssistantMessage">
|
||||||
<time>{{ step.duration }}</time>
|
<time>{{ workspace?.sessionCode || '-' }}</time>
|
||||||
<span>{{ step.node }} · {{ step.output }}</span>
|
<span>{{ latestAssistantMessage.content }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,29 +1,71 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { UploadFilled } from '@element-plus/icons-vue';
|
import { UploadFilled } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { ingestionSteps } from '@/data/studioMock';
|
import type { IngestionRun } from '@/api/ingestion';
|
||||||
|
import { getIngestionRun } from '@/api/ingestion';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const runId = ref('run-20260601');
|
||||||
|
const storeId = ref('1001');
|
||||||
|
const documentId = ref('11');
|
||||||
|
const ingestionRun = ref<IngestionRun | null>(null);
|
||||||
|
|
||||||
|
const primaryFile = computed(() => ingestionRun.value?.files?.[0] ?? null);
|
||||||
|
|
||||||
|
function stepClass(status?: string | null) {
|
||||||
|
if (status === 'done') {
|
||||||
|
return 'is-done';
|
||||||
|
}
|
||||||
|
if (status === 'running') {
|
||||||
|
return 'is-running';
|
||||||
|
}
|
||||||
|
if (status === 'blocked') {
|
||||||
|
return 'is-blocked';
|
||||||
|
}
|
||||||
|
return 'is-idle';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRun() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getIngestionRun(runId.value, storeId.value, documentId.value);
|
||||||
|
ingestionRun.value = response.data;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadRun);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="studio-page ingestion-page">
|
<section class="studio-page ingestion-page">
|
||||||
<header class="page-title-row">
|
<header class="page-title-row">
|
||||||
<div>
|
<div>
|
||||||
<p class="studio-kicker">IngestionPipelineView</p>
|
<p class="studio-kicker">IngestionRunView</p>
|
||||||
<h1>文件解析管道</h1>
|
<h1>文件解析管道</h1>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary">启动索引任务</el-button>
|
<el-button type="primary">启动索引任务</el-button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="ingestion-layout">
|
<div class="ingestion-layout" v-loading="loading">
|
||||||
<section class="studio-panel upload-panel">
|
<section class="studio-panel upload-panel">
|
||||||
<div class="upload-dropzone">
|
<div class="upload-dropzone">
|
||||||
<el-icon><UploadFilled /></el-icon>
|
<el-icon><UploadFilled /></el-icon>
|
||||||
<strong>拖拽文件到这里</strong>
|
<strong>{{ primaryFile?.fileName || '拖拽文件到这里' }}</strong>
|
||||||
<span>支持 PDF / Word / Excel / Markdown / TXT,上传后自动创建 ingestion run。</span>
|
<span>
|
||||||
|
支持 PDF / Word / Excel / Markdown / TXT,上传后自动创建 ingestion run。
|
||||||
|
</span>
|
||||||
<el-button type="primary">选择文件</el-button>
|
<el-button type="primary">选择文件</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pipeline-timeline">
|
<div class="pipeline-timeline">
|
||||||
<article v-for="step in ingestionSteps" :key="step.name" :class="`is-${step.status}`">
|
<article
|
||||||
|
v-for="step in ingestionRun?.steps ?? []"
|
||||||
|
:key="step.name"
|
||||||
|
:class="stepClass(step.status)"
|
||||||
|
:data-test="`ingestion-step-${step.name}`"
|
||||||
|
>
|
||||||
<div class="timeline-dot" />
|
<div class="timeline-dot" />
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<strong>{{ step.name }}</strong>
|
<strong>{{ step.name }}</strong>
|
||||||
@@ -36,36 +78,40 @@ import { ingestionSteps } from '@/data/studioMock';
|
|||||||
<section class="studio-panel preview-panel">
|
<section class="studio-panel preview-panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h2>解析与切片预览</h2>
|
<h2>解析与切片预览</h2>
|
||||||
<span>GET /api/knowledge/ingestion-runs/run-20260531</span>
|
<span>GET /api/knowledge/ingestion-runs/{{ ingestionRun?.runId || runId }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-split">
|
<div class="preview-split">
|
||||||
<article>
|
<article>
|
||||||
<h3>解析文本</h3>
|
<h3>解析文本</h3>
|
||||||
<p>私有化部署章节应覆盖基础设施、网络、安全与运维边界。平台需说明模型服务商、知识库索引策略与日志留存周期...</p>
|
<p data-test="ingestion-parsed-preview">{{ ingestionRun?.parsedTextPreview || '暂无解析文本预览' }}</p>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<h3>切片 #24</h3>
|
<h3>切片预览</h3>
|
||||||
<p>chunk_size=800, overlap=120, strategy=FIXED_LENGTH。该切片将进入 rag_chunk 并在向量化后写入 rag_chunk_embedding。</p>
|
<p data-test="ingestion-chunk-preview">{{ ingestionRun?.chunkPreview || '暂无切片预览' }}</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div class="pipeline-controls">
|
<div class="pipeline-controls">
|
||||||
<label>切片策略 <strong>固定长度</strong></label>
|
<label>切片策略 <strong>{{ ingestionRun?.chunkStrategy ?? '-' }}</strong></label>
|
||||||
<label>Chunk Size <strong>800</strong></label>
|
<label>Chunk Size <strong>{{ ingestionRun?.chunkSize ?? '-' }}</strong></label>
|
||||||
<label>Overlap <strong>120</strong></label>
|
<label>Overlap <strong>{{ ingestionRun?.chunkOverlap ?? '-' }}</strong></label>
|
||||||
<label>Embedding <strong>Qwen3 1024d</strong></label>
|
<label>Embedding <strong>{{ ingestionRun?.embeddingModelId || '-' }} / {{ ingestionRun?.embeddingDimension || '-' }} 维</strong></label>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside class="studio-panel task-log-panel">
|
<aside class="studio-panel task-log-panel">
|
||||||
<div class="panel-heading compact">
|
<div class="panel-heading compact">
|
||||||
<h2>任务日志</h2>
|
<h2>任务日志</h2>
|
||||||
<span>run-20260531</span>
|
<span>{{ ingestionRun?.runId || runId }}</span>
|
||||||
</div>
|
</div>
|
||||||
<ol class="log-list">
|
<ol class="log-list">
|
||||||
<li><time>23:08:12</time><span>上传 4 个文件并创建 rag_document</span></li>
|
<li
|
||||||
<li><time>23:08:24</time><span>Tika 解析完成 3 个文件</span></li>
|
v-for="item in ingestionRun?.logs ?? []"
|
||||||
<li class="warn"><time>23:08:31</time><span>服务条款更新.md 编码检测失败,等待重试</span></li>
|
:key="`${item.time}-${item.message}`"
|
||||||
<li><time>23:08:40</time><span>切片任务进行中 68 / 119</span></li>
|
:class="{ warn: item.level === 'WARN' }"
|
||||||
|
:data-test="`ingestion-log-${item.time}`"
|
||||||
|
>
|
||||||
|
<time>{{ item.time }}</time><span>{{ item.message }}</span>
|
||||||
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,35 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { DataAnalysis, Document, Setting } from '@element-plus/icons-vue';
|
import { DataAnalysis, Document, Setting } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { knowledgeDocuments, knowledgeStores } from '@/data/studioMock';
|
import type { KnowledgeWorkspace } from '@/api/knowledgeWorkspace';
|
||||||
|
import { getKnowledgeWorkspace } from '@/api/knowledgeWorkspace';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const activeStoreId = ref('1001');
|
||||||
|
const workspace = ref<KnowledgeWorkspace | null>(null);
|
||||||
|
|
||||||
|
const stores = computed(() => (workspace.value ? [workspace.value] : []));
|
||||||
|
|
||||||
|
function statusLabel(status?: string | null) {
|
||||||
|
if (status === 'ENABLED' || status === '可检索') {
|
||||||
|
return '可检索';
|
||||||
|
}
|
||||||
|
return status || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWorkspace(storeId = activeStoreId.value) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getKnowledgeWorkspace(storeId);
|
||||||
|
workspace.value = response.data;
|
||||||
|
activeStoreId.value = response.data.storeId;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => loadWorkspace());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -14,61 +42,63 @@ import { knowledgeDocuments, knowledgeStores } from '@/data/studioMock';
|
|||||||
<el-button type="primary">新建知识库</el-button>
|
<el-button type="primary">新建知识库</el-button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="three-column-layout">
|
<div class="three-column-layout" v-loading="loading">
|
||||||
<aside class="studio-panel collection-rail">
|
<aside class="studio-panel collection-rail">
|
||||||
<div class="panel-heading compact">
|
<div class="panel-heading compact">
|
||||||
<h2>知识集合</h2>
|
<h2>知识集合</h2>
|
||||||
<span>{{ knowledgeStores.length }} 个库</span>
|
<span>{{ stores.length }} 个库</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-for="store in knowledgeStores"
|
v-for="store in stores"
|
||||||
:key="store.id"
|
:key="store.storeId"
|
||||||
class="collection-item"
|
class="collection-item"
|
||||||
:class="{ active: store.id === '1001' }"
|
:class="{ active: String(store.storeId) === String(activeStoreId) }"
|
||||||
|
:data-test="`knowledge-store-${store.storeId}`"
|
||||||
|
@click="loadWorkspace(String(store.storeId))"
|
||||||
>
|
>
|
||||||
<strong>{{ store.name }}</strong>
|
<strong>{{ store.storeName }}</strong>
|
||||||
<span>{{ store.docs }} 文档 · 健康度 {{ store.health }}%</span>
|
<span>{{ store.documentCount }} 文档 · 健康度 {{ store.healthScore }}%</span>
|
||||||
<em>{{ store.status }}</em>
|
<em>{{ statusLabel(store.status) }}</em>
|
||||||
</button>
|
</button>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="studio-panel knowledge-main">
|
<main class="studio-panel knowledge-main">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>产品制度库</h2>
|
<h2>{{ workspace?.storeName || '知识工作台' }}</h2>
|
||||||
<span>绑定旧数据语义:rag_store / rag_document / rag_chunk_embedding</span>
|
<span>绑定旧数据语义:rag_store / rag_document / rag_chunk_embedding</span>
|
||||||
</div>
|
</div>
|
||||||
<el-tag type="success">可检索</el-tag>
|
<el-tag type="success">{{ statusLabel(workspace?.status) }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-grid">
|
<div class="config-grid">
|
||||||
<article>
|
<article>
|
||||||
<el-icon><Setting /></el-icon>
|
<el-icon><Setting /></el-icon>
|
||||||
<strong>Embedding 模型</strong>
|
<strong>Embedding 模型</strong>
|
||||||
<span>Qwen3-Embedding · 1024 维</span>
|
<span>{{ workspace?.embeddingModelId || '-' }} · {{ workspace?.embeddingDimension || '-' }} 维</span>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<el-icon><DataAnalysis /></el-icon>
|
<el-icon><DataAnalysis /></el-icon>
|
||||||
<strong>检索配置</strong>
|
<strong>检索配置</strong>
|
||||||
<span>TopK 6 · Score ≥ 0.72 · Rerank 关闭</span>
|
<span>切片策略 {{ workspace?.chunkStrategy || '-' }} · Chunk {{ workspace?.chunkSize || '-' }}</span>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<el-icon><Document /></el-icon>
|
<el-icon><Document /></el-icon>
|
||||||
<strong>索引版本</strong>
|
<strong>索引版本</strong>
|
||||||
<span>index_version 14 · Draft 快照</span>
|
<span>index_version {{ workspace?.indexVersion || '-' }}</span>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="document-table">
|
<div class="document-table">
|
||||||
<div class="table-row table-head">
|
<div class="table-row table-head">
|
||||||
<span>文档</span><span>解析</span><span>索引</span><span>切片</span><span>更新</span>
|
<span>文档</span><span>解析</span><span>索引</span><span>启用</span><span>更新</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="doc in knowledgeDocuments" :key="doc.id" class="table-row">
|
<div v-for="doc in workspace?.documents ?? []" :key="doc.documentId" class="table-row" :data-test="`knowledge-document-${doc.documentId}`">
|
||||||
<strong>{{ doc.name }}</strong>
|
<strong>{{ doc.documentTitle }}</strong>
|
||||||
<span>{{ doc.parseStatus }}</span>
|
<span>{{ doc.parseStatus }}</span>
|
||||||
<span>{{ doc.indexStatus }}</span>
|
<span>{{ doc.indexStatus }}</span>
|
||||||
<span>{{ doc.chunks }}</span>
|
<span>{{ doc.enabled ? '是' : '否' }}</span>
|
||||||
<span>{{ doc.updatedAt }}</span>
|
<span>{{ doc.updateTime || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -80,13 +110,13 @@ import { knowledgeDocuments, knowledgeStores } from '@/data/studioMock';
|
|||||||
</div>
|
</div>
|
||||||
<dl class="inspector-list">
|
<dl class="inspector-list">
|
||||||
<dt>Workspace API</dt>
|
<dt>Workspace API</dt>
|
||||||
<dd>GET /api/knowledge/workspaces/1001</dd>
|
<dd>GET /api/knowledge/workspaces/{{ workspace?.storeId || '-' }}</dd>
|
||||||
<dt>文档健康度</dt>
|
<dt>文档健康度</dt>
|
||||||
<dd>96% · 1 个解析失败</dd>
|
<dd>{{ workspace?.healthScore || 0 }}% · {{ workspace?.parseFailedDocumentCount || 0 }} 个解析失败</dd>
|
||||||
<dt>待处理任务</dt>
|
<dt>待处理任务</dt>
|
||||||
<dd>2 个文档等待向量化</dd>
|
<dd>{{ workspace?.pendingTaskCount || 0 }} 个文档等待处理</dd>
|
||||||
<dt>发布影响</dt>
|
<dt>发布影响</dt>
|
||||||
<dd>更新后需要 Workflow 重新验证引用质量</dd>
|
<dd>{{ workspace?.publishImpact || '-' }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { modelRoutes } from '@/data/studioMock';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import type { ModelWorkspace } from '@/api/modelWorkspace';
|
||||||
|
import { getModelWorkspace } from '@/api/modelWorkspace';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const workspace = ref<ModelWorkspace | null>(null);
|
||||||
|
|
||||||
|
function routeStatus(enabled?: boolean) {
|
||||||
|
return enabled ? '启用' : '草稿';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWorkspace() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getModelWorkspace();
|
||||||
|
workspace.value = response.data;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadWorkspace);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -12,23 +34,28 @@ import { modelRoutes } from '@/data/studioMock';
|
|||||||
<el-button type="primary">新增路由</el-button>
|
<el-button type="primary">新增路由</el-button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="studio-panel model-panel">
|
<div class="studio-panel model-panel" v-loading="loading">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h2>任务路由规则</h2>
|
<h2>任务路由规则</h2>
|
||||||
<span>保留 model_provider / model_config / model_route_rule 语义</span>
|
<span>保留 model_provider / model_config / model_route_rule 语义</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<div><strong>{{ workspace?.providerCount || 0 }}</strong><span>服务商</span></div>
|
||||||
|
<div><strong>{{ workspace?.modelCount || 0 }}</strong><span>模型</span></div>
|
||||||
|
<div><strong>{{ workspace?.routeRuleCount || 0 }}</strong><span>路由规则</span></div>
|
||||||
|
<div><strong>{{ workspace?.recentFailedCallCount || 0 }}</strong><span>最近失败</span></div>
|
||||||
|
</div>
|
||||||
<div class="document-table">
|
<div class="document-table">
|
||||||
<div class="table-row table-head">
|
<div class="table-row table-head">
|
||||||
<span>任务</span><span>主模型</span><span>Fallback</span><span>最大延迟</span><span>状态</span>
|
<span>任务</span><span>主模型</span><span>Fallback</span><span>状态</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="route in modelRoutes" :key="route.task" class="table-row">
|
<div v-for="route in workspace?.routes ?? []" :key="route.id || route.taskType" class="table-row" :data-test="`model-route-${route.taskType}`">
|
||||||
<strong>{{ route.task }}</strong>
|
<strong>{{ route.taskType }}</strong>
|
||||||
<span>{{ route.primary }}</span>
|
<span>{{ route.primaryModelCode || route.primaryModelId }}</span>
|
||||||
<span>{{ route.fallback }}</span>
|
<span>{{ route.fallbackModelCode || route.fallbackModelId || '无' }}</span>
|
||||||
<span>{{ route.latency }}</span>
|
|
||||||
<span class="status-cell">
|
<span class="status-cell">
|
||||||
<span class="status-pill" :class="route.status === '启用' ? 'is-success' : 'is-warning'">
|
<span class="status-pill" :class="routeStatus(route.enabled) === '启用' ? 'is-success' : 'is-warning'">
|
||||||
{{ route.status }}
|
{{ routeStatus(route.enabled) }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,43 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { ArrowRight, Check, Warning } from '@element-plus/icons-vue';
|
import { ArrowRight, Check, Warning } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMock';
|
import type { StudioDashboard } from '@/api/studioDashboard';
|
||||||
|
import { getStudioDashboard } from '@/api/studioDashboard';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const dashboard = ref<StudioDashboard | null>(null);
|
||||||
|
|
||||||
|
const metrics = computed(() => dashboard.value?.metrics);
|
||||||
|
|
||||||
|
function formatPublishStatus(status?: string | null) {
|
||||||
|
if (status === 'PUBLISHED') {
|
||||||
|
return 'Published';
|
||||||
|
}
|
||||||
|
if (status === 'DRAFT') {
|
||||||
|
return 'Draft';
|
||||||
|
}
|
||||||
|
return status || 'Draft';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDashboard() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getStudioDashboard();
|
||||||
|
dashboard.value = response.data;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadDashboard);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="studio-page dashboard-page">
|
<section class="studio-page dashboard-page" v-loading="loading">
|
||||||
<header class="studio-hero">
|
<header class="studio-hero">
|
||||||
<div>
|
<div>
|
||||||
<p class="studio-kicker">项目 / Common Agent Studio</p>
|
<p class="studio-kicker">项目 / {{ dashboard?.projectName || 'Common Agent Studio' }}</p>
|
||||||
<h1>从知识接入到 Agent 发布的一体化工作台</h1>
|
<h1>从知识接入到 Agent 发布的一体化工作台</h1>
|
||||||
<p>
|
<p>
|
||||||
使用新的聚合 ViewModel 驱动原型:知识资产、Workflow、MCP、Skill、Agent 调试与观测都围绕一次发布旅程组织。
|
使用新的聚合 ViewModel 驱动原型:知识资产、Workflow、MCP、Skill、Agent 调试与观测都围绕一次发布旅程组织。
|
||||||
@@ -21,13 +50,19 @@ import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMoc
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="lifecycle-strip">
|
<div class="lifecycle-strip">
|
||||||
<article v-for="(step, index) in lifecycleSteps" :key="step.name" class="lifecycle-step" :class="`is-${step.status}`">
|
<article
|
||||||
|
v-for="(step, index) in dashboard?.lifecycleSteps ?? []"
|
||||||
|
:key="step.name"
|
||||||
|
class="lifecycle-step"
|
||||||
|
:class="`is-${step.status}`"
|
||||||
|
:data-test="`dashboard-lifecycle-${step.name}`"
|
||||||
|
>
|
||||||
<div class="step-index">{{ index + 1 }}</div>
|
<div class="step-index">{{ index + 1 }}</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ step.name }}</strong>
|
<strong>{{ step.name }}</strong>
|
||||||
<span>{{ step.description }}</span>
|
<span>{{ step.description }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-icon v-if="index < lifecycleSteps.length - 1"><ArrowRight /></el-icon>
|
<el-icon v-if="dashboard?.lifecycleSteps && index < dashboard.lifecycleSteps.length - 1"><ArrowRight /></el-icon>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -38,10 +73,15 @@ import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMoc
|
|||||||
<h2>发布就绪检查</h2>
|
<h2>发布就绪检查</h2>
|
||||||
<span>ViewModel: StudioDashboardView</span>
|
<span>ViewModel: StudioDashboardView</span>
|
||||||
</div>
|
</div>
|
||||||
<el-tag type="warning">Draft</el-tag>
|
<el-tag type="warning">{{ formatPublishStatus(dashboard?.publishStatus) }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<ul class="check-list">
|
<ul class="check-list">
|
||||||
<li v-for="item in readinessChecklist" :key="item.label" :class="{ done: item.done }">
|
<li
|
||||||
|
v-for="item in dashboard?.readinessChecklist ?? []"
|
||||||
|
:key="item.label"
|
||||||
|
:class="{ done: item.done }"
|
||||||
|
:data-test="`dashboard-check-${item.label}`"
|
||||||
|
>
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Check v-if="item.done" />
|
<Check v-if="item.done" />
|
||||||
<span v-else class="pending-dot" />
|
<span v-else class="pending-dot" />
|
||||||
@@ -54,13 +94,13 @@ import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMoc
|
|||||||
<section class="studio-panel metrics-panel">
|
<section class="studio-panel metrics-panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h2>运行概览</h2>
|
<h2>运行概览</h2>
|
||||||
<span>环境: Dev</span>
|
<span>环境: {{ dashboard?.environment || 'Dev' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-row">
|
<div class="metric-row">
|
||||||
<div><strong>27</strong><span>今日运行</span></div>
|
<div><strong>{{ metrics?.todayRunCount ?? 0 }}</strong><span>今日运行</span></div>
|
||||||
<div><strong>96.4%</strong><span>成功率</span></div>
|
<div><strong>{{ metrics?.successRate ?? 0 }}%</strong><span>成功率</span></div>
|
||||||
<div><strong>1.28s</strong><span>P50 延迟</span></div>
|
<div><strong>{{ metrics?.p50Latency || '-' }}</strong><span>P50 延迟</span></div>
|
||||||
<div><strong>¥4.82</strong><span>预估成本</span></div>
|
<div><strong>{{ metrics?.estimatedCost || '-' }}</strong><span>预估成本</span></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -73,7 +113,12 @@ import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMoc
|
|||||||
<div class="run-row run-head">
|
<div class="run-row run-head">
|
||||||
<span>名称</span><span>类型</span><span>状态</span><span>延迟</span><span>成本</span>
|
<span>名称</span><span>类型</span><span>状态</span><span>延迟</span><span>成本</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="run in recentRuns" :key="run.id" class="run-row">
|
<div
|
||||||
|
v-for="run in dashboard?.recentRuns ?? []"
|
||||||
|
:key="run.id"
|
||||||
|
class="run-row"
|
||||||
|
:data-test="`dashboard-run-${run.id}`"
|
||||||
|
>
|
||||||
<strong>{{ run.name }}</strong>
|
<strong>{{ run.name }}</strong>
|
||||||
<span>{{ run.type }}</span>
|
<span>{{ run.type }}</span>
|
||||||
<span class="status-cell">
|
<span class="status-cell">
|
||||||
@@ -90,8 +135,8 @@ import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMoc
|
|||||||
<section class="studio-panel warning-panel">
|
<section class="studio-panel warning-panel">
|
||||||
<el-icon><Warning /></el-icon>
|
<el-icon><Warning /></el-icon>
|
||||||
<div>
|
<div>
|
||||||
<h2>生产发布前仍需确认路由兜底</h2>
|
<h2>{{ dashboard?.warningTitle || '生产发布前仍需确认路由兜底' }}</h2>
|
||||||
<p>AGENT_PLAN 任务当前只有草稿路由,建议补齐 fallback 模型和最大延迟阈值。</p>
|
<p>{{ dashboard?.warningMessage || 'AGENT_PLAN 任务当前只有草稿路由,建议补齐 fallback 模型和最大延迟阈值。' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,27 +1,83 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { Connection, Cpu, VideoPlay } from '@element-plus/icons-vue';
|
import { Connection, Cpu, VideoPlay } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { traceSteps, workflowEdges, workflowNodes } from '@/data/studioMock';
|
import type { WorkflowWorkspace } from '@/api/workflow';
|
||||||
|
import { getWorkflowWorkspace } from '@/api/workflow';
|
||||||
|
|
||||||
const nodeById = Object.fromEntries(workflowNodes.map((node) => [node.id, node]));
|
const loading = ref(false);
|
||||||
const canvasEdges = workflowEdges.flatMap((edge) => {
|
const projectId = ref('101');
|
||||||
|
const workflowId = ref('201');
|
||||||
|
const workspace = ref<WorkflowWorkspace | null>(null);
|
||||||
|
|
||||||
|
const nodeLibrary = ['Start', 'LLM', 'Knowledge Retrieval', 'MCP Tool', 'Skill', 'Condition', 'Answer'];
|
||||||
|
|
||||||
|
const workflowNodes = computed(() => {
|
||||||
|
const graphJson = workspace.value?.versions?.[0]?.graphJson;
|
||||||
|
if (!graphJson) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(graphJson) as { nodes?: Array<Record<string, unknown>> };
|
||||||
|
return (parsed.nodes ?? []).map((node, index) => ({
|
||||||
|
id: String(node.id ?? `node-${index}`),
|
||||||
|
type: String(node.type ?? 'NODE'),
|
||||||
|
label: String(node.label ?? node.id ?? `Node ${index + 1}`),
|
||||||
|
description: String(node.description ?? node.prompt ?? ''),
|
||||||
|
x: Number(node.x ?? 10 + index * 18),
|
||||||
|
y: Number(node.y ?? 42),
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflowEdges = computed(() => {
|
||||||
|
const graphJson = workspace.value?.versions?.[0]?.graphJson;
|
||||||
|
if (!graphJson) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(graphJson) as { edges?: Array<Record<string, unknown>> };
|
||||||
|
return (parsed.edges ?? []).map((edge, index) => ({
|
||||||
|
id: `edge-${index}`,
|
||||||
|
from: String(edge.from ?? edge.source ?? ''),
|
||||||
|
to: String(edge.to ?? edge.target ?? ''),
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const canvasEdges = computed(() => {
|
||||||
|
const nodeById = Object.fromEntries(workflowNodes.value.map((node) => [node.id, node]));
|
||||||
|
return workflowEdges.value.flatMap((edge) => {
|
||||||
const from = nodeById[edge.from];
|
const from = nodeById[edge.from];
|
||||||
const to = nodeById[edge.to];
|
const to = nodeById[edge.to];
|
||||||
|
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
return [{
|
||||||
return [
|
id: edge.id,
|
||||||
{
|
|
||||||
id: `${edge.from}-${edge.to}`,
|
|
||||||
x1: from.x + 5,
|
x1: from.x + 5,
|
||||||
y1: from.y + 4,
|
y1: from.y + 4,
|
||||||
x2: to.x,
|
x2: to.x,
|
||||||
y2: to.y + 4,
|
y2: to.y + 4,
|
||||||
},
|
}];
|
||||||
];
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadWorkspace() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getWorkflowWorkspace(projectId.value, workflowId.value);
|
||||||
|
workspace.value = response.data;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadWorkspace);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -37,26 +93,20 @@ const canvasEdges = workflowEdges.flatMap((edge) => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="workflow-layout">
|
<div class="workflow-layout" v-loading="loading">
|
||||||
<aside class="studio-panel node-library">
|
<aside class="studio-panel node-library">
|
||||||
<div class="panel-heading compact">
|
<div class="panel-heading compact">
|
||||||
<h2>节点库</h2>
|
<h2>节点库</h2>
|
||||||
<span>JSON Graph</span>
|
<span>JSON Graph</span>
|
||||||
</div>
|
</div>
|
||||||
<button>Start</button>
|
<button v-for="node in nodeLibrary" :key="node">{{ node }}</button>
|
||||||
<button>LLM</button>
|
|
||||||
<button>Knowledge Retrieval</button>
|
|
||||||
<button>MCP Tool</button>
|
|
||||||
<button>Skill</button>
|
|
||||||
<button>Condition</button>
|
|
||||||
<button>Answer</button>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="studio-panel workflow-canvas">
|
<main class="studio-panel workflow-canvas">
|
||||||
<div class="canvas-toolbar">
|
<div class="canvas-toolbar">
|
||||||
<span><el-icon><Connection /></el-icon> workflow-support-rag</span>
|
<span><el-icon><Connection /></el-icon> {{ workspace?.workflowCode || 'workflow' }}</span>
|
||||||
<span>版本快照 v7</span>
|
<span>版本快照 v{{ workspace?.currentPublishedVersionNo || workspace?.versions?.[0]?.versionNo || '-' }}</span>
|
||||||
<span>环境: Dev</span>
|
<span>环境: {{ workspace?.environment || 'Dev' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="canvas-surface">
|
<div class="canvas-surface">
|
||||||
<svg class="edge-layer" viewBox="0 0 100 100" preserveAspectRatio="none">
|
<svg class="edge-layer" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||||
@@ -73,8 +123,9 @@ const canvasEdges = workflowEdges.flatMap((edge) => {
|
|||||||
v-for="node in workflowNodes"
|
v-for="node in workflowNodes"
|
||||||
:key="node.id"
|
:key="node.id"
|
||||||
class="workflow-node"
|
class="workflow-node"
|
||||||
:class="{ selected: node.id === 'llm' }"
|
:class="{ selected: node.type === 'LLM' }"
|
||||||
:style="{ left: `${node.x}%`, top: `${node.y}%` }"
|
:style="{ left: `${node.x}%`, top: `${node.y}%` }"
|
||||||
|
:data-test="`workflow-node-${node.id}`"
|
||||||
>
|
>
|
||||||
<span>{{ node.type }}</span>
|
<span>{{ node.type }}</span>
|
||||||
<strong>{{ node.label }}</strong>
|
<strong>{{ node.label }}</strong>
|
||||||
@@ -83,9 +134,9 @@ const canvasEdges = workflowEdges.flatMap((edge) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="run-trace-drawer">
|
<div class="run-trace-drawer">
|
||||||
<strong>Run Trace</strong>
|
<strong>Run Trace</strong>
|
||||||
<div v-for="step in traceSteps" :key="step.node">
|
<div v-for="run in workspace?.recentRuns ?? []" :key="run.requestId" :data-test="`workflow-run-${run.requestId}`">
|
||||||
<span>{{ step.node }}</span>
|
<span>{{ run.requestId }}</span>
|
||||||
<em>{{ step.status }} · {{ step.duration }} · {{ step.output }}</em>
|
<em>{{ run.status }} · {{ run.durationMs || 0 }}ms · {{ run.outputJson || '-' }}</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -93,17 +144,17 @@ const canvasEdges = workflowEdges.flatMap((edge) => {
|
|||||||
<aside class="studio-panel inspector-panel">
|
<aside class="studio-panel inspector-panel">
|
||||||
<div class="panel-heading compact">
|
<div class="panel-heading compact">
|
||||||
<h2>节点 Inspector</h2>
|
<h2>节点 Inspector</h2>
|
||||||
<span>LLM</span>
|
<span>{{ workflowNodes[0]?.type || 'LLM' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<dl class="inspector-list">
|
<dl class="inspector-list">
|
||||||
<dt>任务类型</dt>
|
<dt>工作流</dt>
|
||||||
<dd>RAG_ANSWER</dd>
|
<dd>{{ workspace?.workflowName || '-' }}</dd>
|
||||||
<dt>输入 Schema</dt>
|
<dt>发布状态</dt>
|
||||||
<dd>question, retrieved_chunks, conversation</dd>
|
<dd>{{ workspace?.publishStatus || '-' }}</dd>
|
||||||
<dt>输出 Schema</dt>
|
<dt>当前版本</dt>
|
||||||
<dd>answer, citations, safety_flags</dd>
|
<dd>v{{ workspace?.currentPublishedVersionNo || workspace?.versions?.[0]?.versionNo || '-' }}</dd>
|
||||||
<dt>路由策略</dt>
|
<dt>最近请求</dt>
|
||||||
<dd>primary qwen-plus / fallback deepseek-v3</dd>
|
<dd>{{ workspace?.latestRequestId || '-' }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<button class="blue-command"><el-icon><Cpu /></el-icon> 打开模型路由</button>
|
<button class="blue-command"><el-icon><Cpu /></el-icon> 打开模型路由</button>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { flushPromises, mount } from '@vue/test-utils';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import AgentWorkspacePage from '../AgentWorkspacePage.vue';
|
||||||
|
import { appendAgentMessage, getAgentWorkspace } from '@/api/agent';
|
||||||
|
|
||||||
|
vi.mock('@/api/agent', () => ({
|
||||||
|
getAgentWorkspace: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
resultcode: '0',
|
||||||
|
message: null,
|
||||||
|
data: {
|
||||||
|
agentId: '1001',
|
||||||
|
agentCode: 'sales_agent',
|
||||||
|
agentName: '售前问答 Agent',
|
||||||
|
storeId: '2001',
|
||||||
|
status: 'DRAFT',
|
||||||
|
sessionId: '3001',
|
||||||
|
sessionCode: 'session-001',
|
||||||
|
sessionStatus: 'ACTIVE',
|
||||||
|
totalTokens: 1248,
|
||||||
|
citationCount: 1,
|
||||||
|
latestRequestId: 'req-1001',
|
||||||
|
sessions: [],
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
sessionId: '3001',
|
||||||
|
role: 'user',
|
||||||
|
content: '如果客户要求私有化部署,需要说明哪些内容?',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
sessionId: '3001',
|
||||||
|
role: 'assistant',
|
||||||
|
content: '建议说明部署拓扑、权限边界和日志留存策略。',
|
||||||
|
citationJson: '[{"chunkId":"c-1"}]',
|
||||||
|
tokenCount: 612,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
appendAgentMessage: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||||
|
listAgents: vi.fn(),
|
||||||
|
queryAgents: vi.fn(),
|
||||||
|
getAgentById: vi.fn(),
|
||||||
|
saveAgent: vi.fn(),
|
||||||
|
deleteAgent: vi.fn(),
|
||||||
|
chatWithAgent: vi.fn(),
|
||||||
|
createAgentSession: vi.fn(),
|
||||||
|
getAgentSessionById: vi.fn(),
|
||||||
|
listAgentMessages: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('AgentWorkspacePage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads workspace messages and citations from backend api', async () => {
|
||||||
|
const wrapper = mount(AgentWorkspacePage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getAgentWorkspace).toHaveBeenCalledWith('1001');
|
||||||
|
expect(wrapper.text()).toContain('售前问答 Agent');
|
||||||
|
expect(wrapper.text()).toContain('建议说明部署拓扑');
|
||||||
|
expect(wrapper.findAll('[data-test="agent-citation-card"]').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('appends debug message through backend api', async () => {
|
||||||
|
const wrapper = mount(AgentWorkspacePage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
await wrapper.get('[data-test="agent-message-input"]').setValue('补充说明日志留存周期');
|
||||||
|
await wrapper.get('[data-test="agent-send-message"]').trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(appendAgentMessage).toHaveBeenCalledWith('3001', {
|
||||||
|
role: 'user',
|
||||||
|
content: '补充说明日志留存周期',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { flushPromises, mount } from '@vue/test-utils';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import IngestionPipelinePage from '../IngestionPipelinePage.vue';
|
||||||
|
import { getIngestionRun } from '@/api/ingestion';
|
||||||
|
|
||||||
|
vi.mock('@/api/ingestion', () => ({
|
||||||
|
getIngestionRun: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
resultcode: '0',
|
||||||
|
message: null,
|
||||||
|
data: {
|
||||||
|
runId: 'run-20260601',
|
||||||
|
storeId: '1001',
|
||||||
|
documentId: '11',
|
||||||
|
storeName: '产品制度库',
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
documentId: '11',
|
||||||
|
fileName: '售前方案模板.pdf',
|
||||||
|
parseStatus: 'PARSED',
|
||||||
|
indexStatus: 'INDEXED',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
steps: [
|
||||||
|
{ name: '上传', description: '文件已入库并创建 rag_document', status: 'done' },
|
||||||
|
{ name: '解析', description: '解析完成,文本长度 1280,页数 12', status: 'done' },
|
||||||
|
{ name: '切片', description: '已生成 24 个切片,chunk_size=800,overlap=120', status: 'running' },
|
||||||
|
],
|
||||||
|
parsedTextPreview: '私有化部署章节应覆盖基础设施、网络、安全与运维边界。',
|
||||||
|
chunkPreview: 'chunk_size=800, overlap=120, strategy=1。预览:该切片将进入 rag_chunk 并在向量化后写入 rag_chunk_embedding。',
|
||||||
|
chunkStrategy: 1,
|
||||||
|
chunkSize: 800,
|
||||||
|
chunkOverlap: 120,
|
||||||
|
embeddingModelId: '88',
|
||||||
|
embeddingDimension: 1024,
|
||||||
|
logs: [
|
||||||
|
{ time: '23:08:12', level: 'INFO', message: '上传文件并创建 rag_document 记录' },
|
||||||
|
{ time: '23:08:24', level: 'INFO', message: '解析完成,文本长度 1280,页数 12' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('IngestionPipelinePage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads ingestion aggregate from backend api', async () => {
|
||||||
|
const wrapper = mount(IngestionPipelinePage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getIngestionRun).toHaveBeenCalledWith('run-20260601', '1001', '11');
|
||||||
|
expect(wrapper.text()).toContain('售前方案模板.pdf');
|
||||||
|
expect(wrapper.text()).toContain('私有化部署章节应覆盖基础设施');
|
||||||
|
expect(wrapper.text()).toContain('上传文件并创建 rag_document 记录');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { flushPromises, mount } from '@vue/test-utils';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import KnowledgeWorkspacePage from '../KnowledgeWorkspacePage.vue';
|
||||||
|
import { getKnowledgeWorkspace } from '@/api/knowledgeWorkspace';
|
||||||
|
|
||||||
|
vi.mock('@/api/knowledgeWorkspace', () => ({
|
||||||
|
getKnowledgeWorkspace: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
resultcode: '0',
|
||||||
|
message: null,
|
||||||
|
data: {
|
||||||
|
storeId: '1001',
|
||||||
|
storeCode: 'PROD_DOC',
|
||||||
|
storeName: '产品制度库',
|
||||||
|
status: 'ENABLED',
|
||||||
|
documentCount: 9,
|
||||||
|
parsedDocumentCount: 6,
|
||||||
|
parseFailedDocumentCount: 1,
|
||||||
|
indexedDocumentCount: 5,
|
||||||
|
pendingTaskCount: 2,
|
||||||
|
healthScore: 96,
|
||||||
|
embeddingModelId: '88',
|
||||||
|
embeddingDimension: 1024,
|
||||||
|
chunkStrategy: 1,
|
||||||
|
chunkSize: 800,
|
||||||
|
indexVersion: 14,
|
||||||
|
publishImpact: '更新后需要 Workflow 重新验证引用质量',
|
||||||
|
documents: [
|
||||||
|
{
|
||||||
|
documentId: '11',
|
||||||
|
documentTitle: '售前方案模板.pdf',
|
||||||
|
parseStatus: 'PARSED',
|
||||||
|
indexStatus: 'INDEXED',
|
||||||
|
enabled: true,
|
||||||
|
updateTime: '2026-06-01 10:00:00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('KnowledgeWorkspacePage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads knowledge workspace aggregate from backend api', async () => {
|
||||||
|
const wrapper = mount(KnowledgeWorkspacePage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getKnowledgeWorkspace).toHaveBeenCalledWith('1001');
|
||||||
|
expect(wrapper.text()).toContain('产品制度库');
|
||||||
|
expect(wrapper.text()).toContain('96%');
|
||||||
|
expect(wrapper.find('[data-test="knowledge-document-11"]').text()).toContain('售前方案模板.pdf');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { flushPromises, mount } from '@vue/test-utils';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import ModelWorkspacePage from '../ModelWorkspacePage.vue';
|
||||||
|
import { getModelWorkspace } from '@/api/modelWorkspace';
|
||||||
|
|
||||||
|
vi.mock('@/api/modelWorkspace', () => ({
|
||||||
|
getModelWorkspace: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
resultcode: '0',
|
||||||
|
message: null,
|
||||||
|
data: {
|
||||||
|
providerCount: 3,
|
||||||
|
healthyProviderCount: 2,
|
||||||
|
unhealthyProviderCount: 1,
|
||||||
|
modelCount: 5,
|
||||||
|
enabledModelCount: 4,
|
||||||
|
routeRuleCount: 2,
|
||||||
|
enabledRouteRuleCount: 1,
|
||||||
|
recentFailedCallCount: 2,
|
||||||
|
providers: [],
|
||||||
|
models: [],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: '11',
|
||||||
|
taskType: 'RAG_ANSWER',
|
||||||
|
primaryModelCode: 'qwen-plus',
|
||||||
|
fallbackModelCode: 'deepseek-v3',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
failedCallSummaries: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ModelWorkspacePage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads model workspace aggregate from backend api', async () => {
|
||||||
|
const wrapper = mount(ModelWorkspacePage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getModelWorkspace).toHaveBeenCalled();
|
||||||
|
expect(wrapper.text()).toContain('3');
|
||||||
|
expect(wrapper.find('[data-test="model-route-RAG_ANSWER"]').text()).toContain('qwen-plus');
|
||||||
|
expect(wrapper.find('[data-test="model-route-RAG_ANSWER"]').text()).toContain('deepseek-v3');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { flushPromises, mount } from '@vue/test-utils';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import StudioDashboardPage from '../StudioDashboardPage.vue';
|
||||||
|
import { getStudioDashboard } from '@/api/studioDashboard';
|
||||||
|
|
||||||
|
vi.mock('@/api/studioDashboard', () => ({
|
||||||
|
getStudioDashboard: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
resultcode: '0',
|
||||||
|
message: null,
|
||||||
|
data: {
|
||||||
|
projectName: 'Common Agent Studio',
|
||||||
|
environment: 'Dev',
|
||||||
|
publishStatus: 'DRAFT',
|
||||||
|
lifecycleSteps: [
|
||||||
|
{ name: '知识接入', description: '上传、解析、切片、向量化', status: 'done' },
|
||||||
|
{ name: '能力编排', description: 'Workflow 连接模型、工具与 Skill', status: 'running' },
|
||||||
|
],
|
||||||
|
readinessChecklist: [
|
||||||
|
{ label: '知识库已绑定 Embedding 模型', done: true },
|
||||||
|
{ label: 'Workflow 已存在可编辑草稿', done: false },
|
||||||
|
],
|
||||||
|
metrics: {
|
||||||
|
todayRunCount: 27,
|
||||||
|
successRate: 96.4,
|
||||||
|
p50Latency: '1.28s',
|
||||||
|
estimatedCost: '¥4.82',
|
||||||
|
},
|
||||||
|
recentRuns: [
|
||||||
|
{ id: 'req-1001', name: '售前问答 Agent', type: 'Agent', status: '成功', latency: '1.42s', cost: '¥0.018' },
|
||||||
|
],
|
||||||
|
warningTitle: '生产发布前仍需确认路由兜底',
|
||||||
|
warningMessage: 'AGENT_PLAN 任务建议补齐 fallback 模型和最大延迟阈值后再发布。',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('StudioDashboardPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads dashboard aggregate from backend api', async () => {
|
||||||
|
const wrapper = mount(StudioDashboardPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getStudioDashboard).toHaveBeenCalled();
|
||||||
|
expect(wrapper.text()).toContain('Common Agent Studio');
|
||||||
|
expect(wrapper.text()).toContain('96.4%');
|
||||||
|
expect(wrapper.text()).toContain('售前问答 Agent');
|
||||||
|
expect(wrapper.text()).toContain('AGENT_PLAN 任务建议补齐 fallback 模型');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { flushPromises, mount } from '@vue/test-utils';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import WorkflowBuilderPage from '../WorkflowBuilderPage.vue';
|
||||||
|
import { getWorkflowWorkspace } from '@/api/workflow';
|
||||||
|
|
||||||
|
vi.mock('@/api/workflow', () => ({
|
||||||
|
getWorkflowWorkspace: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
resultcode: '0',
|
||||||
|
message: null,
|
||||||
|
data: {
|
||||||
|
projectId: '101',
|
||||||
|
projectCode: 'studio_demo',
|
||||||
|
projectName: '演示项目',
|
||||||
|
environment: 'DEV',
|
||||||
|
publishStatus: 'PUBLISHED',
|
||||||
|
workflowId: '201',
|
||||||
|
workflowCode: 'workflow-support-rag',
|
||||||
|
workflowName: '知识问答流程',
|
||||||
|
workflowStatus: 'ENABLED',
|
||||||
|
currentPublishedVersionNo: 7,
|
||||||
|
latestRequestId: 'req-2001',
|
||||||
|
latestDurationMs: 860,
|
||||||
|
workflows: [],
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
id: '301',
|
||||||
|
workflowId: '201',
|
||||||
|
versionNo: 7,
|
||||||
|
snapshotName: '知识问答发布版',
|
||||||
|
graphJson:
|
||||||
|
'{"nodes":[{"id":"start","type":"START","label":"Start","x":4,"y":42},{"id":"llm","type":"LLM","label":"LLM","x":47,"y":42}],"edges":[{"from":"start","to":"llm"}]}',
|
||||||
|
publishStatus: 'PUBLISHED',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
recentRuns: [
|
||||||
|
{
|
||||||
|
id: '401',
|
||||||
|
workflowId: '201',
|
||||||
|
workflowVersionId: '301',
|
||||||
|
requestId: 'req-2001',
|
||||||
|
status: 'SUCCESS',
|
||||||
|
durationMs: 860,
|
||||||
|
outputJson: '{"answer":"done"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
listWorkflowDefinitions: vi.fn(),
|
||||||
|
getWorkflowDefinition: vi.fn(),
|
||||||
|
saveWorkflowDefinition: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('WorkflowBuilderPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads workflow workspace and renders graph nodes from backend api', async () => {
|
||||||
|
const wrapper = mount(WorkflowBuilderPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getWorkflowWorkspace).toHaveBeenCalledWith('101', '201');
|
||||||
|
expect(wrapper.text()).toContain('workflow-support-rag');
|
||||||
|
expect(wrapper.find('[data-test="workflow-node-start"]').text()).toContain('Start');
|
||||||
|
expect(wrapper.find('[data-test="workflow-node-llm"]').text()).toContain('LLM');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders recent run trace from workspace aggregate', async () => {
|
||||||
|
const wrapper = mount(WorkflowBuilderPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-test="workflow-run-req-2001"]').text()).toContain('SUCCESS');
|
||||||
|
expect(wrapper.find('[data-test="workflow-run-req-2001"]').text()).toContain('860ms');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user