feat(studio): 补齐剩余工作台聚合接口与真实对接

This commit is contained in:
2026-06-01 05:28:11 +08:00
parent 8f7ffd6cc9
commit ebe0fc5a12
35 changed files with 2092 additions and 123 deletions

View File

@@ -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);
}
}

View File

@@ -0,0 +1,14 @@
package com.bruce.dashboard.service;
import com.bruce.dashboard.vo.StudioDashboardVO;
/**
* Studio 总览工作台聚合服务。
*/
public interface IStudioDashboardService {
/**
* 汇总当前项目的发布旅程、运行摘要和风险提示。
*/
StudioDashboardVO getDashboard();
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,12 @@
package com.bruce.dashboard.vo;
import lombok.Data;
/**
* Studio 发布就绪项。
*/
@Data
public class StudioDashboardChecklistItemVO {
private String label;
private Boolean done;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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());
}
}