@web-auto/webauto 0.1.4 → 0.1.6

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.
Files changed (174) hide show
  1. package/apps/desktop-console/default-settings.json +2 -2
  2. package/apps/desktop-console/dist/main/index.mjs +915 -85
  3. package/apps/desktop-console/dist/main/preload.mjs +7 -0
  4. package/apps/desktop-console/dist/renderer/index.html +622 -50
  5. package/apps/desktop-console/dist/renderer/index.js +2415 -470
  6. package/apps/desktop-console/dist/renderer/run.mts +6 -5
  7. package/apps/desktop-console/entry/ui-cli.mjs +672 -0
  8. package/apps/desktop-console/entry/ui-console.mjs +416 -29
  9. package/apps/webauto/entry/account.mjs +89 -53
  10. package/apps/webauto/entry/browser-status.mjs +7 -10
  11. package/apps/webauto/entry/lib/account-detect.mjs +254 -28
  12. package/apps/webauto/entry/lib/account-store.mjs +219 -30
  13. package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
  14. package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
  15. package/apps/webauto/entry/lib/profilepool.mjs +14 -5
  16. package/apps/webauto/entry/lib/quota-status.mjs +23 -0
  17. package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
  18. package/apps/webauto/entry/profilepool.mjs +106 -17
  19. package/apps/webauto/entry/schedule.mjs +612 -0
  20. package/apps/webauto/entry/weibo-unified.mjs +134 -0
  21. package/apps/webauto/entry/xhs-install.mjs +236 -29
  22. package/apps/webauto/entry/xhs-status.mjs +5 -2
  23. package/apps/webauto/entry/xhs-unified.mjs +631 -98
  24. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
  25. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
  26. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
  27. package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
  28. package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
  29. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
  30. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
  31. package/bin/camoufox-cli.mjs +61 -0
  32. package/bin/webauto.mjs +301 -54
  33. package/dist/modules/camo-backend/src/index.js +49 -1
  34. package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
  35. package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
  36. package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
  37. package/dist/modules/collection-manager/bloom-filter.js +91 -0
  38. package/dist/modules/collection-manager/date-utils.js +275 -0
  39. package/dist/modules/collection-manager/index.js +258 -0
  40. package/dist/modules/collection-manager/storage.js +195 -0
  41. package/dist/modules/collection-manager/types.js +47 -0
  42. package/dist/modules/logging/src/index.js +1 -1
  43. package/dist/modules/process-registry/index.js +230 -0
  44. package/dist/modules/rate-limiter/index.js +242 -0
  45. package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
  46. package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
  47. package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
  48. package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
  49. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
  50. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
  51. package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
  52. package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
  53. package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
  54. package/dist/modules/workflow/config/workflowRegistry.js +2 -0
  55. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
  56. package/dist/modules/workflow/src/runner.js +6 -0
  57. package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
  58. package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
  59. package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
  60. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
  61. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
  62. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
  63. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
  64. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
  65. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
  66. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
  67. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
  68. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
  69. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
  70. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
  71. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
  72. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
  73. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
  74. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
  75. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
  76. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
  77. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
  78. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
  79. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
  80. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
  81. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
  82. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
  83. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
  84. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
  85. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
  86. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
  87. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
  88. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
  89. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
  90. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
  91. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
  92. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
  93. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
  94. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
  95. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
  96. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
  97. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
  98. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
  99. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
  100. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
  101. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
  102. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
  103. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
  104. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
  105. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
  106. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
  107. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
  108. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
  109. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
  110. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
  111. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
  112. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
  113. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
  114. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
  115. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
  116. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
  117. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
  118. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
  119. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
  120. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
  121. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
  122. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
  123. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
  124. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
  125. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
  126. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
  127. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
  128. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
  129. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
  130. package/dist/services/shared/serviceProcessLogger.js +1 -1
  131. package/dist/services/unified-api/server.js +105 -11
  132. package/modules/camo-backend/src/index.ts +46 -1
  133. package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
  134. package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
  135. package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
  136. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
  137. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
  138. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
  139. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
  140. package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
  141. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
  142. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
  143. package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
  144. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
  145. package/modules/collection-manager/bloom-filter.ts +112 -0
  146. package/modules/collection-manager/date-utils.ts +316 -0
  147. package/modules/collection-manager/index.ts +309 -0
  148. package/modules/collection-manager/package.json +10 -0
  149. package/modules/collection-manager/storage.ts +174 -0
  150. package/modules/collection-manager/types.ts +156 -0
  151. package/modules/logging/src/index.ts +1 -1
  152. package/modules/process-registry/index.ts +284 -0
  153. package/modules/rate-limiter/index.ts +322 -0
  154. package/modules/state/src/paths.ts +9 -1
  155. package/modules/task-scheduler/index.ts +293 -0
  156. package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
  157. package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
  158. package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
  159. package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
  160. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
  161. package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
  162. package/modules/workflow/config/workflowRegistry.ts +2 -0
  163. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
  164. package/modules/workflow/src/runner.ts +6 -0
  165. package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
  166. package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
  167. package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
  168. package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
  169. package/package.json +13 -4
  170. package/scripts/postinstall-resources.mjs +62 -0
  171. package/scripts/test/run-coverage.mjs +76 -0
  172. package/scripts/weibo/search.ts +49 -0
  173. package/services/shared/serviceProcessLogger.ts +1 -1
  174. package/services/unified-api/server.ts +98 -12
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import minimist from 'minimist';
3
3
  import { spawn } from 'node:child_process';
