@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.
@@ -0,0 +1,466 @@
1
+ import { existsSync } from 'node:fs';
2
+ import fsp from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ function nowIso() {
7
+ return new Date().toISOString();
8
+ }
9
+
10
+ function toInt(value, fallback, min = 0) {
11
+ if (value === undefined || value === null || value === '') return fallback;
12
+ const num = Number(value);
13
+ if (!Number.isFinite(num)) return fallback;
14
+ return Math.max(min, Math.floor(num));
15
+ }
16
+
17
+ function normalizePathForPlatform(raw, platform = process.platform) {
18
+ const input = String(raw || '').trim();
19
+ const isWinPath = platform === 'win32' || /^[A-Za-z]:[\\/]/.test(input);
20
+ const pathApi = isWinPath ? path.win32 : path;
21
+ return isWinPath ? pathApi.normalize(input) : path.resolve(input);
22
+ }
23
+
24
+ function normalizeLegacyWebautoRoot(raw, platform = process.platform) {
25
+ const pathApi = platform === 'win32' ? path.win32 : path;
26
+ const resolved = normalizePathForPlatform(raw, platform);
27
+ const base = pathApi.basename(resolved).toLowerCase();
28
+ if (base === '.webauto' || base === 'webauto') return resolved;
29
+ return pathApi.join(resolved, '.webauto');
30
+ }
31
+
32
+ export function resolveWebautoHome(options = {}) {
33
+ const env = options.env || process.env;
34
+ const platform = String(options.platform || process.platform);
35
+ const homeDir = String(options.homeDir || os.homedir());
36
+ const pathApi = platform === 'win32' ? path.win32 : path;
37
+ const explicitHome = String(env.WEBAUTO_HOME || '').trim();
38
+ if (explicitHome) return normalizePathForPlatform(explicitHome, platform);
39
+ const legacyRoot = String(env.WEBAUTO_ROOT || env.WEBAUTO_PORTABLE_ROOT || '').trim();
40
+ if (legacyRoot) return normalizeLegacyWebautoRoot(legacyRoot, platform);
41
+ const hasDDrive = typeof options.hasDDrive === 'boolean'
42
+ ? options.hasDDrive
43
+ : (platform === 'win32' && existsSync('D:\\'));
44
+ if (platform === 'win32') return hasDDrive ? 'D:\\webauto' : pathApi.join(homeDir, '.webauto');
45
+ return pathApi.join(homeDir, '.webauto');
46
+ }
47
+
48
+ const DEFAULT_PLATFORM_GATES = Object.freeze({
49
+ xiaohongshu: {
50
+ throttle: { minMs: 900, maxMs: 1800 },
51
+ noteInterval: { minMs: 2200, maxMs: 4200 },
52
+ tabPool: { tabCount: 1, openDelayMinMs: 1400, openDelayMaxMs: 2800 },
53
+ submitSearch: {
54
+ method: 'click',
55
+ actionDelayMinMs: 180,
56
+ actionDelayMaxMs: 620,
57
+ settleMinMs: 1200,
58
+ settleMaxMs: 2600,
59
+ },
60
+ openDetail: {
61
+ preClickMinMs: 220,
62
+ preClickMaxMs: 700,
63
+ pollDelayMinMs: 130,
64
+ pollDelayMaxMs: 320,
65
+ postOpenMinMs: 420,
66
+ postOpenMaxMs: 1100,
67
+ },
68
+ commentsHarvest: {
69
+ scrollStepMin: 280,
70
+ scrollStepMax: 420,
71
+ settleMinMs: 280,
72
+ settleMaxMs: 820,
73
+ },
74
+ pacing: {
75
+ defaultOperationMinIntervalMs: 1200,
76
+ defaultEventCooldownMs: 700,
77
+ defaultJitterMs: 900,
78
+ navigationMinIntervalMs: 2200,
79
+ },
80
+ },
81
+ weibo: {
82
+ throttle: { minMs: 800, maxMs: 1600 },
83
+ noteInterval: { minMs: 1800, maxMs: 3600 },
84
+ tabPool: { tabCount: 1, openDelayMinMs: 1200, openDelayMaxMs: 2400 },
85
+ submitSearch: {
86
+ method: 'click',
87
+ actionDelayMinMs: 160,
88
+ actionDelayMaxMs: 560,
89
+ settleMinMs: 900,
90
+ settleMaxMs: 2200,
91
+ },
92
+ openDetail: {
93
+ preClickMinMs: 180,
94
+ preClickMaxMs: 640,
95
+ pollDelayMinMs: 120,
96
+ pollDelayMaxMs: 300,
97
+ postOpenMinMs: 380,
98
+ postOpenMaxMs: 980,
99
+ },
100
+ commentsHarvest: {
101
+ scrollStepMin: 260,
102
+ scrollStepMax: 380,
103
+ settleMinMs: 260,
104
+ settleMaxMs: 760,
105
+ },
106
+ pacing: {
107
+ defaultOperationMinIntervalMs: 1000,
108
+ defaultEventCooldownMs: 600,
109
+ defaultJitterMs: 800,
110
+ navigationMinIntervalMs: 2000,
111
+ },
112
+ },
113
+ '1688': {
114
+ throttle: { minMs: 800, maxMs: 1500 },
115
+ noteInterval: { minMs: 1800, maxMs: 3200 },
116
+ tabPool: { tabCount: 1, openDelayMinMs: 1200, openDelayMaxMs: 2200 },
117
+ submitSearch: {
118
+ method: 'click',
119
+ actionDelayMinMs: 140,
120
+ actionDelayMaxMs: 520,
121
+ settleMinMs: 900,
122
+ settleMaxMs: 2000,
123
+ },
124
+ openDetail: {
125
+ preClickMinMs: 180,
126
+ preClickMaxMs: 620,
127
+ pollDelayMinMs: 120,
128
+ pollDelayMaxMs: 280,
129
+ postOpenMinMs: 320,
130
+ postOpenMaxMs: 920,
131
+ },
132
+ commentsHarvest: {
133
+ scrollStepMin: 260,
134
+ scrollStepMax: 360,
135
+ settleMinMs: 240,
136
+ settleMaxMs: 700,
137
+ },
138
+ pacing: {
139
+ defaultOperationMinIntervalMs: 900,
140
+ defaultEventCooldownMs: 500,
141
+ defaultJitterMs: 700,
142
+ navigationMinIntervalMs: 1800,
143
+ },
144
+ },
145
+ });
146
+
147
+ function cloneDefaultPlatformGate(platform) {
148
+ const key = String(platform || '').trim().toLowerCase() || 'xiaohongshu';
149
+ const fallback = DEFAULT_PLATFORM_GATES[key] || DEFAULT_PLATFORM_GATES.xiaohongshu;
150
+ return JSON.parse(JSON.stringify(fallback));
151
+ }
152
+
153
+ function normalizeMethod(value, fallback = 'click') {
154
+ const method = String(value || '').trim().toLowerCase();
155
+ if (['click', 'enter', 'form'].includes(method)) return method;
156
+ return fallback;
157
+ }
158
+
159
+ function normalizeMinMax(input, defaults, minFloor = 0) {
160
+ const fallbackMin = toInt(defaults?.minMs, minFloor, minFloor);
161
+ const fallbackMax = Math.max(fallbackMin, toInt(defaults?.maxMs, fallbackMin, fallbackMin));
162
+ const minMs = toInt(input?.minMs, fallbackMin, minFloor);
163
+ const maxMs = Math.max(minMs, toInt(input?.maxMs, fallbackMax, minMs));
164
+ return { minMs, maxMs };
165
+ }
166
+
167
+ function normalizePlatformGate(rawGate = {}, defaults = cloneDefaultPlatformGate('xiaohongshu')) {
168
+ const gate = rawGate && typeof rawGate === 'object' ? rawGate : {};
169
+ const out = {
170
+ throttle: normalizeMinMax(gate.throttle, defaults.throttle, 100),
171
+ noteInterval: normalizeMinMax(gate.noteInterval, defaults.noteInterval, 200),
172
+ tabPool: {
173
+ tabCount: toInt(gate?.tabPool?.tabCount, toInt(defaults?.tabPool?.tabCount, 1, 1), 1),
174
+ openDelayMinMs: 0,
175
+ openDelayMaxMs: 0,
176
+ },
177
+ submitSearch: {
178
+ method: normalizeMethod(gate?.submitSearch?.method, normalizeMethod(defaults?.submitSearch?.method, 'click')),
179
+ actionDelayMinMs: 0,
180
+ actionDelayMaxMs: 0,
181
+ settleMinMs: 0,
182
+ settleMaxMs: 0,
183
+ },
184
+ openDetail: {
185
+ preClickMinMs: 0,
186
+ preClickMaxMs: 0,
187
+ pollDelayMinMs: 0,
188
+ pollDelayMaxMs: 0,
189
+ postOpenMinMs: 0,
190
+ postOpenMaxMs: 0,
191
+ },
192
+ commentsHarvest: {
193
+ scrollStepMin: 0,
194
+ scrollStepMax: 0,
195
+ settleMinMs: 0,
196
+ settleMaxMs: 0,
197
+ },
198
+ pacing: {
199
+ defaultOperationMinIntervalMs: 0,
200
+ defaultEventCooldownMs: 0,
201
+ defaultJitterMs: 0,
202
+ navigationMinIntervalMs: 0,
203
+ },
204
+ };
205
+
206
+ const tabDelay = normalizeMinMax(
207
+ {
208
+ minMs: gate?.tabPool?.openDelayMinMs,
209
+ maxMs: gate?.tabPool?.openDelayMaxMs,
210
+ },
211
+ {
212
+ minMs: defaults?.tabPool?.openDelayMinMs,
213
+ maxMs: defaults?.tabPool?.openDelayMaxMs,
214
+ },
215
+ 0,
216
+ );
217
+ out.tabPool.openDelayMinMs = tabDelay.minMs;
218
+ out.tabPool.openDelayMaxMs = tabDelay.maxMs;
219
+
220
+ const submitActionDelay = normalizeMinMax(
221
+ {
222
+ minMs: gate?.submitSearch?.actionDelayMinMs,
223
+ maxMs: gate?.submitSearch?.actionDelayMaxMs,
224
+ },
225
+ {
226
+ minMs: defaults?.submitSearch?.actionDelayMinMs,
227
+ maxMs: defaults?.submitSearch?.actionDelayMaxMs,
228
+ },
229
+ 20,
230
+ );
231
+ out.submitSearch.actionDelayMinMs = submitActionDelay.minMs;
232
+ out.submitSearch.actionDelayMaxMs = submitActionDelay.maxMs;
233
+
234
+ const submitSettle = normalizeMinMax(
235
+ {
236
+ minMs: gate?.submitSearch?.settleMinMs,
237
+ maxMs: gate?.submitSearch?.settleMaxMs,
238
+ },
239
+ {
240
+ minMs: defaults?.submitSearch?.settleMinMs,
241
+ maxMs: defaults?.submitSearch?.settleMaxMs,
242
+ },
243
+ 60,
244
+ );
245
+ out.submitSearch.settleMinMs = submitSettle.minMs;
246
+ out.submitSearch.settleMaxMs = submitSettle.maxMs;
247
+
248
+ const openDetailPreClick = normalizeMinMax(
249
+ {
250
+ minMs: gate?.openDetail?.preClickMinMs,
251
+ maxMs: gate?.openDetail?.preClickMaxMs,
252
+ },
253
+ {
254
+ minMs: defaults?.openDetail?.preClickMinMs,
255
+ maxMs: defaults?.openDetail?.preClickMaxMs,
256
+ },
257
+ 60,
258
+ );
259
+ out.openDetail.preClickMinMs = openDetailPreClick.minMs;
260
+ out.openDetail.preClickMaxMs = openDetailPreClick.maxMs;
261
+
262
+ const openDetailPoll = normalizeMinMax(
263
+ {
264
+ minMs: gate?.openDetail?.pollDelayMinMs,
265
+ maxMs: gate?.openDetail?.pollDelayMaxMs,
266
+ },
267
+ {
268
+ minMs: defaults?.openDetail?.pollDelayMinMs,
269
+ maxMs: defaults?.openDetail?.pollDelayMaxMs,
270
+ },
271
+ 80,
272
+ );
273
+ out.openDetail.pollDelayMinMs = openDetailPoll.minMs;
274
+ out.openDetail.pollDelayMaxMs = openDetailPoll.maxMs;
275
+
276
+ const openDetailPost = normalizeMinMax(
277
+ {
278
+ minMs: gate?.openDetail?.postOpenMinMs,
279
+ maxMs: gate?.openDetail?.postOpenMaxMs,
280
+ },
281
+ {
282
+ minMs: defaults?.openDetail?.postOpenMinMs,
283
+ maxMs: defaults?.openDetail?.postOpenMaxMs,
284
+ },
285
+ 120,
286
+ );
287
+ out.openDetail.postOpenMinMs = openDetailPost.minMs;
288
+ out.openDetail.postOpenMaxMs = openDetailPost.maxMs;
289
+
290
+ const commentsScrollStep = normalizeMinMax(
291
+ {
292
+ minMs: gate?.commentsHarvest?.scrollStepMin,
293
+ maxMs: gate?.commentsHarvest?.scrollStepMax,
294
+ },
295
+ {
296
+ minMs: defaults?.commentsHarvest?.scrollStepMin,
297
+ maxMs: defaults?.commentsHarvest?.scrollStepMax,
298
+ },
299
+ 120,
300
+ );
301
+ out.commentsHarvest.scrollStepMin = commentsScrollStep.minMs;
302
+ out.commentsHarvest.scrollStepMax = commentsScrollStep.maxMs;
303
+
304
+ const commentsSettle = normalizeMinMax(
305
+ {
306
+ minMs: gate?.commentsHarvest?.settleMinMs,
307
+ maxMs: gate?.commentsHarvest?.settleMaxMs,
308
+ },
309
+ {
310
+ minMs: defaults?.commentsHarvest?.settleMinMs,
311
+ maxMs: defaults?.commentsHarvest?.settleMaxMs,
312
+ },
313
+ 80,
314
+ );
315
+ out.commentsHarvest.settleMinMs = commentsSettle.minMs;
316
+ out.commentsHarvest.settleMaxMs = commentsSettle.maxMs;
317
+
318
+ out.pacing.defaultOperationMinIntervalMs = toInt(
319
+ gate?.pacing?.defaultOperationMinIntervalMs,
320
+ toInt(defaults?.pacing?.defaultOperationMinIntervalMs, 700, 0),
321
+ 0,
322
+ );
323
+ out.pacing.defaultEventCooldownMs = toInt(
324
+ gate?.pacing?.defaultEventCooldownMs,
325
+ toInt(defaults?.pacing?.defaultEventCooldownMs, 300, 0),
326
+ 0,
327
+ );
328
+ out.pacing.defaultJitterMs = toInt(
329
+ gate?.pacing?.defaultJitterMs,
330
+ toInt(defaults?.pacing?.defaultJitterMs, 220, 0),
331
+ 0,
332
+ );
333
+ out.pacing.navigationMinIntervalMs = toInt(
334
+ gate?.pacing?.navigationMinIntervalMs,
335
+ toInt(defaults?.pacing?.navigationMinIntervalMs, 1800, 0),
336
+ 0,
337
+ );
338
+
339
+ return out;
340
+ }
341
+
342
+ function normalizePlatformKey(value) {
343
+ const key = String(value || '').trim().toLowerCase();
344
+ if (!key) return 'xiaohongshu';
345
+ if (key === 'xhs') return 'xiaohongshu';
346
+ return key;
347
+ }
348
+
349
+ function buildDefaultDoc() {
350
+ return {
351
+ version: 1,
352
+ updatedAt: nowIso(),
353
+ platforms: {
354
+ xiaohongshu: cloneDefaultPlatformGate('xiaohongshu'),
355
+ weibo: cloneDefaultPlatformGate('weibo'),
356
+ '1688': cloneDefaultPlatformGate('1688'),
357
+ },
358
+ };
359
+ }
360
+
361
+ export function resolveFlowGatePath(options = {}) {
362
+ const home = resolveWebautoHome(options);
363
+ return path.join(home, 'config', 'flow-gates.json');
364
+ }
365
+
366
+ function normalizeDoc(raw) {
367
+ const base = buildDefaultDoc();
368
+ const input = raw && typeof raw === 'object' ? raw : {};
369
+ const sourcePlatforms = input.platforms && typeof input.platforms === 'object' ? input.platforms : {};
370
+ const platforms = {};
371
+ for (const key of Object.keys(base.platforms)) {
372
+ const normalizedKey = normalizePlatformKey(key);
373
+ const defaults = cloneDefaultPlatformGate(normalizedKey);
374
+ platforms[normalizedKey] = normalizePlatformGate(sourcePlatforms[normalizedKey], defaults);
375
+ }
376
+ for (const [rawKey, rawGate] of Object.entries(sourcePlatforms)) {
377
+ const key = normalizePlatformKey(rawKey);
378
+ if (platforms[key]) continue;
379
+ const defaults = cloneDefaultPlatformGate(key);
380
+ platforms[key] = normalizePlatformGate(rawGate, defaults);
381
+ }
382
+ return {
383
+ version: 1,
384
+ updatedAt: String(input.updatedAt || nowIso()),
385
+ platforms,
386
+ };
387
+ }
388
+
389
+ export async function loadFlowGateDoc(options = {}) {
390
+ const filePath = resolveFlowGatePath(options);
391
+ let parsed = null;
392
+ try {
393
+ const raw = await fsp.readFile(filePath, 'utf8');
394
+ parsed = JSON.parse(raw);
395
+ } catch {
396
+ parsed = null;
397
+ }
398
+ if (!parsed && options.ensure !== false) {
399
+ const seeded = buildDefaultDoc();
400
+ await saveFlowGateDoc(seeded, options);
401
+ return normalizeDoc(seeded);
402
+ }
403
+ return normalizeDoc(parsed);
404
+ }
405
+
406
+ export async function saveFlowGateDoc(doc, options = {}) {
407
+ const filePath = resolveFlowGatePath(options);
408
+ const normalized = normalizeDoc(doc);
409
+ const payload = {
410
+ ...normalized,
411
+ updatedAt: nowIso(),
412
+ };
413
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
414
+ await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
415
+ return payload;
416
+ }
417
+
418
+ export async function resolvePlatformFlowGate(platform, options = {}) {
419
+ const key = normalizePlatformKey(platform);
420
+ const doc = await loadFlowGateDoc(options);
421
+ const defaults = cloneDefaultPlatformGate(key);
422
+ return normalizePlatformGate(doc.platforms[key], defaults);
423
+ }
424
+
425
+ function deepMerge(base, patch) {
426
+ const left = base && typeof base === 'object' ? base : {};
427
+ const right = patch && typeof patch === 'object' ? patch : {};
428
+ const out = { ...left };
429
+ for (const [key, value] of Object.entries(right)) {
430
+ if (
431
+ value
432
+ && typeof value === 'object'
433
+ && !Array.isArray(value)
434
+ && left[key]
435
+ && typeof left[key] === 'object'
436
+ && !Array.isArray(left[key])
437
+ ) {
438
+ out[key] = deepMerge(left[key], value);
439
+ } else {
440
+ out[key] = value;
441
+ }
442
+ }
443
+ return out;
444
+ }
445
+
446
+ export async function patchPlatformFlowGate(platform, patch, options = {}) {
447
+ const key = normalizePlatformKey(platform);
448
+ const doc = await loadFlowGateDoc(options);
449
+ const current = doc.platforms[key] || cloneDefaultPlatformGate(key);
450
+ doc.platforms[key] = deepMerge(current, patch || {});
451
+ const saved = await saveFlowGateDoc(doc, options);
452
+ return saved.platforms[key];
453
+ }
454
+
455
+ export async function resetPlatformFlowGate(platform, options = {}) {
456
+ const key = normalizePlatformKey(platform);
457
+ const doc = await loadFlowGateDoc(options);
458
+ doc.platforms[key] = cloneDefaultPlatformGate(key);
459
+ const saved = await saveFlowGateDoc(doc, options);
460
+ return saved.platforms[key];
461
+ }
462
+
463
+ export async function listPlatformFlowGates(options = {}) {
464
+ const doc = await loadFlowGateDoc(options);
465
+ return doc.platforms;
466
+ }
@@ -13,6 +13,7 @@ import { listAccountProfiles, markProfileInvalid } from './lib/account-store.mjs
13
13
  import { listProfilesForPool } from './lib/profilepool.mjs';
