@web-auto/camo 0.1.26 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-auto/camo",
3
- "version": "0.1.26",
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
@@ -120,25 +120,28 @@ export class BrowserSessionInputOps {
120
120
  await clickPage.mouse.move(nudgeX, nudgeY, { steps: 3 }).catch(() => { });
121
121
  await clickPage.waitForTimeout(40).catch(() => { });
122
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
+ };
123
135
  for (let i = 0; i < clicks; i++) {
124
136
  if (i > 0)
125
137
  await new Promise(r => setTimeout(r, 100 + Math.random() * 100));
126
138
  try {
127
- await this.runInputAction(page, 'mouse:click(direct)', async (clickPage) => {
128
- if (nudgeBefore)
129
- await nudgePointer(clickPage);
130
- await moveToTarget(clickPage);
131
- await clickPage.mouse.click(x, y, { button, clickCount: 1, delay: Math.max(0, Number(delay) || 0) });
132
- });
139
+ await performClick(page, 'mouse:click(direct)');
133
140
  }
134
141
  catch (error) {
135
142
  if (!isTimeoutLikeError(error))
136
143
  throw error;
137
- await this.runInputAction(page, 'mouse:click(retry)', async (clickPage) => {
138
- await nudgePointer(clickPage);
139
- await moveToTarget(clickPage);
140
- await clickPage.mouse.click(x, y, { button, clickCount: 1, delay: Math.max(0, Number(delay) || 0) });
141
- });
144
+ await performClick(page, 'mouse:click(retry)');
142
145
  }
143
146
  }
144
147
  });
@@ -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
  }
@@ -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