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:
@@ -0,0 +1,92 @@
|
||||
import { deriveNodes, type NodeTrace, type RunPhase } from "../lib/run";
|
||||
import type { ExecEvent } from "../lib/api";
|
||||
|
||||
// 各节点类别的图标与配色(与后端 ExecEvent.kind 对应)。
|
||||
const KIND: Record<string, { icon: string; cls: string; name: string }> = {
|
||||
system: { icon: "▸", cls: "text-slate-400", name: "系统" },
|
||||
memory: { icon: "◇", cls: "text-violet-300", name: "记忆召回" },
|
||||
tool: { icon: "⚙", cls: "text-amber-300", name: "工具调用" },
|
||||
prompt: { icon: "▤", cls: "text-sky-300", name: "提示词" },
|
||||
model: { icon: "✦", cls: "text-emerald-300", name: "模型推理" },
|
||||
plan: { icon: "◷", cls: "text-cyan-300", name: "规划" },
|
||||
section: { icon: "¶", cls: "text-indigo-300", name: "章节" },
|
||||
render: { icon: "▦", cls: "text-rose-300", name: "渲染" },
|
||||
};
|
||||
|
||||
function meta(kind: string) {
|
||||
return KIND[kind] ?? { icon: "•", cls: "text-slate-400", name: kind };
|
||||
}
|
||||
|
||||
function StatusDot({ status }: { status: NodeTrace["status"] }) {
|
||||
if (status === "running")
|
||||
return <span className="h-2.5 w-2.5 animate-pulse rounded-full bg-cyan-400 shadow-[0_0_8px_rgba(34,211,238,0.8)]" />;
|
||||
if (status === "done") return <span className="h-2.5 w-2.5 rounded-full bg-emerald-400" />;
|
||||
if (status === "error") return <span className="h-2.5 w-2.5 rounded-full bg-rose-500" />;
|
||||
return <span className="h-2.5 w-2.5 rounded-full bg-slate-600" />;
|
||||
}
|
||||
|
||||
// ExecTrace 把执行事件流渲染为竖向轨道:每个节点一颗灯,实时点亮 + 耗时 + 入参/产出。
|
||||
export function ExecTrace({
|
||||
events,
|
||||
phase,
|
||||
compact,
|
||||
}: {
|
||||
events: ExecEvent[];
|
||||
phase?: RunPhase;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const nodes = deriveNodes(events);
|
||||
if (nodes.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center px-4 py-8 text-center text-xs text-slate-600">
|
||||
运行后,这里会逐节点点亮执行轨迹:记忆召回 → 工具调用(入参/产出)→ 提示词组装 → 模型推理。
|
||||
<br />
|
||||
报告任务则显示:规划大纲 → 各章并行检索撰写 → 渲染 Word。
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const total = nodes.reduce((s, n) => s + (n.ms ?? 0), 0);
|
||||
return (
|
||||
<div className={compact ? "" : "p-1"}>
|
||||
{!compact && (
|
||||
<div className="mb-2 flex items-center gap-3 px-1 text-[11px] text-slate-500">
|
||||
<span>{nodes.length} 个节点</span>
|
||||
<span>·</span>
|
||||
<span>累计 {total} ms</span>
|
||||
{phase === "streaming" && <span className="text-cyan-400">● 执行中</span>}
|
||||
{phase === "done" && <span className="text-emerald-400">✓ 完成</span>}
|
||||
{phase === "error" && <span className="text-rose-400">✗ 出错</span>}
|
||||
</div>
|
||||
)}
|
||||
<ol className="relative ml-2 space-y-2 border-l border-line pl-4">
|
||||
{nodes.map((n) => {
|
||||
const m = meta(n.kind);
|
||||
return (
|
||||
<li key={n.node} className="relative">
|
||||
<span className="absolute -left-[22px] top-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-ink-900">
|
||||
<StatusDot status={n.status} />
|
||||
</span>
|
||||
<div className="rounded-lg border border-line bg-ink-950/60 px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm leading-none ${m.cls}`}>{m.icon}</span>
|
||||
<span className="text-xs font-medium text-slate-200">{n.label}</span>
|
||||
<span className={`rounded px-1.5 py-0.5 text-[9px] ${m.cls} bg-white/5`}>{m.name}</span>
|
||||
{n.ms != null && n.ms > 0 && (
|
||||
<span className="ml-auto font-mono text-[10px] text-slate-500">{n.ms} ms</span>
|
||||
)}
|
||||
{n.status === "running" && <span className="ml-auto text-[10px] text-cyan-400">运行中…</span>}
|
||||
</div>
|
||||
{n.detail && <p className="mt-1 break-words text-[11px] leading-relaxed text-slate-400">{n.detail}</p>}
|
||||
{n.notes.map((note, i) => (
|
||||
<p key={i} className="mt-1 break-words border-l border-line pl-2 text-[11px] text-slate-500">
|
||||
{note}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user