@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
|
@@ -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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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>
|
|
290
|
-
--note-interval <ms>
|
|
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)]);
|