@web-auto/webauto 0.1.1 → 0.1.2

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 (354) hide show
  1. package/apps/desktop-console/default-settings.json +1 -0
  2. package/apps/desktop-console/dist/main/index.mjs +1618 -0
  3. package/apps/desktop-console/{src → dist}/main/preload.mjs +10 -0
  4. package/apps/desktop-console/dist/renderer/index.js +3063 -0
  5. package/apps/desktop-console/entry/ui-console.mjs +299 -0
  6. package/apps/webauto/entry/account.mjs +356 -0
  7. package/apps/webauto/entry/lib/account-detect.mjs +160 -0
  8. package/apps/webauto/entry/lib/account-store.mjs +587 -0
  9. package/apps/webauto/entry/lib/profilepool.mjs +1 -1
  10. package/apps/webauto/entry/xhs-install.mjs +27 -3
  11. package/apps/webauto/entry/xhs-status.mjs +152 -0
  12. package/apps/webauto/entry/xhs-unified.mjs +595 -17
  13. package/bin/webauto.mjs +247 -12
  14. package/dist/apps/webauto/server.js +66 -0
  15. package/dist/modules/camo-backend/src/index.js +575 -0
  16. package/dist/modules/camo-backend/src/internal/BrowserSession.js +817 -0
  17. package/dist/modules/camo-backend/src/internal/ElementRegistry.js +61 -0
  18. package/dist/modules/camo-backend/src/internal/ProfileLock.js +85 -0
  19. package/dist/modules/camo-backend/src/internal/SessionManager.js +172 -0
  20. package/dist/modules/camo-backend/src/internal/container-matcher.js +852 -0
  21. package/dist/modules/camo-backend/src/internal/engine-manager.js +258 -0
  22. package/dist/modules/camo-backend/src/internal/fingerprint.js +203 -0
  23. package/dist/modules/camo-backend/src/internal/pageRuntime.js +29 -0
  24. package/dist/modules/camo-backend/src/internal/runtimeInjector.js +30 -0
  25. package/dist/modules/camo-backend/src/internal/state-bus.js +46 -0
  26. package/dist/modules/camo-backend/src/internal/storage-paths.js +36 -0
  27. package/dist/modules/camo-backend/src/internal/ws-server.js +1202 -0
  28. package/dist/modules/camo-runtime/src/utils/browser-service.mjs +423 -0
  29. package/dist/modules/camo-runtime/src/utils/config.mjs +77 -0
  30. package/dist/modules/container-registry/src/index.js +184 -0
  31. package/dist/modules/logging/src/index.js +92 -0
  32. package/dist/modules/operations/src/builtin.js +27 -0
  33. package/dist/modules/operations/src/container-binding.js +75 -0
  34. package/dist/modules/operations/src/executor.js +146 -0
  35. package/dist/modules/operations/src/operations/click.js +167 -0
  36. package/dist/modules/operations/src/operations/extract.js +204 -0
  37. package/dist/modules/operations/src/operations/find-child.js +17 -0
  38. package/dist/modules/operations/src/operations/highlight.js +138 -0
  39. package/dist/modules/operations/src/operations/key.js +61 -0
  40. package/dist/modules/operations/src/operations/navigate.js +148 -0
  41. package/dist/modules/operations/src/operations/scroll.js +126 -0
  42. package/dist/modules/operations/src/operations/type.js +190 -0
  43. package/dist/modules/operations/src/queue.js +100 -0
  44. package/dist/modules/operations/src/registry.js +11 -0
  45. package/dist/modules/operations/src/system/mouse.js +33 -0
  46. package/dist/modules/state/src/atomic-json.js +33 -0
  47. package/dist/modules/workflow/blocks/AnchorVerificationBlock.js +71 -0
  48. package/dist/modules/workflow/blocks/BehaviorRandomizer.js +26 -0
  49. package/dist/modules/workflow/blocks/CallWorkflowBlock.js +38 -0
  50. package/dist/modules/workflow/blocks/CloseDetailBlock.js +209 -0
  51. package/dist/modules/workflow/blocks/CollectBatch.js +137 -0
  52. package/dist/modules/workflow/blocks/CollectCommentsBlock.js +415 -0
  53. package/dist/modules/workflow/blocks/CollectSearchListBlock.js +599 -0
  54. package/dist/modules/workflow/blocks/CollectWeiboPosts.js +229 -0
  55. package/dist/modules/workflow/blocks/DetectPageStateBlock.js +259 -0
  56. package/dist/modules/workflow/blocks/EnsureLoginBlock.js +162 -0
  57. package/dist/modules/workflow/blocks/EnsureSession.js +426 -0
  58. package/dist/modules/workflow/blocks/ErrorClassifier.js +164 -0
  59. package/dist/modules/workflow/blocks/ErrorRecoveryBlock.js +319 -0
  60. package/dist/modules/workflow/blocks/ExpandCommentsBlock.js +1032 -0
  61. package/dist/modules/workflow/blocks/ExtractDetailBlock.js +310 -0
  62. package/dist/modules/workflow/blocks/ExtractPostFields.js +88 -0
  63. package/dist/modules/workflow/blocks/GenerateSmartReplyBlock.js +68 -0
  64. package/dist/modules/workflow/blocks/GoToSearchBlock.js +497 -0
  65. package/dist/modules/workflow/blocks/GracefulFallbackBlock.js +104 -0
  66. package/dist/modules/workflow/blocks/HighlightBlock.js +66 -0
  67. package/dist/modules/workflow/blocks/InitAutoScroll.js +65 -0
  68. package/dist/modules/workflow/blocks/LoadContainerDefinition.js +50 -0
  69. package/dist/modules/workflow/blocks/LoadContainerIndex.js +43 -0
  70. package/dist/modules/workflow/blocks/LocateAndGuardBlock.js +176 -0
  71. package/dist/modules/workflow/blocks/LoginRecoveryBlock.js +242 -0
  72. package/dist/modules/workflow/blocks/MatchContainers.js +64 -0
  73. package/dist/modules/workflow/blocks/MonitoringBlock.js +190 -0
  74. package/dist/modules/workflow/blocks/OpenDetailBlock.js +1240 -0
  75. package/dist/modules/workflow/blocks/OrganizeXhsNotesBlock.js +117 -0
  76. package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +270 -0
  77. package/dist/modules/workflow/blocks/PickSinglePost.js +69 -0
  78. package/dist/modules/workflow/blocks/ProgressTracker.js +125 -0
  79. package/dist/modules/workflow/blocks/RecordFixtureBlock.js +44 -0
  80. package/dist/modules/workflow/blocks/RenderMarkdown.js +48 -0
  81. package/dist/modules/workflow/blocks/SaveFile.js +54 -0
  82. package/dist/modules/workflow/blocks/ScrollNextBatch.js +72 -0
  83. package/dist/modules/workflow/blocks/SessionHealthBlock.js +73 -0
  84. package/dist/modules/workflow/blocks/StartBrowserService.js +45 -0
  85. package/dist/modules/workflow/blocks/ValidateContainerDefinition.js +67 -0
  86. package/dist/modules/workflow/blocks/ValidateExtract.js +35 -0
  87. package/dist/modules/workflow/blocks/WaitSearchPermitBlock.js +162 -0
  88. package/dist/modules/workflow/blocks/WaitStable.js +74 -0
  89. package/dist/modules/workflow/blocks/WarmupCommentsBlock.js +120 -0
  90. package/dist/modules/workflow/blocks/WorkflowExecutor.js +156 -0
  91. package/dist/modules/workflow/blocks/XiaohongshuCollectFromLinksBlock.js +1004 -0
  92. package/dist/modules/workflow/blocks/XiaohongshuCollectLinksBlock.js +1049 -0
  93. package/dist/modules/workflow/blocks/XiaohongshuFullCollectBlock.js +782 -0
  94. package/dist/modules/workflow/blocks/helpers/anchorVerify.js +198 -0
  95. package/dist/modules/workflow/blocks/helpers/asyncWorkQueue.js +53 -0
  96. package/dist/modules/workflow/blocks/helpers/commentScroller.js +334 -0
  97. package/dist/modules/workflow/blocks/helpers/commentSectionLocator.js +126 -0
  98. package/dist/modules/workflow/blocks/helpers/containerAnchors.js +301 -0
  99. package/dist/modules/workflow/blocks/helpers/debugArtifacts.js +6 -0
  100. package/dist/modules/workflow/blocks/helpers/downloadPaths.js +29 -0
  101. package/dist/modules/workflow/blocks/helpers/expandCommentsController.js +53 -0
  102. package/dist/modules/workflow/blocks/helpers/expandCommentsExtractor.js +129 -0
  103. package/dist/modules/workflow/blocks/helpers/macosVisionOcrPlugin.js +116 -0
  104. package/dist/modules/workflow/blocks/helpers/mergeXhsMarkdown.js +109 -0
  105. package/dist/modules/workflow/blocks/helpers/openDetailController.js +56 -0
  106. package/dist/modules/workflow/blocks/helpers/openDetailTypes.js +7 -0
  107. package/dist/modules/workflow/blocks/helpers/openDetailViewport.js +474 -0
  108. package/dist/modules/workflow/blocks/helpers/openDetailWaiter.js +104 -0
  109. package/dist/modules/workflow/blocks/helpers/operationLogger.js +195 -0
  110. package/dist/modules/workflow/blocks/helpers/persistedNotes.js +107 -0
  111. package/dist/modules/workflow/blocks/helpers/replyExpander.js +260 -0
  112. package/dist/modules/workflow/blocks/helpers/scrollIntoView.js +138 -0
  113. package/dist/modules/workflow/blocks/helpers/searchExecutor.js +328 -0
  114. package/dist/modules/workflow/blocks/helpers/searchGate.js +46 -0
  115. package/dist/modules/workflow/blocks/helpers/searchPageState.js +164 -0
  116. package/dist/modules/workflow/blocks/helpers/searchResultWaiter.js +64 -0
  117. package/dist/modules/workflow/blocks/helpers/simpleAnchor.js +134 -0
  118. package/dist/modules/workflow/blocks/helpers/smartReply.js +40 -0
  119. package/dist/modules/workflow/blocks/helpers/systemInput.js +635 -0
  120. package/dist/modules/workflow/blocks/helpers/targetCountMode.js +9 -0
  121. package/dist/modules/workflow/blocks/helpers/xhsCliArgs.js +80 -0
  122. package/dist/modules/workflow/blocks/helpers/xhsCommentDom.js +805 -0
  123. package/dist/modules/workflow/blocks/helpers/xhsNoteOrganizer.js +140 -0
  124. package/dist/modules/workflow/blocks/restore/RestorePhaseBlock.js +204 -0
  125. package/dist/modules/workflow/config/workflowRegistry.js +32 -0
  126. package/dist/modules/workflow/definitions/batch-collect-workflow.js +63 -0
  127. package/dist/modules/workflow/definitions/scroll-extract-workflow.js +74 -0
  128. package/dist/modules/workflow/definitions/xiaohongshu-collect-workflow-v2.js +81 -0
  129. package/dist/modules/workflow/definitions/xiaohongshu-collect-workflow.js +57 -0
  130. package/dist/modules/workflow/definitions/xiaohongshu-full-collect-workflow-v3.js +68 -0
  131. package/dist/modules/workflow/definitions/xiaohongshu-note-collect.js +49 -0
  132. package/dist/modules/workflow/definitions/xiaohongshu-phase1-workflow-v3.js +30 -0
  133. package/dist/modules/workflow/definitions/xiaohongshu-phase2-links-workflow-v3.js +40 -0
  134. package/dist/modules/workflow/definitions/xiaohongshu-phase3-collect-workflow-v1.js +54 -0
  135. package/dist/modules/workflow/definitions/xiaohongshu-phase34-from-links-workflow-v3.js +25 -0
  136. package/dist/modules/workflow/src/WeiboEventDrivenWorkflowRunner.js +308 -0
  137. package/dist/modules/workflow/src/context.js +70 -0
  138. package/dist/modules/workflow/src/index.js +5 -0
  139. package/dist/modules/workflow/src/orchestrator.js +230 -0
  140. package/dist/modules/workflow/src/runner.js +55 -0
  141. package/dist/modules/workflow/src/runtime.js +70 -0
  142. package/dist/modules/workflow/workflows/WeiboFeedExtractionWorkflow.js +359 -0
  143. package/dist/modules/workflow/workflows/XiaohongshuLoginWorkflow.js +110 -0
  144. package/dist/modules/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
  145. package/dist/modules/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
  146. package/dist/modules/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
  147. package/dist/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
  148. package/dist/modules/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
  149. package/dist/modules/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
  150. package/dist/modules/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
  151. package/dist/modules/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
  152. package/dist/modules/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
  153. package/dist/modules/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
  154. package/dist/modules/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
  155. package/dist/modules/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
  156. package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
  157. package/dist/modules/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
  158. package/dist/modules/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
  159. package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
  160. package/dist/modules/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
  161. package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
  162. package/dist/modules/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
  163. package/dist/modules/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
  164. package/dist/modules/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
  165. package/dist/modules/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
  166. package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +42 -0
  167. package/dist/modules/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
  168. package/dist/modules/xiaohongshu/app/src/index.js +9 -0
  169. package/dist/modules/xiaohongshu/app/src/utils/checkpoints.js +222 -0
  170. package/dist/modules/xiaohongshu/app/src/utils/controllerAction.js +43 -0
  171. package/dist/services/controller/src/controller.js +1476 -0
  172. package/dist/services/controller/src/index.js +2 -0
  173. package/dist/services/controller/src/payload-normalizer.js +129 -0
  174. package/dist/services/shared/heartbeat.js +120 -0
  175. package/dist/services/shared/lib/errorHandler.js +2 -0
  176. package/dist/services/shared/serviceProcessLogger.js +139 -0
  177. package/dist/services/unified-api/RemoteBrowserSession.js +176 -0
  178. package/dist/services/unified-api/RemoteSessionManager.js +148 -0
  179. package/dist/services/unified-api/container-operations-handler.js +115 -0
  180. package/dist/services/unified-api/server.js +652 -0
  181. package/dist/services/unified-api/state-registry.js +274 -0
  182. package/dist/services/unified-api/task-persistence.js +66 -0
  183. package/dist/services/unified-api/task-state.js +130 -0
  184. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +12 -5
  185. package/modules/xiaohongshu/app/pnpm-lock.yaml +24 -0
  186. package/package.json +37 -9
  187. package/.beads/README.md +0 -81
  188. package/.beads/config.yaml +0 -67
  189. package/.beads/interactions.jsonl +0 -0
  190. package/.beads/issues.jsonl +0 -180
  191. package/.beads/metadata.json +0 -4
  192. package/.claude/settings.local.json +0 -10
  193. package/.github/workflows/ci.yml +0 -55
  194. package/AGENTS.md +0 -253
  195. package/apps/desktop-console/README.md +0 -27
  196. package/apps/desktop-console/package-lock.json +0 -897
  197. package/apps/desktop-console/package.json +0 -20
  198. package/apps/desktop-console/scripts/build-and-install.mjs +0 -19
  199. package/apps/desktop-console/scripts/build.mjs +0 -45
  200. package/apps/desktop-console/scripts/test-preload.mjs +0 -13
  201. package/apps/desktop-console/src/main/config.mts +0 -26
  202. package/apps/desktop-console/src/main/core-daemon-manager.mts +0 -131
  203. package/apps/desktop-console/src/main/desktop-settings.mts +0 -267
  204. package/apps/desktop-console/src/main/heartbeat-watchdog.mts +0 -50
  205. package/apps/desktop-console/src/main/heartbeat-watchdog.test.mts +0 -68
  206. package/apps/desktop-console/src/main/index-streaming.test.mts +0 -20
  207. package/apps/desktop-console/src/main/index.mts +0 -980
  208. package/apps/desktop-console/src/main/profile-store.mts +0 -239
  209. package/apps/desktop-console/src/main/profile-store.test.mts +0 -54
  210. package/apps/desktop-console/src/main/state-bridge.mts +0 -114
  211. package/apps/desktop-console/src/main/task-state-types.ts +0 -32
  212. package/apps/desktop-console/src/renderer/hooks/use-task-state.mts +0 -120
  213. package/apps/desktop-console/src/renderer/index.mts +0 -133
  214. package/apps/desktop-console/src/renderer/index.test.mts +0 -34
  215. package/apps/desktop-console/src/renderer/path-helpers.mts +0 -46
  216. package/apps/desktop-console/src/renderer/path-helpers.test.mts +0 -14
  217. package/apps/desktop-console/src/renderer/tabs/debug.mts +0 -48
  218. package/apps/desktop-console/src/renderer/tabs/debug.test.mts +0 -22
  219. package/apps/desktop-console/src/renderer/tabs/logs.mts +0 -421
  220. package/apps/desktop-console/src/renderer/tabs/logs.test.mts +0 -27
  221. package/apps/desktop-console/src/renderer/tabs/preflight.mts +0 -486
  222. package/apps/desktop-console/src/renderer/tabs/preflight.test.mts +0 -33
  223. package/apps/desktop-console/src/renderer/tabs/profile-pool.mts +0 -213
  224. package/apps/desktop-console/src/renderer/tabs/results.mts +0 -171
  225. package/apps/desktop-console/src/renderer/tabs/run.test.mts +0 -63
  226. package/apps/desktop-console/src/renderer/tabs/runtime.mts +0 -151
  227. package/apps/desktop-console/src/renderer/tabs/settings.mts +0 -146
  228. package/apps/desktop-console/src/renderer/tabs/xiaohongshu/account-flow.mts +0 -486
  229. package/apps/desktop-console/src/renderer/tabs/xiaohongshu/guide-browser-check.mts +0 -56
  230. package/apps/desktop-console/src/renderer/tabs/xiaohongshu/helpers.mts +0 -262
  231. package/apps/desktop-console/src/renderer/tabs/xiaohongshu/layout-block.mts +0 -430
  232. package/apps/desktop-console/src/renderer/tabs/xiaohongshu/live-stats.mts +0 -847
  233. package/apps/desktop-console/src/renderer/tabs/xiaohongshu/run-flow.mts +0 -443
  234. package/apps/desktop-console/src/renderer/tabs/xiaohongshu-state.mts +0 -425
  235. package/apps/desktop-console/src/renderer/tabs/xiaohongshu.mts +0 -497
  236. package/apps/desktop-console/src/renderer/tabs/xiaohongshu.test.mts +0 -291
  237. package/apps/desktop-console/src/renderer/ui-components.mts +0 -31
  238. package/docs/README_camoufox_chinese.md +0 -141
  239. package/docs/USAGE_V3.md +0 -163
  240. package/docs/arch/OCR_MACOS_PLUGIN.md +0 -39
  241. package/docs/arch/PORTS.md +0 -40
  242. package/docs/arch/REGRESSION_CHECKLIST.md +0 -121
  243. package/docs/arch/SEARCH_GATE.md +0 -224
  244. package/docs/arch/VIEWPORT_SAFETY.md +0 -182
  245. package/docs/arch/XIAOHONGSHU_OFFLINE_MOCK_DESIGN.md +0 -267
  246. package/docs/xiaohongshu-container-driven-summary.md +0 -221
  247. package/docs/xiaohongshu-full-collect-runbook.md +0 -134
  248. package/docs/xiaohongshu-next-steps.md +0 -228
  249. package/docs/xiaohongshu-quickstart.md +0 -73
  250. package/docs/xiaohongshu-workflow-summary.md +0 -227
  251. package/modules/container-registry/tests/container-registry.test.ts +0 -16
  252. package/modules/logging/tests/logging.test.ts +0 -38
  253. package/modules/operations/tests/operations.test.ts +0 -22
  254. package/modules/operations/tests/viewport-filter.test.ts +0 -161
  255. package/modules/operations/tests/visible-only.test.ts +0 -250
  256. package/modules/session-manager/tests/session-manager.test.ts +0 -23
  257. package/modules/state/src/atomic-json.test.ts +0 -30
  258. package/modules/state/src/paths.test.ts +0 -59
  259. package/modules/state/src/xiaohongshu-collect-state.test.ts +0 -259
  260. package/modules/workflow/blocks/AnchorVerificationBlock.d.ts.map +0 -1
  261. package/modules/workflow/blocks/AnchorVerificationBlock.js.map +0 -1
  262. package/modules/workflow/blocks/DetectPageStateBlock.d.ts.map +0 -1
  263. package/modules/workflow/blocks/DetectPageStateBlock.js.map +0 -1
  264. package/modules/workflow/blocks/ErrorRecoveryBlock.d.ts.map +0 -1
  265. package/modules/workflow/blocks/ErrorRecoveryBlock.js.map +0 -1
  266. package/modules/workflow/blocks/WaitSearchPermitBlock.d.ts.map +0 -1
  267. package/modules/workflow/blocks/WaitSearchPermitBlock.js.map +0 -1
  268. package/modules/workflow/blocks/helpers/containerAnchors.d.ts.map +0 -1
  269. package/modules/workflow/blocks/helpers/containerAnchors.js.map +0 -1
  270. package/modules/workflow/blocks/helpers/downloadPaths.test.ts +0 -62
  271. package/modules/workflow/blocks/helpers/mergeXhsMarkdown.test.ts +0 -121
  272. package/modules/workflow/blocks/helpers/operationLogger.d.ts.map +0 -1
  273. package/modules/workflow/blocks/helpers/operationLogger.js.map +0 -1
  274. package/modules/workflow/blocks/helpers/persistedNotes.test.ts +0 -268
  275. package/modules/workflow/blocks/helpers/searchPageState.d.ts.map +0 -1
  276. package/modules/workflow/blocks/helpers/searchPageState.js.map +0 -1
  277. package/modules/workflow/blocks/helpers/targetCountMode.test.ts +0 -29
  278. package/modules/workflow/blocks/helpers/xhsCliArgs.test.ts +0 -75
  279. package/modules/workflow/tests/smartReply.test.ts +0 -32
  280. package/modules/xiaohongshu/app/src/blocks/Phase3Interact.matcher.test.ts +0 -33
  281. package/modules/xiaohongshu/app/src/utils/__tests__/checkpoints.test.ts +0 -141
  282. package/modules/xiaohongshu/app/tests/commentMatchDsl.test.ts +0 -50
  283. package/modules/xiaohongshu/app/tests/commentMatcher.test.ts +0 -46
  284. package/modules/xiaohongshu/app/tests/sharding.test.ts +0 -31
  285. package/package-scripts.json +0 -8
  286. package/runtime/infra/utils/README.md +0 -13
  287. package/runtime/infra/utils/scripts/README.md +0 -0
  288. package/runtime/infra/utils/scripts/development/eval-in-session.mjs +0 -40
  289. package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +0 -35
  290. package/runtime/infra/utils/scripts/service/kill-port.mjs +0 -24
  291. package/runtime/infra/utils/scripts/service/start-api.mjs +0 -39
  292. package/runtime/infra/utils/scripts/service/start-browser-service.mjs +0 -106
  293. package/runtime/infra/utils/scripts/service/stop-api.mjs +0 -18
  294. package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +0 -104
  295. package/runtime/infra/utils/scripts/test-services.mjs +0 -94
  296. package/services/shared/heartbeat.test.ts +0 -102
  297. package/services/unified-api/__tests__/task-state.test.ts +0 -95
  298. package/sitecustomize.py +0 -19
  299. package/tests/README.md +0 -194
  300. package/tests/e2e/workflows/weibo-feed-extraction.test.ts +0 -171
  301. package/tests/fixtures/data/container-definitions.json +0 -67
  302. package/tests/fixtures/pages/simple-page.html +0 -69
  303. package/tests/integration/01-test-container-match.mjs +0 -188
  304. package/tests/integration/02-test-dom-branch.mjs +0 -161
  305. package/tests/integration/03-test-container-operation-system.mjs +0 -91
  306. package/tests/integration/05-test-container-lifecycle-events.mjs +0 -224
  307. package/tests/integration/05-test-container-lifecycle-with-events.mjs +0 -250
  308. package/tests/integration/06-test-container-dom-tree-drawing.mjs +0 -256
  309. package/tests/integration/07-test-weibo-container-lifecycle.mjs +0 -355
  310. package/tests/integration/08-test-weibo-feed-workflow.test.mjs +0 -164
  311. package/tests/integration/10-test-visual-analyzer.mjs +0 -312
  312. package/tests/integration/11-test-visual-loop.mjs +0 -284
  313. package/tests/integration/12-test-simple-visual-loop.mjs +0 -242
  314. package/tests/integration/13-test-visual-robust.mjs +0 -185
  315. package/tests/integration/14-test-visual-highlight-loop.mjs +0 -271
  316. package/tests/integration/inspect-page.mjs +0 -50
  317. package/tests/integration/run-all-tests.mjs +0 -95
  318. package/tests/patch_verification/CODEX_PATCH_TEST.md +0 -103
  319. package/tests/patch_verification/PHASE2_ANALYSIS.md +0 -179
  320. package/tests/patch_verification/PHASE2_OPTIMIZATION_REPORT.md +0 -55
  321. package/tests/patch_verification/PHASE2_TO_PHASE4_SUMMARY.md +0 -126
  322. package/tests/patch_verification/QUICK_TEST_SEQUENCE.md +0 -262
  323. package/tests/patch_verification/README.md +0 -143
  324. package/tests/patch_verification/RUN_TESTS.md +0 -60
  325. package/tests/patch_verification/TEST_EXECUTION.md +0 -99
  326. package/tests/patch_verification/TEST_PLAN.md +0 -328
  327. package/tests/patch_verification/TEST_RESULTS.md +0 -34
  328. package/tests/patch_verification/TOOL_TEST_PLAN.md +0 -48
  329. package/tests/patch_verification/run-tool-test.mjs +0 -121
  330. package/tests/patch_verification/temp_test_files/test01.txt +0 -1
  331. package/tests/patch_verification/temp_test_files/test02.txt +0 -3
  332. package/tests/patch_verification/temp_test_files/test02_gnu.txt +0 -3
  333. package/tests/patch_verification/temp_test_files/test03.txt +0 -1
  334. package/tests/patch_verification/temp_test_files/test03_multiline.txt +0 -5
  335. package/tests/patch_verification/temp_test_files/test04_function.ts +0 -5
  336. package/tests/patch_verification/temp_test_files/test05_import.ts +0 -4
  337. package/tests/patch_verification/temp_test_files/test06_special_chars.txt +0 -4
  338. package/tests/patch_verification/temp_test_files/test07_indentation.ts +0 -5
  339. package/tests/patch_verification/temp_test_files/test08_mismatch.txt +0 -1
  340. package/tests/patch_verification/temp_test_files/test_add_02.txt +0 -3
  341. package/tests/patch_verification/temp_test_files/test_simple.txt +0 -1
  342. package/tests/runner/TestReporter.mjs +0 -57
  343. package/tests/runner/TestRunner.mjs +0 -244
  344. package/tests/unit/commands/profile.test.mjs +0 -10
  345. package/tests/unit/container/change-notifier.test.mjs +0 -181
  346. package/tests/unit/lifecycle/session-registry.test.mjs +0 -135
  347. package/tests/unit/operations/registry.test.ts +0 -73
  348. package/tests/unit/utils/browser-service.test.mjs +0 -153
  349. package/tests/unit/utils/config.test.mjs +0 -166
  350. package/tests/unit/utils/fingerprint.test.mjs +0 -166
  351. package/tsconfig.json +0 -31
  352. package/tsconfig.services.json +0 -26
  353. /package/apps/desktop-console/{src → dist}/renderer/index.html +0 -0
  354. /package/apps/desktop-console/{src/renderer/tabs → dist/renderer}/run.mts +0 -0
