@web-auto/webauto 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/desktop-console/default-settings.json +1 -0
- package/apps/desktop-console/dist/main/index.mjs +1618 -0
- package/apps/desktop-console/{src → dist}/main/preload.mjs +10 -0
- package/apps/desktop-console/dist/renderer/index.js +3063 -0
- package/apps/desktop-console/entry/ui-console.mjs +299 -0
- package/apps/webauto/entry/account.mjs +356 -0
- package/apps/webauto/entry/lib/account-detect.mjs +160 -0
- package/apps/webauto/entry/lib/account-store.mjs +587 -0
- package/apps/webauto/entry/lib/profilepool.mjs +1 -1
- package/apps/webauto/entry/xhs-install.mjs +27 -3
- package/apps/webauto/entry/xhs-status.mjs +152 -0
- package/apps/webauto/entry/xhs-unified.mjs +595 -17
- package/bin/webauto.mjs +263 -15
- package/dist/apps/webauto/server.js +66 -0
- package/dist/modules/camo-backend/src/index.js +575 -0
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +817 -0
- package/dist/modules/camo-backend/src/internal/ElementRegistry.js +61 -0
- package/dist/modules/camo-backend/src/internal/ProfileLock.js +85 -0
- package/dist/modules/camo-backend/src/internal/SessionManager.js +172 -0
- package/dist/modules/camo-backend/src/internal/container-matcher.js +852 -0
- package/dist/modules/camo-backend/src/internal/engine-manager.js +258 -0
- package/dist/modules/camo-backend/src/internal/fingerprint.js +203 -0
- package/dist/modules/camo-backend/src/internal/pageRuntime.js +29 -0
- package/dist/modules/camo-backend/src/internal/runtimeInjector.js +30 -0
- package/dist/modules/camo-backend/src/internal/state-bus.js +46 -0
- package/dist/modules/camo-backend/src/internal/storage-paths.js +36 -0
- package/dist/modules/camo-backend/src/internal/ws-server.js +1202 -0
- package/dist/modules/camo-runtime/src/utils/browser-service.mjs +423 -0
- package/dist/modules/camo-runtime/src/utils/config.mjs +77 -0
- package/dist/modules/container-registry/src/index.js +184 -0
- package/dist/modules/logging/src/index.js +92 -0
- package/dist/modules/operations/src/builtin.js +27 -0
- package/dist/modules/operations/src/container-binding.js +75 -0
- package/dist/modules/operations/src/executor.js +146 -0
- package/dist/modules/operations/src/operations/click.js +167 -0
- package/dist/modules/operations/src/operations/extract.js +204 -0
- package/dist/modules/operations/src/operations/find-child.js +17 -0
- package/dist/modules/operations/src/operations/highlight.js +138 -0
- package/dist/modules/operations/src/operations/key.js +61 -0
- package/dist/modules/operations/src/operations/navigate.js +148 -0
- package/dist/modules/operations/src/operations/scroll.js +126 -0
- package/dist/modules/operations/src/operations/type.js +190 -0
- package/dist/modules/operations/src/queue.js +100 -0
- package/dist/modules/operations/src/registry.js +11 -0
- package/dist/modules/operations/src/system/mouse.js +33 -0
- package/dist/modules/state/src/atomic-json.js +33 -0
- package/dist/modules/workflow/blocks/AnchorVerificationBlock.js +71 -0
- package/dist/modules/workflow/blocks/BehaviorRandomizer.js +26 -0
- package/dist/modules/workflow/blocks/CallWorkflowBlock.js +38 -0
- package/dist/modules/workflow/blocks/CloseDetailBlock.js +209 -0
- package/dist/modules/workflow/blocks/CollectBatch.js +137 -0
- package/dist/modules/workflow/blocks/CollectCommentsBlock.js +415 -0
- package/dist/modules/workflow/blocks/CollectSearchListBlock.js +599 -0
- package/dist/modules/workflow/blocks/CollectWeiboPosts.js +229 -0
- package/dist/modules/workflow/blocks/DetectPageStateBlock.js +259 -0
- package/dist/modules/workflow/blocks/EnsureLoginBlock.js +162 -0
- package/dist/modules/workflow/blocks/EnsureSession.js +426 -0
- package/dist/modules/workflow/blocks/ErrorClassifier.js +164 -0
- package/dist/modules/workflow/blocks/ErrorRecoveryBlock.js +319 -0
- package/dist/modules/workflow/blocks/ExpandCommentsBlock.js +1032 -0
- package/dist/modules/workflow/blocks/ExtractDetailBlock.js +310 -0
- package/dist/modules/workflow/blocks/ExtractPostFields.js +88 -0
- package/dist/modules/workflow/blocks/GenerateSmartReplyBlock.js +68 -0
- package/dist/modules/workflow/blocks/GoToSearchBlock.js +497 -0
- package/dist/modules/workflow/blocks/GracefulFallbackBlock.js +104 -0
- package/dist/modules/workflow/blocks/HighlightBlock.js +66 -0
- package/dist/modules/workflow/blocks/InitAutoScroll.js +65 -0
- package/dist/modules/workflow/blocks/LoadContainerDefinition.js +50 -0
- package/dist/modules/workflow/blocks/LoadContainerIndex.js +43 -0
- package/dist/modules/workflow/blocks/LocateAndGuardBlock.js +176 -0
- package/dist/modules/workflow/blocks/LoginRecoveryBlock.js +242 -0
- package/dist/modules/workflow/blocks/MatchContainers.js +64 -0
- package/dist/modules/workflow/blocks/MonitoringBlock.js +190 -0
- package/dist/modules/workflow/blocks/OpenDetailBlock.js +1240 -0
- package/dist/modules/workflow/blocks/OrganizeXhsNotesBlock.js +117 -0
- package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +270 -0
- package/dist/modules/workflow/blocks/PickSinglePost.js +69 -0
- package/dist/modules/workflow/blocks/ProgressTracker.js +125 -0
- package/dist/modules/workflow/blocks/RecordFixtureBlock.js +44 -0
- package/dist/modules/workflow/blocks/RenderMarkdown.js +48 -0
- package/dist/modules/workflow/blocks/SaveFile.js +54 -0
- package/dist/modules/workflow/blocks/ScrollNextBatch.js +72 -0
- package/dist/modules/workflow/blocks/SessionHealthBlock.js +73 -0
- package/dist/modules/workflow/blocks/StartBrowserService.js +45 -0
- package/dist/modules/workflow/blocks/ValidateContainerDefinition.js +67 -0
- package/dist/modules/workflow/blocks/ValidateExtract.js +35 -0
- package/dist/modules/workflow/blocks/WaitSearchPermitBlock.js +162 -0
- package/dist/modules/workflow/blocks/WaitStable.js +74 -0
- package/dist/modules/workflow/blocks/WarmupCommentsBlock.js +120 -0
- package/dist/modules/workflow/blocks/WorkflowExecutor.js +156 -0
- package/dist/modules/workflow/blocks/XiaohongshuCollectFromLinksBlock.js +1004 -0
- package/dist/modules/workflow/blocks/XiaohongshuCollectLinksBlock.js +1049 -0
- package/dist/modules/workflow/blocks/XiaohongshuFullCollectBlock.js +782 -0
- package/dist/modules/workflow/blocks/helpers/anchorVerify.js +198 -0
- package/dist/modules/workflow/blocks/helpers/asyncWorkQueue.js +53 -0
- package/dist/modules/workflow/blocks/helpers/commentScroller.js +334 -0
- package/dist/modules/workflow/blocks/helpers/commentSectionLocator.js +126 -0
- package/dist/modules/workflow/blocks/helpers/containerAnchors.js +301 -0
- package/dist/modules/workflow/blocks/helpers/debugArtifacts.js +6 -0
- package/dist/modules/workflow/blocks/helpers/downloadPaths.js +29 -0
- package/dist/modules/workflow/blocks/helpers/expandCommentsController.js +53 -0
- package/dist/modules/workflow/blocks/helpers/expandCommentsExtractor.js +129 -0
- package/dist/modules/workflow/blocks/helpers/macosVisionOcrPlugin.js +116 -0
- package/dist/modules/workflow/blocks/helpers/mergeXhsMarkdown.js +109 -0
- package/dist/modules/workflow/blocks/helpers/openDetailController.js +56 -0
- package/dist/modules/workflow/blocks/helpers/openDetailTypes.js +7 -0
- package/dist/modules/workflow/blocks/helpers/openDetailViewport.js +474 -0
- package/dist/modules/workflow/blocks/helpers/openDetailWaiter.js +104 -0
- package/dist/modules/workflow/blocks/helpers/operationLogger.js +195 -0
- package/dist/modules/workflow/blocks/helpers/persistedNotes.js +107 -0
- package/dist/modules/workflow/blocks/helpers/replyExpander.js +260 -0
- package/dist/modules/workflow/blocks/helpers/scrollIntoView.js +138 -0
- package/dist/modules/workflow/blocks/helpers/searchExecutor.js +328 -0
- package/dist/modules/workflow/blocks/helpers/searchGate.js +46 -0
- package/dist/modules/workflow/blocks/helpers/searchPageState.js +164 -0
- package/dist/modules/workflow/blocks/helpers/searchResultWaiter.js +64 -0
- package/dist/modules/workflow/blocks/helpers/simpleAnchor.js +134 -0
- package/dist/modules/workflow/blocks/helpers/smartReply.js +40 -0
- package/dist/modules/workflow/blocks/helpers/systemInput.js +635 -0
- package/dist/modules/workflow/blocks/helpers/targetCountMode.js +9 -0
- package/dist/modules/workflow/blocks/helpers/xhsCliArgs.js +80 -0
- package/dist/modules/workflow/blocks/helpers/xhsCommentDom.js +805 -0
- package/dist/modules/workflow/blocks/helpers/xhsNoteOrganizer.js +140 -0
- package/dist/modules/workflow/blocks/restore/RestorePhaseBlock.js +204 -0
- package/dist/modules/workflow/config/workflowRegistry.js +32 -0
- package/dist/modules/workflow/definitions/batch-collect-workflow.js +63 -0
- package/dist/modules/workflow/definitions/scroll-extract-workflow.js +74 -0
- package/dist/modules/workflow/definitions/xiaohongshu-collect-workflow-v2.js +81 -0
- package/dist/modules/workflow/definitions/xiaohongshu-collect-workflow.js +57 -0
- package/dist/modules/workflow/definitions/xiaohongshu-full-collect-workflow-v3.js +68 -0
- package/dist/modules/workflow/definitions/xiaohongshu-note-collect.js +49 -0
- package/dist/modules/workflow/definitions/xiaohongshu-phase1-workflow-v3.js +30 -0
- package/dist/modules/workflow/definitions/xiaohongshu-phase2-links-workflow-v3.js +40 -0
- package/dist/modules/workflow/definitions/xiaohongshu-phase3-collect-workflow-v1.js +54 -0
- package/dist/modules/workflow/definitions/xiaohongshu-phase34-from-links-workflow-v3.js +25 -0
- package/dist/modules/workflow/src/WeiboEventDrivenWorkflowRunner.js +308 -0
- package/dist/modules/workflow/src/context.js +70 -0
- package/dist/modules/workflow/src/index.js +5 -0
- package/dist/modules/workflow/src/orchestrator.js +230 -0
- package/dist/modules/workflow/src/runner.js +55 -0
- package/dist/modules/workflow/src/runtime.js +70 -0
- package/dist/modules/workflow/workflows/WeiboFeedExtractionWorkflow.js +359 -0
- package/dist/modules/workflow/workflows/XiaohongshuLoginWorkflow.js +110 -0
- package/dist/modules/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
- package/dist/modules/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
- package/dist/modules/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
- package/dist/modules/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
- package/dist/modules/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
- package/dist/modules/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
- package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +42 -0
- package/dist/modules/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
- package/dist/modules/xiaohongshu/app/src/index.js +9 -0
- package/dist/modules/xiaohongshu/app/src/utils/checkpoints.js +222 -0
- package/dist/modules/xiaohongshu/app/src/utils/controllerAction.js +43 -0
- package/dist/services/controller/src/controller.js +1476 -0
- package/dist/services/controller/src/index.js +2 -0
- package/dist/services/controller/src/payload-normalizer.js +129 -0
- package/dist/services/shared/heartbeat.js +120 -0
- package/dist/services/shared/lib/errorHandler.js +2 -0
- package/dist/services/shared/serviceProcessLogger.js +139 -0
- package/dist/services/unified-api/RemoteBrowserSession.js +176 -0
- package/dist/services/unified-api/RemoteSessionManager.js +148 -0
- package/dist/services/unified-api/container-operations-handler.js +115 -0
- package/dist/services/unified-api/server.js +652 -0
- package/dist/services/unified-api/state-registry.js +274 -0
- package/dist/services/unified-api/task-persistence.js +66 -0
- package/dist/services/unified-api/task-state.js +130 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +12 -5
- package/modules/xiaohongshu/app/pnpm-lock.yaml +24 -0
- package/package.json +38 -10
- package/.beads/README.md +0 -81
- package/.beads/config.yaml +0 -67
- package/.beads/interactions.jsonl +0 -0
- package/.beads/issues.jsonl +0 -180
- package/.beads/metadata.json +0 -4
- package/.claude/settings.local.json +0 -10
- package/.github/workflows/ci.yml +0 -55
- package/AGENTS.md +0 -253
- package/apps/desktop-console/README.md +0 -27
- package/apps/desktop-console/package-lock.json +0 -897
- package/apps/desktop-console/package.json +0 -20
- package/apps/desktop-console/scripts/build-and-install.mjs +0 -19
- package/apps/desktop-console/scripts/build.mjs +0 -45
- package/apps/desktop-console/scripts/test-preload.mjs +0 -13
- package/apps/desktop-console/src/main/config.mts +0 -26
- package/apps/desktop-console/src/main/core-daemon-manager.mts +0 -131
- package/apps/desktop-console/src/main/desktop-settings.mts +0 -267
- package/apps/desktop-console/src/main/heartbeat-watchdog.mts +0 -50
- package/apps/desktop-console/src/main/heartbeat-watchdog.test.mts +0 -68
- package/apps/desktop-console/src/main/index-streaming.test.mts +0 -20
- package/apps/desktop-console/src/main/index.mts +0 -980
- package/apps/desktop-console/src/main/profile-store.mts +0 -239
- package/apps/desktop-console/src/main/profile-store.test.mts +0 -54
- package/apps/desktop-console/src/main/state-bridge.mts +0 -114
- package/apps/desktop-console/src/main/task-state-types.ts +0 -32
- package/apps/desktop-console/src/renderer/hooks/use-task-state.mts +0 -120
- package/apps/desktop-console/src/renderer/index.mts +0 -133
- package/apps/desktop-console/src/renderer/index.test.mts +0 -34
- package/apps/desktop-console/src/renderer/path-helpers.mts +0 -46
- package/apps/desktop-console/src/renderer/path-helpers.test.mts +0 -14
- package/apps/desktop-console/src/renderer/tabs/debug.mts +0 -48
- package/apps/desktop-console/src/renderer/tabs/debug.test.mts +0 -22
- package/apps/desktop-console/src/renderer/tabs/logs.mts +0 -421
- package/apps/desktop-console/src/renderer/tabs/logs.test.mts +0 -27
- package/apps/desktop-console/src/renderer/tabs/preflight.mts +0 -486
- package/apps/desktop-console/src/renderer/tabs/preflight.test.mts +0 -33
- package/apps/desktop-console/src/renderer/tabs/profile-pool.mts +0 -213
- package/apps/desktop-console/src/renderer/tabs/results.mts +0 -171
- package/apps/desktop-console/src/renderer/tabs/run.test.mts +0 -63
- package/apps/desktop-console/src/renderer/tabs/runtime.mts +0 -151
- package/apps/desktop-console/src/renderer/tabs/settings.mts +0 -146
- package/apps/desktop-console/src/renderer/tabs/xiaohongshu/account-flow.mts +0 -486
- package/apps/desktop-console/src/renderer/tabs/xiaohongshu/guide-browser-check.mts +0 -56
- package/apps/desktop-console/src/renderer/tabs/xiaohongshu/helpers.mts +0 -262
- package/apps/desktop-console/src/renderer/tabs/xiaohongshu/layout-block.mts +0 -430
- package/apps/desktop-console/src/renderer/tabs/xiaohongshu/live-stats.mts +0 -847
- package/apps/desktop-console/src/renderer/tabs/xiaohongshu/run-flow.mts +0 -443
- package/apps/desktop-console/src/renderer/tabs/xiaohongshu-state.mts +0 -425
- package/apps/desktop-console/src/renderer/tabs/xiaohongshu.mts +0 -497
- package/apps/desktop-console/src/renderer/tabs/xiaohongshu.test.mts +0 -291
- package/apps/desktop-console/src/renderer/ui-components.mts +0 -31
- package/docs/README_camoufox_chinese.md +0 -141
- package/docs/USAGE_V3.md +0 -163
- package/docs/arch/OCR_MACOS_PLUGIN.md +0 -39
- package/docs/arch/PORTS.md +0 -40
- package/docs/arch/REGRESSION_CHECKLIST.md +0 -121
- package/docs/arch/SEARCH_GATE.md +0 -224
- package/docs/arch/VIEWPORT_SAFETY.md +0 -182
- package/docs/arch/XIAOHONGSHU_OFFLINE_MOCK_DESIGN.md +0 -267
- package/docs/xiaohongshu-container-driven-summary.md +0 -221
- package/docs/xiaohongshu-full-collect-runbook.md +0 -134
- package/docs/xiaohongshu-next-steps.md +0 -228
- package/docs/xiaohongshu-quickstart.md +0 -73
- package/docs/xiaohongshu-workflow-summary.md +0 -227
- package/modules/container-registry/tests/container-registry.test.ts +0 -16
- package/modules/logging/tests/logging.test.ts +0 -38
- package/modules/operations/tests/operations.test.ts +0 -22
- package/modules/operations/tests/viewport-filter.test.ts +0 -161
- package/modules/operations/tests/visible-only.test.ts +0 -250
- package/modules/session-manager/tests/session-manager.test.ts +0 -23
- package/modules/state/src/atomic-json.test.ts +0 -30
- package/modules/state/src/paths.test.ts +0 -59
- package/modules/state/src/xiaohongshu-collect-state.test.ts +0 -259
- package/modules/workflow/blocks/AnchorVerificationBlock.d.ts.map +0 -1
- package/modules/workflow/blocks/AnchorVerificationBlock.js.map +0 -1
- package/modules/workflow/blocks/DetectPageStateBlock.d.ts.map +0 -1
- package/modules/workflow/blocks/DetectPageStateBlock.js.map +0 -1
- package/modules/workflow/blocks/ErrorRecoveryBlock.d.ts.map +0 -1
- package/modules/workflow/blocks/ErrorRecoveryBlock.js.map +0 -1
- package/modules/workflow/blocks/WaitSearchPermitBlock.d.ts.map +0 -1
- package/modules/workflow/blocks/WaitSearchPermitBlock.js.map +0 -1
- package/modules/workflow/blocks/helpers/containerAnchors.d.ts.map +0 -1
- package/modules/workflow/blocks/helpers/containerAnchors.js.map +0 -1
- package/modules/workflow/blocks/helpers/downloadPaths.test.ts +0 -62
- package/modules/workflow/blocks/helpers/mergeXhsMarkdown.test.ts +0 -121
- package/modules/workflow/blocks/helpers/operationLogger.d.ts.map +0 -1
- package/modules/workflow/blocks/helpers/operationLogger.js.map +0 -1
- package/modules/workflow/blocks/helpers/persistedNotes.test.ts +0 -268
- package/modules/workflow/blocks/helpers/searchPageState.d.ts.map +0 -1
- package/modules/workflow/blocks/helpers/searchPageState.js.map +0 -1
- package/modules/workflow/blocks/helpers/targetCountMode.test.ts +0 -29
- package/modules/workflow/blocks/helpers/xhsCliArgs.test.ts +0 -75
- package/modules/workflow/tests/smartReply.test.ts +0 -32
- package/modules/xiaohongshu/app/src/blocks/Phase3Interact.matcher.test.ts +0 -33
- package/modules/xiaohongshu/app/src/utils/__tests__/checkpoints.test.ts +0 -141
- package/modules/xiaohongshu/app/tests/commentMatchDsl.test.ts +0 -50
- package/modules/xiaohongshu/app/tests/commentMatcher.test.ts +0 -46
- package/modules/xiaohongshu/app/tests/sharding.test.ts +0 -31
- package/package-scripts.json +0 -8
- package/runtime/infra/utils/README.md +0 -13
- package/runtime/infra/utils/scripts/README.md +0 -0
- package/runtime/infra/utils/scripts/development/eval-in-session.mjs +0 -40
- package/runtime/infra/utils/scripts/development/highlight-search-containers.mjs +0 -35
- package/runtime/infra/utils/scripts/service/kill-port.mjs +0 -24
- package/runtime/infra/utils/scripts/service/start-api.mjs +0 -39
- package/runtime/infra/utils/scripts/service/start-browser-service.mjs +0 -106
- package/runtime/infra/utils/scripts/service/stop-api.mjs +0 -18
- package/runtime/infra/utils/scripts/service/stop-browser-service.mjs +0 -104
- package/runtime/infra/utils/scripts/test-services.mjs +0 -94
- package/services/shared/heartbeat.test.ts +0 -102
- package/services/unified-api/__tests__/task-state.test.ts +0 -95
- package/sitecustomize.py +0 -19
- package/tests/README.md +0 -194
- package/tests/e2e/workflows/weibo-feed-extraction.test.ts +0 -171
- package/tests/fixtures/data/container-definitions.json +0 -67
- package/tests/fixtures/pages/simple-page.html +0 -69
- package/tests/integration/01-test-container-match.mjs +0 -188
- package/tests/integration/02-test-dom-branch.mjs +0 -161
- package/tests/integration/03-test-container-operation-system.mjs +0 -91
- package/tests/integration/05-test-container-lifecycle-events.mjs +0 -224
- package/tests/integration/05-test-container-lifecycle-with-events.mjs +0 -250
- package/tests/integration/06-test-container-dom-tree-drawing.mjs +0 -256
- package/tests/integration/07-test-weibo-container-lifecycle.mjs +0 -355
- package/tests/integration/08-test-weibo-feed-workflow.test.mjs +0 -164
- package/tests/integration/10-test-visual-analyzer.mjs +0 -312
- package/tests/integration/11-test-visual-loop.mjs +0 -284
- package/tests/integration/12-test-simple-visual-loop.mjs +0 -242
- package/tests/integration/13-test-visual-robust.mjs +0 -185
- package/tests/integration/14-test-visual-highlight-loop.mjs +0 -271
- package/tests/integration/inspect-page.mjs +0 -50
- package/tests/integration/run-all-tests.mjs +0 -95
- package/tests/patch_verification/CODEX_PATCH_TEST.md +0 -103
- package/tests/patch_verification/PHASE2_ANALYSIS.md +0 -179
- package/tests/patch_verification/PHASE2_OPTIMIZATION_REPORT.md +0 -55
- package/tests/patch_verification/PHASE2_TO_PHASE4_SUMMARY.md +0 -126
- package/tests/patch_verification/QUICK_TEST_SEQUENCE.md +0 -262
- package/tests/patch_verification/README.md +0 -143
- package/tests/patch_verification/RUN_TESTS.md +0 -60
- package/tests/patch_verification/TEST_EXECUTION.md +0 -99
- package/tests/patch_verification/TEST_PLAN.md +0 -328
- package/tests/patch_verification/TEST_RESULTS.md +0 -34
- package/tests/patch_verification/TOOL_TEST_PLAN.md +0 -48
- package/tests/patch_verification/run-tool-test.mjs +0 -121
- package/tests/patch_verification/temp_test_files/test01.txt +0 -1
- package/tests/patch_verification/temp_test_files/test02.txt +0 -3
- package/tests/patch_verification/temp_test_files/test02_gnu.txt +0 -3
- package/tests/patch_verification/temp_test_files/test03.txt +0 -1
- package/tests/patch_verification/temp_test_files/test03_multiline.txt +0 -5
- package/tests/patch_verification/temp_test_files/test04_function.ts +0 -5
- package/tests/patch_verification/temp_test_files/test05_import.ts +0 -4
- package/tests/patch_verification/temp_test_files/test06_special_chars.txt +0 -4
- package/tests/patch_verification/temp_test_files/test07_indentation.ts +0 -5
- package/tests/patch_verification/temp_test_files/test08_mismatch.txt +0 -1
- package/tests/patch_verification/temp_test_files/test_add_02.txt +0 -3
- package/tests/patch_verification/temp_test_files/test_simple.txt +0 -1
- package/tests/runner/TestReporter.mjs +0 -57
- package/tests/runner/TestRunner.mjs +0 -244
- package/tests/unit/commands/profile.test.mjs +0 -10
- package/tests/unit/container/change-notifier.test.mjs +0 -181
- package/tests/unit/lifecycle/session-registry.test.mjs +0 -135
- package/tests/unit/operations/registry.test.ts +0 -73
- package/tests/unit/utils/browser-service.test.mjs +0 -153
- package/tests/unit/utils/config.test.mjs +0 -166
- package/tests/unit/utils/fingerprint.test.mjs +0 -166
- package/tsconfig.json +0 -31
- package/tsconfig.services.json +0 -26
- /package/apps/desktop-console/{src → dist}/renderer/index.html +0 -0
- /package/apps/desktop-console/{src/renderer/tabs → dist/renderer}/run.mts +0 -0
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Block: XiaohongshuFullCollectBlock
|
|
3
|
+
*
|
|
4
|
+
* Phase 3-4 编排:在搜索结果页批量打开详情 → 提取详情 → 采集评论 → 持久化 → 关闭详情。
|
|
5
|
+
*
|
|
6
|
+
* 约定:
|
|
7
|
+
* - 已由上游步骤完成 EnsureSession / EnsureLogin / WaitSearchPermit / GoToSearch;
|
|
8
|
+
* - 本 Block 不做任何 URL 构造导航,只通过容器点击进入详情;
|
|
9
|
+
* - 所有点击/滚动/输入均走系统级能力(container:operation / keyboard:press / mouse:wheel)。
|
|
10
|
+
*/
|
|
11
|
+
import { execute as collectSearchList } from './CollectSearchListBlock.js';
|
|
12
|
+
import { execute as openDetail } from './OpenDetailBlock.js';
|
|
13
|
+
import { execute as extractDetail } from './ExtractDetailBlock.js';
|
|
14
|
+
import { execute as collectComments } from './CollectCommentsBlock.js';
|
|
15
|
+
import { execute as persistXhsNote } from './PersistXhsNoteBlock.js';
|
|
16
|
+
import { execute as closeDetail } from './CloseDetailBlock.js';
|
|
17
|
+
import { countPersistedNotes } from './helpers/persistedNotes.js';
|
|
18
|
+
import { AsyncWorkQueue } from './helpers/asyncWorkQueue.js';
|
|
19
|
+
import { organizeOneNote } from './helpers/xhsNoteOrganizer.js';
|
|
20
|
+
import { isDebugArtifactsEnabled } from './helpers/debugArtifacts.js';
|
|
21
|
+
import { mergeNotesMarkdown } from './helpers/mergeXhsMarkdown.js';
|
|
22
|
+
import { logControllerActionError, logControllerActionResult, logControllerActionStart, } from './helpers/operationLogger.js';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { promises as fs } from 'node:fs';
|
|
26
|
+
function resolveDownloadRoot() {
|
|
27
|
+
const custom = process.env.WEBAUTO_DOWNLOAD_ROOT || process.env.WEBAUTO_DOWNLOAD_DIR;
|
|
28
|
+
if (custom && custom.trim())
|
|
29
|
+
return custom;
|
|
30
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
31
|
+
if (home && home.trim())
|
|
32
|
+
return path.join(home, '.webauto', 'download');
|
|
33
|
+
return path.join(os.homedir(), '.webauto', 'download');
|
|
34
|
+
}
|
|
35
|
+
export async function execute(input) {
|
|
36
|
+
const { sessionId, keyword, env = 'debug', targetCount, mode = 'phase34', maxScrollRounds = 40, maxWarmupRounds, allowClickCommentButton, strictTargetCount = true, serviceUrl = 'http://127.0.0.1:7701', enableOcr = true, ocrLanguages, ocrConcurrency = 1, } = input;
|
|
37
|
+
const processed = [];
|
|
38
|
+
const downloadRoot = resolveDownloadRoot();
|
|
39
|
+
const debugArtifactsEnabled = isDebugArtifactsEnabled();
|
|
40
|
+
const requiredFiles = mode === 'phase34' ? ['content.md', 'comments.md'] : ['content.md'];
|
|
41
|
+
const persistedAtStart = await countPersistedNotes({
|
|
42
|
+
platform: 'xiaohongshu',
|
|
43
|
+
env,
|
|
44
|
+
keyword,
|
|
45
|
+
downloadRoot,
|
|
46
|
+
requiredFiles: [...requiredFiles],
|
|
47
|
+
...(mode === 'phase34' ? { requireCommentsDone: true } : {}),
|
|
48
|
+
});
|
|
49
|
+
const seenNoteIds = new Set(persistedAtStart.noteIds);
|
|
50
|
+
let persistedCount = persistedAtStart.count;
|
|
51
|
+
const openDetailDebugDir = debugArtifactsEnabled
|
|
52
|
+
? `${persistedAtStart.keywordDir}/_debug/open_detail`
|
|
53
|
+
: '';
|
|
54
|
+
const ocrDebugDir = debugArtifactsEnabled
|
|
55
|
+
? path.join(persistedAtStart.keywordDir, '_debug', 'ocr')
|
|
56
|
+
: '';
|
|
57
|
+
const ocrQueue = enableOcr && mode === 'phase34' && process.platform === 'darwin'
|
|
58
|
+
? new AsyncWorkQueue({ concurrency: ocrConcurrency, label: 'ocr' })
|
|
59
|
+
: null;
|
|
60
|
+
if (enableOcr && mode === 'phase34' && process.platform !== 'darwin') {
|
|
61
|
+
console.warn(`[FullCollect][ocr] enableOcr=true but platform=${process.platform}; OCR skipped (macOS only)`);
|
|
62
|
+
}
|
|
63
|
+
if (strictTargetCount && persistedCount > targetCount) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
processedCount: persistedCount,
|
|
67
|
+
targetCount,
|
|
68
|
+
initialPersistedCount: persistedAtStart.count,
|
|
69
|
+
finalPersistedCount: persistedCount,
|
|
70
|
+
addedCount: 0,
|
|
71
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
72
|
+
processed,
|
|
73
|
+
error: `existing_count_exceeds_target: ${persistedCount} > ${targetCount}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const controllerUrl = `${serviceUrl}/v1/controller/action`;
|
|
77
|
+
const ensureClosedDebugDir = debugArtifactsEnabled
|
|
78
|
+
? path.join(persistedAtStart.keywordDir, '_debug', 'ensure_closed')
|
|
79
|
+
: '';
|
|
80
|
+
async function saveEnsureClosedDebug(kind, meta) {
|
|
81
|
+
if (!debugArtifactsEnabled)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
await fs.mkdir(ensureClosedDebugDir, { recursive: true });
|
|
85
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
86
|
+
const base = `${ts}-${kind}`;
|
|
87
|
+
const pngPath = path.join(ensureClosedDebugDir, `${base}.png`);
|
|
88
|
+
const jsonPath = path.join(ensureClosedDebugDir, `${base}.json`);
|
|
89
|
+
let shot = null;
|
|
90
|
+
try {
|
|
91
|
+
shot = await controllerAction('browser:screenshot', { profileId: sessionId, fullPage: false });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
shot = null;
|
|
95
|
+
}
|
|
96
|
+
const b64 = shot?.data?.data ??
|
|
97
|
+
shot?.data?.body?.data ??
|
|
98
|
+
shot?.body?.data ??
|
|
99
|
+
shot?.result?.data ??
|
|
100
|
+
shot?.result ??
|
|
101
|
+
shot?.data ??
|
|
102
|
+
shot;
|
|
103
|
+
if (typeof b64 === 'string' && b64.length > 10) {
|
|
104
|
+
await fs.writeFile(pngPath, Buffer.from(b64, 'base64'));
|
|
105
|
+
}
|
|
106
|
+
await fs.writeFile(jsonPath, JSON.stringify({
|
|
107
|
+
ts,
|
|
108
|
+
kind,
|
|
109
|
+
sessionId,
|
|
110
|
+
keyword,
|
|
111
|
+
url: await getCurrentUrl(),
|
|
112
|
+
...meta,
|
|
113
|
+
pngPath: typeof b64 === 'string' && b64.length > 10 ? pngPath : null,
|
|
114
|
+
}, null, 2), 'utf-8');
|
|
115
|
+
console.log(`[FullCollect][debug] ensureClosed saved ${kind}: ${pngPath}`);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
console.warn(`[FullCollect][debug] ensureClosed save failed (${kind}): ${e?.message || String(e)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function saveOcrDebug(kind, meta) {
|
|
122
|
+
if (!debugArtifactsEnabled || !ocrDebugDir)
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
await fs.mkdir(ocrDebugDir, { recursive: true });
|
|
126
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
127
|
+
const jsonPath = path.join(ocrDebugDir, `${ts}-${kind}.json`);
|
|
128
|
+
await fs.writeFile(jsonPath, JSON.stringify(meta, null, 2), 'utf-8');
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
console.warn(`[FullCollect][ocr] save debug failed (${kind}): ${e?.message || String(e)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function ensureClosed() {
|
|
135
|
+
// 已在搜索结果页,直接视为关闭成功
|
|
136
|
+
const url0 = await getCurrentUrl();
|
|
137
|
+
if (url0.includes('/search_result') && urlMatchesKeyword(url0)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const res = await closeDetail({ sessionId, serviceUrl });
|
|
142
|
+
if (res?.success)
|
|
143
|
+
return true;
|
|
144
|
+
if (res?.error) {
|
|
145
|
+
processed.push({ ok: false, error: `CloseDetail failed: ${res.error}` });
|
|
146
|
+
}
|
|
147
|
+
await saveEnsureClosedDebug('close_detail_failed', {
|
|
148
|
+
urlBefore: url0,
|
|
149
|
+
result: res || null,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
processed.push({ ok: false, error: `CloseDetail threw: ${e?.message || String(e)}` });
|
|
154
|
+
await saveEnsureClosedDebug('close_detail_threw', {
|
|
155
|
+
urlBefore: url0,
|
|
156
|
+
error: e?.message || String(e),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
await saveEnsureClosedDebug('stuck_in_detail_after_close_attempts', {});
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
function buildStuckInDetailResult(reason) {
|
|
163
|
+
return {
|
|
164
|
+
success: false,
|
|
165
|
+
processedCount: persistedCount,
|
|
166
|
+
targetCount,
|
|
167
|
+
initialPersistedCount: persistedAtStart.count,
|
|
168
|
+
finalPersistedCount: persistedCount,
|
|
169
|
+
addedCount: Math.max(0, persistedCount - persistedAtStart.count),
|
|
170
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
171
|
+
processed,
|
|
172
|
+
error: reason,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async function assertKeywordStillCorrect(tag) {
|
|
176
|
+
const url = await getCurrentUrl();
|
|
177
|
+
if (url && url.includes('/search_result') && !urlMatchesKeyword(url)) {
|
|
178
|
+
await saveEnsureClosedDebug(`keyword_changed_${tag}`, { url, keyword });
|
|
179
|
+
return { ok: false, url };
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
async function controllerAction(action, payload = {}) {
|
|
184
|
+
const opId = logControllerActionStart(action, payload, { source: 'XiaohongshuFullCollectBlock' });
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetch(controllerUrl, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: { 'Content-Type': 'application/json' },
|
|
189
|
+
body: JSON.stringify({ action, payload }),
|
|
190
|
+
});
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
193
|
+
}
|
|
194
|
+
const data = await response.json();
|
|
195
|
+
const result = data.data || data;
|
|
196
|
+
logControllerActionResult(opId, action, result, { source: 'XiaohongshuFullCollectBlock' });
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
logControllerActionError(opId, action, error, payload, { source: 'XiaohongshuFullCollectBlock' });
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function safeDecodeURIComponent(value) {
|
|
205
|
+
try {
|
|
206
|
+
return decodeURIComponent(value);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function getCurrentUrl() {
|
|
213
|
+
try {
|
|
214
|
+
const res = await controllerAction('browser:execute', {
|
|
215
|
+
profile: sessionId,
|
|
216
|
+
script: 'location.href',
|
|
217
|
+
});
|
|
218
|
+
return (res?.result ?? res?.data?.result ?? res ?? '');
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function urlMatchesKeyword(url) {
|
|
225
|
+
const raw = url || '';
|
|
226
|
+
if (!raw.includes('/search_result'))
|
|
227
|
+
return false;
|
|
228
|
+
// 严格等于:必须能解析出 keyword=... 且解码后完全等于目标 keyword
|
|
229
|
+
try {
|
|
230
|
+
const u = new URL(raw);
|
|
231
|
+
const kw = u.searchParams.get('keyword');
|
|
232
|
+
if (!kw)
|
|
233
|
+
return false;
|
|
234
|
+
const decodedKw = safeDecodeURIComponent(safeDecodeURIComponent(kw)).trim();
|
|
235
|
+
return decodedKw === String(keyword || '').trim();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// 兼容某些情况下 keyword 可能被双重编码导致 URL 解析失败
|
|
239
|
+
const decoded = safeDecodeURIComponent(safeDecodeURIComponent(raw));
|
|
240
|
+
if (!decoded.includes('/search_result'))
|
|
241
|
+
return false;
|
|
242
|
+
try {
|
|
243
|
+
const u2 = new URL(decoded);
|
|
244
|
+
const kw2 = u2.searchParams.get('keyword');
|
|
245
|
+
if (!kw2)
|
|
246
|
+
return false;
|
|
247
|
+
const decodedKw2 = safeDecodeURIComponent(safeDecodeURIComponent(kw2)).trim();
|
|
248
|
+
return decodedKw2 === String(keyword || '').trim();
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function scrollSearchListDown() {
|
|
256
|
+
return scrollSearchList('down', 800);
|
|
257
|
+
}
|
|
258
|
+
async function getSearchListScrollState() {
|
|
259
|
+
try {
|
|
260
|
+
const res = await controllerAction('browser:execute', {
|
|
261
|
+
profile: sessionId,
|
|
262
|
+
script: `(() => {
|
|
263
|
+
const el = document.querySelector('.feeds-container');
|
|
264
|
+
const y = window.scrollY || document.documentElement.scrollTop || 0;
|
|
265
|
+
const feedsY =
|
|
266
|
+
el && typeof el.scrollTop === 'number' && el.scrollHeight && el.clientHeight
|
|
267
|
+
? el.scrollTop
|
|
268
|
+
: null;
|
|
269
|
+
return { windowY: y, feedsY };
|
|
270
|
+
})()`,
|
|
271
|
+
});
|
|
272
|
+
const payload = res?.result ?? res?.data?.result ?? res ?? null;
|
|
273
|
+
const windowY = Number(payload?.windowY ?? 0);
|
|
274
|
+
const feedsYRaw = payload?.feedsY;
|
|
275
|
+
const feedsY = feedsYRaw === null || typeof feedsYRaw === 'undefined' ? null : Number(feedsYRaw);
|
|
276
|
+
return {
|
|
277
|
+
windowY: Number.isFinite(windowY) ? windowY : 0,
|
|
278
|
+
feedsY: feedsY === null ? null : Number.isFinite(feedsY) ? feedsY : null,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return { windowY: 0, feedsY: null };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function scrollSearchList(direction, amount) {
|
|
286
|
+
const before = await getSearchListScrollState();
|
|
287
|
+
// ✅ 系统级滚动:优先走容器 scroll operation
|
|
288
|
+
try {
|
|
289
|
+
const op = await controllerAction('container:operation', {
|
|
290
|
+
containerId: 'xiaohongshu_search.search_result_list',
|
|
291
|
+
operationId: 'scroll',
|
|
292
|
+
sessionId,
|
|
293
|
+
config: { direction, amount: Math.min(800, Math.max(120, Math.floor(amount))) },
|
|
294
|
+
});
|
|
295
|
+
const payload = op?.data ?? op;
|
|
296
|
+
const ok = Boolean(payload?.success ?? payload?.data?.success ?? op?.success);
|
|
297
|
+
await new Promise((r) => setTimeout(r, 1100));
|
|
298
|
+
const after = await getSearchListScrollState();
|
|
299
|
+
const moved = Math.abs(after.windowY - before.windowY) >= 10 ||
|
|
300
|
+
(after.feedsY !== null &&
|
|
301
|
+
before.feedsY !== null &&
|
|
302
|
+
Math.abs(after.feedsY - before.feedsY) >= 10);
|
|
303
|
+
return ok && moved;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// fallback:PageUp/PageDown(系统级)
|
|
307
|
+
try {
|
|
308
|
+
await controllerAction('keyboard:press', {
|
|
309
|
+
profileId: sessionId,
|
|
310
|
+
key: direction === 'up' ? 'PageUp' : 'PageDown',
|
|
311
|
+
});
|
|
312
|
+
await new Promise((r) => setTimeout(r, 1300));
|
|
313
|
+
const after = await getSearchListScrollState();
|
|
314
|
+
const moved = Math.abs(after.windowY - before.windowY) >= 10 ||
|
|
315
|
+
(after.feedsY !== null &&
|
|
316
|
+
before.feedsY !== null &&
|
|
317
|
+
Math.abs(after.feedsY - before.feedsY) >= 10);
|
|
318
|
+
return moved;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// 1) 逐屏处理:每次只采集当前视口内的卡片(maxScrollRounds=1),处理完再滚动下一屏
|
|
326
|
+
let scrollSteps = 0;
|
|
327
|
+
let stagnantRounds = 0;
|
|
328
|
+
let noScrollMoveRounds = 0;
|
|
329
|
+
let bounceAttempts = 0;
|
|
330
|
+
async function saveSearchListScrollDebug(kind, meta) {
|
|
331
|
+
try {
|
|
332
|
+
await controllerAction('container:operation', {
|
|
333
|
+
containerId: 'xiaohongshu_search.search_result_list',
|
|
334
|
+
operationId: 'highlight',
|
|
335
|
+
sessionId,
|
|
336
|
+
config: { style: '3px solid #ff4444', duration: 1500 },
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch { }
|
|
340
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
341
|
+
await saveEnsureClosedDebug(kind, meta);
|
|
342
|
+
}
|
|
343
|
+
while (persistedCount < targetCount && scrollSteps < maxScrollRounds) {
|
|
344
|
+
const urlNow = await getCurrentUrl();
|
|
345
|
+
if (urlNow && !urlMatchesKeyword(urlNow)) {
|
|
346
|
+
await saveEnsureClosedDebug('keyword_changed', { url: urlNow });
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
processedCount: persistedCount,
|
|
350
|
+
targetCount,
|
|
351
|
+
initialPersistedCount: persistedAtStart.count,
|
|
352
|
+
finalPersistedCount: persistedCount,
|
|
353
|
+
addedCount: Math.max(0, persistedCount - persistedAtStart.count),
|
|
354
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
355
|
+
processed,
|
|
356
|
+
error: `keyword_changed: ${urlNow}`,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const remaining = Math.max(0, targetCount - persistedCount);
|
|
360
|
+
const list = await collectSearchList({
|
|
361
|
+
sessionId,
|
|
362
|
+
targetCount: Math.min(remaining, 30),
|
|
363
|
+
maxScrollRounds: 1, // 只采集当前视口,避免 domIndex 漂移/虚拟列表导致 selector 不存在
|
|
364
|
+
serviceUrl,
|
|
365
|
+
});
|
|
366
|
+
const kwOkAfterList = await assertKeywordStillCorrect('after_collect_list');
|
|
367
|
+
if (kwOkAfterList !== true) {
|
|
368
|
+
return {
|
|
369
|
+
success: false,
|
|
370
|
+
processedCount: persistedCount,
|
|
371
|
+
targetCount,
|
|
372
|
+
initialPersistedCount: persistedAtStart.count,
|
|
373
|
+
finalPersistedCount: persistedCount,
|
|
374
|
+
addedCount: Math.max(0, persistedCount - persistedAtStart.count),
|
|
375
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
376
|
+
processed,
|
|
377
|
+
error: `keyword_changed_after_collect_list: ${kwOkAfterList.url}`,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (!list.success || !Array.isArray(list.items) || list.items.length === 0) {
|
|
381
|
+
await saveEnsureClosedDebug('collect_search_list_failed', {
|
|
382
|
+
success: Boolean(list.success),
|
|
383
|
+
error: list.error || null,
|
|
384
|
+
itemCount: Array.isArray(list.items) ? list.items.length : null,
|
|
385
|
+
});
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
processedCount: persistedCount,
|
|
389
|
+
targetCount,
|
|
390
|
+
initialPersistedCount: persistedAtStart.count,
|
|
391
|
+
finalPersistedCount: persistedCount,
|
|
392
|
+
addedCount: Math.max(0, persistedCount - persistedAtStart.count),
|
|
393
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
394
|
+
processed,
|
|
395
|
+
error: list.error || 'CollectSearchListBlock returned no items',
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
let processedThisRound = 0;
|
|
399
|
+
// 倒序处理:优先处理当前更可能在视口内的卡片
|
|
400
|
+
for (const item of list.items.slice().reverse()) {
|
|
401
|
+
if (persistedCount >= targetCount)
|
|
402
|
+
break;
|
|
403
|
+
const domIndex = typeof item.domIndex === 'number' ? item.domIndex : undefined;
|
|
404
|
+
const noteId = typeof item.noteId === 'string' ? item.noteId : undefined;
|
|
405
|
+
const clickRect = item.rect &&
|
|
406
|
+
typeof item.rect.x === 'number' &&
|
|
407
|
+
typeof item.rect.y === 'number' &&
|
|
408
|
+
typeof item.rect.width === 'number' &&
|
|
409
|
+
typeof item.rect.height === 'number'
|
|
410
|
+
? item.rect
|
|
411
|
+
: undefined;
|
|
412
|
+
if (noteId && seenNoteIds.has(noteId)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (typeof domIndex !== 'number' || !Number.isFinite(domIndex) || domIndex < 0) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const beforePersistCount = persistedCount;
|
|
420
|
+
// 开发阶段:严格禁止 keyword 漂移。若在处理列表项前 URL 已变为其它 keyword(search_result),立即停止并保留调试信息。
|
|
421
|
+
// 另外:若此时仍停留在详情页(/explore),优先判定为“未能退出详情”,直接 fail-fast。
|
|
422
|
+
const urlBeforeOpen = await getCurrentUrl();
|
|
423
|
+
if (urlBeforeOpen && urlBeforeOpen.includes('/explore/')) {
|
|
424
|
+
await saveEnsureClosedDebug('unexpected_in_detail_before_open_detail', {
|
|
425
|
+
urlNow,
|
|
426
|
+
urlBeforeOpen,
|
|
427
|
+
keyword,
|
|
428
|
+
domIndex,
|
|
429
|
+
expectedNoteId: noteId || null,
|
|
430
|
+
});
|
|
431
|
+
const closed = await ensureClosed();
|
|
432
|
+
if (!closed)
|
|
433
|
+
return buildStuckInDetailResult('stuck_in_detail_before_open_detail');
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (urlBeforeOpen && urlBeforeOpen.includes('/search_result') && !urlMatchesKeyword(urlBeforeOpen)) {
|
|
437
|
+
await saveEnsureClosedDebug('keyword_changed_before_open_detail', {
|
|
438
|
+
urlNow,
|
|
439
|
+
urlBeforeOpen,
|
|
440
|
+
keyword,
|
|
441
|
+
domIndex,
|
|
442
|
+
expectedNoteId: noteId || null,
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
success: false,
|
|
446
|
+
processedCount: persistedCount,
|
|
447
|
+
targetCount,
|
|
448
|
+
initialPersistedCount: persistedAtStart.count,
|
|
449
|
+
finalPersistedCount: persistedCount,
|
|
450
|
+
addedCount: Math.max(0, persistedCount - persistedAtStart.count),
|
|
451
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
452
|
+
processed,
|
|
453
|
+
error: `keyword_changed_before_open_detail: ${urlBeforeOpen}`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const opened = await openDetail({
|
|
457
|
+
sessionId,
|
|
458
|
+
containerId: item.containerId || 'xiaohongshu_search.search_result_item',
|
|
459
|
+
domIndex,
|
|
460
|
+
clickRect,
|
|
461
|
+
expectedNoteId: item.noteId,
|
|
462
|
+
expectedHref: item.hrefAttr,
|
|
463
|
+
debugDir: openDetailDebugDir || undefined,
|
|
464
|
+
serviceUrl,
|
|
465
|
+
});
|
|
466
|
+
if (!opened.success || !opened.safeDetailUrl || !opened.noteId) {
|
|
467
|
+
processed.push({
|
|
468
|
+
domIndex,
|
|
469
|
+
noteId,
|
|
470
|
+
ok: false,
|
|
471
|
+
error: opened.error || 'OpenDetailBlock failed',
|
|
472
|
+
});
|
|
473
|
+
// 开发阶段:不做兜底纠错/自动补偿。任何“打开详情失败”(尤其是风控/验证码/误点)都应立即停下,保留证据排查。
|
|
474
|
+
const closed = await ensureClosed();
|
|
475
|
+
if (!closed)
|
|
476
|
+
return buildStuckInDetailResult('stuck_in_detail_after_open_detail_failed');
|
|
477
|
+
return {
|
|
478
|
+
success: false,
|
|
479
|
+
processedCount: persistedCount,
|
|
480
|
+
targetCount,
|
|
481
|
+
initialPersistedCount: persistedAtStart.count,
|
|
482
|
+
finalPersistedCount: persistedCount,
|
|
483
|
+
addedCount: Math.max(0, persistedCount - persistedAtStart.count),
|
|
484
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
485
|
+
processed,
|
|
486
|
+
error: `open_detail_failed: ${opened.error || 'unknown'}`,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (seenNoteIds.has(opened.noteId)) {
|
|
490
|
+
const closed = await ensureClosed();
|
|
491
|
+
if (!closed)
|
|
492
|
+
return buildStuckInDetailResult('stuck_in_detail_after_duplicate_note');
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
seenNoteIds.add(opened.noteId);
|
|
496
|
+
const detail = await extractDetail({ sessionId, serviceUrl });
|
|
497
|
+
if (!detail.success) {
|
|
498
|
+
processed.push({
|
|
499
|
+
noteId: opened.noteId,
|
|
500
|
+
detailUrl: opened.safeDetailUrl,
|
|
501
|
+
domIndex,
|
|
502
|
+
ok: false,
|
|
503
|
+
error: detail.error || 'ExtractDetailBlock failed',
|
|
504
|
+
});
|
|
505
|
+
const closed = await ensureClosed();
|
|
506
|
+
if (!closed)
|
|
507
|
+
return buildStuckInDetailResult('stuck_in_detail_after_extract_failed');
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const comments = mode === 'phase3'
|
|
511
|
+
? {
|
|
512
|
+
success: true,
|
|
513
|
+
comments: [],
|
|
514
|
+
reachedEnd: false,
|
|
515
|
+
emptyState: false,
|
|
516
|
+
warmupCount: 0,
|
|
517
|
+
totalFromHeader: null,
|
|
518
|
+
}
|
|
519
|
+
: await collectComments({
|
|
520
|
+
sessionId,
|
|
521
|
+
serviceUrl,
|
|
522
|
+
...(typeof maxWarmupRounds === 'number' && maxWarmupRounds > 0
|
|
523
|
+
? { maxWarmupRounds }
|
|
524
|
+
: {}),
|
|
525
|
+
...(allowClickCommentButton === false ? { allowClickCommentButton: false } : {}),
|
|
526
|
+
commentTabMode: 'rotate4',
|
|
527
|
+
commentTabBatch: 50,
|
|
528
|
+
commentTabMaxTabs: 4,
|
|
529
|
+
});
|
|
530
|
+
if (mode === 'phase34' && !comments?.success) {
|
|
531
|
+
processed.push({
|
|
532
|
+
noteId: opened.noteId,
|
|
533
|
+
detailUrl: opened.safeDetailUrl,
|
|
534
|
+
domIndex,
|
|
535
|
+
ok: false,
|
|
536
|
+
error: comments?.error || 'CollectCommentsBlock failed',
|
|
537
|
+
});
|
|
538
|
+
const closed = await ensureClosed();
|
|
539
|
+
if (!closed)
|
|
540
|
+
return buildStuckInDetailResult('stuck_in_detail_after_collect_comments_failed');
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const persisted = await persistXhsNote({
|
|
544
|
+
sessionId,
|
|
545
|
+
env,
|
|
546
|
+
platform: 'xiaohongshu',
|
|
547
|
+
keyword,
|
|
548
|
+
noteId: opened.noteId,
|
|
549
|
+
searchUrl: urlNow,
|
|
550
|
+
detailUrl: opened.safeDetailUrl,
|
|
551
|
+
detail: detail.detail,
|
|
552
|
+
commentsResult: comments,
|
|
553
|
+
persistMode: mode === 'phase3' ? 'detail' : 'both',
|
|
554
|
+
});
|
|
555
|
+
processed.push({
|
|
556
|
+
noteId: opened.noteId,
|
|
557
|
+
detailUrl: opened.safeDetailUrl,
|
|
558
|
+
domIndex,
|
|
559
|
+
ok: Boolean(persisted.success),
|
|
560
|
+
outputDir: persisted.outputDir,
|
|
561
|
+
contentPath: persisted.contentPath,
|
|
562
|
+
...(persisted.success ? {} : { error: persisted.error || 'PersistXhsNoteBlock failed' }),
|
|
563
|
+
});
|
|
564
|
+
// OCR + merged.md 后台并行:与后续的“开详情/抓评论”并行,不阻塞主流程。
|
|
565
|
+
// 注意:仅做本地文件处理,不涉及浏览器操作,不会触发风控。
|
|
566
|
+
if (persisted.success && ocrQueue && persisted.outputDir) {
|
|
567
|
+
const noteDir = String(persisted.outputDir);
|
|
568
|
+
const noteIdForJob = String(opened.noteId);
|
|
569
|
+
const languagesForJob = typeof ocrLanguages === 'string' && ocrLanguages.trim() ? ocrLanguages.trim() : 'chi_sim+eng';
|
|
570
|
+
console.log(`[FullCollect][ocr] queue note=${noteIdForJob} dir=${noteDir}`);
|
|
571
|
+
void ocrQueue
|
|
572
|
+
.enqueue(async () => {
|
|
573
|
+
const t0 = Date.now();
|
|
574
|
+
await saveOcrDebug(`start-${noteIdForJob}`, { noteId: noteIdForJob, noteDir, languages: languagesForJob });
|
|
575
|
+
const res = await organizeOneNote({
|
|
576
|
+
noteDir,
|
|
577
|
+
noteId: noteIdForJob,
|
|
578
|
+
keyword,
|
|
579
|
+
ocrLanguages: languagesForJob,
|
|
580
|
+
runOcr: true,
|
|
581
|
+
});
|
|
582
|
+
const ms = Date.now() - t0;
|
|
583
|
+
await saveOcrDebug(`done-${noteIdForJob}`, { ...res, ms });
|
|
584
|
+
console.log(`[FullCollect][ocr] done note=${noteIdForJob} images=${res.imageCount} ocrErrors=${res.ocrErrors} ms=${ms}`);
|
|
585
|
+
return res;
|
|
586
|
+
})
|
|
587
|
+
.catch(async (e) => {
|
|
588
|
+
await saveOcrDebug(`error-${noteIdForJob}`, { noteId: noteIdForJob, error: e?.message || String(e) });
|
|
589
|
+
console.warn(`[FullCollect][ocr] failed note=${noteIdForJob}: ${e?.message || String(e)}`);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (persisted.success) {
|
|
593
|
+
const persistedAfter = await countPersistedNotes({
|
|
594
|
+
platform: 'xiaohongshu',
|
|
595
|
+
env,
|
|
596
|
+
keyword,
|
|
597
|
+
requiredFiles: [...requiredFiles],
|
|
598
|
+
...(mode === 'phase34' ? { requireCommentsDone: true } : {}),
|
|
599
|
+
});
|
|
600
|
+
if (persistedAfter.count > beforePersistCount) {
|
|
601
|
+
persistedCount = persistedAfter.count;
|
|
602
|
+
processedThisRound += 1;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const closed = await ensureClosed();
|
|
606
|
+
if (!closed) {
|
|
607
|
+
processed.push({
|
|
608
|
+
noteId: opened.noteId,
|
|
609
|
+
detailUrl: opened.safeDetailUrl,
|
|
610
|
+
domIndex,
|
|
611
|
+
ok: false,
|
|
612
|
+
error: 'CloseDetailBlock failed repeatedly (stuck in detail)',
|
|
613
|
+
});
|
|
614
|
+
return {
|
|
615
|
+
success: false,
|
|
616
|
+
processedCount: persistedCount,
|
|
617
|
+
targetCount,
|
|
618
|
+
initialPersistedCount: persistedAtStart.count,
|
|
619
|
+
finalPersistedCount: persistedCount,
|
|
620
|
+
addedCount: Math.max(0, persistedCount - persistedAtStart.count),
|
|
621
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
622
|
+
processed,
|
|
623
|
+
error: 'stuck_in_detail',
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const kwOkAfterClose = await assertKeywordStillCorrect('after_close_detail');
|
|
627
|
+
if (kwOkAfterClose !== true) {
|
|
628
|
+
return {
|
|
629
|
+
success: false,
|
|
630
|
+
processedCount: persistedCount,
|
|
631
|
+
targetCount,
|
|
632
|
+
initialPersistedCount: persistedAtStart.count,
|
|
633
|
+
finalPersistedCount: persistedCount,
|
|
634
|
+
addedCount: Math.max(0, persistedCount - persistedAtStart.count),
|
|
635
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
636
|
+
processed,
|
|
637
|
+
error: `keyword_changed_after_close_detail: ${kwOkAfterClose.url}`,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
// 每次成功后立即进入下一轮(重新采集当前视口),避免列表重渲染导致 selector/href 失效
|
|
641
|
+
if (persistedCount > beforePersistCount) {
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
processed.push({
|
|
647
|
+
domIndex,
|
|
648
|
+
noteId,
|
|
649
|
+
ok: false,
|
|
650
|
+
error: err?.message || String(err),
|
|
651
|
+
});
|
|
652
|
+
const closed = await ensureClosed();
|
|
653
|
+
if (!closed) {
|
|
654
|
+
return {
|
|
655
|
+
success: false,
|
|
656
|
+
processedCount: persistedCount,
|
|
657
|
+
targetCount,
|
|
658
|
+
initialPersistedCount: persistedAtStart.count,
|
|
659
|
+
finalPersistedCount: persistedCount,
|
|
660
|
+
addedCount: Math.max(0, persistedCount - persistedAtStart.count),
|
|
661
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
662
|
+
processed,
|
|
663
|
+
error: 'stuck_in_detail',
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (processedThisRound === 0) {
|
|
669
|
+
stagnantRounds += 1;
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
stagnantRounds = 0;
|
|
673
|
+
}
|
|
674
|
+
if (persistedCount >= targetCount)
|
|
675
|
+
break;
|
|
676
|
+
// 若连续多轮无新增,先尝试“回滚一次再下滚”来触发虚拟列表重排(不改变 keyword,不做纠错)
|
|
677
|
+
if (stagnantRounds >= 4 && bounceAttempts < 3) {
|
|
678
|
+
bounceAttempts += 1;
|
|
679
|
+
console.log(`[FullCollect] stagnantRounds=${stagnantRounds}, bounce=${bounceAttempts} (up then down)`);
|
|
680
|
+
await scrollSearchList('up', 600);
|
|
681
|
+
await scrollSearchList('down', 800);
|
|
682
|
+
stagnantRounds = 0;
|
|
683
|
+
}
|
|
684
|
+
scrollSteps += 1;
|
|
685
|
+
const scrolled = await scrollSearchListDown();
|
|
686
|
+
if (!scrolled) {
|
|
687
|
+
noScrollMoveRounds += 1;
|
|
688
|
+
console.log(`[FullCollect] scroll did not move (round=${noScrollMoveRounds})`);
|
|
689
|
+
// 滚不动:往回滚几次再继续往前滚,触发虚拟列表重排
|
|
690
|
+
// 注意:这里只是滚动策略,不做 keyword 纠错/重搜
|
|
691
|
+
for (let j = 0; j < 3; j += 1) {
|
|
692
|
+
await scrollSearchList('up', 520);
|
|
693
|
+
}
|
|
694
|
+
const movedAfterBounce = await scrollSearchList('down', 800);
|
|
695
|
+
if (movedAfterBounce) {
|
|
696
|
+
noScrollMoveRounds = 0;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
// 连续 3 次滚动都不动:基本可判定触底/卡死,退出避免无限循环
|
|
700
|
+
if (noScrollMoveRounds >= 3) {
|
|
701
|
+
await saveSearchListScrollDebug('search_list_scroll_stuck', {
|
|
702
|
+
persistedCount,
|
|
703
|
+
targetCount,
|
|
704
|
+
scrollSteps,
|
|
705
|
+
noScrollMoveRounds,
|
|
706
|
+
});
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
noScrollMoveRounds = 0;
|
|
712
|
+
const kwOkAfterScroll = await assertKeywordStillCorrect('after_scroll_list');
|
|
713
|
+
if (kwOkAfterScroll !== true) {
|
|
714
|
+
return {
|
|
715
|
+
success: false,
|
|
716
|
+
processedCount: persistedCount,
|
|
717
|
+
targetCount,
|
|
718
|
+
initialPersistedCount: persistedAtStart.count,
|
|
719
|
+
finalPersistedCount: persistedCount,
|
|
720
|
+
addedCount: Math.max(0, persistedCount - persistedAtStart.count),
|
|
721
|
+
keywordDir: persistedAtStart.keywordDir,
|
|
722
|
+
processed,
|
|
723
|
+
error: `keyword_changed_after_scroll_list: ${kwOkAfterScroll.url}`,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const persistedAtEnd = await countPersistedNotes({
|
|
728
|
+
platform: 'xiaohongshu',
|
|
729
|
+
env,
|
|
730
|
+
keyword,
|
|
731
|
+
requiredFiles: [...requiredFiles],
|
|
732
|
+
...(mode === 'phase34' ? { requireCommentsDone: true } : {}),
|
|
733
|
+
});
|
|
734
|
+
// 等待后台 OCR 收尾(已与采集过程并行执行)
|
|
735
|
+
if (ocrQueue) {
|
|
736
|
+
console.log(`[FullCollect][ocr] draining... pending=${ocrQueue.getPendingCount()} running=${ocrQueue.getRunningCount()}`);
|
|
737
|
+
await ocrQueue.drain();
|
|
738
|
+
console.log('[FullCollect][ocr] drained');
|
|
739
|
+
}
|
|
740
|
+
const finalCount = persistedAtEnd.count;
|
|
741
|
+
let mergedMarkdownPath;
|
|
742
|
+
let mergedMarkdownNotes;
|
|
743
|
+
try {
|
|
744
|
+
const merged = await mergeNotesMarkdown({
|
|
745
|
+
platform: 'xiaohongshu',
|
|
746
|
+
env,
|
|
747
|
+
keyword,
|
|
748
|
+
downloadRoot,
|
|
749
|
+
});
|
|
750
|
+
if (merged.success) {
|
|
751
|
+
mergedMarkdownPath = merged.outputPath;
|
|
752
|
+
mergedMarkdownNotes = merged.mergedNotes;
|
|
753
|
+
console.log(`[FullCollect] merged markdown: ${merged.outputPath} (notes=${merged.mergedNotes})`);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
console.warn(`[FullCollect] merge markdown skipped: ${merged.error}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
catch (err) {
|
|
760
|
+
console.warn(`[FullCollect] merge markdown failed: ${err?.message || String(err)}`);
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
success: strictTargetCount ? finalCount === targetCount : finalCount >= targetCount,
|
|
764
|
+
processedCount: finalCount,
|
|
765
|
+
targetCount,
|
|
766
|
+
initialPersistedCount: persistedAtStart.count,
|
|
767
|
+
finalPersistedCount: finalCount,
|
|
768
|
+
addedCount: Math.max(0, finalCount - persistedAtStart.count),
|
|
769
|
+
keywordDir: persistedAtEnd.keywordDir,
|
|
770
|
+
mergedMarkdownPath,
|
|
771
|
+
mergedMarkdownNotes,
|
|
772
|
+
processed,
|
|
773
|
+
...(strictTargetCount
|
|
774
|
+
? finalCount === targetCount
|
|
775
|
+
? {}
|
|
776
|
+
: { error: `persisted_count_mismatch: ${finalCount}/${targetCount}` }
|
|
777
|
+
: finalCount >= targetCount
|
|
778
|
+
? {}
|
|
779
|
+
: { error: `only processed ${finalCount}/${targetCount}` }),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
//# sourceMappingURL=XiaohongshuFullCollectBlock.js.map
|