14
14
  import { runCamo } from './lib/camo-cli.mjs';
15
15
  import { publishBusEvent } from './lib/bus-publish.mjs';
16
+ import { resolvePlatformFlowGate } from './lib/flow-gate.mjs';
16
17
 
17
18
  function nowIso() {
18
19
  return new Date().toISOString();
@@ -45,6 +46,13 @@ function parseNonNegativeInt(value, fallback = 0) {
45
46
  return Math.max(0, Math.floor(num));
46
47
  }
47
48
 
49
+ function pickRandomInt(min, max) {
50
+ const floorMin = Math.max(0, Math.floor(Number(min) || 0));
51
+ const floorMax = Math.max(floorMin, Math.floor(Number(max) || 0));
52
+ if (floorMax <= floorMin) return floorMin;
53
+ return floorMin + Math.floor(Math.random() * (floorMax - floorMin + 1));
54
+ }
55
+
48
56
  function parseProfiles(argv) {
49
57
  const profile = String(argv.profile || '').trim();
50
58
  const profilesRaw = String(argv.profiles || '').trim();
@@ -301,7 +309,7 @@ function createTaskReporter(seed = {}) {
301
309
  };
302
310
  }
303
311
 
304
- function buildTemplateOptions(argv, profileId, overrides = {}) {
312
+ async function buildTemplateOptions(argv, profileId, overrides = {}) {
305
313
  const keyword = String(argv.keyword || argv.k || '').trim();
306
314
  const env = String(argv.env || 'prod').trim() || 'prod';
307
315
  const inputMode = String(argv['input-mode'] || 'protocol').trim() || 'protocol';
@@ -309,9 +317,63 @@ function buildTemplateOptions(argv, profileId, overrides = {}) {
309
317
  const ocrCommand = String(argv['ocr-command'] || '').trim();
310
318
  const maxNotes = parseIntFlag(argv['max-notes'] ?? argv.target, 30, 1);
311
319
  const maxComments = parseNonNegativeInt(argv['max-comments'], 0);
312
- const throttle = parseIntFlag(argv.throttle, 500, 100);
313
- const tabCount = parseIntFlag(argv['tab-count'], 4, 1);
314
- const noteIntervalMs = parseIntFlag(argv['note-interval'], 900, 200);
320
+ let flowGate = null;
321
+ try {
322
+ flowGate = await resolvePlatformFlowGate('xiaohongshu');
323
+ } catch {
324
+ flowGate = null;
325
+ }
326
+
327
+ const throttleMin = parseIntFlag(flowGate?.throttle?.minMs, 900, 100);
328
+ const throttleMax = parseIntFlag(flowGate?.throttle?.maxMs, 1800, throttleMin);
329
+ const noteIntervalMin = parseIntFlag(flowGate?.noteInterval?.minMs, 2200, 200);
330
+ const noteIntervalMax = parseIntFlag(flowGate?.noteInterval?.maxMs, 4200, noteIntervalMin);
331
+ const tabCountDefault = parseIntFlag(flowGate?.tabPool?.tabCount, 1, 1);
332
+ const tabOpenDelayMin = parseIntFlag(flowGate?.tabPool?.openDelayMinMs, 1400, 0);
333
+ const tabOpenDelayMax = parseIntFlag(flowGate?.tabPool?.openDelayMaxMs, 2800, tabOpenDelayMin);
334
+ const submitMethodDefault = String(flowGate?.submitSearch?.method || 'click').trim().toLowerCase() || 'click';
335
+ const submitActionDelayMinDefault = parseIntFlag(flowGate?.submitSearch?.actionDelayMinMs, 180, 20);
336
+ const submitActionDelayMaxDefault = parseIntFlag(flowGate?.submitSearch?.actionDelayMaxMs, 620, submitActionDelayMinDefault);
337
+ const submitSettleMinDefault = parseIntFlag(flowGate?.submitSearch?.settleMinMs, 1200, 60);
338
+ const submitSettleMaxDefault = parseIntFlag(flowGate?.submitSearch?.settleMaxMs, 2600, submitSettleMinDefault);
339
+ const openDetailPreClickMinDefault = parseIntFlag(flowGate?.openDetail?.preClickMinMs, 220, 60);
340
+ const openDetailPreClickMaxDefault = parseIntFlag(flowGate?.openDetail?.preClickMaxMs, 700, openDetailPreClickMinDefault);
341
+ const openDetailPollDelayMinDefault = parseIntFlag(flowGate?.openDetail?.pollDelayMinMs, 130, 80);
342
+ const openDetailPollDelayMaxDefault = parseIntFlag(flowGate?.openDetail?.pollDelayMaxMs, 320, openDetailPollDelayMinDefault);
343
+ const openDetailPostOpenMinDefault = parseIntFlag(flowGate?.openDetail?.postOpenMinMs, 420, 120);
344
+ const openDetailPostOpenMaxDefault = parseIntFlag(flowGate?.openDetail?.postOpenMaxMs, 1100, openDetailPostOpenMinDefault);
345
+ const commentsScrollStepMinDefault = parseIntFlag(flowGate?.commentsHarvest?.scrollStepMin, 280, 120);
346
+ const commentsScrollStepMaxDefault = parseIntFlag(flowGate?.commentsHarvest?.scrollStepMax, 420, commentsScrollStepMinDefault);
347
+ const commentsSettleMinDefault = parseIntFlag(flowGate?.commentsHarvest?.settleMinMs, 280, 80);
348
+ const commentsSettleMaxDefault = parseIntFlag(flowGate?.commentsHarvest?.settleMaxMs, 820, commentsSettleMinDefault);
349
+ const defaultOperationMinIntervalDefault = parseIntFlag(flowGate?.pacing?.defaultOperationMinIntervalMs, 1200, 0);
350
+ const defaultEventCooldownDefault = parseIntFlag(flowGate?.pacing?.defaultEventCooldownMs, 700, 0);
351
+ const defaultPacingJitterDefault = parseIntFlag(flowGate?.pacing?.defaultJitterMs, 900, 0);
352
+ const navigationMinIntervalDefault = parseIntFlag(flowGate?.pacing?.navigationMinIntervalMs, 2200, 0);
353
+
354
+ const throttle = parseIntFlag(argv.throttle, pickRandomInt(throttleMin, throttleMax), 100);
355
+ const tabCount = parseIntFlag(argv['tab-count'], tabCountDefault, 1);
356
+ const noteIntervalMs = parseIntFlag(argv['note-interval'], pickRandomInt(noteIntervalMin, noteIntervalMax), 200);
357
+ const tabOpenDelayMs = parseIntFlag(argv['tab-open-delay'], pickRandomInt(tabOpenDelayMin, tabOpenDelayMax), 0);
358
+ const submitMethod = String(argv['search-submit-method'] || submitMethodDefault).trim().toLowerCase() || 'click';
359
+ const submitActionDelayMinMs = parseIntFlag(argv['submit-action-delay-min'], submitActionDelayMinDefault, 20);
360
+ const submitActionDelayMaxMs = parseIntFlag(argv['submit-action-delay-max'], submitActionDelayMaxDefault, submitActionDelayMinMs);
361
+ const submitSettleMinMs = parseIntFlag(argv['submit-settle-min'], submitSettleMinDefault, 60);
362
+ const submitSettleMaxMs = parseIntFlag(argv['submit-settle-max'], submitSettleMaxDefault, submitSettleMinMs);
363
+ const openDetailPreClickMinMs = parseIntFlag(argv['open-detail-preclick-min'], openDetailPreClickMinDefault, 60);
364
+ const openDetailPreClickMaxMs = parseIntFlag(argv['open-detail-preclick-max'], openDetailPreClickMaxDefault, openDetailPreClickMinMs);
365
+ const openDetailPollDelayMinMs = parseIntFlag(argv['open-detail-poll-min'], openDetailPollDelayMinDefault, 80);
366
+ const openDetailPollDelayMaxMs = parseIntFlag(argv['open-detail-poll-max'], openDetailPollDelayMaxDefault, openDetailPollDelayMinMs);
367
+ const openDetailPostOpenMinMs = parseIntFlag(argv['open-detail-postopen-min'], openDetailPostOpenMinDefault, 120);
368
+ const openDetailPostOpenMaxMs = parseIntFlag(argv['open-detail-postopen-max'], openDetailPostOpenMaxDefault, openDetailPostOpenMinMs);
369
+ const commentsScrollStepMin = parseIntFlag(argv['comments-scroll-step-min'], commentsScrollStepMinDefault, 120);
370
+ const commentsScrollStepMax = parseIntFlag(argv['comments-scroll-step-max'], commentsScrollStepMaxDefault, commentsScrollStepMin);
371
+ const commentsSettleMinMs = parseIntFlag(argv['comments-settle-min'], commentsSettleMinDefault, 80);
372
+ const commentsSettleMaxMs = parseIntFlag(argv['comments-settle-max'], commentsSettleMaxDefault, commentsSettleMinMs);
373
+ const defaultOperationMinIntervalMs = parseIntFlag(argv['operation-min-interval'], defaultOperationMinIntervalDefault, 0);
374
+ const defaultEventCooldownMs = parseIntFlag(argv['event-cooldown'], defaultEventCooldownDefault, 0);
375
+ const defaultPacingJitterMs = parseIntFlag(argv['pacing-jitter'], defaultPacingJitterDefault, 0);
376
+ const navigationMinIntervalMs = parseIntFlag(argv['navigation-min-interval'], navigationMinIntervalDefault, 0);
315
377
  const maxLikesPerRound = parseNonNegativeInt(argv['max-likes'], 0);
316
378
  const matchMode = String(argv['match-mode'] || 'any').trim() || 'any';
317
379
  const matchMinHits = parseIntFlag(argv['match-min-hits'], 1, 1);
@@ -348,7 +410,27 @@ function buildTemplateOptions(argv, profileId, overrides = {}) {
348
410
  outputRoot,
349
411
  throttle,
350
412
  tabCount,
413
+ tabOpenDelayMs,
351
414
  noteIntervalMs,
415
+ submitMethod,
416
+ submitActionDelayMinMs,
417
+ submitActionDelayMaxMs,
418
+ submitSettleMinMs,
419
+ submitSettleMaxMs,
420
+ openDetailPreClickMinMs,
421
+ openDetailPreClickMaxMs,
422
+ openDetailPollDelayMinMs,
423
+ openDetailPollDelayMaxMs,
424
+ openDetailPostOpenMinMs,
425
+ openDetailPostOpenMaxMs,
426
+ commentsScrollStepMin,
427
+ commentsScrollStepMax,
428
+ commentsSettleMinMs,
429
+ commentsSettleMaxMs,
430
+ defaultOperationMinIntervalMs,
431
+ defaultEventCooldownMs,
432
+ defaultPacingJitterMs,
433
+ navigationMinIntervalMs,
352
434
  maxNotes,
353
435
  maxComments,
354
436
  maxLikesPerRound,
@@ -543,7 +625,24 @@ async function runProfile(spec, argv, baseOverrides = {}) {
543
625
  if (spec.seedCollectMaxRounds !== undefined && spec.seedCollectMaxRounds !== null) {
544
626
  overrides.seedCollectMaxRounds = parseNonNegativeInt(spec.seedCollectMaxRounds, 0);
545
627
  }
546
- const options = buildTemplateOptions(argv, profileId, overrides);
628
+ const options = await buildTemplateOptions(argv, profileId, overrides);
629
+ console.log(JSON.stringify({
630
+ event: 'xhs.unified.flow_gate',
631
+ profileId,
632
+ throttle: options.throttle,
633
+ noteIntervalMs: options.noteIntervalMs,
634
+ tabCount: options.tabCount,
635
+ tabOpenDelayMs: options.tabOpenDelayMs,
636
+ submitMethod: options.submitMethod,
637
+ submitActionDelayMinMs: options.submitActionDelayMinMs,
638
+ submitActionDelayMaxMs: options.submitActionDelayMaxMs,
639
+ submitSettleMinMs: options.submitSettleMinMs,
640
+ submitSettleMaxMs: options.submitSettleMaxMs,
641
+ commentsScrollStepMin: options.commentsScrollStepMin,
642
+ commentsScrollStepMax: options.commentsScrollStepMax,
643
+ commentsSettleMinMs: options.commentsSettleMinMs,
644
+ commentsSettleMaxMs: options.commentsSettleMaxMs,
645
+ }));
547
646
  const script = buildXhsUnifiedAutoscript(options);
548
647
  const normalized = normalizeAutoscript(script, `xhs-unified:${profileId}`);
549
648
  const validation = validateAutoscript(normalized);
@@ -1283,6 +1382,11 @@ async function main() {
1283
1382
  ' --seed-collect-rounds <n> 首账号预采样滚动轮数(默认6)',
1284
1383
  ' --search-serial-key <key> 搜索阶段串行锁key(默认自动生成)',
1285
1384
  ' --shared-harvest-path <path> 共享harvest去重列表路径(默认自动生成)',
1385
+ ' --search-submit-method <m> 搜索提交方式 click|enter|form(默认 flow-gate)',
1386
+ ' --tab-open-delay <ms> 新开 tab 间隔(默认 flow-gate 区间随机)',
1387
+ ' --operation-min-interval <ms> 基础操作最小间隔(默认 flow-gate)',
1388
+ ' --event-cooldown <ms> 基础事件冷却(默认 flow-gate)',
1389
+ ' --pacing-jitter <ms> 基础抖动区间(默认 flow-gate)',
1286
1390
  ].join('\n'));
1287
1391
  return;
1288
1392
  }
package/bin/webauto.mjs CHANGED
@@ -167,6 +167,7 @@ Core Commands:
167
167
  webauto xhs install [--download-browser] [--download-geoip] [--ensure-backend]
168
168
  webauto xhs unified [xhs options...]
169
169
  webauto xhs status [--run-id <id>] [--json]
170
+ webauto xhs gate <get|list|set|reset|path> [--platform <name>] [--patch-json <json>] [--json]
170
171
  webauto xhs orchestrate [xhs options...]
171
172
  webauto version [--json]
172
173
  webauto version bump [patch|minor|major]
@@ -266,12 +267,14 @@ Usage:
266
267
  webauto xhs install [--download-browser] [--download-geoip] [--ensure-backend] [--install|--reinstall|--uninstall] [--browser|--geoip|--all]
267
268
  webauto xhs unified --profile <id> --keyword <kw> [options...]
268
269
  webauto xhs status [--run-id <id>] [--json]
270
+ webauto xhs gate <get|list|set|reset|path> [--platform <name>] [--patch-json <json>] [--json]
269
271
  webauto xhs orchestrate --profile <id> --keyword <kw> [options...]
270
272
 
271
273
  Subcommands:
272
274
  install 运行资源管理(兼容旧入口),支持检查/安装/卸载/重装 camoufox、geoip,按需拉起 backend
273
275
  unified 运行统一脚本(搜索 + 打开详情 + 评论抓取 + 点赞)
274
276
  status 查询当前任务状态与错误摘要(支持 runId 详情)
277
+ gate 管理平台流控参数(默认配置可修改并自动生效)
275
278
  orchestrate 运行编排入口(默认调用 unified 模式)
276
279
 
277
280
  Unified Required:
@@ -286,8 +289,8 @@ Unified Common Options:
286
289
  --concurrency <n> 并行度(默认=账号数)
287
290
  --plan-only 仅生成分片计划,不执行
288
291
  --tab-count <n> 轮询 tab 数,默认 4
289
- --throttle <ms> 操作节流,默认 500
290
- --note-interval <ms> 帖子间等待,默认 900
292
+ --throttle <ms> 操作节流(默认走 flow-gate 平台配置并随机化)
293
+ --note-interval <ms> 帖子间等待(默认走 flow-gate 平台配置并随机化)
291
294
  --env <name> 输出环境目录,默认 debug
292
295
  --output-root <path> 自定义输出根目录
293
296
  --dry-run 干跑(禁用点赞/回复)
@@ -332,6 +335,10 @@ Standard Workflows:
332
335
  webauto xhs status
333
336
  webauto xhs status --run-id <runId> --json
334
337
 
338
+ 7) 查看/修改流控 gate(按平台隔离)
339
+ webauto xhs gate get --platform xiaohongshu --json
340
+ webauto xhs gate set --platform xiaohongshu --patch-json '{"noteInterval":{"minMs":2600,"maxMs":5200}}' --json
341
+
335
342
  Output:
336
343
  默认目录: ~/.webauto/download/xiaohongshu/<env>/<keyword>/
337
344
  典型产物:
@@ -869,6 +876,12 @@ async function main() {
869
876
  return;
870
877
  }
871
878
 
879
+ if (sub === 'gate') {
880
+ const script = path.join(ROOT, 'apps', 'webauto', 'entry', 'flow-gate.mjs');
881
+ await run(process.execPath, [script, ...rawArgv.slice(2)]);
882
+ return;
883
+ }
884
+
872
885
  if (sub === 'orchestrate') {
873
886
  const script = path.join(ROOT, 'apps', 'webauto', 'entry', 'xhs-orchestrate.mjs');
874
887
  await run(process.execPath, [script, ...rawArgv.slice(2)]);