diff --git a/sundynix-desktop/frontend/src/lib/api.ts b/sundynix-desktop/frontend/src/lib/api.ts index 0441332..db500a6 100644 --- a/sundynix-desktop/frontend/src/lib/api.ts +++ b/sundynix-desktop/frontend/src/lib/api.ts @@ -519,3 +519,11 @@ export async function taskEval(taskId: string): Promise { if (!res.ok) return null; return res.json() as Promise; } + +// runReplay: 历史运行复盘 —— 读库取持久化的输出 + 轨迹(不依赖 Redis TTL,秒回)。 +export async function runReplay(taskId: string): Promise<{ output: string; exec: ExecEvent[] }> { + const res = guard401(await fetch(`${GATEWAY}/api/v1/tasks/${taskId}/replay`, { headers: bearer() })); + if (!res.ok) throw new Error(`replay failed: ${res.status}`); + const d = (await res.json()) as { output?: string; exec?: ExecEvent[] }; + return { output: d.output ?? "", exec: d.exec ?? [] }; +} diff --git a/sundynix-desktop/frontend/src/views/RunsView.tsx b/sundynix-desktop/frontend/src/views/RunsView.tsx index 6a45b8c..7470035 100644 --- a/sundynix-desktop/frontend/src/views/RunsView.tsx +++ b/sundynix-desktop/frontend/src/views/RunsView.tsx @@ -1,11 +1,11 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Activity, FileText, History, Wrench } from "lucide-react"; import { ExecTrace } from "../components/ExecTrace"; import { Markdown } from "../components/Markdown"; import { ChartView } from "../components/ChartView"; import { extractChartBlocks, hasChart } from "../lib/chartspec"; import { deriveNodes, emptyRun, type RunState } from "../lib/run"; -import { listRuns, taskEval, streamExec, streamTokens, type RunSummary, type EvalResult } from "../lib/api"; +import { listRuns, taskEval, runReplay, type RunSummary, type EvalResult } from "../lib/api"; import { Tabs, Panel, Dot, Badge, EmptyState, cn, type TabDef } from "../ui"; type DetailTab = "trace" | "tools" | "eval"; @@ -32,31 +32,30 @@ export function RunsView({ run }: { run: RunState }) { const [replay, setReplay] = useState(emptyRun); const [evalRes, setEvalRes] = useState(null); const [tab, setTab] = useState("trace"); - const closeRef = useRef<(() => void) | null>(null); useEffect(() => { let alive = true; const load = () => listRuns(40).then((r) => alive && setRuns(r)).catch(() => {}); load(); const id = setInterval(load, 5000); - return () => { alive = false; clearInterval(id); closeRef.current?.(); }; + return () => { alive = false; clearInterval(id); }; }, []); + // 选中历史运行:读库取持久化的输出 + 轨迹复盘(秒回、不依赖 Redis TTL)。 const selectRun = useCallback((taskId: string) => { - closeRef.current?.(); if (taskId === run.taskId) { setSel(null); setEvalRes(null); return; } setSel(taskId); - setReplay({ phase: "streaming", taskId, output: "", events: [], exec: [] }); + setReplay({ phase: "submitting", taskId, output: "", events: [], exec: [] }); setEvalRes(null); - const closeExec = streamExec(taskId, (ev) => setReplay((r) => ({ ...r, exec: [...r.exec, ev] })), () => setReplay((r) => ({ ...r, phase: "done" })), () => {}); - const closeTok = streamTokens(taskId, (tok) => setReplay((r) => ({ ...r, output: r.output + tok })), () => {}, () => {}); - closeRef.current = () => { closeExec(); closeTok(); }; + runReplay(taskId) + .then((d) => setReplay({ phase: "done", taskId, output: d.output, events: [], exec: d.exec })) + .catch(() => setReplay({ phase: "error", taskId, output: "", events: [], exec: [], error: "复盘数据不可用" })); taskEval(taskId).then(setEvalRes).catch(() => {}); }, [run.taskId]); // 新实时运行开始 → 切回「当前运行」+ 回到轨迹标签。 useEffect(() => { - if (run.taskId) { closeRef.current?.(); setSel(null); setEvalRes(null); setTab("trace"); } + if (run.taskId) { setSel(null); setEvalRes(null); setTab("trace"); } }, [run.taskId]); // 当前实时运行的评测:完成后拉一次。 @@ -99,7 +98,7 @@ export function RunsView({ run }: { run: RunState }) {
{liveActive && ( -