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>
|
||||
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
setAgentDrawerOpen,
|
||||
setAgentDrawerRun,
|
||||
setAgentDrawerTab,
|
||||
shortGoalTitle,
|
||||
showAgentRunInDrawer,
|
||||
splitContextGoal,
|
||||
useAgentDrawer,
|
||||
} from "./agent-store.ts";
|
||||
import { AgentRunOpenContext } from "./agent-subagent-tree.tsx";
|
||||
@@ -108,7 +108,7 @@ function HistoryRunView({ runId }: { runId: string }) {
|
||||
返回列表
|
||||
</Button>
|
||||
<p className="min-w-0 flex-1 truncate text-caption text-muted-foreground" title={run.data?.run.goal}>
|
||||
{run.data ? splitContextGoal(run.data.run.goal).text : ""}
|
||||
{run.data ? shortGoalTitle(run.data.run.goal) : ""}
|
||||
</p>
|
||||
<Button asChild variant="ghost" size="xs" className="shrink-0 text-muted-foreground">
|
||||
<Link to={`/console/agent/${runId}${mock ? "?mock=1" : ""}`} onClick={() => setAgentDrawerOpen(false)}>
|
||||
@@ -163,11 +163,8 @@ export function AgentDrawer() {
|
||||
<Bot className="h-4 w-4" />
|
||||
智能体
|
||||
</SheetTitle>
|
||||
<SheetDescription className={cn("break-words", history && "sr-only")}>
|
||||
{context?.label ? `当前对象:${context.label}` : "交代目标,智能体在授权范围内执行并汇报。"}
|
||||
<span className="ml-1 hidden text-muted-foreground/75 sm:inline">
|
||||
({agentShortcutLabel()} 开关,Esc 关闭)
|
||||
</span>
|
||||
<SheetDescription className={cn("break-words", (history || !context?.label) && "sr-only")}>
|
||||
{context?.label ? `当前对象:${context.label}` : `${agentShortcutLabel()} 打开或收起`}
|
||||
</SheetDescription>
|
||||
{context && !history ? <ContextChips context={context} /> : null}
|
||||
<DrawerTabs tab={tab} />
|
||||
@@ -288,7 +285,7 @@ function AgentMiniDock({ runId }: { runId: string }) {
|
||||
<Bot aria-hidden className="size-4 shrink-0 text-muted-foreground" />
|
||||
{status ? <RunStatusBadge status={status} /> : null}
|
||||
<span className="min-w-0 flex-1 truncate text-meta text-muted-foreground">
|
||||
{run.data ? splitContextGoal(run.data.run.goal).text : ""}
|
||||
{run.data ? shortGoalTitle(run.data.run.goal) : ""}
|
||||
</span>
|
||||
<Button variant="ghost" size="icon-xs" aria-label="展开智能体" title="展开" onClick={restoreAgentDrawer}>
|
||||
<Maximize2 />
|
||||
@@ -326,7 +323,7 @@ export function AgentDrawerTrigger({ className }: { className?: string }) {
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="打开智能体"
|
||||
title={`Agent(${agentShortcutLabel()})`}
|
||||
title={`智能体(${agentShortcutLabel()})`}
|
||||
className={className}
|
||||
onClick={() =>
|
||||
minimized && runId ? restoreAgentDrawer() : openAgentDrawer(contextFromPath(pathname), lastAgentDrawerTab())
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
toolSummary,
|
||||
} from "./agent-conversation.tsx";
|
||||
import { matchesHistory, rangeStart } from "./agent-session-list.tsx";
|
||||
import { contextFromPath, contextPrefix, splitContextGoal } from "./agent-store.ts";
|
||||
import { contextFromPath, contextPrefix, shortGoalTitle, splitContextGoal } from "./agent-store.ts";
|
||||
import { foldEvents, type StreamItem, type ToolCallView } from "./use-agent-stream.ts";
|
||||
|
||||
const call = (p: Partial<ToolCallView>): ToolCallView => ({
|
||||
@@ -185,3 +185,20 @@ describe("会话历史筛选", () => {
|
||||
expect(rangeStart("", now)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shortGoalTitle(会话短标题)", () => {
|
||||
it("去 UUID、JSON 参数,能力 ID 换中文名,取第一句", () => {
|
||||
const t = shortGoalTitle(
|
||||
'在浏览器会话 16be7338-1111-4222-8333-944445555666 上直接调用一次 browser.solve_challenge,参数:{"sessionId":"x","mode":"auto"}',
|
||||
);
|
||||
expect(t).not.toMatch(/16be7338|\{|sessionId/);
|
||||
expect(t.startsWith("在浏览器会话上直接调用一次")).toBe(true);
|
||||
});
|
||||
it("去上下文前缀,超长截断", () => {
|
||||
expect(shortGoalTitle("[上下文 作品:沙丘] 把沙丘:第二部的 4K 收进来。然后再看看别的")).toBe(
|
||||
"把沙丘:第二部的 4K 收进来",
|
||||
);
|
||||
expect(shortGoalTitle("啊".repeat(60))).toBe(`${"啊".repeat(40)}…`);
|
||||
expect(shortGoalTitle(" ")).toBe("未命名会话");
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,7 @@ import { PromptInput } from "~/components/agentui/prompt-input.tsx";
|
||||
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { formatUsd } from "~/components/schedules/labels.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Label } from "~/components/ui/label.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
@@ -115,8 +116,7 @@ export function GoalComposer({
|
||||
<p className="px-1 text-meta text-muted-foreground" aria-live="polite">
|
||||
{proposeFirst
|
||||
? "智能体先调查并给出方案,你选定后才执行。"
|
||||
: "智能体列出步骤后直接执行,需要审批的动作会停在对应步骤等你批准。"}{" "}
|
||||
Enter 发送,Shift+Enter 换行。
|
||||
: "智能体列出步骤后直接执行,需要审批的动作会停在对应步骤等你批准。"}
|
||||
</p>
|
||||
{suggestions?.length ? (
|
||||
<div className="pt-2">
|
||||
@@ -143,10 +143,6 @@ export function GoalComposer({
|
||||
);
|
||||
}
|
||||
|
||||
function shortTokens(n: number | undefined) {
|
||||
return n === undefined ? null : n >= 1000 ? `${(n / 1000).toFixed(1)}k token` : `${n} token`;
|
||||
}
|
||||
|
||||
/** 状态 + 来源 + 时间 + 模型 */
|
||||
function RunMeta({ run, status }: { run: RunSnapshot["run"]; status: AgentRunStatus }) {
|
||||
return (
|
||||
@@ -163,7 +159,6 @@ function RunMeta({ run, status }: { run: RunSnapshot["run"]; status: AgentRunSta
|
||||
</div>
|
||||
<p className="text-meta break-words text-muted-foreground">
|
||||
{TRIGGER_LABEL[run.trigger]} · {formatDateTime(run.createdAt)}
|
||||
{run.model ? ` · ${run.provider ?? ""}/${run.model}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -194,8 +189,7 @@ function RunOverviewBar({
|
||||
const current = steps.find((s) => s.status === "running") ?? steps.find((s) => s.status === "pending");
|
||||
const failed = steps.some((s) => s.status === "failed");
|
||||
const u = { ...r.usage, ...(live.usage ?? {}) };
|
||||
const calls = Math.max(r.toolCallCount, live.items.filter((i) => i.kind === "tool").length);
|
||||
const usage = [`${calls} 次调用`, shortTokens(u.totalTokens)].filter(Boolean).join(" · ");
|
||||
const usage = u.costUsd ? `费用约 ${formatUsd(u.costUsd)}` : "";
|
||||
return (
|
||||
<section aria-label="会话概况" className={cn("shrink-0 rounded-xl border border-border/70", className)}>
|
||||
<button
|
||||
@@ -240,7 +234,7 @@ function RunOverviewBar({
|
||||
<RunPlan plan={live.plan} active={active} fill />
|
||||
<SubagentTree snapshot={snapshot} className="p-2.5" />
|
||||
<div>
|
||||
<p className="mb-2 text-meta text-muted-foreground">预算与用量</p>
|
||||
<p className="mb-2 text-meta text-muted-foreground">用量</p>
|
||||
<BudgetBar run={r} live={live} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -462,8 +456,8 @@ export function AgentRunPanel({
|
||||
<RunMeta run={r} status={shownStatus} />
|
||||
<RunPlan plan={live.plan} active={streaming || r.status === "waiting"} fill />
|
||||
<SubagentTree snapshot={run.data} className="p-2.5" />
|
||||
<section aria-label="预算与用量" className="mt-auto rounded-xl border border-border/70 p-3">
|
||||
<p className="mb-2 text-meta text-muted-foreground">预算与用量</p>
|
||||
<section aria-label="用量" className="mt-auto rounded-xl border border-border/70 p-3">
|
||||
<p className="mb-2 text-meta text-muted-foreground">用量</p>
|
||||
<BudgetBar run={r} live={live} stacked />
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
@@ -4,13 +4,14 @@ import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "re
|
||||
import { Link } from "react-router";
|
||||
import { type AgentRunView, RUN_STATUS_LABEL, TRIGGER_LABEL, useAgentRuns } from "~/api/agent.ts";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { formatUsd } from "~/components/schedules/labels.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Input } from "~/components/ui/input.tsx";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { RunStatusBadge } from "./agent-conversation.tsx";
|
||||
import { splitContextGoal } from "./agent-store.ts";
|
||||
import { shortGoalTitle, splitContextGoal } from "./agent-store.ts";
|
||||
|
||||
// 会话历史列表:会话页左栏与抽屉「历史」Tab 共用。状态、来源在服务端筛选(游标翻页),
|
||||
// 搜索与时间在已加载的条目里筛(按创建时间倒序,早于时间范围的页不再加载)。
|
||||
@@ -57,16 +58,9 @@ export function matchesHistory(
|
||||
return run.goal.toLowerCase().includes(q) || run.id.startsWith(q);
|
||||
}
|
||||
|
||||
/** 列表副行:只写费用(token 与调用次数在会话的技术信息里) */
|
||||
export function usageText(r: Pick<AgentRunView, "toolCallCount" | "usage">) {
|
||||
const parts = [`${r.toolCallCount} 次调用`];
|
||||
if (r.usage.totalTokens !== undefined)
|
||||
parts.push(
|
||||
r.usage.totalTokens >= 1000
|
||||
? `${(r.usage.totalTokens / 1000).toFixed(1)}k token`
|
||||
: `${r.usage.totalTokens} token`,
|
||||
);
|
||||
if (r.usage.costUsd !== undefined) parts.push(`$${r.usage.costUsd.toFixed(3)}`);
|
||||
return parts.join(" · ");
|
||||
return r.usage.costUsd ? `费用约 ${formatUsd(r.usage.costUsd)}` : "";
|
||||
}
|
||||
|
||||
const ALL = "__all__";
|
||||
@@ -115,7 +109,7 @@ function RunRow({ r, current, children }: { r: AgentRunView; current: boolean; c
|
||||
<>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<p className="min-w-0 flex-1 truncate text-caption text-foreground" title={goal.text}>
|
||||
{goal.text}
|
||||
{shortGoalTitle(r.goal)}
|
||||
</p>
|
||||
{children}
|
||||
</div>
|
||||
@@ -130,10 +124,11 @@ function RunRow({ r, current, children }: { r: AgentRunView; current: boolean; c
|
||||
{relTime(r.createdAt)}
|
||||
</time>
|
||||
</div>
|
||||
<p className="mt-0.5 truncate text-meta text-muted-foreground tabular-nums">
|
||||
{goal.context ? `${goal.context.split(" · ")[0]} · ` : ""}
|
||||
{usageText(r)}
|
||||
</p>
|
||||
{goal.context || usageText(r) ? (
|
||||
<p className="mt-0.5 truncate text-meta text-muted-foreground tabular-nums">
|
||||
{[goal.context?.split(" · ")[0], usageText(r)].filter(Boolean).join(" · ")}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CAPABILITY_CATALOG, type KnownCapabilityId } from "@umt/contracts";
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
// Agent 侧栏抽屉的全局开关与上下文:任意页面调用 openAgentDrawer({ entityId, ... }) 即可带着当前对象打开。
|
||||
@@ -192,3 +193,29 @@ export function splitContextGoal(goal: string): { context: string | null; text:
|
||||
const m = /^\[上下文 ([^\]]*)\]\s*/.exec(goal);
|
||||
return m ? { context: m[1] ?? null, text: goal.slice(m[0].length) } : { context: null, text: goal };
|
||||
}
|
||||
|
||||
const capabilityTitle = (id: string): string | null =>
|
||||
(CAPABILITY_CATALOG as Record<string, { title?: string }>)[id as KnownCapabilityId]?.title ?? null;
|
||||
|
||||
const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
||||
const CAP_RE = /\b[a-z][a-z_]*(?:\.[a-z_]+)+\b/g;
|
||||
|
||||
/**
|
||||
* 会话标题:从目标生成短标题(DEAI-2)。去掉上下文前缀、UUID、JSON 参数,能力 ID 换成中文名,
|
||||
* 取第一句,超过 40 字截断。完整目标仍在会话里第一条消息。
|
||||
*/
|
||||
export function shortGoalTitle(goal: string, max = 40): string {
|
||||
let t = splitContextGoal(goal).text;
|
||||
t = t.replace(/\s*[{[]\s*["{[][\s\S]*$/, ""); // JSON 参数到结尾
|
||||
t = t.replace(UUID_RE, "");
|
||||
t = t.replace(/\b[0-9a-f]{8}(?:-[0-9a-f]{0,4})*…?/gi, ""); // 截断的短 ID
|
||||
t = t.replace(CAP_RE, (id) => capabilityTitle(id) ?? id);
|
||||
t = t.split(/[\n。!?!?]/)[0] ?? "";
|
||||
t = t
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/(?<=[\p{Script=Han}(])\s+(?=[\p{Script=Han},。:;、)])/gu, "")
|
||||
.replace(/[,,::;;、\s]*(?:参数|args?)?[::]?\s*$/i, "")
|
||||
.trim();
|
||||
if (!t) return "未命名会话";
|
||||
return t.length > max ? `${t.slice(0, max)}…` : t;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Link } from "react-router";
|
||||
import { agentMockEnabled, type RunSnapshot } from "~/api/agent.ts";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { RunStatusBadge } from "./agent-conversation.tsx";
|
||||
import { splitContextGoal } from "./agent-store.ts";
|
||||
import { shortGoalTitle } from "./agent-store.ts";
|
||||
|
||||
// 子代理(agent.spawn):会话页显示父会话链接 + 继承信息,以及子会话树(可展开、可跳转)与会话树合计用量。
|
||||
// 未派生过子会话、也不是子会话时不渲染(不显示空的多 Agent 看板)。
|
||||
@@ -70,7 +70,7 @@ function Node({ node, byParent, depth }: { node: AgentRunNode; byParent: Map<str
|
||||
<span className="mt-0.5 size-4 shrink-0" aria-hidden />
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<RunLink id={node.id}>{splitContextGoal(node.goal).text}</RunLink>
|
||||
<RunLink id={node.id}>{shortGoalTitle(node.goal)}</RunLink>
|
||||
<div className="mt-0.5 flex flex-wrap items-center gap-1.5">
|
||||
<RunStatusBadge status={node.status} />
|
||||
<span className="text-meta text-muted-foreground tabular-nums">
|
||||
@@ -116,7 +116,7 @@ export function SubagentTree({ snapshot, className }: { snapshot: RunSnapshot; c
|
||||
<CornerLeftUp aria-hidden className="size-3.5" />
|
||||
父会话
|
||||
</p>
|
||||
<RunLink id={parent.id}>{splitContextGoal(parent.goal).text}</RunLink>
|
||||
<RunLink id={parent.id}>{shortGoalTitle(parent.goal)}</RunLink>
|
||||
<p className="text-meta text-muted-foreground">
|
||||
第 {run.depth} 层子会话 · 继承父会话的发起人与授权上限 · 能力 {caps} 项(⊆ 父会话)· 划拨预算{" "}
|
||||
{budgetText(run.budget as Record<string, number | undefined>)}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AGENT_LAUNCHER_ATTR } from "~/components/agent/agent-drawer.tsx";
|
||||
import {
|
||||
agentShortcutLabel,
|
||||
isAgentShortcut,
|
||||
splitContextGoal,
|
||||
shortGoalTitle,
|
||||
toggleAgentDrawer,
|
||||
useAgentDrawer,
|
||||
} from "~/components/agent/agent-store.ts";
|
||||
@@ -24,7 +24,7 @@ export function useActiveAgentRuns() {
|
||||
const latest = active[0];
|
||||
return {
|
||||
count: active.length,
|
||||
summary: latest ? `${RUN_STATUS_LABEL[latest.status]} · ${splitContextGoal(latest.goal).text}` : null,
|
||||
summary: latest ? `${RUN_STATUS_LABEL[latest.status]} · ${shortGoalTitle(latest.goal)}` : null,
|
||||
};
|
||||
}, [runs.data]);
|
||||
}
|
||||
@@ -154,7 +154,7 @@ export function AgentLauncher() {
|
||||
aria-label={label}
|
||||
aria-keyshortcuts="Control+J Meta+J"
|
||||
aria-expanded={open}
|
||||
title={`${open ? "收起" : "打开"} Agent(${shortcut})${summary && !open ? `\n${summary}` : ""}`}
|
||||
title={`${open ? "收起" : "打开"}智能体(${shortcut})${summary && !open ? `\n${summary}` : ""}`}
|
||||
style={{ bottom: `calc(${BASE_BOTTOM + lift}px + env(safe-area-inset-bottom))` }}
|
||||
className={cn(
|
||||
"pg-float prism-ring group pointer-events-auto fixed right-4 z-40 flex h-14 min-w-14 cursor-pointer items-center justify-center gap-3 rounded-full text-foreground outline-none select-none",
|
||||
@@ -179,7 +179,7 @@ export function AgentLauncher() {
|
||||
</kbd>
|
||||
</span>
|
||||
<span className="max-w-56 truncate text-meta leading-4 text-muted-foreground">
|
||||
{open ? "Esc 关闭" : (summary ?? "交代目标,边做边汇报")}
|
||||
{open ? "Esc 关闭" : (summary ?? "")}
|
||||
</span>
|
||||
</span>
|
||||
</button>,
|
||||
@@ -196,7 +196,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
|
||||
onNavigate?.();
|
||||
toggleAgentDrawer(pathname);
|
||||
};
|
||||
const title = `Agent(${shortcut})${summary ? `\n${summary}` : ""}`;
|
||||
const title = `智能体(${shortcut})${summary ? `\n${summary}` : ""}`;
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
@@ -231,9 +231,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
|
||||
智能体
|
||||
{count ? <PulseDot /> : null}
|
||||
</span>
|
||||
<span className="truncate text-meta text-muted-foreground">
|
||||
{count ? `${count} 个会话进行中` : "交代目标,边做边汇报"}
|
||||
</span>
|
||||
{count ? <span className="truncate text-meta text-muted-foreground">{count} 个会话进行中</span> : null}
|
||||
</span>
|
||||
{count ? (
|
||||
<CountBadge count={count} />
|
||||
|
||||
@@ -64,7 +64,7 @@ export const NAV_GROUPS: NavGroup[] = [
|
||||
to: "/console/agent",
|
||||
label: "智能体会话",
|
||||
icon: Bot,
|
||||
description: "交代目标,智能体在授权内执行并汇报",
|
||||
description: "给智能体一个目标,看它做了什么",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link, useParams } from "react-router";
|
||||
import { agentMockEnabled, useAgentRun } from "~/api/agent.ts";
|
||||
import { AgentRunPanel } from "~/components/agent/agent-run-panel.tsx";
|
||||
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
|
||||
import { splitContextGoal } from "~/components/agent/agent-store.ts";
|
||||
import { shortGoalTitle } from "~/components/agent/agent-store.ts";
|
||||
import { PageHeader } from "~/components/console/page-header.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function AgentRunPage() {
|
||||
description={
|
||||
run.data ? (
|
||||
<span className="line-clamp-2 break-words" title={run.data.run.goal}>
|
||||
{splitContextGoal(run.data.run.goal).text}
|
||||
{shortGoalTitle(run.data.run.goal)}
|
||||
</span>
|
||||
) : (
|
||||
"加载中…"
|
||||
|
||||
@@ -15,10 +15,7 @@ export default function AgentPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="智能体会话"
|
||||
description="交代目标,智能体列出步骤后在授权范围内执行并汇报;需要审批的动作停在对应步骤等你批准。"
|
||||
/>
|
||||
<PageHeader title="智能体会话" />
|
||||
{mock ? (
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-caption text-warning">
|
||||
<FlaskConical className="h-4 w-4 shrink-0" />
|
||||
|
||||
Reference in new issue
Block a user