4
- import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
4
+ import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync, statSync, rmSync } from 'node:fs';
5
+ import os from 'node:os';
5
6
  import path from 'node:path';
6
7
  import { fileURLToPath } from 'node:url';
7
8
 
8
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const APP_ROOT = path.resolve(__dirname, '../..');
10
+ const APP_ROOT = path.resolve(__dirname, '..');
10
11
  const DIST_MAIN = path.join(APP_ROOT, 'dist', 'main', 'index.mjs');
11
12
 
12
13
  const args = minimist(process.argv.slice(2), {
13
- boolean: ['build', 'install', 'check', 'help', 'headless', 'no-daemon'],
14
- string: ['profile', 'keyword', 'target', 'scenario', 'output'],
14
+ boolean: ['build', 'install', 'check', 'help', 'headless', 'no-daemon', 'dry-run', 'no-dry-run', 'parallel', 'do-likes'],
15
+ string: ['profile', 'profiles', 'keyword', 'target', 'scenario', 'output', 'concurrency', 'like-keywords', 'max-likes'],
15
16
  alias: { h: 'help', p: 'profile', k: 'keyword', t: 'target', o: 'output' }
16
17
  });
17
18
 
@@ -27,6 +28,7 @@ Test Scenarios:
27
28
  account-flow - Account creation/login flow test
28
29
  config-save - Config save/load test
29
30
  crawl-run - Full crawl flow test
31
+ full-cover - Real end-to-end UI control + status coverage (no mock)
30
32
 
31
33
  Options:
32
34
  --check Check build/dep status only
@@ -35,13 +37,54 @@ Options:
35
37
  --no-daemon Run in foreground mode
36
38
  --scenario Test scenario name
37
39
  --profile Test profile ID
40
+ --profiles Test profile IDs (comma-separated)
38
41
  --keyword Test keyword
39
42
  --target Target count
43
+ --like-keywords Like keyword filter
44
+ --do-likes Enable likes
45
+ --max-likes Max likes per note (0=unlimited)
46
+ --parallel Enable parallel sharding
47
+ --concurrency Parallel concurrency
40
48
  --headless Headless mode
41
49
  --output Output report path
