@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.
@@ -60,9 +60,11 @@ function extractErrorEvents(events = [], limit = 20) {
60
60
  for (const event of events) {
61
61
  const payload = event?.data && typeof event.data === 'object' ? event.data : event;
62
62
  const type = asText(payload?.type || payload?.event || '').toLowerCase();
63
+ if (type.includes('operation_progress')) continue;
63
64
  const hasErrorType = type.includes('error') || type.includes('fail');
65
+ const hasExplicitError = Boolean(payload?.error);
64
66
  const errText = asText(payload?.error || payload?.message || payload?.reason || '');
65
- if (!hasErrorType && !errText) continue;
67
+ if (!hasErrorType && !hasExplicitError) continue;
66
68
  items.push({
67
69
  ts: asText(payload?.timestamp || payload?.ts || ''),
68
70
  type: type || 'error',
@@ -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,
@@ -524,6 +606,7 @@ async function runProfile(spec, argv, baseOverrides = {}) {
524
606
  'xhs.unified.stop_screenshot',
525
607
  'xhs.unified.profile_failed',
526
608
  'autoscript:operation_done',
609
+ 'autoscript:operation_progress',
527
610
  'autoscript:operation_error',
528
611
  'autoscript:operation_terminal',
529
612
  'autoscript:operation_recovery_failed',
@@ -542,7 +625,24 @@ async function runProfile(spec, argv, baseOverrides = {}) {
542
625
  if (spec.seedCollectMaxRounds !== undefined && spec.seedCollectMaxRounds !== null) {
543
626
  overrides.seedCollectMaxRounds = parseNonNegativeInt(spec.seedCollectMaxRounds, 0);
544
627
  }
545
- 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
+ }));
546
646
  const script = buildXhsUnifiedAutoscript(options);
547
647
  const normalized = normalizeAutoscript(script, `xhs-unified:${profileId}`);
548
648
  const validation = validateAutoscript(normalized);
@@ -601,6 +701,7 @@ async function runProfile(spec, argv, baseOverrides = {}) {
601
701
  || eventName === 'autoscript:stop'
602
702
  || eventName === 'autoscript:impact'
603
703
  || eventName === 'autoscript:operation_start'
704
+ || eventName === 'autoscript:operation_progress'
604
705
  || eventName === 'autoscript:operation_done'
605
706
  || eventName === 'autoscript:operation_error'
606
707
  || eventName === 'autoscript:operation_recovery_failed'
@@ -1281,6 +1382,11 @@ async function main() {
1281
1382
  ' --seed-collect-rounds <n> 首账号预采样滚动轮数(默认6)',
1282
1383
  ' --search-serial-key <key> 搜索阶段串行锁key(默认自动生成)',
1283
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)',
1284
1390
  ].join('\n'));
1285
1391
  return;
1286
1392
  }
package/bin/webauto.mjs CHANGED
@@ -108,6 +108,7 @@ function resolveOnPath(candidates) {
108
108
  function wrapWindowsRunner(cmdPath, prefix = []) {
109
109
  if (process.platform !== 'win32') return { cmd: cmdPath, prefix };
110
110
  const lower = String(cmdPath || '').toLowerCase();
111
+ const quotedCmdPath = /\s/.test(String(cmdPath || '')) ? `"${cmdPath}"` : cmdPath;
111
112
  if (lower.endsWith('.ps1')) {
112
113
  return {
113
114
  cmd: 'powershell.exe',
@@ -117,7 +118,7 @@ function wrapWindowsRunner(cmdPath, prefix = []) {
117
118
  if (lower.endsWith('.cmd') || lower.endsWith('.bat')) {
118
119
  return {
119
120
  cmd: 'cmd.exe',
120
- prefix: ['/d', '/s', '/c', cmdPath, ...prefix],
121
+ prefix: ['/d', '/s', '/c', quotedCmdPath, ...prefix],
121
122
  };
122
123
  }
123
124
  return { cmd: cmdPath, prefix };
@@ -125,9 +126,9 @@ function wrapWindowsRunner(cmdPath, prefix = []) {
125
126
 
126
127
  function npmRunner() {
127
128
  if (process.platform !== 'win32') return { cmd: 'npm', prefix: [] };
128
- const npmNames = ['npm.cmd', 'npm.exe', 'npm.bat', 'npm.ps1'];
129
- const resolved = resolveOnPath(npmNames) || 'npm.cmd';
130
- return wrapWindowsRunner(resolved);
129
+ // Always prefer PATH-resolved npm.cmd to avoid space-path quoting issues
130
+ // like "C:\Program Files\..." when invoking via cmd /c.
131
+ return wrapWindowsRunner('npm.cmd');
131
132
  }
132
133
 
133
134
  function uiConsoleScriptPath() {
@@ -166,6 +167,7 @@ Core Commands:
166
167
  webauto xhs install [--download-browser] [--download-geoip] [--ensure-backend]
167
168
  webauto xhs unified [xhs options...]
168
169
  webauto xhs status [--run-id <id>] [--json]
170
+ webauto xhs gate <get|list|set|reset|path> [--platform <name>] [--patch-json <json>] [--json]
169
171
  webauto xhs orchestrate [xhs options...]
170
172
  webauto version [--json]
171
173
  webauto version bump [patch|minor|major]
@@ -265,12 +267,14 @@ Usage:
265
267
  webauto xhs install [--download-browser] [--download-geoip] [--ensure-backend] [--install|--reinstall|--uninstall] [--browser|--geoip|--all]
266
268
  webauto xhs unified --profile <id> --keyword <kw> [options...]
267
269
  webauto xhs status [--run-id <id>] [--json]
270
+ webauto xhs gate <get|list|set|reset|path> [--platform <name>] [--patch-json <json>] [--json]
268
271
  webauto xhs orchestrate --profile <id> --keyword <kw> [options...]
269
272
 
270
273
  Subcommands:
271
274
  install 运行资源管理(兼容旧入口),支持检查/安装/卸载/重装 camoufox、geoip,按需拉起 backend
272
275
  unified 运行统一脚本(搜索 + 打开详情 + 评论抓取 + 点赞)
273
276
  status 查询当前任务状态与错误摘要(支持 runId 详情)
277
+ gate 管理平台流控参数(默认配置可修改并自动生效)
274
278
  orchestrate 运行编排入口(默认调用 unified 模式)
275
279
 
276
280
  Unified Required:
@@ -285,8 +289,8 @@ Unified Common Options:
285
289
  --concurrency <n> 并行度(默认=账号数)
286
290
  --plan-only 仅生成分片计划,不执行
287
291
  --tab-count <n> 轮询 tab 数,默认 4
288
- --throttle <ms> 操作节流,默认 500
289
- --note-interval <ms> 帖子间等待,默认 900
292
+ --throttle <ms> 操作节流(默认走 flow-gate 平台配置并随机化)
293
+ --note-interval <ms> 帖子间等待(默认走 flow-gate 平台配置并随机化)
290
294
  --env <name> 输出环境目录,默认 debug
291
295
  --output-root <path> 自定义输出根目录
292
296
  --dry-run 干跑(禁用点赞/回复)
@@ -331,6 +335,10 @@ Standard Workflows:
331
335
  webauto xhs status
332
336
  webauto xhs status --run-id <runId> --json
333
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
+
334
342
  Output:
335
343
  默认目录: ~/.webauto/download/xiaohongshu/<env>/<keyword>/
336
344
  典型产物:
@@ -477,17 +485,12 @@ async function runInDir(dir, cmd, args) {
477
485
  }
478
486
 
479
487
  function checkDesktopConsoleDeps() {
480
- if (isGlobalInstall()) return true;
481
- // Check for electron in various locations:
482
- // 1. Global npm root (when installed globally alongside webauto)
483
- // 2. Package's own node_modules
484
- // 3. apps/desktop-console/node_modules (local dev)
485
- const globalRoot = path.resolve(ROOT, '..', '..');
486
- return (
487
- exists(path.join(globalRoot, 'electron')) ||
488
- exists(path.join(ROOT, 'node_modules', 'electron')) ||
489
- exists(path.join(ROOT, 'apps', 'desktop-console', 'node_modules', 'electron'))
490
- );
488
+ const electronName = process.platform === 'win32' ? 'electron.exe' : 'electron';
489
+ const candidates = [
490
+ path.join(ROOT, 'node_modules', 'electron', 'dist', electronName),
491
+ path.join(ROOT, 'apps', 'desktop-console', 'node_modules', 'electron', 'dist', electronName),
492
+ ];
493
+ return candidates.some((p) => exists(p));
491
494
  }
492
495
 
493
496
  function checkDesktopConsoleBuilt() {
@@ -510,6 +513,15 @@ async function ensureDepsAndBuild() {
510
513
 
511
514
  // Global package should already ship renderer build.
512
515
  if (isGlobalInstall()) {
516
+ if (!checkDesktopConsoleDeps()) {
517
+ console.log('[webauto] Installing desktop-console runtime dependencies...');
518
+ const npm = npmRunner();
519
+ await run(npm.cmd, [...npm.prefix, '--prefix', appDir, '--workspaces=false', 'install', '--omit=dev']);
520
+ }
521
+ if (!checkDesktopConsoleDeps()) {
522
+ console.error('❌ electron runtime installation failed for desktop-console.');
523
+ process.exit(1);
524
+ }
513
525
  if (!checkDesktopConsoleBuilt()) {
514
526
  console.error('❌ desktop-console dist missing from package. Please reinstall @web-auto/webauto.');
515
527
  process.exit(1);
@@ -540,62 +552,80 @@ async function ensureDepsAndBuild() {
540
552
  console.log('[webauto] Setup complete!');
541
553
  }
542
554
 
543
- async function uiConsole({ build, install, checkOnly, noDaemon }) {
544
- console.log(`[webauto] version ${ROOT_VERSION}`);
545
- const okServices = checkServicesBuilt();
546
- const okDeps = checkDesktopConsoleDeps();
547
- const okUiBuilt = checkDesktopConsoleBuilt();
548
-
549
- if (checkOnly) {
550
- console.log(`[check] repoRoot: ${ROOT}`);
551
- console.log(`[check] dist/services: ${okServices ? 'OK' : 'MISSING'}`);
552
- console.log(`[check] desktop-console deps: ${okDeps ? 'OK' : 'MISSING'}`);
553
- console.log(`[check] desktop-console dist: ${okUiBuilt ? 'OK' : 'MISSING'}`);
554
- console.log(`[check] isGlobalInstall: ${isGlobalInstall()}`);
555
- return;
556
- }
555
+ async function ensureUiRuntimeReady({ build, install }) {
556
+ let okServices = checkServicesBuilt();
557
+ let okDeps = checkDesktopConsoleDeps();
558
+ let okUiBuilt = checkDesktopConsoleBuilt();
557
559
 
558
- // For global install, auto-setup on first run
559
560
  if (isGlobalInstall()) {
560
561
  const state = loadState();
561
562
  const pkgJson = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf-8'));
562
- if (!state.initialized || state.version !== pkgJson.version) {
563
+ if (!state.initialized || state.version !== pkgJson.version || !okDeps || !okUiBuilt) {
563
564
  await ensureDepsAndBuild();
565
+ okDeps = checkDesktopConsoleDeps();
566
+ okUiBuilt = checkDesktopConsoleBuilt();
564
567
  }
565
- } else {
566
- // Local dev mode - require explicit build
567
- if (!okServices) {
568
- if (!build) {
569
- console.error('❌ missing dist/ (services/modules). Run: npm run build:services');
570
- process.exit(2);
571
- }
572
- const npm = npmRunner();
573
- await run(npm.cmd, [...npm.prefix, 'run', 'build:services']);
568
+ } else if (!okServices) {
569
+ if (!build) {
570
+ console.error('❌ missing dist/ (services/modules). Run: npm run build:services');
571
+ process.exit(2);
574
572
  }
573
+ const npm = npmRunner();
574
+ await run(npm.cmd, [...npm.prefix, 'run', 'build:services']);
575
+ okServices = checkServicesBuilt();
575
576
  }
576
577
 
577
578
  if (!okDeps) {
578
- // For global install, okDeps is always true since we check global electron
579
- // For local dev, require explicit --install or --build
580
- if (!isGlobalInstall() && !install && !build) {
581
- console.error('❌ missing apps/desktop-console/node_modules. Run: npm --prefix apps/desktop-console install');
579
+ if (isGlobalInstall()) {
580
+ console.error('❌ electron runtime missing from installed package after setup.');
582
581
  process.exit(2);
583
582
  }
584
- if (!isGlobalInstall()) {
585
- const npm = npmRunner();
586
- await runInDir(path.join(ROOT, 'apps', 'desktop-console'), npm.cmd, [...npm.prefix, 'install']);
583
+ if (!install && !build) {
584
+ console.error('❌ missing apps/desktop-console/node_modules. Run: npm --prefix apps/desktop-console install');
585
+ process.exit(2);
587
586
  }
587
+ const npm = npmRunner();
588
+ await runInDir(path.join(ROOT, 'apps', 'desktop-console'), npm.cmd, [...npm.prefix, 'install']);
589
+ okDeps = checkDesktopConsoleDeps();
588
590
  }
589
591
 
590
592
  if (!okUiBuilt) {
593
+ if (isGlobalInstall()) {
594
+ console.error('❌ missing apps/desktop-console/dist in installed package after setup.');
595
+ process.exit(2);
596
+ }
591
597
  if (!build) {
592
598
  console.error('❌ missing apps/desktop-console/dist. Run: npm --prefix apps/desktop-console run build');
593
599
  process.exit(2);
594
600
  }
595
601
  const npm = npmRunner();
596
602
  await runInDir(path.join(ROOT, 'apps', 'desktop-console'), npm.cmd, [...npm.prefix, 'run', 'build']);
603
+ okUiBuilt = checkDesktopConsoleBuilt();
604
+ }
605
+
606
+ if (!okDeps || !okUiBuilt || (!isGlobalInstall() && !okServices)) {
607
+ console.error('❌ ui runtime prerequisites are not ready');
608
+ process.exit(2);
609
+ }
610
+ }
611
+
612
+ async function uiConsole({ build, install, checkOnly, noDaemon }) {
613
+ console.log(`[webauto] version ${ROOT_VERSION}`);
614
+ const okServices = checkServicesBuilt();
615
+ const okDeps = checkDesktopConsoleDeps();
616
+ const okUiBuilt = checkDesktopConsoleBuilt();
617
+
618
+ if (checkOnly) {
619
+ console.log(`[check] repoRoot: ${ROOT}`);
620
+ console.log(`[check] dist/services: ${okServices ? 'OK' : 'MISSING'}`);
621
+ console.log(`[check] desktop-console deps: ${okDeps ? 'OK' : 'MISSING'}`);
622
+ console.log(`[check] desktop-console dist: ${okUiBuilt ? 'OK' : 'MISSING'}`);
623
+ console.log(`[check] isGlobalInstall: ${isGlobalInstall()}`);
624
+ return;
597
625
  }
598
626
 
627
+ await ensureUiRuntimeReady({ build, install });
628
+
599
629
  const uiScript = uiConsoleScriptPath();
600
630
  const uiArgs = [];
601
631
  if (noDaemon) uiArgs.push('--no-daemon');
@@ -746,6 +776,10 @@ async function main() {
746
776
  }
747
777
 
748
778
  if (cmd === 'ui' && sub === 'cli') {
779
+ await ensureUiRuntimeReady({
780
+ build: args.build === true,
781
+ install: args.install === true,
782
+ });
749
783
  const script = path.join(ROOT, 'apps', 'desktop-console', 'entry', 'ui-cli.mjs');
750
784
  await run(process.execPath, [script, ...rawArgv.slice(2)]);
751
785
  return;
@@ -842,6 +876,12 @@ async function main() {
842
876
  return;
843
877
  }
844
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
+
845
885
  if (sub === 'orchestrate') {
846
886
  const script = path.join(ROOT, 'apps', 'webauto', 'entry', 'xhs-orchestrate.mjs');
847
887
  await run(process.execPath, [script, ...rawArgv.slice(2)]);
@@ -1,9 +1,58 @@
1
1
  #!/usr/bin/env node
2
- import { execSync } from 'node:child_process';
2
+ import { execSync, spawnSync } from 'node:child_process';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
+ import { createRequire } from 'node:module';
7
+ import { fileURLToPath } from 'node:url';
6
8
  import { BROWSER_SERVICE_URL, loadConfig, setRepoRoot } from './config.mjs';
9
+ const requireFromHere = createRequire(import.meta.url);
10
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
11
+ function resolveNodeBin() {
12
+ const explicit = String(process.env.WEBAUTO_NODE_BIN || '').trim();
13
+ if (explicit)
14
+ return explicit;
15
+ const npmNode = String(process.env.npm_node_execpath || '').trim();
16
+ if (npmNode)
17
+ return npmNode;
18
+ return process.execPath;
19
+ }
20
+ function resolveCamoCliEntry() {
21
+ try {
22
+ const resolved = requireFromHere.resolve('@web-auto/camo/bin/camo.mjs');
23
+ if (resolved && fs.existsSync(resolved))
24
+ return resolved;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ return null;
30
+ }
31
+ function runCamoCli(args = [], options = {}) {
32
+ const entry = resolveCamoCliEntry();
33
+ if (!entry) {
34
+ return {
35
+ ok: false,
36
+ code: null,
37
+ stdout: '',
38
+ stderr: '@web-auto/camo/bin/camo.mjs not found',
39
+ entry: null,
40
+ };
41
+ }
42
+ const ret = spawnSync(resolveNodeBin(), [entry, ...args], {
43
+ encoding: 'utf8',
44
+ windowsHide: true,
45
+ stdio: options.stdio || 'pipe',
46
+ env: { ...process.env, ...(options.env || {}) },
47
+ });
48
+ return {
49
+ ok: ret.status === 0,
50
+ code: ret.status,
51
+ stdout: String(ret.stdout || ''),
52
+ stderr: String(ret.stderr || ''),
53
+ entry,
54
+ };
55
+ }
7
56
  export async function callAPI(action, payload = {}) {
8
57
  const r = await fetch(`${BROWSER_SERVICE_URL}/command`, {
9
58
  method: 'POST',
@@ -319,20 +368,13 @@ function scanCommonRepoRoots() {
319
368
  }
320
369
  export function findRepoRootCandidate() {
321
370
  const cfg = loadConfig();
371
+ const cwdRoot = walkUpForRepoRoot(process.cwd());
372
+ const moduleRoot = walkUpForRepoRoot(MODULE_DIR);
322
373
  const candidates = [
323
374
  process.env.WEBAUTO_REPO_ROOT,
324
- process.cwd(),
325
375
  cfg.repoRoot,
326
- path.join('/Volumes', 'extension', 'code', 'webauto'),
327
- path.join('/Volumes', 'extension', 'code', 'WebAuto'),
328
- path.join(os.homedir(), 'Documents', 'github', 'webauto'),
329
- path.join(os.homedir(), 'Documents', 'github', 'WebAuto'),
330
- path.join(os.homedir(), 'github', 'webauto'),
331
- path.join(os.homedir(), 'github', 'WebAuto'),
332
- path.join('C:', 'code', 'webauto'),
333
- path.join('C:', 'code', 'WebAuto'),
334
- path.join('C:', 'Users', os.userInfo().username, 'code', 'webauto'),
335
- path.join('C:', 'Users', os.userInfo().username, 'code', 'WebAuto'),
376
+ moduleRoot,
377
+ cwdRoot,
336
378
  ].filter(Boolean);
337
379
  for (const root of candidates) {
338
380
  if (!hasContainerLibrary(root))
@@ -343,20 +385,6 @@ export function findRepoRootCandidate() {
343
385
  }
344
386
  return resolved;
345
387
  }
346
- const walked = walkUpForRepoRoot(process.cwd());
347
- if (walked) {
348
- if (cfg.repoRoot !== walked) {
349
- setRepoRoot(walked);
350
- }
351
- return walked;
352
- }
353
- const scanned = scanCommonRepoRoots();
354
- if (scanned) {
355
- if (cfg.repoRoot !== scanned) {
356
- setRepoRoot(scanned);
357
- }
358
- return scanned;
359
- }
360
388
  return null;
361
389
  }
362
390
  export function detectCamoufoxPath() {
@@ -381,12 +409,7 @@ export function detectCamoufoxPath() {
381
409
  export function ensureCamoufox() {
382
410
  if (detectCamoufoxPath())
383
411
  return;
384
- console.log('Camoufox is not found. Installing...');
385
- execSync('npx --yes --package=camoufox camoufox fetch', { stdio: 'inherit' });
386
- if (!detectCamoufoxPath()) {
387
- throw new Error('Camoufox install finished but executable was not detected');
388
- }
389
- console.log('Camoufox installed.');
412
+ throw new Error('Camoufox is not installed. Run: webauto xhs install --download-browser');
390
413
  }
391
414
  export async function ensureBrowserService() {
392
415
  if (await checkBrowserService())
@@ -398,20 +421,17 @@ export async function ensureBrowserService() {
398
421
  }
399
422
  if (provider === 'camo') {
400
423
  const repoRoot = findRepoRootCandidate();
401
- if (repoRoot) {
402
- try {
403
- execSync(`npx --yes @web-auto/camo config repo-root ${JSON.stringify(repoRoot)}`, { stdio: 'ignore' });
404
- }
405
- catch {
406
- // best-effort only; init will still try using current config
407
- }
424
+ if (!repoRoot) {
425
+ throw new Error('WEBAUTO_REPO_ROOT is not set and no valid repo root was found');
408
426
  }
409
- try {
410
- console.log('Starting browser backend via camo init...');
411
- execSync('npx --yes @web-auto/camo init', { stdio: 'inherit' });
427
+ const configRet = runCamoCli(['config', 'repo-root', repoRoot], { stdio: 'pipe' });
428
+ if (!configRet.ok) {
429
+ throw new Error(`camo config repo-root failed: ${configRet.stderr.trim() || configRet.stdout.trim() || `exit ${configRet.code ?? 'null'}`}`);
412
430
  }
413
- catch (error) {
414
- throw new Error(`camo init failed: ${error?.message || String(error)}`);
431
+ console.log('Starting browser backend via camo init...');
432
+ const initRet = runCamoCli(['init'], { stdio: 'inherit' });
433
+ if (!initRet.ok) {
434
+ throw new Error(`camo init failed: ${initRet.stderr.trim() || initRet.stdout.trim() || `exit ${initRet.code ?? 'null'}`}`);
415
435
  }
416
436
  for (let i = 0; i < 20; i += 1) {
417
437
  await new Promise((r) => setTimeout(r, 400));
@@ -1,9 +1,20 @@
1
1
  import { executeXhsAutoscriptOperation, isXhsAutoscriptAction } from './xhs.mjs';
2
2
 
3
- export async function executeAutoscriptAction({ profileId, action, params = {} }) {
3
+ export async function executeAutoscriptAction({
4
+ profileId,
5
+ action,
6
+ params = {},
7
+ operation = null,
8
+ context = {},
9
+ }) {
4
10
  if (isXhsAutoscriptAction(action)) {
5
- return executeXhsAutoscriptOperation({ profileId, action, params });
11
+ return executeXhsAutoscriptOperation({
12
+ profileId,
13
+ action,
14
+ params,
15
+ operation,
16
+ context,
17
+ });
6
18
  }
7
19
  return null;
8
20
  }
9
-