@web-auto/webauto 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@webauto/desktop-console",
3
+ "version": "0.1.11",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "dist/main/index.mjs",
7
+ "description": "Cross-platform desktop console for WebAuto (scripts runner)",
8
+ "scripts": {
9
+ "build": "node scripts/build.mjs",
10
+ "start": "electron .",
11
+ "test-preload": "node scripts/test-preload.mjs",
12
+ "test:renderer": "tsx --test src/renderer/index.test.mts src/renderer/index.runtime.test.mts src/renderer/path-helpers.test.mts src/renderer/tabs/debug.test.mts src/renderer/tabs/logs.test.mts src/renderer/tabs/preflight.test.mts src/renderer/tabs/preflight.runtime.test.mts src/renderer/tabs/run.test.mts src/renderer/tabs/runtime-smoke.test.mts src/renderer/tabs/xiaohongshu.test.mts src/renderer/tabs-new/setup-wizard.test.mts src/renderer/tabs-new/setup-wizard.runtime.test.mts src/renderer/tabs-new/account-manager.test.mts src/renderer/tabs-new/account-manager.runtime.test.mts src/renderer/tabs-new/config-panel.runtime.test.mts src/renderer/tabs-new/dashboard.runtime.test.mts src/renderer/tabs-new/scheduler.test.mts src/renderer/tabs-new/scheduler.runtime.test.mts src/renderer/tabs-new/tasks.runtime.test.mts src/renderer/ui-runtime.test.mts src/renderer/tabs/xiaohongshu/helpers.runtime.test.mts src/renderer/tabs/xiaohongshu-state.runtime.test.mts src/renderer/tabs/xiaohongshu/guide-browser-check.runtime.test.mts src/renderer/tabs/xiaohongshu/live-stats/runtime.test.mts",
13
+ "test:renderer:coverage": "c8 --all --extension .mts --extension .ts --reporter=text --reporter=text-summary --check-coverage --lines 90 --functions 85 --branches 55 --statements 90 --include src/renderer/index.mts --include src/renderer/account-source.mts --include src/renderer/hooks/use-task-state.mts --include src/renderer/path-helpers.mts --include src/renderer/ui-components.mts --include src/renderer/tabs/preflight.mts --include src/renderer/tabs/logs.mts --include src/renderer/tabs/profile-pool.mts --include src/renderer/tabs/results.mts --include src/renderer/tabs/run.mts --include src/renderer/tabs/runtime.mts --include src/renderer/tabs/settings.mts --include src/renderer/tabs-new/setup-wizard.mts --include src/renderer/tabs-new/account-manager.mts --include src/renderer/tabs-new/config-panel.mts --include src/renderer/tabs-new/dashboard.mts --include src/renderer/tabs-new/scheduler.mts --include src/renderer/tabs-new/tasks.mts --include src/renderer/tabs/xiaohongshu/helpers.mts --include src/renderer/tabs/xiaohongshu-state.mts --include src/renderer/tabs/xiaohongshu/guide-browser-check.mts --include src/renderer/tabs/xiaohongshu/live-stats/state-patch.mts --include src/renderer/tabs/xiaohongshu/live-stats/stdout-parser.mts tsx --test src/renderer/index.test.mts src/renderer/index.runtime.test.mts src/renderer/path-helpers.test.mts src/renderer/tabs/debug.test.mts src/renderer/tabs/logs.test.mts src/renderer/tabs/preflight.test.mts src/renderer/tabs/preflight.runtime.test.mts src/renderer/tabs/run.test.mts src/renderer/tabs/runtime-smoke.test.mts src/renderer/tabs/xiaohongshu.test.mts src/renderer/tabs-new/setup-wizard.test.mts src/renderer/tabs-new/setup-wizard.runtime.test.mts src/renderer/tabs-new/account-manager.test.mts src/renderer/tabs-new/account-manager.runtime.test.mts src/renderer/tabs-new/config-panel.runtime.test.mts src/renderer/tabs-new/dashboard.runtime.test.mts src/renderer/tabs-new/scheduler.test.mts src/renderer/tabs-new/scheduler.runtime.test.mts src/renderer/tabs-new/tasks.runtime.test.mts src/renderer/ui-runtime.test.mts src/renderer/tabs/xiaohongshu/helpers.runtime.test.mts src/renderer/tabs/xiaohongshu-state.runtime.test.mts src/renderer/tabs/xiaohongshu/guide-browser-check.runtime.test.mts src/renderer/tabs/xiaohongshu/live-stats/runtime.test.mts",
14
+ "clean": "node --input-type=module -e \"import('node:fs').then((fs)=>fs.rmSync('dist',{recursive:true,force:true}))\""
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.19.33",
18
+ "c8": "^10.1.3",
19
+ "jsdom": "^26.1.0"
20
+ },
21
+ "dependencies": {
22
+ "electron": "^39.6.0"
23
+ }
24
+ }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ import minimist from 'minimist';
3
+ import {
4
+ listPlatformFlowGates,
5
+ loadFlowGateDoc,
6
+ patchPlatformFlowGate,
7
+ resetPlatformFlowGate,
8
+ resolveFlowGatePath,
9
+ resolvePlatformFlowGate,
10
+ } from './lib/flow-gate.mjs';
11
+
12
+ function normalizePlatform(value) {
13
+ const text = String(value || '').trim().toLowerCase();
14
+ if (!text) return 'xiaohongshu';
15
+ if (text === 'xhs') return 'xiaohongshu';
16
+ return text;
17
+ }
18
+
19
+ function toJsonMode(argv) {
20
+ return argv.json === true || argv.j === true;
21
+ }
22
+
23
+ function printHelp() {
24
+ console.log([
25
+ 'Usage: node apps/webauto/entry/flow-gate.mjs <get|list|set|reset|path> [options]',
26
+ 'Options:',
27
+ ' --platform <name> 平台名(默认 xiaohongshu)',
28
+ ' --patch-json <json> set 动作补丁 JSON(对象)',
29
+ ' --json 输出 JSON',
30
+ '',
31
+ 'Examples:',
32
+ ' node apps/webauto/entry/flow-gate.mjs get --platform xiaohongshu --json',
33
+ ' node apps/webauto/entry/flow-gate.mjs list --json',
34
+ ' node apps/webauto/entry/flow-gate.mjs set --platform xiaohongshu --patch-json \'{"noteInterval":{"minMs":2600,"maxMs":5200}}\' --json',
35
+ ' node apps/webauto/entry/flow-gate.mjs reset --platform xiaohongshu --json',
36
+ ].join('\n'));
37
+ }
38
+
39
+ function parsePatchJson(raw) {
40
+ const text = String(raw || '').trim();
41
+ if (!text) throw new Error('missing --patch-json');
42
+ let parsed = null;
43
+ try {
44
+ parsed = JSON.parse(text);
45
+ } catch (error) {
46
+ throw new Error(`invalid --patch-json: ${error?.message || String(error)}`);
47
+ }
48
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
49
+ throw new Error('--patch-json must be object JSON');
50
+ }
51
+ return parsed;
52
+ }
53
+
54
+ async function main() {
55
+ const argv = minimist(process.argv.slice(2), {
56
+ boolean: ['help', 'h', 'json', 'j'],
57
+ string: ['platform', 'patch-json'],
58
+ });
59
+ if (argv.help || argv.h) {
60
+ printHelp();
61
+ return;
62
+ }
63
+
64
+ const action = String(argv._[0] || 'get').trim().toLowerCase();
65
+ const platform = normalizePlatform(argv.platform);
66
+ const jsonMode = toJsonMode(argv);
67
+ const path = resolveFlowGatePath();
68
+
69
+ if (action === 'path') {
70
+ if (jsonMode) {
71
+ console.log(JSON.stringify({ ok: true, path }, null, 2));
72
+ } else {
73
+ console.log(path);
74
+ }
75
+ return;
76
+ }
77
+
78
+ if (action === 'get') {
79
+ const gate = await resolvePlatformFlowGate(platform);
80
+ if (jsonMode) {
81
+ console.log(JSON.stringify({ ok: true, platform, path, gate }, null, 2));
82
+ } else {
83
+ console.log(`[flow-gate] platform=${platform}`);
84
+ console.log(JSON.stringify(gate, null, 2));
85
+ console.log(`[flow-gate] file=${path}`);
86
+ }
87
+ return;
88
+ }
89
+
90
+ if (action === 'list') {
91
+ const gates = await listPlatformFlowGates();
92
+ if (jsonMode) {
93
+ console.log(JSON.stringify({ ok: true, path, platforms: gates }, null, 2));
94
+ } else {
95
+ console.log(JSON.stringify(gates, null, 2));
96
+ console.log(`[flow-gate] file=${path}`);
97
+ }
98
+ return;
99
+ }
100
+
101
+ if (action === 'set') {
102
+ const patch = parsePatchJson(argv['patch-json']);
103
+ const gate = await patchPlatformFlowGate(platform, patch);
104
+ if (jsonMode) {
105
+ console.log(JSON.stringify({ ok: true, action, platform, path, gate }, null, 2));
106
+ } else {
107
+ console.log(`[flow-gate] updated platform=${platform}`);
108
+ console.log(JSON.stringify(gate, null, 2));
109
+ console.log(`[flow-gate] file=${path}`);
110
+ }
111
+ return;
112
+ }
113
+
114
+ if (action === 'reset') {
115
+ const gate = await resetPlatformFlowGate(platform);
116
+ if (jsonMode) {
117
+ console.log(JSON.stringify({ ok: true, action, platform, path, gate }, null, 2));
118
+ } else {
119
+ console.log(`[flow-gate] reset platform=${platform}`);
120
+ console.log(JSON.stringify(gate, null, 2));
121
+ console.log(`[flow-gate] file=${path}`);
122
+ }
123
+ return;
124
+ }
125
+
126
+ if (action === 'show-doc') {
127
+ const doc = await loadFlowGateDoc();
128
+ if (jsonMode) console.log(JSON.stringify({ ok: true, path, doc }, null, 2));
129
+ else console.log(JSON.stringify(doc, null, 2));
130
+ return;
131
+ }
132
+
133
+ throw new Error(`unknown action: ${action}`);
134
+ }
135
+
136
+ main().catch((err) => {
137
+ console.error(err?.message || String(err));
138
+ process.exit(1);
139
+ });
@@ -44,7 +44,6 @@ export function getCamoRunner(rootDir = process.cwd()) {
44
44
  const isWin = process.platform === 'win32';
45
45
  const localBin = path.join(rootDir, 'node_modules', '.bin');
46
46
  const camoNames = isWin ? ['camo.cmd', 'camo.exe', 'camo.bat', 'camo.ps1'] : ['camo'];
47
- const npxNames = isWin ? ['npx.cmd', 'npx.exe', 'npx.bat', 'npx.ps1'] : ['npx'];
48
47
 
49
48
  const local = resolveInDir(localBin, camoNames);
50
49
  if (local) return wrapWindowsRunner(local);
@@ -52,8 +51,7 @@ export function getCamoRunner(rootDir = process.cwd()) {
52
51
  const global = resolveOnPath(camoNames);
53
52
  if (global) return wrapWindowsRunner(global);
54
53
 
55
- const npx = resolveOnPath(npxNames) || (isWin ? 'npx.cmd' : 'npx');
56
- return wrapWindowsRunner(npx, ['--yes', '--package=@web-auto/camo', 'camo']);
54
+ return null;
57
55
  }
58
56
 
59
57
  function parseLastJson(stdout) {
@@ -74,6 +72,15 @@ export function runCamo(args, options = {}) {
74
72
  const rootDir = String(options.rootDir || process.cwd());
75
73
  const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : 60000;
76
74
  const runner = getCamoRunner(rootDir);
75
+ if (!runner) {
76
+ return {
77
+ ok: false,
78
+ code: null,
79
+ stdout: '',
80
+ stderr: 'camo cli not found in node_modules/.bin or PATH',
81
+ json: null,
82
+ };
83
+ }
77
84
  const ret = spawnSync(runner.cmd, [...runner.prefix, ...args], {
78
85
  cwd: rootDir,
79
86
  env: { ...process.env, ...(options.env || {}) },
@@ -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
+ }
@@ -172,10 +172,6 @@ function checkGeoIPInstalled() {
172
172
  return hasValidGeoIPFile(resolveGeoIPPath());
173
173
  }
174
174
 
175
- function installGeoIP() {
176
- return runCamoCommand(['init', 'geoip']);
177
- }
178
-
179
175
  async function installGeoIPDirect() {
180
176
  const target = resolveGeoIPPath();
181
177
  const tmp = `${target}.tmp`;
@@ -195,18 +191,14 @@ async function installGeoIPDirect() {
195
191
  }
196
192
 
197
193
  async function ensureGeoIPInstalled() {
198
- const commandResult = installGeoIP();
199
- if (checkGeoIPInstalled()) {
200
- return { ok: true, source: 'camo', ret: commandResult.ret };
201
- }
202
194
  try {
203
195
  await installGeoIPDirect();
204
- return { ok: checkGeoIPInstalled(), source: 'direct', ret: commandResult.ret };
196
+ return { ok: checkGeoIPInstalled(), source: 'direct', ret: null, detail: '' };
205
197
  } catch (error) {
206
198
  return {
207
199
  ok: false,
208
- source: 'none',
209
- ret: commandResult.ret,
200
+ source: 'direct',
201
+ ret: null,
210
202
  detail: error?.message || String(error),
211
203
  };
212
204
  }