@web-auto/camo 0.1.25 → 0.2.0

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/README.md CHANGED
@@ -473,6 +473,7 @@ Condition types:
473
473
 
474
474
  ### Environment Variables
475
475
 
476
+ - `CAMO_INPUT_MODE` - Input mode: `playwright` (default) or `cdp`. CDP mode uses `Input.dispatchMouseEvent` via Chrome DevTools Protocol, bypassing OS-level input system. Does not require window foreground. See [CDP Input Mode](#cdp-input-mode) below.
476
477
  - `CAMO_BROWSER_URL` - Browser service URL (default: `http://127.0.0.1:7704`)
477
478
  - `CAMO_INSTALL_DIR` - `@web-auto/camo` 安装目录(可选,首次安装兜底)
478
479
  - `CAMO_REPO_ROOT` - Camo repository root (optional, dev mode)
@@ -484,6 +485,51 @@ Condition types:
484
485
  - `CAMO_PROGRESS_WS_HOST` / `CAMO_PROGRESS_WS_PORT` - Progress websocket daemon bind address (default: `127.0.0.1:7788`)
485
486
  - `CAMO_DEFAULT_WINDOW_VERTICAL_RESERVE` - Reserved vertical pixels for default headful auto-size
486
487
 
488
+ ### CDP Input Mode
489
+
490
+ By default, Camo uses Playwright's high-level input API (`page.mouse.click`), which goes through the OS input system and requires the browser window to be in the foreground. This can cause hangs (up to 30s timeout) on Windows when the window loses focus.
491
+
492
+ CDP mode sends mouse events directly via the Chrome DevTools Protocol (`Input.dispatchMouseEvent`), which:
493
+
494
+ - **Does not require window foreground** — works with minimized, background, or headless windows
495
+ - **Does not depend on OS input system** — no `bringToFront`, no `ensureInputReady`
496
+ - **Bypasses input pipeline checks** — no 30s timeout risk from `ensureInputReady` hanging
497
+
498
+ #### How to enable
499
+
500
+ ```bash
501
+ # Environment variable (recommended)
502
+ CAMO_INPUT_MODE=cdp camo start xhs-qa-1 --url https://www.xiaohongshu.com
503
+
504
+ # Or set in shell profile
505
+ export CAMO_INPUT_MODE=cdp
506
+ ```
507
+
508
+ #### Behavior differences
509
+
510
+ | Feature | Playwright (default) | CDP mode |
511
+ |---------|---------------------|----------|
512
+ | Window foreground required | Yes | No |
513
+ | OS input system | Yes | No |
514
+ | Auto-scroll to element | Yes (via Playwright) | No (caller must ensure element in viewport) |
515
+ | `ensureInputReady` check | Yes (can hang 30s) | Skipped |
516
+ | `bringToFront` | Yes (default) | Skipped |
517
+ | Nudge/recovery on timeout | Yes | No (fast fail) |
518
+ | Input coordinate system | Viewport-relative | Viewport-relative (same) |
519
+
520
+ #### Limitations
521
+
522
+ - **Element must be in viewport**: CDP clicks at coordinates only. If the target element is scrolled out of view, the click will miss. Callers (like webauto's `clickPoint`) already resolve viewport-relative coordinates via `getBoundingClientRect`.
523
+ - **No auto-scroll**: Unlike Playwright's `page.click(selector)`, CDP mode does not scroll to bring elements into view.
524
+ - **keyboard operations still use Playwright**: `keyboard:press` and `keyboard:type` are not affected by CDP mode (they already work reliably in background via Playwright's keyboard API).
525
+
526
+ #### Related environment variables
527
+
528
+ - `CAMO_INPUT_ACTION_TIMEOUT_MS` — Max wait for input action (default: 30000)
529
+ - `CAMO_INPUT_ACTION_MAX_ATTEMPTS` — Retry count on failure (default: 2)
530
+ - `CAMO_INPUT_READY_SETTLE_MS` — Settle time after input ready (default: 80)
531
+ - `CAMO_BRING_TO_FRONT_MODE` — `never` (skip) or `auto` (default, bring window to front)
532
+
487
533
  ## Session Persistence
488
534
 
489
535
  Camo CLI persists session information locally:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-auto/camo",
3
- "version": "0.1.25",
3
+ "version": "0.2.0",
4
4
  "description": "Camoufox Browser CLI - Cross-platform browser automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -496,7 +496,6 @@ export async function handleStartCommand(args) {
496
496
  const alias = validateAlias(readFlagValue(args, ['--alias']));
497
497
  const idleTimeoutRaw = readFlagValue(args, ['--idle-timeout']);
498
498
  const parsedIdleTimeoutMs = parseDurationMs(idleTimeoutRaw, DEFAULT_HEADLESS_IDLE_TIMEOUT_MS);
499
- const maxTabs = Math.max(1, Math.floor(Number(readFlagValue(args, ['--max-tabs']) || 1) || 1));
500
499
  const wantsDevtools = args.includes('--devtools');
501
500
  const wantsRecord = args.includes('--record');
502
501
  const recordName = readFlagValue(args, ['--record-name']);
@@ -507,7 +506,7 @@ export async function handleStartCommand(args) {
507
506
  ? true
508
507
  : null;
509
508
  if (hasExplicitWidth !== hasExplicitHeight) {
510
- throw new Error('Usage: camo start [profileId] [--url <url>] [--no-headless|--visible] [--devtools] [--record] [--record-name <name>] [--record-output <path>] [--record-overlay|--no-record-overlay] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h> --max-tabs <n>]');
509
+ throw new Error('Usage: camo start [profileId] [--url <url>] [--no-headless|--visible] [--devtools] [--record] [--record-name <name>] [--record-output <path>] [--record-overlay|--no-record-overlay] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]');
511
510
  }
512
511
  if ((hasExplicitWidth && explicitWidth < START_WINDOW_MIN_WIDTH) || (hasExplicitHeight && explicitHeight < START_WINDOW_MIN_HEIGHT)) {
513
512
  throw new Error(`Window size too small. Minimum is ${START_WINDOW_MIN_WIDTH}x${START_WINDOW_MIN_HEIGHT}`);
@@ -528,7 +527,7 @@ export async function handleStartCommand(args) {
528
527
  const arg = args[i];
529
528
  if (arg === '--url') { i++; continue; }
530
529
  if (arg === '--width' || arg === '--height') { i++; continue; }
531
- if (arg === '--alias' || arg === '--idle-timeout' || arg === '--record-name' || arg === '--record-output' || arg === '--max-tabs') { i++; continue; }
530
+ if (arg === '--alias' || arg === '--idle-timeout' || arg === '--record-name' || arg === '--record-output') { i++; continue; }
532
531
  if (arg === '--headless' || arg === '--no-headless' || arg === '--visible') continue;
533
532
  if (arg === '--record' || arg === '--record-overlay' || arg === '--no-record-overlay') continue;
534
533
  if (arg.startsWith('--')) continue;
@@ -628,7 +627,6 @@ export async function handleStartCommand(args) {
628
627
  headless,
629
628
  devtools: wantsDevtools,
630
629
  ...(wantsRecord ? { record: true } : {}),
631
- ...(Number.isFinite(maxTabs) ? { maxTabs } : {}),
632
630
  ...(recordName ? { recordName } : {}),
633
631
  ...(recordOutput ? { recordOutput } : {}),
634
632
  ...(recordOverlay !== null ? { recordOverlay } : {}),
@@ -26,21 +26,13 @@ async function validatePage(profileId, spec = {}, platform = 'generic') {
26
26
  errors.push(`url host mismatch, expected one of: ${hostIncludes.join(',')}`);
27
27
  }
28
28
 
29
- // 锚点驱动的 checkpoint 检查
30
29
  const checkpoints = normalizeArray(spec.checkpointIn || []);
31
30
  let checkpoint = null;
32
31
  if (checkpoints.length > 0) {
33
- // 使用锚点轮询,而非 evaluate
34
- const checkpointSelectors = getCheckpointSelectors(checkpoints, platform);
35
- const anchorResult = await validateAnchors(profileId, checkpointSelectors, {
36
- timeoutMs: 15000, // 15秒最大等待
37
- intervalMs: 500,
38
- });
39
-
40
- if (anchorResult.found) {
41
- checkpoint = checkpoints[0]; // 锚点存在,使用第一个 checkpoint
42
- } else {
43
- errors.push(`anchor not found within ${anchorResult.elapsed}ms, expected one of: ${checkpoints.join(',')}`);
32
+ const detected = await detectCheckpoint({ profileId, platform });
33
+ checkpoint = detected?.data?.checkpoint || null;
34
+ if (!checkpoints.includes(checkpoint)) {
35
+ errors.push(`checkpoint mismatch: got ${checkpoint}, expect one of ${checkpoints.join(',')}`);
44
36
  }
45
37
  }
46
38
 
@@ -50,29 +42,8 @@ async function validatePage(profileId, spec = {}, platform = 'generic') {
50
42
  checkpoint,
51
43
  errors,
52
44
  };
53
- };
54
-
55
- // 添加 getCheckpointSelectors 辅助函数
56
- function getCheckpointSelectors(checkpoints, platform = 'generic') {
57
- const XHS_CHECKPOINTS = {
58
- search_ready: ['#search-input', 'input.search-input', '.search-result-list'],
59
- home_ready: ['.note-item', '.note-item a', 'a[href*="/explore/"]', '[class*="note-item"]'],
60
- detail_ready: ['.note-scroller', '.note-content', '.interaction-container'],
61
- comments_ready: ['.comments-container', '.comment-item'],
62
- login_guard: ['.login-container', '.login-dialog', '#login-container'],
63
- risk_control: ['.qrcode-box', '.captcha-container', '[class*="captcha"]'],
64
- };
65
-
66
- const selectors = [];
67
- for (const cp of checkpoints) {
68
- if (platform === 'xiaohongshu' && XHS_CHECKPOINTS[cp]) {
69
- selectors.push(...XHS_CHECKPOINTS[cp]);
70
- }
71
- }
72
- return selectors.length > 0 ? selectors : checkpoints;
73
45
  }
74
46
 
75
-
76
47
  async function validateContainer(profileId, spec = {}) {
77
48
  const snapshot = await getDomSnapshotByProfile(profileId);
78
49
  const selector = maybeSelector({
@@ -154,31 +125,3 @@ export async function validateOperation({
154
125
  return asErrorPayload('VALIDATION_FAILED', err?.message || String(err), { phase, context });
155
126
  }
156
127
  }
157
-
158
- // 锚点驱动的验证:轮询容器 selector,而非 evaluate
159
- async function validateAnchors(profileId, selectors = [], options = {}) {
160
- const maxMs = Math.max(1000, Number(options.timeoutMs || 30000));
161
- const intervalMs = Math.max(200, Number(options.intervalMs || 500));
162
- const startTime = Date.now();
163
-
164
- while (Date.now() - startTime < maxMs) {
165
- try {
166
- const snapshot = await getDomSnapshotByProfile(profileId, { maxDepth: 5, maxChildren: 50 });
167
- for (const selector of selectors) {
168
- const matched = buildSelectorCheck(snapshot, selector);
169
- if (matched.length > 0) {
170
- return { ok: true, found: true, selector, count: matched.length, elapsed: Date.now() - startTime };
171
- }
172
- }
173
- } catch (err) {
174
- // 忽略 snapshot 错误,继续轮询
175
- }
176
-
177
- // 等待下次检查
178
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
179
- }
180
-
181
- return { ok: false, found: false, elapsed: maxMs };
182
- }
183
-
184
- export { validateAnchors };
@@ -305,19 +305,16 @@ async function handleCommand(payload, manager, wsServer, options = {}) {
305
305
  switch (action) {
306
306
  case 'start': {
307
307
  const startViewport = resolveStartViewport(args);
308
- const opts = {
309
- profileId: args.profileId || 'default',
310
- sessionName: args.profileId || 'default',
311
- headless: !!args.headless,
312
- initialUrl: args.url,
313
- engine: args.engine || 'camoufox',
314
- fingerprintPlatform: args.fingerprintPlatform || null,
315
- ...(startViewport ? { viewport: startViewport } : {}),
316
- ...(args.ownerPid ? { ownerPid: args.ownerPid } : {}),
317
- };
318
- if (Number.isFinite(args.maxTabs) && args.maxTabs >= 1) {
319
- opts.maxTabs = Math.floor(args.maxTabs);
320
- }
308
+ const opts = {
309
+ profileId: args.profileId || 'default',
310
+ sessionName: args.profileId || 'default',
311
+ headless: !!args.headless,
312
+ initialUrl: args.url,
313
+ engine: args.engine || 'camoufox',
314
+ fingerprintPlatform: args.fingerprintPlatform || null,
315
+ ...(startViewport ? { viewport: startViewport } : {}),
316
+ ...(args.ownerPid ? { ownerPid: args.ownerPid } : {}),
317
+ };
321
318
  const res = await manager.createSession(opts);
322
319
  const session = manager.getSession(opts.profileId);
323
320
  if (!session) {
@@ -495,8 +492,8 @@ async function handleCommand(payload, manager, wsServer, options = {}) {
495
492
  const activeIndex = pages.find((p) => p.active)?.index ?? 0;
496
493
  return { ok: true, body: { ok: true, pages, activeIndex } };
497
494
  }
498
- case 'newTab':
499
495
  case 'page:new':
496
+ case 'newTab':
500
497
  case 'newPage': {
501
498
  const profileId = args.profileId || 'default';
502
499
  const session = manager.getSession(profileId);
@@ -37,7 +37,6 @@ export class BrowserSession {
37
37
  exitNotified = false;
38
38
  constructor(options) {
39
39
  this.options = options;
40
- this.maxTabs = Math.max(5, Math.floor(Number(options.maxTabs ?? 5) || 5));
41
40
  const profileId = options.profileId || 'default';
42
41
  const root = resolveProfilesRoot();
43
42
  this.profileDir = path.join(root, profileId);
@@ -70,7 +69,6 @@ export class BrowserSession {
70
69
  recordLastKnownUrl: (url) => { if (url)
71
70
  this.lastKnownUrl = url; },
72
71
  isHeadless: () => this.options.headless === true,
73
- getMaxTabs: () => this.maxTabs,
74
72
  });
75
73
  this.navigation = new BrowserSessionNavigation({
76
74
  ensurePrimaryPage: () => this.pageManager.ensurePrimaryPage(),
@@ -149,10 +147,7 @@ export class BrowserSession {
149
147
  const existing = this.context.pages();
150
148
  this.page = existing.length ? existing[0] : await this.context.newPage();
151
149
  this.setupPageHooks(this.page);
152
- this.context.on('page', (p) => {
153
- this.setupPageHooks(p);
154
- this.enforceMaxTabs();
155
- });
150
+ this.context.on('page', (p) => this.setupPageHooks(p));
156
151
  if (this.viewportManager.isFollowingWindow()) {
157
152
  await this.viewportManager.refreshFromWindow(this.page).catch(() => { });
158
153
  }
@@ -163,33 +158,6 @@ export class BrowserSession {
163
158
  setupPageHooks(page) {
164
159
  this.pageHooks.setupPageHooks(page);
165
160
  }
166
-
167
- async enforceMaxTabs() {
168
- try {
169
- const ctx = this.context;
170
- if (!ctx) return;
171
- const pages = ctx.pages().filter((p) => !p.isClosed());
172
- if (pages.length <= this.maxTabs) return;
173
- const activePage = this.getActivePage();
174
- const excess = pages.length - this.maxTabs;
175
- const closable = pages
176
- .filter((p) => p !== activePage)
177
- .sort((a, b) => {
178
- const aUrl = a.url() || '';
179
- const bUrl = b.url() || '';
180
- const aScore = (aUrl.includes('captcha') || aUrl === 'about:blank') ? 1 : 0;
181
- const bScore = (bUrl.includes('captcha') || bUrl === 'about:blank') ? 1 : 0;
182
- if (aScore !== bScore) return bScore - aScore;
183
- return 0;
184
- });
185
- const toClose = closable.slice(0, Math.min(excess, closable.length));
186
- for (const page of toClose) {
187
- try {
188
- await page.close({ runBeforeUnload: false });
189
- } catch { /* ignore */ }
190
- }
191
- } catch { /* ignore enforcement errors */ }
192
- }
193
161
  addRuntimeEventObserver(observer) {
194
162
  return this.runtimeEvents.addObserver(observer);
195
163
  }
@@ -333,4 +301,4 @@ export class BrowserSession {
333
301
  this.onExit?.(this.options.profileId);
334
302
  }
335
303
  }
336
- //# sourceMappingURL=BrowserSession.js.map
304
+ //# sourceMappingURL=BrowserSession.js.map
@@ -1,4 +1,39 @@
1
1
  import { isTimeoutLikeError } from './utils.js';
2
+ import { resolveInputMode } from './utils.js';
3
+
4
+ async function createCDPSession(page) {
5
+ const context = page.context();
6
+ return context.newCDPSession(page);
7
+ }
8
+
9
+ async function cdpMouseClick(cdp, x, y, button = 'left', delay = 50) {
10
+ const normalizedButton = button === 'left' ? 'left' : button === 'right' ? 'right' : button === 'middle' ? 'middle' : 'left';
11
+ await cdp.send('Input.dispatchMouseEvent', {
12
+ type: 'mousePressed',
13
+ x: Math.round(x),
14
+ y: Math.round(y),
15
+ button: normalizedButton,
16
+ clickCount: 1
17
+ });
18
+ if (delay > 0) {
19
+ await new Promise(r => setTimeout(r, delay));
20
+ }
21
+ await cdp.send('Input.dispatchMouseEvent', {
22
+ type: 'mouseReleased',
23
+ x: Math.round(x),
24
+ y: Math.round(y),
25
+ button: normalizedButton,
26
+ clickCount: 1
27
+ });
28
+ }
29
+
30
+ async function cdpMouseMove(cdp, x, y) {
31
+ await cdp.send('Input.dispatchMouseEvent', {
32
+ type: 'mouseMoved',
33
+ x: Math.round(x),
34
+ y: Math.round(y)
35
+ });
36
+ }
2
37
 
3
38
  async function readInteractiveViewport(page) {
4
39
  const fallback = page.viewportSize?.() || null;
@@ -39,9 +74,34 @@ export class BrowserSessionInputOps {
39
74
  this.withInputActionLock = withInputActionLock;
40
75
  const envMode = String(process.env.CAMO_SCROLL_INPUT_MODE || '').trim().toLowerCase();
41
76
  this.wheelMode = envMode === 'keyboard' ? 'keyboard' : 'wheel';
77
+ this.inputMode = resolveInputMode();
42
78
  }
43
79
  async mouseClick(opts) {
44
80
  const page = await this.ensurePrimaryPage();
81
+
82
+ if (this.inputMode === 'cdp') {
83
+ const { x, y, button = 'left', clicks = 1, delay = 50 } = opts;
84
+ let cdp = null;
85
+ try {
86
+ cdp = await createCDPSession(page);
87
+ for (let i = 0; i < clicks; i++) {
88
+ if (i > 0) {
89
+ await new Promise(r => setTimeout(r, 100 + Math.random() * 100));
90
+ }
91
+ await this.withInputActionLock(async () => {
92
+ await this.runInputAction(page, 'mouse:click(cdp)', async () => {
93
+ await cdpMouseClick(cdp, x, y, button, delay);
94
+ });
95
+ });
96
+ }
97
+ } finally {
98
+ if (cdp) {
99
+ await cdp.detach().catch(() => {});
100
+ }
101
+ }
102
+ return;
103
+ }
104
+
45
105
  await this.withInputActionLock(async () => {
46
106
  await this.runInputAction(page, 'input:ready', (activePage) => this.ensureInputReady(activePage));
47
107
  const { x, y, button = 'left', clicks = 1, delay = 50, nudgeBefore = false } = opts;
@@ -60,25 +120,28 @@ export class BrowserSessionInputOps {
60
120
  await clickPage.mouse.move(nudgeX, nudgeY, { steps: 3 }).catch(() => { });
61
121
  await clickPage.waitForTimeout(40).catch(() => { });
62
122
  };
123
+ const performClick = async (clickPage, label) => {
124
+ await this.runInputAction(page, label, async (activePage) => {
125
+ if (nudgeBefore)
126
+ await nudgePointer(activePage);
127
+ await moveToTarget(activePage);
128
+ await activePage.mouse.down({ button });
129
+ const pause = Math.max(0, Number(delay) || 0);
130
+ if (pause > 0)
131
+ await activePage.waitForTimeout(pause).catch(() => { });
132
+ await activePage.mouse.up({ button });
133
+ });
134
+ };
63
135
  for (let i = 0; i < clicks; i++) {
64
136
  if (i > 0)
65
137
  await new Promise(r => setTimeout(r, 100 + Math.random() * 100));
66
138
  try {
67
- await this.runInputAction(page, 'mouse:click(direct)', async (clickPage) => {
68
- if (nudgeBefore)
69
- await nudgePointer(clickPage);
70
- await moveToTarget(clickPage);
71
- await clickPage.mouse.click(x, y, { button, clickCount: 1, delay: Math.max(0, Number(delay) || 0) });
72
- });
139
+ await performClick(page, 'mouse:click(direct)');
73
140
  }
74
141
  catch (error) {
75
142
  if (!isTimeoutLikeError(error))
76
143
  throw error;
77
- await this.runInputAction(page, 'mouse:click(retry)', async (clickPage) => {
78
- await nudgePointer(clickPage);
79
- await moveToTarget(clickPage);
80
- await clickPage.mouse.click(x, y, { button, clickCount: 1, delay: Math.max(0, Number(delay) || 0) });
81
- });
144
+ await performClick(page, 'mouse:click(retry)');
82
145
  }
83
146
  }
84
147
  });
@@ -1,4 +1,4 @@
1
- import { resolveInputActionMaxAttempts, resolveInputActionTimeoutMs, resolveInputRecoveryBringToFrontTimeoutMs, resolveInputRecoveryDelayMs, resolveInputReadySettleMs, shouldSkipBringToFront } from './utils.js';
1
+ import { resolveInputActionMaxAttempts, resolveInputActionTimeoutMs, resolveInputMode, resolveInputRecoveryBringToFrontTimeoutMs, resolveInputRecoveryDelayMs, resolveInputReadySettleMs, shouldSkipBringToFront } from './utils.js';
2
2
  import { ensurePageRuntime } from '../pageRuntime.js';
3
3
  export class BrowserInputPipeline {
4
4
  ensurePrimaryPage;
@@ -9,6 +9,8 @@ export class BrowserInputPipeline {
9
9
  }
10
10
  inputActionTail = Promise.resolve();
11
11
  async ensureInputReady(page) {
12
+ if (resolveInputMode() === 'cdp')
13
+ return;
12
14
  if (this.isHeadless())
13
15
  return;
14
16
  if (shouldSkipBringToFront()) {
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { ensurePageRuntime } from '../pageRuntime.js';
3
- import { resolveNavigationWaitUntil, normalizeUrl, shouldSkipBringToFront } from './utils.js';
3
+ import { resolveNavigationWaitUntil, normalizeUrl } from './utils.js';
4
4
  export class BrowserSessionPageManagement {
5
5
  deps;
6
6
  trackedPages = [];
@@ -182,24 +182,14 @@ export class BrowserSessionPageManagement {
182
182
  active: active === p,
183
183
  }));
184
184
  }
185
- async newPage(url, options = {}) {
186
- const ctx = this.deps.ensureContext();
187
- const maxTabs = typeof this.deps.getMaxTabs === 'function' ? this.deps.getMaxTabs() : null;
188
- if (Number.isFinite(maxTabs) && maxTabs >= 1) {
189
- const pages = this.collectPages(ctx);
190
- if (pages.length >= maxTabs) {
191
- throw new Error(`max_tabs_exceeded:${maxTabs}`);
192
- }
193
- }
194
- const isMac = process.platform === 'darwin';
185
+ async newPage(url, options = {}) {
186
+ const ctx = this.deps.ensureContext();
187
+ const isMac = process.platform === 'darwin';
195
188
  const shortcut = isMac ? 'Meta+t' : 'Control+t';
196
189
  let page = null;
197
190
  const opener = this.deps.getActivePage() || ctx.pages()[0];
198
191
  if (!opener)
199
192
  throw new Error('no_opener_page');
200
- if (!shouldSkipBringToFront()) {
201
- await opener.bringToFront().catch(() => null);
202
- }
203
193
  const before = this.collectPages(ctx).length;
204
194
  if (!options?.strictShortcut) {
205
195
  page = await this.openPageViaContext(ctx, before);
@@ -252,14 +242,6 @@ export class BrowserSessionPageManagement {
252
242
  catch {
253
243
  /* ignore */
254
244
  }
255
- if (!shouldSkipBringToFront()) {
256
- try {
257
- await page.bringToFront();
258
- }
259
- catch {
260
- /* ignore */
261
- }
262
- }
263
245
  if (url) {
264
246
  await page.goto(url, { waitUntil: resolveNavigationWaitUntil() });
265
247
  await ensurePageRuntime(page);
@@ -283,14 +265,6 @@ export class BrowserSessionPageManagement {
283
265
  catch {
284
266
  /* ignore */
285
267
  }
286
- if (!shouldSkipBringToFront()) {
287
- try {
288
- await page.bringToFront();
289
- }
290
- catch {
291
- /* ignore */
292
- }
293
- }
294
268
  await ensurePageRuntime(page, true).catch(() => { });
295
269
  this.deps.recordLastKnownUrl(page.url());
296
270
  return { index: idx, url: page.url() };
@@ -316,14 +290,6 @@ export class BrowserSessionPageManagement {
316
290
  if (nextIndex >= 0) {
317
291
  const nextPage = remaining[nextIndex];
318
292
  this.deps.setActivePage(nextPage);
319
- if (!shouldSkipBringToFront()) {
320
- try {
321
- await nextPage.bringToFront();
322
- }
323
- catch {
324
- /* ignore */
325
- }
326
- }
327
293
  await ensurePageRuntime(nextPage, true).catch(() => { });
328
294
  this.deps.recordLastKnownUrl(nextPage.url());
329
295
  }
@@ -46,6 +46,12 @@ export function isTimeoutLikeError(error) {
46
46
  const message = String(error?.message || error || '').toLowerCase();
47
47
  return message.includes('timed out') || message.includes('timeout');
48
48
  }
49
+
50
+ export function resolveInputMode() {
51
+ const raw = String(process.env.CAMO_INPUT_MODE ?? '').trim().toLowerCase();
52
+ return raw === 'cdp' ? 'cdp' : 'playwright';
53
+ }
54
+
49
55
  export function normalizeUrl(raw) {
50
56
  try {
51
57
  const url = new URL(raw);
@@ -26,7 +26,7 @@ CONFIG:
26
26
 
27
27
  BROWSER CONTROL:
28
28
  init Ensure camoufox + ensure browser-service daemon
29
- start [profileId] [--url <url>] [--no-headless|--visible] [--devtools] [--record] [--record-name <name>] [--record-output <path>] [--record-overlay|--no-record-overlay] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h> --max-tabs <n>]
29
+ start [profileId] [--url <url>] [--no-headless|--visible] [--devtools] [--record] [--record-name <name>] [--record-output <path>] [--record-overlay|--no-record-overlay] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
30
30
  stop [profileId]
31
31
  stop --id <instanceId> Stop by instance id
32
32
  stop --alias <alias> Stop by alias