@web-auto/webauto 0.1.4 → 0.1.7

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 +983 -128
  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 +2423 -469
  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 +256 -31
  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,13 +1,13 @@
1
1
  // src/main/index.mts
2
2
  import electron from "electron";
3
3
  import { spawn as spawn2 } from "node:child_process";
4
- import os4 from "node:os";
5
- import path5 from "node:path";
4
+ import os5 from "node:os";
5
+ import path6 from "node:path";
6
6
  import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL3 } from "node:url";
7
- import { mkdirSync, promises as fs3 } from "node:fs";
7
+ import { mkdirSync, promises as fs4 } from "node:fs";
8
8
 
9
9
  // src/main/desktop-settings.mts
10
- import { promises as fs } from "node:fs";
10
+ import { existsSync, promises as fs } from "node:fs";
11
11
  import os from "node:os";
12
12
  import path from "node:path";
13
13
  import { pathToFileURL } from "node:url";
@@ -19,9 +19,26 @@ function resolveHomeDir() {
19
19
  function resolveLegacySettingsPath() {
20
20
  return path.join(resolveHomeDir(), ".webauto", "ui-settings.console.json");
21
21
  }
22
- function resolveDefaultDownloadRoot() {
23
- if (process.platform === "win32") return "D:\\webauto";
24
- return path.join(resolveHomeDir(), ".webauto", "download");
22
+ function hasWindowsDriveD() {
23
+ try {
24
+ return existsSync("D:\\");
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ function normalizeWindowsDefaultRoot(input) {
30
+ const value = String(input || "").trim();
31
+ if (!value) return path.join(resolveHomeDir(), ".webauto");
32
+ if (!/^[dD]:[\\/]/.test(value)) return value;
33
+ if (hasWindowsDriveD()) return value;
34
+ return path.join(resolveHomeDir(), ".webauto");
35
+ }
36
+ function resolveDefaultDownloadRoot(options) {
37
+ const platform = String(options?.platform || process.platform).trim();
38
+ const homeDir = String(options?.homeDir || resolveHomeDir()).trim() || resolveHomeDir();
39
+ if (platform !== "win32") return path.join(homeDir, ".webauto", "download");
40
+ const driveExists = typeof options?.windowsDriveDExists === "boolean" ? options.windowsDriveDExists : hasWindowsDriveD();
41
+ return driveExists ? "D:\\webauto" : path.join(homeDir, ".webauto");
25
42
  }
26
43
  function normalizeAiReplyConfig(raw) {
27
44
  if (!raw || typeof raw !== "object") {
@@ -67,10 +84,12 @@ function normalizeSettings(defaults, input) {
67
84
  }
68
85
  const merged = {
69
86
  unifiedApiUrl: String(input.unifiedApiUrl || defaults.unifiedApiUrl || "http://127.0.0.1:7701"),
70
- browserServiceUrl: String(input.browserServiceUrl || defaults.browserServiceUrl || "http://127.0.0.1:7704"),
87
+ camoRuntimeUrl: String(
88
+ input.camoRuntimeUrl || input.browserServiceUrl || defaults.camoRuntimeUrl || defaults.browserServiceUrl || "http://127.0.0.1:7704"
89
+ ),
71
90
  searchGateUrl: String(input.searchGateUrl || defaults.searchGateUrl || "http://127.0.0.1:7790"),
72
91
  downloadRoot: String(input.downloadRoot || defaults.downloadRoot || resolveDefaultDownloadRoot()),
73
- defaultEnv: String(input.defaultEnv || defaults.defaultEnv || "debug") === "prod" ? "prod" : "debug",
92
+ defaultEnv: String(input.defaultEnv || defaults.defaultEnv || "prod") === "prod" ? "prod" : "debug",
74
93
  defaultKeyword: String(input.defaultKeyword ?? defaults.defaultKeyword ?? ""),
75
94
  defaultTarget: Math.max(1, Math.floor(Number(input.defaultTarget ?? defaults.defaultTarget ?? 20) || 20)),
76
95
  defaultDryRun: Boolean(input.defaultDryRun ?? defaults.defaultDryRun ?? false),
@@ -95,10 +114,30 @@ function normalizeSettings(defaults, input) {
95
114
  profileAliases: aliases,
96
115
  profileColors: normalizeColorMap(input.profileColors ?? defaults.profileColors ?? {}),
97
116
  aiReply: normalizeAiReplyConfig(input.aiReply ?? defaults.aiReply ?? {}),
117
+ envRepairHistory: normalizeRepairHistory(
118
+ input.envRepairHistory ?? defaults.envRepairHistory ?? []
119
+ ),
98
120
  lastCrawlConfig: input.lastCrawlConfig ?? defaults.lastCrawlConfig ?? void 0
99
121
  };
100
122
  return merged;
101
123
  }
124
+ function normalizeRepairHistory(raw) {
125
+ if (!Array.isArray(raw)) return [];
126
+ const out = [];
127
+ for (const item of raw) {
128
+ if (!item || typeof item !== "object") continue;
129
+ const ts = String(item.ts || "").trim();
130
+ const action = String(item.action || "").trim();
131
+ if (!ts || !action) continue;
132
+ out.push({
133
+ ts,
134
+ action,
135
+ ok: Boolean(item.ok),
136
+ detail: String(item.detail || "").trim() || void 0
137
+ });
138
+ }
139
+ return out.slice(-30);
140
+ }
102
141
  function normalizeColorMap(raw) {
103
142
  const out = {};
104
143
  if (!raw || typeof raw !== "object") return out;
@@ -122,13 +161,14 @@ async function readDefaultSettingsFromAppRoot(appRoot) {
122
161
  }
123
162
  const base = {
124
163
  unifiedApiUrl: raw.unifiedApiUrl,
125
- browserServiceUrl: raw.browserServiceUrl,
164
+ camoRuntimeUrl: raw.camoRuntimeUrl || raw.browserServiceUrl,
126
165
  searchGateUrl: raw.searchGateUrl,
127
166
  defaultEnv: raw.defaultEnv,
128
167
  defaultKeyword: raw.defaultKeyword,
129
168
  timeouts: raw.timeouts
130
169
  };
131
- const downloadRoot = typeof raw.downloadRoot === "string" ? String(raw.downloadRoot) : process.platform === "win32" && typeof raw.downloadRootWindows === "string" ? String(raw.downloadRootWindows) : process.platform !== "win32" && typeof raw.downloadRootPosix === "string" ? String(raw.downloadRootPosix) : Array.isArray(raw.downloadRootParts) ? path.join(resolveHomeDir(), ...raw.downloadRootParts.map((x) => String(x))) : resolveDefaultDownloadRoot();
170
+ const downloadRootRaw = typeof raw.downloadRoot === "string" ? String(raw.downloadRoot) : process.platform === "win32" && typeof raw.downloadRootWindows === "string" ? String(raw.downloadRootWindows) : process.platform !== "win32" && typeof raw.downloadRootPosix === "string" ? String(raw.downloadRootPosix) : Array.isArray(raw.downloadRootParts) ? path.join(resolveHomeDir(), ...raw.downloadRootParts.map((x) => String(x))) : resolveDefaultDownloadRoot();
171
+ const downloadRoot = process.platform === "win32" ? normalizeWindowsDefaultRoot(downloadRootRaw) : downloadRootRaw;
132
172
  return normalizeSettings({ ...base, downloadRoot }, {});
133
173
  }
134
174
  async function readLegacySettings() {
@@ -230,6 +270,7 @@ async function importConfigFromFile(filePath) {
230
270
  // src/main/core-daemon-manager.mts
231
271
  import { spawn } from "child_process";
232
272
  import path2 from "path";
273
+ import { existsSync as existsSync2 } from "fs";
233
274
  import { fileURLToPath } from "url";
234
275
  var REPO_ROOT = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "../../../..");
235
276
  var CORE_HEALTH_URLS = ["http://127.0.0.1:7701/health", "http://127.0.0.1:7704/health"];
@@ -243,11 +284,33 @@ function resolveNodeBin() {
243
284
  if (explicit) return explicit;
244
285
  const npmNode = String(process.env.npm_node_execpath || "").trim();
245
286
  if (npmNode) return npmNode;
246
- return process.platform === "win32" ? "node.exe" : "node";
287
+ const fromPath = resolveOnPath(process.platform === "win32" ? ["node.exe", "node.cmd", "node"] : ["node"]);
288
+ if (fromPath) return fromPath;
289
+ return process.execPath;
247
290
  }
248
291
  function resolveNpxBin() {
292
+ const fromPath = resolveOnPath(
293
+ process.platform === "win32" ? ["npx.cmd", "npx.exe", "npx.bat", "npx.ps1"] : ["npx"]
294
+ );
295
+ if (fromPath) return fromPath;
249
296
  return process.platform === "win32" ? "npx.cmd" : "npx";
250
297
  }
298
+ function resolveOnPath(candidates) {
299
+ const pathEnv = process.env.PATH || process.env.Path || "";
300
+ const dirs = pathEnv.split(path2.delimiter).filter(Boolean);
301
+ for (const dir of dirs) {
302
+ for (const name of candidates) {
303
+ const full = path2.join(dir, name);
304
+ if (existsSync2(full)) return full;
305
+ }
306
+ }
307
+ return null;
308
+ }
309
+ function quoteCmdArg(value) {
310
+ if (!value) return '""';
311
+ if (!/[\s"]/u.test(value)) return value;
312
+ return `"${value.replace(/"/g, '""')}"`;
313
+ }
251
314
  async function checkHttpHealth(url) {
252
315
  try {
253
316
  const res = await fetch(url, { signal: AbortSignal.timeout(1e3) });
@@ -292,7 +355,18 @@ async function runNodeScript(scriptPath, timeoutMs) {
292
355
  }
293
356
  async function runCommand(command, args, timeoutMs) {
294
357
  return new Promise((resolve) => {
295
- const child = spawn(command, args, {
358
+ const lower = String(command || "").toLowerCase();
359
+ let spawnCommand2 = command;
360
+ let spawnArgs = args;
361
+ if (process.platform === "win32" && (lower.endsWith(".cmd") || lower.endsWith(".bat"))) {
362
+ spawnCommand2 = "cmd.exe";
363
+ const cmdLine = [quoteCmdArg(command), ...args.map(quoteCmdArg)].join(" ");
364
+ spawnArgs = ["/d", "/s", "/c", cmdLine];
365
+ } else if (process.platform === "win32" && lower.endsWith(".ps1")) {
366
+ spawnCommand2 = "powershell.exe";
367
+ spawnArgs = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command, ...args];
368
+ }
369
+ const child = spawn(spawnCommand2, spawnArgs, {
296
370
  cwd: REPO_ROOT,
297
371
  stdio: "ignore",
298
372
  windowsHide: true,
@@ -325,7 +399,11 @@ async function startCoreDaemon() {
325
399
  console.error("[CoreDaemonManager] Failed to start unified API service");
326
400
  return false;
327
401
  }
328
- const startedBrowser = await runCommand(resolveNpxBin(), ["--yes", "@web-auto/camo", "init"], 4e4);
402
+ const startedBrowser = await runCommand(
403
+ resolveNpxBin(),
404
+ ["--yes", "--package=@web-auto/camo", "camo", "init"],
405
+ 4e4
406
+ );
329
407
  if (!startedBrowser) {
330
408
  console.error("[CoreDaemonManager] Failed to start camo browser backend");
331
409
  return false;
@@ -546,6 +624,10 @@ var StateBridge = class {
546
624
  win = null;
547
625
  tasks = /* @__PURE__ */ new Map();
548
626
  handlersRegistered = false;
627
+ emitBusEvent(payload) {
628
+ if (!this.win) return;
629
+ this.win.webContents.send("bus:event", payload);
630
+ }
549
631
  start(win2) {
550
632
  this.win = win2;
551
633
  this.connect();
@@ -561,12 +643,26 @@ var StateBridge = class {
561
643
  this.ws.on("open", () => {
562
644
  console.log("[StateBridge] connected to", UNIFIED_API_WS);
563
645
  this.ws?.send(JSON.stringify({ type: "subscribe", topic: "task:*" }));
646
+ this.emitBusEvent({ type: "env:unified", ok: true, ts: Date.now() });
564
647
  });
565
648
  this.ws.on("message", (data) => {
566
649
  try {
567
650
  const msg = JSON.parse(data.toString());
568
651
  if (msg.type === "task:update" && msg.data) {
569
652
  this.handleTaskUpdate(msg.data);
653
+ return;
654
+ }
655
+ if (msg.type === "event" && msg.topic === "bus.message") {
656
+ const raw = msg?.payload?.data;
657
+ if (typeof raw === "string" && raw.trim()) {
658
+ try {
659
+ const parsed = JSON.parse(raw);
660
+ if (parsed && typeof parsed === "object") {
661
+ this.emitBusEvent(parsed);
662
+ }
663
+ } catch {
664
+ }
665
+ }
570
666
  }
571
667
  } catch (err) {
572
668
  console.warn("[StateBridge] parse error:", err);
@@ -574,6 +670,7 @@ var StateBridge = class {
574
670
  });
575
671
  this.ws.on("close", () => {
576
672
  console.log("[StateBridge] disconnected, reconnecting...");
673
+ this.emitBusEvent({ type: "env:unified", ok: false, ts: Date.now() });
577
674
  this.scheduleReconnect();
578
675
  });
579
676
  this.ws.on("error", (err) => {
@@ -632,14 +729,29 @@ var StateBridge = class {
632
729
  var stateBridge = new StateBridge();
633
730
 
634
731
  // src/main/env-check.mts
635
- import { promisify } from "node:util";
636
- import { exec, spawnSync } from "node:child_process";
637
- import { existsSync } from "node:fs";
732
+ import { spawnSync } from "node:child_process";
733
+ import { existsSync as existsSync3 } from "node:fs";
638
734
  import path4 from "node:path";
639
735
  import os3 from "node:os";
640
- var execAsync = promisify(exec);
736
+ function resolveWebautoRoot() {
737
+ const portableRoot = String(process.env.WEBAUTO_PORTABLE_ROOT || process.env.WEBAUTO_ROOT || "").trim();
738
+ return portableRoot ? path4.join(portableRoot, ".webauto") : path4.join(os3.homedir(), ".webauto");
739
+ }
641
740
  function resolveNpxBin2() {
642
- return process.platform === "win32" ? "npx.cmd" : "npx";
741
+ if (process.platform !== "win32") return "npx";
742
+ const resolved = resolveOnPath2(["npx.cmd", "npx.exe", "npx.bat", "npx.ps1"]);
743
+ return resolved || "npx.cmd";
744
+ }
745
+ function resolveOnPath2(candidates) {
746
+ const pathEnv = process.env.PATH || process.env.Path || "";
747
+ const dirs = pathEnv.split(path4.delimiter).filter(Boolean);
748
+ for (const dir of dirs) {
749
+ for (const name of candidates) {
750
+ const full = path4.join(dir, name);
751
+ if (existsSync3(full)) return full;
752
+ }
753
+ }
754
+ return null;
643
755
  }
644
756
  function resolveCamoVersionFromText(stdout, stderr) {
645
757
  const merged = `${String(stdout || "")}
@@ -653,13 +765,35 @@ ${String(stderr || "")}`.trim();
653
765
  }
654
766
  return "unknown";
655
767
  }
768
+ function quoteCmdArg2(value) {
769
+ if (!value) return '""';
770
+ if (!/[\s"]/u.test(value)) return value;
771
+ return `"${value.replace(/"/g, '""')}"`;
772
+ }
656
773
  function runVersionCheck(command, args, explicitPath) {
657
774
  try {
658
- const ret = spawnSync(command, args, {
659
- encoding: "utf8",
660
- timeout: 8e3,
661
- windowsHide: true
662
- });
775
+ const lower = String(command || "").toLowerCase();
776
+ let ret;
777
+ if (process.platform === "win32" && (lower.endsWith(".cmd") || lower.endsWith(".bat"))) {
778
+ const cmdLine = [quoteCmdArg2(command), ...args.map(quoteCmdArg2)].join(" ");
779
+ ret = spawnSync("cmd.exe", ["/d", "/s", "/c", cmdLine], {
780
+ encoding: "utf8",
781
+ timeout: 8e3,
782
+ windowsHide: true
783
+ });
784
+ } else if (process.platform === "win32" && lower.endsWith(".ps1")) {
785
+ ret = spawnSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command, ...args], {
786
+ encoding: "utf8",
787
+ timeout: 8e3,
788
+ windowsHide: true
789
+ });
790
+ } else {
791
+ ret = spawnSync(command, args, {
792
+ encoding: "utf8",
793
+ timeout: 8e3,
794
+ windowsHide: true
795
+ });
796
+ }
663
797
  if (ret.status !== 0) {
664
798
  return {
665
799
  installed: false,
@@ -675,20 +809,33 @@ function runVersionCheck(command, args, explicitPath) {
675
809
  return { installed: false, error: String(err) };
676
810
  }
677
811
  }
812
+ function resolvePathFromOutput(stdout) {
813
+ const lines = String(stdout || "").split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
814
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
815
+ const line = lines[i];
816
+ if (line.startsWith("/") || /^[A-Z]:\\/i.test(line)) return line;
817
+ }
818
+ return "";
819
+ }
678
820
  async function checkCamoCli() {
679
- const pathCheck = runVersionCheck(process.platform === "win32" ? "camo.cmd" : "camo", ["help"], "PATH:camo");
680
- if (pathCheck.installed) return pathCheck;
821
+ const camoCandidates = process.platform === "win32" ? ["camo.cmd", "camo.exe", "camo.bat", "camo.ps1"] : ["camo"];
822
+ for (const candidate of camoCandidates) {
823
+ const pathCheck = runVersionCheck(candidate, ["help"], `PATH:${candidate}`);
824
+ if (pathCheck.installed) return pathCheck;
825
+ }
681
826
  const cwd = process.cwd();
682
- const suffix = process.platform === "win32" ? "camo.cmd" : "camo";
683
- const localCandidates = [
684
- path4.resolve(cwd, "node_modules", ".bin", suffix),
685
- path4.resolve(cwd, "..", "node_modules", ".bin", suffix),
686
- path4.resolve(cwd, "..", "..", "node_modules", ".bin", suffix)
827
+ const localRoots = [
828
+ path4.resolve(cwd, "node_modules", ".bin"),
829
+ path4.resolve(cwd, "..", "node_modules", ".bin"),
830
+ path4.resolve(cwd, "..", "..", "node_modules", ".bin")
687
831
  ];
688
- for (const candidate of localCandidates) {
689
- if (!existsSync(candidate)) continue;
690
- const ret = runVersionCheck(candidate, ["help"], candidate);
691
- if (ret.installed) return ret;
832
+ for (const localRoot of localRoots) {
833
+ for (const suffix of camoCandidates) {
834
+ const candidate = path4.resolve(localRoot, suffix);
835
+ if (!existsSync3(candidate)) continue;
836
+ const ret = runVersionCheck(candidate, ["help"], candidate);
837
+ if (ret.installed) return ret;
838
+ }
692
839
  }
693
840
  const npxCheck = runVersionCheck(
694
841
  resolveNpxBin2(),
@@ -702,59 +849,40 @@ async function checkCamoCli() {
702
849
  };
703
850
  }
704
851
  async function checkServices() {
705
- const [unifiedApi, browserService, searchGate] = await Promise.all([
852
+ const [unifiedApi, camoRuntime, searchGate] = await Promise.all([
706
853
  fetch("http://127.0.0.1:7701/health", { signal: AbortSignal.timeout(3e3) }).then((r) => r.ok).catch(() => false),
707
854
  fetch("http://127.0.0.1:7704/health", { signal: AbortSignal.timeout(3e3) }).then((r) => r.ok).catch(() => false),
708
855
  fetch("http://127.0.0.1:7790/health", { signal: AbortSignal.timeout(3e3) }).then((r) => r.ok).catch(() => false)
709
856
  ]);
710
- return { unifiedApi, browserService, searchGate };
857
+ return { unifiedApi, camoRuntime, searchGate };
711
858
  }
712
859
  async function checkFirefox() {
713
- try {
714
- const pythonBin = process.platform === "win32" ? "python" : "python3";
715
- const ret = spawnSync(pythonBin, ["-m", "camoufox", "path"], {
716
- encoding: "utf8",
717
- timeout: 8e3,
718
- windowsHide: true
719
- });
720
- if (ret.status === 0) {
721
- const lines = String(ret.stdout || "").split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
722
- for (let i = lines.length - 1; i >= 0; i -= 1) {
723
- const line = lines[i];
724
- if (line && (line.startsWith("/") || /^[A-Z]:\\/.test(line))) return { installed: true, path: line };
725
- }
726
- return { installed: true };
727
- }
728
- } catch {
729
- }
730
- const platform = process.platform;
731
- try {
732
- if (platform === "win32") {
733
- const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
734
- const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
735
- const localAppData = process.env.LOCALAPPDATA || path4.join(os3.homedir(), "AppData", "Local");
736
- const possiblePaths = [
737
- path4.join(programFiles, "Mozilla Firefox", "firefox.exe"),
738
- path4.join(programFilesX86, "Mozilla Firefox", "firefox.exe"),
739
- path4.join(localAppData, "Mozilla Firefox", "firefox.exe")
740
- ];
741
- for (const firefoxPath2 of possiblePaths) {
742
- if (existsSync(firefoxPath2)) return { installed: true, path: firefoxPath2 };
743
- }
744
- return { installed: false };
860
+ const candidates = process.platform === "win32" ? [
861
+ { command: "python", args: ["-m", "camoufox", "path"] },
862
+ { command: "py", args: ["-3", "-m", "camoufox", "path"] },
863
+ { command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
864
+ ] : [
865
+ { command: "python3", args: ["-m", "camoufox", "path"] },
866
+ { command: resolveNpxBin2(), args: ["--yes", "--package=camoufox", "camoufox", "path"] }
867
+ ];
868
+ for (const candidate of candidates) {
869
+ try {
870
+ const ret = spawnSync(candidate.command, candidate.args, {
871
+ encoding: "utf8",
872
+ timeout: 8e3,
873
+ windowsHide: true
874
+ });
875
+ if (ret.status !== 0) continue;
876
+ const resolvedPath = resolvePathFromOutput(String(ret.stdout || ""));
877
+ return resolvedPath ? { installed: true, path: resolvedPath } : { installed: true };
878
+ } catch {
745
879
  }
746
- const macBundle = "/Applications/Firefox.app/Contents/MacOS/firefox";
747
- if (platform === "darwin" && existsSync(macBundle)) return { installed: true, path: macBundle };
748
- const { stdout } = await execAsync("which firefox", { timeout: 3e3 });
749
- const firefoxPath = String(stdout || "").trim();
750
- return firefoxPath ? { installed: true, path: firefoxPath } : { installed: false };
751
- } catch {
752
- return { installed: false };
753
880
  }
881
+ return { installed: false };
754
882
  }
755
883
  async function checkGeoIP() {
756
- const geoIpPath = path4.join(os3.homedir(), ".webauto", "geoip", "GeoLite2-City.mmdb");
757
- if (existsSync(geoIpPath)) {
884
+ const geoIpPath = path4.join(resolveWebautoRoot(), "geoip", "GeoLite2-City.mmdb");
885
+ if (existsSync3(geoIpPath)) {
758
886
  return { installed: true, path: geoIpPath };
759
887
  }
760
888
  return { installed: false };
@@ -766,32 +894,585 @@ async function checkEnvironment() {
766
894
  checkFirefox(),
767
895
  checkGeoIP()
768
896
  ]);
769
- const allReady = camo.installed && services.unifiedApi && services.browserService && firefox.installed && geoip.installed;
897
+ const allReady = camo.installed && services.unifiedApi && firefox.installed;
770
898
  return { camo, services, firefox, geoip, allReady };
771
899
  }
772
900
 
901
+ // src/main/ui-cli-bridge.mts
902
+ import { createServer } from "node:http";
903
+ import os4 from "node:os";
904
+ import path5 from "node:path";
905
+ import { promises as fs3 } from "node:fs";
906
+ var DEFAULT_HOST = "127.0.0.1";
907
+ var DEFAULT_PORT = 7716;
908
+ var CONTROL_FILE = path5.join(os4.homedir(), ".webauto", "run", "ui-cli.json");
909
+ function readInt(input, fallback) {
910
+ const n = Number(input);
911
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
912
+ }
913
+ function sendJson(res, code, payload) {
914
+ const body = JSON.stringify(payload);
915
+ res.statusCode = code;
916
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
917
+ res.setHeader("Content-Length", Buffer.byteLength(body));
918
+ res.end(body);
919
+ }
920
+ function parseBody(req) {
921
+ return new Promise((resolve) => {
922
+ const chunks = [];
923
+ req.on("data", (c) => chunks.push(c));
924
+ req.on("end", () => {
925
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
926
+ if (!raw) return resolve({});
927
+ try {
928
+ resolve(JSON.parse(raw));
929
+ } catch {
930
+ resolve({});
931
+ }
932
+ });
933
+ req.on("error", () => resolve({}));
934
+ });
935
+ }
936
+ function isUiReady(win2) {
937
+ if (!win2 || win2.isDestroyed()) return false;
938
+ const wc = win2.webContents;
939
+ if (!wc || wc.isDestroyed()) return false;
940
+ if (typeof wc.isCrashed === "function" && wc.isCrashed()) return false;
941
+ return true;
942
+ }
943
+ function toActionError(input, error, extra = {}) {
944
+ const action = String(input?.action || "").trim();
945
+ const selector = String(input?.selector || "").trim();
946
+ const state = String(input?.state || "").trim();
947
+ const payload = {
948
+ ok: false,
949
+ error: String(error || "unknown_error"),
950
+ action: action || null,
951
+ selector: selector || null,
952
+ state: state || null,
953
+ ...extra
954
+ };
955
+ return payload;
956
+ }
957
+ async function writeControlFile(host, port) {
958
+ const payload = {
959
+ pid: process.pid,
960
+ host,
961
+ port,
962
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
963
+ };
964
+ try {
965
+ await fs3.mkdir(path5.dirname(CONTROL_FILE), { recursive: true });
966
+ await fs3.writeFile(CONTROL_FILE, JSON.stringify(payload, null, 2), "utf8");
967
+ } catch {
968
+ }
969
+ }
970
+ async function removeControlFile() {
971
+ try {
972
+ await fs3.unlink(CONTROL_FILE);
973
+ } catch {
974
+ }
975
+ }
976
+ function buildSnapshotScript() {
977
+ return `(() => {
978
+ const text = (sel) => {
979
+ const el = document.querySelector(sel);
980
+ return el ? String(el.textContent || '').trim() : '';
981
+ };
982
+ const value = (sel) => {
983
+ const el = document.querySelector(sel);
984
+ if (!el) return '';
985
+ if ('value' in el) return String(el.value ?? '');
986
+ return String(el.textContent || '').trim();
987
+ };
988
+ const activeTab = document.querySelector('.tab.active');
989
+ const errors = Array.from(document.querySelectorAll('#recent-errors-list li'))
990
+ .map((el) => String(el.textContent || '').trim())
991
+ .filter(Boolean)
992
+ .slice(0, 20);
993
+ return {
994
+ ready: true,
995
+ activeTabId: String(activeTab?.dataset?.tabId || '').trim(),
996
+ activeTabLabel: String(activeTab?.textContent || '').trim(),
997
+ status: text('#status'),
998
+ runId: text('#run-id-text'),
999
+ errorCount: text('#error-count-text'),
1000
+ currentPhase: text('#current-phase'),
1001
+ currentAction: text('#current-action'),
1002
+ progressPercent: text('#progress-percent'),
1003
+ keyword: value('#keyword-input'),
1004
+ target: value('#target-input'),
1005
+ account: value('#account-select'),
1006
+ env: value('#env-select'),
1007
+ recentErrors: errors,
1008
+ ts: new Date().toISOString(),
1009
+ };
1010
+ })()`;
1011
+ }
1012
+ function buildActionScript(action) {
1013
+ const payloadJson = JSON.stringify(action);
1014
+ const snapshotScript = buildSnapshotScript();
1015
+ return `(() => {
1016
+ const payload = ${payloadJson};
1017
+ const normalize = (v) => String(v || '').trim();
1018
+ const isVisible = (el) => {
1019
+ if (!el) return false;
1020
+ const rect = el.getBoundingClientRect();
1021
+ if (!rect || rect.width <= 0 || rect.height <= 0) return false;
1022
+ const style = window.getComputedStyle(el);
1023
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
1024
+ };
1025
+ const query = (selector) => {
1026
+ const s = normalize(selector);
1027
+ if (!s) return null;
1028
+ return document.querySelector(s);
1029
+ };
1030
+ const queryAll = (selector) => {
1031
+ const s = normalize(selector) || 'body';
1032
+ return Array.from(document.querySelectorAll(s));
1033
+ };
1034
+ const findByText = ({ selector, text, exact, nth }) => {
1035
+ const q = normalize(selector) || 'button';
1036
+ const target = normalize(text);
1037
+ const lower = target.toLowerCase();
1038
+ if (!target) return null;
1039
+ const nodes = Array.from(document.querySelectorAll(q));
1040
+ const matched = nodes.filter((el) => {
1041
+ const t = normalize(el.textContent);
1042
+ if (!t) return false;
1043
+ if (exact === true) return t === target;
1044
+ return t.toLowerCase().includes(lower);
1045
+ });
1046
+ const index = Number.isFinite(Number(nth)) ? Math.max(0, Math.floor(Number(nth))) : 0;
1047
+ return matched[index] || null;
1048
+ };
1049
+ const getElementDetails = (el) => {
1050
+ if (!el) return null;
1051
+ const rect = el.getBoundingClientRect();
1052
+ const style = window.getComputedStyle(el);
1053
+ const attrs = {};
1054
+ for (const attr of el.attributes) {
1055
+ attrs[attr.name] = attr.value;
1056
+ }
1057
+ return {
1058
+ rect: {
1059
+ x: Math.round(rect.x),
1060
+ y: Math.round(rect.y),
1061
+ width: Math.round(rect.width),
1062
+ height: Math.round(rect.height),
1063
+ top: Math.round(rect.top),
1064
+ left: Math.round(rect.left),
1065
+ right: Math.round(rect.right),
1066
+ bottom: Math.round(rect.bottom),
1067
+ },
1068
+ computedStyle: {
1069
+ display: style.display,
1070
+ visibility: style.visibility,
1071
+ opacity: style.opacity,
1072
+ backgroundColor: style.backgroundColor,
1073
+ color: style.color,
1074
+ fontSize: style.fontSize,
1075
+ fontFamily: style.fontFamily,
1076
+ position: style.position,
1077
+ zIndex: style.zIndex,
1078
+ },
1079
+ attributes: attrs,
1080
+ innerText: el.innerText,
1081
+ outerHTML: el.outerHTML?.slice(0, 2000),
1082
+ tagName: el.tagName,
1083
+ className: el.className,
1084
+ id: el.id,
1085
+ };
1086
+ };
1087
+ const focusEl = (el) => {
1088
+ if (!el || typeof el.focus !== 'function') return false;
1089
+ el.focus();
1090
+ return document.activeElement === el;
1091
+ };
1092
+ const clickEl = (el) => {
1093
+ if (!el || typeof el.click !== 'function') return false;
1094
+ if (typeof el.scrollIntoView === 'function') {
1095
+ try { el.scrollIntoView({ block: 'center', inline: 'nearest' }); } catch {}
1096
+ }
1097
+ focusEl(el);
1098
+ el.click();
1099
+ return true;
1100
+ };
1101
+ const setInputValue = (el, value) => {
1102
+ if (!el) return false;
1103
+ const text = String(value ?? '');
1104
+ if ('value' in el) {
1105
+ el.value = text;
1106
+ } else {
1107
+ el.textContent = text;
1108
+ }
1109
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1110
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1111
+ return true;
1112
+ };
1113
+ const pressKey = (el, key) => {
1114
+ const k = normalize(key) || 'Enter';
1115
+ const target = el || document.activeElement || document.body;
1116
+ const code = k === 'Escape' ? 'Escape' : k === 'Enter' ? 'Enter' : k;
1117
+ const init = { key: k, code, bubbles: true, cancelable: true };
1118
+ target.dispatchEvent(new KeyboardEvent('keydown', init));
1119
+ target.dispatchEvent(new KeyboardEvent('keyup', init));
1120
+ return true;
1121
+ };
1122
+ const findTab = () => {
1123
+ const tabId = normalize(payload.tabId || payload.value);
1124
+ const tabLabel = normalize(payload.tabLabel || payload.selector);
1125
+ const tabs = Array.from(document.querySelectorAll('.tab'));
1126
+ if (tabId) {
1127
+ const byId = tabs.find((el) => normalize(el?.dataset?.tabId) === tabId);
1128
+ if (byId) return byId;
1129
+ }
1130
+ if (tabLabel) {
1131
+ const lower = tabLabel.toLowerCase();
1132
+ return tabs.find((el) => normalize(el.textContent).toLowerCase().includes(lower)) || null;
1133
+ }
1134
+ return null;
1135
+ };
1136
+
1137
+ if (payload.action === 'snapshot') {
1138
+ return { ok: true, snapshot: ${snapshotScript} };
1139
+ }
1140
+ if (payload.action === 'dialogs') {
1141
+ const mode = normalize(payload.value).toLowerCase();
1142
+ const w = window;
1143
+ const key = '__webauto_ui_cli_dialogs__';
1144
+ if (mode === 'silent') {
1145
+ if (!w[key]) {
1146
+ w[key] = {
1147
+ alert: w.alert,
1148
+ confirm: w.confirm,
1149
+ prompt: w.prompt,
1150
+ };
1151
+ }
1152
+ w.alert = () => {};
1153
+ w.confirm = () => true;
1154
+ w.prompt = () => '';
1155
+ return { ok: true, mode: 'silent' };
1156
+ }
1157
+ if (mode === 'restore') {
1158
+ if (w[key]) {
1159
+ w.alert = w[key].alert;
1160
+ w.confirm = w[key].confirm;
1161
+ w.prompt = w[key].prompt;
1162
+ delete w[key];
1163
+ }
1164
+ return { ok: true, mode: 'restore' };
1165
+ }
1166
+ return { ok: false, error: 'unsupported_dialog_mode' };
1167
+ }
1168
+ if (payload.action === 'tab') {
1169
+ const tab = findTab();
1170
+ if (!tab) return { ok: false, error: 'tab_not_found' };
1171
+ clickEl(tab);
1172
+ return { ok: true, tab: normalize(tab.textContent), tabId: normalize(tab?.dataset?.tabId) };
1173
+ }
1174
+ if (payload.action === 'click') {
1175
+ const el = query(payload.selector);
1176
+ if (!el) return { ok: false, error: 'selector_not_found', selector: normalize(payload.selector) };
1177
+ clickEl(el);
1178
+ return { ok: true };
1179
+ }
1180
+ if (payload.action === 'focus') {
1181
+ const el = query(payload.selector);
1182
+ if (!el) return { ok: false, error: 'selector_not_found', selector: normalize(payload.selector) };
1183
+ const focused = focusEl(el);
1184
+ return { ok: focused, focused };
1185
+ }
1186
+ if (payload.action === 'input') {
1187
+ const el = query(payload.selector);
1188
+ if (!el) return { ok: false, error: 'selector_not_found', selector: normalize(payload.selector) };
1189
+ focusEl(el);
1190
+ const written = setInputValue(el, payload.value || '');
1191
+ return { ok: written, value: String(payload.value || '') };
1192
+ }
1193
+ if (payload.action === 'select') {
1194
+ const el = query(payload.selector);
1195
+ if (!el || el.tagName !== 'SELECT') return { ok: false, error: 'select_not_found', selector: normalize(payload.selector) };
1196
+ el.value = String(payload.value || '');
1197
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1198
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1199
+ return { ok: true, value: el.value };
1200
+ }
1201
+ if (payload.action === 'press') {
1202
+ const el = query(payload.selector);
1203
+ const ok = pressKey(el, payload.key);
1204
+ return { ok, key: normalize(payload.key) || 'Enter' };
1205
+ }
1206
+ if (payload.action === 'click_text') {
1207
+ const el = findByText({
1208
+ selector: payload.selector,
1209
+ text: payload.text || payload.value,
1210
+ exact: payload.exact === true,
1211
+ nth: payload.nth,
1212
+ });
1213
+ if (!el) return { ok: false, error: 'text_not_found', text: normalize(payload.text || payload.value), selector: normalize(payload.selector) };
1214
+ clickEl(el);
1215
+ return { ok: true, text: normalize(el.textContent) };
1216
+ }
1217
+ if (payload.action === 'probe') {
1218
+ const selector = normalize(payload.selector) || 'body';
1219
+ const nodes = queryAll(selector);
1220
+ const first = nodes[0] || null;
1221
+ const firstVisible = isVisible(first);
1222
+ const text = normalize(first?.textContent);
1223
+ const value = first && 'value' in first ? String(first.value ?? '') : text;
1224
+ const checked = Boolean(first && 'checked' in first && first.checked === true);
1225
+ const disabled = Boolean(first && 'disabled' in first && first.disabled === true);
1226
+ const probeText = normalize(payload.text || payload.value);
1227
+ let details = null;
1228
+ if (first && payload.detailed === true) {
1229
+ details = getElementDetails(first);
1230
+ }
1231
+ let textMatchedCount = 0;
1232
+ if (probeText) {
1233
+ const target = payload.exact === true ? probeText : probeText.toLowerCase();
1234
+ textMatchedCount = nodes.filter((el) => {
1235
+ const current = normalize(el.textContent);
1236
+ if (!current) return false;
1237
+ if (payload.exact === true) return current === target;
1238
+ return current.toLowerCase().includes(target);
1239
+ }).length;
1240
+ }
1241
+ return {
1242
+ ok: true,
1243
+ selector,
1244
+ exists: Boolean(first),
1245
+ count: nodes.length,
1246
+ visible: firstVisible,
1247
+ text,
1248
+ value,
1249
+ checked,
1250
+ disabled,
1251
+ tagName: first?.tagName || '',
1252
+ className: first?.className || '',
1253
+ details,
1254
+ textMatchedCount,
1255
+ };
1256
+ }
1257
+ if (payload.action === 'close_window') {
1258
+ window.close();
1259
+ return { ok: true };
1260
+ }
1261
+ return { ok: false, error: 'unsupported_action', action: normalize(payload.action) };
1262
+ })()`;
1263
+ }
1264
+ var UiCliBridge = class {
1265
+ server = null;
1266
+ options;
1267
+ host;
1268
+ port;
1269
+ constructor(options) {
1270
+ this.options = options;
1271
+ this.host = String(options.host || process.env.WEBAUTO_UI_CLI_HOST || DEFAULT_HOST);
1272
+ this.port = readInt(options.port || process.env.WEBAUTO_UI_CLI_PORT, DEFAULT_PORT);
1273
+ }
1274
+ getAddress() {
1275
+ return { host: this.host, port: this.port };
1276
+ }
1277
+ async start() {
1278
+ if (this.server) return this.getAddress();
1279
+ await new Promise((resolve, reject) => {
1280
+ const server = createServer((req, res) => {
1281
+ void this.handleRequest(req, res);
1282
+ });
1283
+ server.on("error", (err) => reject(err));
1284
+ server.listen(this.port, this.host, () => {
1285
+ this.server = server;
1286
+ resolve();
1287
+ });
1288
+ });
1289
+ await writeControlFile(this.host, this.port);
1290
+ return this.getAddress();
1291
+ }
1292
+ async stop() {
1293
+ if (!this.server) {
1294
+ await removeControlFile();
1295
+ return;
1296
+ }
1297
+ const srv = this.server;
1298
+ this.server = null;
1299
+ await new Promise((resolve) => srv.close(() => resolve()));
1300
+ await removeControlFile();
1301
+ }
1302
+ async handleRequest(req, res) {
1303
+ const method = String(req.method || "GET").toUpperCase();
1304
+ const url = new URL(req.url || "/", `http://${this.host}:${this.port}`);
1305
+ if (method === "GET" && url.pathname === "/health") {
1306
+ return sendJson(res, 200, await this.status());
1307
+ }
1308
+ if (method === "GET" && (url.pathname === "/status" || url.pathname === "/snapshot")) {
1309
+ return sendJson(res, 200, await this.status(true));
1310
+ }
1311
+ if (method === "POST" && url.pathname === "/action") {
1312
+ const body = await parseBody(req);
1313
+ const result = await this.handleAction(body || {});
1314
+ return sendJson(res, result.ok ? 200 : 400, result);
1315
+ }
1316
+ return sendJson(res, 404, { ok: false, error: "not_found" });
1317
+ }
1318
+ async status(includeSnapshot = false) {
1319
+ const win2 = this.options.getWindow();
1320
+ const ready = isUiReady(win2);
1321
+ if (!ready) {
1322
+ return {
1323
+ ok: false,
1324
+ pid: process.pid,
1325
+ ready: false,
1326
+ host: this.host,
1327
+ port: this.port,
1328
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1329
+ error: "window_not_ready"
1330
+ };
1331
+ }
1332
+ let snapshot;
1333
+ if (includeSnapshot) {
1334
+ try {
1335
+ snapshot = await win2.webContents.executeJavaScript(buildSnapshotScript(), true);
1336
+ } catch (err) {
1337
+ return {
1338
+ ok: false,
1339
+ pid: process.pid,
1340
+ ready,
1341
+ host: this.host,
1342
+ port: this.port,
1343
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1344
+ error: err?.message || String(err)
1345
+ };
1346
+ }
1347
+ }
1348
+ return {
1349
+ ok: true,
1350
+ pid: process.pid,
1351
+ ready,
1352
+ host: this.host,
1353
+ port: this.port,
1354
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1355
+ snapshot
1356
+ };
1357
+ }
1358
+ async handleAction(input) {
1359
+ const action = String(input?.action || "").trim();
1360
+ if (!action) return toActionError(input, "missing_action");
1361
+ if (action === "wait") {
1362
+ return this.waitForSelector(input);
1363
+ }
1364
+ const win2 = this.options.getWindow();
1365
+ if (!isUiReady(win2)) return toActionError(input, "window_not_ready");
1366
+ try {
1367
+ const out = await win2.webContents.executeJavaScript(buildActionScript(input), true);
1368
+ return out && typeof out === "object" ? out : toActionError(input, "empty_result");
1369
+ } catch (err) {
1370
+ return toActionError(input, err?.message || String(err), { details: err?.stack || null });
1371
+ }
1372
+ }
1373
+ async waitForSelector(input) {
1374
+ const selector = String(input.selector || "").trim();
1375
+ if (!selector) return toActionError(input, "missing_selector");
1376
+ const expected = input.state || "visible";
1377
+ const timeoutMs = readInt(input.timeoutMs, 15e3);
1378
+ const intervalMs = readInt(input.intervalMs, 250);
1379
+ const startedAt = Date.now();
1380
+ while (Date.now() - startedAt <= timeoutMs) {
1381
+ const win2 = this.options.getWindow();
1382
+ if (!isUiReady(win2)) return toActionError(input, "window_not_ready");
1383
+ try {
1384
+ const checkScript = `(() => {
1385
+ const el = document.querySelector(${JSON.stringify(selector)});
1386
+ const visible = (() => {
1387
+ if (!el) return false;
1388
+ const rect = el.getBoundingClientRect();
1389
+ if (!rect || rect.width <= 0 || rect.height <= 0) return false;
1390
+ const style = window.getComputedStyle(el);
1391
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
1392
+ })();
1393
+ const text = String(el?.textContent || '').trim();
1394
+ const value = el && 'value' in el ? String(el.value ?? '') : '';
1395
+ const disabled = Boolean(el && 'disabled' in el && el.disabled === true);
1396
+ return { exists: Boolean(el), visible, text, value, disabled };
1397
+ })()`;
1398
+ const state = await win2.webContents.executeJavaScript(checkScript, true);
1399
+ const exists = Boolean(state?.exists);
1400
+ const visible = Boolean(state?.visible);
1401
+ const text = String(state?.text || "");
1402
+ const value = String(state?.value || "");
1403
+ const disabled = Boolean(state?.disabled);
1404
+ let matched = false;
1405
+ let reason = "";
1406
+ switch (expected) {
1407
+ case "exists":
1408
+ matched = exists;
1409
+ reason = exists ? "element exists" : "element not found";
1410
+ break;
1411
+ case "visible":
1412
+ matched = visible;
1413
+ reason = visible ? "element visible" : "element not visible";
1414
+ break;
1415
+ case "hidden":
1416
+ matched = !exists || !visible;
1417
+ reason = matched ? "element hidden or absent" : "element is visible";
1418
+ break;
1419
+ case "text_contains":
1420
+ matched = exists && text.includes(String(input.value || ""));
1421
+ reason = matched ? "text matched" : `text '${text}' does not contain '${input.value || ""}'`;
1422
+ break;
1423
+ case "text_equals":
1424
+ matched = exists && text === String(input.value || "");
1425
+ reason = matched ? "text matched" : `text '${text}' !== '${input.value || ""}'`;
1426
+ break;
1427
+ case "value_equals":
1428
+ matched = exists && value === String(input.value || "");
1429
+ reason = matched ? "value matched" : `value '${value}' !== '${input.value || ""}'`;
1430
+ break;
1431
+ case "not_disabled":
1432
+ matched = exists && !disabled;
1433
+ reason = matched ? "element enabled" : "element disabled";
1434
+ break;
1435
+ default:
1436
+ return toActionError(input, "unsupported_state", { expected });
1437
+ }
1438
+ if (matched) {
1439
+ return { ok: true, selector, expected, exists, visible, text, value, disabled, elapsedMs: Date.now() - startedAt, reason };
1440
+ }
1441
+ } catch (err) {
1442
+ return toActionError(input, err?.message || String(err), { details: err?.stack || null });
1443
+ }
1444
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1445
+ }
1446
+ return toActionError(input, "wait_timeout", {
1447
+ expected,
1448
+ timeoutMs,
1449
+ elapsedMs: Date.now() - startedAt
1450
+ });
1451
+ }
1452
+ };
1453
+
773
1454
  // src/main/index.mts
774
1455
  var { app, BrowserWindow: BrowserWindow2, ipcMain: ipcMain2, shell, clipboard } = electron;
775
- var __dirname = path5.dirname(fileURLToPath2(import.meta.url));
776
- var APP_ROOT = path5.resolve(__dirname, "../..");
777
- var REPO_ROOT2 = path5.resolve(APP_ROOT, "../..");
778
- var DESKTOP_HEARTBEAT_FILE = path5.join(
779
- os4.homedir(),
1456
+ var __dirname = path6.dirname(fileURLToPath2(import.meta.url));
1457
+ var APP_ROOT = path6.resolve(__dirname, "../..");
1458
+ var REPO_ROOT2 = path6.resolve(APP_ROOT, "../..");
1459
+ var DESKTOP_HEARTBEAT_FILE = path6.join(
1460
+ os5.homedir(),
780
1461
  ".webauto",
781
1462
  "run",
782
1463
  "desktop-console-heartbeat.json"
783
1464
  );
784
1465
  var profileStore = createProfileStore({ repoRoot: REPO_ROOT2 });
785
- var XHS_SCRIPTS_ROOT = path5.join(REPO_ROOT2, "scripts", "xiaohongshu");
1466
+ var XHS_SCRIPTS_ROOT = path6.join(REPO_ROOT2, "scripts", "xiaohongshu");
786
1467
  var XHS_FULL_COLLECT_RE = /collect-content\.mjs$/;
787
1468
  function configureElectronPaths() {
788
1469
  try {
789
1470
  const downloadRoot = resolveDefaultDownloadRoot();
790
- const normalized = path5.normalize(downloadRoot);
791
- const baseDir = path5.basename(normalized).toLowerCase() === "download" ? path5.dirname(normalized) : normalized;
792
- const userDataRoot = path5.join(baseDir, "desktop-console");
793
- const cacheRoot = path5.join(userDataRoot, "cache");
794
- const gpuCacheRoot = path5.join(cacheRoot, "gpu");
1471
+ const normalized = path6.normalize(downloadRoot);
1472
+ const baseDir = path6.basename(normalized).toLowerCase() === "download" ? path6.dirname(normalized) : normalized;
1473
+ const userDataRoot = path6.join(baseDir, "desktop-console");
1474
+ const cacheRoot = path6.join(userDataRoot, "cache");
1475
+ const gpuCacheRoot = path6.join(cacheRoot, "gpu");
795
1476
  try {
796
1477
  mkdirSync(cacheRoot, { recursive: true });
797
1478
  } catch {
@@ -833,6 +1514,8 @@ var GroupQueue = class {
833
1514
  };
834
1515
  var groupQueues = /* @__PURE__ */ new Map();
835
1516
  var runs = /* @__PURE__ */ new Map();
1517
+ var trackedRunPids = /* @__PURE__ */ new Set();
1518
+ var appExitCleanupPromise = null;
836
1519
  var UI_HEARTBEAT_TIMEOUT_MS = resolveUiHeartbeatTimeoutMs(process.env);
837
1520
  var lastUiHeartbeatAt = Date.now();
838
1521
  var heartbeatWatchdog = null;
@@ -849,8 +1532,8 @@ async function writeCoreServiceHeartbeat(status) {
849
1532
  source: "desktop-console"
850
1533
  };
851
1534
  try {
852
- await fs3.mkdir(path5.dirname(filePath), { recursive: true });
853
- await fs3.writeFile(filePath, JSON.stringify(payload), "utf8");
1535
+ await fs4.mkdir(path6.dirname(filePath), { recursive: true });
1536
+ await fs4.writeFile(filePath, JSON.stringify(payload), "utf8");
854
1537
  } catch {
855
1538
  }
856
1539
  }
@@ -887,11 +1570,31 @@ function ensureStateBridge() {
887
1570
  }
888
1571
  }
889
1572
  var win = null;
1573
+ var uiCliBridge = new UiCliBridge({ getWindow: getWin });
890
1574
  configureElectronPaths();
1575
+ var singleInstanceLock = app.requestSingleInstanceLock();
1576
+ if (!singleInstanceLock) {
1577
+ app.quit();
1578
+ }
891
1579
  function getWin() {
892
1580
  if (!win || win.isDestroyed()) return null;
893
1581
  return win;
894
1582
  }
1583
+ if (singleInstanceLock) {
1584
+ app.on("second-instance", () => {
1585
+ const w = getWin();
1586
+ if (w) {
1587
+ if (w.isMinimized()) w.restore();
1588
+ w.focus();
1589
+ return;
1590
+ }
1591
+ if (app.isReady()) {
1592
+ createWindow();
1593
+ } else {
1594
+ app.whenReady().then(() => createWindow());
1595
+ }
1596
+ });
1597
+ }
895
1598
  function isUiOperational() {
896
1599
  const w = getWin();
897
1600
  if (!w) return false;
@@ -951,6 +1654,85 @@ function killAllRuns(reason = "ui_heartbeat_timeout") {
951
1654
  void stopCoreServicesBestEffort(reason);
952
1655
  }
953
1656
  }
1657
+ function trackRunPid(child) {
1658
+ const pid = Number(child?.pid || 0);
1659
+ if (pid > 0) trackedRunPids.add(pid);
1660
+ }
1661
+ function untrackRunPid(child) {
1662
+ const pid = Number(child?.pid || 0);
1663
+ if (pid > 0) trackedRunPids.delete(pid);
1664
+ }
1665
+ async function cleanupTrackedRunPidsBestEffort(reason) {
1666
+ for (const pid of Array.from(trackedRunPids.values())) {
1667
+ try {
1668
+ if (process.platform === "win32") {
1669
+ spawn2("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
1670
+ } else {
1671
+ spawn2("pkill", ["-TERM", "-P", String(pid)], { stdio: "ignore" }).on("error", () => {
1672
+ });
1673
+ process.kill(pid, "SIGTERM");
1674
+ }
1675
+ } catch {
1676
+ }
1677
+ trackedRunPids.delete(pid);
1678
+ }
1679
+ if (trackedRunPids.size > 0) {
1680
+ console.warn(`[desktop-console] residual run pids after cleanup (${reason}): ${trackedRunPids.size}`);
1681
+ trackedRunPids.clear();
1682
+ }
1683
+ }
1684
+ async function cleanupCamoSessionsBestEffort(reason, includeLocks) {
1685
+ const camoCli = path6.join(REPO_ROOT2, "bin", "camoufox-cli.mjs");
1686
+ const invoke = async (args, timeoutMs = 6e4) => runJson({
1687
+ title: `camo ${args.join(" ")}`,
1688
+ cwd: REPO_ROOT2,
1689
+ args: [camoCli, ...args, "--json"],
1690
+ timeoutMs
1691
+ }).catch((err) => ({ ok: false, error: err?.message || String(err) }));
1692
+ const stopAll = await invoke(["stop", "all"]);
1693
+ if (!stopAll?.ok) {
1694
+ console.warn(`[desktop-console] camo stop all failed (${reason})`, stopAll?.error || stopAll?.stderr || stopAll?.stdout || stopAll);
1695
+ }
1696
+ if (!includeLocks) return;
1697
+ const cleanupLocks = await invoke(["cleanup", "locks"]);
1698
+ if (!cleanupLocks?.ok) {
1699
+ console.warn(`[desktop-console] camo cleanup locks failed (${reason})`, cleanupLocks?.error || cleanupLocks?.stderr || cleanupLocks?.stdout || cleanupLocks);
1700
+ }
1701
+ }
1702
+ async function cleanupRuntimeEnvironment(reason, options = {}) {
1703
+ killAllRuns(reason);
1704
+ await cleanupTrackedRunPidsBestEffort(reason);
1705
+ await cleanupCamoSessionsBestEffort(reason, options.includeLockCleanup !== false);
1706
+ if (options.stopUiBridge) {
1707
+ await uiCliBridge.stop().catch(() => null);
1708
+ }
1709
+ if (options.stopHeartbeat) {
1710
+ stopCoreServiceHeartbeat();
1711
+ }
1712
+ if (heartbeatWatchdog) {
1713
+ clearInterval(heartbeatWatchdog);
1714
+ heartbeatWatchdog = null;
1715
+ }
1716
+ if (options.stopCoreServices) {
1717
+ await stopCoreServicesBestEffort(reason);
1718
+ }
1719
+ if (options.stopStateBridge) {
1720
+ stateBridge.stop();
1721
+ }
1722
+ }
1723
+ function ensureAppExitCleanup(reason, options = {}) {
1724
+ if (appExitCleanupPromise) return appExitCleanupPromise;
1725
+ appExitCleanupPromise = cleanupRuntimeEnvironment(reason, {
1726
+ stopUiBridge: true,
1727
+ stopHeartbeat: true,
1728
+ stopCoreServices: true,
1729
+ stopStateBridge: options.stopStateBridge === true,
1730
+ includeLockCleanup: true
1731
+ }).finally(() => {
1732
+ appExitCleanupPromise = null;
1733
+ });
1734
+ return appExitCleanupPromise;
1735
+ }
954
1736
  function ensureHeartbeatWatchdog() {
955
1737
  if (heartbeatWatchdog) return;
956
1738
  heartbeatWatchdog = setInterval(() => {
@@ -1028,13 +1810,13 @@ function resolveNodeBin2() {
1028
1810
  function resolveCwd(input) {
1029
1811
  const raw = String(input || "").trim();
1030
1812
  if (!raw) return REPO_ROOT2;
1031
- return path5.isAbsolute(raw) ? raw : path5.resolve(REPO_ROOT2, raw);
1813
+ return path6.isAbsolute(raw) ? raw : path6.resolve(REPO_ROOT2, raw);
1032
1814
  }
1033
1815
  var cachedStateMod = null;
1034
1816
  async function getStateModule() {
1035
1817
  if (cachedStateMod) return cachedStateMod;
1036
1818
  try {
1037
- const p = path5.join(REPO_ROOT2, "dist", "modules", "state", "src", "xiaohongshu-collect-state.js");
1819
+ const p = path6.join(REPO_ROOT2, "dist", "modules", "state", "src", "xiaohongshu-collect-state.js");
1038
1820
  cachedStateMod = await import(pathToFileURL3(p).href);
1039
1821
  return cachedStateMod;
1040
1822
  } catch {
@@ -1047,6 +1829,34 @@ async function spawnCommand(spec) {
1047
1829
  const groupKey = spec.groupKey || "xiaohongshu";
1048
1830
  const q = getQueue(groupKey);
1049
1831
  const cwd = resolveCwd(spec.cwd);
1832
+ const args = Array.isArray(spec.args) ? spec.args : [];
1833
+ const isXhsRunCommand = args.some((item) => /xhs-(orchestrate|unified)\.mjs$/i.test(String(item || "").replace(/\\/g, "/")));
1834
+ const extractProfilesFromArgs = (argv) => {
1835
+ const out = [];
1836
+ for (let i = 0; i < argv.length; i += 1) {
1837
+ const flag = String(argv[i] || "").trim();
1838
+ if (flag === "--profile" || flag === "--profile-id") {
1839
+ const value = String(argv[i + 1] || "").trim();
1840
+ if (value) out.push(value);
1841
+ } else if (flag === "--profiles") {
1842
+ const value = String(argv[i + 1] || "").trim();
1843
+ if (value) {
1844
+ value.split(",").map((v) => v.trim()).filter(Boolean).forEach((v) => out.push(v));
1845
+ }
1846
+ }
1847
+ }
1848
+ return Array.from(new Set(out));
1849
+ };
1850
+ const requestedProfiles = isXhsRunCommand ? extractProfilesFromArgs(args) : [];
1851
+ if (requestedProfiles.length > 0) {
1852
+ for (const run of runs.values()) {
1853
+ const activeProfiles = Array.isArray(run.profiles) ? run.profiles : [];
1854
+ const conflict = requestedProfiles.find((p) => activeProfiles.includes(p));
1855
+ if (conflict) {
1856
+ throw new Error(`profile already running: ${conflict}`);
1857
+ }
1858
+ }
1859
+ }
1050
1860
  q.enqueue(
1051
1861
  () => new Promise((resolve) => {
1052
1862
  let finished = false;
@@ -1059,7 +1869,7 @@ async function spawnCommand(spec) {
1059
1869
  runs.delete(runId);
1060
1870
  resolve();
1061
1871
  };
1062
- const child = spawn2(resolveNodeBin2(), spec.args, {
1872
+ const child = spawn2(resolveNodeBin2(), args, {
1063
1873
  cwd,
1064
1874
  env: {
1065
1875
  ...process.env,
@@ -1070,7 +1880,8 @@ async function spawnCommand(spec) {
1070
1880
  stdio: ["ignore", "pipe", "pipe"],
1071
1881
  windowsHide: true
1072
1882
  });
1073
- runs.set(runId, { child, title: spec.title, startedAt: now() });
1883
+ trackRunPid(child);
1884
+ runs.set(runId, { child, title: spec.title, startedAt: now(), profiles: requestedProfiles });
1074
1885
  sendEvent({ type: "started", runId, title: spec.title, pid: child.pid ?? -1, ts: now() });
1075
1886
  const stdoutLines = createLineEmitter(runId, "stdout");
1076
1887
  const stderrLines = createLineEmitter(runId, "stderr");
@@ -1089,6 +1900,7 @@ async function spawnCommand(spec) {
1089
1900
  exitSignal = signal;
1090
1901
  });
1091
1902
  child.on("close", (code, signal) => {
1903
+ untrackRunPid(child);
1092
1904
  stdoutLines.flush();
1093
1905
  stderrLines.flush();
1094
1906
  finalize(exitCode ?? code ?? null, exitSignal ?? signal ?? null);
@@ -1134,21 +1946,21 @@ async function runJson(spec) {
1134
1946
  }
1135
1947
  async function scanResults(input) {
1136
1948
  const downloadRoot = String(input.downloadRoot || resolveDefaultDownloadRoot());
1137
- const root = path5.join(downloadRoot, "xiaohongshu");
1949
+ const root = path6.join(downloadRoot, "xiaohongshu");
1138
1950
  const result = { ok: true, root, entries: [] };
1139
1951
  try {
1140
1952
  const stateMod = await getStateModule();
1141
- const envDirs = await fs3.readdir(root, { withFileTypes: true });
1953
+ const envDirs = await fs4.readdir(root, { withFileTypes: true });
1142
1954
  for (const envEnt of envDirs) {
1143
1955
  if (!envEnt.isDirectory()) continue;
1144
1956
  const env = envEnt.name;
1145
- const envPath = path5.join(root, env);
1146
- const keywordDirs = await fs3.readdir(envPath, { withFileTypes: true });
1957
+ const envPath = path6.join(root, env);
1958
+ const keywordDirs = await fs4.readdir(envPath, { withFileTypes: true });
1147
1959
  for (const kwEnt of keywordDirs) {
1148
1960
  if (!kwEnt.isDirectory()) continue;
1149
1961
  const keyword = kwEnt.name;
1150
- const kwPath = path5.join(envPath, keyword);
1151
- const stat = await fs3.stat(kwPath).catch(() => null);
1962
+ const kwPath = path6.join(envPath, keyword);
1963
+ const stat = await fs4.stat(kwPath).catch(() => null);
1152
1964
  let stateSummary = null;
1153
1965
  if (stateMod?.loadXhsCollectState) {
1154
1966
  try {
@@ -1176,13 +1988,13 @@ async function scanResults(input) {
1176
1988
  }
1177
1989
  async function listXhsFullCollectScripts() {
1178
1990
  try {
1179
- const entries = await fs3.readdir(XHS_SCRIPTS_ROOT, { withFileTypes: true });
1991
+ const entries = await fs4.readdir(XHS_SCRIPTS_ROOT, { withFileTypes: true });
1180
1992
  const scripts = entries.filter((ent) => ent.isFile() && XHS_FULL_COLLECT_RE.test(ent.name)).map((ent) => {
1181
1993
  const name = ent.name;
1182
1994
  return {
1183
1995
  id: `xhs:${name}`,
1184
1996
  label: `Full Collect (${name})`,
1185
- path: path5.join(XHS_SCRIPTS_ROOT, name)
1997
+ path: path6.join(XHS_SCRIPTS_ROOT, name)
1186
1998
  };
1187
1999
  });
1188
2000
  return { ok: true, scripts };
@@ -1195,7 +2007,7 @@ async function readTextPreview(input) {
1195
2007
  const maxBytes = typeof input.maxBytes === "number" ? input.maxBytes : 8e4;
1196
2008
  const maxLines = typeof input.maxLines === "number" ? input.maxLines : 200;
1197
2009
  try {
1198
- const raw = await fs3.readFile(filePath, "utf8");
2010
+ const raw = await fs4.readFile(filePath, "utf8");
1199
2011
  const clipped = raw.slice(0, maxBytes);
1200
2012
  const lines = clipped.split(/\r?\n/g).slice(0, maxLines);
1201
2013
  return { ok: true, path: filePath, text: lines.join("\n") };
@@ -1208,14 +2020,14 @@ async function readTextTail(input) {
1208
2020
  const filePath = String(input?.path || "");
1209
2021
  const requestedOffset = typeof input?.fromOffset === "number" ? Math.max(0, Math.floor(input.fromOffset)) : 0;
1210
2022
  const maxBytes = typeof input?.maxBytes === "number" ? Math.max(1024, Math.floor(input.maxBytes)) : 256e3;
1211
- const st = await fs3.stat(filePath);
2023
+ const st = await fs4.stat(filePath);
1212
2024
  const size = Number(st?.size || 0);
1213
2025
  const fromOffset = requestedOffset > size ? 0 : requestedOffset;
1214
2026
  const toRead = Math.max(0, Math.min(maxBytes, size - fromOffset));
1215
2027
  if (toRead <= 0) {
1216
2028
  return { ok: true, path: filePath, text: "", fromOffset, nextOffset: fromOffset, fileSize: size };
1217
2029
  }
1218
- const fh = await fs3.open(filePath, "r");
2030
+ const fh = await fs4.open(filePath, "r");
1219
2031
  try {
1220
2032
  const buf = Buffer.allocUnsafe(toRead);
1221
2033
  const { bytesRead } = await fh.read(buf, 0, toRead, fromOffset);
@@ -1235,7 +2047,7 @@ async function readTextTail(input) {
1235
2047
  async function readFileBase64(input) {
1236
2048
  const filePath = String(input.path || "");
1237
2049
  const maxBytes = typeof input.maxBytes === "number" ? input.maxBytes : 8e6;
1238
- const buf = await fs3.readFile(filePath);
2050
+ const buf = await fs4.readFile(filePath);
1239
2051
  if (buf.byteLength > maxBytes) {
1240
2052
  return { ok: false, error: `file too large: ${buf.byteLength}` };
1241
2053
  }
@@ -1249,14 +2061,14 @@ async function listDir(input) {
1249
2061
  const stack = [root];
1250
2062
  while (stack.length > 0 && entries.length < maxEntries) {
1251
2063
  const dir = stack.pop();
1252
- const items = await fs3.readdir(dir, { withFileTypes: true }).catch(() => []);
2064
+ const items = await fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
1253
2065
  for (const ent of items) {
1254
2066
  if (entries.length >= maxEntries) break;
1255
- const full = path5.join(dir, ent.name);
1256
- const st = await fs3.stat(full).catch(() => null);
2067
+ const full = path6.join(dir, ent.name);
2068
+ const st = await fs4.stat(full).catch(() => null);
1257
2069
  entries.push({
1258
2070
  path: full,
1259
- rel: path5.relative(root, full),
2071
+ rel: path6.relative(root, full),
1260
2072
  name: ent.name,
1261
2073
  isDir: ent.isDirectory(),
1262
2074
  size: st?.size || 0,
@@ -1279,7 +2091,7 @@ function createWindow() {
1279
2091
  minWidth: 920,
1280
2092
  minHeight: 800,
1281
2093
  webPreferences: {
1282
- preload: path5.join(APP_ROOT, "dist", "main", "preload.mjs"),
2094
+ preload: path6.join(APP_ROOT, "dist", "main", "preload.mjs"),
1283
2095
  contextIsolation: true,
1284
2096
  nodeIntegration: false,
1285
2097
  sandbox: false,
@@ -1287,28 +2099,19 @@ function createWindow() {
1287
2099
  backgroundThrottling: false
1288
2100
  }
1289
2101
  });
1290
- const htmlPath = path5.join(APP_ROOT, "dist", "renderer", "index.html");
2102
+ const htmlPath = path6.join(APP_ROOT, "dist", "renderer", "index.html");
1291
2103
  void win.loadFile(htmlPath);
1292
2104
  ensureStateBridge();
1293
2105
  }
1294
2106
  app.on("window-all-closed", () => {
1295
- killAllRuns("window_closed");
2107
+ void ensureAppExitCleanup("window_closed");
1296
2108
  app.quit();
1297
2109
  });
1298
2110
  app.on("before-quit", () => {
1299
- killAllRuns("before_quit");
1300
- stopCoreServiceHeartbeat();
1301
- void stopCoreServicesBestEffort("before_quit");
1302
- if (heartbeatWatchdog) {
1303
- clearInterval(heartbeatWatchdog);
1304
- heartbeatWatchdog = null;
1305
- }
2111
+ void ensureAppExitCleanup("before_quit");
1306
2112
  });
1307
2113
  app.on("will-quit", () => {
1308
- killAllRuns("will_quit");
1309
- stopCoreServiceHeartbeat();
1310
- void stopCoreServicesBestEffort("will_quit");
1311
- stateBridge.stop();
2114
+ void ensureAppExitCleanup("will_quit", { stopStateBridge: true });
1312
2115
  });
1313
2116
  app.whenReady().then(async () => {
1314
2117
  startCoreServiceHeartbeat();
@@ -1319,6 +2122,9 @@ app.whenReady().then(async () => {
1319
2122
  markUiHeartbeat("main_ready");
1320
2123
  ensureHeartbeatWatchdog();
1321
2124
  createWindow();
2125
+ await uiCliBridge.start().catch((err) => {
2126
+ console.warn("[desktop-console] ui-cli bridge start failed", err);
2127
+ });
1322
2128
  });
1323
2129
  ipcMain2.on("preload:test", () => {
1324
2130
  console.log("[preload-test] window.api OK");
@@ -1468,9 +2274,58 @@ ipcMain2.handle("env:checkGeoIP", async () => checkGeoIP());
1468
2274
  ipcMain2.handle("env:checkAll", async () => checkEnvironment());
1469
2275
  ipcMain2.handle("env:repairCore", async () => {
1470
2276
  const ok = await startCoreDaemon().catch(() => false);
1471
- const services = await checkServices().catch(() => ({ unifiedApi: false, browserService: false }));
2277
+ const services = await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }));
1472
2278
  return { ok, services };
1473
2279
  });
2280
+ ipcMain2.handle("env:repairDeps", async (_evt, input) => {
2281
+ const wantCore = Boolean(input?.core);
2282
+ const wantBrowser = Boolean(input?.browser);
2283
+ const wantGeoip = Boolean(input?.geoip);
2284
+ const wantReinstall = Boolean(input?.reinstall);
2285
+ const wantUninstall = Boolean(input?.uninstall);
2286
+ const result = { ok: true, core: null, install: null, env: null };
2287
+ if (wantCore) {
2288
+ const coreOk = await startCoreDaemon().catch(() => false);
2289
+ result.core = {
2290
+ ok: coreOk,
2291
+ services: await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }))
2292
+ };
2293
+ if (!coreOk) result.ok = false;
2294
+ }
2295
+ if (wantBrowser || wantGeoip) {
2296
+ const args = [path6.join("apps", "webauto", "entry", "xhs-install.mjs")];
2297
+ if (wantReinstall) args.push("--reinstall");
2298
+ else if (wantUninstall) args.push("--uninstall");
2299
+ else args.push("--install");
2300
+ if (wantBrowser) args.push("--download-browser");
2301
+ if (wantGeoip) args.push("--download-geoip");
2302
+ if (!wantUninstall) args.push("--ensure-backend");
2303
+ const installRes = await runJson({
2304
+ title: "env repair deps",
2305
+ cwd: REPO_ROOT2,
2306
+ args,
2307
+ timeoutMs: 3e5
2308
+ }).catch((err) => ({ ok: false, error: err?.message || String(err) }));
2309
+ result.install = installRes;
2310
+ if (!installRes?.ok) result.ok = false;
2311
+ }
2312
+ result.env = await checkEnvironment().catch(() => null);
2313
+ return result;
2314
+ });
2315
+ ipcMain2.handle("env:cleanup", async () => {
2316
+ markUiHeartbeat("env_cleanup");
2317
+ await cleanupRuntimeEnvironment("env_cleanup", {
2318
+ stopUiBridge: false,
2319
+ stopHeartbeat: false,
2320
+ stopCoreServices: false,
2321
+ stopStateBridge: false,
2322
+ includeLockCleanup: true
2323
+ });
2324
+ return {
2325
+ ok: true,
2326
+ services: await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }))
2327
+ };
2328
+ });
1474
2329
  ipcMain2.handle("config:saveLast", async (_evt, config) => {
1475
2330
  await saveCrawlConfig({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }, config);
1476
2331
  return { ok: true };
@@ -1561,7 +2416,7 @@ ipcMain2.handle("runtime:kill", async (_evt, input) => {
1561
2416
  ipcMain2.handle("runtime:restartPhase1", async (_evt, input) => {
1562
2417
  const profileId = String(input?.profileId || "").trim();
1563
2418
  if (!profileId) return { ok: false, error: "missing profileId" };
1564
- const args = [path5.join(REPO_ROOT2, "scripts", "xiaohongshu", "phase1-boot.mjs"), "--profile", profileId, "--headless", "false"];
2419
+ const args = [path6.join(REPO_ROOT2, "scripts", "xiaohongshu", "phase1-boot.mjs"), "--profile", profileId, "--headless", "false"];
1565
2420
  return spawnCommand({ title: `Phase1 restart ${profileId}`, cwd: REPO_ROOT2, args, groupKey: "phase1" });
1566
2421
  });
1567
2422
  ipcMain2.handle("runtime:setBrowserTitle", async (_evt, input) => {