diff --git a/apps/extension/src/entrypoints/popup/App.test.tsx b/apps/extension/src/entrypoints/popup/App.test.tsx index f32dacd..5800104 100644 --- a/apps/extension/src/entrypoints/popup/App.test.tsx +++ b/apps/extension/src/entrypoints/popup/App.test.tsx @@ -100,6 +100,24 @@ describe("App", () => { expect(screen.queryByText("录制你的操作,供 Agent 参考")).toBeNull(); }); + it("renders localized copy for a version failure instead of protocol details", () => { + mockUseConnectionState.mockReturnValue({ + snapshot: { + ...baseSnapshot, + lastError: + "[handshake] daemon rejected handshake: version_too_old — peer protocol 1.0 is below local min_compatible_protocol 1.1", + }, + statusState: "disconnected", + setLabel, + setConnectionEnabled, + }); + + render(); + + const error = screen.getByText(/bsk 与 BrowserSkill 扩展版本不兼容/); + expect(error.textContent).not.toContain("min_compatible_protocol"); + }); + it("shows single-line compact metadata and copies the instance id", async () => { mockUseConnectionState.mockReturnValue({ snapshot: { diff --git a/apps/extension/src/entrypoints/popup/App.tsx b/apps/extension/src/entrypoints/popup/App.tsx index 3092a06..c8be08c 100644 --- a/apps/extension/src/entrypoints/popup/App.tsx +++ b/apps/extension/src/entrypoints/popup/App.tsx @@ -83,6 +83,9 @@ export function App() { const daemonProtocol = snapshot.handshake?.protocol_version ?? "—"; const extensionVersion = snapshot.extensionVersion || "—"; const instanceId = snapshot.instanceId || "—"; + const errorMessage = snapshot.lastError?.includes("version_too_old") + ? t("popup.versionIncompatibleError") + : snapshot.lastError; const copyInstanceId = async () => { if (!snapshot.instanceId || !navigator.clipboard?.writeText) return; @@ -252,12 +255,12 @@ export function App() { - {snapshot.lastError && ( + {errorMessage && (
- {snapshot.lastError} + {errorMessage}
)} diff --git a/apps/extension/src/lib/__tests__/connection-controller.test.ts b/apps/extension/src/lib/__tests__/connection-controller.test.ts index 6d5b578..6790916 100644 --- a/apps/extension/src/lib/__tests__/connection-controller.test.ts +++ b/apps/extension/src/lib/__tests__/connection-controller.test.ts @@ -27,19 +27,19 @@ function handshake( describe("computeConnectedState (protocol-based compat)", () => { it("returns connected when protocol strings match", () => { - expect(computeConnectedState(handshake("1.0", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({ + expect(computeConnectedState(handshake("1.1", "1.1"), MIN_COMPATIBLE_PROTOCOL)).toEqual({ kind: "connected", }); }); it("returns version_skew when daemon protocol minor is newer", () => { - expect(computeConnectedState(handshake("1.1", "1.0"))).toEqual({ + expect(computeConnectedState(handshake("1.2", "1.1"))).toEqual({ kind: "version_skew", }); }); it("returns version_skew when daemon protocol string differs but floor is satisfied", () => { - expect(computeConnectedState(handshake("1", "1.0"))).toEqual({ + expect(computeConnectedState(handshake("1.1.0", "1.1"))).toEqual({ kind: "version_skew", }); }); @@ -48,16 +48,19 @@ describe("computeConnectedState (protocol-based compat)", () => { const result = computeConnectedState(handshake("2.0", "1.0")); expect(result.kind).toBe("rejected"); if (result.kind === "rejected") { - expect(result.reason).toContain("protocol-major mismatch"); + expect(result.reason).toContain("bsk CLI"); + expect(result.reason).toContain("BrowserSkill extension"); + expect(result.reason).not.toContain("protocol"); } }); it("rejects when extension is below daemon min_compatible_protocol", () => { - const result = computeConnectedState(handshake("1.0", "1.5")); + const result = computeConnectedState(handshake("1.1", "1.5")); expect(result.kind).toBe("rejected"); if (result.kind === "rejected") { - expect(result.reason).toContain("min_compatible_protocol"); - expect(result.reason).toContain("1.5"); + expect(result.reason).toContain("BrowserSkill extension is out of date"); + expect(result.reason).toContain("Update and reload the extension"); + expect(result.reason).not.toContain("min_compatible_protocol"); } }); @@ -65,7 +68,7 @@ describe("computeConnectedState (protocol-based compat)", () => { const result = computeConnectedState({ server: "browser-skill-daemon", version: "0.1.0", - protocol_version: "1.0", + protocol_version: "1.1", min_compatible_peer: "0.1.0", }); expect(result).toEqual({ kind: "connected" }); @@ -75,16 +78,23 @@ describe("computeConnectedState (protocol-based compat)", () => { const result = computeConnectedState(handshake("1.0", "1.0"), "1.1"); expect(result.kind).toBe("rejected"); if (result.kind === "rejected") { - expect(result.reason).toContain("below extension min_compatible_protocol"); + expect(result.reason).toContain("bsk CLI is out of date"); + expect(result.reason).toContain("bsk daemon restart"); + expect(result.reason).not.toContain("min_compatible_protocol"); } }); + it("rejects a pre-Trace-v3 daemon using the configured extension floor", () => { + const result = computeConnectedState(handshake("1.0", "1.0")); + expect(result.kind).toBe("rejected"); + }); + it("rejects malformed daemon min_compatible_protocol with a daemon-floor reason", () => { - const result = computeConnectedState(handshake("1.0", "not-a-protocol")); + const result = computeConnectedState(handshake("1.1", "not-a-protocol")); expect(result.kind).toBe("rejected"); if (result.kind === "rejected") { - expect(result.reason).toContain("daemon min_compatible_protocol"); - expect(result.reason).toContain("not-a-protocol"); + expect(result.reason).toContain("could not verify version compatibility"); + expect(result.reason).not.toContain("min_compatible_protocol"); } }); @@ -242,11 +252,11 @@ describe("ConnectionController connectionEnabled", () => { const second = transport.send.mock.calls[1]?.[0] as { id: string }; expect(second.id).not.toBe(first.id); - transport.emitMessage({ id: first.id, result: handshake("1.0", "1.0") }); + transport.emitMessage({ id: first.id, result: handshake("1.1", "1.1") }); await Promise.resolve(); expect(controller.snapshot().state).not.toBe("connected"); - transport.emitMessage({ id: second.id, result: handshake("1.0", "1.0") }); + transport.emitMessage({ id: second.id, result: handshake("1.1", "1.1") }); await vi.waitFor(() => expect(controller.snapshot().state).toBe("connected")); }); }); diff --git a/apps/extension/src/lib/connection-controller.ts b/apps/extension/src/lib/connection-controller.ts index 687fe8a..8da78a3 100644 --- a/apps/extension/src/lib/connection-controller.ts +++ b/apps/extension/src/lib/connection-controller.ts @@ -313,6 +313,15 @@ export type HandshakeVerdict = | { kind: "version_skew" } | { kind: "rejected"; reason: string }; +const UPDATE_BOTH_VERSION_MESSAGE = + "The installed bsk CLI and BrowserSkill extension are incompatible. Update both, reload the extension, then run `bsk daemon restart`."; +const UPDATE_BSK_VERSION_MESSAGE = + "The installed bsk CLI is out of date for this BrowserSkill extension. Update bsk, then run `bsk daemon restart`."; +const UPDATE_EXTENSION_VERSION_MESSAGE = + "This BrowserSkill extension is out of date for the installed bsk CLI. Update and reload the extension, then try again."; +const INVALID_VERSION_MESSAGE = + "BrowserSkill could not verify version compatibility. Update bsk and the BrowserSkill extension, reload the extension, then run `bsk daemon restart`."; + /** * Symmetric extension-side **protocol** compat check. * @@ -339,19 +348,19 @@ export function computeConnectedState( if (daemonMajor === null || ourMajor === null || daemonMajor !== ourMajor) { return { kind: "rejected", - reason: `protocol-major mismatch (daemon protocol v${handshake.protocol_version}, extension protocol v${PROTOCOL_VERSION})`, + reason: UPDATE_BOTH_VERSION_MESSAGE, }; } if (compareProtocol(handshake.protocol_version, localMinCompatibleProtocol) === null) { return { kind: "rejected", - reason: `daemon protocol v${handshake.protocol_version} is unparseable`, + reason: INVALID_VERSION_MESSAGE, }; } if (compareProtocol(handshake.protocol_version, localMinCompatibleProtocol)! < 0) { return { kind: "rejected", - reason: `daemon protocol v${handshake.protocol_version} below extension min_compatible_protocol ${localMinCompatibleProtocol}`, + reason: UPDATE_BSK_VERSION_MESSAGE, }; } const peerFloor = handshake.min_compatible_protocol; @@ -359,13 +368,13 @@ export function computeConnectedState( if (compareProtocol(PROTOCOL_VERSION, peerFloor) === null) { return { kind: "rejected", - reason: `daemon min_compatible_protocol ${peerFloor} is unparseable`, + reason: INVALID_VERSION_MESSAGE, }; } if (compareProtocol(PROTOCOL_VERSION, peerFloor)! < 0) { return { kind: "rejected", - reason: `extension protocol v${PROTOCOL_VERSION} below daemon min_compatible_protocol ${peerFloor}`, + reason: UPDATE_EXTENSION_VERSION_MESSAGE, }; } } diff --git a/crates/bsk-cli/skill/SKILL.md b/crates/bsk-cli/skill/SKILL.md index e1cffb9..8546a15 100644 --- a/crates/bsk-cli/skill/SKILL.md +++ b/crates/bsk-cli/skill/SKILL.md @@ -265,19 +265,26 @@ retry — complete the task autonomously or stop gracefully. ### Recording — `bsk record` -Capture the user's own actions in the Agent Window to a `trace.json`, for later LLM-driven automation: +Capture the user's own actions in the Agent Window to a **trace v3 bundle**, for later LLM-driven automation: ```bash -bsk record start --browser [--url https://…] [--purpose "publish a wiki doc"] [--output trace.json] +bsk record start --browser \ + [--url https://…] [--purpose "publish a wiki doc"] \ + [--max-page-tokens 3000] [--redact-values] \ + [--output trace] # `--url` is optional; default https://example.com/ when omitted (must be http(s)). -# Blocks until the user clicks Finish in the recording panel, then writes ./trace.json and closes the window. +# Blocks until the user clicks Finish in the recording panel, then writes: +# trace/trace.json — action chain + state index (version 3) +# trace/pages/ — one `.vom.txt` observe snapshot per settled page state -bsk record stop [--output trace.json] # terminal fallback if the browser panel is unavailable +bsk record stop [--output trace] # terminal fallback if the browser panel is unavailable ``` -- The trace is a **record-only action log** (a `pages[]` dictionary + `navigate`/`click`/`fill`/`select`/`press` steps with `target` descriptors). It records *what the user did*; deciding which inputs are variable is left to the executing agent. +- **Bundle layout:** `--output` is a directory (default `./trace`) containing `trace.json` and `pages/`. `trace.json` lists `states[]` (page observation ids) and `steps[]` (each step binds `state` = observe snapshot *before* the action and `result.state` = snapshot *after* settle). Page bodies live in `pages/sN.vom.txt` using the same VOM format as `bsk observe`. +- Each `states[]` entry is one **settled page observation** (captured after recording start, navigation landing, or action settle — not on a timer). +- `target.ref` values like `@e12` exist **only inside** the bundle for disambiguation; do **not** copy `@eN` refs into SKILL.md or agent runbooks — use visible names from the observation text instead. - `--purpose` is optional context metadata; it does **not** change what gets captured. -- There is **no** `bsk replay` — to redo a flow, read the trace and reuse the existing `session` / `snapshot` / `@eN` / `click` / `fill` tools. Follow **Stop when the goal is met**. +- `--redact-values` masks all form values in page files as `[filled]` / `[empty]`. - Do **not** record on banking/SSO/password-manager pages; passwords are redacted but traces may still contain sensitive text. ## Error handling diff --git a/packages/i18n/src/locales/en-US/extension.json b/packages/i18n/src/locales/en-US/extension.json index 68c1a3e..4ef0956 100644 --- a/packages/i18n/src/locales/en-US/extension.json +++ b/packages/i18n/src/locales/en-US/extension.json @@ -6,20 +6,20 @@ "disconnected": "Disconnected", "connecting": "Connecting…", "connected": "Connected", - "version_skew": "Protocol mismatch", + "version_skew": "Version mismatch", "disabled": "Connection off" }, "stateDetail": { "disconnected": "Open BrowserSkill to connect.", "connecting": "Connecting to BrowserSkill…", "connected": "Ready for automation tasks.", - "version_skew": "Some features may be unavailable." + "version_skew": "bsk and the browser extension differ in version. You can continue using them, but updating is recommended." }, "stateBadge": { "disconnected": "Standby", "connecting": "Igniting", "connected": "Ready", - "version_skew": "Action needed", + "version_skew": "Update recommended", "disabled": "Off" }, "connectionToggleTitle": "BrowserSkill connection", @@ -27,6 +27,7 @@ "controlHintsToggleHint": "Show the status pill and orange glow while the Agent controls a page.", "controlHintsInfoLabel": "About control hints", "versionSkewWarning": "Extension protocol v{{extensionProtocol}}, daemon v{{daemonProtocol}}.", + "versionIncompatibleError": "bsk and the BrowserSkill extension are incompatible. Update both, reload the extension, then run bsk daemon restart.", "upgradeAvailable": "Upgradable", "launcher": { "title": "Quick actions" diff --git a/packages/i18n/src/locales/zh-CN/extension.json b/packages/i18n/src/locales/zh-CN/extension.json index 520b791..331e173 100644 --- a/packages/i18n/src/locales/zh-CN/extension.json +++ b/packages/i18n/src/locales/zh-CN/extension.json @@ -6,20 +6,20 @@ "disconnected": "未连接", "connecting": "连接中…", "connected": "已连接", - "version_skew": "协议不一致", + "version_skew": "版本不一致", "disabled": "连接已关闭" }, "stateDetail": { "disconnected": "请先打开 BrowserSkill。", "connecting": "正在连接 BrowserSkill…", "connected": "可接收自动化任务。", - "version_skew": "部分能力可能不可用。" + "version_skew": "bsk 与浏览器扩展版本不同,当前仍可使用,建议更新至最新版本。" }, "stateBadge": { "disconnected": "Standby", "connecting": "Igniting", "connected": "Ready", - "version_skew": "Action needed", + "version_skew": "建议更新", "disabled": "Off" }, "connectionToggleTitle": "BrowserSkill 连接", @@ -27,6 +27,7 @@ "controlHintsToggleHint": "Agent 控制页面时显示提示条和橙色闪光。", "controlHintsInfoLabel": "控制提示说明", "versionSkewWarning": "扩展协议 v{{extensionProtocol}},daemon 协议 v{{daemonProtocol}}。", + "versionIncompatibleError": "bsk 与 BrowserSkill 扩展版本不兼容。请将两者更新至最新版本,重新加载扩展后,再运行 bsk daemon restart。", "upgradeAvailable": "可升级", "launcher": { "title": "快捷功能" diff --git a/skill/SKILL.md b/skill/SKILL.md index e1cffb9..8546a15 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -265,19 +265,26 @@ retry — complete the task autonomously or stop gracefully. ### Recording — `bsk record` -Capture the user's own actions in the Agent Window to a `trace.json`, for later LLM-driven automation: +Capture the user's own actions in the Agent Window to a **trace v3 bundle**, for later LLM-driven automation: ```bash -bsk record start --browser [--url https://…] [--purpose "publish a wiki doc"] [--output trace.json] +bsk record start --browser \ + [--url https://…] [--purpose "publish a wiki doc"] \ + [--max-page-tokens 3000] [--redact-values] \ + [--output trace] # `--url` is optional; default https://example.com/ when omitted (must be http(s)). -# Blocks until the user clicks Finish in the recording panel, then writes ./trace.json and closes the window. +# Blocks until the user clicks Finish in the recording panel, then writes: +# trace/trace.json — action chain + state index (version 3) +# trace/pages/ — one `.vom.txt` observe snapshot per settled page state -bsk record stop [--output trace.json] # terminal fallback if the browser panel is unavailable +bsk record stop [--output trace] # terminal fallback if the browser panel is unavailable ``` -- The trace is a **record-only action log** (a `pages[]` dictionary + `navigate`/`click`/`fill`/`select`/`press` steps with `target` descriptors). It records *what the user did*; deciding which inputs are variable is left to the executing agent. +- **Bundle layout:** `--output` is a directory (default `./trace`) containing `trace.json` and `pages/`. `trace.json` lists `states[]` (page observation ids) and `steps[]` (each step binds `state` = observe snapshot *before* the action and `result.state` = snapshot *after* settle). Page bodies live in `pages/sN.vom.txt` using the same VOM format as `bsk observe`. +- Each `states[]` entry is one **settled page observation** (captured after recording start, navigation landing, or action settle — not on a timer). +- `target.ref` values like `@e12` exist **only inside** the bundle for disambiguation; do **not** copy `@eN` refs into SKILL.md or agent runbooks — use visible names from the observation text instead. - `--purpose` is optional context metadata; it does **not** change what gets captured. -- There is **no** `bsk replay` — to redo a flow, read the trace and reuse the existing `session` / `snapshot` / `@eN` / `click` / `fill` tools. Follow **Stop when the goal is met**. +- `--redact-values` masks all form values in page files as `[filled]` / `[empty]`. - Do **not** record on banking/SSO/password-manager pages; passwords are redacted but traces may still contain sensitive text. ## Error handling