@web-auto/webauto 0.1.3 → 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 +14 -5
  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() {
@@ -546,6 +586,10 @@ var StateBridge = class {
546
586
  win = null;
547
587
  tasks = /* @__PURE__ */ new Map();
548
588
  handlersRegistered = false;
589
+ emitBusEvent(payload) {
590
+ if (!this.win) return;
591
+ this.win.webContents.send("bus:event", payload);
592
+ }
549
593
  start(win2) {
550
594
  this.win = win2;
551
595
  this.connect();
@@ -561,12 +605,26 @@ var StateBridge = class {
561
605
  this.ws.on("open", () => {
562
606
  console.log("[StateBridge] connected to", UNIFIED_API_WS);
563
607
  this.ws?.send(JSON.stringify({ type: "subscribe", topic: "task:*" }));
608
+ this.emitBusEvent({ type: "env:unified", ok: true, ts: Date.now() });
564
609
  });
565
610
  this.ws.on("message", (data) => {
566
611
  try {
567
612
  const msg = JSON.parse(data.toString());
568
613
  if (msg.type === "task:update" && msg.data) {
569
614
  this.handleTaskUpdate(msg.data);
615
+ return;
616
+ }
617
+ if (msg.type === "event" && msg.topic === "bus.message") {
618
+ const raw = msg?.payload?.data;
619
+ if (typeof raw === "string" && raw.trim()) {
620
+ try {
621
+ const parsed = JSON.parse(raw);
622
+ if (parsed && typeof parsed === "object") {
623
+ this.emitBusEvent(parsed);
624
+ }
625
+ } catch {
626
+ }
627
+ }
570
628
  }
571
629
  } catch (err) {
572
630
  console.warn("[StateBridge] parse error:", err);
@@ -574,6 +632,7 @@ var StateBridge = class {
574
632
  });
575
633
  this.ws.on("close", () => {
576
634
  console.log("[StateBridge] disconnected, reconnecting...");
635
+ this.emitBusEvent({ type: "env:unified", ok: false, ts: Date.now() });
577
636
  this.scheduleReconnect();
578
637
  });
579
638
  this.ws.on("error", (err) => {
@@ -634,12 +693,29 @@ var stateBridge = new StateBridge();
634
693
  // src/main/env-check.mts
635
694
  import { promisify } from "node:util";
636
695
  import { exec, spawnSync } from "node:child_process";
637
- import { existsSync } from "node:fs";
696
+ import { existsSync as existsSync2 } from "node:fs";
638
697
  import path4 from "node:path";
639
698
  import os3 from "node:os";
640
699
  var execAsync = promisify(exec);
700
+ function resolveWebautoRoot() {
701
+ const portableRoot = String(process.env.WEBAUTO_PORTABLE_ROOT || process.env.WEBAUTO_ROOT || "").trim();
702
+ return portableRoot ? path4.join(portableRoot, ".webauto") : path4.join(os3.homedir(), ".webauto");
703
+ }
641
704
  function resolveNpxBin2() {
642
- return process.platform === "win32" ? "npx.cmd" : "npx";
705
+ if (process.platform !== "win32") return "npx";
706
+ const resolved = resolveOnPath(["npx.cmd", "npx.exe", "npx.bat", "npx.ps1"]);
707
+ return resolved || "npx.cmd";
708
+ }
709
+ function resolveOnPath(candidates) {
710
+ const pathEnv = process.env.PATH || process.env.Path || "";
711
+ const dirs = pathEnv.split(path4.delimiter).filter(Boolean);
712
+ for (const dir of dirs) {
713
+ for (const name of candidates) {
714
+ const full = path4.join(dir, name);
715
+ if (existsSync2(full)) return full;
716
+ }
717
+ }
718
+ return null;
643
719
  }
644
720
  function resolveCamoVersionFromText(stdout, stderr) {
645
721
  const merged = `${String(stdout || "")}
@@ -653,13 +729,35 @@ ${String(stderr || "")}`.trim();
653
729
  }
654
730
  return "unknown";
655
731
  }
732
+ function quoteCmdArg(value) {
733
+ if (!value) return '""';
734
+ if (!/[\s"]/u.test(value)) return value;
735
+ return `"${value.replace(/"/g, '""')}"`;
736
+ }
656
737
  function runVersionCheck(command, args, explicitPath) {
657
738
  try {
658
- const ret = spawnSync(command, args, {
659
- encoding: "utf8",
660
- timeout: 8e3,
661
- windowsHide: true
662
- });
739
+ const lower = String(command || "").toLowerCase();
740
+ let ret;
741
+ if (process.platform === "win32" && (lower.endsWith(".cmd") || lower.endsWith(".bat"))) {
742
+ const cmdLine = [quoteCmdArg(command), ...args.map(quoteCmdArg)].join(" ");
743
+ ret = spawnSync("cmd.exe", ["/d", "/s", "/c", cmdLine], {
744
+ encoding: "utf8",
745
+ timeout: 8e3,
746
+ windowsHide: true
747
+ });
748
+ } else if (process.platform === "win32" && lower.endsWith(".ps1")) {
749
+ ret = spawnSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command, ...args], {
750
+ encoding: "utf8",
751
+ timeout: 8e3,
752
+ windowsHide: true
753
+ });
754
+ } else {
755
+ ret = spawnSync(command, args, {
756
+ encoding: "utf8",
757
+ timeout: 8e3,
758
+ windowsHide: true
759
+ });
760
+ }
663
761
  if (ret.status !== 0) {
664
762
  return {
665
763
  installed: false,
@@ -676,19 +774,24 @@ function runVersionCheck(command, args, explicitPath) {
676
774
  }
677
775
  }
678
776
  async function checkCamoCli() {
679
- const pathCheck = runVersionCheck(process.platform === "win32" ? "camo.cmd" : "camo", ["help"], "PATH:camo");
680
- if (pathCheck.installed) return pathCheck;
777
+ const camoCandidates = process.platform === "win32" ? ["camo.cmd", "camo.exe", "camo.bat", "camo.ps1"] : ["camo"];
778
+ for (const candidate of camoCandidates) {
779
+ const pathCheck = runVersionCheck(candidate, ["help"], `PATH:${candidate}`);
780
+ if (pathCheck.installed) return pathCheck;
781
+ }
681
782
  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)
783
+ const localRoots = [
784
+ path4.resolve(cwd, "node_modules", ".bin"),
785
+ path4.resolve(cwd, "..", "node_modules", ".bin"),
786
+ path4.resolve(cwd, "..", "..", "node_modules", ".bin")
687
787
  ];
688
- for (const candidate of localCandidates) {
689
- if (!existsSync(candidate)) continue;
690
- const ret = runVersionCheck(candidate, ["help"], candidate);
691
- if (ret.installed) return ret;
788
+ for (const localRoot of localRoots) {
789
+ for (const suffix of camoCandidates) {
790
+ const candidate = path4.resolve(localRoot, suffix);
791
+ if (!existsSync2(candidate)) continue;
792
+ const ret = runVersionCheck(candidate, ["help"], candidate);
793
+ if (ret.installed) return ret;
794
+ }
692
795
  }
693
796
  const npxCheck = runVersionCheck(
694
797
  resolveNpxBin2(),
@@ -702,12 +805,12 @@ async function checkCamoCli() {
702
805
  };
703
806
  }
704
807
  async function checkServices() {
705
- const [unifiedApi, browserService, searchGate] = await Promise.all([
808
+ const [unifiedApi, camoRuntime, searchGate] = await Promise.all([
706
809
  fetch("http://127.0.0.1:7701/health", { signal: AbortSignal.timeout(3e3) }).then((r) => r.ok).catch(() => false),
707
810
  fetch("http://127.0.0.1:7704/health", { signal: AbortSignal.timeout(3e3) }).then((r) => r.ok).catch(() => false),
708
811
  fetch("http://127.0.0.1:7790/health", { signal: AbortSignal.timeout(3e3) }).then((r) => r.ok).catch(() => false)
709
812
  ]);
710
- return { unifiedApi, browserService, searchGate };
813
+ return { unifiedApi, camoRuntime, searchGate };
711
814
  }
712
815
  async function checkFirefox() {
713
816
  try {
@@ -739,12 +842,12 @@ async function checkFirefox() {
739
842
  path4.join(localAppData, "Mozilla Firefox", "firefox.exe")
740
843
  ];
741
844
  for (const firefoxPath2 of possiblePaths) {
742
- if (existsSync(firefoxPath2)) return { installed: true, path: firefoxPath2 };
845
+ if (existsSync2(firefoxPath2)) return { installed: true, path: firefoxPath2 };
743
846
  }
744
847
  return { installed: false };
745
848
  }
746
849
  const macBundle = "/Applications/Firefox.app/Contents/MacOS/firefox";
747
- if (platform === "darwin" && existsSync(macBundle)) return { installed: true, path: macBundle };
850
+ if (platform === "darwin" && existsSync2(macBundle)) return { installed: true, path: macBundle };
748
851
  const { stdout } = await execAsync("which firefox", { timeout: 3e3 });
749
852
  const firefoxPath = String(stdout || "").trim();
750
853
  return firefoxPath ? { installed: true, path: firefoxPath } : { installed: false };
@@ -753,8 +856,8 @@ async function checkFirefox() {
753
856
  }
754
857
  }
755
858
  async function checkGeoIP() {
756
- const geoIpPath = path4.join(os3.homedir(), ".webauto", "geoip", "GeoLite2-City.mmdb");
757
- if (existsSync(geoIpPath)) {
859
+ const geoIpPath = path4.join(resolveWebautoRoot(), "geoip", "GeoLite2-City.mmdb");
860
+ if (existsSync2(geoIpPath)) {
758
861
  return { installed: true, path: geoIpPath };
759
862
  }
760
863
  return { installed: false };
@@ -766,32 +869,585 @@ async function checkEnvironment() {
766
869
  checkFirefox(),
767
870
  checkGeoIP()
768
871
  ]);
769
- const allReady = camo.installed && services.unifiedApi && services.browserService && firefox.installed && geoip.installed;
872
+ const allReady = camo.installed && services.unifiedApi && firefox.installed;
770
873
  return { camo, services, firefox, geoip, allReady };
771
874
  }
772
875
 
876
+ // src/main/ui-cli-bridge.mts
877
+ import { createServer } from "node:http";
878
+ import os4 from "node:os";
879
+ import path5 from "node:path";
880
+ import { promises as fs3 } from "node:fs";
881
+ var DEFAULT_HOST = "127.0.0.1";
882
+ var DEFAULT_PORT = 7716;
883
+ var CONTROL_FILE = path5.join(os4.homedir(), ".webauto", "run", "ui-cli.json");
884
+ function readInt(input, fallback) {
885
+ const n = Number(input);
886
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
887
+ }
888
+ function sendJson(res, code, payload) {
889
+ const body = JSON.stringify(payload);
890
+ res.statusCode = code;
891
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
892
+ res.setHeader("Content-Length", Buffer.byteLength(body));
893
+ res.end(body);
894
+ }
895
+ function parseBody(req) {
896
+ return new Promise((resolve) => {
897
+ const chunks = [];
898
+ req.on("data", (c) => chunks.push(c));
899
+ req.on("end", () => {
900
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
901
+ if (!raw) return resolve({});
902
+ try {
903
+ resolve(JSON.parse(raw));
904
+ } catch {
905
+ resolve({});
906
+ }
907
+ });
908
+ req.on("error", () => resolve({}));
909
+ });
910
+ }
911
+ function isUiReady(win2) {
912
+ if (!win2 || win2.isDestroyed()) return false;
913
+ const wc = win2.webContents;
914
+ if (!wc || wc.isDestroyed()) return false;
915
+ if (typeof wc.isCrashed === "function" && wc.isCrashed()) return false;
916
+ return true;
917
+ }
918
+ function toActionError(input, error, extra = {}) {
919
+ const action = String(input?.action || "").trim();
920
+ const selector = String(input?.selector || "").trim();
921
+ const state = String(input?.state || "").trim();
922
+ const payload = {
923
+ ok: false,
924
+ error: String(error || "unknown_error"),
925
+ action: action || null,
926
+ selector: selector || null,
927
+ state: state || null,
928
+ ...extra
929
+ };
930
+ return payload;
931
+ }
932
+ async function writeControlFile(host, port) {
933
+ const payload = {
934
+ pid: process.pid,
935
+ host,
936
+ port,
937
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
938
+ };
939
+ try {
940
+ await fs3.mkdir(path5.dirname(CONTROL_FILE), { recursive: true });
941
+ await fs3.writeFile(CONTROL_FILE, JSON.stringify(payload, null, 2), "utf8");
942
+ } catch {
943
+ }
944
+ }
945
+ async function removeControlFile() {
946
+ try {
947
+ await fs3.unlink(CONTROL_FILE);
948
+ } catch {
949
+ }
950
+ }
951
+ function buildSnapshotScript() {
952
+ return `(() => {
953
+ const text = (sel) => {
954
+ const el = document.querySelector(sel);
955
+ return el ? String(el.textContent || '').trim() : '';
956
+ };
957
+ const value = (sel) => {
958
+ const el = document.querySelector(sel);
959
+ if (!el) return '';
960
+ if ('value' in el) return String(el.value ?? '');
961
+ return String(el.textContent || '').trim();
962
+ };
963
+ const activeTab = document.querySelector('.tab.active');
964
+ const errors = Array.from(document.querySelectorAll('#recent-errors-list li'))
965
+ .map((el) => String(el.textContent || '').trim())
966
+ .filter(Boolean)
967
+ .slice(0, 20);
968
+ return {
969
+ ready: true,
970
+ activeTabId: String(activeTab?.dataset?.tabId || '').trim(),
971
+ activeTabLabel: String(activeTab?.textContent || '').trim(),
972
+ status: text('#status'),
973
+ runId: text('#run-id-text'),
974
+ errorCount: text('#error-count-text'),
975
+ currentPhase: text('#current-phase'),
976
+ currentAction: text('#current-action'),
977
+ progressPercent: text('#progress-percent'),
978
+ keyword: value('#keyword-input'),
979
+ target: value('#target-input'),
980
+ account: value('#account-select'),
981
+ env: value('#env-select'),
982
+ recentErrors: errors,
983
+ ts: new Date().toISOString(),
984
+ };
985
+ })()`;
986
+ }
987
+ function buildActionScript(action) {
988
+ const payloadJson = JSON.stringify(action);
989
+ const snapshotScript = buildSnapshotScript();
990
+ return `(() => {
991
+ const payload = ${payloadJson};
992
+ const normalize = (v) => String(v || '').trim();
993
+ const isVisible = (el) => {
994
+ if (!el) return false;
995
+ const rect = el.getBoundingClientRect();
996
+ if (!rect || rect.width <= 0 || rect.height <= 0) return false;
997
+ const style = window.getComputedStyle(el);
998
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
999
+ };
1000
+ const query = (selector) => {
1001
+ const s = normalize(selector);
1002
+ if (!s) return null;
1003
+ return document.querySelector(s);
1004
+ };
1005
+ const queryAll = (selector) => {
1006
+ const s = normalize(selector) || 'body';
1007
+ return Array.from(document.querySelectorAll(s));
1008
+ };
1009
+ const findByText = ({ selector, text, exact, nth }) => {
1010
+ const q = normalize(selector) || 'button';
1011
+ const target = normalize(text);
1012
+ const lower = target.toLowerCase();
1013
+ if (!target) return null;
1014
+ const nodes = Array.from(document.querySelectorAll(q));
1015
+ const matched = nodes.filter((el) => {
1016
+ const t = normalize(el.textContent);
1017
+ if (!t) return false;
1018
+ if (exact === true) return t === target;
1019
+ return t.toLowerCase().includes(lower);
1020
+ });
1021
+ const index = Number.isFinite(Number(nth)) ? Math.max(0, Math.floor(Number(nth))) : 0;
1022
+ return matched[index] || null;
1023
+ };
1024
+ const getElementDetails = (el) => {
1025
+ if (!el) return null;
1026
+ const rect = el.getBoundingClientRect();
1027
+ const style = window.getComputedStyle(el);
1028
+ const attrs = {};
1029
+ for (const attr of el.attributes) {
1030
+ attrs[attr.name] = attr.value;
1031
+ }
1032
+ return {
1033
+ rect: {
1034
+ x: Math.round(rect.x),
1035
+ y: Math.round(rect.y),
1036
+ width: Math.round(rect.width),
1037
+ height: Math.round(rect.height),
1038
+ top: Math.round(rect.top),
1039
+ left: Math.round(rect.left),
1040
+ right: Math.round(rect.right),
1041
+ bottom: Math.round(rect.bottom),
1042
+ },
1043
+ computedStyle: {
1044
+ display: style.display,
1045
+ visibility: style.visibility,
1046
+ opacity: style.opacity,
1047
+ backgroundColor: style.backgroundColor,
1048
+ color: style.color,
1049
+ fontSize: style.fontSize,
1050
+ fontFamily: style.fontFamily,
1051
+ position: style.position,
1052
+ zIndex: style.zIndex,
1053
+ },
1054
+ attributes: attrs,
1055
+ innerText: el.innerText,
1056
+ outerHTML: el.outerHTML?.slice(0, 2000),
1057
+ tagName: el.tagName,
1058
+ className: el.className,
1059
+ id: el.id,
1060
+ };
1061
+ };
1062
+ const focusEl = (el) => {
1063
+ if (!el || typeof el.focus !== 'function') return false;
1064
+ el.focus();
1065
+ return document.activeElement === el;
1066
+ };
1067
+ const clickEl = (el) => {
1068
+ if (!el || typeof el.click !== 'function') return false;
1069
+ if (typeof el.scrollIntoView === 'function') {
1070
+ try { el.scrollIntoView({ block: 'center', inline: 'nearest' }); } catch {}
1071
+ }
1072
+ focusEl(el);
1073
+ el.click();
1074
+ return true;
1075
+ };
1076
+ const setInputValue = (el, value) => {
1077
+ if (!el) return false;
1078
+ const text = String(value ?? '');
1079
+ if ('value' in el) {
1080
+ el.value = text;
1081
+ } else {
1082
+ el.textContent = text;
1083
+ }
1084
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1085
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1086
+ return true;
1087
+ };
1088
+ const pressKey = (el, key) => {
1089
+ const k = normalize(key) || 'Enter';
1090
+ const target = el || document.activeElement || document.body;
1091
+ const code = k === 'Escape' ? 'Escape' : k === 'Enter' ? 'Enter' : k;
1092
+ const init = { key: k, code, bubbles: true, cancelable: true };
1093
+ target.dispatchEvent(new KeyboardEvent('keydown', init));
1094
+ target.dispatchEvent(new KeyboardEvent('keyup', init));
1095
+ return true;
1096
+ };
1097
+ const findTab = () => {
1098
+ const tabId = normalize(payload.tabId || payload.value);
1099
+ const tabLabel = normalize(payload.tabLabel || payload.selector);
1100
+ const tabs = Array.from(document.querySelectorAll('.tab'));
1101
+ if (tabId) {
1102
+ const byId = tabs.find((el) => normalize(el?.dataset?.tabId) === tabId);
1103
+ if (byId) return byId;
1104
+ }
1105
+ if (tabLabel) {
1106
+ const lower = tabLabel.toLowerCase();
1107
+ return tabs.find((el) => normalize(el.textContent).toLowerCase().includes(lower)) || null;
1108
+ }
1109
+ return null;
1110
+ };
1111
+
1112
+ if (payload.action === 'snapshot') {
1113
+ return { ok: true, snapshot: ${snapshotScript} };
1114
+ }
1115
+ if (payload.action === 'dialogs') {
1116
+ const mode = normalize(payload.value).toLowerCase();
1117
+ const w = window;
1118
+ const key = '__webauto_ui_cli_dialogs__';
1119
+ if (mode === 'silent') {
1120
+ if (!w[key]) {
1121
+ w[key] = {
1122
+ alert: w.alert,
1123
+ confirm: w.confirm,
1124
+ prompt: w.prompt,
1125
+ };
1126
+ }
1127
+ w.alert = () => {};
1128
+ w.confirm = () => true;
1129
+ w.prompt = () => '';
1130
+ return { ok: true, mode: 'silent' };
1131
+ }
1132
+ if (mode === 'restore') {
1133
+ if (w[key]) {
1134
+ w.alert = w[key].alert;
1135
+ w.confirm = w[key].confirm;
1136
+ w.prompt = w[key].prompt;
1137
+ delete w[key];
1138
+ }
1139
+ return { ok: true, mode: 'restore' };
1140
+ }
1141
+ return { ok: false, error: 'unsupported_dialog_mode' };
1142
+ }
1143
+ if (payload.action === 'tab') {
1144
+ const tab = findTab();
1145
+ if (!tab) return { ok: false, error: 'tab_not_found' };
1146
+ clickEl(tab);
1147
+ return { ok: true, tab: normalize(tab.textContent), tabId: normalize(tab?.dataset?.tabId) };
1148
+ }
1149
+ if (payload.action === 'click') {
1150
+ const el = query(payload.selector);
1151
+ if (!el) return { ok: false, error: 'selector_not_found', selector: normalize(payload.selector) };
1152
+ clickEl(el);
1153
+ return { ok: true };
1154
+ }
1155
+ if (payload.action === 'focus') {
1156
+ const el = query(payload.selector);
1157
+ if (!el) return { ok: false, error: 'selector_not_found', selector: normalize(payload.selector) };
1158
+ const focused = focusEl(el);
1159
+ return { ok: focused, focused };
1160
+ }
1161
+ if (payload.action === 'input') {
1162
+ const el = query(payload.selector);
1163
+ if (!el) return { ok: false, error: 'selector_not_found', selector: normalize(payload.selector) };
1164
+ focusEl(el);
1165
+ const written = setInputValue(el, payload.value || '');
1166
+ return { ok: written, value: String(payload.value || '') };
1167
+ }
1168
+ if (payload.action === 'select') {
1169
+ const el = query(payload.selector);
1170
+ if (!el || el.tagName !== 'SELECT') return { ok: false, error: 'select_not_found', selector: normalize(payload.selector) };
1171
+ el.value = String(payload.value || '');
1172
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1173
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1174
+ return { ok: true, value: el.value };
1175
+ }
1176
+ if (payload.action === 'press') {
1177
+ const el = query(payload.selector);
1178
+ const ok = pressKey(el, payload.key);
1179
+ return { ok, key: normalize(payload.key) || 'Enter' };
1180
+ }
1181
+ if (payload.action === 'click_text') {
1182
+ const el = findByText({
1183
+ selector: payload.selector,
1184
+ text: payload.text || payload.value,
1185
+ exact: payload.exact === true,
1186
+ nth: payload.nth,
1187
+ });
1188
+ if (!el) return { ok: false, error: 'text_not_found', text: normalize(payload.text || payload.value), selector: normalize(payload.selector) };
1189
+ clickEl(el);
1190
+ return { ok: true, text: normalize(el.textContent) };
1191
+ }
1192
+ if (payload.action === 'probe') {
1193
+ const selector = normalize(payload.selector) || 'body';
1194
+ const nodes = queryAll(selector);
1195
+ const first = nodes[0] || null;
1196
+ const firstVisible = isVisible(first);
1197
+ const text = normalize(first?.textContent);
1198
+ const value = first && 'value' in first ? String(first.value ?? '') : text;
1199
+ const checked = Boolean(first && 'checked' in first && first.checked === true);
1200
+ const disabled = Boolean(first && 'disabled' in first && first.disabled === true);
1201
+ const probeText = normalize(payload.text || payload.value);
1202
+ let details = null;
1203
+ if (first && payload.detailed === true) {
1204
+ details = getElementDetails(first);
1205
+ }
1206
+ let textMatchedCount = 0;
1207
+ if (probeText) {
1208
+ const target = payload.exact === true ? probeText : probeText.toLowerCase();
1209
+ textMatchedCount = nodes.filter((el) => {
1210
+ const current = normalize(el.textContent);
1211
+ if (!current) return false;
1212
+ if (payload.exact === true) return current === target;
1213
+ return current.toLowerCase().includes(target);
1214
+ }).length;
1215
+ }
1216
+ return {
1217
+ ok: true,
1218
+ selector,
1219
+ exists: Boolean(first),
1220
+ count: nodes.length,
1221
+ visible: firstVisible,
1222
+ text,
1223
+ value,
1224
+ checked,
1225
+ disabled,
1226
+ tagName: first?.tagName || '',
1227
+ className: first?.className || '',
1228
+ details,
1229
+ textMatchedCount,
1230
+ };
1231
+ }
1232
+ if (payload.action === 'close_window') {
1233
+ window.close();
1234
+ return { ok: true };
1235
+ }
1236
+ return { ok: false, error: 'unsupported_action', action: normalize(payload.action) };
1237
+ })()`;
1238
+ }
1239
+ var UiCliBridge = class {
1240
+ server = null;
1241
+ options;
1242
+ host;
1243
+ port;
1244
+ constructor(options) {
1245
+ this.options = options;
1246
+ this.host = String(options.host || process.env.WEBAUTO_UI_CLI_HOST || DEFAULT_HOST);
1247
+ this.port = readInt(options.port || process.env.WEBAUTO_UI_CLI_PORT, DEFAULT_PORT);
1248
+ }
1249
+ getAddress() {
1250
+ return { host: this.host, port: this.port };
1251
+ }
1252
+ async start() {
1253
+ if (this.server) return this.getAddress();
1254
+ await new Promise((resolve, reject) => {
1255
+ const server = createServer((req, res) => {
1256
+ void this.handleRequest(req, res);
1257
+ });
1258
+ server.on("error", (err) => reject(err));
1259
+ server.listen(this.port, this.host, () => {
1260
+ this.server = server;
1261
+ resolve();
1262
+ });
1263
+ });
1264
+ await writeControlFile(this.host, this.port);
1265
+ return this.getAddress();
1266
+ }
1267
+ async stop() {
1268
+ if (!this.server) {
1269
+ await removeControlFile();
1270
+ return;
1271
+ }
1272
+ const srv = this.server;
1273
+ this.server = null;
1274
+ await new Promise((resolve) => srv.close(() => resolve()));
1275
+ await removeControlFile();
1276
+ }
1277
+ async handleRequest(req, res) {
1278
+ const method = String(req.method || "GET").toUpperCase();
1279
+ const url = new URL(req.url || "/", `http://${this.host}:${this.port}`);
1280
+ if (method === "GET" && url.pathname === "/health") {
1281
+ return sendJson(res, 200, await this.status());
1282
+ }
1283
+ if (method === "GET" && (url.pathname === "/status" || url.pathname === "/snapshot")) {
1284
+ return sendJson(res, 200, await this.status(true));
1285
+ }
1286
+ if (method === "POST" && url.pathname === "/action") {
1287
+ const body = await parseBody(req);
1288
+ const result = await this.handleAction(body || {});
1289
+ return sendJson(res, result.ok ? 200 : 400, result);
1290
+ }
1291
+ return sendJson(res, 404, { ok: false, error: "not_found" });
1292
+ }
1293
+ async status(includeSnapshot = false) {
1294
+ const win2 = this.options.getWindow();
1295
+ const ready = isUiReady(win2);
1296
+ if (!ready) {
1297
+ return {
1298
+ ok: false,
1299
+ pid: process.pid,
1300
+ ready: false,
1301
+ host: this.host,
1302
+ port: this.port,
1303
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1304
+ error: "window_not_ready"
1305
+ };
1306
+ }
1307
+ let snapshot;
1308
+ if (includeSnapshot) {
1309
+ try {
1310
+ snapshot = await win2.webContents.executeJavaScript(buildSnapshotScript(), true);
1311
+ } catch (err) {
1312
+ return {
1313
+ ok: false,
1314
+ pid: process.pid,
1315
+ ready,
1316
+ host: this.host,
1317
+ port: this.port,
1318
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1319
+ error: err?.message || String(err)
1320
+ };
1321
+ }
1322
+ }
1323
+ return {
1324
+ ok: true,
1325
+ pid: process.pid,
1326
+ ready,
1327
+ host: this.host,
1328
+ port: this.port,
1329
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1330
+ snapshot
1331
+ };
1332
+ }
1333
+ async handleAction(input) {
1334
+ const action = String(input?.action || "").trim();
1335
+ if (!action) return toActionError(input, "missing_action");
1336
+ if (action === "wait") {
1337
+ return this.waitForSelector(input);
1338
+ }
1339
+ const win2 = this.options.getWindow();
1340
+ if (!isUiReady(win2)) return toActionError(input, "window_not_ready");
1341
+ try {
1342
+ const out = await win2.webContents.executeJavaScript(buildActionScript(input), true);
1343
+ return out && typeof out === "object" ? out : toActionError(input, "empty_result");
1344
+ } catch (err) {
1345
+ return toActionError(input, err?.message || String(err), { details: err?.stack || null });
1346
+ }
1347
+ }
1348
+ async waitForSelector(input) {
1349
+ const selector = String(input.selector || "").trim();
1350
+ if (!selector) return toActionError(input, "missing_selector");
1351
+ const expected = input.state || "visible";
1352
+ const timeoutMs = readInt(input.timeoutMs, 15e3);
1353
+ const intervalMs = readInt(input.intervalMs, 250);
1354
+ const startedAt = Date.now();
1355
+ while (Date.now() - startedAt <= timeoutMs) {
1356
+ const win2 = this.options.getWindow();
1357
+ if (!isUiReady(win2)) return toActionError(input, "window_not_ready");
1358
+ try {
1359
+ const checkScript = `(() => {
1360
+ const el = document.querySelector(${JSON.stringify(selector)});
1361
+ const visible = (() => {
1362
+ if (!el) return false;
1363
+ const rect = el.getBoundingClientRect();
1364
+ if (!rect || rect.width <= 0 || rect.height <= 0) return false;
1365
+ const style = window.getComputedStyle(el);
1366
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
1367
+ })();
1368
+ const text = String(el?.textContent || '').trim();
1369
+ const value = el && 'value' in el ? String(el.value ?? '') : '';
1370
+ const disabled = Boolean(el && 'disabled' in el && el.disabled === true);
1371
+ return { exists: Boolean(el), visible, text, value, disabled };
1372
+ })()`;
1373
+ const state = await win2.webContents.executeJavaScript(checkScript, true);
1374
+ const exists = Boolean(state?.exists);
1375
+ const visible = Boolean(state?.visible);
1376
+ const text = String(state?.text || "");
1377
+ const value = String(state?.value || "");
1378
+ const disabled = Boolean(state?.disabled);
1379
+ let matched = false;
1380
+ let reason = "";
1381
+ switch (expected) {
1382
+ case "exists":
1383
+ matched = exists;
1384
+ reason = exists ? "element exists" : "element not found";
1385
+ break;
1386
+ case "visible":
1387
+ matched = visible;
1388
+ reason = visible ? "element visible" : "element not visible";
1389
+ break;
1390
+ case "hidden":
1391
+ matched = !exists || !visible;
1392
+ reason = matched ? "element hidden or absent" : "element is visible";
1393
+ break;
1394
+ case "text_contains":
1395
+ matched = exists && text.includes(String(input.value || ""));
1396
+ reason = matched ? "text matched" : `text '${text}' does not contain '${input.value || ""}'`;
1397
+ break;
1398
+ case "text_equals":
1399
+ matched = exists && text === String(input.value || "");
1400
+ reason = matched ? "text matched" : `text '${text}' !== '${input.value || ""}'`;
1401
+ break;
1402
+ case "value_equals":
1403
+ matched = exists && value === String(input.value || "");
1404
+ reason = matched ? "value matched" : `value '${value}' !== '${input.value || ""}'`;
1405
+ break;
1406
+ case "not_disabled":
1407
+ matched = exists && !disabled;
1408
+ reason = matched ? "element enabled" : "element disabled";
1409
+ break;
1410
+ default:
1411
+ return toActionError(input, "unsupported_state", { expected });
1412
+ }
1413
+ if (matched) {
1414
+ return { ok: true, selector, expected, exists, visible, text, value, disabled, elapsedMs: Date.now() - startedAt, reason };
1415
+ }
1416
+ } catch (err) {
1417
+ return toActionError(input, err?.message || String(err), { details: err?.stack || null });
1418
+ }
1419
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1420
+ }
1421
+ return toActionError(input, "wait_timeout", {
1422
+ expected,
1423
+ timeoutMs,
1424
+ elapsedMs: Date.now() - startedAt
1425
+ });
1426
+ }
1427
+ };
1428
+
773
1429
  // src/main/index.mts
774
1430
  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(),
1431
+ var __dirname = path6.dirname(fileURLToPath2(import.meta.url));
1432
+ var APP_ROOT = path6.resolve(__dirname, "../..");
1433
+ var REPO_ROOT2 = path6.resolve(APP_ROOT, "../..");
1434
+ var DESKTOP_HEARTBEAT_FILE = path6.join(
1435
+ os5.homedir(),
780
1436
  ".webauto",
781
1437
  "run",
782
1438
  "desktop-console-heartbeat.json"
783
1439
  );
784
1440
  var profileStore = createProfileStore({ repoRoot: REPO_ROOT2 });
785
- var XHS_SCRIPTS_ROOT = path5.join(REPO_ROOT2, "scripts", "xiaohongshu");
1441
+ var XHS_SCRIPTS_ROOT = path6.join(REPO_ROOT2, "scripts", "xiaohongshu");
786
1442
  var XHS_FULL_COLLECT_RE = /collect-content\.mjs$/;
787
1443
  function configureElectronPaths() {
788
1444
  try {
789
1445
  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");
1446
+ const normalized = path6.normalize(downloadRoot);
1447
+ const baseDir = path6.basename(normalized).toLowerCase() === "download" ? path6.dirname(normalized) : normalized;
1448
+ const userDataRoot = path6.join(baseDir, "desktop-console");
1449
+ const cacheRoot = path6.join(userDataRoot, "cache");
1450
+ const gpuCacheRoot = path6.join(cacheRoot, "gpu");
795
1451
  try {
796
1452
  mkdirSync(cacheRoot, { recursive: true });
797
1453
  } catch {
@@ -833,6 +1489,8 @@ var GroupQueue = class {
833
1489
  };
834
1490
  var groupQueues = /* @__PURE__ */ new Map();
835
1491
  var runs = /* @__PURE__ */ new Map();
1492
+ var trackedRunPids = /* @__PURE__ */ new Set();
1493
+ var appExitCleanupPromise = null;
836
1494
  var UI_HEARTBEAT_TIMEOUT_MS = resolveUiHeartbeatTimeoutMs(process.env);
837
1495
  var lastUiHeartbeatAt = Date.now();
838
1496
  var heartbeatWatchdog = null;
@@ -849,8 +1507,8 @@ async function writeCoreServiceHeartbeat(status) {
849
1507
  source: "desktop-console"
850
1508
  };
851
1509
  try {
852
- await fs3.mkdir(path5.dirname(filePath), { recursive: true });
853
- await fs3.writeFile(filePath, JSON.stringify(payload), "utf8");
1510
+ await fs4.mkdir(path6.dirname(filePath), { recursive: true });
1511
+ await fs4.writeFile(filePath, JSON.stringify(payload), "utf8");
854
1512
  } catch {
855
1513
  }
856
1514
  }
@@ -887,11 +1545,31 @@ function ensureStateBridge() {
887
1545
  }
888
1546
  }
889
1547
  var win = null;
1548
+ var uiCliBridge = new UiCliBridge({ getWindow: getWin });
890
1549
  configureElectronPaths();
1550
+ var singleInstanceLock = app.requestSingleInstanceLock();
1551
+ if (!singleInstanceLock) {
1552
+ app.quit();
1553
+ }
891
1554
  function getWin() {
892
1555
  if (!win || win.isDestroyed()) return null;
893
1556
  return win;
894
1557
  }
1558
+ if (singleInstanceLock) {
1559
+ app.on("second-instance", () => {
1560
+ const w = getWin();
1561
+ if (w) {
1562
+ if (w.isMinimized()) w.restore();
1563
+ w.focus();
1564
+ return;
1565
+ }
1566
+ if (app.isReady()) {
1567
+ createWindow();
1568
+ } else {
1569
+ app.whenReady().then(() => createWindow());
1570
+ }
1571
+ });
1572
+ }
895
1573
  function isUiOperational() {
896
1574
  const w = getWin();
897
1575
  if (!w) return false;
@@ -951,6 +1629,85 @@ function killAllRuns(reason = "ui_heartbeat_timeout") {
951
1629
  void stopCoreServicesBestEffort(reason);
952
1630
  }
953
1631
  }
1632
+ function trackRunPid(child) {
1633
+ const pid = Number(child?.pid || 0);
1634
+ if (pid > 0) trackedRunPids.add(pid);
1635
+ }
1636
+ function untrackRunPid(child) {
1637
+ const pid = Number(child?.pid || 0);
1638
+ if (pid > 0) trackedRunPids.delete(pid);
1639
+ }
1640
+ async function cleanupTrackedRunPidsBestEffort(reason) {
1641
+ for (const pid of Array.from(trackedRunPids.values())) {
1642
+ try {
1643
+ if (process.platform === "win32") {
1644
+ spawn2("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
1645
+ } else {
1646
+ spawn2("pkill", ["-TERM", "-P", String(pid)], { stdio: "ignore" }).on("error", () => {
1647
+ });
1648
+ process.kill(pid, "SIGTERM");
1649
+ }
1650
+ } catch {
1651
+ }
1652
+ trackedRunPids.delete(pid);
1653
+ }
1654
+ if (trackedRunPids.size > 0) {
1655
+ console.warn(`[desktop-console] residual run pids after cleanup (${reason}): ${trackedRunPids.size}`);
1656
+ trackedRunPids.clear();
1657
+ }
1658
+ }
1659
+ async function cleanupCamoSessionsBestEffort(reason, includeLocks) {
1660
+ const camoCli = path6.join(REPO_ROOT2, "bin", "camoufox-cli.mjs");
1661
+ const invoke = async (args, timeoutMs = 6e4) => runJson({
1662
+ title: `camo ${args.join(" ")}`,
1663
+ cwd: REPO_ROOT2,
1664
+ args: [camoCli, ...args, "--json"],
1665
+ timeoutMs
1666
+ }).catch((err) => ({ ok: false, error: err?.message || String(err) }));
1667
+ const stopAll = await invoke(["stop", "all"]);
1668
+ if (!stopAll?.ok) {
1669
+ console.warn(`[desktop-console] camo stop all failed (${reason})`, stopAll?.error || stopAll?.stderr || stopAll?.stdout || stopAll);
1670
+ }
1671
+ if (!includeLocks) return;
1672
+ const cleanupLocks = await invoke(["cleanup", "locks"]);
1673
+ if (!cleanupLocks?.ok) {
1674
+ console.warn(`[desktop-console] camo cleanup locks failed (${reason})`, cleanupLocks?.error || cleanupLocks?.stderr || cleanupLocks?.stdout || cleanupLocks);
1675
+ }
1676
+ }
1677
+ async function cleanupRuntimeEnvironment(reason, options = {}) {
1678
+ killAllRuns(reason);
1679
+ await cleanupTrackedRunPidsBestEffort(reason);
1680
+ await cleanupCamoSessionsBestEffort(reason, options.includeLockCleanup !== false);
1681
+ if (options.stopUiBridge) {
1682
+ await uiCliBridge.stop().catch(() => null);
1683
+ }
1684
+ if (options.stopHeartbeat) {
1685
+ stopCoreServiceHeartbeat();
1686
+ }
1687
+ if (heartbeatWatchdog) {
1688
+ clearInterval(heartbeatWatchdog);
1689
+ heartbeatWatchdog = null;
1690
+ }
1691
+ if (options.stopCoreServices) {
1692
+ await stopCoreServicesBestEffort(reason);
1693
+ }
1694
+ if (options.stopStateBridge) {
1695
+ stateBridge.stop();
1696
+ }
1697
+ }
1698
+ function ensureAppExitCleanup(reason, options = {}) {
1699
+ if (appExitCleanupPromise) return appExitCleanupPromise;
1700
+ appExitCleanupPromise = cleanupRuntimeEnvironment(reason, {
1701
+ stopUiBridge: true,
1702
+ stopHeartbeat: true,
1703
+ stopCoreServices: true,
1704
+ stopStateBridge: options.stopStateBridge === true,
1705
+ includeLockCleanup: true
1706
+ }).finally(() => {
1707
+ appExitCleanupPromise = null;
1708
+ });
1709
+ return appExitCleanupPromise;
1710
+ }
954
1711
  function ensureHeartbeatWatchdog() {
955
1712
  if (heartbeatWatchdog) return;
956
1713
  heartbeatWatchdog = setInterval(() => {
@@ -1028,13 +1785,13 @@ function resolveNodeBin2() {
1028
1785
  function resolveCwd(input) {
1029
1786
  const raw = String(input || "").trim();
1030
1787
  if (!raw) return REPO_ROOT2;
1031
- return path5.isAbsolute(raw) ? raw : path5.resolve(REPO_ROOT2, raw);
1788
+ return path6.isAbsolute(raw) ? raw : path6.resolve(REPO_ROOT2, raw);
1032
1789
  }
1033
1790
  var cachedStateMod = null;
1034
1791
  async function getStateModule() {
1035
1792
  if (cachedStateMod) return cachedStateMod;
1036
1793
  try {
1037
- const p = path5.join(REPO_ROOT2, "dist", "modules", "state", "src", "xiaohongshu-collect-state.js");
1794
+ const p = path6.join(REPO_ROOT2, "dist", "modules", "state", "src", "xiaohongshu-collect-state.js");
1038
1795
  cachedStateMod = await import(pathToFileURL3(p).href);
1039
1796
  return cachedStateMod;
1040
1797
  } catch {
@@ -1047,6 +1804,34 @@ async function spawnCommand(spec) {
1047
1804
  const groupKey = spec.groupKey || "xiaohongshu";
1048
1805
  const q = getQueue(groupKey);
1049
1806
  const cwd = resolveCwd(spec.cwd);
1807
+ const args = Array.isArray(spec.args) ? spec.args : [];
1808
+ const isXhsRunCommand = args.some((item) => /xhs-(orchestrate|unified)\.mjs$/i.test(String(item || "").replace(/\\/g, "/")));
1809
+ const extractProfilesFromArgs = (argv) => {
1810
+ const out = [];
1811
+ for (let i = 0; i < argv.length; i += 1) {
1812
+ const flag = String(argv[i] || "").trim();
1813
+ if (flag === "--profile" || flag === "--profile-id") {
1814
+ const value = String(argv[i + 1] || "").trim();
1815
+ if (value) out.push(value);
1816
+ } else if (flag === "--profiles") {
1817
+ const value = String(argv[i + 1] || "").trim();
1818
+ if (value) {
1819
+ value.split(",").map((v) => v.trim()).filter(Boolean).forEach((v) => out.push(v));
1820
+ }
1821
+ }
1822
+ }
1823
+ return Array.from(new Set(out));
1824
+ };
1825
+ const requestedProfiles = isXhsRunCommand ? extractProfilesFromArgs(args) : [];
1826
+ if (requestedProfiles.length > 0) {
1827
+ for (const run of runs.values()) {
1828
+ const activeProfiles = Array.isArray(run.profiles) ? run.profiles : [];
1829
+ const conflict = requestedProfiles.find((p) => activeProfiles.includes(p));
1830
+ if (conflict) {
1831
+ throw new Error(`profile already running: ${conflict}`);
1832
+ }
1833
+ }
1834
+ }
1050
1835
  q.enqueue(
1051
1836
  () => new Promise((resolve) => {
1052
1837
  let finished = false;
@@ -1059,7 +1844,7 @@ async function spawnCommand(spec) {
1059
1844
  runs.delete(runId);
1060
1845
  resolve();
1061
1846
  };
1062
- const child = spawn2(resolveNodeBin2(), spec.args, {
1847
+ const child = spawn2(resolveNodeBin2(), args, {
1063
1848
  cwd,
1064
1849
  env: {
1065
1850
  ...process.env,
@@ -1070,7 +1855,8 @@ async function spawnCommand(spec) {
1070
1855
  stdio: ["ignore", "pipe", "pipe"],
1071
1856
  windowsHide: true
1072
1857
  });
1073
- runs.set(runId, { child, title: spec.title, startedAt: now() });
1858
+ trackRunPid(child);
1859
+ runs.set(runId, { child, title: spec.title, startedAt: now(), profiles: requestedProfiles });
1074
1860
  sendEvent({ type: "started", runId, title: spec.title, pid: child.pid ?? -1, ts: now() });
1075
1861
  const stdoutLines = createLineEmitter(runId, "stdout");
1076
1862
  const stderrLines = createLineEmitter(runId, "stderr");
@@ -1089,6 +1875,7 @@ async function spawnCommand(spec) {
1089
1875
  exitSignal = signal;
1090
1876
  });
1091
1877
  child.on("close", (code, signal) => {
1878
+ untrackRunPid(child);
1092
1879
  stdoutLines.flush();
1093
1880
  stderrLines.flush();
1094
1881
  finalize(exitCode ?? code ?? null, exitSignal ?? signal ?? null);
@@ -1134,21 +1921,21 @@ async function runJson(spec) {
1134
1921
  }
1135
1922
  async function scanResults(input) {
1136
1923
  const downloadRoot = String(input.downloadRoot || resolveDefaultDownloadRoot());
1137
- const root = path5.join(downloadRoot, "xiaohongshu");
1924
+ const root = path6.join(downloadRoot, "xiaohongshu");
1138
1925
  const result = { ok: true, root, entries: [] };
1139
1926
  try {
1140
1927
  const stateMod = await getStateModule();
1141
- const envDirs = await fs3.readdir(root, { withFileTypes: true });
1928
+ const envDirs = await fs4.readdir(root, { withFileTypes: true });
1142
1929
  for (const envEnt of envDirs) {
1143
1930
  if (!envEnt.isDirectory()) continue;
1144
1931
  const env = envEnt.name;
1145
- const envPath = path5.join(root, env);
1146
- const keywordDirs = await fs3.readdir(envPath, { withFileTypes: true });
1932
+ const envPath = path6.join(root, env);
1933
+ const keywordDirs = await fs4.readdir(envPath, { withFileTypes: true });
1147
1934
  for (const kwEnt of keywordDirs) {
1148
1935
  if (!kwEnt.isDirectory()) continue;
1149
1936
  const keyword = kwEnt.name;
1150
- const kwPath = path5.join(envPath, keyword);
1151
- const stat = await fs3.stat(kwPath).catch(() => null);
1937
+ const kwPath = path6.join(envPath, keyword);
1938
+ const stat = await fs4.stat(kwPath).catch(() => null);
1152
1939
  let stateSummary = null;
1153
1940
  if (stateMod?.loadXhsCollectState) {
1154
1941
  try {
@@ -1176,13 +1963,13 @@ async function scanResults(input) {
1176
1963
  }
1177
1964
  async function listXhsFullCollectScripts() {
1178
1965
  try {
1179
- const entries = await fs3.readdir(XHS_SCRIPTS_ROOT, { withFileTypes: true });
1966
+ const entries = await fs4.readdir(XHS_SCRIPTS_ROOT, { withFileTypes: true });
1180
1967
  const scripts = entries.filter((ent) => ent.isFile() && XHS_FULL_COLLECT_RE.test(ent.name)).map((ent) => {
1181
1968
  const name = ent.name;
1182
1969
  return {
1183
1970
  id: `xhs:${name}`,
1184
1971
  label: `Full Collect (${name})`,
1185
- path: path5.join(XHS_SCRIPTS_ROOT, name)
1972
+ path: path6.join(XHS_SCRIPTS_ROOT, name)
1186
1973
  };
1187
1974
  });
1188
1975
  return { ok: true, scripts };
@@ -1195,7 +1982,7 @@ async function readTextPreview(input) {
1195
1982
  const maxBytes = typeof input.maxBytes === "number" ? input.maxBytes : 8e4;
1196
1983
  const maxLines = typeof input.maxLines === "number" ? input.maxLines : 200;
1197
1984
  try {
1198
- const raw = await fs3.readFile(filePath, "utf8");
1985
+ const raw = await fs4.readFile(filePath, "utf8");
1199
1986
  const clipped = raw.slice(0, maxBytes);
1200
1987
  const lines = clipped.split(/\r?\n/g).slice(0, maxLines);
1201
1988
  return { ok: true, path: filePath, text: lines.join("\n") };
@@ -1208,14 +1995,14 @@ async function readTextTail(input) {
1208
1995
  const filePath = String(input?.path || "");
1209
1996
  const requestedOffset = typeof input?.fromOffset === "number" ? Math.max(0, Math.floor(input.fromOffset)) : 0;
1210
1997
  const maxBytes = typeof input?.maxBytes === "number" ? Math.max(1024, Math.floor(input.maxBytes)) : 256e3;
1211
- const st = await fs3.stat(filePath);
1998
+ const st = await fs4.stat(filePath);
1212
1999
  const size = Number(st?.size || 0);
1213
2000
  const fromOffset = requestedOffset > size ? 0 : requestedOffset;
1214
2001
  const toRead = Math.max(0, Math.min(maxBytes, size - fromOffset));
1215
2002
  if (toRead <= 0) {
1216
2003
  return { ok: true, path: filePath, text: "", fromOffset, nextOffset: fromOffset, fileSize: size };
1217
2004
  }
1218
- const fh = await fs3.open(filePath, "r");
2005
+ const fh = await fs4.open(filePath, "r");
1219
2006
  try {
1220
2007
  const buf = Buffer.allocUnsafe(toRead);
1221
2008
  const { bytesRead } = await fh.read(buf, 0, toRead, fromOffset);
@@ -1235,7 +2022,7 @@ async function readTextTail(input) {
1235
2022
  async function readFileBase64(input) {
1236
2023
  const filePath = String(input.path || "");
1237
2024
  const maxBytes = typeof input.maxBytes === "number" ? input.maxBytes : 8e6;
1238
- const buf = await fs3.readFile(filePath);
2025
+ const buf = await fs4.readFile(filePath);
1239
2026
  if (buf.byteLength > maxBytes) {
1240
2027
  return { ok: false, error: `file too large: ${buf.byteLength}` };
1241
2028
  }
@@ -1249,14 +2036,14 @@ async function listDir(input) {
1249
2036
  const stack = [root];
1250
2037
  while (stack.length > 0 && entries.length < maxEntries) {
1251
2038
  const dir = stack.pop();
1252
- const items = await fs3.readdir(dir, { withFileTypes: true }).catch(() => []);
2039
+ const items = await fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
1253
2040
  for (const ent of items) {
1254
2041
  if (entries.length >= maxEntries) break;
1255
- const full = path5.join(dir, ent.name);
1256
- const st = await fs3.stat(full).catch(() => null);
2042
+ const full = path6.join(dir, ent.name);
2043
+ const st = await fs4.stat(full).catch(() => null);
1257
2044
  entries.push({
1258
2045
  path: full,
1259
- rel: path5.relative(root, full),
2046
+ rel: path6.relative(root, full),
1260
2047
  name: ent.name,
1261
2048
  isDir: ent.isDirectory(),
1262
2049
  size: st?.size || 0,
@@ -1279,7 +2066,7 @@ function createWindow() {
1279
2066
  minWidth: 920,
1280
2067
  minHeight: 800,
1281
2068
  webPreferences: {
1282
- preload: path5.join(APP_ROOT, "dist", "main", "preload.mjs"),
2069
+ preload: path6.join(APP_ROOT, "dist", "main", "preload.mjs"),
1283
2070
  contextIsolation: true,
1284
2071
  nodeIntegration: false,
1285
2072
  sandbox: false,
@@ -1287,28 +2074,19 @@ function createWindow() {
1287
2074
  backgroundThrottling: false
1288
2075
  }
1289
2076
  });
1290
- const htmlPath = path5.join(APP_ROOT, "dist", "renderer", "index.html");
2077
+ const htmlPath = path6.join(APP_ROOT, "dist", "renderer", "index.html");
1291
2078
  void win.loadFile(htmlPath);
1292
2079
  ensureStateBridge();
1293
2080
  }
1294
2081
  app.on("window-all-closed", () => {
1295
- killAllRuns("window_closed");
2082
+ void ensureAppExitCleanup("window_closed");
1296
2083
  app.quit();
1297
2084
  });
1298
2085
  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
- }
2086
+ void ensureAppExitCleanup("before_quit");
1306
2087
  });
1307
2088
  app.on("will-quit", () => {
1308
- killAllRuns("will_quit");
1309
- stopCoreServiceHeartbeat();
1310
- void stopCoreServicesBestEffort("will_quit");
1311
- stateBridge.stop();
2089
+ void ensureAppExitCleanup("will_quit", { stopStateBridge: true });
1312
2090
  });
1313
2091
  app.whenReady().then(async () => {
1314
2092
  startCoreServiceHeartbeat();
@@ -1319,6 +2097,9 @@ app.whenReady().then(async () => {
1319
2097
  markUiHeartbeat("main_ready");
1320
2098
  ensureHeartbeatWatchdog();
1321
2099
  createWindow();
2100
+ await uiCliBridge.start().catch((err) => {
2101
+ console.warn("[desktop-console] ui-cli bridge start failed", err);
2102
+ });
1322
2103
  });
1323
2104
  ipcMain2.on("preload:test", () => {
1324
2105
  console.log("[preload-test] window.api OK");
@@ -1468,9 +2249,58 @@ ipcMain2.handle("env:checkGeoIP", async () => checkGeoIP());
1468
2249
  ipcMain2.handle("env:checkAll", async () => checkEnvironment());
1469
2250
  ipcMain2.handle("env:repairCore", async () => {
1470
2251
  const ok = await startCoreDaemon().catch(() => false);
1471
- const services = await checkServices().catch(() => ({ unifiedApi: false, browserService: false }));
2252
+ const services = await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }));
1472
2253
  return { ok, services };
1473
2254
  });
2255
+ ipcMain2.handle("env:repairDeps", async (_evt, input) => {
2256
+ const wantCore = Boolean(input?.core);
2257
+ const wantBrowser = Boolean(input?.browser);
2258
+ const wantGeoip = Boolean(input?.geoip);
2259
+ const wantReinstall = Boolean(input?.reinstall);
2260
+ const wantUninstall = Boolean(input?.uninstall);
2261
+ const result = { ok: true, core: null, install: null, env: null };
2262
+ if (wantCore) {
2263
+ const coreOk = await startCoreDaemon().catch(() => false);
2264
+ result.core = {
2265
+ ok: coreOk,
2266
+ services: await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }))
2267
+ };
2268
+ if (!coreOk) result.ok = false;
2269
+ }
2270
+ if (wantBrowser || wantGeoip) {
2271
+ const args = [path6.join("apps", "webauto", "entry", "xhs-install.mjs")];
2272
+ if (wantReinstall) args.push("--reinstall");
2273
+ else if (wantUninstall) args.push("--uninstall");
2274
+ else args.push("--install");
2275
+ if (wantBrowser) args.push("--download-browser");
2276
+ if (wantGeoip) args.push("--download-geoip");
2277
+ if (!wantUninstall) args.push("--ensure-backend");
2278
+ const installRes = await runJson({
2279
+ title: "env repair deps",
2280
+ cwd: REPO_ROOT2,
2281
+ args,
2282
+ timeoutMs: 3e5
2283
+ }).catch((err) => ({ ok: false, error: err?.message || String(err) }));
2284
+ result.install = installRes;
2285
+ if (!installRes?.ok) result.ok = false;
2286
+ }
2287
+ result.env = await checkEnvironment().catch(() => null);
2288
+ return result;
2289
+ });
2290
+ ipcMain2.handle("env:cleanup", async () => {
2291
+ markUiHeartbeat("env_cleanup");
2292
+ await cleanupRuntimeEnvironment("env_cleanup", {
2293
+ stopUiBridge: false,
2294
+ stopHeartbeat: false,
2295
+ stopCoreServices: false,
2296
+ stopStateBridge: false,
2297
+ includeLockCleanup: true
2298
+ });
2299
+ return {
2300
+ ok: true,
2301
+ services: await checkServices().catch(() => ({ unifiedApi: false, camoRuntime: false }))
2302
+ };
2303
+ });
1474
2304
  ipcMain2.handle("config:saveLast", async (_evt, config) => {
1475
2305
  await saveCrawlConfig({ appRoot: APP_ROOT, repoRoot: REPO_ROOT2 }, config);
1476
2306
  return { ok: true };
@@ -1561,7 +2391,7 @@ ipcMain2.handle("runtime:kill", async (_evt, input) => {
1561
2391
  ipcMain2.handle("runtime:restartPhase1", async (_evt, input) => {
1562
2392
  const profileId = String(input?.profileId || "").trim();
1563
2393
  if (!profileId) return { ok: false, error: "missing profileId" };
1564
- const args = [path5.join(REPO_ROOT2, "scripts", "xiaohongshu", "phase1-boot.mjs"), "--profile", profileId, "--headless", "false"];
2394
+ const args = [path6.join(REPO_ROOT2, "scripts", "xiaohongshu", "phase1-boot.mjs"), "--profile", profileId, "--headless", "false"];
1565
2395
  return spawnCommand({ title: `Phase1 restart ${profileId}`, cwd: REPO_ROOT2, args, groupKey: "phase1" });
1566
2396
  });
1567
2397
  ipcMain2.handle("runtime:setBrowserTitle", async (_evt, input) => {