import { type AgentRunStatus, type Approval, CAPABILITY_CATALOG, type CapabilityDescriptor, type KnownCapabilityId, type RiskLevel, } from "@umt/contracts"; import { Bot, Brain, ChevronDown, CircleSlash, Download, FolderInput, Layers, LoaderCircle, ShieldCheck, User, } from "lucide-react"; import { type ReactNode, useEffect, useId, useRef, useState } from "react"; import { toast } from "sonner"; import { type AgentRunView, type GoalReport, type InvocationAudit, isRunActive, REPORT_STATE, RUN_STATUS_LABEL, RUN_STATUS_TONE, useDecideApproval, useIsAdminSession, } from "~/api/agent.ts"; import { ApprovalCard, type ApprovalCardStatus } from "~/components/agentui/approval-card.tsx"; import { type CitationItem, CitationList } from "~/components/agentui/citations.tsx"; import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx"; import { FileDiff } from "~/components/agentui/file-diff.tsx"; import { Message, MessageAvatar, MessageContent, MessageFooter, MessageHeader, MessageMarker, MessageTyping, } from "~/components/agentui/message.tsx"; import { MessageBubble, MessageBubbleContent } from "~/components/agentui/message-bubble.tsx"; import { StreamingCursor, StreamingResponse } from "~/components/agentui/streaming-response.tsx"; import { type TodoItem, TodoList } from "~/components/agentui/todo-list.tsx"; import { ToolApproval } from "~/components/agentui/tool-approval.tsx"; import { ToolResult, ToolResultOutput, type ToolResultStatus } from "~/components/agentui/tool-result.tsx"; import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx"; import { errorMessage } from "~/lib/api.ts"; import { cn, formatDateTime } from "~/lib/utils.ts"; import type { AgentStreamState, StreamItem, ToolCallView } from "./use-agent-stream.ts"; // Agent 会话的展示层:把 use-agent-stream 折叠出的 StreamItem 映射到 agentui 组件(components/agentui/, // 改造自 github.com/ashish200729/agentui,MIT)。服务端事件形状见 packages/contracts/src/agent-events.ts。 export const BUDGET_LABEL: Record = { tool_calls: "工具调用次数", duration: "运行时长", tokens: "token", cost: "费用", }; const COMPACT_REASON: Record = { manual: "手动", threshold: "达到阈值", overflow: "上下文溢出" }; const COMPACT_STATUS: Record = { completed: "已完成,业务进度保留", declined: "未压缩", aborted: "已中止", failed: "失败", }; // ── 能力元数据(scope / 风险 / 类别来自 contracts 的 CAPABILITY_CATALOG) ── export function capabilityMeta(id: string): CapabilityDescriptor | null { return (CAPABILITY_CATALOG as Record)[id as KnownCapabilityId] ?? null; } const RISK: Record = { low: { label: "低风险", tone: "neutral" }, medium: { label: "中风险", tone: "warning" }, high: { label: "高风险", tone: "destructive" }, }; const KIND_LABEL: Record = { read: "只读", preview: "预览", propose: "提案", approve: "审批", execute: "执行", }; export function RiskBadge({ level }: { level: RiskLevel }) { const r = RISK[level]; return {r.label}; } function CapabilityBadges({ id, extra }: { id: string; extra?: ReactNode }) { const m = capabilityMeta(id); return ( <> {m ? ( <> {KIND_LABEL[m.kind] ?? m.kind} scope · {m.scope} {m.idempotent ? 幂等 : null} ) : ( 未登记能力 )} {extra} ); } function durationText(a: string, b: string | null) { if (!b) return null; const ms = new Date(b).getTime() - new Date(a).getTime(); return ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`; } const TOOL_STATUS: Record, ToolResultStatus> = { running: "running", succeeded: "success", failed: "error", }; /** * 授权校验拒绝:能力处理器前的授权回调失败时,事件是 tool_call_end(isError)+ 错误码 unauthorized/forbidden, * 不是 tool_call_rejected(那个只在白名单/预算拦截时发)。两者在界面上都按"服务端拒绝"醒目展示。 */ export function authDenial(call: ToolCallView): string | null { if (call.status !== "failed" || !call.outputSummary) return null; try { const e = (JSON.parse(call.outputSummary) as { error?: { code?: string; message?: string } }).error; if (e && (e.code === "unauthorized" || e.code === "forbidden")) return e.message ?? e.code; } catch { const m = /"code":"(unauthorized|forbidden)"(?:,"message":"([^"]*)")?/.exec(call.outputSummary); if (m) return m[2] ?? m[1] ?? "未授权"; } return null; } /** 被服务端拒绝(白名单拦截或授权校验失败) */ export const isDenied = (call: ToolCallView) => call.status === "denied" || authDenial(call) !== null; /** 工具调用卡片:脱敏输入/输出折叠、耗时、scope/风险徽标、幂等键(审计);服务端拒绝时换醒目拒绝态 */ export function ToolCallCard({ call, mode, audit, }: { call: ToolCallView; mode?: string; /** 与这次调用对应的审计记录(幂等键等);未取到为 undefined */ audit?: InvocationAudit; }) { const meta = capabilityMeta(call.capabilityId); const authReason = authDenial(call); if (call.status === "denied" || authReason) { const writeKind = meta && !["read", "preview", "propose"].includes(meta.kind); return ( {authReason ? "授权校验未通过" : "白名单拦截"}} /> } reason={authReason ?? call.reason ?? "未授权"} explanation={ authReason ? '有副作用的能力需要有效的持续授权或单次审批,由服务端在执行前核对,模型声称"用户已同意"无效。发布收录策略会生成持续授权,也可以为计划申请单次审批。' : mode === "plan_only" && writeKind ? "本次是「只要方案」,只放行只读、预览、提案类能力。要执行,点「按方案执行」。" : "这个能力不在本次运行的白名单内,或已超出调用预算,调用没有执行。" } parameters={[ { id: "call", label: "调用 ID", value: call.toolCallId }, ...(audit?.idempotencyKey ? [{ id: "idem", label: "幂等键", value: audit.idempotencyKey }] : []), { id: "at", label: "时间", value: formatDateTime(call.startedAt) }, ]} /> ); } const status = TOOL_STATUS[call.status as Exclude]; return ( 恢复补记 : null} /> } summary={call.outputSummary ?? undefined} > {call.inputSummary ? {call.inputSummary} : null} {call.outputSummary ? ( {call.outputSummary} ) : call.status === "running" ? (

