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:
@@ -1,5 +1,6 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { generateReport, streamTokens, reportDownloadUrl, type Identity } from "../lib/api";
|
||||
import { generateReport, streamTokens, streamExec, reportDownloadUrl, type Identity, type ExecEvent } from "../lib/api";
|
||||
import { ExecTrace } from "../components/ExecTrace";
|
||||
|
||||
type Phase = "idle" | "running" | "done" | "error";
|
||||
|
||||
@@ -10,22 +11,32 @@ export function ReportView({ identity }: { identity: Identity }) {
|
||||
const [kb, setKb] = useState("");
|
||||
const [phase, setPhase] = useState<Phase>("idle");
|
||||
const [out, setOut] = useState("");
|
||||
const [exec, setExec] = useState<ExecEvent[]>([]);
|
||||
const [taskId, setTaskId] = useState("");
|
||||
const [err, setErr] = useState("");
|
||||
const closeRef = useRef<(() => void) | null>(null);
|
||||
const execCloseRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const running = phase === "running";
|
||||
|
||||
const onGenerate = async () => {
|
||||
if (!topic.trim() || running) return;
|
||||
closeRef.current?.();
|
||||
execCloseRef.current?.();
|
||||
setPhase("running");
|
||||
setOut("");
|
||||
setExec([]);
|
||||
setErr("");
|
||||
setTaskId("");
|
||||
try {
|
||||
const id = await generateReport(identity, topic.trim(), kb.trim() || undefined);
|
||||
setTaskId(id);
|
||||
execCloseRef.current = streamExec(
|
||||
id,
|
||||
(ev) => setExec((xs) => [...xs, ev]),
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
closeRef.current = streamTokens(
|
||||
id,
|
||||
(tok) => setOut((o) => o + tok),
|
||||
@@ -95,21 +106,30 @@ export function ReportView({ identity }: { identity: Identity }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 实时进度 / 正文 */}
|
||||
<div className="flex min-h-0 flex-1 flex-col rounded-xl border border-line bg-ink-900 shadow-card">
|
||||
<div className="flex items-center gap-2 border-b border-line px-4 py-2.5 text-[11px] text-slate-500">
|
||||
<span className={`h-2 w-2 rounded-full ${running ? "animate-pulse bg-cyan-400" : phase === "done" ? "bg-emerald-400" : "bg-slate-600"}`} />
|
||||
实时编排 · {taskId || "未开始"}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
{out ? (
|
||||
<pre className="whitespace-pre-wrap break-words font-sans text-sm leading-relaxed text-slate-300">{out}</pre>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-slate-600">
|
||||
输入主题并点击「生成报告」,这里将实时显示规划与撰写过程。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 执行轨迹 + 报告正文 */}
|
||||
<div className="grid min-h-0 flex-1 grid-cols-[340px_1fr] gap-4">
|
||||
<section className="flex min-h-0 flex-col rounded-xl border border-line bg-ink-900 shadow-card">
|
||||
<div className="border-b border-line px-4 py-2.5 text-[11px] font-medium text-slate-400">执行轨迹</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
<ExecTrace events={exec} phase={running ? "streaming" : phase === "done" ? "done" : phase === "error" ? "error" : "idle"} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex min-h-0 flex-col rounded-xl border border-line bg-ink-900 shadow-card">
|
||||
<div className="flex items-center gap-2 border-b border-line px-4 py-2.5 text-[11px] text-slate-500">
|
||||
<span className={`h-2 w-2 rounded-full ${running ? "animate-pulse bg-cyan-400" : phase === "done" ? "bg-emerald-400" : "bg-slate-600"}`} />
|
||||
报告正文 · {taskId || "未开始"}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
{out ? (
|
||||
<pre className="whitespace-pre-wrap break-words font-sans text-sm leading-relaxed text-slate-300">{out}</pre>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-slate-600">
|
||||
输入主题并点击「生成报告」,这里将实时显示规划与撰写过程。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ExecTrace } from "../components/ExecTrace";
|
||||
import { deriveNodes, type RunState } from "../lib/run";
|
||||
|
||||
// 运行·观测:把最近一次运行的执行轨迹实时可视化(节点逐个点亮 + 工具入参/产出 + 耗时),
|
||||
// 右侧并列模型输出。数据来自 Studio 运行时订阅的 sundynix.exec.<id> 事件流。
|
||||
export function RunsView({ run }: { run: RunState }) {
|
||||
const nodes = deriveNodes(run.exec);
|
||||
const tools = nodes.filter((n) => n.kind === "tool");
|
||||
const phaseText =
|
||||
run.phase === "streaming" ? "执行中" : run.phase === "done" ? "完成" : run.phase === "error" ? "出错" : run.phase === "submitting" ? "提交中" : "就绪";
|
||||
const phaseCls =
|
||||
run.phase === "streaming" ? "text-cyan-400" : run.phase === "done" ? "text-emerald-400" : run.phase === "error" ? "text-rose-400" : "text-slate-500";
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col gap-4 overflow-hidden p-6">
|
||||
<header className="flex items-center gap-3">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-slate-100">运行 · 观测</h1>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
实时执行轨迹:每个节点(记忆/工具/提示词/模型,或报告的规划/分章/渲染)逐个点亮,附入参产出与耗时。
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-4 text-xs">
|
||||
<span className="text-slate-500">
|
||||
任务 <span className="font-mono text-slate-300">{run.taskId ?? "—"}</span>
|
||||
</span>
|
||||
<span className={phaseCls}>● {phaseText}</span>
|
||||
<span className="text-slate-500">{nodes.length} 节点 · {tools.length} 次工具调用</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid min-h-0 flex-1 grid-cols-[1.2fr_1fr] gap-4">
|
||||
{/* 执行轨迹 */}
|
||||
<section className="flex min-h-0 flex-col rounded-xl border border-line bg-ink-900 shadow-card">
|
||||
<div className="border-b border-line px-4 py-2.5 text-[11px] font-medium text-slate-400">执行轨迹</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
<ExecTrace events={run.exec} phase={run.phase} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 模型输出 */}
|
||||
<section className="flex min-h-0 flex-col rounded-xl border border-line bg-ink-900 shadow-card">
|
||||
<div className="border-b border-line px-4 py-2.5 text-[11px] font-medium text-slate-400">模型输出</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
{run.output ? (
|
||||
<pre className="whitespace-pre-wrap break-words font-sans text-sm leading-relaxed text-slate-300">{run.output}</pre>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-center text-xs text-slate-600">
|
||||
在「编排」页搭图并运行,或在「报告」页生成报告,<br />执行轨迹与输出会实时出现在这里。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user