dsh-browser-playwright-codex 0.7.8 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/NOTICE.md +26 -5
- package/README.md +33 -10
- package/assets/swarm-rebuild.js +3864 -0
- package/lib/browser-lifecycle.js +27 -1
- package/lib/challenge.d.ts +7 -4
- package/lib/challenge.js +28 -6
- package/lib/client.js +1245 -647
- package/lib/config.d.ts +26 -0
- package/lib/config.js +19 -0
- package/lib/errors.d.ts +17 -0
- package/lib/errors.js +28 -0
- package/lib/gates-patch.d.ts +41 -1
- package/lib/gates-patch.js +103 -5
- package/lib/injected.js +45 -0
- package/lib/page-actions.d.ts +24 -0
- package/lib/page-actions.js +73 -11
- package/lib/provider.js +8 -0
- package/lib/read-deadline.d.ts +5 -1
- package/lib/read-deadline.js +10 -3
- package/lib/runtime-state.d.ts +26 -0
- package/lib/runtime-state.js +100 -0
- package/lib/session.d.ts +11 -0
- package/lib/session.js +80 -11
- package/lib/settings-card.js +123 -0
- package/lib/snapshot-render.js +52 -7
- package/lib/swarm-contract.d.ts +232 -0
- package/lib/swarm-contract.js +215 -0
- package/lib/swarm-curtain.d.ts +22 -0
- package/lib/swarm-curtain.js +115 -0
- package/lib/swarm-intro.d.ts +66 -0
- package/lib/swarm-intro.js +829 -0
- package/lib/tool-types.d.ts +10 -0
- package/lib/tool.js +20 -6
- package/lib/types.d.ts +6 -0
- package/package.json +18 -13
package/lib/browser-lifecycle.js
CHANGED
|
@@ -9,6 +9,8 @@ import fs from 'node:fs';
|
|
|
9
9
|
import { AUTO_CHANNELS, HUMANIZED_LAUNCH } from "./config.js";
|
|
10
10
|
import { SNAPSHOT_SCRIPT } from "./injected.js";
|
|
11
11
|
import { BrowserError, launchFailed } from "./errors.js";
|
|
12
|
+
import { forgetOnBrowserClose, showWelcome } from "./swarm-intro.js";
|
|
13
|
+
import { swarmBootScript } from "./swarm-curtain.js";
|
|
12
14
|
/**
|
|
13
15
|
* Ensure the shared persistent context is alive (persistent mode). A
|
|
14
16
|
* profile-backed window is launched once and reused until closed by the
|
|
@@ -50,8 +52,16 @@ async function launchContext(provider) {
|
|
|
50
52
|
const headless = provider.desiredHeadless();
|
|
51
53
|
const headful = !headless;
|
|
52
54
|
const attempts = [];
|
|
55
|
+
// launchPersistentContext waits forever by default (issue #5): a profile another
|
|
56
|
+
// process is holding, a stalled singleton handshake or a wedged GPU init hangs
|
|
57
|
+
// acquire() with no deadline at all. navigationTimeoutMs is the only time scale
|
|
58
|
+
// the user can tune, but it may legitimately be as low as 1s (navigation), which
|
|
59
|
+
// is far too short for a cold Chrome start — so the launch deadline never drops
|
|
60
|
+
// below 30s.
|
|
61
|
+
const launchTimeoutMs = Math.max(provider.config.launch.navigationTimeoutMs, 30_000);
|
|
53
62
|
const baseOptions = {
|
|
54
63
|
headless,
|
|
64
|
+
timeout: launchTimeoutMs,
|
|
55
65
|
// Headful windows must render like a normal Chrome the user opened
|
|
56
66
|
// by hand: the page viewport equals the real window size and the
|
|
57
67
|
// system DPI scaling (e.g. 150%) is honoured. Playwright's default
|
|
@@ -87,7 +97,11 @@ async function launchContext(provider) {
|
|
|
87
97
|
// A stale Chrome singleton lock from a dirty shutdown breaks a relaunch
|
|
88
98
|
// into the same profile; clear the lock files and retry once before
|
|
89
99
|
// surfacing the failure.
|
|
90
|
-
|
|
100
|
+
// Playwright reports the same situation as a timeout ("browserType.launchPersistentContext:
|
|
101
|
+
// Timeout 30000ms exceeded") rather than as a lock message, so match both shapes
|
|
102
|
+
// or the retry never fires against the profile lock it was written for.
|
|
103
|
+
if (error instanceof Error &&
|
|
104
|
+
/user data directory|SingletonLock|ProcessSingleton|Timeout \d+ms exceeded/i.test(error.message)) {
|
|
91
105
|
await provider.clearChromeLocks();
|
|
92
106
|
provider.context = await chromium.launchPersistentContext(provider.profileDir, { ...baseOptions, ...extra });
|
|
93
107
|
provider.launchedHeadless = headless;
|
|
@@ -100,6 +114,8 @@ async function launchContext(provider) {
|
|
|
100
114
|
// External close (user closed the window, crash): forget every
|
|
101
115
|
// session handle bound to this context; the next acquire relaunches.
|
|
102
116
|
void ctx.on('close', () => {
|
|
117
|
+
// 整个插件浏览器窗口关掉:默认忘掉这趟播过哪些站,下次重开全部重播。
|
|
118
|
+
forgetOnBrowserClose(ctx);
|
|
103
119
|
if (provider.context !== ctx)
|
|
104
120
|
return;
|
|
105
121
|
provider.context = undefined;
|
|
@@ -122,11 +138,21 @@ async function launchContext(provider) {
|
|
|
122
138
|
// page cannot leave the list by itself (navigation-guard.ts).
|
|
123
139
|
await provider.installHostGuard(ctx);
|
|
124
140
|
await ctx.addInitScript(SNAPSHOT_SCRIPT);
|
|
141
|
+
// 共享窗口(persistent)这条路也要注册「同一个引擎」的提前启动脚本。
|
|
142
|
+
// 之前只加在 provider.ts 的 newContext 分支,用户实际用的是这里,
|
|
143
|
+
// 第一帧脚本根本没注册 —— 标记留在 window.name 里没被消费(2026-11-09 实测)。
|
|
144
|
+
await ctx.addInitScript({ content: swarmBootScript() });
|
|
125
145
|
// The initial blank page predates addInitScript: reload so the
|
|
126
146
|
// snapshot engine and restored localStorage apply on first use.
|
|
127
147
|
for (const page of ctx.pages()) {
|
|
128
148
|
await page.reload({ waitUntil: 'domcontentloaded' }).catch(() => { });
|
|
129
149
|
}
|
|
150
|
+
// 刚开的窗口停在 about:blank —— 一张纯白占位页。换成启动卡片,并让光标把
|
|
151
|
+
// 它画一遍。只处理还没去任何网址的初始页,不动已恢复的标签。
|
|
152
|
+
const firstPage = ctx.pages()[0];
|
|
153
|
+
if (firstPage !== undefined && firstPage.url() === 'about:blank') {
|
|
154
|
+
await showWelcome(firstPage, provider.config);
|
|
155
|
+
}
|
|
130
156
|
return ctx;
|
|
131
157
|
};
|
|
132
158
|
if (executablePath !== undefined) {
|
package/lib/challenge.d.ts
CHANGED
|
@@ -32,11 +32,14 @@ export declare function challengeNote(headless: boolean): string;
|
|
|
32
32
|
export declare const CHALLENGE_NOTE = "[human verification] The page triggered an anti-bot challenge; it is NOT solvable by automation and must NOT be retried blindly. The browser window is open on the user's desktop \u2014 tell the user a verification appeared, wait for them to finish it (a few seconds), then call browser_snapshot again. The clearance cookie is saved to the profile automatically and keeps working for the session.";
|
|
33
33
|
/**
|
|
34
34
|
* Appended after an action that left the page byte-for-byte unchanged on a page
|
|
35
|
-
* with almost nothing else to interact with.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* with almost nothing else to interact with. It states the observation and the
|
|
36
|
+
* recovery — re-snapshot in case the page is still rendering, then try another
|
|
37
|
+
* approach — and deliberately does NOT guess "anti-bot wall" or send the user to
|
|
38
|
+
* look at the window: a normal fill or checkbox toggle also lands here when the
|
|
39
|
+
* page state it changed is not in the compared subset, and scaring the user off
|
|
40
|
+
* on a successful action is worse than saying less.
|
|
38
41
|
*/
|
|
39
|
-
export declare const NO_PROGRESS_NOTE = "[no visible change]
|
|
42
|
+
export declare const NO_PROGRESS_NOTE = "[no visible change] this action did not produce a visible change. If what you just did was a submit, a search or a navigation, the page may still be rendering \u2014 take one more browser_snapshot to confirm. If it still looks the same, try a different approach (for example another entry point on the page).";
|
|
40
43
|
/** A page with at most this many actionable elements counts as "nearly empty". */
|
|
41
44
|
export declare const NO_PROGRESS_MAX_REFS = 3;
|
|
42
45
|
/**
|
package/lib/challenge.js
CHANGED
|
@@ -40,11 +40,27 @@ function scan(nodes, depth, maxDepth, out, cap) {
|
|
|
40
40
|
return;
|
|
41
41
|
const name = node.name ?? '';
|
|
42
42
|
if (name !== '')
|
|
43
|
-
out.push({ role: (node.role ?? '').toLowerCase(), name });
|
|
43
|
+
out.push({ role: (node.role ?? '').toLowerCase(), name, state: stateOf(node) });
|
|
44
44
|
if (depth < maxDepth)
|
|
45
45
|
scan(node.children, depth + 1, maxDepth, out, cap);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Compact interactive-state signature of one node. A fill or a toggle changes
|
|
50
|
+
* this without changing the role or name, so the no-progress fingerprint has to
|
|
51
|
+
* include it — otherwise a normal fill or checkbox toggle looks like "no visible
|
|
52
|
+
* change" and trips the anti-bot guess.
|
|
53
|
+
*/
|
|
54
|
+
function stateOf(node) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
if (node.checked !== undefined)
|
|
57
|
+
parts.push(node.checked ? 'checked' : 'unchecked');
|
|
58
|
+
if (node.selected !== undefined)
|
|
59
|
+
parts.push(node.selected ? 'selected' : 'unselected');
|
|
60
|
+
if (node.value !== undefined && node.value !== '')
|
|
61
|
+
parts.push('v=' + node.value);
|
|
62
|
+
return parts.join(',');
|
|
63
|
+
}
|
|
48
64
|
/** Evidence string when one header rule holds, else null. */
|
|
49
65
|
function headerEvidence(rule, headers) {
|
|
50
66
|
if (headers === undefined)
|
|
@@ -232,11 +248,14 @@ export function challengeNote(headless) {
|
|
|
232
248
|
export const CHALLENGE_NOTE = "[human verification] The page triggered an anti-bot challenge; it is NOT solvable by automation and must NOT be retried blindly. The browser window is open on the user's desktop — tell the user a verification appeared, wait for them to finish it (a few seconds), then call browser_snapshot again. The clearance cookie is saved to the profile automatically and keeps working for the session.";
|
|
233
249
|
/**
|
|
234
250
|
* Appended after an action that left the page byte-for-byte unchanged on a page
|
|
235
|
-
* with almost nothing else to interact with.
|
|
236
|
-
*
|
|
237
|
-
*
|
|
251
|
+
* with almost nothing else to interact with. It states the observation and the
|
|
252
|
+
* recovery — re-snapshot in case the page is still rendering, then try another
|
|
253
|
+
* approach — and deliberately does NOT guess "anti-bot wall" or send the user to
|
|
254
|
+
* look at the window: a normal fill or checkbox toggle also lands here when the
|
|
255
|
+
* page state it changed is not in the compared subset, and scaring the user off
|
|
256
|
+
* on a successful action is worse than saying less.
|
|
238
257
|
*/
|
|
239
|
-
export const NO_PROGRESS_NOTE = '[no visible change]
|
|
258
|
+
export const NO_PROGRESS_NOTE = '[no visible change] this action did not produce a visible change. If what you just did was a submit, a search or a navigation, the page may still be rendering — take one more browser_snapshot to confirm. If it still looks the same, try a different approach (for example another entry point on the page).';
|
|
240
259
|
/** A page with at most this many actionable elements counts as "nearly empty". */
|
|
241
260
|
export const NO_PROGRESS_MAX_REFS = 3;
|
|
242
261
|
/** Cheap, order-stable hash (djb2) so a fingerprint costs no prompt budget. */
|
|
@@ -261,7 +280,10 @@ export function snapshotFingerprint(input) {
|
|
|
261
280
|
input.url ?? '',
|
|
262
281
|
input.title ?? '',
|
|
263
282
|
String(input.refs ?? 0),
|
|
264
|
-
|
|
283
|
+
// Include interactive state (checked/selected/value) so a fill or a toggle
|
|
284
|
+
// registers as change — otherwise a normal fill looks like "no visible
|
|
285
|
+
// change" and trips the anti-bot guess.
|
|
286
|
+
names.map((node) => node.role + '=' + node.name + (node.state ? '[' + node.state + ']' : '')).join('\u0001'),
|
|
265
287
|
].join('\u0000');
|
|
266
288
|
return hash(material);
|
|
267
289
|
}
|