等待结果…

) : null}
调用 ID
{call.toolCallId}
幂等键
{audit?.idempotencyKey ?? (meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读能力不记操作账本")}
{audit?.grantId || audit?.approvalId ? ( <>
授权依据
{audit.grantId ? `持续授权 ${audit.grantId.slice(0, 8)}` : `单次审批 ${audit.approvalId?.slice(0, 8)}`}
) : null}
); } /** 思考块:thinking_delta 累积,默认收起;流式中显示"思考中"与光标 */ function ThinkingBlock({ text, streaming }: { text: string; streaming: boolean }) { const id = useId(); const [open, setOpen] = useState(false); return (

{text} {streaming ? : null}

); } function AssistantRow({ children, animateIn, avatar = true, }: { children: ReactNode; animateIn?: boolean; avatar?: boolean; }) { return ( {children} ); } function EndNotice({ item }: { item: Extract }) { const budget = item.reason === "budget"; const aborted = item.reason === "aborted"; return (

{budget ? `超出预算${item.budget ? `(${BUDGET_LABEL[item.budget] ?? item.budget})` : ""},已停止` : aborted ? "已停止" : "运行失败"}

{item.message ?

{item.message}

: null} {budget ? (

已完成的外部动作不会撤销。调大预算后追加消息可继续运行。

) : null}
); } export function UserMessage({ text, at, mode, note, animateIn, }: { text: string; at?: string; mode?: string; note?: string; animateIn?: boolean; }) { return ( {at || mode ? ( {mode ? {mode === "plan_only" ? "只要方案" : "授权内执行"} : null} {at ? : null} ) : null} {text} {note ? {note} : null} ); } const UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; const PLAN_IN_INPUT = new RegExp(`"planId"\\s*:\\s*"(${UUID})"`); const PLAN_IN_OUTPUT = new RegExp(`"plan"\\s*:\\s*\\{\\s*"id"\\s*:\\s*"(${UUID})"`); /** 本次运行触达的计划 ID:服务端汇报里的计划 + 下载计划/提交/整理执行的输入输出里出现的计划(摘要可能截断,只取开头能匹配到的) */ export function runPlanIds(items: StreamItem[], report: GoalReport | null): string[] { const ids = new Set(report?.plans.map((p) => p.planId) ?? []); for (const it of items) { if (it.kind !== "tool") continue; const c = it.call; if (["download.submit", "organize.execute"].includes(c.capabilityId)) { const m = c.inputSummary ? PLAN_IN_INPUT.exec(c.inputSummary) : null; if (m?.[1]) ids.add(m[1]); } if (c.capabilityId === "download.plan" && c.outputSummary) { const m = PLAN_IN_OUTPUT.exec(c.outputSummary); if (m?.[1]) ids.add(m[1]); } } return [...ids]; } /** 把工具调用按能力 ID 顺序与审计记录配对(审计没有 toolCallId,按同能力的先后对齐) */ export function pairAudit(calls: ToolCallView[], audit: InvocationAudit[] | undefined) { const out = new Map(); if (!audit?.length) return out; const byCap = new Map(); for (const a of audit) byCap.set(a.capabilityId, [...(byCap.get(a.capabilityId) ?? []), a]); const used = new Map(); for (const c of [...calls].sort((a, b) => a.startedAt.localeCompare(b.startedAt))) { const list = byCap.get(c.capabilityId); const i = used.get(c.capabilityId) ?? 0; if (list?.[i]) out.set(c.toolCallId, list[i]); used.set(c.capabilityId, i + 1); } return out; } /** 会话流:用户目标 → 流式文本/思考/工具卡片/拒绝/压缩 → 结束;追加的用户消息按时间插入 */ export function AgentConversation({ run, live, userMessages = [], audit, after, }: { run: AgentRunView; live: AgentStreamState; userMessages?: { at: string; text: string; pending?: boolean }[]; audit?: InvocationAudit[]; /** 流末尾追加的内容(审批卡片、最终汇报) */ after?: ReactNode; }) { const active = isRunActive(run.status) && !live.finished; const mode = run.mode ?? (run.scope.mode as string | undefined); // 首次载入(回放)的条目不播入场动画,之后新到的才播 const baseline = useRef(null); if (baseline.current === null && (live.events.length > 0 || !active)) baseline.current = live.items.length; const fresh = (i: number) => baseline.current !== null && i >= baseline.current; const calls = live.items .filter((i): i is Extract => i.kind === "tool") .map((i) => i.call); const auditByCall = pairAudit(calls, audit); const lastIndex = live.items.length - 1; const lastText = live.items.map((i) => i.kind).lastIndexOf("text"); type Row = { key: string; at: string; node: ReactNode }; const rows: Row[] = live.items.map((it, idx) => { const streaming = active && idx === lastIndex; const anim = fresh(idx); let node: ReactNode = null; switch (it.kind) { case "text": node = ( {it.text} ); break; case "thinking": node = ( ); break; case "tool": node = ( ); break; case "compaction": node = ( 上下文压缩({COMPACT_REASON[it.reason] ?? it.reason})·{" "} {it.done ? (COMPACT_STATUS[it.status ?? "completed"] ?? it.status) : "进行中…"} ); break; case "end": node = it.ok ? null : ; break; } return { key: it.key, at: it.kind === "tool" ? it.call.startedAt : it.at, node }; }); for (const m of userMessages) rows.push({ key: `u${m.at}`, at: m.at, node: , }); rows.sort((a, b) => a.at.localeCompare(b.at)); return (
{rows.map((r) => (r.node ?
{r.node}
: null))} {active && live.items[lastIndex]?.kind !== "text" && live.items[lastIndex]?.kind !== "thinking" ? ( {run.status === "waiting" ? "等待中…" : run.status === "queued" ? "排队中…" : "正在处理…"} ) : null} {after}
); } // ── plan_only:调查步骤清单 ── export function PlanSteps({ live, active }: { live: AgentStreamState; active: boolean }) { const items: TodoItem[] = live.items .filter((i): i is Extract => i.kind === "tool") .map(({ call }) => ({ id: call.toolCallId, title: capabilityMeta(call.capabilityId)?.title ?? call.capabilityId, detail: {call.capabilityId}, status: call.status === "running" ? "in-progress" : call.status === "succeeded" ? "completed" : isDenied(call) ? "cancelled" : "error", })); items.push({ id: "report", title: "整理方案并汇报", status: active ? (items.some((i) => i.status === "in-progress") ? "pending" : "in-progress") : "completed", }); return ; } // ── 预算用量条 ── function Meter({ label, used, max, format, exceeded, }: { label: string; used: number | undefined; max?: number; format: (n: number) => string; exceeded?: boolean; }) { const pct = max && used !== undefined ? Math.min(100, (used / max) * 100) : null; const over = exceeded || (pct !== null && pct >= 100); const near = !over && pct !== null && pct >= 80; return (
{label} {over ? " · 已超限" : near ? " · 接近上限" : ""} {used === undefined ? "未报告" : format(used)} {max ? / {format(max)} : null}
{pct !== null ? (
) : null}
); } /** 预算用量条:工具调用/token/时长/费用;超限的一项标红,上游未报告的显示"未报告",费用标注估算 */ export function BudgetBar({ run, live }: { run: AgentRunView; live: AgentStreamState }) { const u = { ...run.usage, ...(live.usage ?? {}) }; const toolCalls = Math.max(run.toolCallCount, live.items.filter((i) => i.kind === "tool").length); const end = [...live.items].reverse().find((i) => i.kind === "end"); const exceeded = end?.kind === "end" && end.reason === "budget" ? end.budget : null; const durationMs = u.durationMs ?? (run.startedAt ? (run.finishedAt ? new Date(run.finishedAt).getTime() : Date.now()) - new Date(run.startedAt).getTime() : undefined); return (
String(n)} exceeded={exceeded === "tool_calls"} /> (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))} exceeded={exceeded === "tokens"} /> `${n}s`} exceeded={exceeded === "duration"} /> `$${n.toFixed(3)}`} exceeded={exceeded === "cost"} />
); } // ── 审批卡片 ── const PLAN_CAPABILITY: Record = { acquisition_plan: "download.submit", organize_plan: "organize.execute", policy_revision: "policy.apply", }; const PLAN_LABEL: Record = { acquisition_plan: "下载计划", organize_plan: "整理计划", policy_revision: "策略版本", }; export function ApprovalItem({ approval: a }: { approval: Approval }) { const admin = useIsAdminSession(); const decide = useDecideApproval(); const cap = PLAN_CAPABILITY[a.planType]; const risk = cap ? capabilityMeta(cap)?.riskLevel : undefined; const expired = a.status === "pending" && a.expiresAt !== null && new Date(a.expiresAt).getTime() < Date.now(); const status: ApprovalCardStatus = decide.isPending ? "submitting" : expired ? "expired" : a.status; const act = (s: "approved" | "rejected") => decide.mutate( { id: a.id, status: s, payloadHash: a.payloadHash }, { onSuccess: () => toast.success(s === "approved" ? "已批准" : "已拒绝"), onError: (e) => toast.error(errorMessage(e)), }, ); return ( {risk ? : null} {cap ? 执行能力 · {cap} : null} } onApprove={() => act("approved")} onReject={() => act("rejected")} disabledReason={admin ? undefined : "只有管理员能批准或拒绝"} result={ a.decidedAt ? `${a.status === "approved" ? "批准" : a.status === "rejected" ? "拒绝" : "处理"}于 ${formatDateTime(a.decidedAt)}` : undefined } >
计划
{a.planType} · {a.planId}
payload hash
{a.payloadHash}
版本
r{a.expectedRevision}
到期
{a.expiresAt ? formatDateTime(a.expiresAt) : "不过期"} {expired ? "(已过期)" : ""}
); } // ── 最终汇报:模型说的 vs 系统核实的 ── const PLAN_STATUS_TONE = (s: string): StatusTone => ["succeeded", "completed", "submitted", "approved"].includes(s) ? "success" : ["failed", "rejected", "error", "cancelled"].includes(s) ? "destructive" : "warning"; /** 最终汇报:左"模型说的(未经核查)",右"系统核实的结果"(服务端从下载计划/下载任务/整理记录汇总,不采信模型自述) */ export function FinalReport({ run, report, live, }: { run: AgentRunView; report: GoalReport | null; live: AgentStreamState; }) { const lastTool = live.items.map((i) => i.kind).lastIndexOf("tool"); const tail = live.items .slice(lastTool + 1) .filter((i): i is Extract => i.kind === "text") .map((i) => i.text) .join(""); const said = report?.modelText ?? (tail || run.summary || ""); const calls = live.items .filter((i): i is Extract => i.kind === "tool") .map((i) => i.call); const denied = calls.filter(isDenied).length; const count = (s: ToolCallView["status"]) => calls.filter((c) => c.status === s && !isDenied(c)).length; const state = report ? REPORT_STATE[report.state] : null; const sources: CitationItem[] = report ? [ ...report.downloads.map((d) => ({ id: `d${d.downloadTaskId}`, icon: , title: d.name ?? `下载任务 ${d.downloadTaskId.slice(0, 8)}`, domain: "下载任务", meta: {d.status}, to: `/console/downloads/${d.downloadTaskId}`, })), ...report.organization.map((o) => ({ id: `o${o.recordId}`, icon: , title: o.targetPath ?? `整理记录 ${o.recordId.slice(0, 8)}`, domain: "整理记录", meta: {o.status}, to: `/console/organize/${o.recordId}`, })), ] : []; const imported = report?.organization.filter((o) => o.targetPath) ?? []; return (

