From c5987a9db3f2ae8b4622ede6ba7c2d512d4d93d0 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 30 Sep 2026 19:27:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E4=BC=9A=E8=AF=9D=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E8=A1=8C=E6=97=B6=E9=95=BF=E4=BB=8E=E6=9C=AC=E8=BD=AE?= =?UTF-8?q?=E5=BC=80=E5=A7=8B=E7=AE=97=E5=88=B0=E7=BB=93=E8=AE=BA=E6=AD=A3?= =?UTF-8?q?=E6=96=87,=E4=B8=8D=E5=86=8D=E6=98=BE=E7=A4=BA=200=20=E7=A7=92(?= =?UTF-8?q?FIX-9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../components/agent/agent-conversation.tsx | 28 ++++++++++++--- apps/web/app/components/agent/agui3.test.ts | 34 ++++++++++++++++++- .../components/agent/conversation-model.ts | 24 +++++++++---- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/apps/web/app/components/agent/agent-conversation.tsx b/apps/web/app/components/agent/agent-conversation.tsx index 7b4a5b87..68218be0 100644 --- a/apps/web/app/components/agent/agent-conversation.tsx +++ b/apps/web/app/components/agent/agent-conversation.tsx @@ -54,6 +54,7 @@ import { activitySummary, authDenial, buildConversation, + type ConvBlock, callCounts, callCountText, capabilityTitle, @@ -261,10 +262,16 @@ export function StepSummary({ activity, running, auditByCall, + since, + until, }: { activity: ActivityItem[]; running: boolean; auditByCall?: Map; + /** 这一轮开始的时间(会话开始 / 用户消息);只给回合里的第一段 */ + since?: string | null; + /** 结论正文开始的时间 */ + until?: string | null; }) { const id = useId(); const [open, setOpen] = useState(false); @@ -273,7 +280,7 @@ export function StepSummary({ const done = activitySummary(calls.filter((c) => c.status !== "running")); const denied = calls.filter(isDenied).length; const failed = calls.filter((c) => c.status === "failed" && !isDenied(c)).length; - const span = activitySpan(activity); + const span = activitySpan(activity, { since, until }); const counted = running ? "" : callCountText(callCounts(calls), "调用"); const thinking = activity.some((a) => a.kind === "thinking"); const head = current @@ -510,12 +517,12 @@ export function AgentConversation({ return (
- {blocks.map((b) => + {blocks.map((b, bi) => b.kind === "user" ? ( ) : (
- {b.parts.map((p) => { + {b.parts.map((p, pi) => { const fresh = freshKeys.has(p.key.slice(1)) || freshKeys.has(p.key); switch (p.kind) { case "segment": { @@ -525,7 +532,13 @@ export function AgentConversation({ return (
{p.activity.length ? ( - + ) : null} {p.body.map((t) => { const streaming = streamingText && t.key === lastKey; @@ -586,6 +599,13 @@ export function AgentConversation({ ); } +/** 回合开始的时间:前一条用户消息;第一轮是会话开始 */ +function turnSince(blocks: ConvBlock[], index: number, run: AgentRunView): string | null { + const prev = blocks[index - 1]; + if (prev?.kind === "user") return prev.at; + return index === 0 ? run.startedAt : null; +} + // ── 实际结果 ── type ResultFact = { text: string; to?: string; tone?: "warning" }; diff --git a/apps/web/app/components/agent/agui3.test.ts b/apps/web/app/components/agent/agui3.test.ts index bda0c88f..df5e6380 100644 --- a/apps/web/app/components/agent/agui3.test.ts +++ b/apps/web/app/components/agent/agui3.test.ts @@ -19,7 +19,13 @@ import { import { OptionsCard, optionPoint, orderOptions } from "./agent-interaction.tsx"; import { goalFromInput, RunStatsLine } from "./agent-run-panel.tsx"; import { arrangeHistory } from "./agent-session-list.tsx"; -import { type ActivityItem, activitySummary, buildConversation, deniedReason } from "./conversation-model.ts"; +import { + type ActivityItem, + activitySpan, + activitySummary, + buildConversation, + deniedReason, +} from "./conversation-model.ts"; import { parseCommand, RUN_COMMANDS } from "./slash-commands.ts"; import { type AgentStreamState, @@ -539,3 +545,29 @@ describe("实际结果行(FIX-9):只列系统核实的事实", () => { expect(resultFacts(null)).toEqual([{ text: "汇总中…" }]); }); }); + +describe("进度行时长(FIX-9):从这一轮开始算到结论正文", () => { + const quick: ActivityItem[] = [ + { kind: "tool", key: "c1", call: call({ capabilityId: "site.list", startedAt: at(6), endedAt: at(6) }) }, + ]; + it("只有一个瞬时工具调用时不再是 0 秒", () => { + expect(activitySpan(quick)).toBe(0); + expect(activitySpan(quick, { since: at(0), until: at(7) })).toBe(7000); + const t = text(render(createElement(StepSummary, { activity: quick, running: false, since: at(0), until: at(7) }))); + expect(t).toContain("· 7 秒"); + }); + it("没有工具调用、只有思考时也有时长;没有步骤为 null", () => { + expect(activitySpan([{ kind: "thinking", key: "k", text: "想", at: at(2) }], { since: at(0) })).toBe(2000); + expect(activitySpan([])).toBeNull(); + }); + it("整段会话:第一轮从会话开始算", () => { + const events = [ + ev(0, { type: "run_started", goal: "g", resumed: false }), + ...tool(5, "t1", "site.list", '{"items":[]}'), + ev(7, { type: "text_delta", delta: "都正常。" }), + ev(8, { type: "run_completed", finalText: "", usage: {} }), + ]; + const t = text(render(createElement(AgentConversation, { run: RUN, live: liveOf(events) }))); + expect(t).toMatch(/· 7 秒/); + }); +}); diff --git a/apps/web/app/components/agent/conversation-model.ts b/apps/web/app/components/agent/conversation-model.ts index babd2d12..73e47080 100644 --- a/apps/web/app/components/agent/conversation-model.ts +++ b/apps/web/app/components/agent/conversation-model.ts @@ -291,13 +291,23 @@ export function buildConversation( return out; } -/** 一段进度的起止时间(毫秒);没有工具调用为 null */ -export function activitySpan(activity: ActivityItem[]): number | null { - const calls = activity.flatMap((a) => (a.kind === "tool" ? [a.call] : [])); - if (!calls.length) return null; - const start = Math.min(...calls.map((c) => Date.parse(c.startedAt))); - const end = Math.max(...calls.map((c) => Date.parse(c.endedAt ?? c.startedAt))); - return Number.isFinite(start) && Number.isFinite(end) ? end - start : null; +/** + * 一段进度的时长(毫秒):从这一轮开始(since,如会话开始 / 用户消息的时间)或首个步骤, + * 到最后一个步骤结束或结论正文开始(until)。只算工具起止会漏掉模型思考的时间(整轮 7 秒显示成 0 秒)。 + * 没有任何步骤为 null。 + */ +export function activitySpan( + activity: ActivityItem[], + bounds: { since?: string | null; until?: string | null } = {}, +): number | null { + if (!activity.length) return null; + const starts = activity.map((a) => Date.parse(a.kind === "tool" ? a.call.startedAt : a.at)); + const ends = activity.map((a) => Date.parse(a.kind === "tool" ? (a.call.endedAt ?? a.call.startedAt) : a.at)); + const since = bounds.since ? Date.parse(bounds.since) : Number.NaN; + const until = bounds.until ? Date.parse(bounds.until) : Number.NaN; + const start = Math.min(...starts.filter(Number.isFinite), ...(Number.isFinite(since) ? [since] : [])); + const end = Math.max(...ends.filter(Number.isFinite), ...(Number.isFinite(until) ? [until] : [])); + return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : null; } /** 长正文:超过 ~12 行或 600 字按长文处理(收起时限高) */