diff --git a/apps/sidecar/src/index.ts b/apps/sidecar/src/index.ts index 21f3d93d..c23d72cc 100644 --- a/apps/sidecar/src/index.ts +++ b/apps/sidecar/src/index.ts @@ -25,7 +25,7 @@ import { } from "./services/infra/logger"; import { assertSidecarNativeRuntime } from "./services/infra/native-runtime"; import { createProcessRpcTransport, MAX_RPC_MESSAGE_BYTES } from "./rpc/process-transport"; -import { classifyBrowserRpcResponse } from "./rpc/browser-rpc-sequence"; +import { browserRpcErrorFromPayload, classifyBrowserRpcResponse } from "./rpc/browser-rpc-sequence"; import { createReverseRpcRenderClient } from "./services/agent-runtime/tools/web/reverse-rpc-render-client"; import { setSidecarRenderClient } from "./services/agent-runtime/tools/web/render-client-holder"; import { setPersistedSettingsMutationWriter } from "./services/system/settings-store"; @@ -204,7 +204,7 @@ async function handleRpcLine(line: string): Promise { return; } const response = responsePayload; - if (response.error) pending.reject(new Error("browser request failed")); + if (response.error) pending.reject(browserRpcErrorFromPayload(response.error)); else pending.resolve(response.result); return; } diff --git a/apps/sidecar/src/rpc/browser-rpc-sequence.test.ts b/apps/sidecar/src/rpc/browser-rpc-sequence.test.ts index 8f64426b..17a1cc9f 100644 --- a/apps/sidecar/src/rpc/browser-rpc-sequence.test.ts +++ b/apps/sidecar/src/rpc/browser-rpc-sequence.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { classifyBrowserRpcResponse } from "./browser-rpc-sequence"; +import { browserRpcErrorFromPayload, classifyBrowserRpcResponse } from "./browser-rpc-sequence"; describe("classifyBrowserRpcResponse(#156 序列号单调判定)", () => { test("正常路径:MAC 有效且序号 = 当前 +1 → advance", () => { @@ -21,3 +21,19 @@ describe("classifyBrowserRpcResponse(#156 序列号单调判定)", () => { expect(classifyBrowserRpcResponse(9, false, 0)).toBe("reject-pending"); }); }); + +describe("browserRpcErrorFromPayload(#252 错误码传播)", () => { + test("保留 Desktop 返回的稳定错误码", () => { + const error = browserRpcErrorFromPayload({ code: "actionability_failed" }); + + expect(error.code).toBe("actionability_failed"); + expect(error.message).toBe("actionability_failed"); + }); + + test("无效错误载荷安全退化为 browser_internal_error", () => { + const error = browserRpcErrorFromPayload(undefined); + + expect(error.code).toBe("browser_internal_error"); + expect(error.message).toBe("browser_internal_error"); + }); +}); diff --git a/apps/sidecar/src/rpc/browser-rpc-sequence.ts b/apps/sidecar/src/rpc/browser-rpc-sequence.ts index dca5d113..ee77c2d9 100644 --- a/apps/sidecar/src/rpc/browser-rpc-sequence.ts +++ b/apps/sidecar/src/rpc/browser-rpc-sequence.ts @@ -15,3 +15,19 @@ export function classifyBrowserRpcResponse( if (!macOk || sequence <= inboundSequence) return "reject-pending"; return "advance"; } + +export class BrowserRpcError extends Error { + readonly code: string; + + constructor(code: string, message = code) { + super(message); + this.name = "BrowserRpcError"; + this.code = code; + } +} + +export function browserRpcErrorFromPayload(error: { code?: unknown; message?: unknown } | undefined): BrowserRpcError { + const code = typeof error?.code === "string" && error.code ? error.code : "browser_internal_error"; + const message = typeof error?.message === "string" && error.message ? error.message : code; + return new BrowserRpcError(code, message); +} diff --git a/apps/sidecar/src/services/browser/browser-broker.test.ts b/apps/sidecar/src/services/browser/browser-broker.test.ts index c4b86b13..7128e007 100644 --- a/apps/sidecar/src/services/browser/browser-broker.test.ts +++ b/apps/sidecar/src/services/browser/browser-broker.test.ts @@ -1,6 +1,7 @@ import { strict as assert } from "node:assert"; import { test } from "node:test"; import { BrowserBroker } from "./browser-broker"; +import { BrowserRpcError } from "../../rpc/browser-rpc-sequence"; test("extension backend is absent until browser/chrome plugins, setting, and live host agree", async () => { const calls: unknown[] = []; @@ -18,6 +19,18 @@ test("extension backend is absent until browser/chrome plugins, setting, and liv assert.equal(calls.length, 0); }); +test("broker preserves a structured Desktop browser error code", async () => { + const broker = new BrowserBroker({ + request: async () => { throw new BrowserRpcError("actionability_failed", "browser request failed"); }, + }); + broker.setPluginState({ browserEnabled: true }); + + await assert.rejects( + () => broker.dispatch({ method: "playwright_locator_fill", params: { tabId: "tab-1", locator: { version: 1, steps: [{ kind: "css", selector: "#kw" }] }, value: "agent loop" }, browserSessionId: "s", browserTurnId: "t" }), + /actionability_failed/, + ); +}); + test("connected Chrome cookie export is capability-gated, user-only, and paged", async () => { const calls: any[] = [] const broker = new BrowserBroker( diff --git a/apps/sidecar/src/services/browser/browser-broker.ts b/apps/sidecar/src/services/browser/browser-broker.ts index 723192dd..4cb32053 100644 --- a/apps/sidecar/src/services/browser/browser-broker.ts +++ b/apps/sidecar/src/services/browser/browser-broker.ts @@ -425,7 +425,10 @@ function stableJson(value: unknown): string { return JSON.stringify(value) ?? "null" } function stableBrowserErrorCode(error: unknown): string { - const value = error instanceof Error ? error.message : "" + const structuredCode = error && typeof error === "object" && typeof (error as { code?: unknown }).code === "string" + ? (error as { code: string }).code + : "" + const value = structuredCode || (error instanceof Error ? error.message : "") return new Set(["browser_unavailable", "invalid_browser_request", "invalid_url", "private_origin_confirmation_required", "stale_target", "tab_not_found", "tab_generation_changed", "confirmation_unavailable", "reference_grant_expired", "action_denied", "strict_locator_violation", "actionability_failed", "dialog_blocking", "unsupported", "executed_unknown"]).has(value) ? value : "browser_internal_error" }