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.
@@ -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
- if (error instanceof Error && /user data directory|SingletonLock|ProcessSingleton/i.test(error.message)) {
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) {
@@ -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. Worded as a guess, because that is
36
- * what it is: it is the shape an unrecognised wall has, and the whole point of
37
- * this tier is to cover the vendors we have no signature for.
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] that action left the page identical \u2014 same URL, same title, same elements \u2014 and the page offers almost nothing else to click. This is a guess, not a diagnosis: it is what an unrecognised anti-bot wall looks like. Take one browser_snapshot to confirm; if the page still looks unchanged, stop and tell the user what you were trying to do and ask them to look at the open window, instead of retrying the same action.";
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. Worded as a guess, because that is
236
- * what it is: it is the shape an unrecognised wall has, and the whole point of
237
- * this tier is to cover the vendors we have no signature for.
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] that action left the page identical — same URL, same title, same elements — and the page offers almost nothing else to click. This is a guess, not a diagnosis: it is what an unrecognised anti-bot wall looks like. Take one browser_snapshot to confirm; if the page still looks unchanged, stop and tell the user what you were trying to do and ask them to look at the open window, instead of retrying the same action.';
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
- names.map((node) => node.role + '=' + node.name).join('\u0001'),
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
  }