@web-auto/webauto 0.1.1 → 0.1.3

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 +263 -15
  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 +38 -10
  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,1476 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { promises as fsPromises } from 'node:fs';
4
+ import os from 'node:os';
5
+ import { spawn } from 'node:child_process';
6
+ import WebSocket from 'ws';
7
+ import { logDebug } from '../../../modules/logging/src/index.js';
8
+ import { normalizePayload } from './payload-normalizer.js';
9
+ function normalizeInputMode(mode) {
10
+ const raw = String(mode || '').trim().toLowerCase();
11
+ return raw === 'protocol' ? 'protocol' : 'system';
12
+ }
13
+ export class UiController {
14
+ repoRoot;
15
+ messageBus;
16
+ userContainerRoot;
17
+ containerIndexPath;
18
+ cliTargets;
19
+ defaultWsHost;
20
+ defaultWsPort;
21
+ defaultHttpHost;
22
+ defaultHttpPort;
23
+ defaultHttpProtocol;
24
+ _containerIndexCache;
25
+ inputMode;
26
+ snapshotCache = new Map();
27
+ constructor(options = {}) {
28
+ this.repoRoot = options.repoRoot || process.cwd();
29
+ this.messageBus = options.messageBus;
30
+ this.userContainerRoot = options.userContainerRoot || path.join(os.homedir(), '.webauto', 'container-lib');
31
+ this.containerIndexPath = options.containerIndexPath || path.join(this.repoRoot, 'apps/webauto/resources/container-library.index.json');
32
+ this.cliTargets = options.cliTargets || {};
33
+ this.defaultWsHost = options.defaultWsHost || '127.0.0.1';
34
+ this.defaultWsPort = Number(options.defaultWsPort || 8765);
35
+ this.defaultHttpHost = options.defaultHttpHost || '127.0.0.1';
36
+ this.defaultHttpPort = Number(options.defaultHttpPort || 7704);
37
+ this.defaultHttpProtocol = options.defaultHttpProtocol || 'http';
38
+ this._containerIndexCache = null;
39
+ this.inputMode = normalizeInputMode(process.env.WEBAUTO_INPUT_MODE);
40
+ this.cliTargets = options.cliTargets || {};
41
+ logDebug('controller', 'init', {
42
+ wsHost: this.defaultWsHost,
43
+ wsPort: this.defaultWsPort,
44
+ httpHost: this.defaultHttpHost,
45
+ httpPort: this.defaultHttpPort
46
+ });
47
+ }
48
+ getSnapshotCacheKey(options, targetUrl, sessionId) {
49
+ const maxDepth = options.maxDepth ?? 2;
50
+ const maxChildren = options.maxChildren ?? 5;
51
+ const rootSelector = options.rootSelector || '';
52
+ const containerId = options.containerId || options.rootContainerId || '';
53
+ return [sessionId, targetUrl, String(maxDepth), String(maxChildren), rootSelector, containerId].join('::');
54
+ }
55
+ async runCliCommand(moduleName, args) {
56
+ const scriptPath = this.cliTargets[moduleName];
57
+ if (!scriptPath) {
58
+ throw new Error(`Unknown module: ${moduleName}`);
59
+ }
60
+ console.log(`[controller] runCliCommand ${moduleName} ${scriptPath} ${args.join(' ')}`);
61
+ return new Promise((resolve, reject) => {
62
+ // Check if scriptPath ends with .js, if so run with node directly
63
+ const isJs = scriptPath.endsWith('.js');
64
+ const cmd = isJs ? 'node' : 'npx';
65
+ const cmdArgs = isJs ? [scriptPath, ...args] : ['tsx', scriptPath, ...args];
66
+ const child = spawn(cmd, cmdArgs, {
67
+ cwd: this.repoRoot,
68
+ env: process.env,
69
+ windowsHide: true,
70
+ });
71
+ let stdout = '';
72
+ let stderr = '';
73
+ child.stdout.on('data', (data) => {
74
+ stdout += data.toString();
75
+ });
76
+ child.stderr.on('data', (data) => {
77
+ stderr += data.toString();
78
+ });
79
+ child.on('close', (code) => {
80
+ console.log(`[controller] cli exit ${moduleName} ${code}`);
81
+ if (code !== 0) {
82
+ console.error(`[controller] cli stderr ${moduleName}`, stderr);
83
+ reject(new Error(`Command failed with code ${code}: ${stderr}`));
84
+ return;
85
+ }
86
+ try {
87
+ console.log(`[controller] cli stdout ${moduleName}`, stdout.trim());
88
+ const result = JSON.parse(stdout.trim());
89
+ resolve(result);
90
+ }
91
+ catch (e) {
92
+ console.error(`[controller] cli parse error ${moduleName}`, e);
93
+ resolve({ success: true, raw: stdout.trim() });
94
+ }
95
+ });
96
+ child.on('error', (err) => {
97
+ reject(err);
98
+ });
99
+ });
100
+ }
101
+ async handleAction(action, payload = {}) {
102
+ logDebug('controller', 'handleAction', { action, payload });
103
+ switch (action) {
104
+ case 'browser:status':
105
+ return this.fetchBrowserStatus();
106
+ case 'session:list':
107
+ return this.runCliCommand('session-manager', ['list']);
108
+ case 'session:create':
109
+ return this.handleSessionCreate(payload);
110
+ case 'session:delete':
111
+ return this.handleSessionDelete(payload);
112
+ case 'logs:stream':
113
+ return this.handleLogsStream(payload);
114
+ case 'operations:list':
115
+ return this.runCliCommand('operations', ['list']);
116
+ case 'operations:run':
117
+ return this.handleOperationRun(payload);
118
+ case 'containers:inspect':
119
+ case 'containers:get':
120
+ return this.handleContainerInspect(payload);
121
+ case 'containers:inspect-container':
122
+ case 'containers:get-container':
123
+ return this.handleContainerInspectContainer(payload);
124
+ case 'containers:inspect-branch':
125
+ return this.handleContainerInspectBranch(payload);
126
+ case 'containers:remap':
127
+ return this.handleContainerRemap(payload);
128
+ case 'containers:create-child':
129
+ return this.handleContainerCreateChild(payload);
130
+ case 'containers:update-alias':
131
+ return this.handleContainerUpdateAlias(payload);
132
+ case 'containers:update-operations':
133
+ return this.handleContainerUpdateOperations(payload);
134
+ case 'containers:match':
135
+ case 'containers:status':
136
+ return this.handleContainerMatch(payload);
137
+ case 'browser:highlight':
138
+ return this.handleBrowserHighlight(payload);
139
+ case 'browser:clear-highlight':
140
+ return this.handleBrowserClearHighlight(payload);
141
+ case 'browser:highlight-dom-path':
142
+ return this.handleBrowserHighlightDomPath(payload);
143
+ case 'browser:execute':
144
+ return this.handleBrowserExecute(payload);
145
+ case 'browser:screenshot':
146
+ return this.handleBrowserScreenshot(payload);
147
+ case 'browser:fill':
148
+ return this.handleBrowserFill(payload);
149
+ case 'browser:page:list':
150
+ return this.handleBrowserPageList(payload);
151
+ case 'browser:page:new':
152
+ return this.handleBrowserPageNew(payload);
153
+ case 'browser:page:switch':
154
+ return this.handleBrowserPageSwitch(payload);
155
+ case 'browser:page:close':
156
+ return this.handleBrowserPageClose(payload);
157
+ case 'browser:goto':
158
+ return this.handleBrowserGoto(payload);
159
+ case 'browser:set-viewport':
160
+ return this.handleBrowserSetViewport(payload);
161
+ case 'browser:set-viewport':
162
+ return this.handleBrowserSetViewport(payload);
163
+ case 'browser:cancel-pick':
164
+ return this.handleBrowserCancelDomPick(payload);
165
+ case 'browser:pick-dom':
166
+ return this.handleBrowserPickDom(payload);
167
+ case 'keyboard:press':
168
+ return this.handleKeyboardPress(payload);
169
+ case 'keyboard:type':
170
+ return this.handleKeyboardType(payload);
171
+ case 'system:shortcut':
172
+ return this.handleSystemShortcut(payload);
173
+ case 'system:input-mode:set':
174
+ return this.handleSystemInputModeSet(payload);
175
+ case 'system:input-mode:get':
176
+ return this.handleSystemInputModeGet();
177
+ case 'mouse:wheel':
178
+ return this.handleMouseWheel(payload);
179
+ case 'dom:branch:2':
180
+ return this.handleDomBranch2(payload);
181
+ case 'dom:pick:2':
182
+ return this.handleDomPick2(payload);
183
+ case 'container:operation':
184
+ return this.handleContainerOperation(payload);
185
+ default:
186
+ return { success: false, error: `Unknown action: ${action}` };
187
+ }
188
+ }
189
+ async handleSessionCreate(payload = {}) {
190
+ const { profile, url, headless, keepOpen } = normalizePayload(payload, { required: ['profile'] });
191
+ logDebug('controller', 'session:create', { payload });
192
+ const args = ['create', '--profile', profile];
193
+ if (url)
194
+ args.push('--url', url);
195
+ if (headless !== undefined)
196
+ args.push('--headless', String(headless));
197
+ if (keepOpen !== undefined)
198
+ args.push('--keep-open', String(keepOpen));
199
+ return this.runCliCommand('session-manager', args);
200
+ }
201
+ async handleSessionDelete(payload = {}) {
202
+ const { profile } = normalizePayload(payload, { required: ['profile'] });
203
+ return this.runCliCommand('session-manager', ['delete', '--profile', profile]);
204
+ }
205
+ async handleLogsStream(payload = {}) {
206
+ const args = ['stream'];
207
+ if (payload.source)
208
+ args.push('--source', payload.source);
209
+ if (payload.session)
210
+ args.push('--session', payload.session);
211
+ if (payload.lines)
212
+ args.push('--lines', String(payload.lines));
213
+ return this.runCliCommand('logging', args);
214
+ }
215
+ async handleSystemShortcut(payload = {}) {
216
+ const shortcut = String(payload.shortcut || '').trim();
217
+ const app = String(payload.app || 'camoufox').trim();
218
+ if (!shortcut)
219
+ throw new Error('shortcut required');
220
+ if (process.platform === 'darwin') {
221
+ const { spawnSync } = await import('node:child_process');
222
+ spawnSync('osascript', ['-e', `tell application "${app}" to activate`]);
223
+ if (shortcut === 'new-tab') {
224
+ const res = spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "t" using command down']);
225
+ if (res.status != 0)
226
+ throw new Error('osascript new-tab failed');
227
+ return { success: true, data: { ok: true, shortcut } };
228
+ }
229
+ throw new Error(`unsupported shortcut: ${shortcut}`);
230
+ }
231
+ if (process.platform === 'win32') {
232
+ const { spawnSync } = await import('node:child_process');
233
+ if (shortcut === 'new-tab') {
234
+ const script = 'Add-Type -AssemblyName System.Windows.Forms; $ws = New-Object -ComObject WScript.Shell; $ws.SendKeys("^t");';
235
+ const res = spawnSync('powershell', ['-NoProfile', '-Command', script], { windowsHide: true });
236
+ if (res.status != 0)
237
+ throw new Error('powershell new-tab failed');
238
+ return { success: true, data: { ok: true, shortcut } };
239
+ }
240
+ throw new Error(`unsupported shortcut: ${shortcut}`);
241
+ }
242
+ throw new Error('unsupported platform');
243
+ }
244
+ async handleSystemInputModeSet(payload = {}) {
245
+ const mode = normalizeInputMode(payload.mode);
246
+ this.inputMode = mode;
247
+ process.env.WEBAUTO_INPUT_MODE = mode;
248
+ logDebug('controller', 'input-mode:set', { mode });
249
+ return { success: true, data: { mode } };
250
+ }
251
+ async handleSystemInputModeGet() {
252
+ return { success: true, data: { mode: this.inputMode } };
253
+ }
254
+ async handleOperationRun(payload = {}) {
255
+ const op = payload.op || payload.operation || payload.id;
256
+ if (!op)
257
+ throw new Error('缺少操作 ID');
258
+ const args = ['run', '--op', op];
259
+ if (payload.config) {
260
+ args.push('--config', JSON.stringify(payload.config));
261
+ }
262
+ return this.runCliCommand('operations', args);
263
+ }
264
+ async handleContainerInspect(payload = {}) {
265
+ const { profile, url, maxDepth, maxChildren, containerId, rootSelector } = normalizePayload(payload, { required: ['profile'] });
266
+ logDebug('controller', 'containers:inspect', { profile, payload });
267
+ const context = await this.captureInspectorSnapshot({
268
+ profile,
269
+ url,
270
+ maxDepth,
271
+ maxChildren,
272
+ containerId,
273
+ rootSelector,
274
+ });
275
+ const snapshot = context.snapshot;
276
+ return {
277
+ success: true,
278
+ data: {
279
+ sessionId: context.sessionId,
280
+ profileId: context.profileId,
281
+ url: context.targetUrl,
282
+ snapshot,
283
+ containerSnapshot: snapshot,
284
+ domTree: snapshot?.dom_tree || null,
285
+ },
286
+ };
287
+ }
288
+ async handleContainerInspectContainer(payload = {}) {
289
+ const { profile, containerId, url, maxDepth, maxChildren, rootSelector } = normalizePayload(payload, { required: ['profile', 'containerId'] });
290
+ const context = await this.captureInspectorSnapshot({
291
+ profile,
292
+ url,
293
+ maxDepth,
294
+ maxChildren,
295
+ containerId,
296
+ rootSelector,
297
+ });
298
+ return {
299
+ success: true,
300
+ data: {
301
+ sessionId: context.sessionId,
302
+ profileId: context.profileId,
303
+ url: context.targetUrl,
304
+ snapshot: context.snapshot,
305
+ },
306
+ };
307
+ }
308
+ async handleContainerInspectBranch(payload = {}) {
309
+ const { profile, path, url, maxDepth, maxChildren, rootSelector } = normalizePayload(payload, { required: ['profile', 'path'] });
310
+ logDebug('controller', 'containers:inspect:branch', { profile, path, payload });
311
+ const context = await this.captureInspectorBranch({
312
+ profile,
313
+ url,
314
+ path,
315
+ rootSelector,
316
+ maxDepth,
317
+ maxChildren,
318
+ });
319
+ return {
320
+ success: true,
321
+ data: {
322
+ sessionId: context.sessionId,
323
+ profileId: context.profileId,
324
+ url: context.targetUrl,
325
+ branch: context.branch,
326
+ },
327
+ };
328
+ }
329
+ async handleContainerRemap(payload = {}) {
330
+ const containerId = payload.containerId || payload.id;
331
+ const selector = (payload.selector || '').trim();
332
+ const definition = payload.definition || {};
333
+ if (!containerId) {
334
+ throw new Error('缺少容器 ID');
335
+ }
336
+ if (!selector) {
337
+ throw new Error('缺少新的 selector');
338
+ }
339
+ const siteKey = payload.siteKey ||
340
+ this.resolveSiteKeyFromUrl(payload.url) ||
341
+ this.inferSiteFromContainerId(containerId);
342
+ if (!siteKey) {
343
+ throw new Error('无法确定容器所属站点');
344
+ }
345
+ const normalizedDefinition = { ...definition, id: containerId };
346
+ const existingSelectors = Array.isArray(normalizedDefinition.selectors) ? normalizedDefinition.selectors : [];
347
+ const filtered = existingSelectors.filter((item) => (item?.css || '').trim() && (item.css || '').trim() !== selector);
348
+ normalizedDefinition.selectors = [{ css: selector, variant: 'primary', score: 1 }, ...filtered];
349
+ await this.writeUserContainerDefinition(siteKey, containerId, normalizedDefinition);
350
+ const { profile, url } = normalizePayload(payload, { required: ['profile'] });
351
+ return this.handleContainerInspect({ profile, url });
352
+ }
353
+ async handleContainerCreateChild(payload = {}) {
354
+ const parentId = payload.parentId || payload.parent_id;
355
+ const containerId = payload.containerId || payload.childId || payload.id;
356
+ if (!parentId) {
357
+ throw new Error('缺少父容器 ID');
358
+ }
359
+ if (!containerId) {
360
+ throw new Error('缺少子容器 ID');
361
+ }
362
+ const siteKey = payload.siteKey ||
363
+ this.resolveSiteKeyFromUrl(payload.url) ||
364
+ this.inferSiteFromContainerId(containerId) ||
365
+ this.inferSiteFromContainerId(parentId);
366
+ if (!siteKey) {
367
+ throw new Error('无法确定容器所属站点');
368
+ }
369
+ const selectorEntries = this.normalizeSelectors(payload.selectors || payload.selector || []) || [];
370
+ if (!selectorEntries.length) {
371
+ throw new Error('缺少 selector 定义');
372
+ }
373
+ const parentDefinition = (await this.readContainerDefinition(siteKey, parentId)) || { id: parentId, children: [] };
374
+ const normalizedChild = {
375
+ ...(payload.definition || {}),
376
+ id: containerId,
377
+ selectors: selectorEntries,
378
+ name: payload.definition?.name || payload.alias || containerId,
379
+ type: payload.definition?.type || 'section',
380
+ capabilities: Array.isArray(payload.definition?.capabilities) && payload.definition.capabilities.length
381
+ ? payload.definition.capabilities
382
+ : ['highlight', 'find-child', 'scroll'],
383
+ };
384
+ const alias = typeof payload.alias === 'string' ? payload.alias.trim() : '';
385
+ const metadata = { ...(normalizedChild.metadata || {}) };
386
+ if (alias) {
387
+ metadata.alias = alias;
388
+ normalizedChild.alias = alias;
389
+ normalizedChild.nickname = alias;
390
+ if (!normalizedChild.name) {
391
+ normalizedChild.name = alias;
392
+ }
393
+ }
394
+ else {
395
+ delete metadata.alias;
396
+ }
397
+ if (payload.domPath) {
398
+ metadata.source_dom_path = payload.domPath;
399
+ }
400
+ if (payload.domMeta && typeof payload.domMeta === 'object') {
401
+ metadata.source_dom_meta = payload.domMeta;
402
+ }
403
+ normalizedChild.metadata = metadata;
404
+ if (!normalizedChild.page_patterns || !normalizedChild.page_patterns.length) {
405
+ const parentPatterns = parentDefinition.page_patterns || parentDefinition.pagePatterns;
406
+ if (parentPatterns?.length) {
407
+ normalizedChild.page_patterns = parentPatterns;
408
+ }
409
+ }
410
+ const nextParent = { ...parentDefinition };
411
+ const childList = Array.isArray(nextParent.children) ? [...nextParent.children] : [];
412
+ if (!childList.includes(containerId)) {
413
+ childList.push(containerId);
414
+ }
415
+ nextParent.children = childList;
416
+ await this.writeUserContainerDefinition(siteKey, containerId, normalizedChild);
417
+ await this.writeUserContainerDefinition(siteKey, parentId, nextParent);
418
+ // 正确流程:写入定义后立即进行容器匹配,并通过 containers.matched 事件推送最新树
419
+ const { profile, url, maxDepth, maxChildren, rootSelector } = normalizePayload(payload, { required: ['profile'] });
420
+ return this.handleContainerMatch({
421
+ profile,
422
+ url,
423
+ maxDepth,
424
+ maxChildren,
425
+ rootSelector,
426
+ });
427
+ }
428
+ async handleContainerUpdateAlias(payload = {}) {
429
+ const containerId = payload.containerId || payload.id;
430
+ if (!containerId) {
431
+ throw new Error('缺少容器 ID');
432
+ }
433
+ const alias = typeof payload.alias === 'string' ? payload.alias.trim() : '';
434
+ const siteKey = payload.siteKey ||
435
+ this.resolveSiteKeyFromUrl(payload.url) ||
436
+ this.inferSiteFromContainerId(containerId);
437
+ if (!siteKey) {
438
+ throw new Error('无法确定容器所属站点');
439
+ }
440
+ const baseDefinition = (await this.readContainerDefinition(siteKey, containerId)) || { id: containerId };
441
+ const metadata = { ...baseDefinition.metadata || {} };
442
+ if (alias) {
443
+ metadata.alias = alias;
444
+ }
445
+ else {
446
+ delete metadata.alias;
447
+ }
448
+ const next = {
449
+ ...baseDefinition,
450
+ name: baseDefinition.name || alias || containerId,
451
+ metadata,
452
+ };
453
+ if (alias) {
454
+ next.alias = alias;
455
+ next.nickname = alias;
456
+ }
457
+ else {
458
+ delete next.alias;
459
+ delete next.nickname;
460
+ }
461
+ await this.writeUserContainerDefinition(siteKey, containerId, next);
462
+ const { profile, url } = normalizePayload(payload, { required: ['profile'] });
463
+ return this.handleContainerInspect({ profile, url });
464
+ }
465
+ async handleContainerUpdateOperations(payload = {}) {
466
+ const containerId = payload.containerId || payload.id;
467
+ if (!containerId) {
468
+ throw new Error('缺少容器 ID');
469
+ }
470
+ const siteKey = payload.siteKey ||
471
+ this.resolveSiteKeyFromUrl(payload.url) ||
472
+ this.inferSiteFromContainerId(containerId);
473
+ if (!siteKey) {
474
+ throw new Error('无法确定容器所属站点');
475
+ }
476
+ const operations = Array.isArray(payload.operations) ? payload.operations : [];
477
+ const baseDefinition = (await this.readContainerDefinition(siteKey, containerId)) || { id: containerId };
478
+ const next = {
479
+ ...baseDefinition,
480
+ operations,
481
+ };
482
+ await this.writeUserContainerDefinition(siteKey, containerId, next);
483
+ const { profile, url } = normalizePayload(payload, { required: ['profile'] });
484
+ return this.handleContainerInspect({ profile, url, containerId });
485
+ }
486
+ async handleContainerMatch(payload = {}) {
487
+ const { profile, url, maxDepth, maxChildren, rootSelector, cache, cacheTtlMs, invalidateCache } = normalizePayload(payload, { required: ['profile'] });
488
+ logDebug('controller', 'containers:match', { profile, url, payload });
489
+ try {
490
+ const context = await this.captureInspectorSnapshot({
491
+ profile,
492
+ url,
493
+ maxDepth: maxDepth || 2,
494
+ maxChildren: maxChildren || 5,
495
+ rootSelector,
496
+ cache,
497
+ cacheTtlMs,
498
+ invalidateCache,
499
+ });
500
+ const snapshot = context.snapshot;
501
+ const rootContainer = snapshot?.root_match?.container || snapshot?.container_tree?.container || snapshot?.container_tree?.containers?.[0];
502
+ const matchPayload = {
503
+ sessionId: context.sessionId,
504
+ profileId: context.profileId,
505
+ url: context.targetUrl,
506
+ matched: !!rootContainer,
507
+ container: rootContainer || null,
508
+ snapshot,
509
+ cache: context.cache || null,
510
+ };
511
+ this.messageBus?.publish?.('containers.matched', matchPayload);
512
+ if (matchPayload.container) {
513
+ this.emitContainerAppearEvents(snapshot.container_tree, matchPayload);
514
+ }
515
+ this.messageBus?.publish?.('handshake.status', {
516
+ status: matchPayload.matched ? 'ready' : 'pending',
517
+ profileId: matchPayload.profileId,
518
+ sessionId: matchPayload.sessionId,
519
+ url: matchPayload.url,
520
+ matched: matchPayload.matched,
521
+ containerId: matchPayload.container?.id || null,
522
+ source: 'containers:match',
523
+ ts: Date.now(),
524
+ });
525
+ logDebug('controller', 'containers.matched', {
526
+ profileId: matchPayload.profileId,
527
+ matched: matchPayload.matched,
528
+ containerId: matchPayload.container?.id,
529
+ childrenCount: matchPayload.container?.children?.length,
530
+ nodesCount: matchPayload.container?.match?.nodes?.length
531
+ });
532
+ return {
533
+ success: true,
534
+ data: matchPayload,
535
+ };
536
+ }
537
+ catch (err) {
538
+ throw new Error(`容器匹配失败: ${err?.message || String(err)}`);
539
+ }
540
+ }
541
+ async handleBrowserHighlight(payload = {}) {
542
+ const { profile, selector, style, duration, channel, sticky, maxMatches } = normalizePayload(payload, { required: ['profile', 'selector'] });
543
+ const highlightOpts = {
544
+ style,
545
+ duration,
546
+ channel,
547
+ sticky,
548
+ maxMatches,
549
+ };
550
+ try {
551
+ const result = await this.sendHighlightViaWs(profile, selector, highlightOpts);
552
+ this.messageBus?.publish?.('ui.highlight.result', {
553
+ success: true,
554
+ selector,
555
+ source: result?.source || 'unknown',
556
+ details: result?.details || null,
557
+ });
558
+ return { success: true, data: result };
559
+ }
560
+ catch (err) {
561
+ const errorMessage = err?.message || '高亮请求失败';
562
+ this.messageBus?.publish?.('ui.highlight.result', {
563
+ success: false,
564
+ selector,
565
+ error: errorMessage,
566
+ });
567
+ throw err || new Error(errorMessage);
568
+ }
569
+ }
570
+ async handleBrowserClearHighlight(payload = {}) {
571
+ const { profile, channel } = normalizePayload(payload, { required: ['profile'] });
572
+ try {
573
+ const result = await this.sendClearHighlightViaWs(profile, channel || null);
574
+ this.messageBus?.publish?.('ui.highlight.result', {
575
+ success: true,
576
+ selector: null,
577
+ details: result,
578
+ });
579
+ return { success: true, data: result };
580
+ }
581
+ catch (err) {
582
+ const message = err?.message || '清除高亮失败';
583
+ this.messageBus?.publish?.('ui.highlight.result', {
584
+ success: false,
585
+ selector: null,
586
+ error: message,
587
+ });
588
+ throw err;
589
+ }
590
+ }
591
+ async handleBrowserExecute(payload = {}) {
592
+ const { profile, script } = normalizePayload(payload, { required: ['profile', 'script'] });
593
+ try {
594
+ const result = await this.sendExecuteViaWs(profile, script);
595
+ return { success: true, data: result };
596
+ }
597
+ catch (err) {
598
+ const errorMessage = err?.message || '执行脚本失败';
599
+ throw new Error(errorMessage);
600
+ }
601
+ }
602
+ async handleBrowserScreenshot(payload = {}) {
603
+ const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
604
+ const fullPage = typeof payload.fullPage === 'boolean' ? payload.fullPage : Boolean(payload.fullPage);
605
+ // 截图在某些页面会更慢,放宽超时以保证调试证据可落盘
606
+ const result = await this.browserServiceCommand('screenshot', { profileId, fullPage }, { timeoutMs: 60000 });
607
+ return { success: true, data: result };
608
+ }
609
+ async handleBrowserFill(payload = {}) {
610
+ const { profile, selector, text } = normalizePayload(payload, { required: ['profile', 'selector', 'text'] });
611
+ logDebug('controller', 'browser:fill', { profile, selector, textLen: String(text || '').length });
612
+ // Use browser-service fill (Playwright page.fill) so it stays in system-level input pathway.
613
+ return this.browserServiceCommand('fill', { profileId: profile, selector, value: text }, { timeoutMs: 60000 });
614
+ }
615
+ async handleBrowserPageList(payload = {}) {
616
+ const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
617
+ const result = await this.browserServiceCommand('page:list', { profileId }, { timeoutMs: 30000 });
618
+ return { success: true, data: result };
619
+ }
620
+ async handleBrowserPageNew(payload = {}) {
621
+ const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
622
+ const url = payload.url ? String(payload.url) : undefined;
623
+ const result = await this.browserServiceCommand('page:new', { profileId, ...(url ? { url } : {}) }, { timeoutMs: 30000 });
624
+ const index = Number(result?.index ?? result?.data?.index);
625
+ if (Number.isFinite(index)) {
626
+ return { success: true, data: result };
627
+ }
628
+ const list = await this.browserServiceCommand('page:list', { profileId }, { timeoutMs: 30000 });
629
+ const activeIndexRaw = list?.activeIndex ?? list?.data?.activeIndex;
630
+ const activeIndex = Number(activeIndexRaw);
631
+ if (Number.isFinite(activeIndex)) {
632
+ return { success: true, data: { ...(result || {}), index: activeIndex, fallback: 'activeIndex' } };
633
+ }
634
+ return { success: true, data: result };
635
+ }
636
+ async handleBrowserPageSwitch(payload = {}) {
637
+ const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
638
+ const index = Number(payload.index);
639
+ if (!Number.isFinite(index))
640
+ throw new Error('index required');
641
+ const result = await this.browserServiceCommand('page:switch', { profileId, index }, { timeoutMs: 30000 });
642
+ return { success: true, data: result };
643
+ }
644
+ async handleBrowserPageClose(payload = {}) {
645
+ const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
646
+ const hasIndex = typeof payload.index !== 'undefined' && payload.index !== null;
647
+ const index = hasIndex ? Number(payload.index) : undefined;
648
+ const result = await this.browserServiceCommand('page:close', { profileId, ...(Number.isFinite(index) ? { index } : {}) }, { timeoutMs: 30000 });
649
+ return { success: true, data: result };
650
+ }
651
+ async fetchBrowserStatus() {
652
+ try {
653
+ const data = await this.browserServiceCommand('getStatus', {}, { timeoutMs: 10000 });
654
+ return { success: true, data };
655
+ }
656
+ catch (err) {
657
+ return { success: false, error: err?.message || String(err) };
658
+ }
659
+ }
660
+ getBrowserServiceHttpUrl() {
661
+ return `${this.defaultHttpProtocol}://${this.defaultHttpHost}:${this.defaultHttpPort}`;
662
+ }
663
+ async browserServiceCommand(action, args, options = {}) {
664
+ const timeoutMs = typeof options.timeoutMs === 'number' && options.timeoutMs > 0 ? options.timeoutMs : 20000;
665
+ const abortController1 = new AbortController();
666
+ const timeoutId1 = setTimeout(() => abortController1.abort(), timeoutMs);
667
+ const res = await fetch(`${this.getBrowserServiceHttpUrl()}/command`, {
668
+ method: 'POST',
669
+ headers: { 'Content-Type': 'application/json' },
670
+ body: JSON.stringify({ action, args }),
671
+ signal: abortController1.signal,
672
+ });
673
+ clearTimeout(timeoutId1);
674
+ const raw = await res.text();
675
+ let data = {};
676
+ try {
677
+ data = raw ? JSON.parse(raw) : {};
678
+ }
679
+ catch {
680
+ data = { raw };
681
+ }
682
+ if (!res.ok) {
683
+ throw new Error(data?.error || data?.body?.error || `browser-service command "${action}" HTTP ${res.status}`);
684
+ }
685
+ if (data && data.ok === false) {
686
+ throw new Error(data.error || `browser-service command "${action}" failed`);
687
+ }
688
+ if (data && data.error) {
689
+ throw new Error(data.error);
690
+ }
691
+ return data.body ?? data;
692
+ }
693
+ async handleBrowserGoto(payload = {}) {
694
+ const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
695
+ const url = (payload.url || '').toString();
696
+ if (!url)
697
+ throw new Error('url required');
698
+ const result = await this.browserServiceCommand('goto', { profileId, url });
699
+ return { success: true, data: result };
700
+ }
701
+ async handleBrowserSetViewport(payload = {}) {
702
+ const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
703
+ const width = Number(payload.width);
704
+ const height = Number(payload.height);
705
+ if (!Number.isFinite(width) || !Number.isFinite(height)) {
706
+ throw new Error('width/height required');
707
+ }
708
+ const result = await this.browserServiceCommand('page:setViewport', { profileId, width, height }, { timeoutMs: 30000 });
709
+ return { success: true, data: result };
710
+ }
711
+ async handleKeyboardPress(payload = {}) {
712
+ const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
713
+ const key = (payload.key || 'Enter').toString();
714
+ const delay = typeof payload.delay === 'number' ? payload.delay : undefined;
715
+ const result = await this.browserServiceCommand('keyboard:press', { profileId, key, ...(delay ? { delay } : {}) });
716
+ return { success: true, data: result };
717
+ }
718
+ async handleKeyboardType(payload = {}) {
719
+ const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
720
+ const text = (payload.text ?? '').toString();
721
+ const delay = typeof payload.delay === 'number' ? payload.delay : undefined;
722
+ const submit = typeof payload.submit === 'boolean' ? payload.submit : Boolean(payload.submit);
723
+ const result = await this.browserServiceCommand('keyboard:type', { profileId, text, ...(delay ? { delay } : {}), ...(submit ? { submit } : {}) });
724
+ return { success: true, data: result };
725
+ }
726
+ async handleMouseWheel(payload = {}) {
727
+ const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
728
+ const deltaY = Number(payload.deltaY ?? payload.y ?? payload.dy ?? 0) || 0;
729
+ const deltaX = Number(payload.deltaX ?? payload.x ?? payload.dx ?? 0) || 0;
730
+ const result = await this.browserServiceCommand('mouse:wheel', { profileId, deltaX, deltaY });
731
+ return { success: true, data: result };
732
+ }
733
+ async handleBrowserHighlightDomPath(payload = {}) {
734
+ const { profile, path, url, rootSelector, style, sticky, duration, channel } = normalizePayload(payload, { required: ['profile', 'path'] });
735
+ // 先加载 DOM 分支确保元素存在
736
+ if (path !== 'root' && url) {
737
+ try {
738
+ await this.fetchDomBranchFromService({
739
+ sessionId: profile,
740
+ url,
741
+ path,
742
+ rootSelector,
743
+ maxDepth: 2,
744
+ maxChildren: 10,
745
+ });
746
+ logDebug('controller', 'highlight-dom-path', { message: 'DOM branch loaded', path });
747
+ }
748
+ catch (err) {
749
+ logDebug('controller', 'highlight-dom-path', { message: 'Failed to load DOM branch', path, error: err.message });
750
+ }
751
+ }
752
+ const finalChannel = channel || 'hover-dom';
753
+ const finalStyle = style || '2px solid rgba(96, 165, 250, 0.95)';
754
+ const finalSticky = typeof sticky === 'boolean' ? sticky : true;
755
+ try {
756
+ const result = await this.sendHighlightDomPathViaWs(profile, path, {
757
+ channel: finalChannel,
758
+ style: finalStyle,
759
+ sticky: finalSticky,
760
+ duration,
761
+ rootSelector: rootSelector || null,
762
+ });
763
+ this.messageBus?.publish?.('ui.highlight.result', {
764
+ success: true,
765
+ selector: null,
766
+ details: result?.details || null,
767
+ });
768
+ return { success: true, data: result };
769
+ }
770
+ catch (err) {
771
+ const errorMessage = err?.message || 'DOM 路径高亮失败';
772
+ this.messageBus?.publish?.('ui.highlight.result', {
773
+ success: false,
774
+ selector: null,
775
+ error: errorMessage,
776
+ });
777
+ throw err;
778
+ }
779
+ }
780
+ async handleBrowserCancelDomPick(payload = {}) {
781
+ const { profile } = normalizePayload(payload, { required: ['profile'] });
782
+ try {
783
+ const data = await this.sendCancelDomPickViaWs(profile);
784
+ this.messageBus?.publish?.('ui.domPicker.result', {
785
+ success: false,
786
+ cancelled: true,
787
+ source: 'cancel-action',
788
+ details: data,
789
+ });
790
+ return { success: true, data };
791
+ }
792
+ catch (err) {
793
+ const message = err?.message || '取消捕获失败';
794
+ this.messageBus?.publish?.('ui.domPicker.result', {
795
+ success: false,
796
+ cancelled: true,
797
+ error: message,
798
+ });
799
+ throw err;
800
+ }
801
+ }
802
+ async handleBrowserPickDom(payload = {}) {
803
+ const { profile, timeout, rootSelector } = normalizePayload(payload, { required: ['profile'] });
804
+ const finalTimeout = Math.min(Math.max(Number(timeout) || 25000, 3000), 60000);
805
+ const startedAt = Date.now();
806
+ try {
807
+ const result = await this.sendDomPickerViaWs(profile, {
808
+ timeout: finalTimeout,
809
+ rootSelector,
810
+ });
811
+ this.messageBus?.publish?.('ui.domPicker.result', {
812
+ success: true,
813
+ selector: result?.selector || null,
814
+ domPath: result?.dom_path || null,
815
+ durationMs: Date.now() - startedAt,
816
+ });
817
+ return { success: true, data: result };
818
+ }
819
+ catch (err) {
820
+ const message = err?.message || '元素拾取失败';
821
+ this.messageBus?.publish?.('ui.domPicker.result', { success: false, error: message });
822
+ throw err;
823
+ }
824
+ }
825
+ // v2 DOM pick:直接暴露 dom_path + selector 给 UI
826
+ async handleDomPick2(payload = {}) {
827
+ const { profile, timeout, rootSelector } = normalizePayload(payload, { required: ['profile'] });
828
+ const finalTimeout = Math.min(Math.max(Number(timeout) || 25000, 3000), 60000);
829
+ const result = await this.sendDomPickerViaWs(profile, { timeout: finalTimeout, rootSelector });
830
+ // 统一输出结构:domPath + selector
831
+ return {
832
+ success: true,
833
+ data: {
834
+ domPath: result?.dom_path || null,
835
+ selector: result?.selector || null,
836
+ raw: result,
837
+ },
838
+ };
839
+ }
840
+ async sendHighlightViaWs(sessionId, selector, options = {}) {
841
+ const payload = {
842
+ type: 'command',
843
+ session_id: sessionId,
844
+ data: {
845
+ command_type: 'dev_command',
846
+ action: 'highlight_element',
847
+ parameters: {
848
+ selector,
849
+ ...(options.style ? { style: options.style } : {}),
850
+ ...(typeof options.duration === 'number' ? { duration: options.duration } : {}),
851
+ ...(options.channel ? { channel: options.channel } : {}),
852
+ ...(typeof options.sticky === 'boolean' ? { sticky: options.sticky } : {}),
853
+ ...(typeof options.maxMatches === 'number' ? { max_matches: options.maxMatches } : {}),
854
+ },
855
+ },
856
+ };
857
+ const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
858
+ const data = response?.data || response;
859
+ const success = data?.success !== false;
860
+ if (!success) {
861
+ const err = data?.error || response?.error;
862
+ throw new Error(err || 'highlight_element failed');
863
+ }
864
+ return {
865
+ success: true,
866
+ source: 'ws',
867
+ details: data?.data || data,
868
+ };
869
+ }
870
+ async sendHighlightDomPathViaWs(sessionId, domPath, options = {}) {
871
+ const payload = {
872
+ type: 'command',
873
+ session_id: sessionId,
874
+ data: {
875
+ command_type: 'dev_command',
876
+ action: 'highlight_dom_path',
877
+ parameters: {
878
+ path: domPath,
879
+ ...(options.style ? { style: options.style } : {}),
880
+ ...(typeof options.duration === 'number' ? { duration: options.duration } : {}),
881
+ ...(options.channel ? { channel: options.channel } : {}),
882
+ ...(typeof options.sticky === 'boolean' ? { sticky: options.sticky } : {}),
883
+ ...(options.rootSelector ? { root_selector: options.rootSelector } : {}),
884
+ },
885
+ },
886
+ };
887
+ const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
888
+ const data = response?.data || response;
889
+ const success = data?.success !== false;
890
+ if (!success) {
891
+ const err = data?.error || response?.error;
892
+ throw new Error(err || 'highlight_dom_path failed');
893
+ }
894
+ return {
895
+ success: true,
896
+ source: 'ws',
897
+ details: data?.data || data,
898
+ };
899
+ }
900
+ async sendClearHighlightViaWs(sessionId, channel = null) {
901
+ const payload = {
902
+ type: 'command',
903
+ session_id: sessionId,
904
+ data: {
905
+ command_type: 'dev_command',
906
+ action: 'clear_highlight',
907
+ parameters: channel ? { channel } : {},
908
+ },
909
+ };
910
+ const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 10000);
911
+ const data = response?.data || response;
912
+ if (data?.success === false) {
913
+ throw new Error(data?.error || 'clear_highlight failed');
914
+ }
915
+ return data?.data || data || { removed: 0 };
916
+ }
917
+ async sendCancelDomPickViaWs(sessionId) {
918
+ const payload = {
919
+ type: 'command',
920
+ session_id: sessionId,
921
+ data: {
922
+ command_type: 'dev_command',
923
+ action: 'cancel_dom_pick',
924
+ parameters: {},
925
+ },
926
+ };
927
+ const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 10000);
928
+ const data = response?.data || response;
929
+ if (data?.success === false) {
930
+ throw new Error(data?.error || 'cancel_dom_pick failed');
931
+ }
932
+ return data?.data || data || { cancelled: false };
933
+ }
934
+ async sendDomPickerViaWs(sessionId, options = {}) {
935
+ const timeout = Math.min(Math.max(Number(options.timeout) || 25000, 3000), 60000);
936
+ const payload = {
937
+ type: 'command',
938
+ session_id: sessionId,
939
+ data: {
940
+ command_type: 'node_execute',
941
+ node_type: 'pick_dom',
942
+ parameters: {
943
+ timeout,
944
+ ...(options.rootSelector ? { root_selector: options.rootSelector } : {}),
945
+ },
946
+ },
947
+ };
948
+ const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, timeout + 5000);
949
+ const data = response?.data;
950
+ if (data?.success === false) {
951
+ throw new Error(data?.error || 'pick_dom failed');
952
+ }
953
+ const result = data?.data || data;
954
+ if (!result) {
955
+ throw new Error('picker result missing');
956
+ }
957
+ return result;
958
+ }
959
+ async sendExecuteViaWs(sessionId, script) {
960
+ const payload = {
961
+ type: 'command',
962
+ session_id: sessionId,
963
+ data: {
964
+ command_type: 'node_execute',
965
+ node_type: 'evaluate',
966
+ parameters: {
967
+ script,
968
+ },
969
+ },
970
+ };
971
+ const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 10000);
972
+ const data = response?.data || response;
973
+ if (data?.success === false) {
974
+ throw new Error(data?.error || 'execute failed');
975
+ }
976
+ return data?.data || data || { result: null };
977
+ }
978
+ async fetchContainerSnapshotFromService({ sessionId, url, maxDepth, maxChildren, rootContainerId, rootSelector }) {
979
+ if (!sessionId || !url) {
980
+ throw new Error('缺少 sessionId 或 URL');
981
+ }
982
+ const payload = {
983
+ type: 'command',
984
+ session_id: sessionId,
985
+ data: {
986
+ command_type: 'container_operation',
987
+ action: 'inspect_tree',
988
+ page_context: { url },
989
+ parameters: {
990
+ ...(typeof maxDepth === 'number' ? { max_depth: maxDepth } : {}),
991
+ ...(typeof maxChildren === 'number' ? { max_children: maxChildren } : {}),
992
+ ...(rootContainerId ? { root_container_id: rootContainerId } : {}),
993
+ ...(rootSelector ? { root_selector: rootSelector } : {}),
994
+ },
995
+ },
996
+ };
997
+ const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
998
+ if (response?.data?.success) {
999
+ return response.data.data || response.data.snapshot || response.data;
1000
+ }
1001
+ throw new Error(response?.data?.error || response?.error || 'inspect_tree failed');
1002
+ }
1003
+ async fetchDomBranchFromService({ sessionId, url, path, rootSelector, maxDepth, maxChildren }) {
1004
+ if (!sessionId || !url || !path) {
1005
+ throw new Error('缺少 sessionId / URL / DOM 路径');
1006
+ }
1007
+ const payload = {
1008
+ type: 'command',
1009
+ session_id: sessionId,
1010
+ data: {
1011
+ command_type: 'container_operation',
1012
+ action: 'inspect_dom_branch',
1013
+ page_context: { url },
1014
+ parameters: {
1015
+ path,
1016
+ ...(rootSelector ? { root_selector: rootSelector } : {}),
1017
+ ...(typeof maxDepth === 'number' ? { max_depth: maxDepth } : {}),
1018
+ ...(typeof maxChildren === 'number' ? { max_children: maxChildren } : {}),
1019
+ },
1020
+ }
1021
+ };
1022
+ const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
1023
+ if (response?.data?.success) {
1024
+ return response.data.data || response.data.snapshot || response.data;
1025
+ }
1026
+ throw new Error(response?.data?.error || response?.error || 'inspect_dom_branch failed');
1027
+ }
1028
+ // 下列方法尚未在 TS 版本实现,仅为保持编译通过的占位实现
1029
+ async handleDomBranch2(_payload = {}) {
1030
+ throw new Error('handleDomBranch2 is not implemented in controller.ts');
1031
+ }
1032
+ async fetchSessions() {
1033
+ try {
1034
+ const res = await this.runCliCommand('session-manager', ['list']);
1035
+ const sessions = res?.sessions || res?.data?.sessions || res?.data || [];
1036
+ return Array.isArray(sessions) ? sessions : [];
1037
+ }
1038
+ catch {
1039
+ return [];
1040
+ }
1041
+ }
1042
+ findSessionByProfile(sessions, profile) {
1043
+ if (!profile)
1044
+ return null;
1045
+ return (sessions.find((session) => session?.profileId === profile ||
1046
+ session?.profile_id === profile ||
1047
+ session?.session_id === profile ||
1048
+ session?.sessionId === profile) || null);
1049
+ }
1050
+ focusSnapshotOnContainer(snapshot, containerId) {
1051
+ if (!containerId || !snapshot?.container_tree) {
1052
+ return snapshot;
1053
+ }
1054
+ const target = this.cloneContainerSubtree(snapshot.container_tree, containerId);
1055
+ if (!target) {
1056
+ return snapshot;
1057
+ }
1058
+ const nextSnapshot = {
1059
+ ...snapshot,
1060
+ container_tree: target,
1061
+ metadata: {
1062
+ ...(snapshot.metadata || {}),
1063
+ root_container_id: containerId,
1064
+ },
1065
+ };
1066
+ if (!nextSnapshot.root_match || nextSnapshot.root_match?.container?.id !== containerId) {
1067
+ nextSnapshot.root_match = {
1068
+ container: {
1069
+ id: containerId,
1070
+ ...(target.name ? { name: target.name } : {}),
1071
+ },
1072
+ matched_selector: target.match?.matched_selector,
1073
+ };
1074
+ }
1075
+ return nextSnapshot;
1076
+ }
1077
+ cloneContainerSubtree(node, targetId) {
1078
+ if (!node)
1079
+ return null;
1080
+ if (node.id === targetId || node.container_id === targetId) {
1081
+ return this.deepClone(node);
1082
+ }
1083
+ if (Array.isArray(node.children)) {
1084
+ for (const child of node.children) {
1085
+ const match = this.cloneContainerSubtree(child, targetId);
1086
+ if (match)
1087
+ return match;
1088
+ }
1089
+ }
1090
+ return null;
1091
+ }
1092
+ deepClone(payload) {
1093
+ return JSON.parse(JSON.stringify(payload));
1094
+ }
1095
+ async captureInspectorSnapshot(options = {}) {
1096
+ const profile = options.profile;
1097
+ // 优先使用调用方提供的 URL,避免为了 containers:match 再额外跑一次 session-manager CLI
1098
+ // 只有在 URL 缺失时才回退到 CLI 查询当前会话列表。
1099
+ let targetSession = null;
1100
+ let sessions = [];
1101
+ if (!options.url) {
1102
+ sessions = await this.fetchSessions();
1103
+ targetSession = profile ? this.findSessionByProfile(sessions, profile) : sessions[0] || null;
1104
+ }
1105
+ const sessionId = profile || targetSession?.session_id || targetSession?.sessionId || null;
1106
+ const profileId = profile || targetSession?.profileId || targetSession?.profile_id || sessionId || null;
1107
+ const targetUrl = options.url || targetSession?.current_url || targetSession?.currentUrl;
1108
+ const requestedContainerId = options.containerId || options.rootContainerId;
1109
+ if (!targetUrl) {
1110
+ throw new Error('无法确定会话 URL,请先在浏览器中打开目标页面');
1111
+ }
1112
+ let liveError = null;
1113
+ let snapshot = null;
1114
+ let fromCache = false;
1115
+ let cacheAgeMs = null;
1116
+ const cacheEnabled = typeof options.cache === 'boolean'
1117
+ ? options.cache
1118
+ : String(process.env.WEBAUTO_CONTAINER_SNAPSHOT_CACHE || '0') === '1';
1119
+ const cacheTtlMs = typeof options.cacheTtlMs === 'number' && Number.isFinite(options.cacheTtlMs)
1120
+ ? Math.max(0, Math.floor(options.cacheTtlMs))
1121
+ : Number(process.env.WEBAUTO_CONTAINER_SNAPSHOT_CACHE_TTL_MS || 5000);
1122
+ if (sessionId && targetUrl && cacheEnabled) {
1123
+ const cacheKey = this.getSnapshotCacheKey(options, targetUrl, sessionId);
1124
+ if (options.invalidateCache) {
1125
+ this.snapshotCache.delete(cacheKey);
1126
+ }
1127
+ else {
1128
+ const cached = this.snapshotCache.get(cacheKey);
1129
+ if (cached) {
1130
+ const age = Date.now() - cached.ts;
1131
+ if (age <= cacheTtlMs) {
1132
+ snapshot = cached.snapshot;
1133
+ fromCache = true;
1134
+ cacheAgeMs = age;
1135
+ }
1136
+ else {
1137
+ this.snapshotCache.delete(cacheKey);
1138
+ }
1139
+ }
1140
+ }
1141
+ }
1142
+ if (sessionId) {
1143
+ try {
1144
+ if (!snapshot) {
1145
+ snapshot = await this.fetchContainerSnapshotFromService({
1146
+ sessionId,
1147
+ url: targetUrl,
1148
+ maxDepth: options.maxDepth,
1149
+ maxChildren: options.maxChildren,
1150
+ rootContainerId: requestedContainerId,
1151
+ rootSelector: options.rootSelector,
1152
+ });
1153
+ if (snapshot && cacheEnabled && sessionId && targetUrl) {
1154
+ const cacheKey = this.getSnapshotCacheKey(options, targetUrl, sessionId);
1155
+ this.snapshotCache.set(cacheKey, {
1156
+ ts: Date.now(),
1157
+ sessionId,
1158
+ profileId: profileId || 'default',
1159
+ url: targetUrl,
1160
+ snapshot,
1161
+ });
1162
+ }
1163
+ }
1164
+ }
1165
+ catch (err) {
1166
+ liveError = err;
1167
+ }
1168
+ }
1169
+ if (!snapshot || !snapshot.container_tree) {
1170
+ const rootError = liveError || new Error('容器树为空,检查容器定义或选择器是否正确');
1171
+ throw rootError;
1172
+ }
1173
+ if (requestedContainerId) {
1174
+ snapshot = this.focusSnapshotOnContainer(snapshot, requestedContainerId);
1175
+ }
1176
+ return {
1177
+ sessionId: sessionId || profileId || 'unknown-session',
1178
+ profileId: profileId || 'default',
1179
+ targetUrl,
1180
+ snapshot,
1181
+ cache: {
1182
+ enabled: cacheEnabled,
1183
+ hit: fromCache,
1184
+ ageMs: cacheAgeMs,
1185
+ ttlMs: cacheTtlMs,
1186
+ },
1187
+ };
1188
+ }
1189
+ async captureInspectorBranch(options = {}) {
1190
+ const profile = options.profile;
1191
+ const domPath = options.path;
1192
+ if (!profile)
1193
+ throw new Error('缺少 profile');
1194
+ if (!domPath)
1195
+ throw new Error('缺少 DOM 路径');
1196
+ const sessions = await this.fetchSessions();
1197
+ const targetSession = profile ? this.findSessionByProfile(sessions, profile) : sessions[0] || null;
1198
+ const sessionId = targetSession?.session_id || targetSession?.sessionId || profile || null;
1199
+ const profileId = profile || targetSession?.profileId || targetSession?.profile_id || sessionId || null;
1200
+ const targetUrl = options.url || targetSession?.current_url || targetSession?.currentUrl;
1201
+ if (!targetUrl) {
1202
+ throw new Error('无法确定会话 URL');
1203
+ }
1204
+ let branch = null;
1205
+ let liveError = null;
1206
+ if (sessionId) {
1207
+ try {
1208
+ branch = await this.fetchDomBranchFromService({
1209
+ sessionId,
1210
+ url: targetUrl,
1211
+ path: domPath,
1212
+ rootSelector: options.rootSelector,
1213
+ maxDepth: options.maxDepth,
1214
+ maxChildren: options.maxChildren,
1215
+ });
1216
+ }
1217
+ catch (err) {
1218
+ liveError = err;
1219
+ }
1220
+ }
1221
+ if (!branch?.node) {
1222
+ throw liveError || new Error('无法获取 DOM 分支');
1223
+ }
1224
+ return {
1225
+ sessionId: sessionId || profileId || 'unknown-session',
1226
+ profileId: profileId || 'default',
1227
+ targetUrl,
1228
+ branch,
1229
+ };
1230
+ }
1231
+ resolveSiteKeyFromUrl(url) {
1232
+ if (!url)
1233
+ return null;
1234
+ let host = '';
1235
+ try {
1236
+ host = new URL(url).hostname.toLowerCase();
1237
+ }
1238
+ catch {
1239
+ return null;
1240
+ }
1241
+ const index = this.loadContainerIndex();
1242
+ let bestKey = null;
1243
+ let bestLen = -1;
1244
+ for (const [key, meta] of Object.entries(index)) {
1245
+ const domain = (meta?.website || '').toLowerCase();
1246
+ if (!domain)
1247
+ continue;
1248
+ if (host === domain || host.endsWith(`.${domain}`)) {
1249
+ if (domain.length > bestLen) {
1250
+ bestKey = key;
1251
+ bestLen = domain.length;
1252
+ }
1253
+ }
1254
+ }
1255
+ return bestKey;
1256
+ }
1257
+ loadContainerIndex() {
1258
+ if (this._containerIndexCache) {
1259
+ return this._containerIndexCache;
1260
+ }
1261
+ if (!fs.existsSync(this.containerIndexPath)) {
1262
+ this._containerIndexCache = {};
1263
+ return this._containerIndexCache;
1264
+ }
1265
+ try {
1266
+ this._containerIndexCache = JSON.parse(fs.readFileSync(this.containerIndexPath, 'utf-8'));
1267
+ }
1268
+ catch {
1269
+ this._containerIndexCache = {};
1270
+ }
1271
+ return this._containerIndexCache;
1272
+ }
1273
+ inferSiteFromContainerId(containerId) {
1274
+ if (!containerId)
1275
+ return null;
1276
+ const dotIdx = containerId.indexOf('.');
1277
+ if (dotIdx > 0) {
1278
+ return containerId.slice(0, dotIdx);
1279
+ }
1280
+ const underscoreIdx = containerId.indexOf('_');
1281
+ if (underscoreIdx > 0) {
1282
+ return containerId.slice(0, underscoreIdx);
1283
+ }
1284
+ return null;
1285
+ }
1286
+ async writeUserContainerDefinition(siteKey, containerId, definition) {
1287
+ const parts = containerId.split('.').filter(Boolean);
1288
+ const targetDir = path.join(this.userContainerRoot, siteKey, ...parts);
1289
+ await fsPromises.mkdir(targetDir, { recursive: true });
1290
+ const filePath = path.join(targetDir, 'container.json');
1291
+ const payload = { ...definition, id: containerId };
1292
+ await fsPromises.writeFile(filePath, JSON.stringify(payload, null, 2), 'utf-8');
1293
+ }
1294
+ normalizeSelectors(_selectors) {
1295
+ return _selectors;
1296
+ }
1297
+ async readContainerDefinition(siteKey, containerId) {
1298
+ const parts = containerId.split('.').filter(Boolean);
1299
+ const targetDir = path.join(this.userContainerRoot, siteKey, ...parts);
1300
+ const filePath = path.join(targetDir, 'container.json');
1301
+ if (!fs.existsSync(filePath)) {
1302
+ return null;
1303
+ }
1304
+ try {
1305
+ const content = await fsPromises.readFile(filePath, 'utf-8');
1306
+ return JSON.parse(content);
1307
+ }
1308
+ catch {
1309
+ return null;
1310
+ }
1311
+ }
1312
+ async sendWsCommand(_url, _payload, _timeout = 15000) {
1313
+ return new Promise((resolve, reject) => {
1314
+ const socket = new WebSocket(_url);
1315
+ let settled = false;
1316
+ const timeout = setTimeout(() => {
1317
+ if (settled)
1318
+ return;
1319
+ settled = true;
1320
+ socket.terminate();
1321
+ reject(new Error("WebSocket command timeout"));
1322
+ }, _timeout);
1323
+ const cleanup = () => {
1324
+ clearTimeout(timeout);
1325
+ socket.removeAllListeners();
1326
+ };
1327
+ socket.once("open", () => {
1328
+ try {
1329
+ socket.send(JSON.stringify(_payload));
1330
+ }
1331
+ catch (err) {
1332
+ cleanup();
1333
+ if (!settled) {
1334
+ settled = true;
1335
+ reject(err);
1336
+ }
1337
+ }
1338
+ });
1339
+ socket.once("message", (data) => {
1340
+ cleanup();
1341
+ if (settled)
1342
+ return;
1343
+ settled = true;
1344
+ try {
1345
+ resolve(JSON.parse(data.toString("utf-8")));
1346
+ }
1347
+ catch (err) {
1348
+ reject(err);
1349
+ }
1350
+ finally {
1351
+ socket.close();
1352
+ }
1353
+ });
1354
+ socket.once("error", (err) => {
1355
+ cleanup();
1356
+ if (settled)
1357
+ return;
1358
+ settled = true;
1359
+ reject(err);
1360
+ });
1361
+ socket.once("close", () => {
1362
+ cleanup();
1363
+ if (!settled) {
1364
+ settled = true;
1365
+ reject(new Error("WebSocket closed before response"));
1366
+ }
1367
+ });
1368
+ });
1369
+ }
1370
+ getBrowserWsUrl() {
1371
+ return `ws://${this.defaultWsHost || '127.0.0.1'}:${this.defaultWsPort || 7701}/ws`;
1372
+ }
1373
+ async handleContainerOperation(payload = {}) {
1374
+ const { containerId, operationId, config, profile } = normalizePayload(payload, { required: ['containerId', 'operationId', 'profile'] });
1375
+ // Allow caller to override timeout; without this Phase2 can hang until the outer controllerAction aborts.
1376
+ const timeoutMs = typeof payload?.timeoutMs === 'number' && Number.isFinite(payload.timeoutMs) && payload.timeoutMs > 0
1377
+ ? Math.floor(payload.timeoutMs)
1378
+ : 120000;
1379
+ // Determine target URL for HTTP post to container endpoint
1380
+ const port = process.env.WEBAUTO_UNIFIED_PORT || 7701;
1381
+ const host = '127.0.0.1';
1382
+ logDebug('controller', 'container:operation', { containerId, operationId, profile, timeoutMs });
1383
+ const abortController2 = new AbortController();
1384
+ const timeoutId2 = setTimeout(() => abortController2.abort(), timeoutMs);
1385
+ try {
1386
+ const mergedConfig = { ...(config || {}) };
1387
+ if (typeof mergedConfig.timeoutMs !== 'number') {
1388
+ mergedConfig.timeoutMs = timeoutMs;
1389
+ }
1390
+ // Global input mode switch: keeps API shape unchanged, only toggles execution strategy.
1391
+ if (['click', 'type', 'key', 'scroll'].includes(String(operationId))) {
1392
+ if (this.inputMode === 'protocol') {
1393
+ mergedConfig.useSystemMouse = false;
1394
+ }
1395
+ else if (typeof mergedConfig.useSystemMouse !== 'boolean') {
1396
+ mergedConfig.useSystemMouse = true;
1397
+ }
1398
+ }
1399
+ const response = await fetch(`http://${host}:${port}/v1/container/${containerId}/execute`, {
1400
+ method: 'POST',
1401
+ headers: { 'Content-Type': 'application/json' },
1402
+ body: JSON.stringify({
1403
+ operationId,
1404
+ config: mergedConfig,
1405
+ sessionId: profile
1406
+ }),
1407
+ signal: abortController2.signal,
1408
+ });
1409
+ clearTimeout(timeoutId2);
1410
+ if (!response.ok) {
1411
+ return { success: false, error: await response.text() };
1412
+ }
1413
+ const result = await response.json();
1414
+ clearTimeout(timeoutId2);
1415
+ return result;
1416
+ }
1417
+ catch (error) {
1418
+ clearTimeout(timeoutId2);
1419
+ logDebug('controller', 'container:operation:error', { containerId, operationId, profile, timeoutMs, error: error?.message || String(error) });
1420
+ return { success: false, error: error.message };
1421
+ }
1422
+ }
1423
+ emitContainerAppearEvents(containerTree, context) {
1424
+ if (!this.messageBus?.publish)
1425
+ return;
1426
+ if (!containerTree || !containerTree.id) {
1427
+ logDebug('controller', 'emitContainerAppearEvents', { message: 'No valid container tree provided' });
1428
+ return;
1429
+ }
1430
+ const visited = new Set();
1431
+ // Emit for root container (the containerTree itself)
1432
+ this.emitSingleContainerAppear(containerTree, context, visited);
1433
+ // Emit for all child containers in tree
1434
+ if (containerTree.children && Array.isArray(containerTree.children)) {
1435
+ for (const child of containerTree.children) {
1436
+ this.emitTreeContainerAppear(child, context, visited);
1437
+ }
1438
+ }
1439
+ }
1440
+ emitSingleContainerAppear(container, context, visited) {
1441
+ if (!container || !container.id)
1442
+ return;
1443
+ const containerId = String(container.id);
1444
+ if (visited.has(containerId))
1445
+ return;
1446
+ visited.add(containerId);
1447
+ const payload = {
1448
+ containerId,
1449
+ containerName: container.name || null,
1450
+ sessionId: context.sessionId,
1451
+ profileId: context.profileId,
1452
+ url: context.url,
1453
+ bbox: container.match?.bbox || container.bbox || null,
1454
+ visible: container.match?.visible ?? container.visible ?? null,
1455
+ score: container.match?.score ?? container.score ?? null,
1456
+ timestamp: Date.now(),
1457
+ source: 'containers:match',
1458
+ };
1459
+ this.messageBus?.publish?.('container:appear', payload);
1460
+ this.messageBus?.publish?.(`container:${containerId}:appear`, payload);
1461
+ logDebug('controller', 'container:appear', { containerId, containerName: container.name });
1462
+ }
1463
+ emitTreeContainerAppear(node, context, visited) {
1464
+ if (!node || !node.id)
1465
+ return;
1466
+ // Emit for this node (node itself is a container object)
1467
+ this.emitSingleContainerAppear(node, context, visited);
1468
+ // Recursively emit for children
1469
+ if (node.children && Array.isArray(node.children)) {
1470
+ for (const child of node.children) {
1471
+ this.emitTreeContainerAppear(child, context, visited);
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+ //# sourceMappingURL=controller.js.map