From 00ad6c8ac00321d8151b9e9d001b872449cd1f72 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Mon, 20 Jul 2026 14:20:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E4=BB=BB=E5=8A=A1=E4=B8=8B?= =?UTF-8?q?=E9=92=BB=E6=8C=89=E7=9C=9F=E5=AE=9E=E8=BD=A8=E8=BF=B9=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E6=B8=B2=E6=9F=93=20+=20=E4=BF=AE=E8=AF=84=E6=B5=8B?= =?UTF-8?q?=E9=A1=B5=E7=99=BD=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker 起来后拿真库验证,上一版有两处只有真数据才暴露的问题: 1. 轨迹字段名猜错了。实际是 seq/ts/node/kind/phase/label/detail,我写的是 type/msg/at。更糟的是兜底判断用了 !e.msg && !e.node —— node 恰好存在, 兜底不触发,结果会渲染出一列几乎空白的行。现按真实字段渲染:label 作 标题、detail 作正文、kind 上色(system/model/tool/agent/plan/section/ render/approval 八种)、phase=error 整行标红、ts 转本地时间。 2. 评测页签点开直接白屏。库里 flags 存的是字符串 "null"(不是 "[]"), JSON.parse 出来是 null,flags.length 抛 TypeError 把整个控制台打崩。 `as string[]` 的类型断言骗过了 tsc,单测也覆盖不到——只有点开真数据的 页面才会炸。改为 Array.isArray 校验。 验证方式:本地起 gateway + 真 Postgres,浏览器里逐个页签点过(轨迹 15 条 真实事件、最终输出、评测 100/100/100、DSL 格式化),并在真库上确认跨租户 读取、jsonb ->> 取 topic、cast(jsonb as text) 取 graph 均正常。 Co-Authored-By: Claude Opus 4.8 --- .../src/components/TaskDetailDrawer.tsx | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/sundynix-admin/src/components/TaskDetailDrawer.tsx b/sundynix-admin/src/components/TaskDetailDrawer.tsx index 30603c4..9411f17 100644 --- a/sundynix-admin/src/components/TaskDetailDrawer.tsx +++ b/sundynix-admin/src/components/TaskDetailDrawer.tsx @@ -9,7 +9,38 @@ import { adminTaskDetail, type AdminTask, type AdminTaskDetail } from "../api"; // // 不含审批操作:审批是客户端用户的行为(桌面端 ApprovalBar),管理端只做观测。 -type ExecEvent = { node?: string; type?: string; msg?: string; at?: string; [k: string]: unknown }; +// 字段名以库里真实轨迹为准(不是猜的):seq/ts/node/kind/phase/label/detail。 +// ts 是毫秒时间戳;kind ∈ system|model|tool|agent|plan|section|render|approval; +// phase ∈ info|start|end|await|error。 +type ExecEvent = { + seq?: number; + ts?: number; + node?: string; + kind?: string; + phase?: string; + label?: string; + detail?: string; + [k: string]: unknown; +}; + +const KIND_TONE: Record = { + system: "bg-gray-100 text-gray-600", + model: "bg-violet-50 text-violet-600", + tool: "bg-sky-50 text-sky-600", + agent: "bg-indigo-50 text-indigo-600", + plan: "bg-amber-50 text-amber-700", + section: "bg-emerald-50 text-emerald-600", + render: "bg-teal-50 text-teal-600", + approval: "bg-orange-50 text-orange-600", +}; + +// 只有 error/await 值得跳出来:前者是失败点,后者是卡在人工审批。 +const PHASE_TONE: Record = { + error: "text-rose-600 font-medium", + await: "text-orange-600", +}; + +const hhmmss = (ms?: number) => (ms ? new Date(ms).toLocaleTimeString("zh-CN", { hour12: false }) : ""); export function TaskDetailDrawer({ task, onClose }: { task: AdminTask; onClose: () => void }) { const [d, setD] = useState(null); @@ -31,9 +62,12 @@ export function TaskDetailDrawer({ task, onClose }: { task: AdminTask; onClose: }; }, [task.task_id]); + // flags 是库里存的 JSON 字符串,实测可能是 "null"(不是 "[]")——JSON.parse 出来是 null, + // 不校验就会在 flags.length 上抛 TypeError 把整个控制台白屏掉。必须确认真是数组。 const flags: string[] = (() => { try { - return d?.eval?.flags ? (JSON.parse(d.eval.flags) as string[]) : []; + const v: unknown = d?.eval?.flags ? JSON.parse(d.eval.flags) : null; + return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []; } catch { return []; } @@ -104,15 +138,29 @@ export function TaskDetailDrawer({ task, onClose }: { task: AdminTask; onClose: ) : (
    {exec.map((e, i) => ( -
  1. +
  2. - #{i + 1} - {e.node && {e.node}} - {e.type && {e.type}} - {e.at && {e.at}} + #{e.seq ?? i + 1} + {e.label && {e.label}} + {e.kind && ( + + {e.kind} + + )} + {e.phase && e.phase !== "info" && ( + {e.phase} + )} + {hhmmss(e.ts)}
    - {e.msg &&

    {e.msg}

    } - {!e.msg && !e.node && ( + {e.node &&
    {e.node}
    } + {e.detail &&

    {e.detail}

    } + {/* 兜底:字段形状变了也别把线索吞掉(上一版就是猜错字段名,白渲染了一列空行) */} + {!e.label && !e.detail && (
    {JSON.stringify(e)}
    )}