@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,1240 @@
1
+ /**
2
+ * Workflow Block: OpenDetailBlock
3
+ *
4
+ * 打开详情页(通过容器 click 触发模态框)
5
+ */
6
+ import { createOpenDetailControllerClient } from './helpers/openDetailController.js';
7
+ import { waitForDetail } from './helpers/openDetailWaiter.js';
8
+ import { createOpenDetailViewportTools } from './helpers/openDetailViewport.js';
9
+ import { isDebugArtifactsEnabled } from './helpers/debugArtifacts.js';
10
+ import path from 'node:path';
11
+ import { mkdir, writeFile } from 'node:fs/promises';
12
+ /**
13
+ * 打开详情页
14
+ */
15
+ export async function execute(input) {
16
+ const { sessionId, containerId, domIndex, clickRect, expectedNoteId, expectedHref, debugDir: requestedDebugDir, serviceUrl = 'http://127.0.0.1:7701', } = input;
17
+ const debugArtifactsEnabled = isDebugArtifactsEnabled();
18
+ const debugDir = debugArtifactsEnabled ? requestedDebugDir : undefined;
19
+ const profile = sessionId;
20
+ const controllerUrl = `${serviceUrl}/v1/controller/action`;
21
+ const steps = [];
22
+ let entryAnchor;
23
+ let exitAnchor;
24
+ let clickedItemRect;
25
+ function pushStep(step) {
26
+ steps.push(step);
27
+ try {
28
+ console.log('[OpenDetail][step]', JSON.stringify({
29
+ id: step.id,
30
+ status: step.status,
31
+ error: step.error,
32
+ anchor: step.anchor,
33
+ meta: step.meta,
34
+ }, null, 2));
35
+ }
36
+ catch {
37
+ console.log('[OpenDetail][step]', step.id, step.status);
38
+ }
39
+ }
40
+ // 创建 helper 客户端
41
+ const controllerClient = createOpenDetailControllerClient({ profile, controllerUrl });
42
+ const { controllerAction, getCurrentUrl } = controllerClient;
43
+ const waiterDeps = {
44
+ getCurrentUrl,
45
+ controllerAction,
46
+ profile,
47
+ serviceUrl,
48
+ };
49
+ const viewportTools = createOpenDetailViewportTools({
50
+ controllerAction,
51
+ profile,
52
+ serviceUrl,
53
+ });
54
+ const { getViewportMetrics, computeCoverRectByIndex, computeCoverRectByNoteId, computeCardRectByIndex, computeCardRectByNoteId, isPointInsideCover, highlightRect, dumpViewportDiagnostics, } = viewportTools;
55
+ function clamp(n, min, max) {
56
+ return Math.max(min, Math.min(max, n));
57
+ }
58
+ function computeSafeClickPoint(rect, viewport) {
59
+ const viewportW = typeof viewport.innerWidth === 'number' && viewport.innerWidth > 0 ? viewport.innerWidth : 1440;
60
+ const viewportH = typeof viewport.innerHeight === 'number' && viewport.innerHeight > 0 ? viewport.innerHeight : 900;
61
+ const minX = Math.round(rect.x + 12);
62
+ const maxX = Math.round(rect.x + rect.width - 12);
63
+ const minY = Math.round(rect.y + 12);
64
+ const maxY = Math.round(rect.y + rect.height - 12);
65
+ // 目标为封面(a.cover)时,点中心即可;
66
+ // 这里保留“上半区”作为兜底(仅当上游传入的 rect 不是封面 rect 时也尽量降低误点作者区概率)。
67
+ let x = Math.round(rect.x + rect.width / 2);
68
+ let y = Math.round(rect.y + rect.height * 0.12);
69
+ x = clamp(x, minX, maxX);
70
+ y = clamp(y, minY, maxY);
71
+ // 避免点击到顶部固定栏/遮挡层:仅在点位落到顶部栏范围时抬高
72
+ const headerSafeY = 120;
73
+ if (y < headerSafeY) {
74
+ y = clamp(headerSafeY, minY, maxY);
75
+ }
76
+ x = clamp(x, 30, viewportW - 30);
77
+ y = clamp(y, 30, viewportH - 30);
78
+ // 再次确保仍在 rect 内
79
+ x = clamp(x, minX, maxX);
80
+ y = clamp(y, minY, maxY);
81
+ return { x, y };
82
+ }
83
+ async function probeClickTarget(point, coverRect) {
84
+ try {
85
+ const res = await controllerAction('browser:execute', {
86
+ profile,
87
+ script: `(() => {
88
+ const p = ${JSON.stringify(point)};
89
+ const coverRect = ${coverRect ? JSON.stringify(coverRect) : 'undefined'};
90
+ const el = document.elementFromPoint(p.x, p.y);
91
+ const tag = el && el.tagName ? String(el.tagName) : null;
92
+ const className = el && el.className ? String(el.className) : null;
93
+ const a = el && el.closest ? el.closest('a') : null;
94
+ const href = a ? (a.getAttribute('href') || a.href || '') : '';
95
+ const inCover = !!(el && el.closest && el.closest('a.cover'));
96
+ const inQueryNoteWrapper = !!(el && el.closest && el.closest('.query-note-wrapper'));
97
+ const isSearchKeywordLink =
98
+ href.includes('/search_result') &&
99
+ (href.includes('keyword=') || href.includes('?keyword=') || href.includes('&keyword='));
100
+ const isUserProfile =
101
+ href.includes('/user/profile') ||
102
+ href.includes('/user/') ||
103
+ href.includes('/profile/') ||
104
+ href.includes('/profile');
105
+ const textSnippet = el && el.textContent ? String(el.textContent).trim().slice(0, 60) : null;
106
+ const isHashtag = !!(el && el.closest && el.closest('a[href*="search_result"][href*="#"], a[href*="/search_result"][href*="#"], a[href*="search_result"][href*="%23"], a[href*="/search_result"][href*="%23"]'));
107
+
108
+ // 检查点是否在封面 rect 内
109
+ let outOfBounds = false;
110
+ if (coverRect) {
111
+ outOfBounds = p.x < coverRect.x || p.x > coverRect.x + coverRect.width ||
112
+ p.y < coverRect.y || p.y > coverRect.y + coverRect.height;
113
+ }
114
+
115
+ // 检查是否与用户头像元素重合(通过 class 和 href 双重判定)
116
+ let overlapsAvatar = false;
117
+ if (el) {
118
+ const avatarClass = el.className && (
119
+ el.className.includes('avatar') ||
120
+ el.className.includes('user') ||
121
+ el.className.includes('author')
122
+ );
123
+ const avatarParent = el.closest && el.closest('.avatar, .user-avatar, .author-avatar, [class*="avatar"], [class*="user"]');
124
+ const avatarHref = href.includes('/user/') || href.includes('/profile/');
125
+ overlapsAvatar = Boolean(avatarClass || avatarParent || avatarHref);
126
+ }
127
+
128
+ // 检查是否点击到图片元素(详情页内可能触发图片查看器)
129
+ let isImage = false;
130
+ if (el) {
131
+ const isImgTag = tag === 'IMG';
132
+ const hasImageClass = className && (className.includes('image') || className.includes('photo') || className.includes('picture'));
133
+ const isImageParent = el.closest && el.closest('figure, .image, .photo, .picture, [class*="image"], [class*="photo"]');
134
+ isImage = Boolean(isImgTag || hasImageClass || isImageParent);
135
+ }
136
+
137
+ // 检查封面所在的卡片是否被截断(搜索结果列表场景)
138
+ let cardTruncated = false;
139
+ if (inCover && coverRect) {
140
+ const coverEl = el && el.closest && el.closest('a.cover');
141
+ if (coverEl) {
142
+ const card = coverEl.closest('.query-note-wrapper, section, article');
143
+ if (card) {
144
+ const cardRect = card.getBoundingClientRect();
145
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
146
+ // 检查卡片底部是否被截断(允许 20px 容差)
147
+ const cardBottom = cardRect.bottom;
148
+ const safeBottom = viewportHeight - 20;
149
+ cardTruncated = cardBottom > safeBottom;
150
+ }
151
+ }
152
+ }
153
+
154
+ return { inCover, inQueryNoteWrapper, isSearchKeywordLink, href: href || null, isUserProfile, isHashtag, tag, className, textSnippet, outOfBounds, overlapsAvatar, isImage, cardTruncated };
155
+ })()`,
156
+ });
157
+ const payload = res?.result ?? res?.data?.result ?? res ?? {};
158
+ return {
159
+ inCover: Boolean(payload?.inCover),
160
+ closestHref: typeof payload?.href === 'string' ? payload.href : null,
161
+ isUserProfile: Boolean(payload?.isUserProfile),
162
+ isHashtag: Boolean(payload?.isHashtag),
163
+ inQueryNoteWrapper: Boolean(payload?.inQueryNoteWrapper),
164
+ isSearchKeywordLink: Boolean(payload?.isSearchKeywordLink),
165
+ tag: typeof payload?.tag === 'string' ? payload.tag : null,
166
+ className: typeof payload?.className === 'string' ? payload.className : null,
167
+ textSnippet: typeof payload?.textSnippet === 'string' ? payload.textSnippet : null,
168
+ outOfBounds: Boolean(payload?.outOfBounds),
169
+ overlapsAvatar: Boolean(payload?.overlapsAvatar),
170
+ isImage: Boolean(payload?.isImage),
171
+ cardTruncated: Boolean(payload?.cardTruncated),
172
+ };
173
+ }
174
+ catch {
175
+ return {
176
+ inCover: false,
177
+ closestHref: null,
178
+ isUserProfile: false,
179
+ isHashtag: false,
180
+ inQueryNoteWrapper: false,
181
+ isSearchKeywordLink: false,
182
+ tag: null,
183
+ className: null,
184
+ textSnippet: null,
185
+ outOfBounds: true,
186
+ overlapsAvatar: false,
187
+ isImage: false,
188
+ cardTruncated: false,
189
+ };
190
+ }
191
+ }
192
+ async function chooseSafeClickPoint(rect, viewport) {
193
+ const viewportW = typeof viewport.innerWidth === 'number' && viewport.innerWidth > 0 ? viewport.innerWidth : 1440;
194
+ const viewportH = typeof viewport.innerHeight === 'number' && viewport.innerHeight > 0 ? viewport.innerHeight : 900;
195
+ const minX = Math.round(rect.x + 12);
196
+ const maxX = Math.round(rect.x + rect.width - 12);
197
+ const minY = Math.round(rect.y + 12);
198
+ const maxY = Math.round(rect.y + rect.height - 12);
199
+ const headerSafeY = 120;
200
+ function computePointByFraction(fx, fy) {
201
+ let x = Math.round(rect.x + rect.width * fx);
202
+ let y = Math.round(rect.y + rect.height * fy);
203
+ x = clamp(x, minX, maxX);
204
+ y = clamp(y, minY, maxY);
205
+ if (y < headerSafeY) {
206
+ y = clamp(headerSafeY, minY, maxY);
207
+ }
208
+ x = clamp(x, 30, viewportW - 30);
209
+ y = clamp(y, 30, viewportH - 30);
210
+ x = clamp(x, minX, maxX);
211
+ y = clamp(y, minY, maxY);
212
+ return { x, y };
213
+ }
214
+ const candidates = [
215
+ // 搜索结果卡片常见:下半部分是作者/互动区,点太靠下容易误入个人主页
216
+ // 优先点封面上半区(更接近"封面中心"而不是"卡片中心")
217
+ { fx: 0.5, fy: 0.28 },
218
+ { fx: 0.5, fy: 0.22 },
219
+ { fx: 0.5, fy: 0.32 },
220
+ { fx: 0.55, fy: 0.28 },
221
+ { fx: 0.45, fy: 0.28 },
222
+ // 次选:中心(某些纯封面卡片只有 cover)
223
+ { fx: 0.5, fy: 0.5 },
224
+ ];
225
+ for (const c of candidates) {
226
+ const p0 = computePointByFraction(c.fx, c.fy);
227
+ // 探测时传入封面 rect,检查是否在 rect 内且不与头像重合
228
+ const probe = await probeClickTarget(p0, rect);
229
+ // 严格保护:必须在 rect 内,在 cover 内,不与头像重合,不点击图片,卡片不被截断
230
+ if (probe.inCover &&
231
+ !probe.isUserProfile &&
232
+ !probe.isHashtag &&
233
+ !probe.inQueryNoteWrapper &&
234
+ !probe.isSearchKeywordLink &&
235
+ !probe.outOfBounds &&
236
+ !probe.overlapsAvatar &&
237
+ !probe.isImage &&
238
+ !probe.cardTruncated) {
239
+ return { x: p0.x, y: p0.y, probe };
240
+ }
241
+ // 记录被拒绝的候选点原因
242
+ if (probe.outOfBounds || probe.overlapsAvatar || probe.isImage || probe.cardTruncated) {
243
+ console.log(`[chooseSafeClickPoint] Candidate (${c.fx}, ${c.fy}) rejected: outOfBounds=${probe.outOfBounds}, overlapsAvatar=${probe.overlapsAvatar}, isImage=${probe.isImage}, cardTruncated=${probe.cardTruncated}`);
244
+ }
245
+ }
246
+ const fallback = computeSafeClickPoint(rect, viewport);
247
+ return { x: fallback.x, y: fallback.y, probe: await probeClickTarget(fallback, rect) };
248
+ }
249
+ async function highlightClickPoint(point, durationMs = 4000) {
250
+ try {
251
+ await controllerAction('browser:execute', {
252
+ profile,
253
+ script: `(() => {
254
+ const p = ${JSON.stringify(point)};
255
+ let dot = document.getElementById('webauto-click-point');
256
+ if (!dot) {
257
+ dot = document.createElement('div');
258
+ dot.id = 'webauto-click-point';
259
+ dot.style.position = 'fixed';
260
+ dot.style.pointerEvents = 'none';
261
+ dot.style.zIndex = '2147483647';
262
+ dot.style.width = '14px';
263
+ dot.style.height = '14px';
264
+ dot.style.borderRadius = '999px';
265
+ dot.style.border = '3px solid #ff0033';
266
+ dot.style.background = 'rgba(255,0,51,0.15)';
267
+ dot.style.boxSizing = 'border-box';
268
+ dot.style.transform = 'translate(-50%, -50%)';
269
+ document.body.appendChild(dot);
270
+ }
271
+ dot.style.left = p.x + 'px';
272
+ dot.style.top = p.y + 'px';
273
+ setTimeout(() => {
274
+ const el = document.getElementById('webauto-click-point');
275
+ if (el && el.parentElement) el.parentElement.removeChild(el);
276
+ }, ${Math.max(500, Math.floor(durationMs))});
277
+ return true;
278
+ })()`,
279
+ });
280
+ }
281
+ catch {
282
+ // ignore
283
+ }
284
+ }
285
+ function isRectFullyVisible(rect, viewport, safe) {
286
+ const viewportW = typeof viewport.innerWidth === 'number' && viewport.innerWidth > 0 ? viewport.innerWidth : 0;
287
+ const viewportH = typeof viewport.innerHeight === 'number' && viewport.innerHeight > 0 ? viewport.innerHeight : 0;
288
+ if (!viewportH || !rect || rect.width <= 0 || rect.height <= 0)
289
+ return false;
290
+ const cx = rect.x + rect.width / 2;
291
+ const cy = rect.y + rect.height / 2;
292
+ const topOk = cy >= safe.top && cy <= viewportH - safe.bottom;
293
+ if (!viewportW)
294
+ return topOk;
295
+ const leftOk = cx >= safe.left && cx <= viewportW - safe.right;
296
+ return topOk && leftOk;
297
+ }
298
+ async function ensureCoverFullyVisible(params) {
299
+ const { rect: initialRect, viewport, containerId, expectedNoteId, domIndex, maxAttempts = 10, } = params;
300
+ // 安全边距:避免顶部 sticky tab/筛选条、底部悬浮层遮挡
301
+ const SAFE = { top: 180, bottom: 140, left: 24, right: 24 };
302
+ let rect = initialRect;
303
+ for (let i = 0; i < maxAttempts; i += 1) {
304
+ if (!rect)
305
+ return null;
306
+ const nid = typeof expectedNoteId === 'string' ? expectedNoteId.trim() : '';
307
+ const cardRect = nid ? await computeCardRectByNoteId(nid) : typeof domIndex === 'number' ? await computeCardRectByIndex(domIndex) : undefined;
308
+ const coverOk = isRectFullyVisible(rect, viewport, SAFE);
309
+ const cardOk = cardRect ? isRectFullyVisible(cardRect, viewport, SAFE) : true;
310
+ // ✅ 必须“封面 + 整个卡片”都完全可见才允许点击(禁止点击显示不全的 note item)
311
+ if (coverOk && cardOk)
312
+ return rect;
313
+ const targetRect = !cardOk && cardRect ? cardRect : rect;
314
+ const viewportH = typeof viewport.innerHeight === 'number' && viewport.innerHeight > 0 ? viewport.innerHeight : 1100;
315
+ const top = targetRect.y;
316
+ const bottom = targetRect.y + targetRect.height;
317
+ let direction = 'down';
318
+ let delta = 0;
319
+ if (top < SAFE.top) {
320
+ // rect 太靠上(可能被 sticky overlay 遮挡),向上滚动(让内容下移)
321
+ direction = 'up';
322
+ delta = SAFE.top - top + 160;
323
+ }
324
+ else if (bottom > viewportH - SAFE.bottom) {
325
+ // rect 太靠下,向下滚动(让内容上移)
326
+ direction = 'down';
327
+ delta = bottom - (viewportH - SAFE.bottom) + 160;
328
+ }
329
+ else {
330
+ // 理论上不会进入此分支(否则应该 fullyVisible),但兜底微调一下
331
+ direction = 'down';
332
+ delta = 260;
333
+ }
334
+ delta = Math.min(800, Math.max(220, Math.floor(delta)));
335
+ await saveDebugScreenshot('cover-rect-adjust-scroll', {
336
+ attempt: i + 1,
337
+ containerId,
338
+ rect,
339
+ cardRect: cardRect || null,
340
+ coverOk,
341
+ cardOk,
342
+ direction,
343
+ delta,
344
+ });
345
+ await viewportTools.scrollTowardVisibility(direction, delta, containerId).catch(() => false);
346
+ // 滚动后必须重新计算封面 rect(虚拟列表/重排)
347
+ if (nid) {
348
+ const r2 = await computeCoverRectByNoteId(nid);
349
+ rect = r2 || rect;
350
+ continue;
351
+ }
352
+ if (typeof domIndex === 'number') {
353
+ const r2 = await computeCoverRectByIndex(domIndex);
354
+ rect = r2 || rect;
355
+ continue;
356
+ }
357
+ }
358
+ // 超过尝试次数仍无法 fully-visible,返回 null 让上层 fail-fast
359
+ return null;
360
+ }
361
+ function sanitizeFilenamePart(value) {
362
+ return String(value || '')
363
+ .trim()
364
+ .replace(/[\\/:"*?<>|]+/g, '_')
365
+ .replace(/\s+/g, '_')
366
+ .slice(0, 80);
367
+ }
368
+ function extractBase64FromScreenshotResponse(raw) {
369
+ const v = raw?.data?.data ??
370
+ raw?.data?.body?.data ??
371
+ raw?.body?.data ??
372
+ raw?.result?.data ??
373
+ raw?.result ??
374
+ raw?.data ??
375
+ raw;
376
+ return typeof v === 'string' && v.length > 10 ? v : undefined;
377
+ }
378
+ async function saveDebugScreenshot(kind, meta) {
379
+ if (!debugArtifactsEnabled || !debugDir)
380
+ return {};
381
+ try {
382
+ await mkdir(debugDir, { recursive: true });
383
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
384
+ const notePart = sanitizeFilenamePart(expectedNoteId || '');
385
+ const idxPart = Number.isFinite(domIndex) ? `idx${domIndex}` : 'idxna';
386
+ const base = `${ts}-${kind}-${idxPart}${notePart ? `-${notePart}` : ''}`;
387
+ const pngPath = path.join(debugDir, `${base}.png`);
388
+ const jsonPath = path.join(debugDir, `${base}.json`);
389
+ // debug 截图:允许更长超时(10s 在某些场景会误触发 AbortSignal timeout)
390
+ const takeShot = async () => {
391
+ const resp = await fetch(controllerUrl, {
392
+ method: 'POST',
393
+ headers: { 'Content-Type': 'application/json' },
394
+ body: JSON.stringify({
395
+ action: 'browser:screenshot',
396
+ payload: { profileId: profile, fullPage: false },
397
+ }),
398
+ signal: AbortSignal.timeout ? AbortSignal.timeout(25000) : undefined,
399
+ });
400
+ if (!resp.ok)
401
+ throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
402
+ const data = await resp.json().catch(() => ({}));
403
+ return data.data || data;
404
+ };
405
+ let shot = null;
406
+ let shotError = null;
407
+ try {
408
+ shot = await takeShot();
409
+ }
410
+ catch {
411
+ // 再试一次(避免偶发超时导致缺少关键复盘截图)
412
+ try {
413
+ shot = await takeShot();
414
+ }
415
+ catch (e2) {
416
+ shot = null;
417
+ shotError = e2?.message || String(e2);
418
+ }
419
+ }
420
+ const b64 = extractBase64FromScreenshotResponse(shot);
421
+ if (b64) {
422
+ await writeFile(pngPath, Buffer.from(b64, 'base64'));
423
+ }
424
+ await writeFile(jsonPath, JSON.stringify({
425
+ ts,
426
+ kind,
427
+ sessionId: profile,
428
+ domIndex: Number.isFinite(domIndex) ? domIndex : null,
429
+ expectedNoteId: expectedNoteId || null,
430
+ expectedHref: expectedHref || null,
431
+ ...meta,
432
+ pngPath: b64 ? pngPath : null,
433
+ ...(shotError ? { screenshotError: shotError } : {}),
434
+ }, null, 2), 'utf-8');
435
+ console.log(`[OpenDetail][debug] saved ${kind}: ${pngPath}`);
436
+ return { pngPath: b64 ? pngPath : undefined, jsonPath };
437
+ }
438
+ catch (e) {
439
+ console.warn(`[OpenDetail][debug] save screenshot failed (${kind}): ${e?.message || String(e)}`);
440
+ return {};
441
+ }
442
+ }
443
+ function rectFromOperationRect(raw) {
444
+ if (!raw || typeof raw !== 'object')
445
+ return undefined;
446
+ if (typeof raw.x === 'number' &&
447
+ typeof raw.y === 'number' &&
448
+ typeof raw.width === 'number' &&
449
+ typeof raw.height === 'number') {
450
+ return { x: raw.x, y: raw.y, width: raw.width, height: raw.height };
451
+ }
452
+ if (typeof raw.x1 === 'number' &&
453
+ typeof raw.y1 === 'number' &&
454
+ typeof raw.x2 === 'number' &&
455
+ typeof raw.y2 === 'number') {
456
+ return { x: raw.x1, y: raw.y1, width: raw.x2 - raw.x1, height: raw.y2 - raw.y1 };
457
+ }
458
+ if (typeof raw.left === 'number' &&
459
+ typeof raw.top === 'number' &&
460
+ typeof raw.right === 'number' &&
461
+ typeof raw.bottom === 'number') {
462
+ return { x: raw.left, y: raw.top, width: raw.right - raw.left, height: raw.bottom - raw.top };
463
+ }
464
+ return undefined;
465
+ }
466
+ try {
467
+ const { highlightContainer, getContainerRect } = await import('./helpers/anchorVerify.js');
468
+ const startUrl = await getCurrentUrl();
469
+ console.log(`[OpenDetail] Start URL: ${startUrl}`);
470
+ // 0. 点击前:严格要求点击封面区域(避免点到作者/广告)
471
+ const normalizedExpectedNoteId = typeof expectedNoteId === 'string' ? expectedNoteId.trim() : '';
472
+ const normalizedExpectedHref = typeof expectedHref === 'string' ? expectedHref.trim() : '';
473
+ const normalizeHrefForCompare = (raw) => {
474
+ const href = String(raw || '').trim();
475
+ if (!href)
476
+ return '';
477
+ try {
478
+ const u = new URL(href, 'https://www.xiaohongshu.com');
479
+ // xsec_source 在不同上下文下可能缺失/不同,不能作为“点错”的判据
480
+ u.searchParams.delete('xsec_source');
481
+ // 参数排序,避免同值不同序导致误判
482
+ const entries = Array.from(u.searchParams.entries()).sort(([a], [b]) => a.localeCompare(b));
483
+ const p = new URL('https://www.xiaohongshu.com' + u.pathname);
484
+ for (const [k, v] of entries)
485
+ p.searchParams.append(k, v);
486
+ return `${p.pathname}${p.search || ''}`;
487
+ }
488
+ catch {
489
+ // fallback:仅做最小归一化(去掉 xsec_source)
490
+ return href.replace(/([?&])xsec_source=[^&]*(&|$)/i, '$1').replace(/[?&]$/, '');
491
+ }
492
+ };
493
+ // 0.0 优先:如果上游已经给了视口内 Rect,直接使用(最稳:不依赖 domIndex/selector)
494
+ if (clickRect &&
495
+ typeof clickRect.x === 'number' &&
496
+ typeof clickRect.y === 'number' &&
497
+ typeof clickRect.width === 'number' &&
498
+ typeof clickRect.height === 'number' &&
499
+ clickRect.width > 0 &&
500
+ clickRect.height > 0) {
501
+ clickedItemRect = clickRect;
502
+ }
503
+ // 0.2 点击必须发生在封面(a.cover)内
504
+ const coverByNoteId = normalizedExpectedNoteId ? await computeCoverRectByNoteId(normalizedExpectedNoteId) : undefined;
505
+ if (coverByNoteId) {
506
+ clickedItemRect = coverByNoteId;
507
+ }
508
+ else if (!clickedItemRect && typeof domIndex === 'number') {
509
+ const coverByIndex = await computeCoverRectByIndex(domIndex);
510
+ if (coverByIndex) {
511
+ clickedItemRect = coverByIndex;
512
+ }
513
+ }
514
+ if (!clickedItemRect) {
515
+ await saveDebugScreenshot('cover-rect-not-found', {
516
+ url: startUrl,
517
+ containerId,
518
+ domIndex: typeof domIndex === 'number' ? domIndex : null,
519
+ expectedNoteId: normalizedExpectedNoteId || null,
520
+ expectedHref: typeof expectedHref === 'string' ? expectedHref : null,
521
+ });
522
+ pushStep({
523
+ id: 'verify_result_item_anchor',
524
+ status: 'failed',
525
+ anchor: { containerId, clickedItemRect: undefined, verified: false },
526
+ error: 'cover_rect_not_found',
527
+ });
528
+ return {
529
+ success: false,
530
+ detailReady: false,
531
+ entryAnchor: undefined,
532
+ exitAnchor: undefined,
533
+ steps,
534
+ anchor: {
535
+ clickedItemContainerId: containerId,
536
+ clickedItemRect: undefined,
537
+ detailContainerId: undefined,
538
+ detailRect: undefined,
539
+ verified: false,
540
+ },
541
+ error: 'cover_rect_not_found',
542
+ };
543
+ }
544
+ if (clickedItemRect) {
545
+ const viewport = await getViewportMetrics();
546
+ const ensuredCover = await ensureCoverFullyVisible({
547
+ rect: clickedItemRect,
548
+ viewport,
549
+ containerId,
550
+ expectedNoteId: normalizedExpectedNoteId || undefined,
551
+ domIndex: typeof domIndex === 'number' ? domIndex : undefined,
552
+ });
553
+ if (!ensuredCover) {
554
+ await saveDebugScreenshot('cover-rect-not-fully-visible', {
555
+ url: startUrl,
556
+ containerId,
557
+ clickedItemRect,
558
+ viewport,
559
+ });
560
+ pushStep({
561
+ id: 'verify_result_item_anchor',
562
+ status: 'failed',
563
+ anchor: { containerId, clickedItemRect, verified: false },
564
+ error: 'cover_rect_not_fully_visible',
565
+ });
566
+ return {
567
+ success: false,
568
+ detailReady: false,
569
+ entryAnchor: undefined,
570
+ exitAnchor: undefined,
571
+ steps,
572
+ anchor: {
573
+ clickedItemContainerId: containerId,
574
+ clickedItemRect,
575
+ detailContainerId: undefined,
576
+ detailRect: undefined,
577
+ verified: false,
578
+ },
579
+ error: 'cover_rect_not_fully_visible',
580
+ };
581
+ }
582
+ clickedItemRect = ensuredCover;
583
+ // ✅ 每次点击前必须可视化确认:先高亮容器,再高亮封面 Rect
584
+ try {
585
+ if (typeof domIndex === 'number' && Number.isFinite(domIndex)) {
586
+ await controllerAction('container:operation', {
587
+ containerId,
588
+ operationId: 'highlight',
589
+ sessionId: profile,
590
+ config: { index: domIndex, style: '3px solid #00ff00', duration: 1200 },
591
+ });
592
+ }
593
+ else {
594
+ await controllerAction('container:operation', {
595
+ containerId,
596
+ operationId: 'highlight',
597
+ sessionId: profile,
598
+ config: { style: '3px solid #00ff00', duration: 1200 },
599
+ });
600
+ }
601
+ }
602
+ catch {
603
+ // ignore highlight failures (debug only)
604
+ }
605
+ try {
606
+ await highlightRect(clickedItemRect, 1200, '#00ff00');
607
+ }
608
+ catch {
609
+ // ignore
610
+ }
611
+ await new Promise((r) => setTimeout(r, 650));
612
+ const chosen = await chooseSafeClickPoint(clickedItemRect, viewport);
613
+ const okCover = Boolean(chosen.probe?.inCover) || (await isPointInsideCover({ x: chosen.x, y: chosen.y }));
614
+ const unsafeProfile = Boolean(chosen.probe?.isUserProfile);
615
+ const unsafeHashtag = Boolean(chosen.probe?.isHashtag);
616
+ const unsafeQuery = Boolean(chosen.probe?.inQueryNoteWrapper);
617
+ const unsafeSearchKeywordLink = Boolean(chosen.probe?.isSearchKeywordLink) && !unsafeHashtag;
618
+ const unsafeHashText = typeof chosen.probe?.textSnippet === 'string' && chosen.probe.textSnippet.includes('#');
619
+ const outOfBounds = Boolean(chosen.probe?.outOfBounds);
620
+ const overlapsAvatar = Boolean(chosen.probe?.overlapsAvatar);
621
+ const isImage = Boolean(chosen.probe?.isImage);
622
+ const cardTruncated = Boolean(chosen.probe?.cardTruncated);
623
+ if (!okCover ||
624
+ unsafeProfile ||
625
+ unsafeHashtag ||
626
+ unsafeQuery ||
627
+ unsafeSearchKeywordLink ||
628
+ unsafeHashText ||
629
+ outOfBounds ||
630
+ overlapsAvatar ||
631
+ isImage ||
632
+ cardTruncated) {
633
+ await saveDebugScreenshot('click-point-not-in-cover', {
634
+ url: startUrl,
635
+ containerId,
636
+ clickedItemRect,
637
+ probe: { x: chosen.x, y: chosen.y, ...chosen.probe },
638
+ });
639
+ pushStep({
640
+ id: 'verify_result_item_anchor',
641
+ status: 'failed',
642
+ anchor: { containerId, clickedItemRect, verified: false },
643
+ error: outOfBounds
644
+ ? 'click_point_out_of_bounds'
645
+ : overlapsAvatar
646
+ ? 'click_point_overlaps_avatar'
647
+ : !okCover
648
+ ? 'click_point_not_in_cover'
649
+ : unsafeProfile
650
+ ? 'click_point_hits_user_profile'
651
+ : unsafeHashtag
652
+ ? 'click_point_hits_hashtag'
653
+ : unsafeQuery
654
+ ? 'click_point_hits_query_note_wrapper'
655
+ : unsafeSearchKeywordLink
656
+ ? 'click_point_hits_search_keyword_link'
657
+ : 'click_point_hits_hash_text',
658
+ meta: { probe: { x: chosen.x, y: chosen.y, ...chosen.probe } },
659
+ });
660
+ return {
661
+ success: false,
662
+ detailReady: false,
663
+ entryAnchor: undefined,
664
+ exitAnchor: undefined,
665
+ steps,
666
+ anchor: {
667
+ clickedItemContainerId: containerId,
668
+ clickedItemRect,
669
+ detailContainerId: undefined,
670
+ detailRect: undefined,
671
+ verified: false,
672
+ },
673
+ error: outOfBounds
674
+ ? 'click_point_out_of_bounds'
675
+ : overlapsAvatar
676
+ ? 'click_point_overlaps_avatar'
677
+ : !okCover
678
+ ? 'click_point_not_in_cover'
679
+ : unsafeProfile
680
+ ? 'click_point_hits_user_profile'
681
+ : unsafeHashtag
682
+ ? 'click_point_hits_hashtag'
683
+ : unsafeQuery
684
+ ? 'click_point_hits_query_note_wrapper'
685
+ : unsafeSearchKeywordLink
686
+ ? 'click_point_hits_search_keyword_link'
687
+ : 'click_point_hits_hash_text',
688
+ };
689
+ }
690
+ // 若 probe 到的 href 能解析出 noteId,则必须与 expectedNoteId 严格一致(否则直接判定为“点错卡片”)
691
+ if (normalizedExpectedNoteId && chosen.probe?.closestHref) {
692
+ const href = String(chosen.probe.closestHref || '');
693
+ if (/[#]|%23/i.test(href)) {
694
+ await saveDebugScreenshot('click-point-hashtag-href', {
695
+ url: startUrl,
696
+ containerId,
697
+ clickedItemRect,
698
+ probe: { x: chosen.x, y: chosen.y, ...chosen.probe },
699
+ });
700
+ pushStep({
701
+ id: 'verify_result_item_anchor',
702
+ status: 'failed',
703
+ anchor: { containerId, clickedItemRect, verified: false },
704
+ error: 'click_point_hits_hashtag',
705
+ meta: { href, probe: { x: chosen.x, y: chosen.y, ...chosen.probe } },
706
+ });
707
+ return {
708
+ success: false,
709
+ detailReady: false,
710
+ entryAnchor: undefined,
711
+ exitAnchor: undefined,
712
+ steps,
713
+ anchor: {
714
+ clickedItemContainerId: containerId,
715
+ clickedItemRect,
716
+ detailContainerId: undefined,
717
+ detailRect: undefined,
718
+ verified: false,
719
+ },
720
+ error: 'click_point_hits_hashtag',
721
+ };
722
+ }
723
+ }
724
+ // 若 probe 到的 href 能解析出 noteId,则必须与 expectedNoteId 严格一致(否则直接判定为“点错卡片”)
725
+ if (normalizedExpectedNoteId && chosen.probe?.closestHref) {
726
+ const href = String(chosen.probe.closestHref || '');
727
+ const m = href.match(/\/(?:explore|search_result)\/([0-9a-z]+)/i);
728
+ const probedNoteId = m ? String(m[1] || '') : '';
729
+ if (probedNoteId && probedNoteId !== normalizedExpectedNoteId) {
730
+ await saveDebugScreenshot('click-point-noteid-mismatch', {
731
+ url: startUrl,
732
+ containerId,
733
+ clickedItemRect,
734
+ probe: { x: chosen.x, y: chosen.y, ...chosen.probe },
735
+ expectedNoteId: normalizedExpectedNoteId,
736
+ probedNoteId,
737
+ });
738
+ pushStep({
739
+ id: 'verify_result_item_anchor',
740
+ status: 'failed',
741
+ anchor: { containerId, clickedItemRect, verified: false },
742
+ error: 'click_point_noteid_mismatch',
743
+ meta: { expectedNoteId: normalizedExpectedNoteId, probedNoteId, href },
744
+ });
745
+ return {
746
+ success: false,
747
+ detailReady: false,
748
+ entryAnchor: undefined,
749
+ exitAnchor: undefined,
750
+ steps,
751
+ anchor: {
752
+ clickedItemContainerId: containerId,
753
+ clickedItemRect,
754
+ detailContainerId: undefined,
755
+ detailRect: undefined,
756
+ verified: false,
757
+ },
758
+ error: 'click_point_noteid_mismatch',
759
+ };
760
+ }
761
+ }
762
+ if (normalizedExpectedHref && chosen.probe?.closestHref) {
763
+ const href = String(chosen.probe.closestHref || '');
764
+ // 如果已经验证过 noteId,则允许 href query 差异(例如 xsec_source 的不同/缺失)
765
+ const m = href.match(/\/(?:explore|search_result)\/([0-9a-z]+)/i);
766
+ const probedNoteId = m ? String(m[1] || '') : '';
767
+ if (normalizedExpectedNoteId) {
768
+ // expectedNoteId 存在但 probe 的 href 不是 note 链接:直接判定为点错(例如点到用户/话题/相关搜索)
769
+ if (!probedNoteId) {
770
+ await saveDebugScreenshot('click-point-href-not-note', {
771
+ url: startUrl,
772
+ containerId,
773
+ clickedItemRect,
774
+ probe: { x: chosen.x, y: chosen.y, ...chosen.probe },
775
+ expectedNoteId: normalizedExpectedNoteId,
776
+ expectedHref: normalizedExpectedHref || null,
777
+ probedHref: href,
778
+ });
779
+ pushStep({
780
+ id: 'verify_result_item_anchor',
781
+ status: 'failed',
782
+ anchor: { containerId, clickedItemRect, verified: false },
783
+ error: 'click_point_href_not_note',
784
+ meta: { expectedNoteId: normalizedExpectedNoteId, expectedHref: normalizedExpectedHref, probedHref: href },
785
+ });
786
+ return {
787
+ success: false,
788
+ detailReady: false,
789
+ entryAnchor: undefined,
790
+ exitAnchor: undefined,
791
+ steps,
792
+ anchor: {
793
+ clickedItemContainerId: containerId,
794
+ clickedItemRect,
795
+ detailContainerId: undefined,
796
+ detailRect: undefined,
797
+ verified: false,
798
+ },
799
+ error: 'click_point_href_not_note',
800
+ };
801
+ }
802
+ }
803
+ else if (href && normalizeHrefForCompare(href) !== normalizeHrefForCompare(normalizedExpectedHref)) {
804
+ await saveDebugScreenshot('click-point-href-mismatch', {
805
+ url: startUrl,
806
+ containerId,
807
+ clickedItemRect,
808
+ probe: { x: chosen.x, y: chosen.y, ...chosen.probe },
809
+ expectedHref: normalizedExpectedHref,
810
+ probedHref: href,
811
+ });
812
+ pushStep({
813
+ id: 'verify_result_item_anchor',
814
+ status: 'failed',
815
+ anchor: { containerId, clickedItemRect, verified: false },
816
+ error: 'click_point_href_mismatch',
817
+ meta: {
818
+ expectedHref: normalizedExpectedHref,
819
+ probedHref: href,
820
+ expectedNorm: normalizeHrefForCompare(normalizedExpectedHref),
821
+ probedNorm: normalizeHrefForCompare(href),
822
+ },
823
+ });
824
+ return {
825
+ success: false,
826
+ detailReady: false,
827
+ entryAnchor: undefined,
828
+ exitAnchor: undefined,
829
+ steps,
830
+ anchor: {
831
+ clickedItemContainerId: containerId,
832
+ clickedItemRect,
833
+ detailContainerId: undefined,
834
+ detailRect: undefined,
835
+ verified: false,
836
+ },
837
+ error: 'click_point_href_mismatch',
838
+ };
839
+ }
840
+ }
841
+ }
842
+ // 0.1 入口锚点验证
843
+ const viewport = await getViewportMetrics();
844
+ const entryInViewport = Boolean(clickedItemRect &&
845
+ clickedItemRect.width > 0 &&
846
+ clickedItemRect.height > 0 &&
847
+ (viewport.innerHeight ? clickedItemRect.y >= 0 && clickedItemRect.y + clickedItemRect.height <= viewport.innerHeight : true));
848
+ if (entryInViewport) {
849
+ entryAnchor = {
850
+ containerId,
851
+ clickedItemRect,
852
+ verified: true,
853
+ };
854
+ console.log('[OpenDetail][entryAnchor]', JSON.stringify(entryAnchor, null, 2));
855
+ pushStep({
856
+ id: 'verify_result_item_anchor',
857
+ status: 'success',
858
+ anchor: {
859
+ containerId,
860
+ clickedItemRect,
861
+ verified: true,
862
+ },
863
+ });
864
+ }
865
+ else {
866
+ entryAnchor = {
867
+ containerId,
868
+ clickedItemRect,
869
+ verified: false,
870
+ };
871
+ console.warn('[OpenDetail] clickedItemRect missing or invalid, aborting detail open');
872
+ pushStep({
873
+ id: 'verify_result_item_anchor',
874
+ status: 'failed',
875
+ anchor: {
876
+ containerId,
877
+ clickedItemRect,
878
+ verified: false,
879
+ },
880
+ error: 'invalid_or_missing_clickedItemRect',
881
+ });
882
+ return {
883
+ success: false,
884
+ detailReady: false,
885
+ entryAnchor,
886
+ exitAnchor: undefined,
887
+ steps,
888
+ anchor: {
889
+ clickedItemContainerId: containerId,
890
+ clickedItemRect,
891
+ detailContainerId: undefined,
892
+ detailRect: undefined,
893
+ verified: false,
894
+ },
895
+ error: 'Result item anchor not ready (offscreen or invalid rect)',
896
+ };
897
+ }
898
+ // 1. 打开详情
899
+ try {
900
+ // ✅ 每次点击前先高亮目标(容器 + 封面 Rect),并等待 1-2 秒便于肉眼确认与截图复盘
901
+ try {
902
+ if (typeof domIndex === 'number' && Number.isFinite(domIndex)) {
903
+ await controllerAction('container:operation', {
904
+ containerId,
905
+ operationId: 'highlight',
906
+ sessionId: profile,
907
+ config: { index: domIndex, style: '3px solid #00ff00', duration: 2200 },
908
+ });
909
+ }
910
+ else {
911
+ await controllerAction('container:operation', {
912
+ containerId,
913
+ operationId: 'highlight',
914
+ sessionId: profile,
915
+ config: { style: '3px solid #00ff00', duration: 2200 },
916
+ });
917
+ }
918
+ }
919
+ catch {
920
+ // ignore
921
+ }
922
+ await highlightRect(clickedItemRect, 2200, '#00ff00').catch(() => { });
923
+ await new Promise((r) => setTimeout(r, 900));
924
+ const chosen = await chooseSafeClickPoint(clickedItemRect, viewport);
925
+ const x = chosen.x;
926
+ const y = chosen.y;
927
+ // 记录点击前页面列表,用于诊断“误点打开新 tab”
928
+ let pagesBefore = null;
929
+ try {
930
+ const r = await controllerAction('browser:page:list', { profileId: profile });
931
+ pagesBefore = r?.data ?? r;
932
+ }
933
+ catch {
934
+ pagesBefore = null;
935
+ }
936
+ await highlightClickPoint({ x, y }, 2600);
937
+ await new Promise((r) => setTimeout(r, 650));
938
+ await saveDebugScreenshot('pre-click', {
939
+ url: startUrl,
940
+ clickedItemRect,
941
+ clickPoint: { x, y },
942
+ clickTarget: chosen.probe,
943
+ pagesBefore,
944
+ });
945
+ await new Promise((r) => setTimeout(r, 280));
946
+ const clickResp = await controllerAction('container:operation', {
947
+ containerId,
948
+ operationId: 'click',
949
+ config: { x, y },
950
+ sessionId: profile,
951
+ });
952
+ const clickOk = Boolean(clickResp?.success ?? clickResp?.data?.success);
953
+ if (!clickOk) {
954
+ await saveDebugScreenshot('click-failed', { url: startUrl, clickedItemRect, clickPoint: { x, y }, clickResp });
955
+ return {
956
+ success: false,
957
+ detailReady: false,
958
+ pageDelta: undefined,
959
+ entryAnchor,
960
+ exitAnchor: undefined,
961
+ steps,
962
+ anchor: {
963
+ clickedItemContainerId: containerId,
964
+ clickedItemRect,
965
+ detailContainerId: undefined,
966
+ detailRect: undefined,
967
+ verified: false,
968
+ },
969
+ error: 'container_click_failed',
970
+ };
971
+ }
972
+ // 点击后:检查是否意外打开了新 tab(常见于点到用户主页/推荐词)
973
+ let pagesAfter = null;
974
+ try {
975
+ const r = await controllerAction('browser:page:list', { profileId: profile });
976
+ pagesAfter = r?.data ?? r;
977
+ }
978
+ catch {
979
+ pagesAfter = null;
980
+ }
981
+ const normalizeList = (raw) => {
982
+ const pagesRaw = Array.isArray(raw?.pages)
983
+ ? raw.pages
984
+ : Array.isArray(raw?.data?.pages)
985
+ ? raw.data.pages
986
+ : [];
987
+ const activeIndex = Number(raw?.activeIndex ?? raw?.data?.activeIndex ?? 0) || 0;
988
+ const pages = pagesRaw
989
+ .map((p) => {
990
+ const obj = (p && typeof p === 'object') ? p : {};
991
+ return {
992
+ index: Number(obj?.index ?? 0),
993
+ url: String(obj?.url ?? ''),
994
+ active: Boolean(obj?.active),
995
+ };
996
+ })
997
+ .filter((p) => Number.isFinite(p.index));
998
+ return { pages, activeIndex, count: pages.length };
999
+ };
1000
+ const before = pagesBefore ? normalizeList(pagesBefore) : null;
1001
+ const after = pagesAfter ? normalizeList(pagesAfter) : null;
1002
+ const beforeUrls = new Set((before?.pages ?? []).map((p) => `${p.index}:${p.url}`));
1003
+ const newPages = after && before
1004
+ ? after.pages
1005
+ .filter((p) => !beforeUrls.has(`${p.index}:${p.url}`))
1006
+ .map((p) => ({ index: p.index, url: p.url }))
1007
+ : [];
1008
+ const openedNewTab = before && after ? after.count > before.count : false;
1009
+ if (openedNewTab) {
1010
+ await saveDebugScreenshot('new-tab-opened', {
1011
+ url: startUrl,
1012
+ clickedItemRect,
1013
+ clickPoint: { x, y },
1014
+ clickTarget: chosen.probe,
1015
+ pagesBefore: before,
1016
+ pagesAfter: after,
1017
+ newPages,
1018
+ });
1019
+ return {
1020
+ success: false,
1021
+ detailReady: false,
1022
+ pageDelta: {
1023
+ before: before || undefined,
1024
+ after: after || undefined,
1025
+ newPages,
1026
+ },
1027
+ entryAnchor,
1028
+ exitAnchor: undefined,
1029
+ steps,
1030
+ anchor: {
1031
+ clickedItemContainerId: containerId,
1032
+ clickedItemRect,
1033
+ detailContainerId: undefined,
1034
+ detailRect: undefined,
1035
+ verified: false,
1036
+ },
1037
+ error: 'unexpected_new_tab_opened',
1038
+ };
1039
+ }
1040
+ pushStep({
1041
+ id: 'system_click_detail_item',
1042
+ status: 'success',
1043
+ anchor: {
1044
+ containerId,
1045
+ clickedItemRect,
1046
+ verified: true,
1047
+ },
1048
+ meta: { via: 'container:operation click(xy)' },
1049
+ });
1050
+ }
1051
+ catch (e) {
1052
+ console.warn('[OpenDetail] system click threw error:', e.message || e);
1053
+ return {
1054
+ success: false,
1055
+ detailReady: false,
1056
+ entryAnchor,
1057
+ exitAnchor: undefined,
1058
+ steps,
1059
+ anchor: {
1060
+ clickedItemContainerId: containerId,
1061
+ clickedItemRect,
1062
+ detailContainerId: undefined,
1063
+ detailRect: undefined,
1064
+ verified: false,
1065
+ },
1066
+ error: `System click threw error: ${e.message || String(e)}`,
1067
+ };
1068
+ }
1069
+ await new Promise((r) => setTimeout(r, 3000));
1070
+ let midUrl = await getCurrentUrl();
1071
+ console.log(`[OpenDetail] Post-click URL: ${midUrl}`);
1072
+ // 若误点进入个人页:直接失败(不做任何兜底/重试)
1073
+ if (midUrl.includes('/user/profile') && clickedItemRect) {
1074
+ console.warn('[OpenDetail] Detected navigation to user profile (misclick), stopping');
1075
+ await saveDebugScreenshot('misclick-user-profile', { url: midUrl, clickedItemRect });
1076
+ return {
1077
+ success: false,
1078
+ detailReady: false,
1079
+ entryAnchor,
1080
+ exitAnchor: undefined,
1081
+ steps,
1082
+ anchor: {
1083
+ clickedItemContainerId: containerId,
1084
+ clickedItemRect,
1085
+ detailContainerId: undefined,
1086
+ detailRect: undefined,
1087
+ verified: false,
1088
+ },
1089
+ error: 'clicked_user_profile',
1090
+ };
1091
+ }
1092
+ // 2. 等待详情模态出现
1093
+ let detailState = await waitForDetail(waiterDeps);
1094
+ let detailReady = detailState.ready;
1095
+ if (detailReady &&
1096
+ normalizedExpectedNoteId &&
1097
+ detailState.noteId &&
1098
+ detailState.noteId !== normalizedExpectedNoteId) {
1099
+ pushStep({
1100
+ id: 'wait_detail_dom_ready',
1101
+ status: 'failed',
1102
+ error: 'opened_unexpected_note',
1103
+ anchor: {
1104
+ containerId,
1105
+ clickedItemRect,
1106
+ verified: false,
1107
+ },
1108
+ meta: {
1109
+ expectedNoteId: normalizedExpectedNoteId,
1110
+ openedNoteId: detailState.noteId,
1111
+ safeDetailUrl: detailState.safeUrl || null,
1112
+ },
1113
+ });
1114
+ return {
1115
+ success: false,
1116
+ detailReady: false,
1117
+ entryAnchor,
1118
+ exitAnchor: undefined,
1119
+ steps,
1120
+ anchor: {
1121
+ clickedItemContainerId: containerId,
1122
+ clickedItemRect,
1123
+ detailContainerId: undefined,
1124
+ detailRect: undefined,
1125
+ verified: false,
1126
+ },
1127
+ error: `Opened unexpected noteId: expected=${normalizedExpectedNoteId}, got=${detailState.noteId}`,
1128
+ };
1129
+ }
1130
+ if (!detailReady) {
1131
+ console.warn('[OpenDetail] detail not ready after system click, dumping viewport diagnostics for analysis');
1132
+ await saveDebugScreenshot('detail-not-ready', { url: midUrl, clickedItemRect });
1133
+ await dumpViewportDiagnostics();
1134
+ }
1135
+ pushStep({
1136
+ id: 'wait_detail_dom_ready',
1137
+ status: detailReady ? 'success' : 'failed',
1138
+ anchor: {
1139
+ containerId,
1140
+ clickedItemRect,
1141
+ verified: detailReady,
1142
+ },
1143
+ meta: {
1144
+ safeDetailUrl: detailState.safeUrl || null,
1145
+ noteId: detailState.noteId || null,
1146
+ url: midUrl,
1147
+ },
1148
+ error: detailReady ? undefined : 'detail_not_ready',
1149
+ });
1150
+ // 3. 详情出现后,对 modal_shell 做锚点高亮 + Rect 回环
1151
+ let detailContainerId;
1152
+ let detailRect;
1153
+ let verified = false;
1154
+ if (detailReady) {
1155
+ try {
1156
+ const { verifyAnchorByContainerId } = await import('./helpers/containerAnchors.js');
1157
+ const candidateIds = ['xiaohongshu_detail.modal_shell', 'xiaohongshu_detail'];
1158
+ for (const cid of candidateIds) {
1159
+ const anchor = await verifyAnchorByContainerId(cid, profile, serviceUrl, '3px solid #ff4444', 2000);
1160
+ if (!anchor.found || !anchor.rect) {
1161
+ continue;
1162
+ }
1163
+ detailContainerId = cid;
1164
+ detailRect = anchor.rect;
1165
+ console.log(`[OpenDetail] Detail container rect: ${JSON.stringify(detailRect)}`);
1166
+ verified =
1167
+ detailRect.width > 400 &&
1168
+ detailRect.height > 400 &&
1169
+ detailRect.y < 200;
1170
+ break;
1171
+ }
1172
+ if (!detailContainerId) {
1173
+ console.warn('[OpenDetail] Detail anchor verify failed: no modal_shell/detail container visible');
1174
+ }
1175
+ }
1176
+ catch (e) {
1177
+ console.warn(`[OpenDetail] Detail anchor verify error: ${e.message}`);
1178
+ }
1179
+ }
1180
+ if (detailContainerId && detailRect) {
1181
+ exitAnchor = {
1182
+ containerId: detailContainerId,
1183
+ detailRect,
1184
+ verified,
1185
+ };
1186
+ console.log('[OpenDetail][exitAnchor]', JSON.stringify(exitAnchor, null, 2));
1187
+ pushStep({
1188
+ id: 'verify_detail_anchor',
1189
+ status: verified ? 'success' : 'success',
1190
+ anchor: {
1191
+ containerId: detailContainerId,
1192
+ detailRect,
1193
+ verified,
1194
+ },
1195
+ meta: {
1196
+ safeDetailUrl: detailState.safeUrl || null,
1197
+ noteId: detailState.noteId || null,
1198
+ },
1199
+ });
1200
+ }
1201
+ else {
1202
+ pushStep({
1203
+ id: 'verify_detail_anchor',
1204
+ status: 'failed',
1205
+ anchor: detailContainerId
1206
+ ? {
1207
+ containerId: detailContainerId,
1208
+ detailRect,
1209
+ verified: false,
1210
+ }
1211
+ : undefined,
1212
+ error: 'detail_anchor_not_found',
1213
+ });
1214
+ }
1215
+ return {
1216
+ success: true,
1217
+ detailReady,
1218
+ entryAnchor,
1219
+ exitAnchor,
1220
+ steps,
1221
+ safeDetailUrl: detailState.safeUrl,
1222
+ noteId: detailState.noteId,
1223
+ anchor: {
1224
+ clickedItemContainerId: containerId,
1225
+ clickedItemRect,
1226
+ detailContainerId,
1227
+ detailRect,
1228
+ verified
1229
+ }
1230
+ };
1231
+ }
1232
+ catch (error) {
1233
+ return {
1234
+ success: false,
1235
+ detailReady: false,
1236
+ error: `OpenDetail failed: ${error.message}`
1237
+ };
1238
+ }
1239
+ }
1240
+ //# sourceMappingURL=OpenDetailBlock.js.map