feat(web): 智能体会话去噪——单一免责、短标题、技术信息折叠(DEAI-2)

- 「未经核查」只在最终汇报左栏标题保留一处,删「仅供参考」与数据来源说明;
  右栏改叫「实际结果」,去掉「运行已完成」「工具成功 N」徽章,计数只列非零项,
  状态值用中文标签,不再显示计划 / 记录短 ID
- 工具卡片只标会改数据的动作和中高风险;能力 ID、scope、幂等、耗时、调用 ID、
  schema 入口收进技术信息;拒绝说明改成一句「要你批准」
- 用量只显示费用与时长,工具调用数、token、模型、会话 ID 进技术信息(超限时照常显示)
- 会话标题 shortGoalTitle:去上下文前缀、UUID、JSON 参数,能力 ID 换中文名,取第一句,40 字截断
- 入口去口号:删「交代目标,边做边汇报」与抽屉里的重复快捷键说明
- 对话组件结构不动

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 20:52:48 +08:00
1 parent 260f3ab27f
commit 5a717e414c
11 files changed
+223 -143

No files matched your search

@@ -32,6 +32,8 @@ import {
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";
@@ -51,8 +53,11 @@ import { StreamingCursor, StreamingResponse } from "~/components/agentui/streami
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";
@@ -103,16 +108,13 @@ function CapabilityBadges({ id, extra }: { id: string; extra?: ReactNode }) {
const m = capabilityMeta(id);
return (
<>
{/* 只标需要注意的:会改动数据的动作和中高风险;只读、scope、幂等进技术信息 */}
{m ? (
<>
<StatusBadge>{KIND_LABEL[m.kind] ?? m.kind}</StatusBadge>
<StatusBadge>scope · {m.scope}</StatusBadge>
<RiskBadge level={m.riskLevel} />
{m.idempotent ? <StatusBadge>幂等</StatusBadge> : null}
{m.kind !== "read" ? <StatusBadge>{KIND_LABEL[m.kind] ?? m.kind}</StatusBadge> : null}
{m.riskLevel !== "low" ? <RiskBadge level={m.riskLevel} /> : null}
</>
) : (
<StatusBadge>未登记能力</StatusBadge>
)}
) : null}
{extra}
</>
);
@@ -197,20 +199,21 @@ export function ToolCallCard({
<ToolApproval
status="rejected"
tool={call.capabilityId}
title={`服务端拒绝了「${meta?.title ?? call.capabilityId}」`}
title={`未执行:${meta?.title ?? "这个操作"}`}
badges={
<CapabilityBadges
id={call.capabilityId}
extra={<StatusBadge tone="destructive">{authReason ? "授权校验未通过" : "白名单拦截"}</StatusBadge>}
extra={<StatusBadge tone="destructive">{authReason ? "需要你批准" : "本次没有开放"}</StatusBadge>}
/>
}
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) },
@@ -222,9 +225,8 @@ export function ToolCallCard({
return (
<ToolResult
status={status}
tool={call.capabilityId}
tool={meta ? null : call.capabilityId}
title={meta?.title ?? call.capabilityId}
meta={durationText(call.startedAt, call.endedAt) ?? undefined}
badges={
<CapabilityBadges
id={call.capabilityId}
@@ -244,23 +246,39 @@ export function ToolCallCard({
) : call.status === "running" ? (
<p className="text-meta text-muted-foreground">等待结果…</p>
) : null}
<dl className="grid grid-cols-[minmax(0,6rem)_minmax(0,1fr)] gap-x-3 gap-y-1 text-meta">
<dt className="text-muted-foreground">调用 ID</dt>
<dd className="min-w-0 font-mono break-all text-foreground/80">{call.toolCallId}</dd>
<dt className="text-muted-foreground">幂等键</dt>
<dd className="min-w-0 font-mono break-all text-foreground/80">
{audit?.idempotencyKey ?? (meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读操作不记录")}
</dd>
{audit?.grantId || audit?.approvalId ? (
<>
<dt className="text-muted-foreground">授权依据</dt>
<dd className="min-w-0 font-mono break-all text-foreground/80">
{audit.grantId ? `持续授权 ${audit.grantId.slice(0, 8)}` : `单次审批 ${audit.approvalId?.slice(0, 8)}`}
</dd>
</>
) : null}
</dl>
<ToolCallIo call={call} />
<TechInfo>
<dl className="grid grid-cols-[minmax(0,6rem)_minmax(0,1fr)] gap-x-3 gap-y-1 text-meta">
<dt className="text-muted-foreground">能力</dt>
<dd className="min-w-0 font-mono break-all text-foreground/80">
{call.capabilityId}
{meta
? ` · ${KIND_LABEL[meta.kind] ?? meta.kind} · scope ${meta.scope}${meta.idempotent ? " · 幂等" : ""}`
: ""}
</dd>
{durationText(call.startedAt, call.endedAt) ? (
<>
<dt className="text-muted-foreground">耗时</dt>
<dd className="min-w-0 text-foreground/80">{durationText(call.startedAt, call.endedAt)}</dd>
</>
) : null}
<dt className="text-muted-foreground">调用 ID</dt>
<dd className="min-w-0 font-mono break-all text-foreground/80">{call.toolCallId}</dd>
<dt className="text-muted-foreground">幂等键</dt>
<dd className="min-w-0 font-mono break-all text-foreground/80">
{audit?.idempotencyKey ??
(meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读操作不记录")}
</dd>
{audit?.grantId || audit?.approvalId ? (
<>
<dt className="text-muted-foreground">授权依据</dt>
<dd className="min-w-0 font-mono break-all text-foreground/80">
{audit.grantId ? `持续授权 ${audit.grantId.slice(0, 8)}` : `单次审批 ${audit.approvalId?.slice(0, 8)}`}
</dd>
</>
) : null}
</dl>
<ToolCallIo call={call} />
</TechInfo>
</ToolResult>
);
}
@@ -772,35 +790,60 @@ export function BudgetBar({
? (run.finishedAt ? new Date(run.finishedAt).getTime() : Date.now()) - new Date(run.startedAt).getTime()
: undefined);
return (
<div className={cn("grid gap-x-4 gap-y-2", stacked ? "grid-cols-1 gap-y-2.5" : "grid-cols-2 sm:grid-cols-4")}>
<Meter
label="工具调用"
used={toolCalls}
max={run.budget.maxToolCalls}
format={(n) => String(n)}
exceeded={exceeded === "tool_calls"}
/>
<Meter
label="token"
used={u.totalTokens}
max={run.budget.maxTokens}
format={(n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))}
exceeded={exceeded === "tokens"}
/>
<Meter
label="时长"
used={durationMs === undefined ? undefined : Math.round(durationMs / 1000)}
max={run.budget.maxDurationSec}
format={(n) => `${n}s`}
exceeded={exceeded === "duration"}
/>
<Meter
label="费用(估算)"
used={u.costUsd}
max={run.budget.maxCostUsd}
format={(n) => `$${n.toFixed(3)}`}
exceeded={exceeded === "cost"}
/>
<div className="grid gap-2">
<div className={cn("grid gap-x-4 gap-y-2", stacked ? "grid-cols-1 gap-y-2.5" : "grid-cols-2")}>
<Meter
label="费用(估算)"
used={u.costUsd}
max={run.budget.maxCostUsd}
format={(n) => formatUsd(n)}
exceeded={exceeded === "cost"}
/>
<Meter
label="时长"
used={durationMs === undefined ? undefined : Math.round(durationMs / 1000)}
max={run.budget.maxDurationSec}
format={(n) => `${n} 秒`}
exceeded={exceeded === "duration"}
/>
</div>
{/* 调用次数与 token 超限时直接显示,否则收进技术信息 */}
{exceeded === "tool_calls" || exceeded === "tokens" ? (
<div className={cn("grid gap-x-4 gap-y-2", stacked ? "grid-cols-1 gap-y-2.5" : "grid-cols-2")}>
<Meter
label="工具调用"
used={toolCalls}
max={run.budget.maxToolCalls}
format={(n) => String(n)}
exceeded={exceeded === "tool_calls"}
/>
<Meter
label="token"
used={u.totalTokens}
max={run.budget.maxTokens}
format={(n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))}
exceeded={exceeded === "tokens"}
/>
</div>
) : (
<TechInfo
items={[
{
label: "工具调用",
value: `${toolCalls}${run.budget.maxToolCalls ? ` / ${run.budget.maxToolCalls}` : ""}`,
},
{
label: "token",
value:
u.totalTokens === undefined
? null
: `${u.totalTokens.toLocaleString("zh-CN")}${run.budget.maxTokens ? ` / ${run.budget.maxTokens.toLocaleString("zh-CN")}` : ""}`,
},
{ label: "模型", value: run.model ? `${run.provider ? `${run.provider}/` : ""}${run.model}` : null },
{ label: "会话 ID", value: run.id, wide: true },
]}
/>
)}
</div>
);
}
@@ -946,17 +989,25 @@ export function FinalReport({
...report.downloads.map((d) => ({
id: `d${d.downloadTaskId}`,
icon: <Download />,
title: d.name ?? `下载任务 ${d.downloadTaskId.slice(0, 8)}`,
title: d.name ?? "下载任务",
domain: "下载任务",
meta: <StatusBadge tone={PLAN_STATUS_TONE(d.status)}>{d.status}</StatusBadge>,
meta: (
<StatusBadge tone={PLAN_STATUS_TONE(d.status)}>
{DOWNLOAD_STATUS_LABEL[d.status as keyof typeof DOWNLOAD_STATUS_LABEL] ?? d.status}
</StatusBadge>
),
to: `/console/downloads/${d.downloadTaskId}`,
})),
...report.organization.map((o) => ({
id: `o${o.recordId}`,
icon: <FolderInput />,
title: o.targetPath ?? `整理记录 ${o.recordId.slice(0, 8)}`,
title: o.targetPath ?? "整理记录",
domain: "整理记录",
meta: <StatusBadge tone={PLAN_STATUS_TONE(o.status)}>{o.status}</StatusBadge>,
meta: (
<StatusBadge tone={PLAN_STATUS_TONE(o.status)}>
{RECORD_STATUS_LABEL[o.status as keyof typeof RECORD_STATUS_LABEL] ?? o.status}
</StatusBadge>
),
to: `/console/organize/${o.recordId}`,
})),
]
@@ -975,7 +1026,6 @@ export function FinalReport({
{said ? <Markdown text={said} /> : "(没有文字汇报)"}
</StreamingResponse>
</LongText>
<p className="mt-2 text-meta text-muted-foreground">仅供参考,不作为完成依据。</p>
</div>
<div className="min-w-0 rounded-xl border border-border/70 bg-foreground/[0.02] p-3">
<p className="mb-1.5 flex items-center gap-1.5 text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">
@@ -984,19 +1034,19 @@ export function FinalReport({
) : (
<LoaderCircle aria-hidden className="size-3.5 animate-spin motion-reduce:animate-none" />
)}
系统核实的结果
实际结果
</p>
<div className="flex flex-wrap gap-1.5">
{state ? <StatusBadge tone={state.tone}>{state.label}</StatusBadge> : null}
<StatusBadge tone={RUN_STATUS_TONE[run.status]}>运行{RUN_STATUS_LABEL[run.status]}</StatusBadge>
<StatusBadge>工具成功 {count("succeeded")}</StatusBadge>
{count("failed") ? <StatusBadge tone="destructive">失败 {count("failed")}</StatusBadge> : null}
{denied ? <StatusBadge tone="destructive">被拒绝 {denied}</StatusBadge> : null}
</div>
{state || count("failed") || denied ? (
<div className="flex flex-wrap gap-1.5">
{state ? <StatusBadge tone={state.tone}>{state.label}</StatusBadge> : null}
{count("failed") ? <StatusBadge tone="destructive">{count("failed")} 步失败</StatusBadge> : null}
{denied ? <StatusBadge tone="destructive">{denied} 步未执行</StatusBadge> : null}
</div>
) : null}
{report ? (
<>
<p className="mt-2 text-caption break-words text-foreground">{report.summary}</p>
<dl className="mt-2 grid grid-cols-4 gap-2 text-center">
<dl className="mt-2 flex flex-wrap gap-2 text-center">
{(
[
["已提交", report.submitted],
@@ -1004,20 +1054,26 @@ export function FinalReport({
["进行中", report.inProgress],
["失败", report.failed],
] as const
).map(([k, v]) => (
<div key={k} className="flex flex-col-reverse rounded-lg border border-border/60 px-1 py-1.5">
<dt className="text-meta text-muted-foreground">{k}</dt>
<dd className="text-lead font-semibold text-foreground tabular-nums">{v}</dd>
</div>
))}
)
.filter(([, v]) => v > 0)
.map(([k, v]) => (
<div
key={k}
className="flex min-w-16 flex-col-reverse rounded-lg border border-border/60 px-2 py-1.5"
>
<dt className="text-meta text-muted-foreground">{k}</dt>
<dd className="text-lead font-semibold text-foreground tabular-nums">{v}</dd>
</div>
))}
</dl>
{report.plans.length ? (
<ul className="mt-2 space-y-1 text-meta">
{report.plans.map((p) => (
<li key={p.planId} className="flex min-w-0 items-center gap-2">
<span className="text-muted-foreground">下载计划</span>
<code className="min-w-0 truncate font-mono text-foreground/80">{p.planId.slice(0, 8)}</code>
<StatusBadge tone={PLAN_STATUS_TONE(p.status)}>{p.status}</StatusBadge>
<StatusBadge tone={PLAN_STATUS_TONE(p.status)}>
{PLAN_STATUS_LABEL[p.status as keyof typeof PLAN_STATUS_LABEL] ?? p.status}
</StatusBadge>
</li>
))}
</ul>
@@ -1036,18 +1092,17 @@ export function FinalReport({
}))}
/>
) : null}
<p className="mt-2 text-meta text-muted-foreground">
取自下载计划、下载任务和整理记录(生成于 {formatDateTime(report.generatedAt)}
)。「已提交」指已进入下载器,整理成功才算入库。
</p>
</>
) : (
<p className="mt-2 text-meta text-muted-foreground">服务端汇报还没有生成,以任务和整理记录的实际状态为准。</p>
)}
{run.error ? (
<p className="mt-2 text-caption break-words text-destructive">
{String((run.error as { message?: unknown }).message ?? JSON.stringify(run.error))}
</p>
<>
<p className="mt-2 text-caption break-words text-destructive">
{humanizeReason(String((run.error as { message?: unknown }).message ?? "")) || "会话出错"}
</p>
<TechInfo className="mt-1" items={[{ label: "原始错误", value: JSON.stringify(run.error), wide: true }]} />
</>
) : null}
</div>
</section>