import { useMemo, useState, type ReactNode } from "react"; import { AlertTriangle, CheckCircle2, Clock3, FileText, GitFork, Loader2, MessageSquare, RefreshCw, Route, ShieldCheck, UserCheck } from "lucide-react"; import type { AgentExecutionPlan } from "@/features/agent"; import { Button } from "@/shared/ui/button"; import { cn } from "@/shared/ui/cn"; import { Disclosure } from "@/shared/ui/disclosure"; import { Input } from "@/shared/ui/input"; import { StatusBadge, StatusIcon, type StatusTone } from "@/shared/ui/status"; import type { OperationalTask, OperationalTaskAction, OperationalTaskDecision, OperationalTaskEvidence, OperationalTaskPlan, OperationalTaskDataSnapshot } from "../api/operational-task-client"; type ExecutionNode = { id: string; title: string; status: "pending" | "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled"; owner: "agent" | "human"; detail: string; kind?: "activity" | "decision" | "parallel_split" | "parallel_join" | "gate" | null; selected_edge_id?: string | null; gate_ref?: string | null; revisits_node_id?: string | null; }; type ExecutionEdge = { id?: string | null; from: string; to: string; label?: string | null; condition?: string | null; }; export type ExecutionPlanView = { nodes: ExecutionNode[]; edges: ExecutionEdge[]; }; type AgentExecutionPlanPanelProps = { task: OperationalTask; onContinue: () => void; onDecision: (actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise; onExecute: (actionId: string, expectedVersion: number) => Promise; onDraftUpdate?: (actionId: string, expectedVersion: number, payload: Record) => Promise; onConfirmDispatch?: (actionId: string, expectedVersion: number) => Promise; onAccessDecision?: (requestId: string, expectedVersion: number, decision: "approved" | "rejected", scopes: string[]) => Promise; onReconcile: (actionId: string, expectedVersion: number) => Promise; onLoadEvidenceSnapshot: (evidenceId: string) => Promise; onRefresh: () => void; }; export function AgentExecutionPlanPanel({ task, onContinue, onDecision, onExecute, onDraftUpdate, onConfirmDispatch, onAccessDecision, onReconcile, onLoadEvidenceSnapshot, onRefresh }: AgentExecutionPlanPanelProps) { const plan = task.latest_plan?.plan; const report = task.latest_report?.report; const planGeneration = task.latest_plan?.execution_generation; const planState = planGeneration == null ? "historical" : planGeneration === task.execution_generation ? "current" : "previous"; const reportGeneration = task.latest_report?.execution_generation; const reportState = reportGeneration == null ? "historical" : reportGeneration === task.execution_generation ? "current" : "previous"; const completed = plan?.nodes.filter((node) => node.status === "completed").length ?? 0; const active = plan?.nodes.filter((node) => node.status === "running").length ?? 0; const waiting = plan?.nodes.filter((node) => node.status === "waiting" || node.owner === "human" && node.status === "pending").length ?? 0; const externalEvidence = task.evidence.filter((item) => item.kind === "external_feedback"); const taskActive = task.status === "queued" || task.status === "running"; const planSummary = !plan ? "任务已持久化,等待 Agent 发布首版处理计划。" : planState === "current" ? `已完成 ${completed} 项 · 执行中 ${active} 项 · 待人工或资料 ${waiting} 项` : planState === "previous" ? taskActive ? "上一轮流程,本轮规划中" : `上一轮流程,当前任务${taskStatusLabel(task.status)}` : "历史流程"; const reportLabel = reportState === "current" ? "当前结论" : reportState === "previous" ? taskActive ? "上一轮报告,本轮分析中" : `上一轮报告,当前任务${taskStatusLabel(task.status)}` : "历史报告"; const nodeActions = plan ? Object.fromEntries(plan.nodes.map((node) => [ node.id, task.actions.filter((action) => action.node_id === node.id).map((action) => ( )) ])) : {}; const unlinkedActions = task.actions.filter((action) => !action.node_id || !plan?.nodes.some((node) => node.id === action.node_id)); return (

{taskStatusLabel(task.status)}

{planSummary}

{taskTypeLabel(task.task_type)} · 更新于 {formatDateTime(task.updated_at)}

{task.task_type === "external_work_order" && externalEvidence.length > 0 ? ( ) : plan ? ( ) : (

尚无执行节点,页面不会按时间推算或虚构完成进度。

)}
{task.access_requests.length > 0 ?
{task.access_requests.map((request) => node.id === request.node_id)?.title} canDecide={task.permissions.can_decide_access} onDecision={onAccessDecision} onRefresh={onRefresh} />)}
: null}

结论与证据独立呈现,不展示模型推理或节点执行日志。

{report ? ( <>

{reportLabel}

{report.current_conclusion}

{report.recommendations.length > 0 ? ( ) : null} {report.pending_items.length > 0 ? ( ) : null}

结论边界:{report.conclusion_boundary}

) : (

尚未形成任务报告。

)}
{unlinkedActions.length > 0 ? (
{unlinkedActions.map((action) => ( ))}
) : null}
); } function ReportList({ title, items, tone }: { title: string; items: string[]; tone: "blue" | "amber" }) { return (

{title}

    {items.map((item) => (
  • ))}
); } export function ExecutionPlanSteps({ plan, nodeActions = {} }: { plan: ExecutionPlanView | AgentExecutionPlan | OperationalTaskPlan; nodeActions?: Record }) { return
{ plan.nodes.some((node) => node.kind) ? : }
; } function SemanticExecutionFlow({ plan, nodeActions }: { plan: ExecutionPlanView; nodeActions: Record }) { const nodeById = new Map(plan.nodes.map((node) => [node.id, node])); const activeEdges = plan.edges.filter((edge) => { const source = nodeById.get(edge.from); return source?.kind !== "decision" || source.selected_edge_id === edge.id; }); const roots = plan.nodes.filter((node) => !plan.edges.some((edge) => edge.to === node.id)); const visible = new Set(); const queue = roots.map((node) => node.id); while (queue.length > 0) { const current = queue.shift()!; if (visible.has(current)) continue; visible.add(current); for (const edge of activeEdges.filter((item) => item.from === current)) queue.push(edge.to); } const ordered = topologicalNodes(plan); const alternatives = ordered.filter((node) => !visible.has(node.id)); return
{roots.filter((node) => visible.has(node.id)).map((node) => )}
{alternatives.length > 0 ?
    {alternatives.map((node) =>
  1. )}
: null}
; } function SemanticPath({ nodeId, stopId, plan, activeEdges, nodeById, nodeActions, seen }: { nodeId: string; stopId: string | null; plan: ExecutionPlanView; activeEdges: ExecutionEdge[]; nodeById: Map; nodeActions: Record; seen: Set; }) { if (nodeId === stopId || seen.has(nodeId)) return null; const node = nodeById.get(nodeId); if (!node) return null; const nextSeen = new Set(seen).add(nodeId); const outgoing = activeEdges.filter((edge) => edge.from === nodeId); const allRoutes = plan.edges.filter((edge) => edge.from === nodeId); const join = node.kind === "parallel_split" ? findSemanticJoin(outgoing, activeEdges, nodeById, stopId) : null; const branchEnd = join?.id ?? stopId; return
{node.kind === "decision" ?
路线选择:{allRoutes.find((edge) => edge.id === node.selected_edge_id)?.condition ?? allRoutes.find((edge) => edge.id === node.selected_edge_id)?.label ?? "等待判断"}
{allRoutes.map((edge) => `${edge.condition ?? edge.label ?? "未命名路线"}${edge.id === node.selected_edge_id ? "(已选择)" : ""}`).join(" · ")}
: null} {node.kind === "parallel_split" ?

{outgoing.map((edge, index) =>

分支 {index + 1} · {edge.condition ?? edge.label ?? nodeById.get(edge.to)?.title}

)}
: outgoing[0] ? : null} {join ? : null}
; } function findSemanticJoin(outgoing: ExecutionEdge[], activeEdges: ExecutionEdge[], nodeById: Map, stopId: string | null) { if (outgoing.length < 2) return null; const reachable = outgoing.map((edge) => reachableFrom(edge.to, new Map([...nodeById.keys()].map((id) => [id, activeEdges.filter((item) => item.from === id)])))); for (const node of nodeById.values()) { if (node.id === stopId) continue; if (node.kind === "parallel_join" && reachable.every((items) => items.has(node.id))) return node; } return null; } function ExactDependencyList({ plan, nodeActions }: { plan: ExecutionPlanView; nodeActions: Record }) { const titleById = new Map(plan.nodes.map((node) => [node.id, node.title])); return

历史流程按原始依赖展示;未标注路线或并行类型。

    {topologicalNodes(plan).map((node) => { const prerequisites = plan.edges.filter((edge) => edge.to === node.id); return
  1. {prerequisites.length > 0 ?

    前置:{prerequisites.map((edge) => `${titleById.get(edge.from) ?? edge.from}${edge.condition || edge.label ? `(${edge.condition ?? edge.label})` : ""}`).join("、")}

    : null}
  2. ; })}
; } function ExecutionNodeCard({ node, action, compact = false }: { node: ExecutionNode; action?: ReactNode; compact?: boolean }) { const { tone, Icon, label } = executionNodePresentation(node); return (
{node.title}{label}
{node.kind && node.kind !== "activity" ?

{{ decision: "条件判断", parallel_split: "并行分叉", parallel_join: "并行汇合", gate: "等待审核" }[node.kind]}

: null}

{node.detail || (node.owner === "human" ? "等待人工补充执行事实。" : "等待 Agent 更新执行事实。")}

{action ?
{action}
: null}
); } function ExternalProgress({ evidence }: { evidence: OperationalTaskEvidence[] }) { const ordered = [...evidence].sort((left, right) => Date.parse(left.observed_at) - Date.parse(right.observed_at)); return (
    {ordered.map((item, index) => (
  1. {index < ordered.length - 1 ?
    {item.source_label}{formatDateTime(item.observed_at)}

    {item.summary}

  2. ))}
); } function EvidenceSection({ evidence, referencedIds, onLoadSnapshot }: { evidence: OperationalTaskEvidence[]; referencedIds: string[]; onLoadSnapshot: (evidenceId: string) => Promise }) { const ordered = useMemo(() => { const referenced = new Set(referencedIds); return [...evidence].sort((left, right) => Number(referenced.has(right.evidence_id)) - Number(referenced.has(left.evidence_id))); }, [evidence, referencedIds]); return (

关键证据

{ordered.length} 条
{ordered.length > 0 ?
{ordered.map((item) => )}
:

尚无可核验的支撑证据。

}
); } function EvidenceDisclosure({ item, onLoadSnapshot }: { item: OperationalTaskEvidence; onLoadSnapshot: (evidenceId: string) => Promise }) { const [snapshot, setSnapshot] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const load = async () => { setLoading(true); setError(null); try { setSnapshot(await onLoadSnapshot(item.evidence_id)); } catch (nextError) { setError(nextError instanceof Error ? nextError.message : "数据快照读取失败。"); } finally { setLoading(false); } }; return {item.summary}} className="surface-control rounded-lg border border-slate-200/70 px-2.5">
{item.source_ref ? : null}
{item.has_snapshot && !snapshot ? : null} {error ?

{error}

: null} {snapshot ? : null}
; } function SnapshotSummary({ snapshot }: { snapshot: OperationalTaskDataSnapshot }) { const rows = snapshotRows(snapshot.data).slice(0, 12); return

{snapshot.source_ref} · 查询于 {formatDateTime(snapshot.queried_at)}

{rows.map(([key, value]) => )}
{snapshot.truncated ?

数据已按有界查询范围截断。

: null}
; } function snapshotRows(value: unknown, prefix = ""): [string, string][] { if (Array.isArray(value)) return value.slice(0, 6).flatMap((item, index) => snapshotRows(item, `${prefix}${index + 1}`)); if (value && typeof value === "object") return Object.entries(value as Record).flatMap(([key, item]) => snapshotRows(item, prefix ? `${prefix}.${key}` : key)); return [[prefix || "值", value === null || value === undefined ? "空" : String(value)]]; } function EvidenceMeta({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) { return
{label}
{value}
; } export function AccessRequestCard({ request, nodeTitle, canDecide, onDecision, onRefresh }: { request: OperationalTask["access_requests"][number]; nodeTitle?: string; canDecide: boolean; onDecision?: AgentExecutionPlanPanelProps["onAccessDecision"]; onRefresh: () => void; }) { const [selected, setSelected] = useState(request.scopes); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const decide = async (decision: "approved" | "rejected") => { if (!onDecision) return; setBusy(true); setError(null); try { await onDecision(request.access_request_id, request.version, decision, decision === "approved" ? selected : []); } catch (nextError) { setError(nextError instanceof Error ? nextError.message : "审核提交失败"); if (/409|版本|conflict/i.test(String(nextError))) onRefresh(); } finally { setBusy(false); } }; return

数据访问申请

{request.status === "approved" ? "已批准" : request.status === "rejected" ? "已拒绝" : "待审核"}

{request.reason}

关联步骤 {nodeTitle ?? request.node_id} · 申请于 {formatDateTime(request.requested_at)}

{request.scopes.map((scope) => )}
{request.status !== "pending" ?

{request.status === "approved" ? `已授权:${request.approved_scopes.map(dataScopeLabel).join("、")}` : "未授予数据访问权限"}

: null} {request.status === "pending" && canDecide && onDecision ?
: null} {request.status === "pending" && !canDecide ?

等待具有该数据源读取权限的项目成员审核。

: null} {error ?

{error}

: null}
; } export function HumanActionCard({ task, action, onDecision, onExecute, onDraftUpdate, onConfirmDispatch, onReconcile, onRefresh }: { task: OperationalTask; action: OperationalTaskAction; onDecision: AgentExecutionPlanPanelProps["onDecision"]; onExecute: AgentExecutionPlanPanelProps["onExecute"]; onDraftUpdate?: AgentExecutionPlanPanelProps["onDraftUpdate"]; onConfirmDispatch?: AgentExecutionPlanPanelProps["onConfirmDispatch"]; onReconcile: AgentExecutionPlanPanelProps["onReconcile"]; onRefresh: () => void; }) { const [submitting, setSubmitting] = useState<"approve" | "reject" | "execute" | "reconcile" | "save" | "confirm" | null>(null); const [error, setError] = useState(null); const [stale, setStale] = useState(false); const [rejectComment, setRejectComment] = useState(""); const [editing, setEditing] = useState(false); const [savedVersion, setSavedVersion] = useState(null); const [draft, setDraft] = useState>(action.payload); const command = action.kind === "issue_command"; const canDecide = command ? task.permissions.can_decide_commands : task.permissions.can_decide_work_orders; const canExecute = command ? task.permissions.can_execute_commands : task.permissions.can_execute_work_orders; const run = async (kind: "approve" | "reject" | "execute" | "reconcile" | "save" | "confirm") => { setSubmitting(kind); setError(null); try { if (kind === "save") { if (!onDraftUpdate) throw new Error("工单草稿编辑不可用"); await onDraftUpdate(action.action_id, action.version, draft); setSavedVersion(action.version); setEditing(false); } else if (kind === "confirm") { if (!onConfirmDispatch) throw new Error("工单确认下发不可用"); await onConfirmDispatch(action.action_id, action.version); } else if (kind === "execute") await onExecute(action.action_id, action.version); else if (kind === "reconcile") await onReconcile(action.action_id, action.version); else { if (kind === "reject" && !rejectComment.trim()) { setError("请填写退回理由后再拒绝动作。"); return; } await onDecision(action.action_id, action.version, kind === "approve" ? "approved" : "rejected", kind === "reject" ? rejectComment.trim() : undefined); } } catch (nextError) { const message = nextError instanceof Error ? nextError.message : "人工动作提交失败。"; setError(message); setStale(/version|版本|409|conflict/i.test(message)); } finally { setSubmitting(null); } }; return (

{command ? "命令下发确认" : "工单创建确认"}

{approvalLabel(action.approval_status)}{executionLabel(action.execution_status)}

原因:{action.reason}

{editing && !command ?
setDraft({ ...draft, title: event.target.value })} /> setDraft({ ...draft, description: event.target.value })} />
setDraft({ ...draft, location: event.target.value || null })} /> setDraft({ ...draft, assignee: event.target.value || null })} /> setDraft({ ...draft, scheduled_at: event.target.value ? new Date(event.target.value).toISOString() : null })} />
: null} {action.evidence_refs.length > 0 ? (

确认依据

    {action.evidence_refs.map((evidenceId) => { const evidence = task.evidence.find((item) => item.evidence_id === evidenceId); return
  • {evidence ? `${evidence.summary} · ${evidence.source_label}` : `证据 ${evidenceId}`}
  • ; })}
) : null} {action.receipt ? : null} {action.execution_detail && !action.receipt ?

{action.execution_detail}

: null}
{JSON.stringify(action.payload, null, 2)}

