feat(observability): 执行可视化 — 节点级实时轨迹(运行·观测)
把任务执行做成可观测:Dispatcher 在每个节点/阶段发结构化 ExecEvent, 经独立 NATS 通道回流,前端逐节点点亮(状态/耗时/工具入参产出)。 - shared: contract.ExecEvent + ExecSubject(sundynix.exec.<id>,与 Token 流分流); bus.PublishExec/CompleteExec/SubscribeExec(core NATS,复用结束头) - dispatcher: execTracer(自增 Seq 保序 + span 自动计耗时); Orchestrator 加 ExecSink;通用图(init 召回 / 各 tool 入参→产出 / prompt / model 首token+token数)与报告编排(规划大纲 / 各章并行 start-end / 渲染)全程埋点 - gateway: SubscribeExec + GET /tasks/:id/exec SSE(与 token 流并行) - desktop: streamExec + deriveNodes(按 node 归并 start/end/error/info); 复用组件 ExecTrace(竖向轨道,按 kind 着色,运行中脉冲灯); 新 RunsView(运行·观测:轨迹+输出双栏);BottomDrawer 轨迹/工具调用 tab 接真实数据; ReportView 加执行轨迹栏;左导航「运行」置就绪 实测: - 报告任务 /exec:规划(2680ms,4章) → 4 章并行(seq 交错,各~7-8s 重叠=真并行, 每章带 docs 知识库检索预览+成稿字数) → 渲染(docx 落盘) - 通用图 /exec:tool:kb_search(678ms,入参→Milvus 产出) → prompt(2消息) → model(首token 860ms / 4 tokens) - 浏览器(Preview):报告页执行轨迹逐节点点亮、章节带耗时/字数/检索片段,完成后下载 Word Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,38 @@ export function streamTokens(
|
||||
return () => es.close();
|
||||
}
|
||||
|
||||
// 执行轨迹事件(与后端 contract.ExecEvent 对应):运行·观测的节点级实时事件。
|
||||
export interface ExecEvent {
|
||||
seq: number;
|
||||
ts: number;
|
||||
node: string; // init / tool:wiki_search / prompt / model / plan / section:0 / render / task / compile
|
||||
kind: string; // memory|tool|prompt|model|plan|section|render|system
|
||||
phase: string; // start|end|error|info
|
||||
label: string;
|
||||
detail?: string;
|
||||
ms?: number;
|
||||
}
|
||||
|
||||
// streamExec: 订阅 SSE /api/v1/tasks/:id/exec —— 与 token 流并行,逐节点点亮执行轨迹。
|
||||
export function streamExec(
|
||||
taskId: string,
|
||||
onEvent: (ev: ExecEvent) => void,
|
||||
onDone: () => void,
|
||||
onError?: (e: unknown) => void,
|
||||
): () => void {
|
||||
const es = new EventSource(`${GATEWAY}/api/v1/tasks/${taskId}/exec`);
|
||||
es.addEventListener("exec", (e) => onEvent(JSON.parse((e as MessageEvent).data) as ExecEvent));
|
||||
es.addEventListener("done", () => {
|
||||
es.close();
|
||||
onDone();
|
||||
});
|
||||
es.onerror = (e) => {
|
||||
es.close();
|
||||
onError?.(e);
|
||||
};
|
||||
return () => es.close();
|
||||
}
|
||||
|
||||
// 入库进度事件(与后端 contract.IngestEvent 对应)。
|
||||
export interface IngestEvent {
|
||||
stage: string;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// 运行状态 —— 跨 Studio 与底部抽屉共享。
|
||||
import type { ExecEvent } from "./api";
|
||||
|
||||
export type RunPhase = "idle" | "submitting" | "streaming" | "done" | "error";
|
||||
|
||||
export interface RunEvent {
|
||||
@@ -11,7 +13,58 @@ export interface RunState {
|
||||
taskId?: string;
|
||||
output: string;
|
||||
events: RunEvent[];
|
||||
exec: ExecEvent[]; // 后端回流的节点级执行轨迹(运行·观测)
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const emptyRun: RunState = { phase: "idle", output: "", events: [] };
|
||||
export const emptyRun: RunState = { phase: "idle", output: "", events: [], exec: [] };
|
||||
|
||||
// ---- 执行轨迹派生:把扁平 ExecEvent 流归并为按节点聚合的轨迹 ----
|
||||
|
||||
export type NodeStatus = "running" | "done" | "error" | "info";
|
||||
|
||||
export interface NodeTrace {
|
||||
node: string;
|
||||
kind: string; // memory|tool|prompt|model|plan|section|render|system
|
||||
label: string;
|
||||
status: NodeStatus;
|
||||
ms?: number;
|
||||
detail?: string;
|
||||
notes: string[]; // info 子事件(如检索到的参考资料预览)
|
||||
order: number;
|
||||
}
|
||||
|
||||
// deriveNodes 把事件流按 node 归并:start→running,end→done(带耗时),error→error,info→点事件/附注。
|
||||
export function deriveNodes(events: ExecEvent[]): NodeTrace[] {
|
||||
const map = new Map<string, NodeTrace>();
|
||||
let order = 0;
|
||||
for (const e of events) {
|
||||
let n = map.get(e.node);
|
||||
if (!n) {
|
||||
n = { node: e.node, kind: e.kind, label: e.label, status: "info", notes: [], order: order++ };
|
||||
map.set(e.node, n);
|
||||
}
|
||||
if (e.label) n.label = e.label;
|
||||
n.kind = e.kind;
|
||||
switch (e.phase) {
|
||||
case "start":
|
||||
if (n.status !== "done" && n.status !== "error") n.status = "running";
|
||||
break;
|
||||
case "end":
|
||||
n.status = "done";
|
||||
n.ms = e.ms;
|
||||
if (e.detail) n.detail = e.detail;
|
||||
break;
|
||||
case "error":
|
||||
n.status = "error";
|
||||
n.ms = e.ms;
|
||||
if (e.detail) n.detail = e.detail;
|
||||
break;
|
||||
case "info":
|
||||
if (e.detail) n.notes.push(e.detail);
|
||||
else if (e.label) n.notes.push(e.label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [...map.values()].sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user