@web-auto/webauto 0.1.14 → 0.1.16
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 +137 -0
- package/apps/desktop-console/dist/renderer/index.js +63 -17
- package/apps/webauto/entry/flow-gate.mjs +139 -0
- package/apps/webauto/entry/lib/flow-gate.mjs +466 -0
- package/apps/webauto/entry/xhs-unified.mjs +109 -5
- package/bin/webauto.mjs +15 -2
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +64 -44
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +31 -11
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +83 -24
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +69 -8
- package/package.json +2 -1
|
@@ -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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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
|
|
4
|
-
const
|
|
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(
|
|
14
|
-
const recoveryDownStep = Math.max(120, Number(params.recoveryDownStep ?? Math.floor(
|
|
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(
|
|
18
|
-
) || Math.max(12, Math.floor(
|
|
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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
button
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
94
|
+
await waitRandom(${settleMinMs}, ${settleMaxMs}, 'submit_settle');
|
|
54
95
|
return {
|
|
55
96
|
submitted: true,
|
|
56
|
-
via: clickedSelector ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
533
|
-
eventCooldownMs:
|
|
534
|
-
jitterMs:
|
|
535
|
-
navigationMinIntervalMs
|
|
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:
|
|
736
|
-
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"webauto": "bin/webauto.mjs"
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"postinstall": "node scripts/postinstall-resources.mjs",
|
|
53
|
+
"prepack": "npm run build",
|
|
53
54
|
"start:mcp:browser": "npx @browsermcp/mcp@latest",
|
|
54
55
|
"build": "npm run prebuild && npm run build:services && npm run self-check:post-build && npm run ui:test && npm --prefix apps/desktop-console run build",
|
|
55
56
|
"build:services": "tsc -p tsconfig.services.json",
|