确认版本 v{action.version}。内容或版本变化后必须重新确认。

{error ?

{error} 当前状态未按成功处理。

: null} {stale ? : null} {!canDecide && action.approval_status === "pending" ?

当前账号仅可查看,需要具备此动作审批权限的人员确认。

: null} {action.approval_status === "pending" && canDecide ? setRejectComment(event.target.value)} /> : null}
{action.approval_status === "pending" && canDecide ? <>{!command && onDraftUpdate ? : null} : null} {action.approval_status === "approved" && action.execution_status === "not_submitted" ? : null} {action.approval_status === "approved" && ["submitted", "unknown"].includes(action.execution_status) ? : null}
); } function ActionReceipt({ receipt }: { receipt: NonNullable }) { return

执行回执

{receipt.completed_at ? : null}
; } function ActionPayloadSummary({ payload }: { payload: Record }) { const rows = Object.entries(payload).flatMap(([key, value]) => { if (key !== "parameters" && value && typeof value === "object" && !Array.isArray(value)) { return Object.entries(value as Record).map(([childKey, childValue]) => ({ key: `${key}.${childKey}`, value: childValue })); } return [{ key, value }]; }); return
{rows.map((row) =>
{payloadLabel(row.key)}
{payloadValue(row.key, row.value)}
)}
; } function payloadLabel(key: string) { const labels: Record = { title: "工单标题", description: "工作内容", work_order_type: "工单类型", priority: "优先级", scheduled_at: "预约时间", location: "位置", assignee: "处理班组", command_type: "命令类型", "target.kind": "对象类型", "target.id": "目标对象", parameters: "执行参数", impact_summary: "影响说明", preconditions: "执行条件", expires_at: "失效时间" }; return labels[key] ?? key; } function payloadValue(key: string, value: unknown) { if (value === null || value === undefined || value === "") return "未指定"; if (key === "command_type") return knownLabel(value, { set_valve: "调节阀门" }); if (key === "target.kind") return knownLabel(value, { valve: "阀门", pump: "泵站", pipe: "管段" }); if (key === "work_order_type") return knownLabel(value, { general: "一般工单", water_quality_complaint: "水质投诉", burst_report: "爆管上报" }); if (key === "priority") return knownLabel(value, { normal: "普通", urgent: "紧急", high: "高", low: "低" }); if (key === "scheduled_at" || key === "expires_at") return typeof value === "string" ? formatDateTime(value) : String(value); if (Array.isArray(value)) return value.map(String).join("、") || "无"; if (key === "parameters" && typeof value === "object") { return Object.entries(value as Record).map(([parameter, item]) => parameter === "opening" ? `开度 ${String(item)}%` : `${parameter}=${String(item)}` ).join(";"); } if (typeof value === "object") return Object.entries(value as Record).map(([itemKey, item]) => `${itemKey}=${String(item)}`).join(";"); return String(value); } function knownLabel(value: unknown, labels: Record) { const raw = String(value); return labels[raw] ?? raw; } function externalStatusLabel(value: string) { return ({ completed: "已完成", submitted: "已提交", accepted: "已接收", failed: "失败", pending: "处理中" } as Record)[value] ?? value; } function reachableFrom(start: string, outgoing: Map) { const seen = new Set(); const stack = [start]; while (stack.length > 0) { const current = stack.pop()!; if (seen.has(current)) continue; seen.add(current); for (const edge of outgoing.get(current) ?? []) stack.push(edge.to); } return seen; } function topologicalNodes(plan: ExecutionPlanView) { const remaining = new Map(plan.nodes.map((node) => [node.id, node])); const result: ExecutionNode[] = []; while (remaining.size > 0) { const ready = plan.nodes.filter((node) => remaining.has(node.id) && plan.edges.filter((edge) => edge.to === node.id).every((edge) => !remaining.has(edge.from))); if (ready.length === 0) return result; ready.forEach((node) => { result.push(node); remaining.delete(node.id); }); } return result; } function executionNodePresentation(node: ExecutionNode) { const tone: StatusTone = node.status === "completed" ? "success" : node.status === "running" ? "info" : node.status === "failed" ? "danger" : node.status === "waiting" ? "warning" : "neutral"; const Icon = node.status === "completed" ? CheckCircle2 : node.status === "running" ? Loader2 : node.status === "waiting" || node.status === "failed" ? AlertTriangle : Clock3; const labels = { pending: "待执行", running: "执行中", waiting: "待人工/资料", completed: "已完成", failed: "失败", skipped: "不适用", cancelled: "已取消" } as const; return { tone, Icon, label: labels[node.status] }; } function taskTypeLabel(type: OperationalTask["task_type"]) { return { scheduled_flow: "定时流程", external_work_order: "外部工单", event_investigation: "事件调查" }[type]; } function dataScopeLabel(scope: string) { return ({ workbench_runtime: "工作台运行数据", scada_devices: "SCADA 设备", scada_readings: "SCADA 监测读数", peripheral_points: "外围监测点", peripheral_work_orders: "外围工单", peripheral_observations: "外围监测记录", peripheral_test_data: "外围测试数据" } as Record)[scope] ?? scope; } function taskStatusLabel(status: OperationalTask["status"]) { return { queued: "排队中", running: "处理中", waiting_human: "待人工", completed: "已完成", failed: "失败", cancelled: "已取消" }[status]; } function evidenceKindLabel(kind: OperationalTaskEvidence["kind"]) { return { system_data: "系统数据", external_feedback: "外部反馈", human_input: "人工提供", synthetic: "合成演练" }[kind]; } function formatDateTime(value: string) { const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? value : new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false }).format(parsed); } function toLocalDateTime(value: unknown) { if (typeof value !== "string") return ""; const date = new Date(value); if (Number.isNaN(date.getTime())) return ""; return new Date(date.getTime() - date.getTimezoneOffset() * 60_000).toISOString().slice(0, 16); } function approvalLabel(status: OperationalTaskAction["approval_status"]) { return { pending: "待确认", approved: "已批准", rejected: "已拒绝" }[status]; } function approvalTone(status: OperationalTaskAction["approval_status"]): StatusTone { return status === "approved" ? "success" : status === "rejected" ? "danger" : "warning"; } function executionLabel(status: OperationalTaskAction["execution_status"]) { return { not_submitted: "未提交", submitted: "已提交", succeeded: "执行成功", failed: "执行失败", unsupported: "未接执行器", unknown: "结果未知" }[status]; } function executionTone(status: OperationalTaskAction["execution_status"]): StatusTone { return status === "succeeded" ? "success" : status === "failed" || status === "unsupported" ? "danger" : status === "submitted" || status === "unknown" ? "warning" : "neutral"; }