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, isProposeFirst, isRunActive, REPORT_STATE, RUN_STATUS_LABEL, RUN_STATUS_TONE, useDecideApproval, useIsAdminSession, } from "~/api/agent.ts"; import { DOWNLOAD_STATUS_LABEL, PLAN_STATUS_LABEL } from "~/api/downloads.ts"; import { RECORD_STATUS_LABEL } from "~/api/records.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 { Markdown } from "~/components/agentui/markdown.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 { 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 { TechInfo } from "~/components/console/tech-info.tsx"; import { formatUsd } from "~/components/schedules/labels.ts"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { humanizeReason } from "~/lib/reasons.ts"; import { cn, formatDateTime } from "~/lib/utils.ts"; import { InlineApproval, OptionsCard } from "./agent-interaction.tsx"; import { ToolCallIo } from "./tool-call-card-io.tsx"; 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 ( <> {/* 只标需要注意的:会改动数据的动作和中高风险;只读、scope、幂等进技术信息 */} {m ? ( <> {m.kind !== "read" ? {KIND_LABEL[m.kind] ?? m.kind} : null} {m.riskLevel !== "low" ? : null} ) : 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; /** * 工具卡收起时那一行的摘要:错误取 message;数组 / items 取条数;对象取前几个简单字段; * 其余截断原文。只做展示,不做判断(纯函数,单测覆盖)。 */ export function toolSummary(call: Pick): string | null { const raw = call.outputSummary?.trim(); if (!raw) return call.status === "running" ? "等待结果…" : null; const clip = (t: string) => (t.length > 120 ? `${t.slice(0, 120)}…` : t); let v: unknown; try { v = JSON.parse(raw); } catch { return clip(raw.replace(/\s+/g, " ")); } if (Array.isArray(v)) return `${v.length} 条`; if (v && typeof v === "object") { const o = v as Record; const err = o.error as { message?: unknown; code?: unknown } | undefined; if (err && typeof err === "object") return clip(String(err.message ?? err.code ?? "失败")); for (const k of ["summary", "message"]) if (typeof o[k] === "string") return clip(o[k] as string); for (const k of ["items", "results", "releases", "candidates"]) if (Array.isArray(o[k])) return `${(o[k] as unknown[]).length} 条结果`; const simple = Object.entries(o) .filter(([, x]) => ["string", "number", "boolean"].includes(typeof x)) .slice(0, 3) .map(([k, x]) => `${k}: ${String(x)}`); if (simple.length) return clip(simple.join(" · ")); return `${Object.keys(o).length} 个字段`; } return clip(String(v)); } /** 工具调用卡片:脱敏输入/输出折叠、耗时、scope/风险徽标、幂等键(审计);服务端拒绝时换醒目拒绝态 */ export function ToolCallCard({ call, audit, }: { call: ToolCallView; /** 与这次调用对应的审计记录(幂等键等);未取到为 undefined */ audit?: InvocationAudit; }) { const meta = capabilityMeta(call.capabilityId); const authReason = authDenial(call); if (call.status === "denied" || authReason) { return ( {authReason ? "需要你批准" : "本次没有开放"}} /> } reason={authReason ?? call.reason ?? "未授权"} explanation={ authReason ? "会改动数据的操作要你批准:在对应步骤的审批卡上批准,或在设置 → 智能体里给它持续授权。" : "这次会话没有开放这个操作,或已用完调用次数。" } parameters={[ { id: "cap", label: "能力", value: call.capabilityId }, { 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={toolSummary(call) ?? undefined} > {call.inputSummary ? {call.inputSummary} : null} {call.outputSummary ? ( {call.outputSummary} ) : call.status === "running" ? (

等待结果…

) : null}
能力
{call.capabilityId} {meta ? ` · ${KIND_LABEL[meta.kind] ?? meta.kind} · scope ${meta.scope}${meta.idempotent ? " · 幂等" : ""}` : ""}
{durationText(call.startedAt, call.endedAt) ? ( <>
耗时
{durationText(call.startedAt, call.endedAt)}
) : 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, tag, note, animateIn, }: { text: string; at?: string; /** 会话选项标记(如"先看方案再执行") */ tag?: string; note?: string; animateIn?: boolean; }) { return ( {at || tag ? ( {tag ? {tag} : 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; } // ── 消息分组:同一助手回合(两条用户消息 / 系统提示之间)的文本、思考、工具卡归为一组,时间戳只在组头; // 组内连续 ≥3 条已结束的思考 / 工具调用再折叠成一行"活动"(被拒绝、运行中的保持展开,醒目可见) ── export type ConvEntry = { key: string; at: string; role: "user" | "assistant" | "system"; /** 可并入"活动"折叠(已结束的思考 / 工具调用) */ foldable: boolean; /** 折叠摘要里的名字(能力名 / "思考") */ label?: string; failed?: boolean; node: ReactNode; }; export type ConvPart = { kind: "item"; entry: ConvEntry } | { kind: "fold"; key: string; entries: ConvEntry[] }; export type ConvBlock = | { kind: "single"; entry: ConvEntry } | { kind: "turn"; key: string; at: string; parts: ConvPart[] }; function foldRuns(parts: ConvPart[], min: number): ConvPart[] { const out: ConvPart[] = []; let run: ConvEntry[] = []; const flush = () => { if (run.length >= min) out.push({ kind: "fold", key: `f${run[0]?.key}`, entries: run }); else for (const e of run) out.push({ kind: "item", entry: e }); run = []; }; for (const p of parts) { if (p.kind === "item" && p.entry.foldable) run.push(p.entry); else { flush(); out.push(p); } } flush(); return out; } /** 纯函数(单测覆盖):按时间排好的条目 → 用户/系统单条 + 助手回合组 */ export function groupConversation(entries: ConvEntry[], minFold = 3): ConvBlock[] { const out: ConvBlock[] = []; let turn: Extract | null = null; for (const e of entries) { if (e.role !== "assistant") { turn = null; out.push({ kind: "single", entry: e }); continue; } if (!turn) { turn = { kind: "turn", key: `t${e.key}`, at: e.at, parts: [] }; out.push(turn); } turn.parts.push({ kind: "item", entry: e }); } for (const b of out) if (b.kind === "turn") b.parts = foldRuns(b.parts, minFold); return out; } function clockText(iso: string) { const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; const sameDay = new Date().toDateString() === d.toDateString(); return sameDay ? d.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }) : formatDateTime(iso); } /** 折叠的一串活动:一行摘要(次数 + 能力名预览 + 失败数),展开看每张卡 */ function ActivityFold({ entries }: { entries: ConvEntry[] }) { const id = useId(); const [open, setOpen] = useState(false); const tools = entries.filter((e) => e.label !== "思考").length; const thoughts = entries.length - tools; const failed = entries.filter((e) => e.failed).length; const names = [...new Set(entries.filter((e) => e.label && e.label !== "思考").map((e) => e.label))]; return (
{entries.map((e) => (
{e.node}
))}
); } /** 会话流:用户目标 → 助手回合(流式文本/思考/工具卡片/方案/审批)→ 结束;追加的用户消息按时间插入 */ 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 waiting = run.status === "waiting" || run.status === "running" || run.status === "queued"; const stepTitle = (id: string | null) => (id ? (live.plan?.steps.find((s) => s.id === id)?.title ?? null) : null); // 首次载入(回放)的条目不播入场动画,之后新到的才播 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"); const entries: ConvEntry[] = []; live.items.forEach((it, idx) => { const streaming = active && idx === lastIndex; const anim = fresh(idx); const wrap = (node: ReactNode) => anim ? (
{node}
) : ( node ); switch (it.kind) { case "text": entries.push({ key: it.key, at: it.at, role: "assistant", foldable: false, node: wrap( {/* 流式中按原文显示(半截 Markdown 会闪),结束后按安全子集渲染 */} {streaming ? it.text : } , ), }); break; case "thinking": entries.push({ key: it.key, at: it.at, role: "assistant", foldable: !streaming, label: "思考", node: wrap(), }); break; case "tool": { const denied = isDenied(it.call); entries.push({ key: it.key, at: it.call.startedAt, role: "assistant", foldable: !denied && it.call.status !== "running" && idx !== lastIndex, label: capabilityMeta(it.call.capabilityId)?.title ?? it.call.capabilityId, failed: it.call.status === "failed", node: wrap(), }); break; } case "compaction": entries.push({ key: it.key, at: it.at, role: "system", foldable: false, node: ( 上下文压缩({COMPACT_REASON[it.reason] ?? it.reason})·{" "} {it.done ? (COMPACT_STATUS[it.status ?? "completed"] ?? it.status) : "进行中…"} ), }); break; case "options": entries.push({ key: it.key, at: it.at, role: "assistant", foldable: false, node: wrap(), }); break; case "approval": entries.push({ key: it.key, at: it.at, role: "assistant", foldable: false, node: wrap(), }); break; case "end": if (!it.ok) entries.push({ key: it.key, at: it.at, role: "system", foldable: false, node: }); break; } }); for (const m of userMessages) entries.push({ key: `u${m.at}`, at: m.at, role: "user", foldable: false, node: , }); entries.sort((a, b) => a.at.localeCompare(b.at)); const blocks = groupConversation(entries); const tail = live.items[lastIndex]?.kind; const typing = active && tail !== "text" && tail !== "thinking" ? ( {run.status === "waiting" ? `${run.summary === "等待选择方案" || run.summary === "等待审批" ? run.summary : "等待中"}…` : run.status === "queued" ? "排队中…" : "正在处理…"} ) : null; const lastBlock = blocks[blocks.length - 1]; const typingInTurn = typing && lastBlock?.kind === "turn"; return (
{blocks.map((b) => b.kind === "single" ? (
{b.entry.node}
) : ( 智能体 {b.parts.map((p) => p.kind === "fold" ? ( ) : (
{p.entry.node}
), )} {typingInTurn && b === lastBlock ? typing : null}
), )} {typing && !typingInTurn ? {typing} : null} {after}
); } // ── 预算用量条 ── 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, stacked = false, }: { run: AgentRunView; live: AgentStreamState; /** 侧栏里单列排 */ stacked?: boolean; }) { 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 (
formatUsd(n)} exceeded={exceeded === "cost"} /> `${n} 秒`} exceeded={exceeded === "duration"} />
{/* 调用次数与 token 超限时直接显示,否则收进技术信息 */} {exceeded === "tool_calls" || exceeded === "tokens" ? (
String(n)} exceeded={exceeded === "tool_calls"} /> (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))} exceeded={exceeded === "tokens"} />
) : ( )}
); } // ── 审批卡片 ── 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"; /** 长汇报:超过 ~12 行或 600 字按长文处理(纯函数,单测覆盖) */ export function isLongReport(text: string): boolean { return text.length > 600 || text.split("\n").length > 12; } /** 长文折叠:收起时限高 + 底部渐隐,"展开全文"/"收起" */ function LongText({ long, children }: { long: boolean; children: ReactNode }) { const id = useId(); const [open, setOpen] = useState(false); if (!long) return <>{children}; return (
{children}
); } /** 最终汇报:左"模型说的(未经核查)",右"系统核实的结果"(服务端从下载计划/下载任务/整理记录汇总,不采信模型自述) */ 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 ?? "下载任务", domain: "下载任务", meta: ( {DOWNLOAD_STATUS_LABEL[d.status as keyof typeof DOWNLOAD_STATUS_LABEL] ?? d.status} ), to: `/console/downloads/${d.downloadTaskId}`, })), ...report.organization.map((o) => ({ id: `o${o.recordId}`, icon: , title: o.targetPath ?? "整理记录", domain: "整理记录", meta: ( {RECORD_STATUS_LABEL[o.status as keyof typeof RECORD_STATUS_LABEL] ?? o.status} ), to: `/console/organize/${o.recordId}`, })), ] : []; const imported = report?.organization.filter((o) => o.targetPath) ?? []; return (

模型说的(未经核查)

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

{report ? ( ) : ( )} 实际结果

{state || count("failed") || denied ? (
{state ? {state.label} : null} {count("failed") ? {count("failed")} 步失败 : null} {denied ? {denied} 步未执行 : null}
) : null} {report ? ( <>

{report.summary}

{( [ ["已提交", report.submitted], ["已入库", report.imported], ["进行中", report.inProgress], ["失败", report.failed], ] as const ) .filter(([, v]) => v > 0) .map(([k, v]) => (
{k}
{v}
))}
{report.plans.length ? (
    {report.plans.map((p) => (
  • 下载计划 {PLAN_STATUS_LABEL[p.status as keyof typeof PLAN_STATUS_LABEL] ?? 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} ) : (

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

)} {run.error ? ( <>

{humanizeReason(String((run.error as { message?: unknown }).message ?? "")) || "会话出错"}

) : 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} ); }