@@ -0,0 +1,3063 @@
1
+ // src/renderer/ui-components.mts
2
+ function createEl(tag, props = {}, children = []) {
3
+ const el = document.createElement(tag);
4
+ for (const [k, v] of Object.entries(props)) {
5
+ el[k] = v;
6
+ }
7
+ for (const c of children) {
8
+ if (typeof c === "string") el.appendChild(document.createTextNode(c));
9
+ else el.appendChild(c);
10
+ }
11
+ return el;
12
+ }
13
+ function labeledInput(label, input) {
14
+ const wrap = createEl("div", { style: "display:flex; flex-direction:column; gap:6px; min-width:140px; max-width:260px;" });
15
+ wrap.appendChild(createEl("label", {}, [label]));
16
+ wrap.appendChild(input);
17
+ return wrap;
18
+ }
19
+ function section(title, children) {
20
+ const card = createEl("div", { className: "card" });
21
+ card.appendChild(createEl("div", { style: "font-weight:700; margin-bottom:10px;" }, [title]));
22
+ children.forEach((c) => card.appendChild(c));
23
+ return card;
24
+ }
25
+
26
+ // src/renderer/path-helpers.mts
27
+ function normalizePath(p, api) {
28
+ if (!p) return "";
29
+ if (api?.pathNormalize) return String(api.pathNormalize(p));
30
+ return String(p);
31
+ }
32
+ function joinPath(api, ...parts) {
33
+ if (api?.pathJoin) return String(api.pathJoin(...parts));
34
+ const sep = api?.pathSep || "/";
35
+ return parts.filter(Boolean).join(sep);
36
+ }
37
+ function resolveWebautoRoot(downloadRoot, api) {
38
+ const raw = String(downloadRoot || "").trim();
39
+ if (!raw) {
40
+ return api?.pathSep === "\\" ? "%USERPROFILE%\\.webauto" : "~/.webauto";
41
+ }
42
+ const normalized = normalizePath(raw, api);
43
+ const sep = api?.pathSep || "/";
44
+ const suffix = `${sep}download`;
45
+ if (normalized.toLowerCase().endsWith(suffix.toLowerCase())) {
46
+ return normalized.slice(0, -suffix.length);
47
+ }
48
+ return normalized;
49
+ }
50
+ function resolveConfigPath(downloadRoot, api) {
51
+ const root = resolveWebautoRoot(downloadRoot, api);
52
+ return joinPath(api, root, "config.json");
53
+ }
54
+
55
+ // src/renderer/tabs/preflight.mts
56
+ function buildArgs(parts) {
57
+ return parts.filter((x) => x != null && String(x).trim() !== "");
58
+ }
59
+ var XHS_NAV_TARGET_KEY = "webauto.xhs.navTarget.v1";
60
+ function renderPreflight(root, ctx2) {
61
+ const filterInput = createEl("input", { value: "", placeholder: "\u8FC7\u6EE4\uFF1AprofileId / alias / platform" });
62
+ const selectAllBox = createEl("input", { type: "checkbox" });
63
+ const selectionHint = createEl("div", { className: "muted" }, ["selected=0"]);
64
+ const onlyMissingFp = createEl("input", { type: "checkbox" });
65
+ const regenPlatform = createEl("select");
66
+ [
67
+ { value: "random", label: "random(win/mac)" },
68
+ { value: "windows", label: "windows" },
69
+ { value: "macos", label: "macos" }
70
+ ].forEach((x) => regenPlatform.appendChild(createEl("option", { value: x.value }, [x.label])));
71
+ regenPlatform.value = "random";
72
+ const statusBox = createEl("div", { className: "muted" }, [""]);
73
+ const listBox = createEl("div", { className: "list" });
74
+ const batchDeleteBtn = createEl("button", { className: "danger" }, ["\u6279\u91CF\u5220\u9664\u9009\u4E2D profile"]);
75
+ const batchDeleteFp = createEl("input", { type: "checkbox" });
76
+ const onboardingSummary = createEl("div", { className: "muted" }, ["\u6B63\u5728\u52A0\u8F7D profile \u4FE1\u606F..."]);
77
+ const onboardingTips = createEl("div", { className: "muted", style: "font-size:12px; margin-top:6px;" }, [
78
+ "\u9996\u6B21\u4F7F\u7528\u5EFA\u8BAE\uFF1A\u8F93\u5165\u8D26\u53F7\u540D\u540E\uFF0C\u7CFB\u7EDF\u81EA\u52A8\u6309 <\u8D26\u53F7\u540D>-batch-1/2/3 \u547D\u540D\uFF1B\u7559\u7A7A\u9ED8\u8BA4 xiaohongshu-batch-1/2/3\u3002\u767B\u5F55\u540E\u53EF\u8BBE\u7F6E alias\uFF08\u8D26\u53F7\u540D\uFF09\u7528\u4E8E\u533A\u5206\uFF0C\u9ED8\u8BA4\u4F1A\u81EA\u52A8\u83B7\u53D6\u7528\u6237\u540D\u3002"
79
+ ]);
80
+ const gotoXhsBtn = createEl("button", { className: "secondary", type: "button" }, ["\u53BB\u5C0F\u7EA2\u4E66\u9996\u9875"]);
81
+ const browserStatus = createEl("div", { className: "muted" }, ["\u6D4F\u89C8\u5668\u72B6\u6001\uFF1A\u672A\u68C0\u67E5"]);
82
+ const browserCheckBtn = createEl("button", { className: "secondary", type: "button" }, ["\u68C0\u67E5\u6D4F\u89C8\u5668/\u4F9D\u8D56"]);
83
+ const browserDownloadBtn = createEl("button", { type: "button" }, ["\u4E0B\u8F7D Camoufox"]);
84
+ let cachedScan = null;
85
+ const webautoRoot = resolveWebautoRoot(ctx2.settings?.downloadRoot || "", window.api);
86
+ function getAliasMap() {
87
+ const m = ctx2.settings?.profileAliases;
88
+ return m && typeof m === "object" ? m : {};
89
+ }
90
+ async function saveAlias(profileId, alias) {
91
+ const nextAliases = { ...getAliasMap() };
92
+ const key = String(profileId || "").trim();
93
+ const val = String(alias || "").trim();
94
+ if (!key) return;
95
+ if (!val) delete nextAliases[key];
96
+ else nextAliases[key] = val;
97
+ const next = await window.api.settingsSet({ profileAliases: nextAliases });
98
+ ctx2.settings = next;
99
+ }
100
+ function toLower(x) {
101
+ return String(x || "").toLowerCase();
102
+ }
103
+ function renderList() {
104
+ listBox.textContent = "";
105
+ const entries = cachedScan?.entries || [];
106
+ const q = toLower(filterInput.value.trim());
107
+ const aliases = getAliasMap();
108
+ const filtered = entries.filter((e) => {
109
+ if (onlyMissingFp.checked && e.fingerprint) return false;
110
+ if (!q) return true;
111
+ const hay = [
112
+ e.profileId,
113
+ aliases[e.profileId] || "",
114
+ e.fingerprint?.platform || "",
115
+ e.fingerprint?.originalPlatform || "",
116
+ e.fingerprint?.osVersion || "",
117
+ e.fingerprint?.userAgent || ""
118
+ ].map(toLower).join(" ");
119
+ return hay.includes(q);
120
+ });
121
+ const header = createEl("div", {
122
+ className: "item",
123
+ // One-row-per-profile, keep actions on a single line; avoid horizontal scroll.
124
+ style: "display:grid; grid-template-columns: 32px 180px 180px 120px minmax(0,1fr) 360px; gap:8px; font-weight:700; align-items:center; min-width:0;"
125
+ });
126
+ header.appendChild(createEl("div", {}, ["sel"]));
127
+ header.appendChild(createEl("div", {}, ["profileId"]));
128
+ header.appendChild(createEl("div", {}, ["alias"]));
129
+ header.appendChild(createEl("div", {}, ["fingerprint"]));
130
+ header.appendChild(createEl("div", {}, ["userAgent"]));
131
+ header.appendChild(createEl("div", {}, ["actions"]));
132
+ listBox.appendChild(header);
133
+ for (const e of filtered) {
134
+ const fp = e.fingerprint;
135
+ const fpLabel = fp ? `${fp.originalPlatform || fp.platform || "unknown"}${fp.osVersion ? ` ${fp.osVersion}` : ""}` : "(missing)";
136
+ const aliasInput = createEl("input", {
137
+ value: String(aliases[e.profileId] || ""),
138
+ placeholder: "\u8D26\u53F7\u540D\uFF08alias\uFF0C\u7528\u4E8E\u533A\u5206\u8D26\u53F7\uFF0C\u9ED8\u8BA4\u767B\u5F55\u540E\u83B7\u53D6\u7528\u6237\u540D\uFF09"
139
+ });
140
+ const row = createEl("div", {
141
+ className: "item",
142
+ style: "display:grid; grid-template-columns: 32px 180px 180px 120px minmax(0,1fr) 360px; gap:8px; align-items:center; min-width:0;"
143
+ });
144
+ const rowSelect = createEl("input", { type: "checkbox" });
145
+ rowSelect.dataset.profileId = String(e.profileId || "");
146
+ rowSelect.onchange = () => updateSelectionHint();
147
+ row.appendChild(rowSelect);
148
+ row.appendChild(createEl("div", {}, [String(e.profileId)]));
149
+ row.appendChild(createEl("div", { style: "min-width:0;" }, [aliasInput]));
150
+ row.appendChild(createEl("div", { className: "muted" }, [fpLabel]));
151
+ row.appendChild(
152
+ createEl(
153
+ "div",
154
+ { className: "muted", style: "overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0;", title: String(fp?.userAgent || "") },
155
+ [String(fp?.userAgent || "")]
156
+ )
157
+ );
158
+ const actions = createEl("div", { style: "display:flex; gap:6px; flex-wrap:nowrap; align-items:center; overflow:hidden; justify-content:flex-end;" });
159
+ const btnOpenProfile = createEl("button", { className: "secondary" }, ["\u6253\u5F00"]);
160
+ const btnOpenFp = createEl("button", { className: "secondary" }, ["\u6307\u7EB9"]);
161
+ const btnSaveAlias = createEl("button", { className: "secondary" }, ["\u4FDD\u5B58"]);
162
+ const btnRegenFp = createEl("button", {}, ["\u91CD\u751F"]);
163
+ const btnDelFp = createEl("button", { className: "secondary" }, ["\u5220\u6307"]);
164
+ const btnDelProfile = createEl("button", { className: "danger" }, ["\u5220\u6863"]);
165
+ btnOpenProfile.onclick = () => void window.api.osOpenPath(e.profileDir);
166
+ btnOpenFp.onclick = () => void window.api.osOpenPath(e.fingerprintPath);
167
+ let lastSavedAlias = String(aliases[e.profileId] || "").trim();
168
+ const commitAlias = async () => {
169
+ const nextAlias = String(aliasInput.value || "").trim();
170
+ if (nextAlias === lastSavedAlias) return;
171
+ await saveAlias(e.profileId, nextAlias);
172
+ lastSavedAlias = nextAlias;
173
+ aliasInput.style.borderColor = "";
174
+ };
175
+ aliasInput.oninput = () => {
176
+ const dirty = String(aliasInput.value || "").trim() !== lastSavedAlias;
177
+ aliasInput.style.borderColor = dirty ? "#2b67ff" : "";
178
+ };
179
+ btnSaveAlias.onclick = () => void commitAlias();
180
+ aliasInput.onkeydown = (ev) => {
181
+ if (ev.key === "Enter") void commitAlias();
182
+ };
183
+ aliasInput.onblur = () => void commitAlias();
184
+ btnRegenFp.onclick = async () => {
185
+ ctx2.clearLog();
186
+ const platform = String(regenPlatform.value || "random");
187
+ const out = await window.api.fingerprintRegenerate({ profileId: e.profileId, platform });
188
+ ctx2.appendLog(JSON.stringify(out, null, 2));
189
+ await refreshScan();
190
+ };
191
+ btnDelFp.onclick = async () => {
192
+ if (!confirm(`\u5220\u9664 fingerprint: ${e.profileId}?`)) return;
193
+ ctx2.clearLog();
194
+ const out = await window.api.fingerprintDelete({ profileId: e.profileId });
195
+ ctx2.appendLog(JSON.stringify(out, null, 2));
196
+ await refreshScan();
197
+ };
198
+ btnDelProfile.onclick = async () => {
199
+ if (!confirm(`\u5220\u9664 profile \u76EE\u5F55: ${e.profileId}?
200
+ (\u53EF\u9009\uFF1A\u540C\u65F6\u5220\u9664\u6307\u7EB9\u6587\u4EF6)`)) return;
201
+ const alsoFp = confirm(`\u540C\u65F6\u5220\u9664 ${e.profileId} \u7684 fingerprint \u6587\u4EF6\uFF1F`);
202
+ ctx2.clearLog();
203
+ const out = await window.api.profileDelete({ profileId: e.profileId, deleteFingerprint: alsoFp });
204
+ ctx2.appendLog(JSON.stringify(out, null, 2));
205
+ await refreshScan();
206
+ };
207
+ actions.appendChild(btnOpenProfile);
208
+ actions.appendChild(btnOpenFp);
209
+ actions.appendChild(btnSaveAlias);
210
+ actions.appendChild(btnRegenFp);
211
+ actions.appendChild(btnDelFp);
212
+ actions.appendChild(btnDelProfile);
213
+ row.appendChild(actions);
214
+ listBox.appendChild(row);
215
+ }
216
+ updateSelectionHint();
217
+ }
218
+ function updateSelectionHint() {
219
+ const selected = listBox.querySelectorAll('input[type="checkbox"][data-profile-id]:checked').length;
220
+ selectionHint.textContent = `selected=${selected}`;
221
+ }
222
+ async function batchDeleteSelectedProfiles() {
223
+ const selected = [];
224
+ listBox.querySelectorAll('input[type="checkbox"][data-profile-id]').forEach((el) => {
225
+ const cb = el;
226
+ if (!cb.checked) return;
227
+ const id = String(cb.dataset.profileId || "").trim();
228
+ if (id) selected.push(id);
229
+ });
230
+ if (selected.length === 0) return;
231
+ if (!confirm(`\u6279\u91CF\u5220\u9664 ${selected.length} \u4E2A profile\uFF1F`)) return;
232
+ const deleteFp = batchDeleteFp.checked && confirm(`\u540C\u65F6\u5220\u9664\u8FD9 ${selected.length} \u4E2A profile \u7684 fingerprint \u6587\u4EF6\uFF1F`);
233
+ ctx2.clearLog();
234
+ for (const profileId of selected) {
235
+ try {
236
+ const out = await window.api.profileDelete({ profileId, deleteFingerprint: deleteFp });
237
+ ctx2.appendLog(JSON.stringify(out, null, 2));
238
+ } catch (err) {
239
+ ctx2.appendLog(`[batch-delete] ${profileId} failed: ${err?.message || String(err)}`);
240
+ }
241
+ }
242
+ await refreshScan();
243
+ }
244
+ async function refreshScan() {
245
+ const out = await window.api.profilesScan();
246
+ cachedScan = out;
247
+ statusBox.textContent = `profiles=${out?.entries?.length || 0} profilesRoot=${out?.profilesRoot || ""} fingerprintsRoot=${out?.fingerprintsRoot || ""}`;
248
+ const entries = out?.entries || [];
249
+ const aliases = getAliasMap();
250
+ const aliasedCount = entries.filter((e) => String(aliases[String(e?.profileId || "")] || "").trim()).length;
251
+ onboardingSummary.textContent = `profile=${entries.length}\uFF0C\u5DF2\u8BBE\u7F6E\u8D26\u53F7\u540D=${aliasedCount}`;
252
+ if (entries.length === 0) {
253
+ onboardingTips.textContent = "\u5F53\u524D\u6CA1\u6709\u53EF\u7528 profile\uFF1A\u8BF7\u5148\u5728\u4E0B\u65B9\u201C\u6279\u91CF\u8D26\u53F7\u6C60\u201D\u91CC\u65B0\u589E\u8D26\u53F7\u3002";
254
+ } else if (aliasedCount < entries.length) {
255
+ onboardingTips.textContent = `\u4ECD\u6709 ${entries.length - aliasedCount} \u4E2A profile \u672A\u8BBE\u7F6E\u8D26\u53F7\u540D\uFF08alias\uFF09\u3002alias \u7528\u4E8E\u533A\u5206\u8D26\u53F7\uFF0C\u9ED8\u8BA4\u767B\u5F55\u540E\u4F1A\u81EA\u52A8\u83B7\u53D6\u7528\u6237\u540D\u3002`;
256
+ } else {
257
+ onboardingTips.textContent = "\u5F88\u597D\uFF1A\u6240\u6709 profile \u90FD\u6709\u8D26\u53F7\u540D\uFF08alias\uFF09\uFF0C\u5728\u5C0F\u7EA2\u4E66\u9996\u9875\u4F1A\u6309\u201C\u8D26\u53F7\u540D (profileId)\u201D\u663E\u793A\u3002";
258
+ }
259
+ renderList();
260
+ }
261
+ const toolbar = createEl("div", { className: "row" }, [
262
+ labeledInput("filter", filterInput),
263
+ createEl("div", { style: "display:flex; flex-direction:column; gap:6px;" }, [
264
+ createEl("label", {}, ["select all"]),
265
+ selectAllBox
266
+ ]),
267
+ createEl("div", { style: "display:flex; flex-direction:column; gap:6px;" }, [
268
+ createEl("label", {}, ["del fingerprint"]),
269
+ batchDeleteFp
270
+ ]),
271
+ createEl("div", { style: "display:flex; flex-direction:column; gap:6px;" }, [
272
+ createEl("label", {}, ["only missing fingerprint"]),
273
+ onlyMissingFp
274
+ ]),
275
+ labeledInput("regen platform", regenPlatform),
276
+ createEl("button", { className: "secondary" }, ["\u5237\u65B0\u5217\u8868"]),
277
+ createEl("button", {}, ["\u6279\u91CF\u8865\u9F50\u7F3A\u5931\u6307\u7EB9\uFF08\u811A\u672C\uFF09"]),
278
+ batchDeleteBtn
279
+ ]);
280
+ toolbar.children[5].onclick = () => void refreshScan();
281
+ toolbar.children[6].onclick = async () => {
282
+ ctx2.clearLog();
283
+ const args = buildArgs([window.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"), "migrate-fingerprints"]);
284
+ await window.api.cmdSpawn({ title: "migrate fingerprints", cwd: "", args, groupKey: "profilepool" });
285
+ setTimeout(() => void refreshScan(), 1e3);
286
+ };
287
+ batchDeleteBtn.onclick = () => void batchDeleteSelectedProfiles();
288
+ filterInput.oninput = () => renderList();
289
+ onlyMissingFp.onchange = () => renderList();
290
+ selectAllBox.onchange = () => {
291
+ const checked = selectAllBox.checked;
292
+ listBox.querySelectorAll('input[type="checkbox"][data-profile-id]').forEach((el) => {
293
+ el.checked = checked;
294
+ });
295
+ updateSelectionHint();
296
+ };
297
+ const runBrowserCheck = async (opts = {}) => {
298
+ const download = opts.download === true;
299
+ const source = opts.source || "manual";
300
+ const script = window.api.pathJoin("apps", "webauto", "entry", "xhs-install.mjs");
301
+ const args = buildArgs([script, "--check-browser-only", ...download ? ["--download-browser"] : []]);
302
+ browserStatus.textContent = download ? "\u6D4F\u89C8\u5668\u72B6\u6001\uFF1A\u4E0B\u8F7D+\u68C0\u67E5\u4E2D..." : "\u6D4F\u89C8\u5668\u72B6\u6001\uFF1A\u68C0\u67E5\u4E2D...";
303
+ browserStatus.style.color = "#8b93a6";
304
+ if (typeof window.api?.cmdRunJson === "function") {
305
+ const out = await window.api.cmdRunJson({
306
+ title: download ? "xhs install download" : "xhs install check",
307
+ cwd: "",
308
+ args,
309
+ timeoutMs: download ? 24e4 : 12e4
310
+ }).catch((err) => ({ ok: false, error: err?.message || String(err) }));
311
+ const mergedOutput = String(out?.stdout || out?.stderr || out?.error || "").replace(/\x1b\[[0-9;]*m/g, "");
312
+ if (out?.ok) {
313
+ browserStatus.textContent = download ? "\u6D4F\u89C8\u5668\u72B6\u6001\uFF1A\u4E0B\u8F7D\u5E76\u68C0\u67E5\u901A\u8FC7" : "\u6D4F\u89C8\u5668\u72B6\u6001\uFF1A\u68C0\u67E5\u901A\u8FC7";
314
+ browserStatus.style.color = "#22c55e";
315
+ } else if (/Camoufox 未安装/i.test(mergedOutput)) {
316
+ browserStatus.textContent = "\u6D4F\u89C8\u5668\u72B6\u6001\uFF1A\u672A\u5B89\u88C5 Camoufox\uFF08\u53EF\u70B9\u201C\u4E0B\u8F7D Camoufox\u201D\uFF09";
317
+ browserStatus.style.color = "#f59e0b";
318
+ } else {
319
+ browserStatus.textContent = `\u6D4F\u89C8\u5668\u72B6\u6001\uFF1A\u68C0\u67E5\u5931\u8D25\uFF08code=${out?.code ?? "n/a"}\uFF09`;
320
+ browserStatus.style.color = "#ef4444";
321
+ }
322
+ if (source === "manual" && mergedOutput) {
323
+ ctx2.appendLog(`[preflight] install output
324
+ ${mergedOutput}`);
325
+ }
326
+ return;
327
+ }
328
+ if (typeof window.api?.cmdSpawn === "function") {
329
+ await window.api.cmdSpawn({ title: download ? "xhs install download" : "xhs install check", cwd: "", args, groupKey: "install" });
330
+ browserStatus.textContent = "\u6D4F\u89C8\u5668\u72B6\u6001\uFF1A\u5DF2\u89E6\u53D1\u68C0\u67E5\uFF08\u67E5\u770B\u65E5\u5FD7\uFF09";
331
+ browserStatus.style.color = "#22c55e";
332
+ return;
333
+ }
334
+ browserStatus.textContent = "\u6D4F\u89C8\u5668\u72B6\u6001\uFF1A\u68C0\u67E5\u80FD\u529B\u4E0D\u53EF\u7528";
335
+ browserStatus.style.color = "#ef4444";
336
+ };
337
+ gotoXhsBtn.onclick = () => {
338
+ try {
339
+ window.localStorage.setItem(XHS_NAV_TARGET_KEY, "account");
340
+ } catch {
341
+ }
342
+ if (typeof ctx2?.setActiveTab === "function") ctx2.setActiveTab("xiaohongshu");
343
+ };
344
+ browserCheckBtn.onclick = async () => {
345
+ ctx2.clearLog();
346
+ await runBrowserCheck({ source: "manual" });
347
+ };
348
+ browserDownloadBtn.onclick = async () => {
349
+ ctx2.clearLog();
350
+ await runBrowserCheck({ download: true, source: "manual" });
351
+ };
352
+ root.appendChild(
353
+ section("\u9996\u6B21\u5F15\u5BFC\uFF08\u8D26\u53F7\u89C6\u89D2\uFF09", [
354
+ onboardingSummary,
355
+ onboardingTips,
356
+ createEl("div", { className: "row" }, [gotoXhsBtn])
357
+ ])
358
+ );
359
+ root.appendChild(
360
+ section("\u6D4F\u89C8\u5668\u68C0\u67E5\u4E0E\u4E0B\u8F7D", [
361
+ browserStatus,
362
+ createEl("div", { className: "row" }, [browserCheckBtn, browserDownloadBtn]),
363
+ createEl("div", { className: "muted", style: "font-size:12px;" }, ["\u8BF4\u660E\uFF1A\u68C0\u67E5/\u4E0B\u8F7D\u7684\u8BE6\u7EC6\u8F93\u51FA\u8BF7\u67E5\u770B\u65E5\u5FD7\u9875\u3002"])
364
+ ])
365
+ );
366
+ root.appendChild(
367
+ section("Profiles + Fingerprints (CRUD)", [
368
+ toolbar,
369
+ statusBox,
370
+ selectionHint,
371
+ listBox,
372
+ createEl("div", { className: "muted" }, [`\u63D0\u793A\uFF1Aprofile \u4E0E fingerprint \u7684\u771F\u5B9E\u8DEF\u5F84\u5747\u5728 ${webautoRoot} \u4E0B\uFF1Balias \u53EA\u5F71\u54CD UI \u663E\u793A\uFF0C\u4E0D\u5F71\u54CD profileId\u3002`])
373
+ ])
374
+ );
375
+ const keywordInput = createEl("input", { value: "xiaohongshu", placeholder: "\u8D26\u53F7\u540D\uFF08\u53EF\u9009\uFF09\uFF0C\u7CFB\u7EDF\u81EA\u52A8\u62FC\u63A5\u4E3A <\u8D26\u53F7\u540D>-batch-1/2/3\uFF1B\u7559\u7A7A\u9ED8\u8BA4 xiaohongshu-batch" });
376
+ const ensureCountInput = createEl("input", { value: "0", type: "number", min: "0" });
377
+ const timeoutInput = createEl("input", { value: String(ctx2.settings?.timeouts?.loginTimeoutSec || 900), type: "number", min: "30" });
378
+ const keepSession = createEl("input", { type: "checkbox" });
379
+ const poolListBox = createEl("div", { className: "list" });
380
+ const poolStatus = createEl("div", { className: "muted" }, [""]);
381
+ function resolveBatchPrefix() {
382
+ const base = String(keywordInput.value || "").trim() || "xiaohongshu";
383
+ return `${base}-batch`;
384
+ }
385
+ async function poolList() {
386
+ ctx2.clearLog();
387
+ const kw = resolveBatchPrefix();
388
+ const out = await window.api.cmdRunJson({
389
+ title: "profilepool list",
390
+ cwd: "",
391
+ args: buildArgs([window.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"), "list", kw, "--json"])
392
+ });
393
+ poolListBox.textContent = "";
394
+ if (!out?.ok || !out?.json) {
395
+ poolListBox.appendChild(createEl("div", { className: "item" }, ["(failed)"]));
396
+ return;
397
+ }
398
+ const profiles = out.json.profiles || [];
399
+ profiles.forEach((p) => poolListBox.appendChild(createEl("div", { className: "item" }, [p])));
400
+ poolStatus.textContent = `count=${profiles.length} root=${out.json.root}`;
401
+ await refreshScan();
402
+ }
403
+ async function poolAdd() {
404
+ ctx2.clearLog();
405
+ const kw = resolveBatchPrefix();
406
+ const out = await window.api.cmdRunJson({
407
+ title: "profilepool add",
408
+ cwd: "",
409
+ args: buildArgs([window.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"), "add", kw, "--json"])
410
+ });
411
+ ctx2.appendLog(JSON.stringify(out?.json || out, null, 2));
412
+ const createdProfileId = String(out?.json?.profileId || "").trim();
413
+ await poolList();
414
+ if (!createdProfileId) return;
415
+ if (typeof window.api?.cmdSpawn !== "function") return;
416
+ const timeoutSec = Math.max(30, Math.floor(Number(timeoutInput.value || "900")));
417
+ const loginArgs = buildArgs([
418
+ window.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
419
+ "login-profile",
420
+ createdProfileId,
421
+ "--timeout-sec",
422
+ String(timeoutSec),
423
+ "--check-interval-sec",
424
+ "2",
425
+ "--keep-session",
426
+ ...ctx2.settings?.unifiedApiUrl ? ["--unified-api", String(ctx2.settings.unifiedApiUrl)] : [],
427
+ ...ctx2.settings?.browserServiceUrl ? ["--browser-service", String(ctx2.settings.browserServiceUrl)] : []
428
+ ]);
429
+ await window.api.cmdSpawn({
430
+ title: `profilepool login-profile ${createdProfileId}`,
431
+ cwd: "",
432
+ args: loginArgs,
433
+ groupKey: "profilepool",
434
+ env: { WEBAUTO_DAEMON: "1" }
435
+ });
436
+ ctx2.appendLog(`[preflight] \u5DF2\u521B\u5EFA\u5E76\u542F\u52A8\u767B\u5F55: ${createdProfileId}`);
437
+ }
438
+ async function poolLogin() {
439
+ ctx2.clearLog();
440
+ const kw = resolveBatchPrefix();
441
+ const ensureCount = Math.max(0, Math.floor(Number(ensureCountInput.value || "0")));
442
+ const timeoutSec = Math.max(30, Math.floor(Number(timeoutInput.value || "900")));
443
+ const args = buildArgs([
444
+ window.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
445
+ "login",
446
+ kw,
447
+ ...ctx2.settings?.unifiedApiUrl ? ["--unified-api", String(ctx2.settings.unifiedApiUrl)] : [],
448
+ ...ctx2.settings?.browserServiceUrl ? ["--browser-service", String(ctx2.settings.browserServiceUrl)] : [],
449
+ "--timeout-sec",
450
+ String(timeoutSec),
451
+ ...ensureCount > 0 ? ["--ensure-count", String(ensureCount)] : [],
452
+ ...keepSession.checked ? ["--keep-session"] : []
453
+ ]);
454
+ await window.api.cmdSpawn({ title: `profilepool login ${kw}`, cwd: "", args, groupKey: "profilepool" });
455
+ }
456
+ const poolActions = createEl("div", { className: "row" }, [
457
+ createEl("button", { className: "secondary" }, ["\u626B\u63CF\u6C60"]),
458
+ createEl("button", { className: "secondary" }, ["\u65B0\u589E\u4E00\u4E2A\u5E76\u767B\u5F55"]),
459
+ createEl("button", {}, ["\u6279\u91CF\u767B\u5F55/\u8865\u767B\u5F55"])
460
+ ]);
461
+ poolActions.children[0].onclick = () => void poolList();
462
+ poolActions.children[1].onclick = () => void poolAdd();
463
+ poolActions.children[2].onclick = () => void poolLogin();
464
+ root.appendChild(
465
+ section("\u6279\u91CF\u8D26\u53F7\u6C60\uFF08\u81EA\u52A8\u5E8F\u53F7\uFF09", [
466
+ createEl("div", { className: "row" }, [
467
+ labeledInput("\u8D26\u53F7\u540D\uFF08\u9ED8\u8BA4 xiaohongshu\uFF09", keywordInput),
468
+ labeledInput("ensure-count (\u53EF\u9009)", ensureCountInput),
469
+ labeledInput("login timeout (sec)", timeoutInput),
470
+ createEl("div", { style: "display:flex; flex-direction:column; gap:6px;" }, [
471
+ createEl("label", {}, ["keep-session"]),
472
+ keepSession
473
+ ])
474
+ ]),
475
+ poolActions,
476
+ poolStatus,
477
+ poolListBox
478
+ ])
479
+ );
480
+ void refreshScan();
481
+ void runBrowserCheck({ source: "auto" });
482
+ }
483
+
484
+ // src/renderer/tabs/run.mts
485
+ function buildArgs2(parts) {
486
+ return parts.filter((x) => x != null && String(x).trim() !== "");
487
+ }
488
+ function renderRun(root, ctx2) {
489
+ const templateSel = createEl("select");
490
+ const templates = [
491
+ { id: "fullCollect", label: "\u8C03\u8BD5\uFF1AUnified Harvest" },
492
+ { id: "smartReply", label: "\u8C03\u8BD5\uFF1ASmart Reply DEV E2E" },
493
+ { id: "virtualLike", label: "\u8C03\u8BD5\uFF1AVirtual Like E2E" }
494
+ ];
495
+ templates.forEach((t) => templateSel.appendChild(createEl("option", { value: t.id }, [t.label])));
496
+ const keywordInput = createEl("input", { value: ctx2.settings?.defaultKeyword || "", placeholder: "keyword" });
497
+ const targetInput = createEl("input", { value: String(ctx2.settings?.defaultTarget || ""), placeholder: "target", type: "number", min: "1" });
498
+ const envSel = createEl("select");
499
+ ["debug", "prod"].forEach((x) => envSel.appendChild(createEl("option", { value: x }, [x])));
500
+ envSel.value = ctx2.settings?.defaultEnv || "debug";
501
+ const dryRun = createEl("input", { type: "checkbox" });
502
+ dryRun.checked = ctx2.settings?.defaultDryRun === true;
503
+ const profileModeSel = createEl("select");
504
+ const profilePickSel = createEl("select");
505
+ const runtimePickSel = createEl("select");
506
+ const profileRefreshBtn = createEl("button", { className: "secondary" }, ["\u5237\u65B0 profiles"]);
507
+ const poolPickSel = createEl("select");
508
+ const profilesBox = createEl("div", { className: "list" });
509
+ const profilesHint = createEl("div", { className: "muted" }, [""]);
510
+ const resolvedHint = createEl("div", { className: "muted" }, [""]);
511
+ const extraInput = createEl("input", { placeholder: "extra args (raw)" });
512
+ let fullCollectScripts = [];
513
+ function maybeFlag(flag, value) {
514
+ const v = String(value || "").trim();
515
+ if (!v) return [];
516
+ return [flag, v];
517
+ }
518
+ function setProfileModes(templateId) {
519
+ const supportsMultiProfile = templateId === "fullCollect";
520
+ profileModeSel.textContent = "";
521
+ const modes = supportsMultiProfile ? [
522
+ { v: "profile", l: "profile\uFF08\u5355\u4E2A\uFF09" },
523
+ { v: "profilepool", l: "profilepool\uFF08\u6309 keyword \u524D\u7F00\u626B\u63CF + \u81EA\u52A8\u5206\u7247\uFF09" },
524
+ { v: "profiles", l: "profiles\uFF08\u624B\u52A8\u5217\u8868 + \u81EA\u52A8\u5206\u7247\uFF09" }
525
+ ] : [{ v: "profile", l: "profile\uFF08\u5355\u4E2A\uFF09" }];
526
+ modes.forEach((m) => profileModeSel.appendChild(createEl("option", { value: m.v }, [m.l])));
527
+ profileModeSel.value = "profile";
528
+ }
529
+ function derivePoolKeys(profiles) {
530
+ const keys = /* @__PURE__ */ new Set();
531
+ for (const p of profiles) {
532
+ const m = /^(.+)-\d+$/.exec(p);
533
+ if (m && m[1]) keys.add(m[1]);
534
+ }
535
+ return Array.from(keys).sort((a, b) => a.localeCompare(b));
536
+ }
537
+ async function refreshProfiles() {
538
+ profilePickSel.textContent = "";
539
+ profilePickSel.appendChild(createEl("option", { value: "" }, ["(\u9009\u62E9\u5DF2\u6709 profile\uFF0C\u586B\u5145\u5230\u8F93\u5165\u6846)"]));
540
+ const profilesRes = await window.api.profilesList().catch(() => null);
541
+ const profiles = profilesRes?.profiles || [];
542
+ const aliases = ctx2.settings?.profileAliases && typeof ctx2.settings.profileAliases === "object" ? ctx2.settings.profileAliases : {};
543
+ profiles.forEach((p) => {
544
+ const alias = String(aliases[p] || "").trim();
545
+ const label = alias ? `${alias} (${p})` : p;
546
+ profilePickSel.appendChild(createEl("option", { value: p }, [label]));
547
+ });
548
+ poolPickSel.textContent = "";
549
+ poolPickSel.appendChild(createEl("option", { value: "" }, ["(\u9009\u62E9 pool keyword)"]));
550
+ const poolKeys = derivePoolKeys(profiles);
551
+ poolKeys.forEach((k) => poolPickSel.appendChild(createEl("option", { value: k }, [String(k)])));
552
+ if (poolKeys.length > 0 && profileModeSel.value === "profilepool" && !poolPickSel.value) {
553
+ poolPickSel.value = poolKeys[0];
554
+ syncProfileValueFromUI();
555
+ }
556
+ profilesBox.textContent = "";
557
+ profilesHint.textContent = "";
558
+ const selected = /* @__PURE__ */ new Set();
559
+ const preferSingle = profiles[0] || "";
560
+ if (preferSingle) {
561
+ profilePickSel.value = preferSingle;
562
+ selected.add(preferSingle);
563
+ }
564
+ profiles.forEach((p) => {
565
+ const id = `p_${p}`;
566
+ const cb = createEl("input", { type: "checkbox", id });
567
+ cb.dataset.profile = p;
568
+ cb.checked = selected.has(p);
569
+ cb.onchange = () => syncProfileValueFromUI();
570
+ const alias = String(aliases[p] || "").trim();
571
+ const labelText = alias ? `${alias} (${p})` : p;
572
+ const label = createEl("label", { for: id, style: "cursor:pointer;" }, [labelText]);
573
+ const row = createEl("div", { className: "row", style: "align-items:center;" }, [cb, label]);
574
+ profilesBox.appendChild(row);
575
+ });
576
+ syncProfileValueFromUI();
577
+ }
578
+ async function refreshFullCollectScripts() {
579
+ const res = await window.api.scriptsXhsFullCollect().catch(() => null);
580
+ fullCollectScripts = Array.isArray(res?.scripts) ? res.scripts : [];
581
+ if (fullCollectScripts.length > 0) {
582
+ const first = fullCollectScripts[0];
583
+ const label = first?.label || "Full Collect (Phase1-4)";
584
+ const option = templateSel.querySelector('option[value="fullCollect"]');
585
+ if (option) option.textContent = label;
586
+ }
587
+ }
588
+ async function refreshRuntimes() {
589
+ runtimePickSel.textContent = "";
590
+ runtimePickSel.appendChild(createEl("option", { value: "" }, ["(\u9009\u62E9\u8FD0\u884C\u4E2D\u7684 runtime)"]));
591
+ const sessions = await window.api.runtimeListSessions().catch(() => []);
592
+ const aliases = ctx2.settings?.profileAliases && typeof ctx2.settings.profileAliases === "object" ? ctx2.settings.profileAliases : {};
593
+ (Array.isArray(sessions) ? sessions : []).forEach((s) => {
594
+ const profileId = String(s?.profileId || s?.sessionId || "").trim();
595
+ if (!profileId) return;
596
+ const alias = String(aliases[profileId] || "").trim();
597
+ const label = alias ? `${alias} (${profileId})` : profileId;
598
+ runtimePickSel.appendChild(createEl("option", { value: profileId }, [label]));
599
+ });
600
+ }
601
+ window.api.onSettingsChanged((next) => {
602
+ ctx2.settings = next;
603
+ void refreshProfiles();
604
+ void refreshRuntimes();
605
+ });
606
+ function getSelectedProfiles() {
607
+ const selected = [];
608
+ profilesBox.querySelectorAll('input[type="checkbox"]').forEach((el) => {
609
+ const cb = el;
610
+ if (!cb.checked) return;
611
+ const id = String(cb.dataset.profile || "").trim();
612
+ if (id) selected.push(id);
613
+ });
614
+ return selected;
615
+ }
616
+ function syncProfileValueFromUI() {
617
+ const mode = profileModeSel.value;
618
+ const useRuntimeForSingle = templateSel.value !== "fullCollect";
619
+ profilePickSel.style.display = mode === "profile" && !useRuntimeForSingle ? "" : "none";
620
+ runtimePickSel.style.display = mode === "profile" && useRuntimeForSingle ? "" : "none";
621
+ poolPickSel.style.display = mode === "profilepool" ? "" : "none";
622
+ profilesBox.style.display = mode === "profiles" || mode === "profilepool" ? "" : "none";
623
+ if (mode === "profile") {
624
+ const v = String(useRuntimeForSingle ? runtimePickSel.value : profilePickSel.value || "").trim();
625
+ profilesHint.textContent = v ? "" : "\u8BF7\u9009\u62E9\u4E00\u4E2A runtime/profile";
626
+ resolvedHint.textContent = v ? `resolved: --profile ${v}` : "";
627
+ return;
628
+ }
629
+ if (mode === "profilepool") {
630
+ const v = String(poolPickSel.value || "").trim();
631
+ profilesHint.textContent = v ? "" : "\u8BF7\u9009\u62E9\u4E00\u4E2A pool keyword";
632
+ resolvedHint.textContent = v ? `resolved: --profilepool ${v}` : "";
633
+ if (v) {
634
+ profilesBox.querySelectorAll('input[type="checkbox"]').forEach((el) => {
635
+ const cb = el;
636
+ const pid = String(cb.dataset.profile || "");
637
+ cb.checked = pid.startsWith(v);
638
+ });
639
+ }
640
+ return;
641
+ }
642
+ if (mode === "profiles") {
643
+ const list = getSelectedProfiles();
644
+ profilesHint.textContent = `selected=${list.length}`;
645
+ resolvedHint.textContent = list.length ? `resolved: --profiles ${list.join(",")}` : "";
646
+ return;
647
+ }
648
+ }
649
+ function resolveProfileArgsForRun(t) {
650
+ const supportsMultiProfile = t === "fullCollect";
651
+ const mode = profileModeSel.value;
652
+ const useRuntimeForSingle = t !== "fullCollect";
653
+ if (!supportsMultiProfile) {
654
+ const v = String(useRuntimeForSingle ? runtimePickSel.value : profilePickSel.value || "").trim();
655
+ if (!v) return { ok: false, error: "\u8BF7\u9009\u62E9\u4E00\u4E2A profile", args: [] };
656
+ return { ok: true, args: ["--profile", v], mode: "profile", value: v };
657
+ }
658
+ if (mode === "profile") {
659
+ const v = String(useRuntimeForSingle ? runtimePickSel.value : profilePickSel.value || "").trim();
660
+ if (!v) return { ok: false, error: "\u8BF7\u9009\u62E9\u4E00\u4E2A profile", args: [] };
661
+ return { ok: true, args: ["--profile", v], mode: "profile", value: v };
662
+ }
663
+ if (mode === "profilepool") {
664
+ const v = String(poolPickSel.value || "").trim();
665
+ if (!v) return { ok: false, error: "\u8BF7\u9009\u62E9\u4E00\u4E2A pool keyword", args: [] };
666
+ return { ok: true, args: ["--profilepool", v], mode: "profilepool", value: v };
667
+ }
668
+ const list = getSelectedProfiles();
669
+ if (list.length === 0) return { ok: false, error: "\u8BF7\u52FE\u9009\u81F3\u5C11\u4E00\u4E2A profile", args: [] };
670
+ return { ok: true, args: ["--profiles", list.join(",")], mode: "profiles", value: list.join(",") };
671
+ }
672
+ function persistRunInputs(next) {
673
+ const curr = ctx2.settings || {};
674
+ const payload = {};
675
+ if (next.keyword && next.keyword !== curr.defaultKeyword) payload.defaultKeyword = next.keyword;
676
+ if (typeof next.target === "number" && next.target > 0 && next.target !== curr.defaultTarget) payload.defaultTarget = next.target;
677
+ if (next.env && next.env !== curr.defaultEnv) payload.defaultEnv = next.env;
678
+ if (typeof next.dryRun === "boolean" && next.dryRun !== curr.defaultDryRun) payload.defaultDryRun = next.dryRun;
679
+ if (Object.keys(payload).length === 0) return;
680
+ window.api.settingsSet(payload).then((updated) => {
681
+ if (updated) ctx2.settings = updated;
682
+ }).catch(() => {
683
+ });
684
+ }
685
+ async function run() {
686
+ ctx2.clearLog();
687
+ const t = templateSel.value;
688
+ const keyword = keywordInput.value.trim();
689
+ const env = envSel.value.trim();
690
+ const extra = extraInput.value.trim();
691
+ const target = targetInput.value.trim();
692
+ if (t === "fullCollect") {
693
+ if (!keyword) {
694
+ profilesHint.textContent = "fullCollect: \u5FC5\u987B\u586B\u5199 keyword";
695
+ alert("Full Collect: \u5FC5\u987B\u586B\u5199 keyword");
696
+ return;
697
+ }
698
+ const n = Number(target);
699
+ if (!Number.isFinite(n) || n <= 0) {
700
+ profilesHint.textContent = "fullCollect: \u5FC5\u987B\u586B\u5199 target\uFF08\u6B63\u6574\u6570\uFF09";
701
+ alert("Full Collect: \u5FC5\u987B\u586B\u5199 target\uFF08\u6B63\u6574\u6570\uFF09");
702
+ return;
703
+ }
704
+ }
705
+ const targetNum = Number(target);
706
+ persistRunInputs({ keyword, target: Number.isFinite(targetNum) ? targetNum : void 0, env, dryRun: dryRun.checked });
707
+ const common = buildArgs2([
708
+ ...keyword ? ["--keyword", keyword] : [],
709
+ ...target ? ["--target", target] : [],
710
+ ...env ? ["--env", env] : [],
711
+ ...dryRun.checked ? ["--dry-run"] : []
712
+ ]);
713
+ const resolved = resolveProfileArgsForRun(t);
714
+ if (!resolved.ok) {
715
+ profilesHint.textContent = resolved.error;
716
+ return;
717
+ }
718
+ const profileArgs = resolved.args;
719
+ const extraArgs = extra ? extra.split(" ").filter(Boolean) : [];
720
+ let script = "";
721
+ let args = [];
722
+ if (t === "fullCollect") {
723
+ const chosen = fullCollectScripts[0];
724
+ const scriptPath = chosen?.path || window.api.pathJoin("apps", "webauto", "entry", "xhs-unified.mjs");
725
+ script = scriptPath;
726
+ args = buildArgs2([script, ...profileArgs, ...common, ...extraArgs]);
727
+ } else if (t === "smartReply") {
728
+ script = window.api.pathJoin("apps", "webauto", "entry", "xhs-unified.mjs");
729
+ args = buildArgs2([
730
+ script,
731
+ ...profileArgs,
732
+ ...common,
733
+ "--do-comments",
734
+ "true",
735
+ "--do-reply",
736
+ "true",
737
+ "--do-likes",
738
+ "false",
739
+ ...extraArgs
740
+ ]);
741
+ } else if (t === "virtualLike") {
742
+ script = window.api.pathJoin("apps", "webauto", "entry", "xhs-unified.mjs");
743
+ args = buildArgs2([
744
+ script,
745
+ ...profileArgs,
746
+ ...common,
747
+ "--do-comments",
748
+ "true",
749
+ "--do-likes",
750
+ "true",
751
+ ...extraArgs
752
+ ]);
753
+ }
754
+ const ok = await window.api.cmdSpawn({
755
+ title: `${t} ${keyword}`,
756
+ cwd: "",
757
+ args,
758
+ groupKey: "xiaohongshu"
759
+ });
760
+ if (!ok || ok.runId == null) {
761
+ alert("\u8FD0\u884C\u5931\u8D25\uFF1A\u547D\u4EE4\u672A\u542F\u52A8\uFF08\u8BF7\u68C0\u67E5\u8F93\u5165\u53C2\u6570\u6216\u65E5\u5FD7\uFF09");
762
+ }
763
+ }
764
+ async function stop() {
765
+ if (!ctx2.activeRunId) return;
766
+ await window.api.cmdKill(ctx2.activeRunId);
767
+ }
768
+ const actions = createEl("div", { className: "row" }, [
769
+ createEl("button", {}, ["\u8FD0\u884C"]),
770
+ createEl("button", { className: "danger" }, ["\u505C\u6B62"])
771
+ ]);
772
+ actions.children[0].onclick = () => void run();
773
+ actions.children[1].onclick = () => void stop();
774
+ root.appendChild(
775
+ section("\u8C03\u7528", [
776
+ createEl("div", { className: "muted" }, [
777
+ "\u8BF4\u660E\uFF1A\u5C0F\u7EA2\u4E66\u5B8C\u6574\u7F16\u6392\uFF08Phase1/2/Unified + \u5206\u7247\uFF09\u5DF2\u8FC1\u79FB\u5230\u201C\u5C0F\u7EA2\u4E66\u201DTab\uFF1B\u8FD9\u91CC\u4EC5\u4FDD\u7559\u8C03\u8BD5\u5165\u53E3\u3002"
778
+ ]),
779
+ createEl("div", { className: "row" }, [
780
+ labeledInput("template", templateSel),
781
+ labeledInput("keyword", keywordInput),
782
+ labeledInput("target", targetInput),
783
+ labeledInput("env", envSel),
784
+ createEl("div", { style: "display:flex; flex-direction:column; gap:6px;" }, [
785
+ createEl("label", {}, ["dry-run"]),
786
+ dryRun
787
+ ])
788
+ ]),
789
+ createEl("div", { className: "row" }, [
790
+ labeledInput("profile mode", profileModeSel),
791
+ createEl("div", { style: "display:flex; gap:8px; align-items:center;" }, [profilePickSel, runtimePickSel, poolPickSel, profileRefreshBtn]),
792
+ labeledInput("extra", extraInput)
793
+ ]),
794
+ profilesHint,
795
+ resolvedHint,
796
+ profilesBox,
797
+ actions,
798
+ createEl("div", { className: "muted" }, [
799
+ "profile\uFF1A\u5355 profile\uFF1Bprofilepool\uFF1A\u6309 keyword \u524D\u7F00\u626B\u63CF pool \u81EA\u52A8\u5206\u7247\uFF1Bprofiles\uFF1A\u624B\u52A8 a,b,c \u81EA\u52A8\u5206\u7247\u3002"
800
+ ])
801
+ ])
802
+ );
803
+ templateSel.onchange = () => {
804
+ setProfileModes(templateSel.value);
805
+ syncProfileValueFromUI();
806
+ void refreshRuntimes();
807
+ };
808
+ profileModeSel.onchange = async () => {
809
+ syncProfileValueFromUI();
810
+ if (profileModeSel.value === "profilepool") {
811
+ const res = await window.api.profilesList().catch(() => null);
812
+ const profiles = res?.profiles || [];
813
+ const poolKeys = derivePoolKeys(profiles);
814
+ if (poolKeys.length > 0 && !poolPickSel.value) {
815
+ poolPickSel.value = poolKeys[0];
816
+ syncProfileValueFromUI();
817
+ }
818
+ }
819
+ };
820
+ profilePickSel.onchange = () => {
821
+ syncProfileValueFromUI();
822
+ };
823
+ runtimePickSel.onchange = () => {
824
+ syncProfileValueFromUI();
825
+ };
826
+ poolPickSel.onchange = () => syncProfileValueFromUI();
827
+ profileRefreshBtn.onclick = () => void refreshProfiles();
828
+ setProfileModes(templateSel.value);
829
+ void refreshProfiles();
830
+ void refreshRuntimes();
831
+ void refreshFullCollectScripts();
832
+ }
833
+
834
+ // src/renderer/tabs/results.mts
835
+ function renderResults(root, ctx2) {
836
+ const entryList = createEl("div", { className: "list" });
837
+ const fileList = createEl("div", { className: "list" });
838
+ const rightTop = createEl("div", {});
839
+ const preview = createEl("div", {});
840
+ const filterInput = createEl("input", { placeholder: "filter filename (contains)", value: "" });
841
+ const onlyImages = createEl("input", { type: "checkbox" });
842
+ const onlyText = createEl("input", { type: "checkbox" });
843
+ const statusLine = createEl("div", { className: "muted" }, [""]);
844
+ let currentEntry = null;
845
+ let currentFiles = [];
846
+ async function refresh() {
847
+ entryList.textContent = "";
848
+ fileList.textContent = "";
849
+ rightTop.textContent = "";
850
+ preview.textContent = "";
851
+ statusLine.textContent = "";
852
+ currentEntry = null;
853
+ currentFiles = [];
854
+ const res = await window.api.resultsScan({ downloadRoot: ctx2.settings?.downloadRoot });
855
+ if (!res?.ok) {
856
+ entryList.appendChild(createEl("div", { className: "item" }, [String(res?.error || "scan failed")]));
857
+ return;
858
+ }
859
+ const entries = res.entries || [];
860
+ entries.forEach((e) => {
861
+ const s = e.state || null;
862
+ const badge = s && s.status ? ` [${s.status}] links=${s.links || 0}/${s.target || 0} ok=${s.completed || 0} fail=${s.failed || 0}` : " [no-state]";
863
+ const item = createEl("div", { className: "item" }, [`${e.env}/${e.keyword}${badge}`]);
864
+ item.onclick = () => void openEntry(e);
865
+ entryList.appendChild(item);
866
+ });
867
+ }
868
+ async function openEntry(e) {
869
+ currentEntry = e;
870
+ currentFiles = [];
871
+ rightTop.textContent = "";
872
+ preview.textContent = "";
873
+ fileList.textContent = "";
874
+ const openDirBtn = createEl("button", { className: "secondary" }, ["\u6253\u5F00\u76EE\u5F55"]);
875
+ openDirBtn.onclick = () => void window.api.osOpenPath(e.path);
876
+ rightTop.appendChild(createEl("div", { className: "row" }, [
877
+ openDirBtn,
878
+ createEl("div", { className: "muted" }, [String(e.path || "")])
879
+ ]));
880
+ const res = await window.api.fsListDir({ root: e.path, recursive: true, maxEntries: 2e3 });
881
+ if (!res?.ok) {
882
+ statusLine.textContent = `list failed: ${String(res?.error || "")}`;
883
+ return;
884
+ }
885
+ const files = (res.entries || []).filter((x) => x && x.isDir === false);
886
+ currentFiles = files;
887
+ statusLine.textContent = `files=${files.length}${res.truncated ? " (truncated)" : ""}`;
888
+ renderFileList();
889
+ }
890
+ function isImagePath(p) {
891
+ const ext = p.toLowerCase().split(".").pop() || "";
892
+ return ext === "png" || ext === "jpg" || ext === "jpeg" || ext === "webp";
893
+ }
894
+ function isTextPath(p) {
895
+ const ext = p.toLowerCase().split(".").pop() || "";
896
+ return ext === "jsonl" || ext === "json" || ext === "txt" || ext === "log" || ext === "md";
897
+ }
898
+ function renderFileList() {
899
+ fileList.textContent = "";
900
+ preview.textContent = "";
901
+ const needle = filterInput.value.trim().toLowerCase();
902
+ const list = currentFiles.filter((x) => {
903
+ const rel = String(x.rel || x.name || "").toLowerCase();
904
+ if (needle && !rel.includes(needle)) return false;
905
+ if (onlyImages.checked && !isImagePath(rel)) return false;
906
+ if (onlyText.checked && !isTextPath(rel)) return false;
907
+ return true;
908
+ }).slice(0, 600);
909
+ list.forEach((f) => {
910
+ const label = String(f.rel || f.name || "");
911
+ const item = createEl("div", { className: "item" }, [label]);
912
+ item.onclick = () => void previewFile(f);
913
+ fileList.appendChild(item);
914
+ });
915
+ }
916
+ async function previewFile(f) {
917
+ preview.textContent = "";
918
+ const p = String(f.path || "");
919
+ if (!p) return;
920
+ const actions = createEl("div", { className: "row" }, [
921
+ createEl("button", { className: "secondary" }, ["\u6253\u5F00\u6587\u4EF6"])
922
+ ]);
923
+ actions.querySelector("button").onclick = () => void window.api.osOpenPath(p);
924
+ preview.appendChild(actions);
925
+ if (isImagePath(p)) {
926
+ const res = await window.api.fsReadFileBase64({ path: p, maxBytes: 8e6 });
927
+ if (!res?.ok) {
928
+ preview.appendChild(createEl("div", { className: "muted" }, [String(res?.error || "read image failed")]));
929
+ return;
930
+ }
931
+ const ext = p.toLowerCase().endsWith(".png") ? "png" : p.toLowerCase().endsWith(".webp") ? "webp" : "jpeg";
932
+ const img = createEl("img", { style: "max-width:100%; border:1px solid #ddd; border-radius:6px;" });
933
+ img.src = `data:image/${ext};base64,${res.data}`;
934
+ preview.appendChild(img);
935
+ return;
936
+ }
937
+ if (isTextPath(p)) {
938
+ await previewText(p);
939
+ return;
940
+ }
941
+ preview.appendChild(createEl("div", { className: "muted" }, ["(no preview)"]));
942
+ }
943
+ async function previewText(filePath) {
944
+ const res = await window.api.fsReadTextPreview({ path: filePath, maxBytes: 5e4, maxLines: 200 });
945
+ if (!res?.ok) {
946
+ preview.appendChild(createEl("div", { className: "muted" }, [String(res?.error || "read failed")]));
947
+ return;
948
+ }
949
+ preview.appendChild(createEl("pre", { style: "white-space:pre-wrap; margin:0;" }, [res.text]));
950
+ }
951
+ root.appendChild(
952
+ section("\u7ED3\u679C\u76EE\u5F55", [
953
+ createEl("div", { className: "row" }, [
954
+ createEl("button", { className: "secondary" }, ["\u5237\u65B0"]),
955
+ createEl("div", { className: "muted" }, [`root=${ctx2.settings?.downloadRoot || ""}`])
956
+ ]),
957
+ createEl("div", { className: "row" }, [
958
+ createEl("div", { style: "display:flex; flex-direction:column; gap:6px;" }, [
959
+ createEl("label", {}, ["only images"]),
960
+ onlyImages
961
+ ]),
962
+ createEl("div", { style: "display:flex; flex-direction:column; gap:6px;" }, [
963
+ createEl("label", {}, ["only text"]),
964
+ onlyText
965
+ ]),
966
+ filterInput,
967
+ statusLine
968
+ ]),
969
+ createEl("div", { className: "split" }, [
970
+ entryList,
971
+ createEl("div", {}, [rightTop, fileList, preview])
972
+ ])
973
+ ])
974
+ );
975
+ root.querySelector("button").onclick = () => void refresh();
976
+ filterInput.oninput = () => renderFileList();
977
+ onlyImages.onchange = () => renderFileList();
978
+ onlyText.onchange = () => renderFileList();
979
+ void refresh();
980
+ }
981
+
982
+ // src/renderer/tabs/debug.mts
983
+ var activeDebugView = "run";
984
+ function renderDebug(root, ctx2) {
985
+ root.textContent = "";
986
+ const title = createEl("div", { style: "font-weight:700; margin-bottom:8px;" }, ["Debug \xB7 \u8C03\u7528\u4E0E\u7ED3\u679C"]);
987
+ const sub = createEl("div", { className: "muted", style: "margin-bottom:10px; font-size:12px;" }, [
988
+ "\u201C\u8C03\u7528\u201D\u548C\u201C\u7ED3\u679C\u201D\u5DF2\u6574\u5408\u5230 Debug \u9875\uFF1BRuntime \u5DF2\u4ECE\u4E3B\u5BFC\u822A\u79FB\u9664\u3002"
989
+ ]);
990
+ const switcher = createEl("div", { className: "row", style: "margin-bottom:8px;" });
991
+ const runBtn = createEl("button", { type: "button" }, ["\u8C03\u7528"]);
992
+ const resultsBtn = createEl("button", { type: "button", className: "secondary" }, ["\u7ED3\u679C"]);
993
+ const panel = createEl("div");
994
+ const setView = (id) => {
995
+ activeDebugView = id;
996
+ runBtn.className = id === "run" ? "" : "secondary";
997
+ resultsBtn.className = id === "results" ? "" : "secondary";
998
+ panel.textContent = "";
999
+ if (id === "run") {
1000
+ renderRun(panel, ctx2);
1001
+ return;
1002
+ }
1003
+ renderResults(panel, ctx2);
1004
+ };
1005
+ runBtn.onclick = () => setView("run");
1006
+ resultsBtn.onclick = () => setView("results");
1007
+ switcher.appendChild(runBtn);
1008
+ switcher.appendChild(resultsBtn);
1009
+ root.appendChild(title);
1010
+ root.appendChild(sub);
1011
+ root.appendChild(switcher);
1012
+ root.appendChild(panel);
1013
+ setView(activeDebugView);
1014
+ }
1015
+
1016
+ // src/renderer/tabs/settings.mts
1017
+ function renderSettings(root, ctx2) {
1018
+ const coreDaemon = createEl("input", { value: ctx2.settings?.coreDaemonUrl || "http://127.0.0.1:7700" });
1019
+ const download = createEl("input", { value: ctx2.settings?.downloadRoot || "" });
1020
+ const env = createEl("select");
1021
+ ["debug", "prod"].forEach((x) => env.appendChild(createEl("option", { value: x }, [x])));
1022
+ env.value = ctx2.settings?.defaultEnv || "debug";
1023
+ const keyword = createEl("input", { value: ctx2.settings?.defaultKeyword || "" });
1024
+ const loginTimeout = createEl("input", { value: String(ctx2.settings?.timeouts?.loginTimeoutSec || 900), type: "number", min: "30" });
1025
+ const cmdTimeout = createEl("input", { value: String(ctx2.settings?.timeouts?.cmdTimeoutSec || 0), type: "number", min: "0" });
1026
+ const aiEnabled = createEl("input", { type: "checkbox", checked: ctx2.settings?.aiReply?.enabled ?? false });
1027
+ const aiBaseUrl = createEl("input", { value: ctx2.settings?.aiReply?.baseUrl || "http://127.0.0.1:5520", placeholder: "http://127.0.0.1:5520" });
1028
+ const aiApiKey = createEl("input", { value: ctx2.settings?.aiReply?.apiKey || "", type: "password", placeholder: "sk-..." });
1029
+ const aiModel = createEl("input", { value: ctx2.settings?.aiReply?.model || "iflow.glm-5", placeholder: "iflow.glm-5" });
1030
+ const aiTemperature = createEl("input", { value: String(ctx2.settings?.aiReply?.temperature ?? 0.7), type: "number", min: "0", max: "2", step: "0.1" });
1031
+ const aiMaxChars = createEl("input", { value: String(ctx2.settings?.aiReply?.maxChars ?? 20), type: "number", min: "5", max: "500" });
1032
+ const aiTimeout = createEl("input", { value: String(ctx2.settings?.aiReply?.timeoutMs ?? 25e3), type: "number", min: "5000", step: "1000" });
1033
+ const aiStyle = createEl("select");
1034
+ ["friendly", "professional", "humorous", "concise", "custom"].forEach((x) => aiStyle.appendChild(createEl("option", { value: x }, [x])));
1035
+ aiStyle.value = ctx2.settings?.aiReply?.stylePreset || "friendly";
1036
+ const aiStyleCustom = createEl("input", { value: ctx2.settings?.aiReply?.styleCustom || "", placeholder: "\u81EA\u5B9A\u4E49\u98CE\u683C\u63CF\u8FF0\uFF08\u53EF\u9009\uFF09" });
1037
+ const aiTestResult = createEl("div", { className: "muted", style: "min-height:1.5em;" });
1038
+ async function fetchModels() {
1039
+ try {
1040
+ aiTestResult.textContent = "\u83B7\u53D6\u6A21\u578B\u5217\u8868\u4E2D...";
1041
+ const result = await window.api.invoke("ai:listModels", {
1042
+ baseUrl: aiBaseUrl.value.trim(),
1043
+ apiKey: aiApiKey.value.trim()
1044
+ });
1045
+ if (result.ok && result.models?.length > 0) {
1046
+ aiTestResult.textContent = "\u627E\u5230 " + result.models.length + " \u4E2A\u6A21\u578B: " + result.models.slice(0, 3).join(", ") + (result.models.length > 3 ? "..." : "");
1047
+ if (!aiModel.value && result.models[0]) {
1048
+ aiModel.value = result.models[0];
1049
+ }
1050
+ } else {
1051
+ aiTestResult.textContent = result.error ? "\u9519\u8BEF: " + result.error : "\u672A\u627E\u5230\u6A21\u578B";
1052
+ }
1053
+ } catch (e) {
1054
+ aiTestResult.textContent = "\u9519\u8BEF: " + (e?.message || String(e));
1055
+ }
1056
+ }
1057
+ async function testConnection() {
1058
+ try {
1059
+ aiTestResult.textContent = "\u6D4B\u8BD5\u8FDE\u901A\u6027...";
1060
+ const result = await window.api.invoke("ai:testChatCompletion", {
1061
+ baseUrl: aiBaseUrl.value.trim(),
1062
+ apiKey: aiApiKey.value.trim(),
1063
+ model: aiModel.value.trim() || "iflow.glm-5",
1064
+ timeoutMs: Number(aiTimeout.value) || 25e3
1065
+ });
1066
+ if (result.ok) {
1067
+ aiTestResult.textContent = "\u8FDE\u901A\u6210\u529F (" + result.latencyMs + "ms)";
1068
+ } else {
1069
+ aiTestResult.textContent = result.error ? "\u9519\u8BEF: " + result.error : "\u6D4B\u8BD5\u5931\u8D25";
1070
+ }
1071
+ } catch (e) {
1072
+ aiTestResult.textContent = "\u9519\u8BEF: " + (e?.message || String(e));
1073
+ }
1074
+ }
1075
+ const configPath = resolveConfigPath(ctx2.settings?.downloadRoot || "", window.api);
1076
+ const debugHost = createEl("div");
1077
+ async function save() {
1078
+ const next = await window.api.settingsSet({
1079
+ coreDaemonUrl: coreDaemon.value.trim(),
1080
+ downloadRoot: download.value.trim(),
1081
+ defaultEnv: env.value,
1082
+ defaultKeyword: keyword.value,
1083
+ timeouts: {
1084
+ loginTimeoutSec: Number(loginTimeout.value || "900"),
1085
+ cmdTimeoutSec: Number(cmdTimeout.value || "0")
1086
+ },
1087
+ aiReply: {
1088
+ enabled: aiEnabled.checked,
1089
+ baseUrl: aiBaseUrl.value.trim(),
1090
+ apiKey: aiApiKey.value.trim(),
1091
+ model: aiModel.value.trim(),
1092
+ temperature: Number(aiTemperature.value) || 0.7,
1093
+ maxChars: Number(aiMaxChars.value) || 20,
1094
+ timeoutMs: Number(aiTimeout.value) || 25e3,
1095
+ stylePreset: aiStyle.value,
1096
+ styleCustom: aiStyleCustom.value.trim()
1097
+ }
1098
+ });
1099
+ ctx2.settings = next;
1100
+ ctx2.appendLog("[settings] saved");
1101
+ }
1102
+ root.appendChild(
1103
+ section("\u8BBE\u7F6E", [
1104
+ createEl("div", { className: "row" }, [
1105
+ labeledInput("Core Daemon", coreDaemon)
1106
+ ]),
1107
+ createEl("div", { className: "row" }, [
1108
+ labeledInput("downloadRoot", download),
1109
+ labeledInput("defaultEnv", env),
1110
+ labeledInput("defaultKeyword", keyword)
1111
+ ]),
1112
+ createEl("div", { className: "row" }, [
1113
+ labeledInput("loginTimeoutSec", loginTimeout),
1114
+ labeledInput("cmdTimeoutSec", cmdTimeout)
1115
+ ]),
1116
+ createEl("div", { className: "row" }, [
1117
+ createEl("button", {}, ["\u4FDD\u5B58"])
1118
+ ]),
1119
+ createEl("div", { className: "muted" }, [`\u4FDD\u5B58\u5230 ${configPath} \u7684 desktopConsole \u914D\u7F6E\u5757\uFF08\u82E5 dist/modules/config \u4E0D\u53EF\u7528\u5219 fallback \u5230 legacy settings \u6587\u4EF6\uFF09`]),
1120
+ section("AI \u667A\u80FD\u56DE\u590D", [
1121
+ createEl("div", { className: "row" }, [
1122
+ labeledInput("\u542F\u7528\u667A\u80FD\u56DE\u590D", aiEnabled)
1123
+ ]),
1124
+ createEl("div", { className: "row" }, [
1125
+ labeledInput("API Base URL", aiBaseUrl),
1126
+ labeledInput("API Key", aiApiKey)
1127
+ ]),
1128
+ createEl("div", { className: "row" }, [
1129
+ labeledInput("\u6A21\u578B", aiModel),
1130
+ createEl("button", { style: "margin-left:8px;" }, ["\u83B7\u53D6\u6A21\u578B\u5217\u8868"]),
1131
+ createEl("button", { style: "margin-left:8px;" }, ["\u6D4B\u8BD5\u8FDE\u901A"])
1132
+ ]),
1133
+ createEl("div", { className: "row" }, [
1134
+ labeledInput("Temperature", aiTemperature),
1135
+ labeledInput("\u6700\u5927\u5B57\u6570", aiMaxChars),
1136
+ labeledInput("\u8D85\u65F6(ms)", aiTimeout)
1137
+ ]),
1138
+ createEl("div", { className: "row" }, [
1139
+ labeledInput("\u56DE\u590D\u98CE\u683C", aiStyle),
1140
+ labeledInput("\u81EA\u5B9A\u4E49\u98CE\u683C", aiStyleCustom)
1141
+ ]),
1142
+ aiTestResult
1143
+ ]),
1144
+ section("\u8C03\u8BD5\uFF08\u5DF2\u5E76\u5165\u8BBE\u7F6E\uFF09", [debugHost])
1145
+ ])
1146
+ );
1147
+ root.querySelector("button").onclick = () => void save();
1148
+ const buttons = root.querySelectorAll("button");
1149
+ if (buttons[1]) buttons[1].onclick = () => void fetchModels();
1150
+ if (buttons[2]) buttons[2].onclick = () => void testConnection();
1151
+ renderDebug(debugHost, ctx2);
1152
+ }
1153
+
1154
+ // src/renderer/tabs/logs.mts
1155
+ function renderLogs(root, ctx2) {
1156
+ root.textContent = "";
1157
+ const title = createEl("div", { style: "font-weight:700; margin-bottom:8px;" }, ["\u65E5\u5FD7 \xB7 Logs"]);
1158
+ const sub = createEl("div", { className: "muted", style: "margin-bottom:10px; font-size:12px;" }, [
1159
+ "\u547D\u4EE4\u4E8B\u4EF6\u7684\u8FD0\u884C\u65E5\u5FD7\uFF08\u4ECE\u4E3B\u58F3\u4E0A\u4E0B\u6587\u6536\u96C6\uFF09\u3002"
1160
+ ]);
1161
+ const toolbar = createEl("div", { className: "row", style: "margin-bottom:8px;" });
1162
+ const clearBtn = createEl("button", { type: "button", className: "secondary" }, ["\u6E05\u7A7A\u65E5\u5FD7"]);
1163
+ const copyGlobalBtn = createEl("button", { type: "button", className: "secondary", title: "\u590D\u5236\u516C\u5171\u65E5\u5FD7\uFF08Ctrl/Cmd+Shift+1\uFF09" }, ["\u590D\u5236\u516C\u5171\u65E5\u5FD7"]);
1164
+ const copyShardBtn = createEl("button", { type: "button", className: "secondary", title: "\u590D\u5236\u5206\u7247\u65E5\u5FD7\uFF08Ctrl/Cmd+Shift+2\uFF09" }, ["\u590D\u5236\u5206\u7247\u65E5\u5FD7"]);
1165
+ const activeOnlyCheckbox = createEl("input", { type: "checkbox", id: "logs-active-only" });
1166
+ activeOnlyCheckbox.checked = true;
1167
+ const activeOnlyLabel = createEl("label", { htmlFor: "logs-active-only", style: "cursor:pointer; user-select:none;" }, ["\u4EC5\u663E\u793A\u6D3B\u8DC3\u5206\u7247"]);
1168
+ const showGlobalCheckbox = createEl("input", { type: "checkbox", id: "logs-show-global" });
1169
+ showGlobalCheckbox.checked = false;
1170
+ const showGlobalLabel = createEl("label", { htmlFor: "logs-show-global", style: "cursor:pointer; user-select:none;" }, ["\u663E\u793A\u516C\u5171\u65E5\u5FD7"]);
1171
+ toolbar.appendChild(clearBtn);
1172
+ toolbar.appendChild(copyGlobalBtn);
1173
+ toolbar.appendChild(copyShardBtn);
1174
+ toolbar.appendChild(activeOnlyCheckbox);
1175
+ toolbar.appendChild(activeOnlyLabel);
1176
+ toolbar.appendChild(showGlobalCheckbox);
1177
+ toolbar.appendChild(showGlobalLabel);
1178
+ const globalContainer = createEl("div", {
1179
+ style: "display:flex; flex-direction:column; gap:10px; margin-bottom:10px;"
1180
+ });
1181
+ const shardContainer = createEl("div", {
1182
+ style: 'display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; align-items:start; font-family:"Cascadia Mono", Consolas, ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12px;'
1183
+ });
1184
+ const sectionMap = /* @__PURE__ */ new Map();
1185
+ const sectionCardMap = /* @__PURE__ */ new Map();
1186
+ const sectionRunIds = /* @__PURE__ */ new Map();
1187
+ const sectionHeaderMap = /* @__PURE__ */ new Map();
1188
+ const sectionTypeMap = /* @__PURE__ */ new Map();
1189
+ const logActiveRunIds = /* @__PURE__ */ new Set();
1190
+ const parentRunIds = /* @__PURE__ */ new Set();
1191
+ const runIdToSection = /* @__PURE__ */ new Map();
1192
+ const parentRunCurrentSection = /* @__PURE__ */ new Map();
1193
+ const shardRunIds = /* @__PURE__ */ new Set();
1194
+ let selectedSectionKey = "global";
1195
+ let shardProfileQueue = [];
1196
+ const parseShardHint = (line) => {
1197
+ const text = String(line || "");
1198
+ const hinted = text.match(/\[shard-hint\]\s*profiles=([A-Za-z0-9_,-]+)/i);
1199
+ const orchestrate = text.match(/\[orchestrate\][^\n]*\bprofiles=([A-Za-z0-9_,-]+)/i);
1200
+ const raw = hinted?.[1] || orchestrate?.[1] || "";
1201
+ if (!raw) return [];
1202
+ return String(raw).split(",").map((x) => x.trim()).filter(Boolean);
1203
+ };
1204
+ const isShardSection = (sectionKey) => String(sectionKey || "").startsWith("shard:");
1205
+ const extractRunId = (line) => {
1206
+ const text = String(line || "");
1207
+ const byRunTag = text.match(/^\[(?:run:|rid:)([A-Za-z0-9_-]+)\]\s*(.*)$/);
1208
+ const prefixedRunId = byRunTag?.[1] ? String(byRunTag[1]) : "";
1209
+ const tailText = byRunTag?.[2] ? String(byRunTag[2]) : text;
1210
+ const byRunIdField = tailText.match(/runId=([A-Za-z0-9_-]+)/);
1211
+ if (byRunIdField?.[1]) return String(byRunIdField[1]);
1212
+ const byRunIdTag = tailText.match(/\[runId:([A-Za-z0-9_-]+)\]/);
1213
+ if (byRunIdTag?.[1]) return String(byRunIdTag[1]);
1214
+ if (prefixedRunId) return prefixedRunId;
1215
+ const byRunTagAnyPos = text.match(/\[(?:run:|rid:)([A-Za-z0-9_-]+)\]/);
1216
+ if (byRunTagAnyPos?.[1]) return String(byRunTagAnyPos[1]);
1217
+ return "global";
1218
+ };
1219
+ const extractPrefixedRunId = (line) => {
1220
+ const text = String(line || "");
1221
+ const byRunTag = text.match(/^\[(?:run:|rid:)([A-Za-z0-9_-]+)\]/);
1222
+ return byRunTag?.[1] ? String(byRunTag[1]) : "";
1223
+ };
1224
+ const ensureSection = (sectionKey, headerLabel) => {
1225
+ const normalized = String(sectionKey || "global").trim() || "global";
1226
+ const existing = sectionMap.get(normalized);
1227
+ if (existing) {
1228
+ if (headerLabel && sectionHeaderMap.has(normalized)) {
1229
+ sectionHeaderMap.get(normalized).textContent = headerLabel;
1230
+ }
1231
+ return existing;
1232
+ }
1233
+ const card = createEl("div", {
1234
+ style: "border:1px solid #23262f; background:#0b0d12; border-radius:10px; overflow:hidden; min-width:0;"
1235
+ });
1236
+ const head = createEl("div", {
1237
+ style: "padding:8px 12px; border-bottom:1px solid #23262f; background:#121622; font-weight:600; color:#9aa4bd;"
1238
+ }, [headerLabel || (normalized === "global" ? "\u516C\u5171\u65E5\u5FD7" : `runId: ${normalized}`)]);
1239
+ const body = createEl("div", {
1240
+ style: "padding:10px 12px; white-space:pre-wrap; word-break:break-all; line-height:1.5; height:calc(100vh - 220px); overflow:auto;"
1241
+ });
1242
+ card.tabIndex = 0;
1243
+ card.appendChild(head);
1244
+ card.appendChild(body);
1245
+ if (normalized === "global" || !isShardSection(normalized)) {
1246
+ globalContainer.appendChild(card);
1247
+ sectionTypeMap.set(normalized, "global");
1248
+ } else {
1249
+ shardContainer.appendChild(card);
1250
+ sectionTypeMap.set(normalized, "shard");
1251
+ }
1252
+ sectionMap.set(normalized, body);
1253
+ sectionCardMap.set(normalized, card);
1254
+ sectionRunIds.set(normalized, /* @__PURE__ */ new Set());
1255
+ sectionHeaderMap.set(normalized, head);
1256
+ const activateCard = () => {
1257
+ selectedSectionKey = normalized;
1258
+ sectionCardMap.forEach((item, key) => {
1259
+ const isSelected = key === selectedSectionKey;
1260
+ item.style.outline = isSelected ? "2px solid #4f86ff" : "none";
1261
+ item.style.outlineOffset = isSelected ? "-2px" : "0";
1262
+ });
1263
+ };
1264
+ card.addEventListener("click", activateCard);
1265
+ card.addEventListener("focus", activateCard);
1266
+ return body;
1267
+ };
1268
+ const getSectionText = (sectionKey) => {
1269
+ const body = sectionMap.get(sectionKey);
1270
+ if (!body) return "";
1271
+ return Array.from(body.children).map((el) => String(el.innerText || el.textContent || "").trim()).filter(Boolean).join("\n");
1272
+ };
1273
+ const collectSectionText = (type) => {
1274
+ return Array.from(sectionMap.keys()).filter((sectionKey) => sectionTypeMap.get(sectionKey) === type).filter((sectionKey) => {
1275
+ const card = sectionCardMap.get(sectionKey);
1276
+ if (!card) return false;
1277
+ return card.style.display !== "none";
1278
+ }).map((sectionKey) => getSectionText(sectionKey)).filter(Boolean).join("\n");
1279
+ };
1280
+ const writeClipboard = async (text) => {
1281
+ if (!text) return { ok: false, error: "empty" };
1282
+ if (typeof window.api?.clipboardWriteText === "function") {
1283
+ return window.api.clipboardWriteText(text);
1284
+ }
1285
+ if (typeof navigator?.clipboard?.writeText === "function") {
1286
+ await navigator.clipboard.writeText(text);
1287
+ return { ok: true };
1288
+ }
1289
+ return { ok: false, error: "clipboard_unavailable" };
1290
+ };
1291
+ const copyByType = async (type) => {
1292
+ const text = collectSectionText(type);
1293
+ if (!text) {
1294
+ ctx2.setStatus?.(`\u65E5\u5FD7\u590D\u5236\u5931\u8D25\uFF1A${type === "global" ? "\u516C\u5171\u65E5\u5FD7\u4E3A\u7A7A" : "\u5206\u7247\u65E5\u5FD7\u4E3A\u7A7A"}`);
1295
+ return;
1296
+ }
1297
+ const ret = await writeClipboard(text);
1298
+ ctx2.setStatus?.(ret?.ok ? `\u5DF2\u590D\u5236${type === "global" ? "\u516C\u5171\u65E5\u5FD7" : "\u5206\u7247\u65E5\u5FD7"}\uFF08${text.split("\n").length}\u884C\uFF09` : `\u65E5\u5FD7\u590D\u5236\u5931\u8D25\uFF1A${ret?.error || "unknown"}`);
1299
+ };
1300
+ const copySelected = async () => {
1301
+ const sectionType = sectionTypeMap.get(selectedSectionKey) || "global";
1302
+ const text = selectedSectionKey ? getSectionText(selectedSectionKey) : "";
1303
+ if (text) {
1304
+ const ret = await writeClipboard(text);
1305
+ ctx2.setStatus?.(
1306
+ ret?.ok ? `\u5DF2\u590D\u5236\u5F53\u524D\u65E5\u5FD7\u5361\u7247\uFF08${selectedSectionKey}\uFF09` : `\u65E5\u5FD7\u590D\u5236\u5931\u8D25\uFF1A${ret?.error || "unknown"}`
1307
+ );
1308
+ return;
1309
+ }
1310
+ await copyByType(sectionType);
1311
+ };
1312
+ const resolveSectionKey = (text, rawRunId, prefixedRunId) => {
1313
+ if (rawRunId === "global") return "global";
1314
+ if (text.includes("[started]") && text.includes("xiaohongshu orchestrate")) {
1315
+ parentRunIds.add(rawRunId);
1316
+ }
1317
+ const mapped = runIdToSection.get(rawRunId);
1318
+ if (mapped) return mapped;
1319
+ if (prefixedRunId && rawRunId === prefixedRunId) {
1320
+ const currentSection = parentRunCurrentSection.get(prefixedRunId);
1321
+ if (currentSection) return currentSection;
1322
+ }
1323
+ if (!parentRunIds.has(rawRunId) && shardProfileQueue.length > 0) {
1324
+ const profile = String(shardProfileQueue.shift() || "").trim();
1325
+ if (profile) {
1326
+ const sectionKey = `shard:${profile}`;
1327
+ runIdToSection.set(rawRunId, sectionKey);
1328
+ shardRunIds.add(rawRunId);
1329
+ ensureSection(sectionKey, `\u5206\u7247: ${profile}`);
1330
+ return sectionKey;
1331
+ }
1332
+ }
1333
+ return rawRunId;
1334
+ };
1335
+ const updateSectionVisibility = () => {
1336
+ const activeOnly = activeOnlyCheckbox.checked;
1337
+ const showGlobal = showGlobalCheckbox.checked;
1338
+ const activeRunIds = ctx2._activeRunIds instanceof Set ? ctx2._activeRunIds : /* @__PURE__ */ new Set();
1339
+ const effectiveActiveRunIds = /* @__PURE__ */ new Set([...activeRunIds, ...logActiveRunIds]);
1340
+ sectionCardMap.forEach((card, sectionKey) => {
1341
+ if (sectionKey === "global") {
1342
+ card.style.display = showGlobal ? "" : "none";
1343
+ return;
1344
+ }
1345
+ if (!isShardSection(sectionKey)) {
1346
+ card.style.display = showGlobal ? "" : "none";
1347
+ return;
1348
+ }
1349
+ if (!activeOnly) {
1350
+ card.style.display = "";
1351
+ return;
1352
+ }
1353
+ const relatedRunIds = sectionRunIds.get(sectionKey) || /* @__PURE__ */ new Set();
1354
+ const visible = effectiveActiveRunIds.has(sectionKey) || shardRunIds.has(sectionKey) || Array.from(relatedRunIds).some((runId) => effectiveActiveRunIds.has(runId));
1355
+ card.style.display = visible ? "" : "none";
1356
+ });
1357
+ };
1358
+ const appendLine = (line) => {
1359
+ const text = String(line || "").trim();
1360
+ if (!text) return;
1361
+ const prefixedRunId = extractPrefixedRunId(text);
1362
+ const rawRunId = extractRunId(text);
1363
+ const hintedProfiles = parseShardHint(text);
1364
+ if (hintedProfiles.length > 0) {
1365
+ shardProfileQueue = hintedProfiles.slice();
1366
+ hintedProfiles.forEach((profile) => {
1367
+ const sectionKey2 = `shard:${profile}`;
1368
+ ensureSection(sectionKey2, `\u5206\u7247: ${profile}`);
1369
+ if (prefixedRunId && rawRunId !== "global") {
1370
+ parentRunCurrentSection.set(prefixedRunId, sectionKey2);
1371
+ runIdToSection.set(rawRunId, sectionKey2);
1372
+ shardRunIds.add(rawRunId);
1373
+ if (!sectionRunIds.has(sectionKey2)) {
1374
+ sectionRunIds.set(sectionKey2, /* @__PURE__ */ new Set());
1375
+ }
1376
+ sectionRunIds.get(sectionKey2).add(rawRunId);
1377
+ }
1378
+ });
1379
+ }
1380
+ const profileMatch = text.match(/\b[Pp]rofile\s*[::]\s*([A-Za-z0-9_-]+)/);
1381
+ const profileEqMatch = text.match(/\bprofile=([A-Za-z0-9_-]+)/);
1382
+ const resolvedProfile = String(profileMatch?.[1] || profileEqMatch?.[1] || "").trim();
1383
+ if (resolvedProfile) {
1384
+ const profileId = resolvedProfile;
1385
+ if (profileId) {
1386
+ const sectionKey2 = `shard:${profileId}`;
1387
+ ensureSection(sectionKey2, `\u5206\u7247: ${profileId}`);
1388
+ if (prefixedRunId && rawRunId && rawRunId !== prefixedRunId) {
1389
+ runIdToSection.set(rawRunId, sectionKey2);
1390
+ if (!sectionRunIds.has(sectionKey2)) {
1391
+ sectionRunIds.set(sectionKey2, /* @__PURE__ */ new Set());
1392
+ }
1393
+ sectionRunIds.get(sectionKey2).add(rawRunId);
1394
+ }
1395
+ if (prefixedRunId) {
1396
+ parentRunCurrentSection.set(prefixedRunId, sectionKey2);
1397
+ }
1398
+ }
1399
+ }
1400
+ const loggerChildRunId = text.match(/\[Logger\]\s+runId=([A-Za-z0-9_-]+)/);
1401
+ if (loggerChildRunId?.[1] && prefixedRunId) {
1402
+ const childRunId = String(loggerChildRunId[1]).trim();
1403
+ const parentSection = parentRunCurrentSection.get(prefixedRunId);
1404
+ if (parentSection) {
1405
+ shardRunIds.add(childRunId);
1406
+ runIdToSection.set(childRunId, parentSection);
1407
+ if (!sectionRunIds.has(parentSection)) {
1408
+ sectionRunIds.set(parentSection, /* @__PURE__ */ new Set());
1409
+ }
1410
+ sectionRunIds.get(parentSection).add(childRunId);
1411
+ }
1412
+ }
1413
+ if (rawRunId !== "global") {
1414
+ logActiveRunIds.add(rawRunId);
1415
+ if (text.includes("[exit]")) {
1416
+ logActiveRunIds.delete(rawRunId);
1417
+ }
1418
+ }
1419
+ const sectionKey = resolveSectionKey(text, rawRunId, prefixedRunId);
1420
+ const body = ensureSection(sectionKey);
1421
+ if (isShardSection(sectionKey) && rawRunId !== "global") {
1422
+ shardRunIds.add(rawRunId);
1423
+ }
1424
+ if (rawRunId !== "global") {
1425
+ if (!sectionRunIds.has(sectionKey)) {
1426
+ sectionRunIds.set(sectionKey, /* @__PURE__ */ new Set());
1427
+ }
1428
+ sectionRunIds.get(sectionKey)?.add(rawRunId);
1429
+ }
1430
+ const div = createEl("div", { className: "muted" }, [text]);
1431
+ body.appendChild(div);
1432
+ };
1433
+ clearBtn.onclick = () => {
1434
+ ctx2.clearLog();
1435
+ sectionMap.clear();
1436
+ sectionCardMap.clear();
1437
+ sectionRunIds.clear();
1438
+ sectionHeaderMap.clear();
1439
+ sectionTypeMap.clear();
1440
+ logActiveRunIds.clear();
1441
+ parentRunIds.clear();
1442
+ runIdToSection.clear();
1443
+ parentRunCurrentSection.clear();
1444
+ shardRunIds.clear();
1445
+ shardProfileQueue = [];
1446
+ selectedSectionKey = "global";
1447
+ globalContainer.textContent = "";
1448
+ shardContainer.textContent = "";
1449
+ };
1450
+ copyGlobalBtn.onclick = () => {
1451
+ void copyByType("global");
1452
+ };
1453
+ copyShardBtn.onclick = () => {
1454
+ void copyByType("shard");
1455
+ };
1456
+ activeOnlyCheckbox.onchange = () => {
1457
+ updateSectionVisibility();
1458
+ };
1459
+ showGlobalCheckbox.onchange = () => {
1460
+ updateSectionVisibility();
1461
+ };
1462
+ const existingLines = ctx2._logLines || [];
1463
+ if (Array.isArray(existingLines)) {
1464
+ existingLines.forEach((line) => {
1465
+ appendLine(line);
1466
+ });
1467
+ updateSectionVisibility();
1468
+ }
1469
+ const originalAppendLog = typeof ctx2._appendLogBase === "function" ? ctx2._appendLogBase : ctx2.appendLog;
1470
+ ctx2._appendLogBase = originalAppendLog;
1471
+ ctx2.appendLog = (line) => {
1472
+ appendLine(line);
1473
+ updateSectionVisibility();
1474
+ if (typeof originalAppendLog === "function") originalAppendLog.call(ctx2, line);
1475
+ };
1476
+ const unsubscribeActiveRuns = typeof ctx2.onActiveRunsChanged === "function" ? ctx2.onActiveRunsChanged(() => updateSectionVisibility()) : null;
1477
+ updateSectionVisibility();
1478
+ root.appendChild(title);
1479
+ root.appendChild(sub);
1480
+ root.appendChild(toolbar);
1481
+ root.appendChild(globalContainer);
1482
+ root.appendChild(shardContainer);
1483
+ const onKeydown = (evt) => {
1484
+ if (!(evt.ctrlKey || evt.metaKey) || !evt.shiftKey) return;
1485
+ const key = String(evt.key || "").toLowerCase();
1486
+ const code = String(evt.code || "");
1487
+ if (code === "Digit1" || key === "1") {
1488
+ evt.preventDefault();
1489
+ void copyByType("global");
1490
+ return;
1491
+ }
1492
+ if (code === "Digit2" || key === "2") {
1493
+ evt.preventDefault();
1494
+ void copyByType("shard");
1495
+ return;
1496
+ }
1497
+ if (code === "KeyC" || key === "c") {
1498
+ evt.preventDefault();
1499
+ void copySelected();
1500
+ }
1501
+ };
1502
+ root.addEventListener("keydown", onKeydown);
1503
+ ensureSection("global", "\u516C\u5171\u65E5\u5FD7");
1504
+ root.addEventListener("DOMNodeRemoved", () => {
1505
+ if (typeof unsubscribeActiveRuns === "function") unsubscribeActiveRuns();
1506
+ root.removeEventListener("keydown", onKeydown);
1507
+ }, { once: true });
1508
+ }
1509
+
1510
+ // src/renderer/account-source.mts
1511
+ function asText(value) {
1512
+ const text = String(value ?? "").trim();
1513
+ return text || null;
1514
+ }
1515
+ function normalizeRow(row) {
1516
+ const profileId = asText(row?.profileId);
1517
+ if (!profileId) return null;
1518
+ return {
1519
+ profileId,
1520
+ accountRecordId: asText(row?.accountRecordId),
1521
+ accountId: asText(row?.accountId),
1522
+ alias: asText(row?.alias),
1523
+ name: asText(row?.name),
1524
+ status: asText(row?.status) || "invalid",
1525
+ valid: row?.valid === true && Boolean(asText(row?.accountId)),
1526
+ reason: asText(row?.reason),
1527
+ updatedAt: asText(row?.updatedAt)
1528
+ };
1529
+ }
1530
+ async function listAccountProfiles(api) {
1531
+ const script = api.pathJoin("apps", "webauto", "entry", "account.mjs");
1532
+ const out = await api.cmdRunJson({
1533
+ title: "account list",
1534
+ cwd: "",
1535
+ args: [script, "list", "--json"],
1536
+ timeoutMs: 2e4
1537
+ });
1538
+ const rows = Array.isArray(out?.json?.profiles) ? out.json.profiles : [];
1539
+ return rows.map(normalizeRow).filter(Boolean);
1540
+ }
1541
+
1542
+ // src/renderer/tabs-new/setup-wizard.mts
1543
+ function renderSetupWizard(root, ctx2) {
1544
+ root.innerHTML = "";
1545
+ const header = createEl("div", { style: "margin-bottom:20px;" }, [
1546
+ createEl("h2", { style: "margin:0 0 8px 0; font-size:20px; color:#dbeafe;" }, ["\u73AF\u5883\u4E0E\u8D26\u6237\u521D\u59CB\u5316"]),
1547
+ createEl("div", { className: "muted", style: "font-size:13px;" }, ['\u9996\u6B21\u4F7F\u7528\u5FC5\u987B\u5B8C\u6210\u73AF\u5883\u68C0\u67E5\u4E0E\u8D26\u6237\u767B\u5F55\uFF0C\u4E4B\u540E\u53EF\u5728"\u8D26\u6237\u7BA1\u7406"Tab\u4E2D\u7EF4\u62A4'])
1548
+ ]);
1549
+ root.appendChild(header);
1550
+ const bentoGrid = createEl("div", { className: "bento-grid bento-sidebar" });
1551
+ const envCard = createEl("div", { className: "bento-cell" });
1552
+ envCard.innerHTML = `
1553
+ <div class="bento-title"><span style="color: var(--warning);">\u25CF</span> \u73AF\u5883\u68C0\u67E5</div>
1554
+ <div class="env-status-grid">
1555
+ <div class="env-item" id="env-camo">
1556
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
1557
+ <span>Camoufox CLI</span>
1558
+ </div>
1559
+ <div class="env-item" id="env-unified">
1560
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
1561
+ <span>Unified API (7701)</span>
1562
+ </div>
1563
+ <div class="env-item" id="env-browser">
1564
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
1565
+ <span>Browser Service (7704)</span>
1566
+ </div>
1567
+ <div class="env-item" id="env-firefox">
1568
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
1569
+ <span>Camoufox Runtime</span>
1570
+ </div>
1571
+ <div class="env-item" id="env-geoip">
1572
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
1573
+ <span>GeoIP Database</span>
1574
+ </div>
1575
+ </div>
1576
+ <div style="margin-top: var(--gap);">
1577
+ <button id="env-check-btn" class="secondary" style="width: 100%;">\u68C0\u67E5\u73AF\u5883</button>
1578
+ </div>
1579
+ `;
1580
+ bentoGrid.appendChild(envCard);
1581
+ const accountCard = createEl("div", { className: "bento-cell" });
1582
+ accountCard.innerHTML = `
1583
+ <div class="bento-title">\u8D26\u6237\u8BBE\u7F6E</div>
1584
+ <div class="account-list" id="account-list" style="margin-bottom: var(--gap); max-height: 200px; overflow: auto;">
1585
+ <div style="padding:12px; text-align:center; color:#8b93a6;">\u6682\u65E0\u8D26\u6237\uFF0C\u8BF7\u70B9\u51FB\u4E0B\u65B9\u6309\u94AE\u6DFB\u52A0</div>
1586
+ </div>
1587
+ <div class="row">
1588
+ <div>
1589
+ <label>\u65B0\u8D26\u6237\u522B\u540D</label>
1590
+ <input id="new-alias-input" placeholder="\u4F8B\u5982: \u7F8E\u98DF\u63A2\u5E97\u8D26\u53F7" style="width: 200px;" />
1591
+ </div>
1592
+ <button id="add-account-btn" style="flex: 0 0 auto; min-width: 120px;">\u6DFB\u52A0\u8D26\u6237</button>
1593
+ </div>
1594
+ `;
1595
+ bentoGrid.appendChild(accountCard);
1596
+ root.appendChild(bentoGrid);
1597
+ const statusRow = createEl("div", { className: "bento-grid", style: "margin-top: var(--gap);" });
1598
+ const statusCard = createEl("div", { className: "bento-cell highlight" });
1599
+ statusCard.id = "setup-status-card";
1600
+ statusCard.innerHTML = `
1601
+ <div style="display: flex; justify-content: space-between; align-items: center;">
1602
+ <div>
1603
+ <div style="font-size: 14px; font-weight: 600; color: var(--text-1); margin-bottom: 4px;">
1604
+ \u8BF7\u5B8C\u6210\u4E0A\u8FF0\u6B65\u9AA4
1605
+ </div>
1606
+ <div style="font-size: 12px; color: var(--text-3);" id="setup-status-text">
1607
+ \u73AF\u5883\u68C0\u67E5\u548C\u8D26\u6237\u767B\u5F55\u540E\u624D\u80FD\u5F00\u59CB\u4F7F\u7528
1608
+ </div>
1609
+ </div>
1610
+ <button id="enter-main-btn" class="secondary" disabled style="padding: 12px 32px; font-size: 14px;">\u8FDB\u5165\u4E3B\u754C\u9762</button>
1611
+ </div>
1612
+ `;
1613
+ statusRow.appendChild(statusCard);
1614
+ root.appendChild(statusRow);
1615
+ const envCheckBtn = root.querySelector("#env-check-btn");
1616
+ const addAccountBtn = root.querySelector("#add-account-btn");
1617
+ const newAliasInput = root.querySelector("#new-alias-input");
1618
+ const enterMainBtn = root.querySelector("#enter-main-btn");
1619
+ const accountListEl = root.querySelector("#account-list");
1620
+ const setupStatusText = root.querySelector("#setup-status-text");
1621
+ let envReady = false;
1622
+ let accounts = [];
1623
+ const isEnvReady = (snapshot) => Boolean(
1624
+ snapshot?.camo?.installed && snapshot?.services?.unifiedApi && snapshot?.services?.browserService && snapshot?.firefox?.installed && snapshot?.geoip?.installed
1625
+ );
1626
+ async function collectEnvironment() {
1627
+ const [camo, services, firefox, geoip] = await Promise.all([
1628
+ ctx2.api.envCheckCamo(),
1629
+ ctx2.api.envCheckServices(),
1630
+ ctx2.api.envCheckFirefox(),
1631
+ ctx2.api.envCheckGeoIP()
1632
+ ]);
1633
+ return { camo, services, firefox, geoip };
1634
+ }
1635
+ function applyEnvironment(snapshot) {
1636
+ updateEnvItem("env-camo", snapshot.camo?.installed, snapshot.camo?.version || (snapshot.camo?.installed ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5"));
1637
+ updateEnvItem("env-unified", snapshot.services?.unifiedApi, "7701");
1638
+ updateEnvItem("env-browser", snapshot.services?.browserService, "7704");
1639
+ updateEnvItem("env-firefox", snapshot.firefox?.installed, snapshot.firefox?.path ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5");
1640
+ updateEnvItem("env-geoip", snapshot.geoip?.installed, snapshot.geoip?.installed ? "\u5DF2\u5B89\u88C5" : "\u672A\u5B89\u88C5");
1641
+ envReady = isEnvReady(snapshot);
1642
+ }
1643
+ async function autoRepairEnvironment(snapshot) {
1644
+ const missingCore = !snapshot.services?.unifiedApi || !snapshot.services?.browserService;
1645
+ const missingCamo = !snapshot.camo?.installed;
1646
+ const missingRuntime = !snapshot.firefox?.installed;
1647
+ const missingGeoIP = !snapshot.geoip?.installed;
1648
+ if (!missingCore && !missingCamo && !missingRuntime && !missingGeoIP) return;
1649
+ setupStatusText.textContent = "\u68C0\u6D4B\u5230\u4F9D\u8D56\u7F3A\u5931\uFF0C\u6B63\u5728\u81EA\u52A8\u4FEE\u590D...";
1650
+ if (missingCore && typeof ctx2.api?.envRepairCore === "function") {
1651
+ setupStatusText.textContent = "\u6B63\u5728\u62C9\u8D77\u6838\u5FC3\u670D\u52A1...";
1652
+ await ctx2.api.envRepairCore().catch(() => null);
1653
+ }
1654
+ if ((missingCamo || missingRuntime || missingGeoIP) && typeof ctx2.api?.cmdRunJson === "function") {
1655
+ setupStatusText.textContent = "\u6B63\u5728\u5B89\u88C5\u4F9D\u8D56\uFF08Camoufox/GeoIP\uFF09...";
1656
+ const script = ctx2.api.pathJoin("apps", "webauto", "entry", "xhs-install.mjs");
1657
+ const args = [script];
1658
+ if (missingRuntime || missingCamo) args.push("--download-browser");
1659
+ if (missingGeoIP) args.push("--download-geoip");
1660
+ args.push("--ensure-backend");
1661
+ await ctx2.api.cmdRunJson({
1662
+ title: "setup auto repair",
1663
+ cwd: "",
1664
+ args,
1665
+ timeoutMs: 3e5
1666
+ }).catch(() => null);
1667
+ }
1668
+ }
1669
+ async function checkEnvironment() {
1670
+ envCheckBtn.disabled = true;
1671
+ envCheckBtn.textContent = "\u68C0\u67E5/\u4FEE\u590D\u4E2D...";
1672
+ try {
1673
+ const before = await collectEnvironment();
1674
+ applyEnvironment(before);
1675
+ if (!isEnvReady(before)) {
1676
+ await autoRepairEnvironment(before);
1677
+ }
1678
+ const after = await collectEnvironment();
1679
+ applyEnvironment(after);
1680
+ updateCompleteStatus();
1681
+ if (!envReady) {
1682
+ const missing = [];
1683
+ if (!after?.camo?.installed) missing.push("camo");
1684
+ if (!after?.services?.unifiedApi) missing.push("unified-api");
1685
+ if (!after?.services?.browserService) missing.push("browser-service");
1686
+ if (!after?.firefox?.installed) missing.push("camoufox-runtime");
1687
+ if (!after?.geoip?.installed) missing.push("geoip");
1688
+ setupStatusText.textContent = `\u81EA\u52A8\u4FEE\u590D\u540E\u4ECD\u7F3A\u5931: ${missing.join(", ")}`;
1689
+ }
1690
+ } catch (err) {
1691
+ console.error("Environment check failed:", err);
1692
+ setupStatusText.textContent = "\u73AF\u5883\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B\u65E5\u5FD7\u5E76\u91CD\u8BD5";
1693
+ }
1694
+ envCheckBtn.disabled = false;
1695
+ envCheckBtn.textContent = "\u91CD\u65B0\u68C0\u67E5";
1696
+ }
1697
+ function updateEnvItem(id, ok, detail) {
1698
+ const el = root.querySelector(`#${id}`);
1699
+ if (!el) return;
1700
+ const icon = el.querySelector(".icon");
1701
+ const text = el.querySelector("span:last-child");
1702
+ const baseLabel = el.dataset.label || text.textContent || "";
1703
+ el.dataset.label = baseLabel;
1704
+ icon.textContent = ok ? "\u2713" : "\u2717";
1705
+ icon.style.color = ok ? "var(--success)" : "var(--danger)";
1706
+ text.textContent = detail ? `${baseLabel} \xB7 ${detail}` : baseLabel;
1707
+ }
1708
+ async function refreshAccounts() {
1709
+ try {
1710
+ accounts = await listAccountProfiles(ctx2.api);
1711
+ renderAccountList();
1712
+ updateCompleteStatus();
1713
+ } catch (err) {
1714
+ console.error("Failed to refresh accounts:", err);
1715
+ }
1716
+ }
1717
+ function renderAccountList() {
1718
+ accountListEl.innerHTML = "";
1719
+ if (accounts.length === 0) {
1720
+ accountListEl.innerHTML = '<div style="padding:12px; text-align:center; color:#8b93a6;">\u6682\u65E0\u8D26\u6237\uFF0C\u8BF7\u70B9\u51FB\u4E0B\u65B9\u6309\u94AE\u6DFB\u52A0</div>';
1721
+ return;
1722
+ }
1723
+ accounts.forEach((acc) => {
1724
+ const statusLabel = acc.valid ? "\u2713 \u6709\u6548" : acc.status === "pending" ? "\u23F3 \u5F85\u767B\u5F55" : "\u2717 \u5931\u6548";
1725
+ const statusClass = acc.valid ? "status-valid" : acc.status === "pending" ? "status-pending" : "status-expired";
1726
+ const row = createEl("div", {
1727
+ style: "display:flex; justify-content:space-between; align-items:center; padding:8px 12px; border-bottom:1px solid var(--border);"
1728
+ }, [
1729
+ createEl("div", {}, [
1730
+ createEl("div", { style: "font-weight:600; margin-bottom:2px;" }, [acc.alias || acc.name || acc.profileId]),
1731
+ createEl("div", { className: "muted", style: "font-size:11px;" }, [acc.profileId])
1732
+ ]),
1733
+ createEl("span", {
1734
+ className: `status-badge ${statusClass}`
1735
+ }, [statusLabel])
1736
+ ]);
1737
+ accountListEl.appendChild(row);
1738
+ });
1739
+ }
1740
+ async function addAccount() {
1741
+ const alias = newAliasInput.value.trim();
1742
+ if (!alias) {
1743
+ alert("\u8BF7\u8F93\u5165\u8D26\u6237\u522B\u540D");
1744
+ return;
1745
+ }
1746
+ addAccountBtn.disabled = true;
1747
+ addAccountBtn.textContent = "\u521B\u5EFA\u4E2D...";
1748
+ try {
1749
+ const batchKey = "xiaohongshu";
1750
+ const out = await ctx2.api.cmdRunJson({
1751
+ title: "profilepool add",
1752
+ cwd: "",
1753
+ args: [ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"), "add", batchKey, "--json"]
1754
+ });
1755
+ if (!out?.ok || !out?.json?.profileId) {
1756
+ alert("\u521B\u5EFA\u8D26\u53F7\u5931\u8D25: " + (out?.error || "\u672A\u77E5\u9519\u8BEF"));
1757
+ return;
1758
+ }
1759
+ const profileId = out.json.profileId;
1760
+ const aliases = { ...ctx2.api.settings?.profileAliases, [profileId]: alias };
1761
+ await ctx2.api.settingsSet({ profileAliases: aliases });
1762
+ if (typeof ctx2.refreshSettings === "function") {
1763
+ await ctx2.refreshSettings();
1764
+ }
1765
+ const timeoutSec = ctx2.api.settings?.timeouts?.loginTimeoutSec || 900;
1766
+ const loginArgs = [
1767
+ ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
1768
+ "login-profile",
1769
+ profileId,
1770
+ "--timeout-sec",
1771
+ String(timeoutSec),
1772
+ "--keep-session"
1773
+ ];
1774
+ await ctx2.api.cmdSpawn({
1775
+ title: `\u767B\u5F55 ${alias}`,
1776
+ cwd: "",
1777
+ args: loginArgs,
1778
+ groupKey: "profilepool"
1779
+ });
1780
+ newAliasInput.value = "";
1781
+ await refreshAccounts();
1782
+ } catch (err) {
1783
+ alert("\u6DFB\u52A0\u8D26\u53F7\u5931\u8D25: " + (err?.message || String(err)));
1784
+ } finally {
1785
+ addAccountBtn.disabled = false;
1786
+ addAccountBtn.textContent = "\u6DFB\u52A0\u8D26\u6237";
1787
+ }
1788
+ }
1789
+ function updateCompleteStatus() {
1790
+ const hasValidAccount = accounts.some((a) => a.valid);
1791
+ const canProceed = envReady && hasValidAccount;
1792
+ enterMainBtn.disabled = !canProceed;
1793
+ if (canProceed) {
1794
+ setupStatusText.textContent = `\u73AF\u5883\u5C31\u7EEA\uFF0C${accounts.length} \u4E2A\u8D26\u6237\u914D\u7F6E\u5B8C\u6210`;
1795
+ enterMainBtn.className = "";
1796
+ } else {
1797
+ const missing = [];
1798
+ if (!envReady) missing.push("\u73AF\u5883\u68C0\u67E5");
1799
+ if (!hasValidAccount) missing.push("\u81F3\u5C11\u4E00\u4E2A\u8D26\u6237");
1800
+ setupStatusText.textContent = `\u5C1A\u672A\u5B8C\u6210: ${missing.join("\u3001")}`;
1801
+ }
1802
+ }
1803
+ envCheckBtn.onclick = checkEnvironment;
1804
+ addAccountBtn.onclick = addAccount;
1805
+ enterMainBtn.onclick = () => {
1806
+ if (typeof ctx2.setActiveTab === "function") {
1807
+ ctx2.setActiveTab("config");
1808
+ }
1809
+ };
1810
+ void checkEnvironment();
1811
+ void refreshAccounts();
1812
+ }
1813
+
1814
+ // src/renderer/tabs-new/config-panel.mts
1815
+ function renderConfigPanel(root, ctx2) {
1816
+ root.innerHTML = "";
1817
+ const pageIndicator = createEl("div", { className: "page-indicator" }, [
1818
+ "\u5F53\u524D: ",
1819
+ createEl("span", {}, ["\u914D\u7F6E\u9875"]),
1820
+ " \u2192 \u5B8C\u6210\u540E\u8DF3\u8F6C ",
1821
+ createEl("span", {}, ["\u770B\u677F\u9875"])
1822
+ ]);
1823
+ root.appendChild(pageIndicator);
1824
+ const bentoGrid = createEl("div", { className: "bento-grid bento-sidebar" });
1825
+ const targetCard = createEl("div", { className: "bento-cell" });
1826
+ targetCard.innerHTML = `
1827
+ <div class="bento-title">\u76EE\u6807\u8BBE\u5B9A</div>
1828
+
1829
+ <div class="row">
1830
+ <div>
1831
+ <label>\u641C\u7D22\u5173\u952E\u8BCD</label>
1832
+ <input id="keyword-input" placeholder="\u8F93\u5165\u5173\u952E\u8BCD" style="width: 200px;" />
1833
+ </div>
1834
+ </div>
1835
+
1836
+ <div class="row">
1837
+ <div>
1838
+ <label>\u76EE\u6807\u6570\u91CF</label>
1839
+ <input id="target-input" type="number" value="50" min="1" style="width: 100px;" />
1840
+ </div>
1841
+ <div>
1842
+ <label>\u8FD0\u884C\u73AF\u5883</label>
1843
+ <select id="env-select" style="width: 120px;">
1844
+ <option value="debug">\u8C03\u8BD5\u6A21\u5F0F</option>
1845
+ <option value="prod">\u751F\u4EA7\u6A21\u5F0F</option>
1846
+ </select>
1847
+ </div>
1848
+ </div>
1849
+
1850
+ <div>
1851
+ <label>\u9009\u62E9\u8D26\u6237</label>
1852
+ <select id="account-select" style="min-width: 200px;">
1853
+ <option value="">\u8BF7\u9009\u62E9\u8D26\u6237...</option>
1854
+ </select>
1855
+ </div>
1856
+
1857
+ <div style="margin-top: var(--gap); padding-top: var(--gap); border-top: 1px solid var(--border);">
1858
+ <div class="bento-title" style="font-size: 13px;">\u914D\u7F6E\u9884\u8BBE</div>
1859
+ <div class="row">
1860
+ <select id="preset-select" style="width: 200px;">
1861
+ <option value="last">\u4E0A\u6B21\u914D\u7F6E</option>
1862
+ <option value="full">\u9884\u8BBE1\uFF1A\u5168\u91CF\u722C\u53D6</option>
1863
+ <option value="body-only">\u9884\u8BBE2\uFF1A\u4EC5\u6B63\u6587</option>
1864
+ <option value="quick">\u9884\u8BBE3\uFF1A\u5FEB\u901F\u91C7\u96C6</option>
1865
+ </select>
1866
+ </div>
1867
+ <div class="btn-group">
1868
+ <button id="import-btn" class="secondary" style="flex: 1;">\u5BFC\u5165\u914D\u7F6E</button>
1869
+ <button id="export-btn" class="secondary" style="flex: 1;">\u5BFC\u51FA\u914D\u7F6E</button>
1870
+ </div>
1871
+ </div>
1872
+ `;
1873
+ bentoGrid.appendChild(targetCard);
1874
+ const optionsCard = createEl("div", { className: "bento-cell" });
1875
+ optionsCard.innerHTML = `
1876
+ <div class="bento-title">\u722C\u53D6\u9009\u9879</div>
1877
+
1878
+ <div style="display: flex; gap: var(--gap); margin-bottom: var(--gap);">
1879
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1880
+ <input id="fetch-body-cb" type="checkbox" checked />
1881
+ <span>\u722C\u53D6\u6B63\u6587</span>
1882
+ </label>
1883
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1884
+ <input id="fetch-comments-cb" type="checkbox" checked />
1885
+ <span>\u722C\u53D6\u8BC4\u8BBA</span>
1886
+ </label>
1887
+ </div>
1888
+
1889
+ <div class="row">
1890
+ <div>
1891
+ <label>\u6700\u591A\u8BC4\u8BBA\u6570</label>
1892
+ <input id="max-comments-input" type="number" value="100" min="0" style="width: 100px;" />
1893
+ </div>
1894
+ </div>
1895
+
1896
+ <div style="margin-top: var(--gap); padding-top: var(--gap); border-top: 1px solid var(--border);">
1897
+ <div class="bento-title" style="font-size: 13px;">\u70B9\u8D5E\u8BBE\u7F6E</div>
1898
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; margin-bottom: var(--gap-sm);">
1899
+ <input id="auto-like-cb" type="checkbox" />
1900
+ <span>\u81EA\u52A8\u70B9\u8D5E</span>
1901
+ </label>
1902
+ <div>
1903
+ <label>\u70B9\u8D5E\u5173\u952E\u8BCD (\u9017\u53F7\u5206\u9694)</label>
1904
+ <input id="like-keywords-input" placeholder="\u4F8B\u5982: \u7F8E\u98DF,\u65C5\u6E38,\u6444\u5F71" disabled />
1905
+ </div>
1906
+ </div>
1907
+
1908
+ <div style="margin-top: var(--gap); padding-top: var(--gap); border-top: 1px solid var(--border);">
1909
+ <div class="bento-title" style="font-size: 13px;">\u9AD8\u7EA7\u9009\u9879</div>
1910
+ <div style="display: flex; gap: var(--gap);">
1911
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1912
+ <input id="headless-cb" type="checkbox" />
1913
+ <span>Headless</span>
1914
+ </label>
1915
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1916
+ <input id="dry-run-cb" type="checkbox" />
1917
+ <span>Dry Run</span>
1918
+ </label>
1919
+ </div>
1920
+ </div>
1921
+ `;
1922
+ bentoGrid.appendChild(optionsCard);
1923
+ root.appendChild(bentoGrid);
1924
+ const actionRow = createEl("div", { className: "bento-grid", style: "margin-top: var(--gap);" });
1925
+ const actionCard = createEl("div", { className: "bento-cell highlight" });
1926
+ actionCard.innerHTML = `
1927
+ <div style="text-align: center;">
1928
+ <button id="start-btn" style="padding: 14px 64px; font-size: 15px;">\u5F00\u59CB\u722C\u53D6</button>
1929
+ </div>
1930
+ `;
1931
+ actionRow.appendChild(actionCard);
1932
+ root.appendChild(actionRow);
1933
+ const keywordInput = root.querySelector("#keyword-input");
1934
+ const targetInput = root.querySelector("#target-input");
1935
+ const envSelect = root.querySelector("#env-select");
1936
+ const accountSelect = root.querySelector("#account-select");
1937
+ const presetSelect = root.querySelector("#preset-select");
1938
+ const fetchBodyCb = root.querySelector("#fetch-body-cb");
1939
+ const fetchCommentsCb = root.querySelector("#fetch-comments-cb");
1940
+ const maxCommentsInput = root.querySelector("#max-comments-input");
1941
+ const autoLikeCb = root.querySelector("#auto-like-cb");
1942
+ const likeKeywordsInput = root.querySelector("#like-keywords-input");
1943
+ const headlessCb = root.querySelector("#headless-cb");
1944
+ const dryRunCb = root.querySelector("#dry-run-cb");
1945
+ const startBtn = root.querySelector("#start-btn");
1946
+ const importBtn = root.querySelector("#import-btn");
1947
+ const exportBtn = root.querySelector("#export-btn");
1948
+ let saveTimeout = null;
1949
+ let accountRows = [];
1950
+ let preferredProfileId = "";
1951
+ function buildConfigPayload() {
1952
+ return {
1953
+ keyword: keywordInput.value.trim(),
1954
+ target: parseInt(targetInput.value) || 50,
1955
+ env: envSelect.value,
1956
+ fetchBody: fetchBodyCb.checked,
1957
+ fetchComments: fetchCommentsCb.checked,
1958
+ maxComments: parseInt(maxCommentsInput.value) || 100,
1959
+ autoLike: autoLikeCb.checked,
1960
+ likeKeywords: likeKeywordsInput.value.trim(),
1961
+ headless: headlessCb.checked,
1962
+ dryRun: dryRunCb.checked,
1963
+ lastProfileId: accountSelect.value || void 0
1964
+ };
1965
+ }
1966
+ async function loadConfig() {
1967
+ try {
1968
+ const config = await ctx2.api.configLoadLast();
1969
+ if (config) {
1970
+ keywordInput.value = config.keyword || "";
1971
+ targetInput.value = String(config.target || 50);
1972
+ envSelect.value = config.env || "debug";
1973
+ fetchBodyCb.checked = config.fetchBody !== false;
1974
+ fetchCommentsCb.checked = config.fetchComments !== false;
1975
+ maxCommentsInput.value = String(config.maxComments || 100);
1976
+ autoLikeCb.checked = config.autoLike === true;
1977
+ likeKeywordsInput.value = config.likeKeywords || "";
1978
+ headlessCb.checked = config.headless === true;
1979
+ dryRunCb.checked = config.dryRun === true;
1980
+ preferredProfileId = String(config.lastProfileId || "").trim();
1981
+ updateLikeKeywordsState();
1982
+ }
1983
+ } catch (err) {
1984
+ console.error("Failed to load config:", err);
1985
+ }
1986
+ }
1987
+ function saveConfig() {
1988
+ if (saveTimeout) clearTimeout(saveTimeout);
1989
+ saveTimeout = setTimeout(async () => {
1990
+ const config = buildConfigPayload();
1991
+ try {
1992
+ await ctx2.api.configSaveLast(config);
1993
+ } catch (err) {
1994
+ console.error("Failed to save config:", err);
1995
+ }
1996
+ }, 1e3);
1997
+ }
1998
+ async function loadAccounts() {
1999
+ try {
2000
+ accountRows = await listAccountProfiles(ctx2.api);
2001
+ const validRows = accountRows.filter((row) => row.valid);
2002
+ accountSelect.innerHTML = '<option value="">\u8BF7\u9009\u62E9\u8D26\u6237...</option>';
2003
+ validRows.forEach((row) => {
2004
+ const profileId = String(row.profileId || "");
2005
+ const label = row.alias ? `${row.alias} (${profileId})` : row.name || profileId;
2006
+ const opt = createEl("option", { value: profileId }, [label]);
2007
+ accountSelect.appendChild(opt);
2008
+ });
2009
+ if (preferredProfileId && validRows.some((row) => row.profileId === preferredProfileId)) {
2010
+ accountSelect.value = preferredProfileId;
2011
+ }
2012
+ } catch (err) {
2013
+ console.error("Failed to load accounts:", err);
2014
+ }
2015
+ }
2016
+ async function exportConfig() {
2017
+ try {
2018
+ const config = buildConfigPayload();
2019
+ const home = ctx2.api.osHomedir();
2020
+ const downloadsPath = ctx2.api.pathJoin(home, "Downloads");
2021
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2022
+ const filePath = ctx2.api.pathJoin(downloadsPath, `webauto-config-${timestamp}.json`);
2023
+ const result = await ctx2.api.configExport({ filePath, config });
2024
+ if (result.ok) {
2025
+ alert(`\u914D\u7F6E\u5DF2\u5BFC\u51FA\u5230: ${result.path}`);
2026
+ }
2027
+ } catch (err) {
2028
+ alert("\u5BFC\u51FA\u5931\u8D25: " + (err?.message || String(err)));
2029
+ }
2030
+ }
2031
+ async function importConfig() {
2032
+ const input = document.createElement("input");
2033
+ input.type = "file";
2034
+ input.accept = ".json";
2035
+ input.onchange = async (e) => {
2036
+ const file = e.target.files?.[0];
2037
+ if (!file) return;
2038
+ try {
2039
+ const text = await file.text();
2040
+ const config = JSON.parse(text.replace(/^\uFEFF/, ""));
2041
+ keywordInput.value = config.keyword || "";
2042
+ targetInput.value = String(config.target || 50);
2043
+ envSelect.value = config.env || "debug";
2044
+ fetchBodyCb.checked = config.fetchBody !== false;
2045
+ fetchCommentsCb.checked = config.fetchComments !== false;
2046
+ maxCommentsInput.value = String(config.maxComments || 100);
2047
+ autoLikeCb.checked = config.autoLike === true;
2048
+ likeKeywordsInput.value = config.likeKeywords || "";
2049
+ headlessCb.checked = config.headless === true;
2050
+ dryRunCb.checked = config.dryRun === true;
2051
+ updateLikeKeywordsState();
2052
+ saveConfig();
2053
+ alert("\u914D\u7F6E\u5DF2\u5BFC\u5165");
2054
+ } catch (err) {
2055
+ alert("\u5BFC\u5165\u5931\u8D25: " + (err?.message || String(err)));
2056
+ }
2057
+ };
2058
+ input.click();
2059
+ }
2060
+ function updateLikeKeywordsState() {
2061
+ likeKeywordsInput.disabled = !autoLikeCb.checked;
2062
+ likeKeywordsInput.style.opacity = autoLikeCb.checked ? "1" : "0.5";
2063
+ }
2064
+ async function startCrawl() {
2065
+ const keyword = keywordInput.value.trim();
2066
+ if (!keyword) {
2067
+ alert("\u8BF7\u8F93\u5165\u5173\u952E\u8BCD");
2068
+ return;
2069
+ }
2070
+ const profileId = accountSelect.value;
2071
+ if (!profileId) {
2072
+ alert("\u8BF7\u9009\u62E9\u8D26\u6237");
2073
+ return;
2074
+ }
2075
+ const account = accountRows.find((row) => row.profileId === profileId);
2076
+ if (!account || !account.valid) {
2077
+ alert("\u5F53\u524D\u8D26\u6237\u65E0\u6548\uFF0C\u8BF7\u5148\u5230\u201C\u8D26\u6237\u7BA1\u7406\u201D\u5B8C\u6210\u767B\u5F55\u5E76\u6821\u9A8C");
2078
+ return;
2079
+ }
2080
+ const config = buildConfigPayload();
2081
+ try {
2082
+ await ctx2.api.configSaveLast(config);
2083
+ } catch {
2084
+ }
2085
+ const script = ctx2.api.pathJoin("apps", "webauto", "entry", "xhs-unified.mjs");
2086
+ const outputRoot = String(ctx2?.settings?.downloadRoot || "").trim();
2087
+ const args = [
2088
+ script,
2089
+ "--profile",
2090
+ profileId,
2091
+ "--keyword",
2092
+ config.keyword,
2093
+ "--max-notes",
2094
+ String(config.target),
2095
+ "--env",
2096
+ config.env,
2097
+ "--do-comments",
2098
+ config.fetchComments ? "true" : "false",
2099
+ "--persist-comments",
2100
+ config.fetchComments ? "true" : "false",
2101
+ "--do-likes",
2102
+ config.autoLike ? "true" : "false",
2103
+ "--like-keywords",
2104
+ config.likeKeywords || "",
2105
+ "--headless",
2106
+ config.headless ? "true" : "false"
2107
+ ];
2108
+ if (outputRoot) args.push("--output-root", outputRoot);
2109
+ if (config.dryRun) args.push("--dry-run");
2110
+ else args.push("--no-dry-run");
2111
+ startBtn.disabled = true;
2112
+ const prevText = startBtn.textContent;
2113
+ startBtn.textContent = "\u542F\u52A8\u4E2D...";
2114
+ try {
2115
+ const ret = await ctx2.api.cmdSpawn({
2116
+ title: `xhs unified ${config.keyword}`.trim(),
2117
+ cwd: "",
2118
+ args,
2119
+ groupKey: "xiaohongshu"
2120
+ });
2121
+ const runId = String(ret?.runId || "").trim();
2122
+ if (!runId) {
2123
+ throw new Error("runId \u4E3A\u7A7A");
2124
+ }
2125
+ ctx2.xhsCurrentRun = {
2126
+ runId,
2127
+ profileId,
2128
+ keyword: config.keyword,
2129
+ target: config.target,
2130
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2131
+ };
2132
+ if (typeof ctx2.appendLog === "function") {
2133
+ ctx2.appendLog(`[ui] started xhs-unified runId=${runId} profile=${profileId} keyword=${config.keyword}`);
2134
+ }
2135
+ if (typeof ctx2.setStatus === "function") {
2136
+ ctx2.setStatus(`running: xhs-unified ${config.keyword}`);
2137
+ }
2138
+ } catch (err) {
2139
+ alert(`\u542F\u52A8\u5931\u8D25: ${err?.message || String(err)}`);
2140
+ if (typeof ctx2.appendLog === "function") {
2141
+ ctx2.appendLog(`[ui][error] xhs-unified \u542F\u52A8\u5931\u8D25: ${err?.message || String(err)}`);
2142
+ }
2143
+ return;
2144
+ } finally {
2145
+ startBtn.disabled = false;
2146
+ startBtn.textContent = prevText || "\u5F00\u59CB\u722C\u53D6";
2147
+ }
2148
+ if (typeof ctx2.setActiveTab === "function") {
2149
+ ctx2.setActiveTab("dashboard");
2150
+ }
2151
+ }
2152
+ autoLikeCb.onchange = updateLikeKeywordsState;
2153
+ importBtn.onclick = importConfig;
2154
+ exportBtn.onclick = exportConfig;
2155
+ startBtn.onclick = startCrawl;
2156
+ [
2157
+ keywordInput,
2158
+ targetInput,
2159
+ envSelect,
2160
+ accountSelect,
2161
+ fetchBodyCb,
2162
+ fetchCommentsCb,
2163
+ maxCommentsInput,
2164
+ autoLikeCb,
2165
+ likeKeywordsInput,
2166
+ headlessCb,
2167
+ dryRunCb
2168
+ ].forEach((el) => {
2169
+ el.onchange = saveConfig;
2170
+ if (el.tagName === "INPUT" && el.type !== "checkbox") {
2171
+ el.oninput = saveConfig;
2172
+ }
2173
+ });
2174
+ void loadConfig();
2175
+ void loadAccounts();
2176
+ }
2177
+
2178
+ // src/renderer/tabs-new/dashboard.mts
2179
+ function renderDashboard(root, ctx2) {
2180
+ root.innerHTML = "";
2181
+ const pageIndicator = createEl("div", { className: "page-indicator" }, [
2182
+ "\u5F53\u524D: ",
2183
+ createEl("span", {}, ["\u770B\u677F\u9875"]),
2184
+ " \u2190 \u4ECE\u914D\u7F6E\u9875\u8DF3\u5165 | \u5B8C\u6210\u540E\u8FD4\u56DE ",
2185
+ createEl("span", {}, ["\u914D\u7F6E\u9875"])
2186
+ ]);
2187
+ root.appendChild(pageIndicator);
2188
+ const statsGrid = createEl("div", { className: "bento-grid bento-4", style: "margin-bottom: var(--gap);" });
2189
+ statsGrid.innerHTML = `
2190
+ <div class="stat-card info">
2191
+ <div class="stat-value" id="stat-collected">0</div>
2192
+ <div class="stat-label">\u5DF2\u91C7\u96C6</div>
2193
+ </div>
2194
+ <div class="stat-card success">
2195
+ <div class="stat-value" id="stat-success">0</div>
2196
+ <div class="stat-label">\u6210\u529F</div>
2197
+ </div>
2198
+ <div class="stat-card danger">
2199
+ <div class="stat-value" id="stat-failed">0</div>
2200
+ <div class="stat-label">\u5931\u8D25</div>
2201
+ </div>
2202
+ <div class="stat-card warning">
2203
+ <div class="stat-value" id="stat-remaining">0</div>
2204
+ <div class="stat-label">\u5269\u4F59</div>
2205
+ </div>
2206
+ `;
2207
+ root.appendChild(statsGrid);
2208
+ const runSummaryGrid = createEl("div", { className: "bento-grid", style: "margin-bottom: var(--gap);" });
2209
+ const runSummaryCard = createEl("div", { className: "bento-cell highlight" });
2210
+ runSummaryCard.innerHTML = `
2211
+ <div class="bento-title">\u8FD0\u884C\u6458\u8981</div>
2212
+ <div style="display:grid; grid-template-columns: minmax(200px, 1fr) 160px; gap: var(--gap); margin-bottom: var(--gap-sm);">
2213
+ <div>
2214
+ <label>\u5F53\u524D Run ID</label>
2215
+ <div id="run-id-text" style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-1); word-break: break-all;">-</div>
2216
+ </div>
2217
+ <div>
2218
+ <label>\u7D2F\u8BA1\u9519\u8BEF</label>
2219
+ <div id="error-count-text" style="font-weight:700; color: var(--danger); font-size: 18px;">0</div>
2220
+ </div>
2221
+ </div>
2222
+ <div>
2223
+ <label>\u6700\u8FD1\u9519\u8BEF\uFF08\u6700\u591A 8 \u6761\uFF09</label>
2224
+ <div id="recent-errors-empty" class="muted" style="font-size: 12px;">\u6682\u65E0\u9519\u8BEF</div>
2225
+ <ul id="recent-errors-list" style="margin: 6px 0 0 16px; padding: 0; font-size: 12px; line-height: 1.5; display:none;"></ul>
2226
+ </div>
2227
+ `;
2228
+ runSummaryGrid.appendChild(runSummaryCard);
2229
+ root.appendChild(runSummaryGrid);
2230
+ const mainGrid = createEl("div", { className: "bento-grid bento-aside" });
2231
+ const taskCard = createEl("div", { className: "bento-cell" });
2232
+ taskCard.innerHTML = `
2233
+ <div class="bento-title">\u5F53\u524D\u4EFB\u52A1</div>
2234
+
2235
+ <div class="row" style="margin-bottom: var(--gap);">
2236
+ <div>
2237
+ <label>\u5173\u952E\u8BCD</label>
2238
+ <div id="task-keyword" style="font-weight: 600; color: var(--text-1);">-</div>
2239
+ </div>
2240
+ <div>
2241
+ <label>\u76EE\u6807\u6570\u91CF</label>
2242
+ <div id="task-target" style="font-weight: 600; color: var(--text-1);">-</div>
2243
+ </div>
2244
+ <div>
2245
+ <label>\u4F7F\u7528\u8D26\u6237</label>
2246
+ <div id="task-account" style="font-weight: 600; color: var(--text-1);">-</div>
2247
+ </div>
2248
+ </div>
2249
+
2250
+ <div class="phase-indicator" style="margin-bottom: var(--gap);">
2251
+ <div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
2252
+ <span style="color: var(--text-3); font-size: 12px;">\u5F53\u524D\u9636\u6BB5</span>
2253
+ <span id="current-phase" style="font-weight: 600; color: var(--accent-light);">\u5F85\u542F\u52A8</span>
2254
+ </div>
2255
+ <div style="display: flex; justify-content: space-between;">
2256
+ <span style="color: var(--text-3); font-size: 12px;">\u5F53\u524D\u64CD\u4F5C</span>
2257
+ <span id="current-action" style="color: var(--text-1);">-</span>
2258
+ </div>
2259
+ </div>
2260
+
2261
+ <div style="margin-bottom: var(--gap);">
2262
+ <div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
2263
+ <span style="font-size: 12px; color: var(--text-3);">\u6574\u4F53\u8FDB\u5EA6</span>
2264
+ <span id="progress-percent" style="font-size: 12px; color: var(--text-1);">0%</span>
2265
+ </div>
2266
+ <div class="progress-bar-container">
2267
+ <div id="progress-bar" class="progress-bar" style="width: 0%;"></div>
2268
+ </div>
2269
+ </div>
2270
+
2271
+ <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gap-sm);">
2272
+ <div style="font-size: 12px;"><span style="color: var(--text-4);">\u8BC4\u8BBA\u91C7\u96C6\uFF1A</span><span id="stat-comments">0\u6761</span></div>
2273
+ <div style="font-size: 12px;"><span style="color: var(--text-4);">\u70B9\u8D5E\u64CD\u4F5C\uFF1A</span><span id="stat-likes">0\u6B21</span></div>
2274
+ <div style="font-size: 12px;"><span style="color: var(--text-4);">\u9650\u6D41\u6B21\u6570\uFF1A</span><span id="stat-ratelimit" style="color: var(--warning);">0\u6B21</span></div>
2275
+ <div style="font-size: 12px;"><span style="color: var(--text-4);">\u8FD0\u884C\u65F6\u95F4\uFF1A</span><span id="stat-elapsed">00:00:00</span></div>
2276
+ </div>
2277
+ `;
2278
+ mainGrid.appendChild(taskCard);
2279
+ const logsCard = createEl("div", { className: "bento-cell" });
2280
+ logsCard.innerHTML = `
2281
+ <div class="bento-title">
2282
+ \u5B9E\u65F6\u65E5\u5FD7
2283
+ <button id="toggle-logs-btn" class="secondary" style="margin-left: auto; padding: 4px 10px; font-size: 11px;">\u5C55\u5F00</button>
2284
+ </div>
2285
+ <div id="logs-container" class="log-container" style="display: none; max-height: 300px;"></div>
2286
+
2287
+ <div style="margin-top: var(--gap);">
2288
+ <div class="btn-group">
2289
+ <button id="pause-btn" class="secondary" style="flex: 1;">\u6682\u505C</button>
2290
+ <button id="stop-btn" class="danger" style="flex: 1;">\u505C\u6B62</button>
2291
+ </div>
2292
+ </div>
2293
+ `;
2294
+ mainGrid.appendChild(logsCard);
2295
+ root.appendChild(mainGrid);
2296
+ const statCollected = root.querySelector("#stat-collected");
2297
+ const statSuccess = root.querySelector("#stat-success");
2298
+ const statFailed = root.querySelector("#stat-failed");
2299
+ const statRemaining = root.querySelector("#stat-remaining");
2300
+ const taskKeyword = root.querySelector("#task-keyword");
2301
+ const taskTarget = root.querySelector("#task-target");
2302
+ const taskAccount = root.querySelector("#task-account");
2303
+ const currentPhase = root.querySelector("#current-phase");
2304
+ const currentAction = root.querySelector("#current-action");
2305
+ const progressPercent = root.querySelector("#progress-percent");
2306
+ const progressBar = root.querySelector("#progress-bar");
2307
+ const statComments = root.querySelector("#stat-comments");
2308
+ const statLikes = root.querySelector("#stat-likes");
2309
+ const statRatelimit = root.querySelector("#stat-ratelimit");
2310
+ const statElapsed = root.querySelector("#stat-elapsed");
2311
+ const runIdText = root.querySelector("#run-id-text");
2312
+ const errorCountText = root.querySelector("#error-count-text");
2313
+ const recentErrorsEmpty = root.querySelector("#recent-errors-empty");
2314
+ const recentErrorsList = root.querySelector("#recent-errors-list");
2315
+ const logsContainer = root.querySelector("#logs-container");
2316
+ const toggleLogsBtn = root.querySelector("#toggle-logs-btn");
2317
+ const pauseBtn = root.querySelector("#pause-btn");
2318
+ const stopBtn = root.querySelector("#stop-btn");
2319
+ let logsExpanded = false;
2320
+ let paused = false;
2321
+ let startTime = Date.now();
2322
+ let elapsedTimer = null;
2323
+ let unsubscribeState = null;
2324
+ let unsubscribeCmd = null;
2325
+ let activeRunId = String(ctx2?.xhsCurrentRun?.runId || "").trim();
2326
+ let errorCountTotal = 0;
2327
+ const recentErrors = [];
2328
+ const maxLogs = 500;
2329
+ const maxRecentErrors = 8;
2330
+ function renderRunSummary() {
2331
+ runIdText.textContent = activeRunId || "-";
2332
+ errorCountText.textContent = String(errorCountTotal);
2333
+ recentErrorsList.innerHTML = "";
2334
+ if (recentErrors.length === 0) {
2335
+ recentErrorsEmpty.style.display = "block";
2336
+ recentErrorsList.style.display = "none";
2337
+ return;
2338
+ }
2339
+ recentErrorsEmpty.style.display = "none";
2340
+ recentErrorsList.style.display = "block";
2341
+ recentErrors.forEach((item) => {
2342
+ const li = document.createElement("li");
2343
+ li.textContent = `[${item.ts}] ${item.source}: ${item.message}`;
2344
+ recentErrorsList.appendChild(li);
2345
+ });
2346
+ }
2347
+ function pushRecentError(message, source = "runtime") {
2348
+ const msg = String(message || "").trim();
2349
+ if (!msg) return;
2350
+ errorCountTotal += 1;
2351
+ recentErrors.push({
2352
+ ts: (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false }),
2353
+ source: String(source || "runtime").trim() || "runtime",
2354
+ message: msg
2355
+ });
2356
+ while (recentErrors.length > maxRecentErrors) recentErrors.shift();
2357
+ renderRunSummary();
2358
+ }
2359
+ function updateElapsed() {
2360
+ const elapsed = Math.floor((Date.now() - startTime) / 1e3);
2361
+ const h = Math.floor(elapsed / 3600);
2362
+ const m = Math.floor(elapsed % 3600 / 60);
2363
+ const s = elapsed % 60;
2364
+ statElapsed.textContent = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
2365
+ }
2366
+ function addLog(line, type = "info") {
2367
+ const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false });
2368
+ const logLine = createEl("div", { className: "log-line" });
2369
+ logLine.innerHTML = `<span class="log-time">[${ts}]</span> <span class="log-${type}">${line}</span>`;
2370
+ logsContainer.appendChild(logLine);
2371
+ while (logsContainer.children.length > maxLogs) {
2372
+ logsContainer.removeChild(logsContainer.firstChild);
2373
+ }
2374
+ if (logsExpanded) {
2375
+ logsContainer.scrollTop = logsContainer.scrollHeight;
2376
+ }
2377
+ }
2378
+ function updateFromTaskState(state) {
2379
+ if (!state) return;
2380
+ const collected = state.collected || state.progress || 0;
2381
+ const target = state.target || 50;
2382
+ const success = state.success || collected;
2383
+ const failed = state.failed || state.errors || 0;
2384
+ const remaining = Math.max(0, target - collected);
2385
+ statCollected.textContent = String(collected);
2386
+ statSuccess.textContent = String(success);
2387
+ statFailed.textContent = String(failed);
2388
+ statRemaining.textContent = String(remaining);
2389
+ const percent = target > 0 ? Math.round(collected / target * 100) : 0;
2390
+ progressPercent.textContent = `${percent}%`;
2391
+ progressBar.style.width = `${percent}%`;
2392
+ if (state.phase) {
2393
+ currentPhase.textContent = state.phase;
2394
+ }
2395
+ if (state.action) {
2396
+ currentAction.textContent = state.action;
2397
+ }
2398
+ if (state.comments) {
2399
+ statComments.textContent = `${state.comments}\u6761`;
2400
+ }
2401
+ if (state.likes) {
2402
+ statLikes.textContent = `${state.likes}\u6B21`;
2403
+ }
2404
+ if (state.ratelimits) {
2405
+ statRatelimit.textContent = `${state.ratelimits}\u6B21`;
2406
+ }
2407
+ if (state.keyword) {
2408
+ taskKeyword.textContent = state.keyword;
2409
+ }
2410
+ if (state.target) {
2411
+ taskTarget.textContent = String(state.target);
2412
+ }
2413
+ if (state.profileId) {
2414
+ const aliases = ctx2.api?.settings?.profileAliases || {};
2415
+ taskAccount.textContent = aliases[state.profileId] || state.profileId;
2416
+ }
2417
+ if (state.runId) {
2418
+ activeRunId = String(state.runId);
2419
+ renderRunSummary();
2420
+ }
2421
+ if (state.error) {
2422
+ pushRecentError(String(state.error), "state");
2423
+ }
2424
+ }
2425
+ function pickTaskFromList(tasks) {
2426
+ const target = activeRunId;
2427
+ if (target) {
2428
+ const matched = tasks.find((item) => String(item?.runId || "").trim() === target);
2429
+ if (matched) return matched;
2430
+ }
2431
+ const running = tasks.find((item) => ["running", "queued", "pending", "starting"].includes(String(item?.status || "").toLowerCase()));
2432
+ return running || tasks[0] || null;
2433
+ }
2434
+ function updateFromEventPayload(payload) {
2435
+ const event = String(payload?.event || "").trim();
2436
+ if (!event) return;
2437
+ if (event === "xhs.unified.start") {
2438
+ currentPhase.textContent = "\u8FD0\u884C\u4E2D";
2439
+ currentAction.textContent = "\u542F\u52A8 autoscript";
2440
+ if (payload.runId) {
2441
+ activeRunId = String(payload.runId || "").trim() || activeRunId;
2442
+ }
2443
+ if (payload.keyword) taskKeyword.textContent = String(payload.keyword);
2444
+ if (payload.maxNotes) taskTarget.textContent = String(payload.maxNotes);
2445
+ renderRunSummary();
2446
+ return;
2447
+ }
2448
+ if (event === "autoscript:operation_done") {
2449
+ const opId = String(payload.operationId || "").trim();
2450
+ currentAction.textContent = opId || currentAction.textContent;
2451
+ const result = payload.result && typeof payload.result === "object" ? payload.result : {};
2452
+ if (opId === "comments_harvest") {
2453
+ const nowComments = Number((statComments.textContent || "0").replace(/[^\d]/g, "")) || 0;
2454
+ const added = Number(result.collected || 0);
2455
+ statComments.textContent = `${Math.max(0, nowComments + added)}\u6761`;
2456
+ }
2457
+ if (opId === "comment_like") {
2458
+ const nowLikes = Number((statLikes.textContent || "0").replace(/[^\d]/g, "")) || 0;
2459
+ const added = Number(result.likedCount || 0);
2460
+ statLikes.textContent = `${Math.max(0, nowLikes + added)}\u6B21`;
2461
+ }
2462
+ return;
2463
+ }
2464
+ if (event === "autoscript:operation_error" || event === "autoscript:operation_recovery_failed" || event === "xhs.unified.profile_failed") {
2465
+ const failed = Number(statFailed.textContent || "0") || 0;
2466
+ statFailed.textContent = String(failed + 1);
2467
+ const opId = String(payload?.operationId || "").trim();
2468
+ const err = String(payload?.error || payload?.message || payload?.code || event).trim();
2469
+ pushRecentError(opId ? `${opId}: ${err}` : err, event);
2470
+ return;
2471
+ }
2472
+ if (event === "xhs.unified.merged") {
2473
+ currentPhase.textContent = "\u5DF2\u5B8C\u6210";
2474
+ currentAction.textContent = "\u7ED3\u679C\u5408\u5E76\u5B8C\u6210";
2475
+ if (payload.profilesFailed) {
2476
+ statFailed.textContent = String(Number(payload.profilesFailed) || 0);
2477
+ }
2478
+ return;
2479
+ }
2480
+ if (event === "xhs.unified.stop") {
2481
+ const reason = String(payload.reason || "").trim();
2482
+ currentPhase.textContent = reason && reason !== "script_failure" ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
2483
+ currentAction.textContent = reason || "stop";
2484
+ if (reason && reason !== "completed") {
2485
+ pushRecentError(`stop reason=${reason}`, event);
2486
+ }
2487
+ renderRunSummary();
2488
+ }
2489
+ }
2490
+ function parseLineEvent(line) {
2491
+ const text = String(line || "").trim();
2492
+ if (!text.startsWith("{") || !text.endsWith("}")) return;
2493
+ try {
2494
+ const payload = JSON.parse(text);
2495
+ updateFromEventPayload(payload);
2496
+ } catch {
2497
+ }
2498
+ }
2499
+ function subscribeToUpdates() {
2500
+ if (typeof ctx2.api?.onStateUpdate === "function") {
2501
+ unsubscribeState = ctx2.api.onStateUpdate((update) => {
2502
+ if (paused) return;
2503
+ const runId = String(update?.runId || "").trim();
2504
+ if (activeRunId && runId && runId !== activeRunId) return;
2505
+ if (!activeRunId && runId) {
2506
+ activeRunId = runId;
2507
+ renderRunSummary();
2508
+ }
2509
+ if (update?.data && typeof update.data === "object") {
2510
+ const payload = { ...update.data || {}, runId };
2511
+ updateFromTaskState(payload);
2512
+ if (payload.action) addLog(String(payload.action), "info");
2513
+ if (payload.error) {
2514
+ addLog(String(payload.error), "error");
2515
+ pushRecentError(String(payload.error), "state");
2516
+ }
2517
+ }
2518
+ });
2519
+ }
2520
+ if (typeof ctx2.api?.onCmdEvent === "function") {
2521
+ unsubscribeCmd = ctx2.api.onCmdEvent((evt) => {
2522
+ if (paused) return;
2523
+ const runId = String(evt?.runId || "").trim();
2524
+ if (!activeRunId && evt?.type === "started" && String(evt?.title || "").includes("xhs unified")) {
2525
+ activeRunId = runId;
2526
+ renderRunSummary();
2527
+ }
2528
+ if (activeRunId && runId && runId !== activeRunId) return;
2529
+ if (evt.type === "stdout") {
2530
+ addLog(evt.line, "info");
2531
+ parseLineEvent(String(evt.line || "").trim());
2532
+ } else if (evt.type === "stderr") {
2533
+ addLog(evt.line, "error");
2534
+ pushRecentError(String(evt.line || ""), "stderr");
2535
+ const failed = Number(statFailed.textContent || "0") || 0;
2536
+ statFailed.textContent = String(failed + 1);
2537
+ } else if (evt.type === "exit") {
2538
+ currentPhase.textContent = Number(evt.exitCode || 0) === 0 ? "\u5DF2\u7ED3\u675F" : "\u5931\u8D25";
2539
+ currentAction.textContent = `exit(${evt.exitCode ?? "null"})`;
2540
+ addLog(`\u8FDB\u7A0B\u9000\u51FA: code=${evt.exitCode}`, evt.exitCode === 0 ? "success" : "error");
2541
+ if (Number(evt.exitCode || 0) !== 0) {
2542
+ pushRecentError(`\u8FDB\u7A0B\u9000\u51FA code=${evt.exitCode ?? "null"}`, "exit");
2543
+ }
2544
+ renderRunSummary();
2545
+ }
2546
+ });
2547
+ }
2548
+ }
2549
+ async function loadTaskInfo() {
2550
+ try {
2551
+ const config = await ctx2.api.configLoadLast();
2552
+ if (config) {
2553
+ taskKeyword.textContent = config.keyword || "-";
2554
+ taskTarget.textContent = String(config.target || 50);
2555
+ if (config.lastProfileId) {
2556
+ const aliases = ctx2.api?.settings?.profileAliases || {};
2557
+ taskAccount.textContent = aliases[config.lastProfileId] || config.lastProfileId;
2558
+ }
2559
+ }
2560
+ } catch (err) {
2561
+ console.error("Failed to load task info:", err);
2562
+ }
2563
+ }
2564
+ async function fetchCurrentState() {
2565
+ try {
2566
+ const tasks = await ctx2.api.stateGetTasks();
2567
+ if (Array.isArray(tasks) && tasks.length > 0) {
2568
+ const picked = pickTaskFromList(tasks);
2569
+ if (picked) {
2570
+ const runId = String(picked?.runId || "").trim();
2571
+ if (runId) {
2572
+ activeRunId = runId;
2573
+ renderRunSummary();
2574
+ }
2575
+ updateFromTaskState(picked);
2576
+ }
2577
+ }
2578
+ } catch (err) {
2579
+ console.error("Failed to fetch state:", err);
2580
+ }
2581
+ }
2582
+ toggleLogsBtn.onclick = () => {
2583
+ logsExpanded = !logsExpanded;
2584
+ logsContainer.style.display = logsExpanded ? "block" : "none";
2585
+ toggleLogsBtn.textContent = logsExpanded ? "\u6536\u8D77" : "\u5C55\u5F00";
2586
+ };
2587
+ pauseBtn.onclick = () => {
2588
+ paused = !paused;
2589
+ pauseBtn.textContent = paused ? "\u7EE7\u7EED" : "\u6682\u505C";
2590
+ if (paused) {
2591
+ addLog("\u4EFB\u52A1\u5DF2\u6682\u505C", "warn");
2592
+ } else {
2593
+ addLog("\u4EFB\u52A1\u7EE7\u7EED\u6267\u884C", "info");
2594
+ }
2595
+ };
2596
+ stopBtn.onclick = async () => {
2597
+ if (confirm("\u786E\u5B9A\u8981\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u5417\uFF1F")) {
2598
+ try {
2599
+ const tasks = await ctx2.api.stateGetTasks();
2600
+ if (Array.isArray(tasks) && tasks.length > 0 && tasks[0].runId) {
2601
+ await ctx2.api.cmdKill(tasks[0].runId);
2602
+ addLog("\u4EFB\u52A1\u5DF2\u505C\u6B62", "warn");
2603
+ }
2604
+ } catch (err) {
2605
+ console.error("Failed to stop task:", err);
2606
+ }
2607
+ setTimeout(() => {
2608
+ if (typeof ctx2.setActiveTab === "function") {
2609
+ ctx2.setActiveTab("config");
2610
+ }
2611
+ }, 1500);
2612
+ }
2613
+ };
2614
+ renderRunSummary();
2615
+ loadTaskInfo();
2616
+ subscribeToUpdates();
2617
+ fetchCurrentState();
2618
+ elapsedTimer = setInterval(updateElapsed, 1e3);
2619
+ return () => {
2620
+ if (elapsedTimer) clearInterval(elapsedTimer);
2621
+ if (unsubscribeState) unsubscribeState();
2622
+ if (unsubscribeCmd) unsubscribeCmd();
2623
+ };
2624
+ }
2625
+
2626
+ // src/renderer/tabs-new/account-manager.mts
2627
+ function renderAccountManager(root, ctx2) {
2628
+ root.innerHTML = "";
2629
+ const bentoGrid = createEl("div", { className: "bento-grid bento-sidebar" });
2630
+ const envCard = createEl("div", { className: "bento-cell" });
2631
+ envCard.innerHTML = `
2632
+ <div class="bento-title"><span style="color: var(--success);">\u25CF</span> \u73AF\u5883\u72B6\u6001</div>
2633
+ <div class="env-status-grid">
2634
+ <div class="env-item" id="env-camo">
2635
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
2636
+ <span>Camoufox CLI</span>
2637
+ </div>
2638
+ <div class="env-item" id="env-unified">
2639
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
2640
+ <span>Unified API</span>
2641
+ </div>
2642
+ <div class="env-item" id="env-browser">
2643
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
2644
+ <span>Browser Service</span>
2645
+ </div>
2646
+ <div class="env-item" id="env-firefox">
2647
+ <span class="icon" style="color: var(--text-4);">\u25CB</span>
2648
+ <span>Firefox</span>
2649
+ </div>
2650
+ </div>
2651
+ <div style="margin-top: var(--gap);">
2652
+ <button id="recheck-env-btn" class="secondary" style="width: 100%;">\u91CD\u65B0\u68C0\u67E5</button>
2653
+ </div>
2654
+ `;
2655
+ bentoGrid.appendChild(envCard);
2656
+ const accountCard = createEl("div", { className: "bento-cell" });
2657
+ accountCard.innerHTML = `
2658
+ <div class="bento-title">
2659
+ \u8D26\u6237\u5217\u8868
2660
+ <button id="add-account-btn" style="margin-left: auto; padding: 6px 12px; font-size: 12px;">\u6DFB\u52A0\u8D26\u6237</button>
2661
+ </div>
2662
+ <div id="account-list" class="account-list" style="margin-bottom: var(--gap); max-height: 300px; overflow: auto;"></div>
2663
+ <div style="margin-top: var(--gap);">
2664
+ <div class="btn-group">
2665
+ <button id="check-all-btn" class="secondary" style="flex: 1;">\u68C0\u67E5\u6240\u6709</button>
2666
+ <button id="refresh-expired-btn" class="secondary" style="flex: 1;">\u5237\u65B0\u5931\u6548</button>
2667
+ </div>
2668
+ </div>
2669
+ `;
2670
+ bentoGrid.appendChild(accountCard);
2671
+ root.appendChild(bentoGrid);
2672
+ const recheckEnvBtn = root.querySelector("#recheck-env-btn");
2673
+ const addAccountBtn = root.querySelector("#add-account-btn");
2674
+ const checkAllBtn = root.querySelector("#check-all-btn");
2675
+ const refreshExpiredBtn = root.querySelector("#refresh-expired-btn");
2676
+ const accountListEl = root.querySelector("#account-list");
2677
+ let accounts = [];
2678
+ async function checkEnvironment() {
2679
+ try {
2680
+ const [camo, services, firefox] = await Promise.all([
2681
+ ctx2.api.envCheckCamo(),
2682
+ ctx2.api.envCheckServices(),
2683
+ ctx2.api.envCheckFirefox()
2684
+ ]);
2685
+ updateEnvItem("env-camo", camo.installed);
2686
+ updateEnvItem("env-unified", services.unifiedApi);
2687
+ updateEnvItem("env-browser", services.browserService);
2688
+ updateEnvItem("env-firefox", firefox.installed);
2689
+ } catch (err) {
2690
+ console.error("Environment check failed:", err);
2691
+ }
2692
+ }
2693
+ function updateEnvItem(id, ok) {
2694
+ const el = root.querySelector(`#${id}`);
2695
+ if (!el) return;
2696
+ const icon = el.querySelector(".icon");
2697
+ icon.textContent = ok ? "\u2713" : "\u2717";
2698
+ icon.style.color = ok ? "var(--success)" : "var(--danger)";
2699
+ }
2700
+ async function loadAccounts() {
2701
+ try {
2702
+ const rows = await listAccountProfiles(ctx2.api);
2703
+ accounts = rows.map((row) => ({
2704
+ ...row,
2705
+ statusView: row.valid ? "valid" : row.status === "pending" ? "pending" : "expired"
2706
+ }));
2707
+ renderAccountList();
2708
+ } catch (err) {
2709
+ console.error("Failed to load accounts:", err);
2710
+ }
2711
+ }
2712
+ function renderAccountList() {
2713
+ accountListEl.innerHTML = "";
2714
+ if (accounts.length === 0) {
2715
+ accountListEl.innerHTML = '<div style="padding: 12px; text-align: center; color: var(--text-4);">\u6682\u65E0\u8D26\u6237</div>';
2716
+ return;
2717
+ }
2718
+ accounts.forEach((acc) => {
2719
+ const row = createEl("div", {
2720
+ className: "account-item",
2721
+ style: "display: grid; grid-template-columns: 1fr 120px 100px 100px; gap: var(--gap-sm); padding: var(--gap-sm); align-items: center; border-bottom: 1px solid var(--border);"
2722
+ });
2723
+ const nameDiv = createEl("div", {}, [
2724
+ createEl("div", { className: "account-name" }, [acc.alias || acc.name || acc.profileId]),
2725
+ createEl("div", { className: "account-alias" }, [acc.profileId])
2726
+ ]);
2727
+ const statusBadge = createEl("span", {
2728
+ className: `status-badge ${acc.statusView === "valid" ? "status-valid" : acc.statusView === "expired" ? "status-expired" : "status-pending"}`
2729
+ }, [
2730
+ acc.statusView === "valid" ? "\u2713 \u6709\u6548" : acc.statusView === "expired" ? "\u2717 \u5931\u6548" : acc.statusView === "checking" ? "\u23F3 \u68C0\u67E5\u4E2D" : "\u23F3 \u5F85\u68C0\u67E5"
2731
+ ]);
2732
+ const checkBtn = createEl("button", {
2733
+ className: "secondary",
2734
+ style: "padding: 6px 10px; font-size: 11px;"
2735
+ }, ["\u68C0\u67E5"]);
2736
+ const actionsDiv = createEl("div", { className: "btn-group", style: "flex: 0;" });
2737
+ const detailBtn = createEl("button", {
2738
+ className: "secondary",
2739
+ style: "padding: 6px 8px; font-size: 10px;"
2740
+ }, ["\u8BE6\u60C5"]);
2741
+ const deleteBtn = createEl("button", {
2742
+ className: "danger",
2743
+ style: "padding: 6px 8px; font-size: 10px;"
2744
+ }, ["\u5220\u9664"]);
2745
+ actionsDiv.appendChild(detailBtn);
2746
+ actionsDiv.appendChild(deleteBtn);
2747
+ row.appendChild(nameDiv);
2748
+ row.appendChild(statusBadge);
2749
+ row.appendChild(checkBtn);
2750
+ row.appendChild(actionsDiv);
2751
+ checkBtn.onclick = () => checkAccountStatus(acc.profileId);
2752
+ detailBtn.onclick = () => {
2753
+ alert(`\u8D26\u6237\u8BE6\u60C5:
2754
+
2755
+ Profile ID: ${acc.profileId}
2756
+ \u8D26\u53F7ID: ${acc.accountId || "\u672A\u8BC6\u522B"}
2757
+ \u522B\u540D: ${acc.alias || "\u672A\u8BBE\u7F6E"}
2758
+ \u72B6\u6001: ${acc.status}
2759
+ \u539F\u56E0: ${acc.reason || "-"}
2760
+ \u6700\u540E\u68C0\u67E5: ${acc.lastCheck || "\u672A\u68C0\u67E5"}`);
2761
+ };
2762
+ deleteBtn.onclick = async () => {
2763
+ if (confirm(`\u786E\u5B9A\u5220\u9664\u8D26\u6237 "${acc.alias || acc.profileId}" \u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002`)) {
2764
+ try {
2765
+ if (acc.accountRecordId) {
2766
+ await ctx2.api.cmdRunJson({
2767
+ title: `account delete ${acc.accountRecordId}`,
2768
+ cwd: "",
2769
+ args: [
2770
+ ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
2771
+ "delete",
2772
+ acc.accountRecordId,
2773
+ "--delete-profile",
2774
+ "--delete-fingerprint",
2775
+ "--json"
2776
+ ]
2777
+ });
2778
+ } else {
2779
+ await ctx2.api.profileDelete({ profileId: acc.profileId, deleteFingerprint: true });
2780
+ }
2781
+ await loadAccounts();
2782
+ } catch (err) {
2783
+ alert("\u5220\u9664\u5931\u8D25: " + (err?.message || String(err)));
2784
+ }
2785
+ }
2786
+ };
2787
+ accountListEl.appendChild(row);
2788
+ });
2789
+ }
2790
+ async function checkAccountStatus(profileId) {
2791
+ const account = accounts.find((a) => a.profileId === profileId);
2792
+ if (!account) return;
2793
+ account.statusView = "checking";
2794
+ renderAccountList();
2795
+ try {
2796
+ const result = await ctx2.api.cmdRunJson({
2797
+ title: `account sync ${profileId}`,
2798
+ cwd: "",
2799
+ args: [
2800
+ ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
2801
+ "sync",
2802
+ profileId,
2803
+ "--json"
2804
+ ]
2805
+ });
2806
+ const profile = result?.json?.profile;
2807
+ if (profile && String(profile.profileId || "").trim() === profileId) {
2808
+ account.accountId = String(profile.accountId || "").trim() || null;
2809
+ account.alias = String(profile.alias || "").trim() || account.alias;
2810
+ account.status = String(profile.status || "").trim() || account.status;
2811
+ account.valid = profile.valid === true && Boolean(account.accountId);
2812
+ account.reason = String(profile.reason || "").trim() || null;
2813
+ }
2814
+ account.statusView = account.valid ? "valid" : "expired";
2815
+ account.lastCheck = (/* @__PURE__ */ new Date()).toLocaleString("zh-CN");
2816
+ } catch (err) {
2817
+ account.statusView = "expired";
2818
+ }
2819
+ renderAccountList();
2820
+ }
2821
+ async function addAccount() {
2822
+ const alias = prompt("\u8BF7\u8F93\u5165\u65B0\u8D26\u6237\u522B\u540D:");
2823
+ if (!alias?.trim()) return;
2824
+ try {
2825
+ const out = await ctx2.api.cmdRunJson({
2826
+ title: "profilepool add",
2827
+ cwd: "",
2828
+ args: [ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"), "add", "xiaohongshu", "--json"]
2829
+ });
2830
+ if (!out?.ok || !out?.json?.profileId) {
2831
+ alert("\u521B\u5EFA\u8D26\u53F7\u5931\u8D25: " + (out?.error || "\u672A\u77E5\u9519\u8BEF"));
2832
+ return;
2833
+ }
2834
+ const profileId = out.json.profileId;
2835
+ const aliases = { ...ctx2.api.settings?.profileAliases, [profileId]: alias.trim() };
2836
+ await ctx2.api.settingsSet({ profileAliases: aliases });
2837
+ if (typeof ctx2.refreshSettings === "function") {
2838
+ await ctx2.refreshSettings();
2839
+ }
2840
+ const timeoutSec = ctx2.api.settings?.timeouts?.loginTimeoutSec || 900;
2841
+ await ctx2.api.cmdSpawn({
2842
+ title: `\u767B\u5F55 ${alias}`,
2843
+ cwd: "",
2844
+ args: [
2845
+ ctx2.api.pathJoin("apps", "webauto", "entry", "profilepool.mjs"),
2846
+ "login-profile",
2847
+ profileId,
2848
+ "--timeout-sec",
2849
+ String(timeoutSec),
2850
+ "--keep-session"
2851
+ ],
2852
+ groupKey: "profilepool"
2853
+ });
2854
+ await loadAccounts();
2855
+ } catch (err) {
2856
+ alert("\u6DFB\u52A0\u8D26\u53F7\u5931\u8D25: " + (err?.message || String(err)));
2857
+ }
2858
+ }
2859
+ async function checkAllAccounts() {
2860
+ checkAllBtn.disabled = true;
2861
+ checkAllBtn.textContent = "\u68C0\u67E5\u4E2D...";
2862
+ for (const acc of accounts) {
2863
+ await checkAccountStatus(acc.profileId);
2864
+ }
2865
+ checkAllBtn.disabled = false;
2866
+ checkAllBtn.textContent = "\u68C0\u67E5\u6240\u6709";
2867
+ }
2868
+ async function refreshExpiredAccounts() {
2869
+ const expired = accounts.filter((a) => !a.valid);
2870
+ if (expired.length === 0) {
2871
+ alert("\u6CA1\u6709\u5931\u6548\u7684\u8D26\u6237\u9700\u8981\u5237\u65B0");
2872
+ return;
2873
+ }
2874
+ refreshExpiredBtn.disabled = true;
2875
+ refreshExpiredBtn.textContent = "\u5237\u65B0\u4E2D...";
2876
+ for (const acc of expired) {
2877
+ try {
2878
+ const accountKey = acc.accountRecordId || acc.profileId;
2879
+ await ctx2.api.cmdSpawn({
2880
+ title: `\u91CD\u65B0\u767B\u5F55 ${acc.alias || acc.profileId}`,
2881
+ cwd: "",
2882
+ args: [
2883
+ ctx2.api.pathJoin("apps", "webauto", "entry", "account.mjs"),
2884
+ "login",
2885
+ accountKey,
2886
+ "--url",
2887
+ "https://www.xiaohongshu.com",
2888
+ "--json"
2889
+ ],
2890
+ groupKey: "profilepool"
2891
+ });
2892
+ } catch (err) {
2893
+ console.error(`Failed to refresh ${acc.profileId}:`, err);
2894
+ }
2895
+ }
2896
+ refreshExpiredBtn.disabled = false;
2897
+ refreshExpiredBtn.textContent = "\u5237\u65B0\u5931\u6548";
2898
+ }
2899
+ recheckEnvBtn.onclick = checkEnvironment;
2900
+ addAccountBtn.onclick = addAccount;
2901
+ checkAllBtn.onclick = checkAllAccounts;
2902
+ refreshExpiredBtn.onclick = refreshExpiredAccounts;
2903
+ void checkEnvironment();
2904
+ void loadAccounts();
2905
+ }
2906
+
2907
+ // src/renderer/index.mts
2908
+ var tabs = [
2909
+ { id: "setup-wizard", label: "\u521D\u59CB\u5316", render: renderSetupWizard },
2910
+ { id: "config", label: "\u914D\u7F6E", render: renderConfigPanel },
2911
+ { id: "dashboard", label: "\u770B\u677F", render: renderDashboard },
2912
+ { id: "account-manager", label: "\u8D26\u6237\u7BA1\u7406", render: renderAccountManager },
2913
+ { id: "preflight", label: "\u65E7\u9884\u5904\u7406", render: renderPreflight, hidden: true },
2914
+ { id: "logs", label: "\u65E5\u5FD7", render: renderLogs },
2915
+ { id: "settings", label: "\u8BBE\u7F6E", render: renderSettings }
2916
+ ];
2917
+ var tabsEl = document.getElementById("tabs");
2918
+ var contentEl = document.getElementById("content");
2919
+ var statusEl = document.getElementById("status");
2920
+ var activeTabCleanup = null;
2921
+ var ctx = {
2922
+ api: window.api,
2923
+ settings: null,
2924
+ xhsCurrentRun: null,
2925
+ activeRunId: null,
2926
+ _activeRunIds: /* @__PURE__ */ new Set(),
2927
+ _activeRunsListeners: [],
2928
+ _logLines: [],
2929
+ appendLog(line) {
2930
+ const l = String(line || "").trim();
2931
+ if (!l) return;
2932
+ this._logLines.push(l);
2933
+ },
2934
+ clearLog() {
2935
+ this._logLines = [];
2936
+ },
2937
+ onActiveRunsChanged(listener) {
2938
+ this._activeRunsListeners.push(listener);
2939
+ return () => {
2940
+ this._activeRunsListeners = this._activeRunsListeners.filter((x) => x !== listener);
2941
+ };
2942
+ },
2943
+ notifyActiveRunsChanged() {
2944
+ this._activeRunsListeners.forEach((listener) => {
2945
+ try {
2946
+ listener();
2947
+ } catch {
2948
+ }
2949
+ });
2950
+ },
2951
+ setStatus(s) {
2952
+ statusEl.textContent = s;
2953
+ },
2954
+ async refreshSettings() {
2955
+ const latest = await window.api.settingsGet();
2956
+ this.settings = latest;
2957
+ if (this.api && typeof this.api === "object") {
2958
+ this.api.settings = latest;
2959
+ }
2960
+ return latest;
2961
+ },
2962
+ setActiveTab(id) {
2963
+ setActiveTab(id);
2964
+ }
2965
+ };
2966
+ function startDesktopHeartbeat() {
2967
+ const sendHeartbeat = async () => {
2968
+ try {
2969
+ if (typeof window.api?.desktopHeartbeat === "function") {
2970
+ await window.api.desktopHeartbeat();
2971
+ }
2972
+ } catch {
2973
+ }
2974
+ };
2975
+ void sendHeartbeat();
2976
+ const timer = setInterval(() => {
2977
+ void sendHeartbeat();
2978
+ }, 1e4);
2979
+ window.addEventListener("beforeunload", () => clearInterval(timer));
2980
+ }
2981
+ async function loadSettings() {
2982
+ await ctx.refreshSettings();
2983
+ }
2984
+ function setActiveTab(id) {
2985
+ if (activeTabCleanup) {
2986
+ try {
2987
+ activeTabCleanup();
2988
+ } catch {
2989
+ }
2990
+ activeTabCleanup = null;
2991
+ }
2992
+ tabsEl.textContent = "";
2993
+ for (const t of tabs.filter((x) => !x.hidden)) {
2994
+ const el = createEl("div", { className: `tab ${t.id === id ? "active" : ""}` }, [t.label]);
2995
+ el.addEventListener("click", () => setActiveTab(t.id));
2996
+ tabsEl.appendChild(el);
2997
+ }
2998
+ contentEl.textContent = "";
2999
+ const tab = tabs.find((x) => x.id === id);
3000
+ const dispose = tab.render(contentEl, ctx);
3001
+ if (typeof dispose === "function") activeTabCleanup = dispose;
3002
+ }
3003
+ function installCmdEvents() {
3004
+ window.api.onCmdEvent((evt) => {
3005
+ const runTag = evt?.runId ? `[rid:${evt.runId}] ` : "";
3006
+ const runId = String(evt?.runId || "").trim();
3007
+ if (evt?.type === "started") {
3008
+ ctx.activeRunId = evt.runId;
3009
+ if (runId) {
3010
+ ctx._activeRunIds.add(runId);
3011
+ ctx.notifyActiveRunsChanged();
3012
+ }
3013
+ ctx.appendLog(`${runTag}[started] ${evt.title} pid=${evt.pid} runId=${evt.runId}`);
3014
+ ctx.setStatus(`running: ${evt.title}`);
3015
+ return;
3016
+ }
3017
+ if (evt?.type === "stdout") {
3018
+ ctx.appendLog(`${runTag}${evt.line}`);
3019
+ return;
3020
+ }
3021
+ if (evt?.type === "stderr") {
3022
+ ctx.appendLog(`${runTag}[stderr] ${evt.line}`);
3023
+ return;
3024
+ }
3025
+ if (evt?.type === "exit") {
3026
+ if (runId) {
3027
+ ctx._activeRunIds.delete(runId);
3028
+ ctx.notifyActiveRunsChanged();
3029
+ }
3030
+ ctx.appendLog(`${runTag}[exit] code=${evt.exitCode ?? "null"} signal=${evt.signal ?? "null"}`);
3031
+ ctx.setStatus(ctx._activeRunIds.size > 0 ? `running: ${ctx._activeRunIds.size} tasks` : "idle");
3032
+ }
3033
+ });
3034
+ }
3035
+ async function detectStartupTab() {
3036
+ try {
3037
+ const env = typeof window.api?.envCheckAll === "function" ? await window.api.envCheckAll() : null;
3038
+ const rows = await listAccountProfiles(window.api).catch(() => []);
3039
+ const hasAccount = rows.some((row) => row.valid);
3040
+ const envReady = Boolean(env?.allReady);
3041
+ if (envReady && hasAccount) return "config";
3042
+ } catch {
3043
+ }
3044
+ return "setup-wizard";
3045
+ }
3046
+ async function main() {
3047
+ startDesktopHeartbeat();
3048
+ await loadSettings();
3049
+ installCmdEvents();
3050
+ const startupTab = await detectStartupTab();
3051
+ setActiveTab(startupTab);
3052
+ ctx.setStatus("idle");
3053
+ }
3054
+ window.addEventListener("beforeunload", () => {
3055
+ if (!activeTabCleanup) return;
3056
+ try {
3057
+ activeTabCleanup();
3058
+ } catch {
3059
+ }
3060
+ activeTabCleanup = null;
3061
+ });
3062
+ void main();
3063
+ //# sourceMappingURL=index.js.map