@web-auto/webauto 0.1.14 → 0.1.15

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.
@@ -1,9 +1,58 @@
1
1
  #!/usr/bin/env node
2
- import { execSync } from 'node:child_process';
2
+ import { execSync, spawnSync } from 'node:child_process';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
+ import { createRequire } from 'node:module';
7
+ import { fileURLToPath } from 'node:url';
6
8
  import { BROWSER_SERVICE_URL, loadConfig, setRepoRoot } from './config.mjs';
9
+ const requireFromHere = createRequire(import.meta.url);
10
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
11
+ function resolveNodeBin() {
12
+ const explicit = String(process.env.WEBAUTO_NODE_BIN || '').trim();
13
+ if (explicit)
14
+ return explicit;
15
+ const npmNode = String(process.env.npm_node_execpath || '').trim();
16
+ if (npmNode)
17
+ return npmNode;
18
+ return process.execPath;
19
+ }
20
+ function resolveCamoCliEntry() {
21
+ try {
22
+ const resolved = requireFromHere.resolve('@web-auto/camo/bin/camo.mjs');
23
+ if (resolved && fs.existsSync(resolved))
24
+ return resolved;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ return null;
30
+ }
31
+ function runCamoCli(args = [], options = {}) {
32
+ const entry = resolveCamoCliEntry();
33
+ if (!entry) {
34
+ return {
35
+ ok: false,
36
+ code: null,
37
+ stdout: '',
38
+ stderr: '@web-auto/camo/bin/camo.mjs not found',
39
+ entry: null,
40
+ };
41
+ }
42
+ const ret = spawnSync(resolveNodeBin(), [entry, ...args], {
43
+ encoding: 'utf8',
44
+ windowsHide: true,
45
+ stdio: options.stdio || 'pipe',
46
+ env: { ...process.env, ...(options.env || {}) },
47
+ });
48
+ return {
49
+ ok: ret.status === 0,
50
+ code: ret.status,
51
+ stdout: String(ret.stdout || ''),
52
+ stderr: String(ret.stderr || ''),
53
+ entry,
54
+ };
55
+ }
7
56
  export async function callAPI(action, payload = {}) {
8
57
  const r = await fetch(`${BROWSER_SERVICE_URL}/command`, {
9
58
  method: 'POST',
@@ -319,20 +368,13 @@ function scanCommonRepoRoots() {
319
368
  }
320
369
  export function findRepoRootCandidate() {
321
370
  const cfg = loadConfig();
371
+ const cwdRoot = walkUpForRepoRoot(process.cwd());
372
+ const moduleRoot = walkUpForRepoRoot(MODULE_DIR);
322
373
  const candidates = [
323
374
  process.env.WEBAUTO_REPO_ROOT,
324
- process.cwd(),
325
375
  cfg.repoRoot,
326
- path.join('/Volumes', 'extension', 'code', 'webauto'),
327
- path.join('/Volumes', 'extension', 'code', 'WebAuto'),
328
- path.join(os.homedir(), 'Documents', 'github', 'webauto'),
329
- path.join(os.homedir(), 'Documents', 'github', 'WebAuto'),
330
- path.join(os.homedir(), 'github', 'webauto'),
331
- path.join(os.homedir(), 'github', 'WebAuto'),
332
- path.join('C:', 'code', 'webauto'),
333
- path.join('C:', 'code', 'WebAuto'),
334
- path.join('C:', 'Users', os.userInfo().username, 'code', 'webauto'),
335
- path.join('C:', 'Users', os.userInfo().username, 'code', 'WebAuto'),
376
+ moduleRoot,
377
+ cwdRoot,
336
378
  ].filter(Boolean);
337
379
  for (const root of candidates) {
338
380
  if (!hasContainerLibrary(root))
@@ -343,20 +385,6 @@ export function findRepoRootCandidate() {
343
385
  }
344
386
  return resolved;
345
387
  }
346
- const walked = walkUpForRepoRoot(process.cwd());
347
- if (walked) {
348
- if (cfg.repoRoot !== walked) {
349
- setRepoRoot(walked);
350
- }
351
- return walked;
352
- }
353
- const scanned = scanCommonRepoRoots();
354
- if (scanned) {
355
- if (cfg.repoRoot !== scanned) {
356
- setRepoRoot(scanned);
357
- }
358
- return scanned;
359
- }
360
388
  return null;
361
389
  }
362
390
  export function detectCamoufoxPath() {
@@ -381,12 +409,7 @@ export function detectCamoufoxPath() {
381
409
  export function ensureCamoufox() {
382
410
  if (detectCamoufoxPath())
383
411
  return;
384
- console.log('Camoufox is not found. Installing...');
385
- execSync('npx --yes --package=camoufox camoufox fetch', { stdio: 'inherit' });
386
- if (!detectCamoufoxPath()) {
387
- throw new Error('Camoufox install finished but executable was not detected');
388
- }
389
- console.log('Camoufox installed.');
412
+ throw new Error('Camoufox is not installed. Run: webauto xhs install --download-browser');
390
413
  }
391
414
  export async function ensureBrowserService() {
392
415
  if (await checkBrowserService())
@@ -398,20 +421,17 @@ export async function ensureBrowserService() {
398
421
  }
399
422
  if (provider === 'camo') {
400
423
  const repoRoot = findRepoRootCandidate();
401
- if (repoRoot) {
402
- try {
403
- execSync(`npx --yes @web-auto/camo config repo-root ${JSON.stringify(repoRoot)}`, { stdio: 'ignore' });
404
- }
405
- catch {
406
- // best-effort only; init will still try using current config
407
- }
424
+ if (!repoRoot) {
425
+ throw new Error('WEBAUTO_REPO_ROOT is not set and no valid repo root was found');
408
426
  }
409
- try {
410
- console.log('Starting browser backend via camo init...');
411
- execSync('npx --yes @web-auto/camo init', { stdio: 'inherit' });
427
+ const configRet = runCamoCli(['config', 'repo-root', repoRoot], { stdio: 'pipe' });
428
+ if (!configRet.ok) {
429
+ throw new Error(`camo config repo-root failed: ${configRet.stderr.trim() || configRet.stdout.trim() || `exit ${configRet.code ?? 'null'}`}`);
412
430
  }
413
- catch (error) {
414
- throw new Error(`camo init failed: ${error?.message || String(error)}`);
431
+ console.log('Starting browser backend via camo init...');
432
+ const initRet = runCamoCli(['init'], { stdio: 'inherit' });
433
+ if (!initRet.ok) {
434
+ throw new Error(`camo init failed: ${initRet.stderr.trim() || initRet.stdout.trim() || `exit ${initRet.code ?? 'null'}`}`);
415
435
  }
416
436
  for (let i = 0; i < 20; i += 1) {
417
437
  await new Promise((r) => setTimeout(r, 400));
@@ -1,7 +1,11 @@
1
1
  export function buildCommentsHarvestScript(params = {}) {
2
2
  const maxRounds = Math.max(1, Number(params.maxRounds ?? params.maxScrollRounds ?? 14) || 14);
3
- const scrollStep = Math.max(120, Number(params.scrollStep ?? 420) || 420);
4
- const settleMs = Math.max(80, Number(params.settleMs ?? 180) || 180);
3
+ const scrollStepMin = Math.max(120, Number(params.scrollStepMin ?? params.scrollStep ?? 420) || 420);
4
+ const scrollStepMax = Math.max(scrollStepMin, Number(params.scrollStepMax ?? scrollStepMin) || scrollStepMin);
5
+ const scrollStepBase = Math.max(scrollStepMin, Math.floor((scrollStepMin + scrollStepMax) / 2));
6
+ const settleMinMs = Math.max(80, Number(params.settleMinMs ?? params.settleMs ?? 180) || 180);
7
+ const settleMaxMs = Math.max(settleMinMs, Number(params.settleMaxMs ?? settleMinMs) || settleMinMs);
8
+ const settleMs = settleMinMs;
5
9
  const stallRounds = Math.max(1, Number(params.stallRounds ?? 2) || 2);
6
10
  const requireBottom = params.requireBottom !== false;
7
11
  const includeComments = params.includeComments !== false;
@@ -10,12 +14,12 @@ export function buildCommentsHarvestScript(params = {}) {
10
14
  const recoveryUpRounds = Math.max(1, Number(params.recoveryUpRounds ?? 2) || 2);
11
15
  const recoveryDownRounds = Math.max(1, Number(params.recoveryDownRounds ?? 3) || 3);
12
16
  const maxRecoveries = Math.max(0, Number(params.maxRecoveries ?? 3) || 3);
13
- const recoveryUpStep = Math.max(80, Number(params.recoveryUpStep ?? Math.floor(scrollStep * 0.75)) || Math.floor(scrollStep * 0.75));
14
- const recoveryDownStep = Math.max(120, Number(params.recoveryDownStep ?? Math.floor(scrollStep * 1.3)) || Math.floor(scrollStep * 1.3));
17
+ const recoveryUpStep = Math.max(80, Number(params.recoveryUpStep ?? Math.floor(scrollStepBase * 0.75)) || Math.floor(scrollStepBase * 0.75));
18
+ const recoveryDownStep = Math.max(120, Number(params.recoveryDownStep ?? Math.floor(scrollStepBase * 1.3)) || Math.floor(scrollStepBase * 1.3));
15
19
  const recoveryNoProgressRounds = Math.max(1, Number(params.recoveryNoProgressRounds ?? 3) || 3);
16
20
  const progressDiffThreshold = Math.max(2, Number(
17
- params.progressDiffThreshold ?? Math.max(12, Math.floor(scrollStep * 0.08)),
18
- ) || Math.max(12, Math.floor(scrollStep * 0.08)));
21
+ params.progressDiffThreshold ?? Math.max(12, Math.floor(scrollStepBase * 0.08)),
22
+ ) || Math.max(12, Math.floor(scrollStepBase * 0.08)));
19
23
  const recoveryDownBoostPerAttempt = Math.max(0, Number(params.recoveryDownBoostPerAttempt ?? 1) || 1);
20
24
  const maxRecoveryDownBoost = Math.max(0, Number(params.maxRecoveryDownBoost ?? 2) || 2);
21
25
  const adaptiveMaxRounds = params.adaptiveMaxRounds !== false;
@@ -165,7 +169,10 @@ export function buildCommentsHarvestScript(params = {}) {
165
169
  };
166
170
 
167
171
  const configuredMaxRounds = Number(${maxRounds});
168
- const scrollStep = Number(${scrollStep});
172
+ const scrollStepMin = Number(${scrollStepMin});
173
+ const scrollStepMax = Number(${scrollStepMax});
174
+ const settleMinMs = Number(${settleMinMs});
175
+ const settleMaxMs = Number(${settleMaxMs});
169
176
  const settleMs = Number(${settleMs});
170
177
  const stallRounds = Number(${stallRounds});
171
178
  const requireBottom = ${requireBottom ? 'true' : 'false'};
@@ -186,6 +193,12 @@ export function buildCommentsHarvestScript(params = {}) {
186
193
  const adaptiveBufferRounds = Number(${adaptiveBufferRounds});
187
194
  const adaptiveMinBoostRounds = Number(${adaptiveMinBoostRounds});
188
195
  const adaptiveMaxRoundsCap = Number(${adaptiveMaxRoundsCap});
196
+ const randomBetween = (min, max) => {
197
+ const lo = Math.max(0, Math.floor(Number(min) || 0));
198
+ const hi = Math.max(lo, Math.floor(Number(max) || 0));
199
+ if (hi <= lo) return lo;
200
+ return lo + Math.floor(Math.random() * (hi - lo + 1));
201
+ };
189
202
  let maxRounds = configuredMaxRounds;
190
203
  let maxRoundsSource = 'configured';
191
204
  let budgetExpectedCommentsCount = null;
@@ -216,10 +229,14 @@ export function buildCommentsHarvestScript(params = {}) {
216
229
  no_new_comments: 0,
217
230
  };
218
231
  const performScroll = async (deltaY, waitMs = settleMs, meta = {}) => {
232
+ const waitFloor = Math.max(settleMinMs, Math.floor(Number(waitMs) || settleMinMs));
233
+ const waitCeil = Math.max(waitFloor, Math.max(settleMaxMs, Math.floor(Number(waitMs) || settleMaxMs)));
234
+ const waitActual = randomBetween(waitFloor, waitCeil);
219
235
  pushTrace({
220
236
  kind: 'scroll',
221
237
  stage: 'xhs_comments_harvest',
222
238
  deltaY: Number(deltaY),
239
+ waitMs: waitActual,
223
240
  ...meta,
224
241
  });
225
242
  if (typeof scroller?.scrollBy === 'function') {
@@ -227,7 +244,7 @@ export function buildCommentsHarvestScript(params = {}) {
227
244
  } else {
228
245
  window.scrollBy({ top: deltaY, behavior: 'auto' });
229
246
  }
230
- await new Promise((resolve) => setTimeout(resolve, waitMs));
247
+ await new Promise((resolve) => setTimeout(resolve, waitActual));
231
248
  };
232
249
 
233
250
  for (let round = 1; round <= maxRounds; round += 1) {
@@ -253,7 +270,8 @@ export function buildCommentsHarvestScript(params = {}) {
253
270
  }
254
271
 
255
272
  const prevTop = beforeMetrics.scrollTop;
256
- await performScroll(scrollStep, settleMs, {
273
+ const roundScrollStep = randomBetween(scrollStepMin, scrollStepMax);
274
+ await performScroll(roundScrollStep, settleMs, {
257
275
  round,
258
276
  reason: 'main_scroll',
259
277
  });
@@ -261,16 +279,18 @@ export function buildCommentsHarvestScript(params = {}) {
261
279
  let afterMetrics = readMetrics();
262
280
  let moved = Math.abs(afterMetrics.scrollTop - prevTop) > 1;
263
281
  if (!moved && typeof window.scrollBy === 'function') {
264
- const fallbackStep = Math.max(120, Math.floor(scrollStep / 2));
282
+ const fallbackStep = Math.max(120, Math.floor(roundScrollStep / 2));
283
+ const fallbackWaitMs = randomBetween(settleMinMs, settleMaxMs);
265
284
  pushTrace({
266
285
  kind: 'scroll',
267
286
  stage: 'xhs_comments_harvest',
268
287
  round,
269
288
  reason: 'fallback_scroll',
270
289
  deltaY: Number(fallbackStep),
290
+ waitMs: fallbackWaitMs,
271
291
  });
272
292
  window.scrollBy({ top: fallbackStep, behavior: 'auto' });
273
- await new Promise((resolve) => setTimeout(resolve, settleMs));
293
+ await new Promise((resolve) => setTimeout(resolve, fallbackWaitMs));
274
294
  collect(round);
275
295
  afterMetrics = readMetrics();
276
296
  moved = Math.abs(afterMetrics.scrollTop - prevTop) > 1;
@@ -1,5 +1,10 @@
1
1
  export function buildSubmitSearchScript(params = {}) {
2
2
  const keyword = String(params.keyword || '').trim();
3
+ const method = String(params.method || params.submitMethod || 'click').trim().toLowerCase();
4
+ const actionDelayMinMs = Math.max(20, Number(params.actionDelayMinMs ?? 180) || 180);
5
+ const actionDelayMaxMs = Math.max(actionDelayMinMs, Number(params.actionDelayMaxMs ?? 620) || 620);
6
+ const settleMinMs = Math.max(60, Number(params.settleMinMs ?? 1200) || 1200);
7
+ const settleMaxMs = Math.max(settleMinMs, Number(params.settleMaxMs ?? 2600) || 2600);
3
8
  return `(async () => {
4
9
  const state = window.__camoXhsState || (window.__camoXhsState = {});
5
10
  const metrics = state.metrics && typeof state.metrics === 'object' ? state.metrics : {};
@@ -17,6 +22,18 @@ export function buildSubmitSearchScript(params = {}) {
17
22
  if (!(input instanceof HTMLInputElement)) {
18
23
  throw new Error('SEARCH_INPUT_NOT_FOUND');
19
24
  }
25
+ const randomBetween = (min, max) => {
26
+ const lo = Math.max(0, Math.floor(Number(min) || 0));
27
+ const hi = Math.max(lo, Math.floor(Number(max) || 0));
28
+ if (hi <= lo) return lo;
29
+ return lo + Math.floor(Math.random() * (hi - lo + 1));
30
+ };
31
+ const waitRandom = async (min, max, stage) => {
32
+ const waitMs = randomBetween(min, max);
33
+ pushTrace({ kind: 'wait', stage, waitMs });
34
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
35
+ return waitMs;
36
+ };
20
37
  const targetKeyword = ${JSON.stringify(keyword)};
21
38
  if (targetKeyword && input.value !== targetKeyword) {
22
39
  input.focus();
@@ -24,38 +41,63 @@ export function buildSubmitSearchScript(params = {}) {
24
41
  input.dispatchEvent(new Event('input', { bubbles: true }));
25
42
  input.dispatchEvent(new Event('change', { bubbles: true }));
26
43
  }
27
- const enterEvent = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };
44
+ const requestedMethod = ${JSON.stringify(method)};
45
+ const normalizedMethod = ['click', 'enter', 'form'].includes(requestedMethod) ? requestedMethod : 'click';
28
46
  const beforeUrl = window.location.href;
29
47
  input.focus();
30
- input.dispatchEvent(new KeyboardEvent('keydown', enterEvent));
31
- input.dispatchEvent(new KeyboardEvent('keypress', enterEvent));
32
- input.dispatchEvent(new KeyboardEvent('keyup', enterEvent));
33
48
  const candidates = ['.input-button .search-icon', '.input-button', 'button.min-width-search-icon'];
49
+ let methodUsed = normalizedMethod;
34
50
  let clickedSelector = null;
35
- for (const selector of candidates) {
36
- const button = document.querySelector(selector);
37
- if (!button) continue;
38
- if (button instanceof HTMLElement) {
39
- pushTrace({ kind: 'scroll', stage: 'submit_search', selector, via: 'scrollIntoView' });
40
- button.scrollIntoView({ behavior: 'auto', block: 'center' });
51
+ const form = input.closest('form');
52
+ if (normalizedMethod === 'click') {
53
+ let clicked = false;
54
+ for (const selector of candidates) {
55
+ const button = document.querySelector(selector);
56
+ if (!button) continue;
57
+ if (button instanceof HTMLElement) {
58
+ pushTrace({ kind: 'scroll', stage: 'submit_search', selector, via: 'scrollIntoView' });
59
+ button.scrollIntoView({ behavior: 'auto', block: 'center' });
60
+ }
61
+ await waitRandom(${actionDelayMinMs}, ${actionDelayMaxMs}, 'submit_pre_click');
62
+ pushTrace({ kind: 'click', stage: 'submit_search', selector });
63
+ button.click();
64
+ clickedSelector = selector;
65
+ clicked = true;
66
+ break;
67
+ }
68
+ if (!clicked) {
69
+ methodUsed = 'form';
41
70
  }
42
- await new Promise((resolve) => setTimeout(resolve, 80));
43
- pushTrace({ kind: 'click', stage: 'submit_search', selector });
44
- button.click();
45
- clickedSelector = selector;
46
- break;
47
71
  }
48
- const form = input.closest('form');
49
- if (form) {
50
- if (typeof form.requestSubmit === 'function') form.requestSubmit();
51
- else form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
72
+ if (methodUsed === 'enter') {
73
+ await waitRandom(${actionDelayMinMs}, ${actionDelayMaxMs}, 'submit_pre_enter');
74
+ const enterEvent = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };
75
+ pushTrace({ kind: 'key', stage: 'submit_search', key: 'Enter' });
76
+ input.dispatchEvent(new KeyboardEvent('keydown', enterEvent));
77
+ input.dispatchEvent(new KeyboardEvent('keypress', enterEvent));
78
+ input.dispatchEvent(new KeyboardEvent('keyup', enterEvent));
79
+ } else if (methodUsed === 'form') {
80
+ await waitRandom(${actionDelayMinMs}, ${actionDelayMaxMs}, 'submit_pre_form');
81
+ if (form) {
82
+ pushTrace({ kind: 'submit', stage: 'submit_search', via: 'form' });
83
+ if (typeof form.requestSubmit === 'function') form.requestSubmit();
84
+ else form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
85
+ } else {
86
+ methodUsed = 'enter';
87
+ const enterEvent = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };
88
+ pushTrace({ kind: 'key', stage: 'submit_search', key: 'Enter', fallback: true });
89
+ input.dispatchEvent(new KeyboardEvent('keydown', enterEvent));
90
+ input.dispatchEvent(new KeyboardEvent('keypress', enterEvent));
91
+ input.dispatchEvent(new KeyboardEvent('keyup', enterEvent));
92
+ }
52
93
  }
53
- await new Promise((resolve) => setTimeout(resolve, 320));
94
+ await waitRandom(${settleMinMs}, ${settleMaxMs}, 'submit_settle');
54
95
  return {
55
96
  submitted: true,
56
- via: clickedSelector || 'enter_or_form_submit',
97
+ via: clickedSelector || methodUsed,
57
98
  beforeUrl,
58
99
  afterUrl: window.location.href,
100
+ method: methodUsed,
59
101
  searchCount: metrics.searchCount,
60
102
  actionTrace,
61
103
  };
@@ -79,9 +121,21 @@ export function buildOpenDetailScript(params = {}) {
79
121
  const nextSeekRounds = Math.max(0, Number(params.nextSeekRounds || 8) || 8);
80
122
  const nextSeekStep = Math.max(0, Number(params.nextSeekStep || 0) || 0);
81
123
  const nextSeekSettleMs = Math.max(120, Number(params.nextSeekSettleMs || 320) || 320);
124
+ const preClickDelayMinMs = Math.max(60, Number(params.preClickDelayMinMs ?? 220) || 220);
125
+ const preClickDelayMaxMs = Math.max(preClickDelayMinMs, Number(params.preClickDelayMaxMs ?? 700) || 700);
126
+ const pollDelayMinMs = Math.max(80, Number(params.pollDelayMinMs ?? 130) || 130);
127
+ const pollDelayMaxMs = Math.max(pollDelayMinMs, Number(params.pollDelayMaxMs ?? 320) || 320);
128
+ const postOpenDelayMinMs = Math.max(120, Number(params.postOpenDelayMinMs ?? 420) || 420);
129
+ const postOpenDelayMaxMs = Math.max(postOpenDelayMinMs, Number(params.postOpenDelayMaxMs ?? 1100) || 1100);
82
130
 
83
131
  return `(async () => {
84
132
  const STATE_KEY = '__camoXhsState';
133
+ const randomBetween = (min, max) => {
134
+ const lo = Math.max(0, Math.floor(Number(min) || 0));
135
+ const hi = Math.max(lo, Math.floor(Number(max) || 0));
136
+ if (hi <= lo) return lo;
137
+ return lo + Math.floor(Math.random() * (hi - lo + 1));
138
+ };
85
139
  const normalizeVisited = (value) => {
86
140
  if (!Array.isArray(value)) return [];
87
141
  return value
@@ -282,7 +336,9 @@ export function buildOpenDetailScript(params = {}) {
282
336
  via: 'scrollIntoView',
283
337
  });
284
338
  next.cover.scrollIntoView({ behavior: 'auto', block: 'center' });
285
- await new Promise((resolve) => setTimeout(resolve, 140));
339
+ const preClickDelay = randomBetween(${preClickDelayMinMs}, ${preClickDelayMaxMs});
340
+ pushTrace({ kind: 'wait', stage: 'open_detail_pre_click', noteId: next.noteId, waitMs: preClickDelay });
341
+ await new Promise((resolve) => setTimeout(resolve, preClickDelay));
286
342
  const beforeUrl = window.location.href;
287
343
  pushTrace({
288
344
  kind: 'click',
@@ -298,12 +354,15 @@ export function buildOpenDetailScript(params = {}) {
298
354
  detailReady = true;
299
355
  break;
300
356
  }
301
- await new Promise((resolve) => setTimeout(resolve, 120));
357
+ const pollDelay = randomBetween(${pollDelayMinMs}, ${pollDelayMaxMs});
358
+ await new Promise((resolve) => setTimeout(resolve, pollDelay));
302
359
  }
303
360
  if (!detailReady) {
304
361
  throw new Error('DETAIL_OPEN_TIMEOUT');
305
362
  }
306
- await new Promise((resolve) => setTimeout(resolve, 220));
363
+ const postOpenDelay = randomBetween(${postOpenDelayMinMs}, ${postOpenDelayMaxMs});
364
+ pushTrace({ kind: 'wait', stage: 'open_detail_post_open', noteId: next.noteId, waitMs: postOpenDelay });
365
+ await new Promise((resolve) => setTimeout(resolve, postOpenDelay));
307
366
  const afterUrl = window.location.href;
308
367
 
309
368
  if (!state.visitedNoteIds.includes(next.noteId)) state.visitedNoteIds.push(next.noteId);
@@ -472,8 +472,28 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
472
472
  const env = toTrimmedString(rawOptions.env, 'prod');
473
473
  const outputRoot = toTrimmedString(rawOptions.outputRoot, '');
474
474
  const throttle = toPositiveInt(rawOptions.throttle, 900, 100);
475
- const tabCount = toPositiveInt(rawOptions.tabCount, 4, 1);
475
+ const tabCount = toPositiveInt(rawOptions.tabCount, 1, 1);
476
+ const tabOpenDelayMs = toNonNegativeInt(rawOptions.tabOpenDelayMs, 1400);
476
477
  const noteIntervalMs = toPositiveInt(rawOptions.noteIntervalMs, 1200, 200);
478
+ const submitMethod = toTrimmedString(rawOptions.submitMethod, 'click').toLowerCase();
479
+ const submitActionDelayMinMs = toPositiveInt(rawOptions.submitActionDelayMinMs, 180, 20);
480
+ const submitActionDelayMaxMs = toPositiveInt(rawOptions.submitActionDelayMaxMs, 620, submitActionDelayMinMs);
481
+ const submitSettleMinMs = toPositiveInt(rawOptions.submitSettleMinMs, 1200, 60);
482
+ const submitSettleMaxMs = toPositiveInt(rawOptions.submitSettleMaxMs, 2600, submitSettleMinMs);
483
+ const openDetailPreClickMinMs = toPositiveInt(rawOptions.openDetailPreClickMinMs, 220, 60);
484
+ const openDetailPreClickMaxMs = toPositiveInt(rawOptions.openDetailPreClickMaxMs, 700, openDetailPreClickMinMs);
485
+ const openDetailPollDelayMinMs = toPositiveInt(rawOptions.openDetailPollDelayMinMs, 130, 80);
486
+ const openDetailPollDelayMaxMs = toPositiveInt(rawOptions.openDetailPollDelayMaxMs, 320, openDetailPollDelayMinMs);
487
+ const openDetailPostOpenMinMs = toPositiveInt(rawOptions.openDetailPostOpenMinMs, 420, 120);
488
+ const openDetailPostOpenMaxMs = toPositiveInt(rawOptions.openDetailPostOpenMaxMs, 1100, openDetailPostOpenMinMs);
489
+ const commentsScrollStepMin = toPositiveInt(rawOptions.commentsScrollStepMin, 280, 120);
490
+ const commentsScrollStepMax = toPositiveInt(rawOptions.commentsScrollStepMax, 420, commentsScrollStepMin);
491
+ const commentsSettleMinMs = toPositiveInt(rawOptions.commentsSettleMinMs, 280, 80);
492
+ const commentsSettleMaxMs = toPositiveInt(rawOptions.commentsSettleMaxMs, 820, commentsSettleMinMs);
493
+ const defaultOperationMinIntervalMs = toNonNegativeInt(rawOptions.defaultOperationMinIntervalMs, 1200);
494
+ const defaultEventCooldownMs = toNonNegativeInt(rawOptions.defaultEventCooldownMs, 700);
495
+ const defaultPacingJitterMs = toNonNegativeInt(rawOptions.defaultPacingJitterMs, 900);
496
+ const navigationMinIntervalMs = toNonNegativeInt(rawOptions.navigationMinIntervalMs, 2200);
477
497
  const maxNotes = toPositiveInt(rawOptions.maxNotes, 30, 1);
478
498
  const maxComments = toNonNegativeInt(rawOptions.maxComments, 0);
479
499
  const resume = toBoolean(rawOptions.resume, false);
@@ -529,10 +549,10 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
529
549
  validationMode: 'none',
530
550
  recovery,
531
551
  pacing: {
532
- operationMinIntervalMs: 700,
533
- eventCooldownMs: 300,
534
- jitterMs: 220,
535
- navigationMinIntervalMs: 1800,
552
+ operationMinIntervalMs: defaultOperationMinIntervalMs,
553
+ eventCooldownMs: defaultEventCooldownMs,
554
+ jitterMs: defaultPacingJitterMs,
555
+ navigationMinIntervalMs,
536
556
  timeoutMs: 0,
537
557
  },
538
558
  timeoutMs: 0,
@@ -542,7 +562,27 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
542
562
  env,
543
563
  outputRoot,
544
564
  tabCount,
565
+ tabOpenDelayMs,
545
566
  noteIntervalMs,
567
+ submitMethod,
568
+ submitActionDelayMinMs,
569
+ submitActionDelayMaxMs,
570
+ submitSettleMinMs,
571
+ submitSettleMaxMs,
572
+ openDetailPreClickMinMs,
573
+ openDetailPreClickMaxMs,
574
+ openDetailPollDelayMinMs,
575
+ openDetailPollDelayMaxMs,
576
+ openDetailPostOpenMinMs,
577
+ openDetailPostOpenMaxMs,
578
+ commentsScrollStepMin,
579
+ commentsScrollStepMax,
580
+ commentsSettleMinMs,
581
+ commentsSettleMaxMs,
582
+ defaultOperationMinIntervalMs,
583
+ defaultEventCooldownMs,
584
+ defaultPacingJitterMs,
585
+ navigationMinIntervalMs,
546
586
  maxNotes,
547
587
  maxComments,
548
588
  maxLikesPerRound,
@@ -645,6 +685,11 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
645
685
  keyword,
646
686
  searchSerialKey,
647
687
  sharedHarvestPath,
688
+ method: submitMethod,
689
+ actionDelayMinMs: submitActionDelayMinMs,
690
+ actionDelayMaxMs: submitActionDelayMaxMs,
691
+ settleMinMs: submitSettleMinMs,
692
+ settleMaxMs: submitSettleMaxMs,
648
693
  },
649
694
  trigger: 'home_search_input.exist',
650
695
  dependsOn: ['fill_keyword'],
@@ -672,6 +717,12 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
672
717
  sharedHarvestPath,
673
718
  seedCollectCount,
674
719
  seedCollectMaxRounds,
720
+ preClickDelayMinMs: openDetailPreClickMinMs,
721
+ preClickDelayMaxMs: openDetailPreClickMaxMs,
722
+ pollDelayMinMs: openDetailPollDelayMinMs,
723
+ pollDelayMaxMs: openDetailPollDelayMaxMs,
724
+ postOpenDelayMinMs: openDetailPostOpenMinMs,
725
+ postOpenDelayMaxMs: openDetailPostOpenMaxMs,
675
726
  },
676
727
  trigger: 'search_result_item.exist',
677
728
  dependsOn: ['submit_search'],
@@ -732,8 +783,12 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
732
783
  persistComments,
733
784
  commentsLimit: maxComments,
734
785
  maxRounds: 48,
735
- scrollStep: 360,
736
- settleMs: 260,
786
+ scrollStep: commentsScrollStepMin,
787
+ scrollStepMin: commentsScrollStepMin,
788
+ scrollStepMax: commentsScrollStepMax,
789
+ settleMs: commentsSettleMinMs,
790
+ settleMinMs: commentsSettleMinMs,
791
+ settleMaxMs: commentsSettleMaxMs,
737
792
  stallRounds: 8,
738
793
  recoveryNoProgressRounds: 3,
739
794
  recoveryStuckRounds: 2,
@@ -874,6 +929,12 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
874
929
  resume,
875
930
  incrementalMax,
876
931
  sharedHarvestPath,
932
+ preClickDelayMinMs: openDetailPreClickMinMs,
933
+ preClickDelayMaxMs: openDetailPreClickMaxMs,
934
+ pollDelayMinMs: openDetailPollDelayMinMs,
935
+ pollDelayMaxMs: openDetailPollDelayMaxMs,
936
+ postOpenDelayMinMs: openDetailPostOpenMinMs,
937
+ postOpenDelayMaxMs: openDetailPostOpenMaxMs,
877
938
  },
878
939
  trigger: 'search_result_item.exist',
879
940
  dependsOn: ['switch_tab_round_robin'],
@@ -924,7 +985,7 @@ export function buildXhsUnifiedAutoscript(rawOptions = {}) {
924
985
  action: 'ensure_tab_pool',
925
986
  params: {
926
987
  tabCount,
927
- openDelayMs: 1200,
988
+ openDelayMs: tabOpenDelayMs,
928
989
  normalizeTabs: false,
929
990
  },
930
991
  trigger: 'search_result_item.exist',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-auto/webauto",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "webauto": "bin/webauto.mjs"