fix(admin): 任务下钻按真实轨迹字段渲染 + 修评测页白屏
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, string> = {
|
||||
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<string, string> = {
|
||||
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<AdminTaskDetail | null>(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:
|
||||
) : (
|
||||
<ol className="space-y-2">
|
||||
{exec.map((e, i) => (
|
||||
<li key={i} className="rounded-lg border border-gray-100 bg-gray-50/60 p-2.5 text-[11px]">
|
||||
<li
|
||||
key={e.seq ?? i}
|
||||
className={`rounded-lg border p-2.5 text-[11px] ${
|
||||
e.phase === "error" ? "border-rose-200 bg-rose-50/50" : "border-gray-100 bg-gray-50/60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="shrink-0 text-gray-300">#{i + 1}</span>
|
||||
{e.node && <span className="font-medium text-gray-700">{e.node}</span>}
|
||||
{e.type && <span className="rounded bg-white px-1.5 py-0.5 text-[10px] text-violet-600">{e.type}</span>}
|
||||
{e.at && <span className="ml-auto shrink-0 text-gray-300">{e.at}</span>}
|
||||
<span className="shrink-0 tabular-nums text-gray-300">#{e.seq ?? i + 1}</span>
|
||||
{e.label && <span className={`font-medium text-gray-700 ${PHASE_TONE[e.phase ?? ""] ?? ""}`}>{e.label}</span>}
|
||||
{e.kind && (
|
||||
<span className={`rounded px-1.5 py-0.5 text-[10px] ${KIND_TONE[e.kind] ?? "bg-gray-100 text-gray-500"}`}>
|
||||
{e.kind}
|
||||
</span>
|
||||
)}
|
||||
{e.phase && e.phase !== "info" && (
|
||||
<span className={`text-[10px] ${PHASE_TONE[e.phase] ?? "text-gray-400"}`}>{e.phase}</span>
|
||||
)}
|
||||
<span className="ml-auto shrink-0 tabular-nums text-gray-300">{hhmmss(e.ts)}</span>
|
||||
</div>
|
||||
{e.msg && <p className="mt-1 whitespace-pre-wrap break-words text-gray-600">{e.msg}</p>}
|
||||
{!e.msg && !e.node && (
|
||||
{e.node && <div className="mt-0.5 font-mono text-[10px] text-gray-400">{e.node}</div>}
|
||||
{e.detail && <p className="mt-1 whitespace-pre-wrap break-words text-gray-600">{e.detail}</p>}
|
||||
{/* 兜底:字段形状变了也别把线索吞掉(上一版就是猜错字段名,白渲染了一列空行) */}
|
||||
{!e.label && !e.detail && (
|
||||
<pre className="mt-1 overflow-x-auto text-[10px] text-gray-500">{JSON.stringify(e)}</pre>
|
||||
)}
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user