@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.
- 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 +247 -12
- 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 +37 -9
- 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,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3-4 Block: 关闭详情页(返回搜索页)
|
|
3
|
+
*
|
|
4
|
+
* 职责:从详情页安全返回搜索结果页
|
|
5
|
+
*/
|
|
6
|
+
async function controllerAction(action, payload, apiUrl) {
|
|
7
|
+
const res = await fetch(`${apiUrl}/v1/controller/action`, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
body: JSON.stringify({ action, payload }),
|
|
11
|
+
signal: AbortSignal.timeout(20000),
|
|
12
|
+
});
|
|
13
|
+
const data = await res.json().catch(() => ({}));
|
|
14
|
+
return data.data || data;
|
|
15
|
+
}
|
|
16
|
+
function delay(ms) {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
export async function execute(input) {
|
|
20
|
+
const { profile = 'xiaohongshu_fresh', unifiedApiUrl = 'http://127.0.0.1:7701', } = input;
|
|
21
|
+
console.log(`[Phase34CloseDetail] ESC 返回搜索页`);
|
|
22
|
+
// 系统 ESC 返回
|
|
23
|
+
await controllerAction('keyboard:press', {
|
|
24
|
+
profileId: profile,
|
|
25
|
+
key: 'Escape',
|
|
26
|
+
}, unifiedApiUrl);
|
|
27
|
+
// 等待导航完成
|
|
28
|
+
await delay(1500);
|
|
29
|
+
// 验证已返回搜索页
|
|
30
|
+
const finalUrl = await controllerAction('browser:execute', {
|
|
31
|
+
profile,
|
|
32
|
+
script: 'window.location.href'
|
|
33
|
+
}, unifiedApiUrl).then(res => res?.result || res?.data?.result || '');
|
|
34
|
+
const success = finalUrl.includes('/search_result');
|
|
35
|
+
console.log(`[Phase34CloseDetail] 完成: success=${success} url=${finalUrl}`);
|
|
36
|
+
return {
|
|
37
|
+
success,
|
|
38
|
+
finalUrl,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=Phase34CloseDetailBlock.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3-4 Block: 关闭所有打开的 Tab
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 接收 Tab 列表
|
|
6
|
+
* 2. 循环调用 browser:close_page 关闭每个 Tab
|
|
7
|
+
* 3. 返回关闭结果
|
|
8
|
+
*/
|
|
9
|
+
import { controllerAction } from '../utils/controllerAction.js';
|
|
10
|
+
export async function execute(input) {
|
|
11
|
+
const { sessionId, tabs = [], unifiedApiUrl = 'http://127.0.0.1:7701' } = input;
|
|
12
|
+
console.log(`[Phase34CloseTabs] 关闭 ${tabs.length} 个 Tab`);
|
|
13
|
+
let closedCount = 0;
|
|
14
|
+
let failedCount = 0;
|
|
15
|
+
const errors = [];
|
|
16
|
+
for (const tab of tabs) {
|
|
17
|
+
if (!tab?.pageId) {
|
|
18
|
+
console.warn(`[Phase34CloseTabs] 跳过无效 Tab: ${JSON.stringify(tab)}`);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
await controllerAction('browser:close_page', {
|
|
23
|
+
profile: sessionId,
|
|
24
|
+
pageId: tab.pageId
|
|
25
|
+
}, unifiedApiUrl);
|
|
26
|
+
closedCount++;
|
|
27
|
+
console.log(`[Phase34CloseTabs] 已关闭 Tab pageId=${tab.pageId}`);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
failedCount++;
|
|
31
|
+
const errorMsg = err?.message || String(err);
|
|
32
|
+
errors.push({ pageId: tab.pageId, error: errorMsg });
|
|
33
|
+
console.warn(`[Phase34CloseTabs] 关闭失败 pageId=${tab.pageId}: ${errorMsg}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
console.log(`[Phase34CloseTabs] 完成: 成功 ${closedCount}, 失败 ${failedCount}`);
|
|
37
|
+
return {
|
|
38
|
+
success: failedCount === 0,
|
|
39
|
+
closedCount,
|
|
40
|
+
failedCount,
|
|
41
|
+
errors: errors.length > 0 ? errors : undefined
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=Phase34CloseTabsBlock.js.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3-4 Block: 采集评论
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 展开评论区(系统级点击 comment_button)
|
|
6
|
+
* 2. 批量采集可见评论(comment_item extract)
|
|
7
|
+
* 3. 点击“展开更多”+评论区滚动加载
|
|
8
|
+
* 4. 返回去重后的评论数组
|
|
9
|
+
*/
|
|
10
|
+
import { ensureCommentsOpened, extractVisibleComments, isCommentEnd, scrollComments, expandAllVisibleReplyButtons, } from './helpers/xhsComments.js';
|
|
11
|
+
import { controllerAction, delay } from '../utils/controllerAction.js';
|
|
12
|
+
async function isCommentEmpty(sessionId, apiUrl) {
|
|
13
|
+
const res = await controllerAction('container:operation', {
|
|
14
|
+
containerId: 'xiaohongshu_detail.comment_section.empty_state',
|
|
15
|
+
operationId: 'extract',
|
|
16
|
+
sessionId,
|
|
17
|
+
config: { max_items: 1, visibleOnly: true },
|
|
18
|
+
}, apiUrl).catch(() => null);
|
|
19
|
+
const extracted = Array.isArray(res?.extracted) ? res.extracted : [];
|
|
20
|
+
return extracted.length > 0;
|
|
21
|
+
}
|
|
22
|
+
function toNormalizedComment(item) {
|
|
23
|
+
return {
|
|
24
|
+
userName: String(item.user_name || ''),
|
|
25
|
+
userId: String(item.user_id || ''),
|
|
26
|
+
content: String(item.text || '').replace(/\s+/g, ' ').trim(),
|
|
27
|
+
time: String(item.timestamp || ''),
|
|
28
|
+
likeCount: 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export async function execute(input) {
|
|
32
|
+
const { sessionId = 'xiaohongshu_fresh', unifiedApiUrl = 'http://127.0.0.1:7701', maxRounds = 0, batchSize = 0, } = input;
|
|
33
|
+
const roundLimit = Number.isFinite(Number(maxRounds)) && Number(maxRounds) > 0
|
|
34
|
+
? Math.floor(Number(maxRounds))
|
|
35
|
+
: Number.POSITIVE_INFINITY;
|
|
36
|
+
const batchLimit = Number.isFinite(Number(batchSize)) && Number(batchSize) > 0
|
|
37
|
+
? Math.floor(Number(batchSize))
|
|
38
|
+
: Number.POSITIVE_INFINITY;
|
|
39
|
+
console.log(`[Phase34CollectComments] 开始采集评论 (batchSize=${Number.isFinite(batchLimit) ? batchLimit : 'unlimited'}, maxRounds=${Number.isFinite(roundLimit) ? roundLimit : 'unlimited'})`);
|
|
40
|
+
const allComments = [];
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
let round = 0;
|
|
43
|
+
let noNewCount = 0;
|
|
44
|
+
let expandNoClickStreak = 0;
|
|
45
|
+
const MAX_NO_NEW = 3;
|
|
46
|
+
// 1) 展开评论区(允许已展开/无按钮时继续尝试 extract)
|
|
47
|
+
try {
|
|
48
|
+
await ensureCommentsOpened(sessionId, unifiedApiUrl);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.warn(`[Phase34CollectComments] 评论区展开异常: ${err?.message || String(err)}`);
|
|
52
|
+
}
|
|
53
|
+
await delay(800);
|
|
54
|
+
while (round < roundLimit) {
|
|
55
|
+
round += 1;
|
|
56
|
+
const empty = await isCommentEmpty(sessionId, unifiedApiUrl);
|
|
57
|
+
if (empty) {
|
|
58
|
+
console.log('[Phase34CollectComments] 评论区空状态,停止采集');
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
const expandNow = await expandAllVisibleReplyButtons(sessionId, unifiedApiUrl, {
|
|
62
|
+
maxPasses: 6,
|
|
63
|
+
maxClicksPerPass: 12,
|
|
64
|
+
}).catch(() => ({ clicked: 0, passes: 0, remaining: 0, detected: 0 }));
|
|
65
|
+
if (expandNow.clicked > 0 || expandNow.remaining > 0) {
|
|
66
|
+
console.log(`[Phase34CollectComments] Round ${round}: 先展开可见回复 clicked=${expandNow.clicked} remaining=${expandNow.remaining}`);
|
|
67
|
+
await delay(320);
|
|
68
|
+
}
|
|
69
|
+
const remaining = Number.isFinite(batchLimit)
|
|
70
|
+
? Math.max(1, batchLimit - allComments.length)
|
|
71
|
+
: 200;
|
|
72
|
+
const visible = await extractVisibleComments(sessionId, unifiedApiUrl, Math.min(200, remaining));
|
|
73
|
+
const prev = allComments.length;
|
|
74
|
+
for (const item of visible) {
|
|
75
|
+
const normalized = toNormalizedComment(item);
|
|
76
|
+
if (!normalized.content)
|
|
77
|
+
continue;
|
|
78
|
+
const key = `${normalized.userId}:${normalized.content}`;
|
|
79
|
+
if (seen.has(key))
|
|
80
|
+
continue;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
allComments.push(normalized);
|
|
83
|
+
}
|
|
84
|
+
const newCount = allComments.length - prev;
|
|
85
|
+
console.log(`[Phase34CollectComments] Round ${round}: 可见${visible.length} 新增${newCount} 总计${allComments.length}`);
|
|
86
|
+
if (newCount > 0) {
|
|
87
|
+
noNewCount = 0;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
noNewCount += 1;
|
|
91
|
+
console.log(`[Phase34CollectComments] 无新评论计数: ${noNewCount}/${MAX_NO_NEW}`);
|
|
92
|
+
}
|
|
93
|
+
if (Number.isFinite(batchLimit) && allComments.length >= batchLimit) {
|
|
94
|
+
console.log(`[Phase34CollectComments] 已达到批次大小 ${batchLimit},停止采集`);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
const endReached = await isCommentEnd(sessionId, unifiedApiUrl).catch(() => false);
|
|
98
|
+
if (endReached) {
|
|
99
|
+
console.log('[Phase34CollectComments] 检测到评论区底部,停止采集');
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (noNewCount >= MAX_NO_NEW) {
|
|
103
|
+
console.log(`[Phase34CollectComments] 连续 ${MAX_NO_NEW} 轮无新增,停止采集`);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
// 滚动前先把视口内“展开回复”按钮点完,再决定是否滚动。
|
|
107
|
+
const expandBeforeScroll = await expandAllVisibleReplyButtons(sessionId, unifiedApiUrl, {
|
|
108
|
+
maxPasses: 8,
|
|
109
|
+
maxClicksPerPass: 15,
|
|
110
|
+
}).catch(() => ({ clicked: 0, passes: 0, remaining: 0, detected: 0 }));
|
|
111
|
+
if (expandBeforeScroll.clicked > 0) {
|
|
112
|
+
expandNoClickStreak = 0;
|
|
113
|
+
console.log(`[Phase34CollectComments] Round ${round}: 滚动前展开 clicked=${expandBeforeScroll.clicked} remaining=${expandBeforeScroll.remaining},先重采当前视口`);
|
|
114
|
+
await delay(420);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (expandBeforeScroll.remaining > 0) {
|
|
118
|
+
expandNoClickStreak += 1;
|
|
119
|
+
console.log(`[Phase34CollectComments] Round ${round}: 检测到 remaining=${expandBeforeScroll.remaining} 但点击为0 (streak=${expandNoClickStreak})`);
|
|
120
|
+
if (expandNoClickStreak < 2) {
|
|
121
|
+
await delay(420);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
console.log('[Phase34CollectComments] 展开按钮无法点击,强制执行滚动以避免卡住');
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
expandNoClickStreak = 0;
|
|
128
|
+
}
|
|
129
|
+
let scrolled = null;
|
|
130
|
+
try {
|
|
131
|
+
console.log(`[Phase34CollectComments] Round ${round}: 无可展开按钮,执行滚动`);
|
|
132
|
+
scrolled = await scrollComments(sessionId, unifiedApiUrl, 650);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
scrolled = null;
|
|
136
|
+
}
|
|
137
|
+
if (scrolled?.success === false) {
|
|
138
|
+
console.log('[Phase34CollectComments] 评论区滚动失败,停止采集');
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
await delay(900);
|
|
142
|
+
}
|
|
143
|
+
console.log(`[Phase34CollectComments] 完成,共采集 ${allComments.length} 条评论`);
|
|
144
|
+
return {
|
|
145
|
+
success: true,
|
|
146
|
+
comments: allComments,
|
|
147
|
+
totalCollected: allComments.length,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=Phase34CollectCommentsBlock.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3-4 Block: 提取详情内容
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 容器匹配详情页内容区域
|
|
6
|
+
* 2. 高亮验证内容可见
|
|
7
|
+
* 3. 容器操作提取标题、正文、作者、图片
|
|
8
|
+
* 4. 返回结构化数据 + anchor
|
|
9
|
+
*/
|
|
10
|
+
async function controllerAction(action, payload, apiUrl) {
|
|
11
|
+
const res = await fetch(`${apiUrl}/v1/controller/action`, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: { 'Content-Type': 'application/json' },
|
|
14
|
+
body: JSON.stringify({ action, payload }),
|
|
15
|
+
signal: AbortSignal.timeout(20000),
|
|
16
|
+
});
|
|
17
|
+
const data = await res.json().catch(() => ({}));
|
|
18
|
+
return data.data || data;
|
|
19
|
+
}
|
|
20
|
+
function delay(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
export async function execute(input) {
|
|
24
|
+
const { noteId, profile = 'xiaohongshu_fresh', unifiedApiUrl = 'http://127.0.0.1:7701', } = input;
|
|
25
|
+
console.log(`[Phase34ExtractDetail] 提取详情: ${noteId}`);
|
|
26
|
+
// 1. 验证当前在详情页
|
|
27
|
+
const currentUrl = await controllerAction('browser:execute', {
|
|
28
|
+
profile,
|
|
29
|
+
script: 'window.location.href',
|
|
30
|
+
}, unifiedApiUrl).then(res => res?.result || res?.data?.result || '');
|
|
31
|
+
if (!currentUrl.includes(`/explore/${noteId}`)) {
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
noteId,
|
|
35
|
+
error: `当前不在详情页: ${currentUrl}`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// 2. 容器匹配内容区域(以 container-library 为准)
|
|
39
|
+
const contentContainerId = 'xiaohongshu_detail.content';
|
|
40
|
+
const headerContainerId = 'xiaohongshu_detail.header';
|
|
41
|
+
const galleryContainerId = 'xiaohongshu_detail.gallery';
|
|
42
|
+
// 3. 高亮验证
|
|
43
|
+
const highlightResult = await controllerAction('container:operation', {
|
|
44
|
+
containerId: contentContainerId,
|
|
45
|
+
operationId: 'highlight',
|
|
46
|
+
sessionId: profile,
|
|
47
|
+
}, unifiedApiUrl);
|
|
48
|
+
if (!highlightResult?.success) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
noteId,
|
|
52
|
+
error: `内容区域不可用: ${contentContainerId}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
await delay(500);
|
|
56
|
+
// 4. 提取正文(包含 title/text)
|
|
57
|
+
const contentResult = await controllerAction('container:operation', {
|
|
58
|
+
containerId: contentContainerId,
|
|
59
|
+
operationId: 'extract',
|
|
60
|
+
sessionId: profile,
|
|
61
|
+
config: { fields: ['title', 'text'] },
|
|
62
|
+
}, unifiedApiUrl).catch(() => null);
|
|
63
|
+
const contentRow = Array.isArray(contentResult?.extracted) ? contentResult.extracted[0] : (contentResult?.data?.extracted?.[0] ?? null);
|
|
64
|
+
const title = String(contentRow?.title || '');
|
|
65
|
+
const content = String(contentRow?.text || '');
|
|
66
|
+
// 5. 提取作者
|
|
67
|
+
const headerResult = await controllerAction('container:operation', {
|
|
68
|
+
containerId: headerContainerId,
|
|
69
|
+
operationId: 'extract',
|
|
70
|
+
sessionId: profile,
|
|
71
|
+
config: { fields: ['author_name', 'author_link'] },
|
|
72
|
+
}, unifiedApiUrl).catch(() => null);
|
|
73
|
+
const headerRow = Array.isArray(headerResult?.extracted) ? headerResult.extracted[0] : (headerResult?.data?.extracted?.[0] ?? null);
|
|
74
|
+
const authorName = String(headerRow?.author_name || '');
|
|
75
|
+
const authorLink = String(headerRow?.author_link || '');
|
|
76
|
+
let authorId = '';
|
|
77
|
+
try {
|
|
78
|
+
if (authorLink) {
|
|
79
|
+
const u = new URL(authorLink, 'https://www.xiaohongshu.com');
|
|
80
|
+
authorId = u.pathname.split('/').filter(Boolean).slice(-1)[0] || '';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
authorId = '';
|
|
85
|
+
}
|
|
86
|
+
// 6. 提取图片
|
|
87
|
+
const galleryResult = await controllerAction('container:operation', {
|
|
88
|
+
containerId: galleryContainerId,
|
|
89
|
+
operationId: 'extract',
|
|
90
|
+
sessionId: profile,
|
|
91
|
+
config: { fields: ['images'] },
|
|
92
|
+
}, unifiedApiUrl).catch(() => null);
|
|
93
|
+
const galleryRow = Array.isArray(galleryResult?.extracted)
|
|
94
|
+
? galleryResult.extracted[0]
|
|
95
|
+
: (galleryResult?.data?.extracted?.[0] ?? null);
|
|
96
|
+
const images = Array.isArray(galleryRow?.images) ? galleryRow.images : (galleryRow?.images ? [galleryRow.images] : []);
|
|
97
|
+
// 8. 获取 anchor(用于调试)
|
|
98
|
+
const rect = highlightResult?.anchor?.rect || highlightResult?.rect;
|
|
99
|
+
console.log(`[Phase34ExtractDetail] ✅ 提取完成: 标题="${title.slice(0, 20)}..." 图片=${images.length}张`);
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
noteId,
|
|
103
|
+
detail: {
|
|
104
|
+
title,
|
|
105
|
+
content,
|
|
106
|
+
authorName,
|
|
107
|
+
authorId,
|
|
108
|
+
publishTime: new Date().toISOString(),
|
|
109
|
+
images: Array.isArray(images) ? images : [images],
|
|
110
|
+
},
|
|
111
|
+
anchor: {
|
|
112
|
+
containerId: contentContainerId,
|
|
113
|
+
rect: rect || { x: 0, y: 0, width: 0, height: 0 },
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=Phase34ExtractDetailBlock.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3-4 Block: 打开详情页
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 使用 safeUrl 打开详情页(禁止构造 URL)
|
|
6
|
+
* 2. 等待页面加载完成
|
|
7
|
+
* 3. 高亮验证内容区域可见
|
|
8
|
+
*/
|
|
9
|
+
async function controllerAction(action, payload, apiUrl) {
|
|
10
|
+
const res = await fetch(`${apiUrl}/v1/controller/action`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13
|
+
body: JSON.stringify({ action, payload }),
|
|
14
|
+
signal: AbortSignal.timeout(30000),
|
|
15
|
+
});
|
|
16
|
+
const data = await res.json().catch(() => ({}));
|
|
17
|
+
return data.data || data;
|
|
18
|
+
}
|
|
19
|
+
function delay(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
async function getViewportHeight(profile, apiUrl) {
|
|
23
|
+
try {
|
|
24
|
+
const res = await controllerAction('browser:execute', {
|
|
25
|
+
profile,
|
|
26
|
+
script: 'window.innerHeight',
|
|
27
|
+
}, apiUrl);
|
|
28
|
+
const h = Number(res?.result || res?.data?.result || res?.data || 0);
|
|
29
|
+
return Number.isFinite(h) && h > 0 ? h : 0;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function execute(input) {
|
|
36
|
+
const { noteId, safeUrl, profile = 'xiaohongshu_fresh', unifiedApiUrl = 'http://127.0.0.1:7701', } = input;
|
|
37
|
+
console.log(`[Phase34OpenDetail] 打开详情页: ${noteId}`);
|
|
38
|
+
// 1. 校验 safeUrl 包含 xsec_token
|
|
39
|
+
if (!safeUrl.includes('xsec_token')) {
|
|
40
|
+
throw new Error(`[Phase34OpenDetail] safeUrl 缺少 xsec_token: ${safeUrl}`);
|
|
41
|
+
}
|
|
42
|
+
// 2. 使用 browser:goto 导航到详情页
|
|
43
|
+
const gotoResult = await controllerAction('browser:goto', {
|
|
44
|
+
profile,
|
|
45
|
+
url: safeUrl,
|
|
46
|
+
}, unifiedApiUrl);
|
|
47
|
+
// controllerAction 返回形态可能是:
|
|
48
|
+
// - { ok: true }
|
|
49
|
+
// - { success: true, data: { ok: true } }
|
|
50
|
+
// - { success: false, error: ... }
|
|
51
|
+
// 这里把 browser-service 的底层错误透传出来,避免 “unknown” 无法诊断。
|
|
52
|
+
const okFlag = gotoResult?.ok === true ||
|
|
53
|
+
gotoResult?.data?.ok === true ||
|
|
54
|
+
gotoResult?.success === true;
|
|
55
|
+
const gotoOk = Boolean(okFlag);
|
|
56
|
+
if (!gotoOk) {
|
|
57
|
+
const detail = gotoResult?.data?.error || gotoResult?.error || JSON.stringify(gotoResult);
|
|
58
|
+
throw new Error(`[Phase34OpenDetail] 导航失败: ${detail}`);
|
|
59
|
+
}
|
|
60
|
+
// 3. 等待页面加载完成
|
|
61
|
+
await delay(3000);
|
|
62
|
+
// 4. 验证当前 URL 匹配 noteId
|
|
63
|
+
const currentUrl = await controllerAction('browser:execute', {
|
|
64
|
+
profile,
|
|
65
|
+
script: 'window.location.href',
|
|
66
|
+
}, unifiedApiUrl).then(res => res?.result || res?.data?.result || '');
|
|
67
|
+
if (!currentUrl.includes(noteId)) {
|
|
68
|
+
throw new Error(`[Phase34OpenDetail] URL 不匹配,期望 ${noteId},实际 ${currentUrl}`);
|
|
69
|
+
}
|
|
70
|
+
// 5. 高亮验证内容区域可见
|
|
71
|
+
// 容器命名以 container-library 为准:详情正文容器为 xiaohongshu_detail.content。
|
|
72
|
+
const highlightResult = await controllerAction('container:operation', {
|
|
73
|
+
containerId: 'xiaohongshu_detail.content',
|
|
74
|
+
operationId: 'highlight',
|
|
75
|
+
sessionId: profile,
|
|
76
|
+
}, unifiedApiUrl);
|
|
77
|
+
if (highlightResult?.success === false) {
|
|
78
|
+
throw new Error(`[Phase34OpenDetail] 内容区域不可见: xiaohongshu_detail.content`);
|
|
79
|
+
}
|
|
80
|
+
// 6. 视口保护:只做“可见性/有尺寸”校验,不强制要求完全在视口内。
|
|
81
|
+
// 说明:在 Camoufox + 多窗口情况下,window.innerHeight 可能固定较小(如 707),
|
|
82
|
+
// 但页面仍可正常交互。强制 in-viewport 会导致误判并中断。
|
|
83
|
+
const rect = highlightResult?.anchor?.rect || highlightResult?.rect;
|
|
84
|
+
if (rect) {
|
|
85
|
+
const width = Number(rect.width ?? (rect.x2 - rect.x1));
|
|
86
|
+
const height = Number(rect.height ?? (rect.y2 - rect.y1));
|
|
87
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
88
|
+
throw new Error(`[Phase34OpenDetail] 内容区域 rect 无效: ${JSON.stringify(rect)}`);
|
|
89
|
+
}
|
|
90
|
+
const viewportHeightRaw = await getViewportHeight(profile, unifiedApiUrl);
|
|
91
|
+
console.log(`[Phase34OpenDetail] rect ok (w=${width}, h=${height}), viewportHeight=${viewportHeightRaw}`);
|
|
92
|
+
}
|
|
93
|
+
await delay(500);
|
|
94
|
+
console.log(`[Phase34OpenDetail] ✅ 详情页加载完成: ${noteId}`);
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
noteId,
|
|
98
|
+
currentUrl,
|
|
99
|
+
anchor: highlightResult?.anchor,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=Phase34OpenDetailBlock.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3-4 Block: 确保固定 Tab 池(5 个:0=搜索页, 1-4=帖子详情页)
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 检查当前 tab 数量,若不足 5 个则补齐到 5 个
|
|
6
|
+
* 2. 返回固定 tab 池索引 [0, 1, 2, 3, 4](0 保留给搜索页,1-4 用于帖子轮转)
|
|
7
|
+
* 3. 确保后续轮转使用固定 tab,不再开新 tab
|
|
8
|
+
*/
|
|
9
|
+
async function controllerAction(action, payload, apiUrl) {
|
|
10
|
+
const res = await fetch(`${apiUrl}/v1/controller/action`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13
|
+
body: JSON.stringify({ action, payload }),
|
|
14
|
+
signal: AbortSignal.timeout(20000),
|
|
15
|
+
});
|
|
16
|
+
const data = await res.json().catch(() => ({}));
|
|
17
|
+
return data.data || data;
|
|
18
|
+
}
|
|
19
|
+
async function listPages(profile, apiUrl) {
|
|
20
|
+
const res = await controllerAction('browser:page:list', { profileId: profile }, apiUrl);
|
|
21
|
+
if (res?.error) {
|
|
22
|
+
console.warn(`[Phase34OpenTabs] page:list error: ${res.error}`);
|
|
23
|
+
}
|
|
24
|
+
return res?.pages || res?.data?.pages || [];
|
|
25
|
+
}
|
|
26
|
+
async function getInputMode(apiUrl) {
|
|
27
|
+
const res = await controllerAction('system:input-mode:get', {}, apiUrl).catch(() => null);
|
|
28
|
+
const mode = String(res?.data?.mode || res?.mode || 'system').trim().toLowerCase();
|
|
29
|
+
return mode === 'protocol' ? 'protocol' : 'system';
|
|
30
|
+
}
|
|
31
|
+
async function openTab(profile, apiUrl, inputMode) {
|
|
32
|
+
const maxRetries = 4;
|
|
33
|
+
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
|
34
|
+
const before = await listPages(profile, apiUrl);
|
|
35
|
+
const beforeCount = before.length;
|
|
36
|
+
const tryCountIncrease = async (waitMs = 450) => {
|
|
37
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
38
|
+
const after = await listPages(profile, apiUrl);
|
|
39
|
+
return after.length > beforeCount;
|
|
40
|
+
};
|
|
41
|
+
if (inputMode === 'protocol') {
|
|
42
|
+
await controllerAction('browser:page:new', { profileId: profile }, apiUrl).catch(() => null);
|
|
43
|
+
if (await tryCountIncrease(350))
|
|
44
|
+
return;
|
|
45
|
+
await controllerAction('browser:execute', { profile, script: 'window.open("about:blank", "_blank"); true;' }, apiUrl).catch(() => null);
|
|
46
|
+
if (await tryCountIncrease(350))
|
|
47
|
+
return;
|
|
48
|
+
await controllerAction('browser:page:switch', { profileId: profile, index: 0 }, apiUrl).catch(() => null);
|
|
49
|
+
await controllerAction('system:shortcut', { app: 'camoufox', shortcut: 'new-tab' }, apiUrl).catch(() => null);
|
|
50
|
+
if (await tryCountIncrease(500)) {
|
|
51
|
+
console.log('[Phase34OpenTabs] protocol fallback -> system shortcut succeeded');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
await controllerAction('browser:page:switch', { profileId: profile, index: 0 }, apiUrl).catch(() => null);
|
|
57
|
+
await controllerAction('system:shortcut', { app: 'camoufox', shortcut: 'new-tab' }, apiUrl).catch(() => null);
|
|
58
|
+
if (await tryCountIncrease(500))
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.warn(`[Phase34OpenTabs] ${inputMode} open-tab attempt ${attempt}/${maxRetries} failed (count=${beforeCount})`);
|
|
62
|
+
}
|
|
63
|
+
throw new Error('open_tab_failed_after_retries');
|
|
64
|
+
}
|
|
65
|
+
export async function execute(input) {
|
|
66
|
+
const { profile = 'xiaohongshu_fresh', tabCount = 4, unifiedApiUrl = 'http://127.0.0.1:7701', } = input;
|
|
67
|
+
const requiredTotal = tabCount + 1; // tab0=搜索页 + tab1~4=帖子页
|
|
68
|
+
console.log(`[Phase34OpenTabs] 确保固定 tab 池: ${requiredTotal} 个 (0=搜索页, 1-${tabCount}=帖子页)`);
|
|
69
|
+
const inputMode = await getInputMode(unifiedApiUrl);
|
|
70
|
+
console.log(`[Phase34OpenTabs] input mode: ${inputMode}`);
|
|
71
|
+
let existing = await listPages(profile, unifiedApiUrl);
|
|
72
|
+
if (!existing.length) {
|
|
73
|
+
console.log('[Phase34OpenTabs] 未检测到 session,尝试创建会话');
|
|
74
|
+
await controllerAction('session:create', { profile, url: 'https://www.xiaohongshu.com' }, unifiedApiUrl);
|
|
75
|
+
await new Promise(r => setTimeout(r, 800));
|
|
76
|
+
existing = await listPages(profile, unifiedApiUrl);
|
|
77
|
+
}
|
|
78
|
+
const currentCount = existing.length;
|
|
79
|
+
console.log(`[Phase34OpenTabs] 当前已有 ${currentCount} 个 tab`);
|
|
80
|
+
const needed = Math.max(0, requiredTotal - currentCount);
|
|
81
|
+
if (needed > 0) {
|
|
82
|
+
console.log(`[Phase34OpenTabs] 需要补齐 ${needed} 个 tab`);
|
|
83
|
+
for (let i = 0; i < needed; i++) {
|
|
84
|
+
await openTab(profile, unifiedApiUrl, inputMode);
|
|
85
|
+
console.log(`[Phase34OpenTabs] 新 tab ${currentCount + i + 1} 已打开`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log(`[Phase34OpenTabs] tab 数量已满足,无需新开`);
|
|
90
|
+
}
|
|
91
|
+
// 验证最终 tab 池
|
|
92
|
+
const finalPages = await listPages(profile, unifiedApiUrl);
|
|
93
|
+
console.log(`[Phase34OpenTabs] 最终 tab 池: ${finalPages.length} 个`);
|
|
94
|
+
finalPages.forEach((p, i) => {
|
|
95
|
+
const role = i === 0 ? '(搜索页)' : `(帖子页-${i})`;
|
|
96
|
+
console.log(` [${i}] index=${p.index} ${role} url=${p.url?.substring(0, 60)}`);
|
|
97
|
+
});
|
|
98
|
+
// 返回固定 tab 索引池(跳过 tab0,返回 tab1~4)
|
|
99
|
+
const tabs = finalPages.slice(1, requiredTotal).map((p) => ({
|
|
100
|
+
index: p.index,
|
|
101
|
+
pageId: undefined,
|
|
102
|
+
}));
|
|
103
|
+
console.log(`[Phase34OpenTabs] 固定帖子页 tab 索引: [${tabs.map(t => t.index).join(', ')}]`);
|
|
104
|
+
return {
|
|
105
|
+
tabs,
|
|
106
|
+
currentTab: 0,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=Phase34OpenTabsBlock.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3-4 Block: 持久化详情内容
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 创建目录:~/.webauto/download/xiaohongshu/{env}/{keyword}/{noteId}/
|
|
6
|
+
* - 下载图片到 images/ 目录
|
|
7
|
+
* - 生成 README.md(含相对路径引用)
|
|
8
|
+
*/
|
|
9
|
+
import { promises as fs } from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
function resolveDownloadRoot() {
|
|
13
|
+
const custom = process.env.WEBAUTO_DOWNLOAD_ROOT || process.env.WEBAUTO_DOWNLOAD_DIR;
|
|
14
|
+
if (custom && custom.trim())
|
|
15
|
+
return custom;
|
|
16
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
17
|
+
return path.join(home, '.webauto', 'download');
|
|
18
|
+
}
|
|
19
|
+
function delay(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
async function ensureDir(dir) {
|
|
23
|
+
await fs.mkdir(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
async function downloadImage(url, destPath, timeout = 30000) {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`HTTP ${response.status}`);
|
|
32
|
+
}
|
|
33
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
34
|
+
await fs.writeFile(destPath, buffer);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function getFileExtension(url) {
|
|
41
|
+
const match = url.match(/\\.([a-z0-9]+)(?:\\?|$)/i);
|
|
42
|
+
return match ? `.${match[1].toLowerCase()}` : '.jpg';
|
|
43
|
+
}
|
|
44
|
+
export async function execute(input) {
|
|
45
|
+
const { noteId, detail, keyword, env = 'download', unifiedApiUrl = 'http://127.0.0.1:7701', } = input;
|
|
46
|
+
console.log(`[Phase34PersistDetail] 持久化: ${noteId}`);
|
|
47
|
+
try {
|
|
48
|
+
// 1. 创建目录结构
|
|
49
|
+
const baseDir = path.join(resolveDownloadRoot(), 'xiaohongshu', env, keyword, noteId);
|
|
50
|
+
const imagesDir = path.join(baseDir, 'images');
|
|
51
|
+
const readmePath = path.join(baseDir, 'README.md');
|
|
52
|
+
await ensureDir(baseDir);
|
|
53
|
+
await ensureDir(imagesDir);
|
|
54
|
+
// 2. 下载图片
|
|
55
|
+
const images = detail.images || [];
|
|
56
|
+
let downloadedCount = 0;
|
|
57
|
+
for (let i = 0; i < images.length; i++) {
|
|
58
|
+
try {
|
|
59
|
+
const imgUrl = images[i];
|
|
60
|
+
const ext = getFileExtension(imgUrl);
|
|
61
|
+
const destPath = path.join(imagesDir, `${i}${ext}`);
|
|
62
|
+
console.log(`[Phase34PersistDetail] 下载图片 ${i + 1}/${images.length}: ${imgUrl.slice(0, 60)}...`);
|
|
63
|
+
await downloadImage(imgUrl, destPath);
|
|
64
|
+
downloadedCount++;
|
|
65
|
+
// 避免请求过快
|
|
66
|
+
if (i < images.length - 1) {
|
|
67
|
+
await delay(200);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.warn(`[Phase34PersistDetail] 图片下载失败 [${i}]: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// 3. 生成 README.md
|
|
75
|
+
const lines = [];
|
|
76
|
+
lines.push(`# ${detail.title || '无标题'}`);
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push(`## 元数据`);
|
|
79
|
+
lines.push(`- **Note ID**: ${noteId}`);
|
|
80
|
+
lines.push(`- **作者**: ${detail.authorName || '未知'} (${detail.authorId || 'N/A'})`);
|
|
81
|
+
lines.push(`- **发布时间**: ${detail.publishTime || '未知'}`);
|
|
82
|
+
lines.push(`- **原始链接**: \`https://www.xiaohongshu.com/explore/${noteId}\``);
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push(`## 正文`);
|
|
85
|
+
lines.push(detail.content || '无正文内容');
|
|
86
|
+
lines.push('');
|
|
87
|
+
if (downloadedCount > 0) {
|
|
88
|
+
lines.push(`## 图片 (${downloadedCount}张)`);
|
|
89
|
+
for (let i = 0; i < downloadedCount; i++) {
|
|
90
|
+
lines.push(``);
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
const readmeContent = lines.join('\\n');
|
|
95
|
+
await fs.writeFile(readmePath, readmeContent, 'utf8');
|
|
96
|
+
console.log(`[Phase34PersistDetail] ✅ 持久化完成: ${baseDir}`);
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
noteDir: baseDir,
|
|
100
|
+
readmePath,
|
|
101
|
+
imagesDir,
|
|
102
|
+
imageCount: downloadedCount,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(`[Phase34PersistDetail] ❌ 失败: ${err.message}`);
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
noteDir: '',
|
|
110
|
+
readmePath: '',
|
|
111
|
+
imagesDir: '',
|
|
112
|
+
imageCount: 0,
|
|
113
|
+
error: err.message,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=Phase34PersistDetailBlock.js.map
|