@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,1032 @@
1
+ /**
2
+ * Workflow Block: ExpandCommentsBlock
3
+ *
4
+ * 展开评论并提取评论列表
5
+ */
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { promises as fs } from 'node:fs';
9
+ import { locateCommentSection } from './helpers/commentSectionLocator.js';
10
+ import { getScrollStats, getViewport, systemMouseWheel } from './helpers/commentScroller.js';
11
+ import { expandRepliesInView } from './helpers/replyExpander.js';
12
+ import { createExpandCommentsControllerClient } from './helpers/expandCommentsController.js';
13
+ import { buildExtractCommentsScript, mergeExtractedComments } from './helpers/expandCommentsExtractor.js';
14
+ import { assertNoCaptcha, systemClickAt, systemHoverAt, isDevMode } from './helpers/systemInput.js';
15
+ import { getCommentEndState, getCommentStats, getScrollContainerInfo, getScrollTargetInfo, isInputFocused, locateCommentsFocusPoint } from './helpers/xhsCommentDom.js';
16
+ import { isDebugArtifactsEnabled } from './helpers/debugArtifacts.js';
17
+ // 调试截图保存目录
18
+ const DEBUG_ENABLED = isDebugArtifactsEnabled();
19
+ const DEBUG_SCREENSHOT_DIR = DEBUG_ENABLED
20
+ ? path.join(os.homedir(), '.webauto', 'logs', 'debug-screenshots')
21
+ : '';
22
+ /**
23
+ * 保存调试截图
24
+ */
25
+ async function saveDebugScreenshot(kind, sessionId, meta = {}) {
26
+ if (!DEBUG_ENABLED)
27
+ return {};
28
+ try {
29
+ await fs.mkdir(DEBUG_SCREENSHOT_DIR, { recursive: true });
30
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
31
+ const base = `${ts}-${kind}-${sessionId}`;
32
+ const pngPath = path.join(DEBUG_SCREENSHOT_DIR, `${base}.png`);
33
+ const jsonPath = path.join(DEBUG_SCREENSHOT_DIR, `${base}.json`);
34
+ const controllerUrl = 'http://127.0.0.1:7701/v1/controller/action';
35
+ // 截图
36
+ async function takeShot() {
37
+ const response = await fetch(controllerUrl, {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify({
41
+ action: 'browser:screenshot',
42
+ payload: { profileId: sessionId, fullPage: false },
43
+ }),
44
+ });
45
+ if (!response.ok)
46
+ throw new Error(`HTTP ${response.status}`);
47
+ const data = await response.json().catch(() => ({}));
48
+ return data.data || data;
49
+ }
50
+ let shot = null;
51
+ try {
52
+ shot = await takeShot();
53
+ }
54
+ catch {
55
+ try {
56
+ shot = await takeShot();
57
+ }
58
+ catch {
59
+ shot = null;
60
+ }
61
+ }
62
+ // 提取 base64
63
+ const b64 = shot?.data?.data ??
64
+ shot?.data?.body?.data ??
65
+ shot?.body?.data ??
66
+ shot?.result?.data ??
67
+ shot?.result ??
68
+ shot?.data ??
69
+ shot;
70
+ if (typeof b64 === 'string' && b64.length > 10) {
71
+ await fs.writeFile(pngPath, Buffer.from(b64, 'base64'));
72
+ }
73
+ // 保存元数据
74
+ await fs.writeFile(jsonPath, JSON.stringify({ ts, kind, sessionId, ...meta, pngPath: b64 ? pngPath : null }, null, 2), 'utf-8');
75
+ console.log(`[ExpandComments][debug] saved ${kind}: ${pngPath}`);
76
+ return { pngPath: b64 ? pngPath : undefined, jsonPath };
77
+ }
78
+ catch {
79
+ return {};
80
+ }
81
+ }
82
+ /**
83
+ * 展开评论并提取列表
84
+ *
85
+ * @param input - 输入参数
86
+ * @returns Promise<ExpandCommentsOutput>
87
+ */
88
+ export async function execute(input) {
89
+ const { sessionId,
90
+ // 默认按“抓完”为目标:滚动到评论底部(或空评论)才结束
91
+ maxRounds = 240, expectedTotal = null, maxNewComments, seedSeenKeys, startFromTop = true, ensureLatestTab = true, serviceUrl = 'http://127.0.0.1:7701' } = input;
92
+ const profile = sessionId;
93
+ const controllerUrl = `${serviceUrl}/v1/controller/action`;
94
+ const controllerClient = createExpandCommentsControllerClient({ profile, controllerUrl });
95
+ const { controllerAction, getCurrentUrl } = controllerClient;
96
+ const browserServiceUrl = process.env.WEBAUTO_BROWSER_SERVICE_URL || 'http://127.0.0.1:7704';
97
+ const browserWsUrl = process.env.WEBAUTO_BROWSER_WS_URL || 'ws://127.0.0.1:8765';
98
+ const commentSectionId = 'xiaohongshu_detail.comment_section';
99
+ const commentButtonId = 'xiaohongshu_detail.comment_button';
100
+ const showMoreContainerId = 'xiaohongshu_detail.comment_section.show_more_button';
101
+ const commentItemContainerId = 'xiaohongshu_detail.comment_section.comment_item';
102
+ const emptyStateContainerId = 'xiaohongshu_detail.comment_section.empty_state';
103
+ const endMarkerContainerId = 'xiaohongshu_detail.comment_section.end_marker';
104
+ const logPrefix = '[ExpandComments]';
105
+ const warn = (msg) => console.warn(`${logPrefix} ${msg}`);
106
+ const log = (msg) => console.log(`${logPrefix} ${msg}`);
107
+ const clamp = (n, min, max) => Math.min(Math.max(n, min), max);
108
+ const viewport0 = await getViewport(controllerUrl, profile);
109
+ const viewportW = viewport0.innerWidth || 1440;
110
+ const viewportH = viewport0.innerHeight || 900;
111
+ // 默认将滚动焦点放在右侧区域(详情页左侧通常是大图,点到会触发媒体查看器/风控)
112
+ let preferredFocusPoint = { x: Math.floor(viewportW * 0.75), y: Math.floor(viewportH * 0.6) };
113
+ // Tab 监控:记录初始状态
114
+ const initialTabs = await controllerAction('browser:page:list', { profileId: profile })
115
+ .then((r) => r?.pages || [])
116
+ .catch(() => []);
117
+ const tryActivateCommentsUi = async (reason) => {
118
+ try {
119
+ const result = await locateCommentSection({
120
+ profile,
121
+ serviceUrl,
122
+ controllerUrl,
123
+ commentSectionContainerId: commentSectionId,
124
+ commentButtonContainerId: commentButtonId,
125
+ canClickCommentButton: true,
126
+ highlightStyle: '2px solid #ffaa00',
127
+ highlightMs: 1200,
128
+ });
129
+ if (!result.found) {
130
+ // 强制按 comment_button 作为激活
131
+ await locateCommentSection({
132
+ profile,
133
+ serviceUrl,
134
+ controllerUrl,
135
+ commentSectionContainerId: commentSectionId,
136
+ commentButtonContainerId: commentButtonId,
137
+ canClickCommentButton: true,
138
+ highlightStyle: '2px solid #ffaa00',
139
+ highlightMs: 1200,
140
+ });
141
+ }
142
+ log(`activated comments (${reason})`);
143
+ }
144
+ catch {
145
+ // ignore
146
+ }
147
+ };
148
+ const systemWheel = async (deltaY, focusPoint, context = 'expand_comments_scroll') => {
149
+ await systemMouseWheel({
150
+ profileId: profile,
151
+ deltaY,
152
+ focusPoint,
153
+ browserServiceUrl,
154
+ browserWsUrl,
155
+ context,
156
+ });
157
+ };
158
+ const scrollTarget = async (rootSelectors) => {
159
+ let t = null;
160
+ try {
161
+ t = await getScrollTargetInfo(rootSelectors, controllerUrl, profile);
162
+ }
163
+ catch {
164
+ t = null;
165
+ }
166
+ if (t && Number.isFinite(t.x) && Number.isFinite(t.y)) {
167
+ return t;
168
+ }
169
+ const s = await getScrollStats(rootSelectors, controllerUrl, profile);
170
+ return {
171
+ ...s,
172
+ x: Math.floor(preferredFocusPoint.x),
173
+ y: Math.floor(preferredFocusPoint.y),
174
+ };
175
+ };
176
+ const locateRectBySelectors = async (selectors) => {
177
+ const filtered = selectors.filter((sel) => typeof sel === 'string' && sel.trim().length > 0);
178
+ if (!filtered.length)
179
+ return null;
180
+ try {
181
+ const response = await controllerAction('browser:execute', {
182
+ profile,
183
+ script: `(() => {
184
+ const selectors = ${JSON.stringify(filtered)};
185
+ for (const sel of selectors) {
186
+ try {
187
+ const el = document.querySelector(sel);
188
+ if (!el) continue;
189
+ const rect = el.getBoundingClientRect();
190
+ if (!rect || !rect.width || !rect.height) continue;
191
+ return { x: rect.left, y: rect.top, width: rect.width, height: rect.height, selector: sel };
192
+ } catch (_) {}
193
+ }
194
+ return null;
195
+ })()`,
196
+ });
197
+ const payload = response?.result || response?.data?.result || response;
198
+ if (payload &&
199
+ ['x', 'y', 'width', 'height'].every((key) => typeof payload[key] === 'number' && Number.isFinite(payload[key]))) {
200
+ return {
201
+ x: Number(payload.x),
202
+ y: Number(payload.y),
203
+ width: Number(payload.width),
204
+ height: Number(payload.height),
205
+ };
206
+ }
207
+ }
208
+ catch (err) {
209
+ warn(`locateRectBySelectors error: ${err?.message || err}`);
210
+ }
211
+ return null;
212
+ };
213
+ const findContainer = (tree, pattern) => {
214
+ if (!tree)
215
+ return null;
216
+ if (pattern.test(tree.id || tree.defId || ''))
217
+ return tree;
218
+ if (Array.isArray(tree.children)) {
219
+ for (const child of tree.children) {
220
+ const found = findContainer(child, pattern);
221
+ if (found)
222
+ return found;
223
+ }
224
+ }
225
+ return null;
226
+ };
227
+ const collectContainers = (tree, pattern, result = []) => {
228
+ if (!tree)
229
+ return result;
230
+ if (pattern.test(tree.id || tree.defId || ''))
231
+ result.push(tree);
232
+ if (Array.isArray(tree.children)) {
233
+ for (const child of tree.children)
234
+ collectContainers(child, pattern, result);
235
+ }
236
+ return result;
237
+ };
238
+ const resolveEndMarkerRectViaSelectors = async (primarySelector) => {
239
+ const selectors = [];
240
+ if (primarySelector)
241
+ selectors.push(primarySelector);
242
+ selectors.push('.comment-end', '.comments-end', '.comment-list .end');
243
+ return locateRectBySelectors(selectors);
244
+ };
245
+ const readEndMarkerTextBySelector = async (primarySelector) => {
246
+ const selectors = [];
247
+ if (primarySelector)
248
+ selectors.push(primarySelector);
249
+ selectors.push('.end-container', '.comment-footer', '.comment-end', '.comments-end');
250
+ try {
251
+ const res = await controllerAction('browser:execute', {
252
+ profile,
253
+ script: `(() => {
254
+ const selectors = ${JSON.stringify(selectors)};
255
+ for (const sel of selectors) {
256
+ try {
257
+ const el = document.querySelector(sel);
258
+ if (!el) continue;
259
+ const t = (el.textContent || '').trim();
260
+ if (t) return t.slice(0, 120);
261
+ } catch (_) {}
262
+ }
263
+ return null;
264
+ })()`,
265
+ });
266
+ const payload = res?.result ?? res?.data?.result ?? res;
267
+ return typeof payload === 'string' ? payload : null;
268
+ }
269
+ catch {
270
+ return null;
271
+ }
272
+ };
273
+ const trySelectLatestTab = async () => {
274
+ try {
275
+ const res = await controllerAction('browser:execute', {
276
+ profile,
277
+ script: `(() => {
278
+ const roots = [
279
+ document.querySelector('.comments-el'),
280
+ document.querySelector('.comment-list'),
281
+ document.querySelector('.comments-container'),
282
+ document.querySelector('[class*="comment-section"]'),
283
+ ].filter(Boolean);
284
+ const root = roots[0] || document;
285
+
286
+ const isVisible = (r) => r && r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < (window.innerHeight || 0);
287
+ const candidates = Array.from(root.querySelectorAll('button, a, div, span'))
288
+ .map((el) => {
289
+ const t = (el.textContent || '').trim();
290
+ if (t !== '最新' && t !== '最新评论') return null;
291
+ const r = el.getBoundingClientRect();
292
+ if (!isVisible(r)) return null;
293
+ const cls = (el.getAttribute('class') || '') + ' ' + (el.parentElement ? (el.parentElement.getAttribute('class') || '') : '');
294
+ const aria = el.getAttribute('aria-selected');
295
+ const active = aria === 'true' || /active|selected|current/.test(cls);
296
+ return { x: r.left, y: r.top, width: r.width, height: r.height, text: t, active };
297
+ })
298
+ .filter(Boolean);
299
+ if (!candidates.length) return null;
300
+ candidates.sort((a, b) => (a.text === '最新' ? -1 : 1));
301
+ return candidates[0];
302
+ })()`,
303
+ });
304
+ const payload = res?.result ?? res?.data?.result ?? res;
305
+ if (!payload || typeof payload.x !== 'number' || typeof payload.y !== 'number')
306
+ return false;
307
+ if (payload.active)
308
+ return true;
309
+ const cx = Math.floor(payload.x + payload.width / 2);
310
+ const cy = Math.floor(payload.y + payload.height / 2);
311
+ await systemClickAt(profile, cx, cy, browserServiceUrl, 'select_latest_tab');
312
+ await new Promise((r) => setTimeout(r, 900));
313
+ return true;
314
+ }
315
+ catch {
316
+ return false;
317
+ }
318
+ };
319
+ const buildFocusPoint = (rect) => {
320
+ if (!rect)
321
+ return { x: Math.floor(viewportW / 2), y: Math.floor(viewportH / 2) };
322
+ return {
323
+ // 详情页评论区内部可能包含图片;为避免任何“坐标点击”误落在图片上,
324
+ // focus 点尽量靠近评论区右侧(更接近滚动条区域),用于 hover/wheel 对齐滚动目标。
325
+ x: clamp(Math.floor(rect.x + rect.width - 12), 30, viewportW - 30),
326
+ y: clamp(Math.floor(rect.y + Math.max(12, rect.height * 0.25)), 160, viewportH - 160),
327
+ };
328
+ };
329
+ // 切 tab 后滚动焦点经常丢失:每个 batch 以及“滚不动恢复”前都要重新定位可滚动容器中心点并 hover/click 以恢复滚动目标。
330
+ const refreshScrollFocus = async (reason) => {
331
+ try {
332
+ let domFocus = null;
333
+ try {
334
+ domFocus = await locateCommentsFocusPoint(controllerUrl, profile);
335
+ }
336
+ catch {
337
+ domFocus = null;
338
+ }
339
+ if (domFocus && Number.isFinite(domFocus.x) && Number.isFinite(domFocus.y)) {
340
+ // 评论区可能包含图片:focus/click 坐标尽量靠右(接近滚动条区域),避免误点图片触发媒体查看器/风控
341
+ await systemHoverAt(profile, domFocus.x, domFocus.y, browserServiceUrl);
342
+ await new Promise((r) => setTimeout(r, 120));
343
+ // 输入框聚焦会导致滚轮无效:仅在确实聚焦 input 时才 click(否则只 hover,避免误点触发媒体/附件面板)
344
+ if (await isInputFocused(controllerUrl, profile)) {
345
+ await systemClickAt(profile, domFocus.x, domFocus.y, browserServiceUrl, `comment_blur_input:${reason}`);
346
+ await new Promise((r) => setTimeout(r, 350));
347
+ }
348
+ return domFocus;
349
+ }
350
+ let info = null;
351
+ try {
352
+ info = await getScrollContainerInfo(controllerUrl, profile);
353
+ }
354
+ catch {
355
+ info = null;
356
+ }
357
+ if (info && Number.isFinite(info.x) && Number.isFinite(info.y)) {
358
+ // 同样靠右,避免点到评论图片
359
+ const fx = clamp(Math.floor(preferredFocusPoint.x), 30, viewportW - 30);
360
+ const fy = clamp(Math.floor(info.y), 160, viewportH - 160);
361
+ await systemHoverAt(profile, fx, fy, browserServiceUrl);
362
+ await new Promise((r) => setTimeout(r, 120));
363
+ if (await isInputFocused(controllerUrl, profile)) {
364
+ await systemClickAt(profile, fx, fy, browserServiceUrl, `comment_blur_input:${reason}`);
365
+ await new Promise((r) => setTimeout(r, 350));
366
+ }
367
+ return { x: fx, y: fy };
368
+ }
369
+ }
370
+ catch {
371
+ // ignore
372
+ }
373
+ return null;
374
+ };
375
+ const extractCommentsOnce = async (params) => {
376
+ const script = buildExtractCommentsScript({
377
+ rootSelectors: params.rootSelectors,
378
+ itemSelector: params.itemSelector,
379
+ extractors: params.extractors,
380
+ });
381
+ const domResult = await controllerAction('browser:execute', { profile, script });
382
+ const payload = domResult?.result || domResult?.data?.result || domResult;
383
+ if (!payload?.found || !Array.isArray(payload.comments))
384
+ return;
385
+ mergeExtractedComments({
386
+ rawList: payload.comments,
387
+ seenKeys: params.seenKeys,
388
+ out: params.out,
389
+ maxOut: params.maxOut ?? null,
390
+ });
391
+ };
392
+ const stripExtractorDefs = (extractors) => {
393
+ const fields = {};
394
+ for (const [field, def] of Object.entries(extractors || {})) {
395
+ const selectors = Array.isArray(def?.selectors) ? def.selectors : [];
396
+ if (!selectors.length)
397
+ continue;
398
+ fields[field] = {
399
+ selectors,
400
+ attr: def?.attr,
401
+ };
402
+ }
403
+ return fields;
404
+ };
405
+ const safeGetPrimarySelectorById = async (getPrimarySelectorByContainerId, id) => {
406
+ try {
407
+ return await getPrimarySelectorByContainerId(id);
408
+ }
409
+ catch (err) {
410
+ warn(`getPrimarySelectorByContainerId error (${id}): ${err?.message || err}`);
411
+ return null;
412
+ }
413
+ };
414
+ const normalizeRect = (raw) => {
415
+ if (!raw || typeof raw !== 'object')
416
+ return null;
417
+ if (typeof raw.x === 'number' && typeof raw.y === 'number' && typeof raw.width === 'number' && typeof raw.height === 'number') {
418
+ return { x: raw.x, y: raw.y, width: raw.width, height: raw.height };
419
+ }
420
+ return null;
421
+ };
422
+ // 以下旧的内联实现将被后续段落删除/替换
423
+ try {
424
+ const { verifyAnchorByContainerId, getPrimarySelectorByContainerId, getContainerExtractorsById } = await import('./helpers/containerAnchors.js');
425
+ // 1. 锚定评论区根容器(只做一次高亮 + Rect 回环)
426
+ let commentSectionRect;
427
+ let commentSectionLocated = false;
428
+ let lastAnchorError = null;
429
+ let totalFromHeader = null;
430
+ try {
431
+ const anchor = await locateCommentSection({
432
+ profile,
433
+ serviceUrl,
434
+ controllerUrl,
435
+ commentSectionContainerId: commentSectionId,
436
+ commentButtonContainerId: commentButtonId,
437
+ canClickCommentButton: true,
438
+ });
439
+ if (anchor.found && anchor.rect) {
440
+ commentSectionRect = anchor.rect;
441
+ commentSectionLocated = true;
442
+ preferredFocusPoint = buildFocusPoint(anchor.rect);
443
+ log(`comment_section rect: ${JSON.stringify(anchor.rect)}`);
444
+ }
445
+ else {
446
+ lastAnchorError = anchor.error || 'not found';
447
+ }
448
+ }
449
+ catch (e) {
450
+ lastAnchorError = e.message || String(e);
451
+ }
452
+ if (!commentSectionLocated) {
453
+ if (lastAnchorError) {
454
+ warn(`comment_section anchor verify failed: ${lastAnchorError}`);
455
+ }
456
+ return {
457
+ success: false,
458
+ comments: [],
459
+ reachedEnd: false,
460
+ emptyState: false,
461
+ totalFromHeader: null,
462
+ anchor: {
463
+ commentSectionContainerId: commentSectionId,
464
+ commentSectionRect: undefined,
465
+ },
466
+ error: lastAnchorError || 'comment_section anchor not found',
467
+ };
468
+ }
469
+ // 2. 读取评论“标称数量”(header total),用于后续校验与落盘展示
470
+ try {
471
+ const stats0 = await getCommentStats(controllerUrl, profile);
472
+ if (typeof stats0?.total === 'number' && Number.isFinite(stats0.total) && stats0.total >= 0) {
473
+ totalFromHeader = stats0.total;
474
+ }
475
+ // 一些页面需要先激活评论区才会出现计数
476
+ if (totalFromHeader === null) {
477
+ await tryActivateCommentsUi('header_total_probe');
478
+ await new Promise((r) => setTimeout(r, 700));
479
+ const stats1 = await getCommentStats(controllerUrl, profile);
480
+ if (typeof stats1?.total === 'number' && Number.isFinite(stats1.total) && stats1.total >= 0) {
481
+ totalFromHeader = stats1.total;
482
+ }
483
+ }
484
+ log(`comment headerTotal=${totalFromHeader === null ? 'null' : String(totalFromHeader)}`);
485
+ }
486
+ catch {
487
+ totalFromHeader = null;
488
+ }
489
+ // 2. 重新 inspect 评论区域,供锚点与终止标记 / 样本评论锚点使用
490
+ const currentUrl = await getCurrentUrl();
491
+ if (!currentUrl)
492
+ throw new Error('无法确定当前页面 URL,ExpandComments 需要在详情页内运行');
493
+ const inspected = await controllerAction('containers:inspect-container', {
494
+ profile,
495
+ containerId: commentSectionId,
496
+ url: currentUrl,
497
+ maxChildren: 200
498
+ });
499
+ const effectiveTree = inspected.snapshot?.container_tree || inspected.container_tree || { id: commentSectionId };
500
+ const emptyStateNode = findContainer(effectiveTree, /xiaohongshu_detail\.comment_section\.empty_state$/);
501
+ const commentNodes = collectContainers(effectiveTree, /xiaohongshu_detail\.comment_section\.comment_item$/);
502
+ // 3.0 锚点兜底:无评论 + 命中 empty_state
503
+ if (commentNodes.length === 0) {
504
+ if (emptyStateNode?.id) {
505
+ let emptyRect;
506
+ try {
507
+ const anchor = await verifyAnchorByContainerId(emptyStateNode.id, profile, serviceUrl, '2px dashed #888888', 2000);
508
+ if (anchor.found && anchor.rect) {
509
+ emptyRect = anchor.rect;
510
+ log(`empty_state rect: ${JSON.stringify(anchor.rect)}`);
511
+ }
512
+ }
513
+ catch {
514
+ // ignore
515
+ }
516
+ if (emptyRect && emptyRect.height > 0) {
517
+ return {
518
+ success: true,
519
+ comments: [],
520
+ reachedEnd: true,
521
+ emptyState: true,
522
+ totalFromHeader,
523
+ anchor: {
524
+ commentSectionContainerId: commentSectionId,
525
+ commentSectionRect,
526
+ endMarkerContainerId: emptyStateNode.id,
527
+ endMarkerRect: emptyRect,
528
+ verified: Boolean(commentSectionRect),
529
+ },
530
+ };
531
+ }
532
+ }
533
+ warn('no comment_item or empty_state anchors found, aborting expand');
534
+ return {
535
+ success: false,
536
+ comments: [],
537
+ reachedEnd: false,
538
+ emptyState: false,
539
+ totalFromHeader,
540
+ anchor: {
541
+ commentSectionContainerId: commentSectionId,
542
+ commentSectionRect,
543
+ },
544
+ error: 'comment_item & empty_state anchors not found',
545
+ };
546
+ }
547
+ // 3.1 样本评论高亮
548
+ let sampleCommentRect;
549
+ let sampleCommentContainerId;
550
+ if (commentNodes.length > 0) {
551
+ const sample = commentNodes[0];
552
+ if (sample?.id) {
553
+ sampleCommentContainerId = sample.id;
554
+ try {
555
+ const anchor = await verifyAnchorByContainerId(sample.id, profile, serviceUrl, '2px solid #00ff00', 2000);
556
+ if (anchor.found && anchor.rect) {
557
+ sampleCommentRect = anchor.rect;
558
+ log(`sample comment rect: ${JSON.stringify(anchor.rect)}`);
559
+ }
560
+ else {
561
+ const primarySelector = await safeGetPrimarySelectorById(getPrimarySelectorByContainerId, sample.id);
562
+ const fallbackSelectors = [
563
+ primarySelector || '',
564
+ '.comments-el .comment-item',
565
+ '.comment-list .comment-item',
566
+ '.comments-container .comment-item',
567
+ '.comment-item',
568
+ ];
569
+ const fallbackRect = await locateRectBySelectors(fallbackSelectors);
570
+ if (fallbackRect)
571
+ sampleCommentRect = fallbackRect;
572
+ }
573
+ }
574
+ catch {
575
+ // ignore
576
+ }
577
+ }
578
+ }
579
+ // 评论项选择器:必须覆盖“主评论 + 回复/子评论”以便与 headerTotal 对齐。
580
+ // 注意:这里只用于只读抽取(browser:execute),不会用于点击。
581
+ const primaryItemSelector = (await safeGetPrimarySelectorById(getPrimarySelectorByContainerId, commentItemContainerId)) || '';
582
+ const itemSelector = Array.from(new Set([
583
+ primaryItemSelector,
584
+ '.comment-item',
585
+ '[class*="comment-item"]',
586
+ // 回复/子评论(不同版本 class 可能不同)
587
+ "[class*='reply-item']",
588
+ "[class*='replyItem']",
589
+ "[class*='sub-comment']",
590
+ "[class*='subComment']",
591
+ ].filter((s) => typeof s === 'string' && s.trim()))).join(', ');
592
+ const rawExtractors = await getContainerExtractorsById(commentItemContainerId);
593
+ const extractors = stripExtractorDefs(rawExtractors);
594
+ const rootSelectors = [
595
+ (await safeGetPrimarySelectorById(getPrimarySelectorByContainerId, commentSectionId)) || '',
596
+ '.comments-el',
597
+ '.comment-list',
598
+ '.comments-container',
599
+ '[class*="comment-section"]',
600
+ ].filter((s) => s && typeof s === 'string');
601
+ // 仅在评论区域内寻找滚动容器:不要把 html/body 放进根选择器,
602
+ // 否则 getScrollTargetInfo 可能选中整页滚动容器,导致 focus 点落到左侧图片区域,触发安全保护。
603
+ const scrollRootSelectors = rootSelectors.length > 0
604
+ ? Array.from(new Set([...rootSelectors]))
605
+ : ['.comments-el', '.comment-list', '.comments-container', '[class*="comment-section"]'];
606
+ let focusPoint = buildFocusPoint(commentSectionRect);
607
+ if (ensureLatestTab) {
608
+ await trySelectLatestTab();
609
+ }
610
+ // batch 开始时先恢复滚动焦点,避免切 tab 后 wheel 失效
611
+ const focused = await refreshScrollFocus('batch_start');
612
+ if (focused)
613
+ focusPoint = focused;
614
+ // 滚动回顶部(仅在该 tab 第一次抓取时执行,避免重复回顶导致“永远只抽到前 50 条”)
615
+ if (startFromTop) {
616
+ const end0 = await getCommentEndState(controllerUrl, profile).catch(() => null);
617
+ if (end0?.emptyStateVisible) {
618
+ log('empty_state visible before scroll_to_top; skip scroll_to_top');
619
+ }
620
+ else {
621
+ try {
622
+ // scroll_to_top 前对齐滚动目标(仅 hover,不做额外 click,避免误点到图片导致 fail-fast)
623
+ const t0 = await scrollTarget(scrollRootSelectors);
624
+ if (t0?.found && Number.isFinite(t0.x) && Number.isFinite(t0.y)) {
625
+ focusPoint = { x: clamp(Math.floor(t0.x), 30, viewportW - 30), y: clamp(Math.floor(t0.y), 160, viewportH - 160) };
626
+ }
627
+ await systemHoverAt(profile, focusPoint.x, focusPoint.y, browserServiceUrl).catch(() => { });
628
+ await new Promise((r) => setTimeout(r, 120));
629
+ for (let i = 0; i < 24; i += 1) {
630
+ const s = await scrollTarget(scrollRootSelectors);
631
+ if (!s.found || s.atTop)
632
+ break;
633
+ focusPoint = { x: clamp(Math.floor(s.x), 30, viewportW - 30), y: clamp(Math.floor(s.y), 160, viewportH - 160) };
634
+ await systemWheel(-(360 + Math.floor(Math.random() * 220)), focusPoint);
635
+ await new Promise((r) => setTimeout(r, 450 + Math.random() * 350));
636
+ }
637
+ }
638
+ catch (e) {
639
+ const msg = String(e?.message || e || '');
640
+ if (msg.includes('captcha_modal_detected')) {
641
+ throw e;
642
+ }
643
+ if (msg.includes('unsafe_click_image_in_detail')) {
644
+ if (isDevMode())
645
+ throw e;
646
+ warn(`scroll_to_top skipped unsafe click (continue): ${msg}`);
647
+ }
648
+ // 非关键错误不影响后续抽取:保留日志即可
649
+ warn(`scroll_to_top ignored error: ${msg}`);
650
+ }
651
+ }
652
+ }
653
+ const comments = [];
654
+ const seenKeys = new Set(Array.isArray(seedSeenKeys) ? seedSeenKeys.filter((k) => typeof k === 'string') : []);
655
+ const maxNew = typeof maxNewComments === 'number' && Number.isFinite(maxNewComments) && maxNewComments > 0
656
+ ? Math.floor(maxNewComments)
657
+ : null;
658
+ let stoppedByMaxNew = false;
659
+ const showMoreSelector = await safeGetPrimarySelectorById(getPrimarySelectorByContainerId, showMoreContainerId);
660
+ // 循环抽取
661
+ let noEffectStreak = 0;
662
+ let lastScrollTop = null;
663
+ let recoveries = 0;
664
+ let emptyDetectTries = 0;
665
+ for (let round = 0; round < maxRounds; round += 1) {
666
+ // 风控/验证码:出现“请通过验证”弹窗必须立刻停止(不要继续滚动/点击/抽取)
667
+ await assertNoCaptcha(profile, 'expand_comments_round');
668
+ try {
669
+ // 先展开视口内回复(只读查找 + 系统点击)
670
+ await expandRepliesInView({
671
+ controllerUrl,
672
+ profile,
673
+ browserServiceUrl,
674
+ // 详情页同屏可能有多个“展开更多”按钮;这里要尽量展开干净,避免漏掉可见回复
675
+ // 每次点击都会重算目标坐标,避免布局变化导致点偏
676
+ // 开发阶段优先保证“点击 100% 正确”,避免一次性连续点击多个按钮导致误触
677
+ maxTargets: 3,
678
+ recomputeEachClick: true,
679
+ focusPoint,
680
+ showMoreContainerId,
681
+ showMoreSelector,
682
+ logPrefix,
683
+ round,
684
+ });
685
+ // 提取当前视口内评论
686
+ await extractCommentsOnce({
687
+ rootSelectors,
688
+ itemSelector,
689
+ extractors,
690
+ seenKeys,
691
+ out: comments,
692
+ maxOut: maxNew,
693
+ });
694
+ }
695
+ catch (e) {
696
+ const msg = String(e?.message || e || '');
697
+ if (msg.includes('captcha_modal_detected')) {
698
+ throw e;
699
+ }
700
+ if (msg.includes('unsafe_click_image_in_detail')) {
701
+ if (isDevMode())
702
+ throw e;
703
+ warn(`round=${round} expand skipped unsafe click (continue): ${msg}`);
704
+ }
705
+ warn(`round=${round} extract/expand error: ${msg}`);
706
+ }
707
+ // 多 tab 渐进式:本次新增达到上限就停止(留在当前位置,下一次切回该 tab 继续)
708
+ if (maxNew && comments.length >= maxNew) {
709
+ stoppedByMaxNew = true;
710
+ break;
711
+ }
712
+ // 空评论兜底:如果没有抽到任何 comment_item,优先用 empty_state 容器确认
713
+ if (comments.length === 0) {
714
+ try {
715
+ const emptyAnchor = await verifyAnchorByContainerId(emptyStateContainerId, profile, serviceUrl, '2px dashed #9c27b0', 900);
716
+ if (emptyAnchor?.found && emptyAnchor.rect) {
717
+ return {
718
+ success: true,
719
+ comments: [],
720
+ reachedEnd: true,
721
+ emptyState: true,
722
+ totalFromHeader,
723
+ anchor: {
724
+ commentSectionContainerId: commentSectionId,
725
+ commentSectionRect,
726
+ endMarkerContainerId: emptyStateContainerId,
727
+ endMarkerRect: emptyAnchor.rect,
728
+ verified: Boolean(commentSectionRect),
729
+ },
730
+ };
731
+ }
732
+ }
733
+ catch {
734
+ // ignore
735
+ }
736
+ if (emptyDetectTries < 2) {
737
+ emptyDetectTries += 1;
738
+ await tryActivateCommentsUi(`no_comments_try_${emptyDetectTries}`);
739
+ await new Promise((r) => setTimeout(r, 900));
740
+ }
741
+ }
742
+ const stats = await scrollTarget(scrollRootSelectors);
743
+ // 对齐滚动目标:确保 wheel 落在“实际可滚动容器”上(避免 focus 在别处导致 scrollTop 永远不变)
744
+ if (stats?.found && Number.isFinite(stats.x) && Number.isFinite(stats.y)) {
745
+ focusPoint = {
746
+ x: clamp(Math.floor(stats.x), 30, viewportW - 30),
747
+ y: clamp(Math.floor(stats.y), 160, viewportH - 160),
748
+ };
749
+ }
750
+ const endState = await getCommentEndState(controllerUrl, profile);
751
+ if (round % 20 === 0) {
752
+ log(`round=${round} comments=${comments.length} scrollTop=${stats.scrollTop}/${stats.scrollHeight} endMarker=${endState.endMarkerVisible} empty=${endState.emptyStateVisible}`);
753
+ }
754
+ // 终止条件:严格仅以 end_marker / empty_state 为准
755
+ // 但 end_marker 可见时,仍可能存在“可见但未展开”的回复按钮(导致 headerTotal 覆盖率不足)。
756
+ // 因此:若 end_marker 可见,先做一次“只展开不滚动”的 sweep;若 sweep 后 end_marker 仍可见且无可展开,则结束。
757
+ if (endState.emptyStateVisible) {
758
+ break;
759
+ }
760
+ if (endState.endMarkerVisible) {
761
+ let expandedAny = false;
762
+ for (let sweep = 0; sweep < 4; sweep += 1) {
763
+ const exp = await expandRepliesInView({
764
+ controllerUrl,
765
+ profile,
766
+ browserServiceUrl,
767
+ maxTargets: 3,
768
+ recomputeEachClick: true,
769
+ focusPoint,
770
+ showMoreContainerId,
771
+ showMoreSelector,
772
+ logPrefix,
773
+ round: round,
774
+ });
775
+ if (exp.clicked > 0)
776
+ expandedAny = true;
777
+ // 展开后立刻抽一次,确保新增回复进入增量集合
778
+ try {
779
+ await extractCommentsOnce({
780
+ rootSelectors,
781
+ itemSelector,
782
+ extractors,
783
+ seenKeys,
784
+ out: comments,
785
+ maxOut: maxNew,
786
+ });
787
+ }
788
+ catch {
789
+ // ignore
790
+ }
791
+ if (!exp.clicked)
792
+ break;
793
+ await new Promise((r) => setTimeout(r, 650 + Math.random() * 450));
794
+ }
795
+ const endAfterSweep = await getCommentEndState(controllerUrl, profile);
796
+ log(`end_marker visible: sweep expandedAny=${expandedAny} endAfterSweep=${endAfterSweep.endMarkerVisible} emptyAfterSweep=${endAfterSweep.emptyStateVisible} comments=${comments.length}`);
797
+ if (!expandedAny && endAfterSweep.endMarkerVisible) {
798
+ break;
799
+ }
800
+ // sweep 可能使列表变长(end_marker 暂时消失),继续下一轮滚动以触达真实底部
801
+ await new Promise((r) => setTimeout(r, 650 + Math.random() * 450));
802
+ continue;
803
+ }
804
+ if (stats.found) {
805
+ if (lastScrollTop !== null && Math.abs(stats.scrollTop - lastScrollTop) < 2) {
806
+ noEffectStreak += 1;
807
+ }
808
+ else {
809
+ noEffectStreak = 0;
810
+ }
811
+ lastScrollTop = stats.scrollTop;
812
+ }
813
+ else {
814
+ noEffectStreak += 1;
815
+ }
816
+ if (noEffectStreak >= 2) {
817
+ recoveries += 1;
818
+ warn(`scroll stuck (streak=${noEffectStreak}), recovery #${recoveries}: rollback then down`);
819
+ try {
820
+ const focused3 = await refreshScrollFocus(`scroll_recovery_${recoveries}`);
821
+ if (focused3)
822
+ focusPoint = focused3;
823
+ // 恢复滚动:按规则仅做“回滚几次再向下滚”的 wheel 操作,不做额外 click(评论区可能含图片,click 误触会触发媒体查看器/风控)
824
+ await systemHoverAt(profile, focusPoint.x, focusPoint.y, browserServiceUrl).catch(() => { });
825
+ }
826
+ catch (e) {
827
+ const msg = String(e?.message || e || '');
828
+ if (msg.includes('captcha_modal_detected'))
829
+ throw e;
830
+ if (msg.includes('unsafe_click_image_in_detail')) {
831
+ if (isDevMode())
832
+ throw e;
833
+ warn(`scroll_recovery skipped unsafe click (continue): ${msg}`);
834
+ }
835
+ }
836
+ // 回滚(向上)2 次,再向下 3 次
837
+ for (let k = 0; k < 2; k += 1) {
838
+ try {
839
+ await systemWheel(-(320 + Math.floor(Math.random() * 160)), focusPoint, 'expand_scroll_recovery_up');
840
+ }
841
+ catch (e) {
842
+ const msg = String(e?.message || e || '');
843
+ if (msg.includes('captcha_modal_detected'))
844
+ throw e;
845
+ }
846
+ await new Promise((r) => setTimeout(r, 500 + Math.random() * 400));
847
+ }
848
+ for (let k = 0; k < 3; k += 1) {
849
+ try {
850
+ await systemWheel(540 + Math.floor(Math.random() * 220), focusPoint, 'expand_scroll_recovery_down');
851
+ }
852
+ catch (e) {
853
+ const msg = String(e?.message || e || '');
854
+ if (msg.includes('captcha_modal_detected'))
855
+ throw e;
856
+ }
857
+ await new Promise((r) => setTimeout(r, 600 + Math.random() * 450));
858
+ }
859
+ noEffectStreak = 0;
860
+ // 避免无穷恢复:按约定最多尝试 3 次“回滚再向下”
861
+ if (recoveries >= 3)
862
+ break;
863
+ }
864
+ const deltaY = 520 + Math.floor(Math.random() * 260);
865
+ try {
866
+ // 每次向下滚前都 hover 一次,确保 wheel 落在可滚动区域(切回 tab 时尤其重要)
867
+ await systemHoverAt(profile, focusPoint.x, focusPoint.y, browserServiceUrl).catch(() => { });
868
+ await systemWheel(deltaY, focusPoint, 'expand_scroll');
869
+ }
870
+ catch (e) {
871
+ const msg = String(e?.message || e || '');
872
+ if (msg.includes('captcha_modal_detected'))
873
+ throw e;
874
+ warn(`systemWheel failed: ${msg}`);
875
+ }
876
+ await new Promise((r) => setTimeout(r, 650 + Math.random() * 650));
877
+ }
878
+ // 补齐最后一屏:
879
+ // - 单 tab“抓完”模式需要补齐
880
+ // - 多 tab “maxNewComments” 分批模式不补齐,避免一次超过上限
881
+ if (!stoppedByMaxNew) {
882
+ try {
883
+ await extractCommentsOnce({
884
+ rootSelectors,
885
+ itemSelector,
886
+ extractors,
887
+ seenKeys,
888
+ out: comments,
889
+ maxOut: maxNew,
890
+ });
891
+ }
892
+ catch {
893
+ // ignore
894
+ }
895
+ }
896
+ // 多 tab 渐进式:达到本批上限就直接返回(不做 end_marker 判定,避免“刚好 50 条且 end_marker 恰好可见”导致误判为完成)
897
+ if (stoppedByMaxNew) {
898
+ let verified = false;
899
+ if (commentSectionRect) {
900
+ const sectionOk = commentSectionRect.height > 0;
901
+ const sampleOk = comments.length > 0 ? !!(sampleCommentRect && sampleCommentRect.height > 0) : true;
902
+ verified = sectionOk && sampleOk;
903
+ }
904
+ return {
905
+ success: true,
906
+ comments,
907
+ reachedEnd: false,
908
+ emptyState: false,
909
+ totalFromHeader,
910
+ stoppedByMaxNew,
911
+ anchor: {
912
+ commentSectionContainerId: commentSectionId,
913
+ commentSectionRect,
914
+ sampleCommentContainerId,
915
+ sampleCommentRect,
916
+ verified,
917
+ },
918
+ };
919
+ }
920
+ // 4. 检查终止条件(严格:end_marker / empty_state)
921
+ const endMarker = findContainer(effectiveTree, /xiaohongshu_detail\.comment_section\.end_marker$/);
922
+ let endMarkerRect;
923
+ let endMarkerContainerId;
924
+ let endMarkerHit = false;
925
+ let emptyStateHit = false;
926
+ let endMarkerText;
927
+ if (endMarker?.id && comments.length > 0) {
928
+ endMarkerContainerId = endMarker.id;
929
+ try {
930
+ const primarySelector = await safeGetPrimarySelectorById(getPrimarySelectorByContainerId, endMarker.id);
931
+ const anchor = await verifyAnchorByContainerId(endMarker.id, profile, serviceUrl, '2px solid #ff8c00', 2000);
932
+ if (anchor.found && anchor.rect) {
933
+ endMarkerRect = anchor.rect;
934
+ endMarkerHit = true;
935
+ log(`end_marker rect: ${JSON.stringify(anchor.rect)}`);
936
+ endMarkerText = (await readEndMarkerTextBySelector(primarySelector)) || undefined;
937
+ }
938
+ else {
939
+ const fallbackRect = await resolveEndMarkerRectViaSelectors(primarySelector);
940
+ if (fallbackRect) {
941
+ const vp = await getViewport(controllerUrl, profile);
942
+ const innerH = vp.innerHeight || 0;
943
+ const ok = innerH ? fallbackRect.y > innerH * 0.55 : true;
944
+ if (ok) {
945
+ endMarkerRect = fallbackRect;
946
+ endMarkerHit = true;
947
+ }
948
+ }
949
+ endMarkerText = (await readEndMarkerTextBySelector(primarySelector)) || undefined;
950
+ }
951
+ }
952
+ catch {
953
+ // ignore
954
+ }
955
+ }
956
+ else if (emptyStateNode?.id && comments.length === 0) {
957
+ endMarkerContainerId = emptyStateNode.id;
958
+ try {
959
+ const anchor = await verifyAnchorByContainerId(emptyStateNode.id, profile, serviceUrl, '2px dashed #888888', 2000);
960
+ if (anchor.found && anchor.rect) {
961
+ endMarkerRect = anchor.rect;
962
+ emptyStateHit = true;
963
+ log(`empty_state rect: ${JSON.stringify(anchor.rect)}`);
964
+ endMarkerText = (await readEndMarkerTextBySelector(emptyStateNode.id)) || undefined;
965
+ }
966
+ }
967
+ catch {
968
+ // ignore
969
+ }
970
+ }
971
+ // 4.1 终态兜底(非重试):如果 DOM 探针已判定 empty/end 可见,但 anchor 反查失败(常见于布局抖动/遮挡),仍视为结束;
972
+ // 同时落一张证据截图用于复盘(避免“空态但未计入完成”导致 Phase34 fail-fast)。
973
+ const finalEndState = await getCommentEndState(controllerUrl, profile).catch(() => ({
974
+ endMarkerVisible: false,
975
+ emptyStateVisible: false,
976
+ }));
977
+ if (finalEndState?.emptyStateVisible && !emptyStateHit && comments.length === 0) {
978
+ await saveDebugScreenshot('empty_state_visible_but_anchor_missing', profile, {
979
+ note: 'empty_state visible by DOM probe, but anchor verify failed; treat as done',
980
+ emptyStateContainerId: emptyStateNode?.id || null,
981
+ });
982
+ emptyStateHit = true;
983
+ if (!endMarkerContainerId && emptyStateNode?.id)
984
+ endMarkerContainerId = emptyStateNode.id;
985
+ }
986
+ if (finalEndState?.endMarkerVisible && !endMarkerHit && comments.length > 0) {
987
+ await saveDebugScreenshot('end_marker_visible_but_anchor_missing', profile, {
988
+ note: 'end_marker visible by DOM probe, but anchor verify failed; treat as done',
989
+ endMarkerContainerId: endMarker?.id || null,
990
+ });
991
+ endMarkerHit = true;
992
+ if (!endMarkerContainerId && endMarker?.id)
993
+ endMarkerContainerId = endMarker.id;
994
+ }
995
+ let verified = false;
996
+ if (commentSectionRect) {
997
+ const sectionOk = commentSectionRect.height > 0;
998
+ const sampleOk = comments.length > 0 ? !!(sampleCommentRect && sampleCommentRect.height > 0) : true;
999
+ const endOk = endMarkerRect ? endMarkerRect.height > 0 : true;
1000
+ verified = sectionOk && sampleOk && endOk;
1001
+ }
1002
+ const reachedEnd = comments.length === 0 ? Boolean(emptyStateHit) : Boolean(endMarkerHit);
1003
+ return {
1004
+ success: true,
1005
+ comments,
1006
+ reachedEnd,
1007
+ emptyState: comments.length === 0 && Boolean(emptyStateHit),
1008
+ totalFromHeader,
1009
+ stoppedByMaxNew,
1010
+ anchor: {
1011
+ commentSectionContainerId: commentSectionId,
1012
+ commentSectionRect,
1013
+ sampleCommentContainerId,
1014
+ sampleCommentRect,
1015
+ endMarkerContainerId,
1016
+ endMarkerRect,
1017
+ verified
1018
+ }
1019
+ };
1020
+ }
1021
+ catch (error) {
1022
+ return {
1023
+ success: false,
1024
+ comments: [],
1025
+ reachedEnd: false,
1026
+ emptyState: false,
1027
+ totalFromHeader: null,
1028
+ error: `ExpandComments failed: ${error.message}`
1029
+ };
1030
+ }
1031
+ }
1032
+ //# sourceMappingURL=ExpandCommentsBlock.js.map