模型说的(未经核查)

{said || "(没有文字汇报)"}

仅供参考,不作为完成依据。

{report ? ( ) : ( )} 系统核实的结果

{state ? {state.label} : null} 运行{RUN_STATUS_LABEL[run.status]} 工具成功 {count("succeeded")} {count("failed") ? 失败 {count("failed")} : null} {denied ? 被拒绝 {denied} : null}
{report ? ( <>

{report.summary}

{( [ ["已提交", report.submitted], ["已入库", report.imported], ["进行中", report.inProgress], ["失败", report.failed], ] as const ).map(([k, v]) => (
{k}
{v}
))}
{report.plans.length ? (
    {report.plans.map((p) => (
  • 下载计划 {p.planId.slice(0, 8)} {p.status}
  • ))}
) : null} {sources.length ? : null} {imported.length ? ( ({ id: o.recordId, type: o.status === "succeeded" ? ("added" as const) : ("context" as const), content: o.targetPath, note: o.status, }))} /> ) : null}

取自下载计划、下载任务和整理记录(生成于 {formatDateTime(report.generatedAt)} )。"已提交"指已进入下载器,整理成功才算入库。

) : (

服务端汇报还没有生成,以任务和整理记录的实际状态为准。

)} {run.error ? (

{String((run.error as { message?: unknown }).message ?? JSON.stringify(run.error))}

) : null}
); } export function RunStatusBadge({ status }: { status: AgentRunStatus }) { return ( {isRunActive(status) ? : null} {RUN_STATUS_LABEL[status]} ); } /** 滚动时保持最新一次状态播报(给读屏用的礼貌播报,流式正文本身不逐字播报) */ export function RunAnnouncer({ status, finished }: { status: AgentRunStatus; finished: boolean }) { const [msg, setMsg] = useState(""); const prev = useRef(status); useEffect(() => { if (prev.current !== status) setMsg(`运行${RUN_STATUS_LABEL[status]}`); prev.current = status; }, [status]); useEffect(() => { if (finished) setMsg("最终汇报已生成"); }, [finished]); return ( {msg} ); }