feat(workbench): redesign investigation reports and diagrams

This commit is contained in:
2026-10-10 16:02:47 +08:00
parent 85a9a3ab45
commit ba78a24384
27 changed files with 2380 additions and 187 deletions
@@ -0,0 +1,104 @@
import type { InvestigationDiagram } from "../api/investigation-report";
import type { OperationalTask, OperationalTaskReport } from "../api/operational-task-client";
const diagramLabel = (value: string) =>
value
.replace(/[\r\n]/g, " ")
.replace(/"/g, "#quot;")
.replace(/</g, "#lt;")
.replace(/>/g, "#gt;")
.replace(/`/g, "#96;");
/** Preserve recorded relationships, without turning plan statuses into execution facts. */
export function investigationDiagram(
task: OperationalTask,
snapshot: OperationalTaskReport | null
): InvestigationDiagram | null {
if (snapshot?.report.diagram) return snapshot.report.diagram;
const plan = task.latest_plan;
if (
!plan ||
(snapshot
? snapshot.revision !== task.latest_report?.revision ||
plan.execution_generation !== snapshot.execution_generation
: plan.execution_generation !== task.execution_generation)
)
return null;
const ids = new Map(plan.plan.nodes.map((node, index) => [node.id, `n${index}`]));
const nodeDefinition = (node: (typeof plan.plan.nodes)[number]) => {
const label = `\`${diagramLabel(node.title)}\``;
return node.kind === "decision" || node.kind === "gate"
? `${ids.get(node.id)}{"${label}"}:::tjDecision`
: `${ids.get(node.id)}["${label}"]:::${node.owner === "human" ? "tjHuman" : "tjAgent"}`;
};
const agentNodes = plan.plan.nodes.filter((node) => node.owner === "agent");
const humanNodes = plan.plan.nodes.filter((node) => node.owner === "human");
const nodes =
agentNodes.length && humanNodes.length
? [
'subgraph tj_agent_group["Agent · 资料核查"]',
...agentNodes.map(nodeDefinition),
"end",
'subgraph tj_human_group["人工 · 现场协作"]',
...humanNodes.map(nodeDefinition),
"end"
]
: plan.plan.nodes.map(nodeDefinition);
const edges = plan.plan.edges.map((edge) =>
edge.condition
? `${ids.get(edge.from)} -->|"${diagramLabel(edge.condition)}"| ${ids.get(edge.to)}`
: `${ids.get(edge.from)} --> ${ids.get(edge.to)}`
);
return {
title: "调查脉络",
mermaid: ["flowchart TD", ...nodes, ...edges].join("\n"),
node_refs: plan.plan.nodes
.filter((node) => node.evidence_refs?.length || node.action_refs?.length)
.map((node) => ({
node_id: ids.get(node.id)!,
label: node.title,
evidence_refs: node.evidence_refs ?? [],
action_refs: node.action_refs ?? []
}))
};
}
export function investigationStatus(task: OperationalTask): string {
if (task.active_run?.status === "queued") return "等待调查";
if (task.active_run?.status === "claimed") return "调查启动中";
if (task.active_run?.status === "running") return "调查中";
if (task.lifecycle_status === "failed") return "本轮执行失败";
if (task.lifecycle_status === "cancelled") return "本轮已取消";
if (task.actions.some((action) => action.approval_status === "pending")) return "待人工审核";
if (
task.actions.some(
(action) =>
action.approval_status === "approved" && action.execution_status === "not_submitted"
)
)
return "待执行动作";
if (
task.actions.some(
(action) =>
action.approval_status === "approved" &&
["submitted", "unknown"].includes(action.execution_status)
)
)
return "待核对回执";
if (
task.latest_report?.execution_generation === task.execution_generation &&
task.latest_report.report.outcome === "insufficient_data" &&
task.lifecycle_status === "completed"
)
return "已结束 · 证据不足";
if (task.lifecycle_status === "completed") return "本轮调查完成";
return "调查中";
}
export function isPendingInvestigationAction(action: OperationalTask["actions"][number]) {
return (
action.approval_status === "pending" ||
(action.approval_status === "approved" &&
["not_submitted", "submitted", "unknown"].includes(action.execution_status))
);
}