fix(browser): preserve explicit ai snapshot refs · openclaw/openclaw@4e42a4c
steipete
·
2026-04-25
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
11 | 11 | |
12 | 12 | ### Fixes |
13 | 13 | |
| 14 | +- Browser/tool: keep explicit AI snapshots from inheriting the efficient role-snapshot default and preserve numeric Playwright AI refs, so `--format ai` remains a real AI snapshot path. Fixes #62550. Thanks @ly85206559. |
14 | 15 | - Slack/messages: serialize write-client requests and whole outbound sends per target so rapid multi-message Slack replies preserve send order. Fixes #69101. (#69105) Thanks @nightq and @ztexydt-cqh. |
15 | 16 | - Slack/messages: keep Slack bot tokens out of internal message-ordering and DM cache keys. |
16 | 17 | - Slack/exec approvals: resolve native approval button clicks over the Gateway instead of delivering `/approve ...` as plain agent text, preserving retry buttons if Gateway resolution fails. Fixes #71023. (#71025) Thanks @marusan03. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -234,13 +234,12 @@ export async function executeSnapshotAction(params: {
|
234 | 234 | const { input, baseUrl, profile, proxyRequest } = params; |
235 | 235 | const snapshotDefaults = browserToolActionDeps.loadConfig().browser?.snapshotDefaults; |
236 | 236 | const format: "ai" | "aria" | undefined = |
237 | | -input.snapshotFormat === "ai" || input.snapshotFormat === "aria" |
238 | | - ? input.snapshotFormat |
239 | | - : undefined; |
| 237 | +input.snapshotFormat === "ai" ? "ai" : input.snapshotFormat === "aria" ? "aria" : undefined; |
| 238 | +const formatExplicit = format !== undefined; |
240 | 239 | const mode: "efficient" | undefined = |
241 | 240 | input.mode === "efficient" |
242 | 241 | ? "efficient" |
243 | | - : format !== "aria" && snapshotDefaults?.mode === "efficient" |
| 242 | + : !formatExplicit && format !== "aria" && snapshotDefaults?.mode === "efficient" |
244 | 243 | ? "efficient" |
245 | 244 | : undefined; |
246 | 245 | const labels = typeof input.labels === "boolean" ? input.labels : undefined; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -404,7 +404,8 @@ describe("browser tool snapshot maxChars", () => {
|
404 | 404 | configMocks.loadConfig.mockReturnValue({ |
405 | 405 | browser: { snapshotDefaults: { mode: "efficient" } }, |
406 | 406 | }); |
407 | | -await runSnapshotToolCall({ snapshotFormat: "ai" }); |
| 407 | +const tool = createBrowserTool(); |
| 408 | +await tool.execute?.("call-1", { action: "snapshot", target: "host" }); |
408 | 409 | |
409 | 410 | expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( |
410 | 411 | undefined, |
@@ -414,6 +415,19 @@ describe("browser tool snapshot maxChars", () => {
|
414 | 415 | ); |
415 | 416 | }); |
416 | 417 | |
| 418 | +it("does not apply config snapshot defaults to explicit ai snapshots", async () => { |
| 419 | +configMocks.loadConfig.mockReturnValue({ |
| 420 | +browser: { snapshotDefaults: { mode: "efficient" } }, |
| 421 | +}); |
| 422 | +await runSnapshotToolCall({ snapshotFormat: "ai" }); |
| 423 | + |
| 424 | +expect(browserClientMocks.browserSnapshot).toHaveBeenCalled(); |
| 425 | +const opts = browserClientMocks.browserSnapshot.mock.calls.at(-1)?.[1] as |
| 426 | +| { mode?: string } |
| 427 | +| undefined; |
| 428 | +expect(opts?.mode).toBeUndefined(); |
| 429 | +}); |
| 430 | + |
417 | 431 | it("does not apply config snapshot defaults to aria snapshots", async () => { |
418 | 432 | configMocks.loadConfig.mockReturnValue({ |
419 | 433 | browser: { snapshotDefaults: { mode: "efficient" } }, |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -136,6 +136,34 @@ describe("pw-ai", () => {
|
136 | 136 | expect(res.snapshot).toContain("TRUNCATED"); |
137 | 137 | }); |
138 | 138 | |
| 139 | +it("returns numeric ai snapshot refs in the public snapshot output", async () => { |
| 140 | +const snapshot = ['- button "OK" [ref=1]', '- link "Docs" [ref=2]'].join("\n"); |
| 141 | +const p1 = createPage({ targetId: "T1", snapshotFull: snapshot }); |
| 142 | +const browser = createBrowser([p1.page]); |
| 143 | +(chromiumMock.connectOverCDP as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(browser); |
| 144 | + |
| 145 | +const res = await snapshotAiViaPlaywright({ |
| 146 | +cdpUrl: "http://127.0.0.1:18792", |
| 147 | +targetId: "T1", |
| 148 | +}); |
| 149 | + |
| 150 | +expect(res.snapshot).toContain("[ref=1]"); |
| 151 | +expect(res.snapshot).toContain("[ref=2]"); |
| 152 | +expect(res.refs).toMatchObject({ |
| 153 | +1: { role: "button", name: "OK" }, |
| 154 | +2: { role: "link", name: "Docs" }, |
| 155 | +}); |
| 156 | + |
| 157 | +await clickViaPlaywright({ |
| 158 | +cdpUrl: "http://127.0.0.1:18792", |
| 159 | +targetId: "T1", |
| 160 | +ref: "1", |
| 161 | +}); |
| 162 | + |
| 163 | +expect(p1.locator).toHaveBeenCalledWith("aria-ref=1"); |
| 164 | +expect(p1.click).toHaveBeenCalledTimes(1); |
| 165 | +}); |
| 166 | + |
139 | 167 | it("clicks a ref using aria-ref locator", async () => { |
140 | 168 | const p1 = createPage({ targetId: "T1" }); |
141 | 169 | const browser = createBrowser([p1.page]); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -64,7 +64,7 @@ describe("pw-role-snapshot", () => {
|
64 | 64 | expect(parseRoleRef("e12")).toBe("e12"); |
65 | 65 | expect(parseRoleRef("@e12")).toBe("e12"); |
66 | 66 | expect(parseRoleRef("ref=e12")).toBe("e12"); |
67 | | -expect(parseRoleRef("12")).toBeNull(); |
| 67 | +expect(parseRoleRef("12")).toBe("12"); |
68 | 68 | expect(parseRoleRef("")).toBeNull(); |
69 | 69 | }); |
70 | 70 | |
@@ -87,4 +87,18 @@ describe("pw-role-snapshot", () => {
|
87 | 87 | expect(res.refs.e5).toMatchObject({ role: "link", name: "Home" }); |
88 | 88 | expect(res.refs.e7).toMatchObject({ role: "button", name: "Save" }); |
89 | 89 | }); |
| 90 | + |
| 91 | +it("preserves numeric Playwright AI snapshot refs", () => { |
| 92 | +const ai = [ |
| 93 | +"- navigation [ref=1]:", |
| 94 | +' - link "Home" [ref=5]', |
| 95 | +' - button "Save" [ref=7] [cursor=pointer]:', |
| 96 | +].join("\n"); |
| 97 | + |
| 98 | +const res = buildRoleSnapshotFromAiSnapshot(ai, { interactive: true }); |
| 99 | +expect(res.snapshot).toContain("[ref=5]"); |
| 100 | +expect(Object.keys(res.refs).toSorted()).toEqual(["5", "7"]); |
| 101 | +expect(res.refs["5"]).toMatchObject({ role: "link", name: "Home" }); |
| 102 | +expect(res.refs["7"]).toMatchObject({ role: "button", name: "Save" }); |
| 103 | +}); |
90 | 104 | }); |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -265,7 +265,13 @@ export function parseRoleRef(raw: string): string | null {
|
265 | 265 | : trimmed.startsWith("ref=") |
266 | 266 | ? trimmed.slice(4) |
267 | 267 | : trimmed; |
268 | | -return /^e\d+$/.test(normalized) ? normalized : null; |
| 268 | +if (/^e\d+$/i.test(normalized)) { |
| 269 | +return normalized; |
| 270 | +} |
| 271 | +if (/^\d{1,9}$/.test(normalized)) { |
| 272 | +return normalized; |
| 273 | +} |
| 274 | +return null; |
269 | 275 | } |
270 | 276 | |
271 | 277 | export function buildRoleSnapshotFromAriaSnapshot( |
@@ -328,8 +334,12 @@ export function buildRoleSnapshotFromAriaSnapshot(
|
328 | 334 | } |
329 | 335 | |
330 | 336 | function parseAiSnapshotRef(suffix: string): string | null { |
331 | | -const match = suffix.match(/\[ref=(e\d+)\]/i); |
332 | | -return match ? match[1] : null; |
| 337 | +const eMatch = suffix.match(/\[ref=(e\d+)\]/i); |
| 338 | +if (eMatch) { |
| 339 | +return eMatch[1]; |
| 340 | +} |
| 341 | +const numMatch = suffix.match(/\[ref=(\d{1,9})\]/); |
| 342 | +return numMatch ? numMatch[1] : null; |
333 | 343 | } |
334 | 344 | |
335 | 345 | /** |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -101,12 +101,17 @@ describe("browser cli snapshot defaults", () => {
|
101 | 101 | args: ["--format", "aria"], |
102 | 102 | expectMode: undefined, |
103 | 103 | }, |
| 104 | +{ |
| 105 | +label: "does not apply config snapshot defaults to explicit ai snapshots", |
| 106 | +args: ["--format", "ai"], |
| 107 | +expectMode: undefined, |
| 108 | +}, |
104 | 109 | ])("$label", async ({ args, expectMode }) => { |
105 | 110 | configMocks.loadConfig.mockReturnValue({ |
106 | 111 | browser: { snapshotDefaults: { mode: "efficient" } }, |
107 | 112 | }); |
108 | 113 | |
109 | | -if (args.includes("--format")) { |
| 114 | +if (args.includes("--format") && args.includes("aria")) { |
110 | 115 | gatewayMocks.callGatewayFromCli.mockResolvedValueOnce({ |
111 | 116 | ok: true, |
112 | 117 | format: "aria", |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -75,8 +75,13 @@ export function registerBrowserInspectCommands(
|
75 | 75 | const parent = parentOpts(cmd); |
76 | 76 | const profile = parent?.browserProfile; |
77 | 77 | const format = opts.format === "aria" ? "aria" : "ai"; |
| 78 | +const formatWasExplicit = |
| 79 | +typeof cmd.getOptionValueSource === "function" && |
| 80 | +cmd.getOptionValueSource("format") === "cli"; |
78 | 81 | const configMode = |
79 | | -format === "ai" && loadConfig().browser?.snapshotDefaults?.mode === "efficient" |
| 82 | +!formatWasExplicit && |
| 83 | +format === "ai" && |
| 84 | +loadConfig().browser?.snapshotDefaults?.mode === "efficient" |
80 | 85 | ? "efficient" |
81 | 86 | : undefined; |
82 | 87 | const mode = opts.efficient === true || opts.mode === "efficient" ? "efficient" : configMode; |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。