diff --git a/internal/cloud/kafka-event-bridge.test.ts b/internal/cloud/kafka-event-bridge.test.ts index 722079ae..0fbc516f 100644 --- a/internal/cloud/kafka-event-bridge.test.ts +++ b/internal/cloud/kafka-event-bridge.test.ts @@ -216,6 +216,35 @@ test("projects AgentRun semantic infrastructure retry without reclassification", assert.equal(projected.event.runnerJobId, "rjob_retry"); }); +test("projects AgentRun Git mirror fetch as running preparation", () => { + const projected = projectAgentRunKafkaEventToHwlabEvent({ + schema: "agentrun.event.v1", + eventType: "agentrun.run.event", + producedAt: "2026-07-20T10:00:01.000Z", + run: { runId: "run_fetch", sessionId: "ses_fetch", status: "running" }, + command: { commandId: "cmd_fetch", state: "pending" }, + event: { + id: "evt_fetch", + seq: 5, + type: "backend_status", + createdAt: "2026-07-20T10:00:01.000Z", + payload: { + phase: "runner-source-fetch-started", + retryPhase: "initialAttempt", + failureDomain: "infrastructure", + component: "git-mirror", + code: "git-mirror-fetch-in-progress", + summary: "正在从 Git mirror 获取 Runner 源码", + attempt: 1, + maxAttempts: 3 + } + } + }, { source: "hwlab-test" }); + assert.equal(projected.event.status, "running"); + assert.equal(projected.event.code, "git-mirror-fetch-in-progress"); + assert.equal(projected.event.failureComponent, "git-mirror"); +}); + test("projects AgentRun assistant_message Kafka event into HWLAB trace event", () => { const projected = projectAgentRunKafkaEventToHwlabEvent({ schema: "agentrun.event.v1", diff --git a/internal/cloud/kafka-event-bridge.ts b/internal/cloud/kafka-event-bridge.ts index 9876913c..f78df3f6 100644 --- a/internal/cloud/kafka-event-bridge.ts +++ b/internal/cloud/kafka-event-bridge.ts @@ -1679,7 +1679,9 @@ function semanticFailureRetryProjection(payload) { ? "failed" : normalized.includes("recovered") ? "running" - : "retrying"; + : normalized.includes("scheduled") || (normalized.includes("retry") && normalized.includes("started")) + ? "retrying" + : "running"; return { status, fields: { diff --git a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts index 6baaaca3..b42ed397 100644 --- a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts +++ b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts @@ -6,6 +6,49 @@ import { describe, expect, it } from "vitest"; import WorkbenchMessageCard from "./WorkbenchMessageCard.vue"; describe("WorkbenchMessageCard semantic retry visibility", () => { + it("shows Git mirror clone progress as runtime preparation", () => { + const wrapper = mount(WorkbenchMessageCard, { + props: { + detailEnabled: false, + message: { + id: "msg_fetch_agent", + messageId: "msg_fetch_agent", + role: "agent", + title: "Code Agent", + text: "", + status: "running", + traceId: "trc_fetch_banner", + sessionId: "ses_fetch_banner", + createdAt: "2026-07-20T10:00:00.000Z", + runnerTrace: { + traceId: "trc_fetch_banner", + eventSource: "hwlab-kafka-sse", + status: "running", + eventCount: 1, + events: [{ + sourceEventId: "evt_fetch_banner", + sourceSeq: 5, + type: "backend", + status: "running", + failureDomain: "infrastructure", + failureCode: "git-mirror-fetch-in-progress", + summary: "正在从 Git mirror 获取 Runner 源码", + retryPhase: "initialAttempt", + retryAttempt: 1, + retryMaxAttempts: 3, + createdAt: new Date().toISOString() + }] + } + } + }, + global: { stubs: { TraceTimeline: true, LoadingState: true, MessageMarkdown: true, ApiErrorDiagnostic: true, StatusBadge: true } } + }); + expect(wrapper.text()).toContain("运行环境准备:正在从 Git mirror 获取 Runner 源码"); + expect(wrapper.text()).toContain("第 1/3 次源码获取进行中"); + expect(wrapper.text()).not.toContain("基础设施故障"); + wrapper.unmount(); + }); + it("shows an infrastructure retry banner without opening Trace details", () => { const wrapper = mount(WorkbenchMessageCard, { props: { diff --git a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue index 0d858d7f..1bb82dea 100644 --- a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue +++ b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue @@ -36,7 +36,7 @@ const diagnostic = computed(() => messageDiagnosticView(props.message)); const visibleText = computed(() => visibleMessageText(props.message)); const durationMeta = computed(() => messageDurationMeta(props.message)); const activityMeta = computed(() => messageActivityMeta(props.message)); -const semanticFailure = computed(() => semanticFailureView(props.message, nowMs.value)); +const semanticRuntimeStatus = computed(() => semanticRuntimeStatusView(props.message, nowMs.value)); const autoExpanded = computed(() => props.traceAutoExpanded ?? traceLifecycleExpanded(props.message, traceTimelinePolicy.value)); function isRunningMessage(message: ChatMessage): boolean { @@ -123,7 +123,7 @@ function traceStorageKey(message: ChatMessage): string { return `${props.storageKeyPrefix}.${message.traceId ?? message.runnerTrace?.traceId ?? message.id}`; } -function semanticFailureView(message: ChatMessage, currentTimeMs: number): { title: string; detail: string; tone: string; code: string | null } | null { +function semanticRuntimeStatusView(message: ChatMessage, currentTimeMs: number): { title: string; detail: string; tone: string; code: string | null } | null { const events = Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : []; const event = [...events].reverse().find((item) => typeof item.failureDomain === "string" && typeof item.retryPhase === "string"); if (!event) return null; @@ -133,6 +133,13 @@ function semanticFailureView(message: ChatMessage, currentTimeMs: number): { tit const attempt = finiteInteger(event.retryAttempt ?? event.attempt); const maxAttempts = finiteInteger(event.retryMaxAttempts ?? event.maxAttempts); const progress = attempt !== null && maxAttempts !== null ? `第 ${attempt}/${maxAttempts} 次` : "本次"; + const code = firstText(event.failureCode, event.code); + if (code === "git-mirror-fetch-in-progress") { + return { title: `运行环境准备:${summary}`, detail: `${progress}源码获取进行中`, tone: "info", code }; + } + if (code === "git-mirror-fetch-completed") { + return { title: `运行环境准备:${summary}`, detail: "源码获取完成,正在启动 Runner", tone: "recovered", code }; + } let detail = "正在判定有限重试"; let tone = "warning"; if (phase.includes("scheduled")) { @@ -152,7 +159,7 @@ function semanticFailureView(message: ChatMessage, currentTimeMs: number): { tit title: `${domain}:${summary}`, detail, tone, - code: firstText(event.failureCode, event.code) + code }; } @@ -191,10 +198,10 @@ function finiteInteger(value: unknown): number | null { -
- {{ semanticFailure.title }} - {{ semanticFailure.detail }} - {{ semanticFailure.code }} +
+ {{ semanticRuntimeStatus.title }} + {{ semanticRuntimeStatus.detail }} + {{ semanticRuntimeStatus.code }}
@@ -221,6 +228,12 @@ function finiteInteger(value: unknown): number | null { color: #166534; } +.semantic-failure-banner[data-tone="info"] { + border-left-color: #2563eb; + background: #eff6ff; + color: #1e40af; +} + .semantic-failure-banner[data-tone="failed"] { border-left-color: #b91c1c; background: #fef2f2;