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:
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>
|
||||
|
||||
Reference in new issue
Block a user