42
50
  `);
43
51
  }
44
52
 
53
+ function resolveDownloadRoot() {
54
+ const fromEnv = String(process.env.WEBAUTO_DOWNLOAD_ROOT || process.env.WEBAUTO_DOWNLOAD_DIR || '').trim();
55
+ if (fromEnv) return path.resolve(fromEnv);
56
+ if (process.platform === 'win32') {
57
+ try {
58
+ if (existsSync('D:\\')) return 'D:\\webauto';
59
+ } catch {
60
+ // ignore
61
+ }
62
+ return path.join(os.homedir(), '.webauto');
63
+ }
64
+ return path.join(os.homedir(), '.webauto', 'download');
65
+ }
66
+
67
+ function findLatestSummary(keyword) {
68
+ const root = resolveDownloadRoot();
69
+ const safeKeyword = String(keyword || '').trim();
70
+ if (!safeKeyword) return null;
71
+ const keywordDir = path.join(root, 'xiaohongshu', 'debug', safeKeyword);
72
+ const mergedDir = path.join(keywordDir, 'merged');
73
+ if (!existsSync(mergedDir)) return null;
74
+ const entries = readdirSync(mergedDir, { withFileTypes: true })
75
+ .filter((e) => e.isDirectory() && e.name.startsWith('run-'))
76
+ .map((e) => {
77
+ const full = path.join(mergedDir, e.name);
78
+ return { full, mtime: statSync(full).mtimeMs };
79
+ })
80
+ .sort((a, b) => b.mtime - a.mtime);
81
+ for (const entry of entries) {
82
+ const summaryPath = path.join(entry.full, 'summary.json');
83
+ if (existsSync(summaryPath)) return summaryPath;
84
+ }
85
+ return null;
86
+ }
87
+
45
88
  function checkBuildStatus() {
46
89
  return existsSync(DIST_MAIN);
47
90
  }
@@ -83,14 +126,23 @@ async function startConsole(noDaemon = false) {
83
126
  }
84
127
 
85
128
  console.log('[ui-console] Starting Desktop Console...');
129
+ const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
86
130
  const env = { ...process.env };
87
131
  if (noDaemon) env.WEBAUTO_NO_DAEMON = '1';
132
+ const detached = !noDaemon;
133
+ const stdio = detached ? 'ignore' : 'inherit';
88
134
 
89
- const child = spawn('npx', ['electron', DIST_MAIN], {
135
+ const useCmd = process.platform === 'win32';
136
+ const spawnCmd = useCmd ? 'cmd.exe' : npxBin;
137
+ const spawnArgs = useCmd
138
+ ? ['/d', '/s', '/c', npxBin, 'electron', DIST_MAIN]
139
+ : ['electron', DIST_MAIN];
140
+
141
+ const child = spawn(spawnCmd, spawnArgs, {
90
142
  cwd: APP_ROOT,
91
143
  env,
92
- stdio: 'inherit',
93
- detached: !noDaemon
144
+ stdio,
145
+ detached
94
146
  });
95
147
 
96
148
  if (noDaemon) {
@@ -122,30 +174,101 @@ class UITestRunner {
122
174
  this.results.push({ ts, type, message });
123
175
  }
124
176
 
125
- async runCommand(cmd, args, timeoutMs = 30000) {
177
+ async runCommand(cmd, args, timeoutMs = 30000, options = {}) {
126
178
  return new Promise((resolve, reject) => {
127
- const child = spawn(cmd, args, { shell: true, stdio: 'pipe' });
179
+ const env = { ...process.env, ...(options.env || {}) };
180
+ const child = spawn(cmd, args, { shell: false, stdio: 'pipe', cwd: options.cwd || process.cwd(), env });
128
181
  let stdout = '';
129
182
  let stderr = '';
130
- const timer = setTimeout(() => {
131
- child.kill('SIGTERM');
132
- reject(new Error(`Command timeout: ${cmd}`));
133
- }, timeoutMs);
183
+ let timer = null;
184
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
185
+ timer = setTimeout(() => {
186
+ child.kill('SIGTERM');
187
+ reject(new Error(`Command timeout: ${cmd}`));
188
+ }, timeoutMs);
189
+ }
134
190
  child.stdout.on('data', (data) => { stdout += data.toString(); });
135
191
  child.stderr.on('data', (data) => { stderr += data.toString(); });
136
192
  child.on('close', (code) => {
137
- clearTimeout(timer);
193
+ if (timer) clearTimeout(timer);
138
194
  if (code === 0) resolve({ ok: true, stdout, stderr });
139
195
  else reject(new Error(stderr || `Exit code: ${code}`));
140
196
  });
141
197
  });
142
198
  }
143
199
 
200
+ ensure(condition, message) {
201
+ if (!condition) throw new Error(message);
202
+ }
203
+
204
+ parseJsonOutput(output) {
205
+ const text = String(output || '').trim();
206
+ if (!text) return null;
207
+ try {
208
+ return JSON.parse(text);
209
+ } catch {
210
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
211
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
212
+ try {
213
+ return JSON.parse(lines[i]);
214
+ } catch {
215
+ // keep scanning
216
+ }
217
+ }
218
+ }
219
+ return null;
220
+ }
221
+
222
+ async runNodeScript(scriptPath, scriptArgs = [], timeoutMs = 30000, options = {}) {
223
+ const cmd = process.execPath;
224
+ return this.runCommand(cmd, [scriptPath, ...scriptArgs], timeoutMs, options);
225
+ }
226
+
227
+ async runJsonNodeScript(scriptPath, scriptArgs = [], timeoutMs = 30000, options = {}) {
228
+ const out = await this.runNodeScript(scriptPath, scriptArgs, timeoutMs, options);
229
+ const json = this.parseJsonOutput(out.stdout);
230
+ this.ensure(json && typeof json === 'object', `JSON parse failed for ${path.basename(scriptPath)}`);
231
+ return json;
232
+ }
233
+
234
+ async waitForHttp(url, timeoutMs = 20000) {
235
+ const started = Date.now();
236
+ while ((Date.now() - started) < timeoutMs) {
237
+ try {
238
+ const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
239
+ if (res.ok) return true;
240
+ } catch {
241
+ // keep polling
242
+ }
243
+ await new Promise((resolve) => setTimeout(resolve, 300));
244
+ }
245
+ return false;
246
+ }
247
+
248
+ async ensureUnifiedServer() {
249
+ const healthUrl = 'http://127.0.0.1:7701/health';
250
+ const running = await this.waitForHttp(healthUrl, 1200);
251
+ if (running) return { child: null, owned: false };
252
+
253
+ const child = spawn(process.execPath, [path.join(process.cwd(), 'dist/apps/webauto/server.js')], {
254
+ cwd: process.cwd(),
255
+ env: { ...process.env, WEBAUTO_RUNTIME_MODE: 'unified' },
256
+ stdio: 'ignore',
257
+ });
258
+ const ready = await this.waitForHttp(healthUrl, 20000);
259
+ if (!ready) {
260
+ try { child.kill('SIGTERM'); } catch {}
261
+ throw new Error('unified api did not become ready on :7701');
262
+ }
263
+ return { child, owned: true };
264
+ }
265
+
144
266
  async testEnvCheck() {
145
- this.log('Starting environment check test', 'test');
267
+ this.log('Starting environment check test', 'test');
146
268
  try {
147
269
  this.log('Testing: camo CLI');
148
- await this.runCommand('which camo');
270
+ const camoCli = path.join(process.cwd(), 'bin', 'camoufox-cli.mjs');
271
+ await this.runCommand('node', [camoCli, 'help']);
149
272
  this.log('PASS: camo CLI found', 'pass');
150
273
 
151
274
  this.log('Testing: Unified API');
@@ -153,10 +276,13 @@ class UITestRunner {
153
276
  if (!apiRes.ok) throw new Error('Unified API not responding');
154
277
  this.log('PASS: Unified API running', 'pass');
155
278
 
156
- this.log('Testing: Browser Service');
157
- const bsRes = await fetch('http://127.0.0.1:7704/health');
158
- if (!bsRes.ok) throw new Error('Browser Service not responding');
159
- this.log('PASS: Browser Service running', 'pass');
279
+ this.log('Testing: Camo Runtime (optional)');
280
+ const runtimeRes = await fetch('http://127.0.0.1:7704/health');
281
+ if (!runtimeRes.ok) {
282
+ this.log('WARN: Camo Runtime not ready (optional)', 'warn');
283
+ } else {
284
+ this.log('PASS: Camo Runtime running', 'pass');
285
+ }
160
286
 
161
287
  this.log('Environment check PASSED', 'success');
162
288
  return { passed: true };
@@ -192,7 +318,7 @@ class UITestRunner {
192
318
  this.log('Starting config save test', 'test');
193
319
  try {
194
320
  const testConfig = {
195
- keyword: this.keyword, target: this.target, env: 'debug',
321
+ keyword: this.keyword, target: this.target, env: 'prod',
196
322
  fetchBody: true, fetchComments: true, maxComments: 50,
197
323
  autoLike: false, likeKeywords: '', headless: this.headless, dryRun: true
198
324
  };
@@ -215,17 +341,58 @@ class UITestRunner {
215
341
  async testCrawlRun() {
216
342
  this.log('Starting crawl run test', 'test');
217
343
  try {
218
- this.log(`Testing: Dry-run crawl (keyword=${this.keyword}, target=${this.target})`);
219
- const args = [
344
+ const runProfilesRaw = String(args.profiles || '').trim();
345
+ const runProfiles = runProfilesRaw
346
+ ? runProfilesRaw.split(',').map((p) => p.trim()).filter(Boolean)
347
+ : [];
348
+ const profileFlag = runProfiles.length > 0
349
+ ? ['--profiles', runProfiles.join(',')]
350
+ : ['--profile', this.profile];
351
+ this.log(`Testing: Crawl run (keyword=${this.keyword}, target=${this.target})`);
352
+ const env = String(args.env || 'prod').trim() || 'prod';
353
+ const runArgs = [
220
354
  path.join(process.cwd(), 'apps/webauto/entry/xhs-unified.mjs'),
221
- '--profile', this.profile,
355
+ ...profileFlag,
222
356
  '--keyword', this.keyword,
223
357
  '--target', String(this.target),
224
- '--env', 'debug',
225
- '--dry-run', 'true'
358
+ '--env', env,
226
359
  ];
227
- if (this.headless) args.push('--headless', 'true');
228
- await this.runCommand('node', args, 120000);
360
+ if (runProfiles.length > 1 && (args.parallel === true || args.parallel === undefined)) {
361
+ runArgs.push('--parallel', 'true');
362
+ const conc = String(args.concurrency || '').trim();
363
+ if (conc) runArgs.push('--concurrency', conc);
364
+ }
365
+ const likeKeywords = String(args['like-keywords'] || '').trim();
366
+ const doLikes = args['do-likes'] === true || Boolean(likeKeywords);
367
+ if (doLikes) {
368
+ runArgs.push('--do-likes', 'true');
369
+ if (likeKeywords) runArgs.push('--like-keywords', likeKeywords);
370
+ const maxLikes = String(args['max-likes'] || '').trim();
371
+ if (maxLikes) runArgs.push('--max-likes', maxLikes);
372
+ }
373
+ const maxComments = String(args['max-comments'] || '').trim();
374
+ if (maxComments) runArgs.push('--max-comments', maxComments);
375
+ const forceDryRun = args['dry-run'] === true && args['no-dry-run'] !== true;
376
+ const dryFlag = forceDryRun ? '--dry-run' : '--no-dry-run';
377
+ runArgs.push(dryFlag, 'true');
378
+ if (this.headless) runArgs.push('--headless', 'true');
379
+ await this.runCommand('node', runArgs, 0, { env: { WEBAUTO_BUS_EVENTS: '1' } });
380
+ const summaryPath = findLatestSummary(this.keyword);
381
+ if (summaryPath && existsSync(summaryPath)) {
382
+ try {
383
+ const summary = JSON.parse(readFileSync(summaryPath, 'utf8'));
384
+ const totals = summary?.totals || {};
385
+ const profiles = Array.isArray(summary?.profiles) ? summary.profiles : [];
386
+ const reasons = profiles.map((p) => `${p.profileId}:${p.reason || p.stats?.stopReason || 'unknown'}`).join(', ');
387
+ this.log(`Summary: profilesSucceeded=${totals.profilesSucceeded ?? '-'} profilesFailed=${totals.profilesFailed ?? '-'} openedNotes=${totals.openedNotes ?? '-'} operationErrors=${totals.operationErrors ?? '-'} recoveryFailed=${totals.recoveryFailed ?? '-'}`, 'info');
388
+ if (reasons) this.log(`Stop reasons: ${reasons}`, 'info');
389
+ this.log(`Summary path: ${summaryPath}`, 'info');
390
+ } catch (err) {
391
+ this.log(`Summary parse failed: ${err.message || String(err)}`, 'warn');
392
+ }
393
+ } else {
394
+ this.log('Summary not found after crawl run', 'warn');
395
+ }
229
396
  this.log('PASS: Crawl run completed', 'pass');
230
397
  this.log('Crawl run PASSED', 'success');
231
398
  return { passed: true };
@@ -235,6 +402,220 @@ class UITestRunner {
235
402
  }
236
403
  }
237
404
 
405
+ async testFullCover() {
406
+ this.log('Starting full-cover real test (no mock)', 'test');
407
+ const portableRoot = path.join(process.cwd(), '.tmp', `ui-full-cover-${Date.now()}`);
408
+ mkdirSync(portableRoot, { recursive: true });
409
+ const isolatedEnv = { ...process.env, WEBAUTO_PORTABLE_ROOT: portableRoot };
410
+ const accountScript = path.join(process.cwd(), 'apps/webauto/entry/account.mjs');
411
+ const scheduleScript = path.join(process.cwd(), 'apps/webauto/entry/schedule.mjs');
412
+ const xhsInstallScript = path.join(process.cwd(), 'apps/webauto/entry/xhs-install.mjs');
413
+ const xhsStatusScript = path.join(process.cwd(), 'apps/webauto/entry/xhs-status.mjs');
414
+ const runIds = [];
415
+ let unifiedServer = null;
416
+ try {
417
+ this.log('Step 1/4: ensure backend dependencies', 'info');
418
+ await this.runNodeScript(xhsInstallScript, ['--ensure-backend'], 180000);
419
+
420
+ this.log('Step 2/4: account controls (add/get/update/list/delete)', 'info');
421
+ const addRes = await this.runJsonNodeScript(
422
+ accountScript,
423
+ ['add', '--platform', 'xiaohongshu', '--alias', 'ui-e2e', '--status', 'pending', '--json'],
424
+ 90000,
425
+ { env: isolatedEnv },
426
+ );
427
+ const accountId = String(addRes?.account?.id || '').trim();
428
+ this.ensure(accountId, 'account add did not return account id');
429
+ const profileId = String(addRes?.account?.profileId || '').trim();
430
+ this.ensure(profileId, 'account add did not return profile id');
431
+
432
+ const getRes = await this.runJsonNodeScript(accountScript, ['get', accountId, '--json'], 30000, { env: isolatedEnv });
433
+ this.ensure(getRes?.ok === true, 'account get failed');
434
+
435
+ const updateAccountRes = await this.runJsonNodeScript(
436
+ accountScript,
437
+ ['update', accountId, '--alias', 'ui-e2e-updated', '--json'],
438
+ 30000,
439
+ { env: isolatedEnv },
440
+ );
441
+ this.ensure(updateAccountRes?.ok === true, 'account update failed');
442
+
443
+ const listRes = await this.runJsonNodeScript(accountScript, ['list', '--json'], 30000, { env: isolatedEnv });
444
+ this.ensure(Number(listRes?.count || 0) >= 1, 'account list returned empty unexpectedly');
445
+
446
+ const deleteRes = await this.runJsonNodeScript(
447
+ accountScript,
448
+ ['delete', accountId, '--delete-profile', '--delete-fingerprint', '--json'],
449
+ 90000,
450
+ { env: isolatedEnv },
451
+ );
452
+ this.ensure(deleteRes?.ok === true, 'account delete failed');
453
+
454
+ this.log('Step 3/4: scheduler controls (CRUD/import-export/run/daemon)', 'info');
455
+ const runAt = new Date(Date.now() + (60 * 60 * 1000)).toISOString();
456
+ const argvJson = JSON.stringify({
457
+ profile: 'xhs-e2e-profile',
458
+ keyword: this.keyword,
459
+ 'max-notes': Math.max(1, Number(this.target) || 1),
460
+ env: 'debug',
461
+ 'do-comments': false,
462
+ 'do-likes': false,
463
+ 'dry-run': true,
464
+ });
465
+
466
+ const addInterval = await this.runJsonNodeScript(
467
+ scheduleScript,
468
+ ['add', '--name', 'ui-interval', '--schedule-type', 'interval', '--interval-minutes', '5', '--max-runs', '2', '--argv-json', argvJson, '--json'],
469
+ 60000,
470
+ { env: isolatedEnv },
471
+ );
472
+ const addOnce = await this.runJsonNodeScript(
473
+ scheduleScript,
474
+ ['add', '--name', 'ui-once', '--schedule-type', 'once', '--run-at', runAt, '--max-runs', '1', '--argv-json', argvJson, '--json'],
475
+ 60000,
476
+ { env: isolatedEnv },
477
+ );
478
+ const addDaily = await this.runJsonNodeScript(
479
+ scheduleScript,
480
+ ['add', '--name', 'ui-daily', '--schedule-type', 'daily', '--run-at', runAt, '--max-runs', '3', '--argv-json', argvJson, '--json'],
481
+ 60000,
482
+ { env: isolatedEnv },
483
+ );
484
+ const addWeekly = await this.runJsonNodeScript(
485
+ scheduleScript,
486
+ ['add', '--name', 'ui-weekly', '--schedule-type', 'weekly', '--run-at', runAt, '--max-runs', '4', '--argv-json', argvJson, '--json'],
487
+ 60000,
488
+ { env: isolatedEnv },
489
+ );
490
+ const taskIds = [
491
+ String(addInterval?.task?.id || '').trim(),
492
+ String(addOnce?.task?.id || '').trim(),
493
+ String(addDaily?.task?.id || '').trim(),
494
+ String(addWeekly?.task?.id || '').trim(),
495
+ ].filter(Boolean);
496
+ this.ensure(taskIds.length === 4, 'schedule add did not create all 4 tasks');
497
+
498
+ const listTasks = await this.runJsonNodeScript(scheduleScript, ['list', '--json'], 30000, { env: isolatedEnv });
499
+ this.ensure(Number(listTasks?.count || 0) >= 4, 'schedule list count < 4');
500
+
501
+ const firstTaskId = taskIds[0];
502
+ const getTask = await this.runJsonNodeScript(scheduleScript, ['get', firstTaskId, '--json'], 30000, { env: isolatedEnv });
503
+ this.ensure(String(getTask?.task?.id || '') === firstTaskId, 'schedule get did not return target task');
504
+
505
+ const updateTask = await this.runJsonNodeScript(
506
+ scheduleScript,
507
+ ['update', firstTaskId, '--name', 'ui-interval-updated', '--enabled', 'true', '--schedule-type', 'interval', '--interval-minutes', '10', '--max-runs', '5', '--argv-json', argvJson, '--json'],
508
+ 60000,
509
+ { env: isolatedEnv },
510
+ );
511
+ this.ensure(updateTask?.ok === true, 'schedule update failed');
512
+
513
+ const exportAll = await this.runJsonNodeScript(scheduleScript, ['export', '--json'], 30000, { env: isolatedEnv });
514
+ this.ensure(Array.isArray(exportAll?.tasks) && exportAll.tasks.length >= 4, 'schedule export returned no tasks');
515
+
516
+ const importMerge = await this.runJsonNodeScript(
517
+ scheduleScript,
518
+ ['import', '--payload-json', JSON.stringify(exportAll), '--mode', 'merge', '--json'],
519
+ 60000,
520
+ { env: isolatedEnv },
521
+ );
522
+ this.ensure(importMerge?.ok === true, 'schedule import merge failed');
523
+
524
+ const runDue = await this.runJsonNodeScript(scheduleScript, ['run-due', '--limit', '20', '--json'], 120000, { env: isolatedEnv });
525
+ this.ensure(runDue?.ok === true, 'schedule run-due failed');
526
+
527
+ const daemonOnce = await this.runJsonNodeScript(
528
+ scheduleScript,
529
+ ['daemon', '--interval-sec', '5', '--limit', '20', '--once', '--json'],
530
+ 120000,
531
+ { env: isolatedEnv },
532
+ );
533
+ this.ensure(daemonOnce?.ok === true, 'schedule daemon --once failed');
534
+
535
+ for (const taskId of taskIds) {
536
+ const del = await this.runJsonNodeScript(scheduleScript, ['delete', taskId, '--json'], 30000, { env: isolatedEnv });
537
+ this.ensure(del?.ok === true, `schedule delete failed: ${taskId}`);
538
+ }
539
+
540
+ this.log('Step 4/4: real state/status coverage via unified API + xhs-status', 'info');
541
+ unifiedServer = await this.ensureUnifiedServer();
542
+ const runId = `ui-full-${Date.now()}`;
543
+ runIds.push(runId);
544
+
545
+ const createRes = await fetch('http://127.0.0.1:7701/api/v1/tasks', {
546
+ method: 'POST',
547
+ headers: { 'Content-Type': 'application/json' },
548
+ body: JSON.stringify({
549
+ runId,
550
+ profileId: 'ui-cover-profile',
551
+ keyword: this.keyword,
552
+ phase: 'unified',
553
+ status: 'starting',
554
+ progress: { total: 100, processed: 0, failed: 0 },
555
+ }),
556
+ });
557
+ this.ensure(createRes.ok, 'POST /api/v1/tasks failed');
558
+
559
+ const updateRes = await fetch(`http://127.0.0.1:7701/api/v1/tasks/${encodeURIComponent(runId)}/update`, {
560
+ method: 'POST',
561
+ headers: { 'Content-Type': 'application/json' },
562
+ body: JSON.stringify({
563
+ status: 'running',
564
+ progress: { total: 100, processed: 40, failed: 1 },
565
+ stats: { notesProcessed: 40, commentsCollected: 88, likesPerformed: 3, repliesGenerated: 0, imagesDownloaded: 0, ocrProcessed: 0 },
566
+ }),
567
+ });
568
+ this.ensure(updateRes.ok, 'POST /api/v1/tasks/<runId>/update failed');
569
+
570
+ const eventRes = await fetch(`http://127.0.0.1:7701/api/v1/tasks/${encodeURIComponent(runId)}/events`, {
571
+ method: 'POST',
572
+ headers: { 'Content-Type': 'application/json' },
573
+ body: JSON.stringify({
574
+ type: 'autoscript:operation_error',
575
+ data: { runId, message: 'ui_full_cover_simulated_error', ts: new Date().toISOString() },
576
+ }),
577
+ });
578
+ this.ensure(eventRes.ok, 'POST /api/v1/tasks/<runId>/events failed');
579
+
580
+ const statusJson = await this.runJsonNodeScript(
581
+ xhsStatusScript,
582
+ ['--run-id', runId, '--json'],
583
+ 30000,
584
+ );
585
+ this.ensure(statusJson?.ok === true, 'xhs-status returned non-ok');
586
+ this.ensure(Number(statusJson?.summary?.totals?.total || 0) >= 1, 'xhs-status total tasks is 0');
587
+ this.ensure(String(statusJson?.detail?.runId || '') === runId, 'xhs-status detail runId mismatch');
588
+ this.ensure((statusJson?.detail?.errorEvents || []).length >= 1, 'xhs-status errorEvents not populated');
589
+
590
+ this.log('Full-cover real test PASSED', 'success');
591
+ return {
592
+ passed: true,
593
+ portableRoot,
594
+ runIds,
595
+ covered: {
596
+ account: true,
597
+ scheduler: true,
598
+ state: true,
599
+ },
600
+ };
601
+ } catch (err) {
602
+ this.log(`FAILED: ${err.message}`, 'fail');
603
+ return { passed: false, error: err.message, portableRoot, runIds };
604
+ } finally {
605
+ for (const rid of runIds) {
606
+ try {
607
+ await fetch(`http://127.0.0.1:7701/api/v1/tasks/${encodeURIComponent(rid)}`, {
608
+ method: 'DELETE',
609
+ });
610
+ } catch {}
611
+ }
612
+ if (unifiedServer?.owned && unifiedServer?.child) {
613
+ try { unifiedServer.child.kill('SIGTERM'); } catch {}
614
+ }
615
+ try { rmSync(portableRoot, { recursive: true, force: true }); } catch {}
616
+ }
617
+ }
618
+
238
619
  async runScenario(scenario) {
239
620
  this.log(`Running test scenario: ${scenario}`, 'info');
240
621
  let result;
@@ -243,6 +624,7 @@ class UITestRunner {
243
624
  case 'account-flow': result = await this.testAccountFlow(); break;
244
625
  case 'config-save': result = await this.testConfigSave(); break;
245
626
  case 'crawl-run': result = await this.testCrawlRun(); break;
627
+ case 'full-cover': result = await this.testFullCover(); break;
246
628
  default: throw new Error(`Unknown scenario: ${scenario}`);
247
629
  }
248
630
  result.duration = Date.now() - this.startTime;
@@ -284,7 +666,12 @@ async function main() {
284
666
  process.exit(1);
285
667
  }
286
668
  } catch (err) {
287
- console.log(`\n❌ Test ERROR: ${err.message}`);
669
+ const message = err?.message || String(err);
670
+ if (process.platform === 'win32' && message.includes('3221226505')) {
671
+ console.warn(`[ui-console] Ignored spurious exit on Windows: ${message}`);
672
+ process.exit(0);
673
+ }
674
+ console.log(`\n❌ Test ERROR: ${message}`);
288
675
  process.exit(1);
289
676
  }
290
677
  return;