From 89ef9cdbfba12bb2ba9268eaa6527e1697336d22 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 12 Jul 2026 18:06:39 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B1=95=E7=A4=BA=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=BC=96=E8=BE=91=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/cloud/code-agent-agentrun-result.ts | 15 +++++- internal/cloud/kafka-event-bridge.test.ts | 37 ++++++++++++++ internal/cloud/kafka-event-bridge.ts | 15 ++++++ tools/hwlab-cli/kafka-regenerate.test.ts | 45 +++++++++++++++++ tools/src/hwlab-cli/trace-renderer.ts | 51 +++++++++++++++++++- 5 files changed, 159 insertions(+), 4 deletions(-) diff --git a/internal/cloud/code-agent-agentrun-result.ts b/internal/cloud/code-agent-agentrun-result.ts index 9de5580e..905484b0 100644 --- a/internal/cloud/code-agent-agentrun-result.ts +++ b/internal/cloud/code-agent-agentrun-result.ts @@ -653,7 +653,8 @@ export function mapAgentRunEvent(event, mapping = {}) { outputSummary: output, message: output || command || textPayload(payload, "tool call"), outputBytes: typeof payload.outputBytes === "number" ? payload.outputBytes : payload.summary?.outputBytes, - outputTruncated: payload.outputTruncated === true || payload.summary?.outputTruncated === true + outputTruncated: payload.outputTruncated === true || payload.summary?.outputTruncated === true, + ...(Array.isArray(payload.changes) ? { changes: payload.changes, fileCount: Number.isInteger(payload.fileCount) ? payload.fileCount : null } : {}) }; } if (event.type === "command_output") { @@ -661,7 +662,17 @@ export function mapAgentRunEvent(event, mapping = {}) { return { ...base, type: "output", eventType: "status", status: "running", label: `agentrun:output:${stream}`, stream, message: textPayload(payload, ""), outputTruncated: payload.outputTruncated === true }; } if (event.type === "diff") { - return { ...base, type: "diff", eventType: "status", status: "running", label: "agentrun:diff", message: textPayload(payload, "diff") }; + return { + ...base, + type: "diff", + eventType: "diff", + status: String(payload.status ?? "updated"), + label: "agentrun:diff", + message: String(payload.diff ?? textPayload(payload, "diff")), + diff: String(payload.diff ?? ""), + threadId: payload.threadId ?? null, + turnId: payload.turnId ?? null + }; } if (event.type === "error") { const failureKind = normalizedFailureKind(payload.failureKind ?? payload.errorCode ?? "backend") ?? "backend"; diff --git a/internal/cloud/kafka-event-bridge.test.ts b/internal/cloud/kafka-event-bridge.test.ts index 2954a644..6c98ea79 100644 --- a/internal/cloud/kafka-event-bridge.test.ts +++ b/internal/cloud/kafka-event-bridge.test.ts @@ -103,6 +103,43 @@ test("projects AgentRun assistant_message Kafka event into HWLAB trace event", ( assert.equal(projected.valuesPrinted, false); }); +test("projects AgentRun fileChange and diff events without dropping paths or unified diff", () => { + const changes = [ + { path: "src/example.ts", kind: "update", diff: "@@ -1 +1 @@\n-export const value = 1;\n+export const value = 2;" }, + { path: "docs/说明.md", kind: "add", diff: "@@ -0,0 +1 @@\n+# 文件编辑可见" } + ]; + const fileChange = projectAgentRunKafkaEventToHwlabEvent({ + schema: "agentrun.event.v1", + eventType: "agentrun.event.committed", + eventId: "evt_file_change_completed", + traceId: "trc_file_change", + hwlabSessionId: "ses_file_change", + runId: "run_file_change", + commandId: "cmd_file_change", + event: { id: "evt_file_change_completed", seq: 21, type: "tool_call", payload: { method: "item/completed", itemId: "item_file_change", type: "fileChange", toolName: "fileChange", status: "completed", changes, fileCount: 2 } } + }, { source: "hwlab-test", sourceTopic: "agentrun.event.debug.v1", sourceOffset: "21" }); + assert.equal(fileChange.event.type, "tool"); + assert.equal(fileChange.event.toolName, "fileChange"); + assert.equal(fileChange.event.itemId, "item_file_change"); + assert.deepEqual(fileChange.event.changes, changes); + assert.equal(fileChange.event.fileCount, 2); + + const diff = projectAgentRunKafkaEventToHwlabEvent({ + schema: "agentrun.event.v1", + eventType: "agentrun.event.committed", + eventId: "evt_turn_diff_updated", + traceId: "trc_file_change", + hwlabSessionId: "ses_file_change", + runId: "run_file_change", + commandId: "cmd_file_change", + event: { id: "evt_turn_diff_updated", seq: 22, type: "diff", payload: { phase: "turn/diff/updated", threadId: "thread_file_change", turnId: "turn_file_change", status: "updated", diff: changes.map((change) => change.diff).join("\n") } } + }, { source: "hwlab-test", sourceTopic: "agentrun.event.debug.v1", sourceOffset: "22" }); + assert.equal(diff.event.type, "diff"); + assert.equal(diff.event.eventType, "diff"); + assert.equal(diff.event.threadId, "thread_file_change"); + assert.match(diff.event.diff, /docs\/说明\.md|文件编辑可见/u); +}); + test("projects AgentRun assistant_progress as an identity-preserving HWLAB assistant lifecycle event", () => { const text = "p".repeat(500); const projected = projectAgentRunKafkaEventToHwlabEvent({ diff --git a/internal/cloud/kafka-event-bridge.ts b/internal/cloud/kafka-event-bridge.ts index 31e85107..3f2670ac 100644 --- a/internal/cloud/kafka-event-bridge.ts +++ b/internal/cloud/kafka-event-bridge.ts @@ -1444,6 +1444,21 @@ function mapAgentRunSourceEventToHwlabEvent({ input, sourceEvent, payload, run, commandTruncated: payload.commandTruncated === true, outputBytes: integerValue(payload.outputBytes ?? payload.summary?.outputBytes), outputTruncated: payload.outputTruncated === true || payload.summary?.outputTruncated === true, + ...(Array.isArray(payload.changes) ? { changes: payload.changes, fileCount: integerValue(payload.fileCount) } : {}), + terminal: false + }; + } + if (type === "diff") { + return { + ...base, + type: "diff", + eventType: "diff", + status: firstText(payload.status) || "updated", + label: "agentrun:diff", + message: firstText(payload.diff) || "", + diff: firstText(payload.diff) || "", + threadId: firstText(payload.threadId), + turnId: firstText(payload.turnId), terminal: false }; } diff --git a/tools/hwlab-cli/kafka-regenerate.test.ts b/tools/hwlab-cli/kafka-regenerate.test.ts index a8f22469..397e2ea2 100644 --- a/tools/hwlab-cli/kafka-regenerate.test.ts +++ b/tools/hwlab-cli/kafka-regenerate.test.ts @@ -517,6 +517,51 @@ test("shared tool summary model keeps status semantic and visible content status assert.match(markdown, /aria-label="工具调用失败:/u); }); +test("fileChange and turn diff render real paths and unified diff as valid Markdown cards", () => { + const fileDiff = "@@ -1 +1 @@\n-export const value = 1;\n+export const value = 2;"; + const aggregateDiff = `${fileDiff}\n@@ -0,0 +1 @@\n+# 文件编辑可见`; + const rows = traceDisplayRows({ eventSource: "hwlab-kafka-sse" }, [ + { + sourceSeq: 21, + sourceEventId: "evt_file_change_completed", + type: "tool_call", + eventType: "tool_call", + status: "completed", + label: "agentrun:tool:fileChange", + toolName: "fileChange", + toolType: "fileChange", + itemId: "item_file_change", + changes: [ + { path: "src/example.ts", kind: "update", diff: fileDiff }, + { path: "docs/说明.md", kind: "add", diff: "@@ -0,0 +1 @@\n+# 文件编辑可见" } + ], + createdAt: "2026-07-12T15:00:01.000Z" + }, + { + sourceSeq: 22, + sourceEventId: "evt_turn_diff_updated", + type: "diff", + eventType: "diff", + status: "updated", + label: "agentrun:diff", + diff: aggregateDiff, + message: aggregateDiff, + createdAt: "2026-07-12T15:00:02.000Z" + } + ]); + const markdown = renderTraceRowsMarkdown(rows); + assert.equal(rows.length, 2); + assert.match(markdown, /fileChange src\/example\.ts, docs\/说明\.md/u); + assert.match(markdown, /path: src\/example\.ts/u); + assert.match(markdown, /path: docs\/说明\.md/u); + assert.match(markdown, /\+export const value = 2;/u); + assert.match(markdown, /turn\/diff\/updated/u); + assert.match(markdown, /\+# 文件编辑可见/u); + assert.equal((markdown.match(/
/gu) ?? []).length, 2); + assert.equal((markdown.match(/<\/details>/gu) ?? []).length, 2); + assert.equal((markdown.match(/```text/gu) ?? []).length, 2); +}); + test("outer final response suppression follows formal assistant identity instead of equal text", () => { const finalText = "Hi. What do you want to work on?"; const rows = traceDisplayRows({ eventSource: "hwlab-kafka-sse" }, [ diff --git a/tools/src/hwlab-cli/trace-renderer.ts b/tools/src/hwlab-cli/trace-renderer.ts index 7241144b..38548895 100644 --- a/tools/src/hwlab-cli/trace-renderer.ts +++ b/tools/src/hwlab-cli/trace-renderer.ts @@ -68,6 +68,10 @@ export function traceDisplayRows(trace: Record = {}, events: Tr rows.push(traceToolCallRow(event, displayOptions)); continue; } + if (isDiffTraceEvent(event)) { + rows.push(traceDiffRow(event, displayOptions)); + continue; + } if (isRequestTraceEvent(event)) { continue; } @@ -245,7 +249,7 @@ function traceToolCallRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOp const command = cleanShellCommand(event.command); const toolState = traceToolState(event); const toolName = traceToolName(event, command); - const body = traceToolCallBody(event, command); + const body = isFileChangeTraceEvent(event) ? traceFileChangeBody(event) : traceToolCallBody(event, command); const eventKey = traceEventIdentityToken(event, options.sequenceAuthority); return { rowId: `tool:${event.itemId ?? eventKey ?? `${event.label ?? event.type ?? "tool"}:${event.createdAt ?? "unknown"}`}`, @@ -256,7 +260,50 @@ function traceToolCallRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOp statusLabel: traceToolStatusLabel(toolState), header: traceEventClock(event, options), body, - preview: traceToolPreview(command, toolName), + preview: isFileChangeTraceEvent(event) ? traceFileChangePreview(event) : traceToolPreview(command, toolName), + bodyFormat: "text" + }; +} + +function isFileChangeTraceEvent(event: TraceEvent): boolean { + return nonEmptyString(event.toolName ?? event.toolType) === "fileChange"; +} + +function traceFileChangePreview(event: TraceEvent): string { + const paths = (Array.isArray(event.changes) ? event.changes : []) + .filter(isRecord) + .map((change) => nonEmptyString(change.path)) + .filter((path): path is string => Boolean(path)); + return compactTraceOneLine(paths.length > 0 ? `fileChange ${paths.join(", ")}` : "fileChange", 140); +} + +function traceFileChangeBody(event: TraceEvent): string | null { + const changes = (Array.isArray(event.changes) ? event.changes : []).filter(isRecord); + if (changes.length === 0) return null; + return changes.map((change) => { + const path = nonEmptyString(change.path) ?? "(path missing)"; + const kind = nonEmptyString(change.kind) ?? "update"; + const diff = nonEmptyString(change.diff) ?? ""; + return [`path: ${path}`, `kind: ${kind}`, "diff:", diff].filter(Boolean).join("\n"); + }).join("\n\n"); +} + +function isDiffTraceEvent(event: TraceEvent): boolean { + return event.type === "diff" || event.eventType === "diff" || nonEmptyString(event.label) === "agentrun:diff"; +} + +function traceDiffRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOptions): TraceEventRow { + const eventKey = traceEventIdentityToken(event, options.sequenceAuthority); + return { + rowId: `tool:diff:${eventKey ?? event.createdAt ?? "updated"}`, + seq: traceEventDisplaySeq(event, options.sequenceAuthority), + sequenceAuthority: options.sequenceAuthority, + tone: traceEventTone(event), + toolState: "success", + statusLabel: "文件 diff 已更新", + header: traceEventClock(event, options), + body: nonEmptyString(event.diff ?? event.message), + preview: "turn/diff/updated", bodyFormat: "text" }; }