@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,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2 Block: 执行搜索
|
|
3
|
+
*
|
|
4
|
+
* 职责:通过容器系统执行搜索操作(全系统级操作)
|
|
5
|
+
*/
|
|
6
|
+
import { detectXhsCheckpoint, ensureXhsCheckpoint } from '../utils/checkpoints.js';
|
|
7
|
+
import { controllerAction, delay } from '../utils/controllerAction.js';
|
|
8
|
+
function isDebugArtifactsEnabled() {
|
|
9
|
+
return (process.env.WEBAUTO_DEBUG === '1' ||
|
|
10
|
+
process.env.WEBAUTO_DEBUG_ARTIFACTS === '1' ||
|
|
11
|
+
process.env.WEBAUTO_DEBUG_SCREENSHOT === '1');
|
|
12
|
+
}
|
|
13
|
+
// controllerAction/delay are shared utilities (with a safer timeout) to avoid
|
|
14
|
+
// per-block drift that breaks regression flows.
|
|
15
|
+
async function readSearchInputValue(profile, unifiedApiUrl) {
|
|
16
|
+
const value = await controllerAction('browser:execute', {
|
|
17
|
+
profile,
|
|
18
|
+
script: `(() => {
|
|
19
|
+
const root =
|
|
20
|
+
document.querySelector('#search-input') ||
|
|
21
|
+
document.querySelector("input[type='search']") ||
|
|
22
|
+
document.querySelector("input[placeholder*='搜索'], input[placeholder*='关键字']");
|
|
23
|
+
if (!root) return null;
|
|
24
|
+
|
|
25
|
+
// 兼容:站点更新后 #search-input 可能是 wrapper/div,而不是 input
|
|
26
|
+
const el =
|
|
27
|
+
('value' in root)
|
|
28
|
+
? root
|
|
29
|
+
: (root.querySelector('input, textarea, [contenteditable="true"], [contenteditable=""]') || root);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (el && typeof el === 'object' && 'value' in el) {
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
const v = el.value;
|
|
35
|
+
return typeof v === 'string' ? v : String(v ?? '');
|
|
36
|
+
}
|
|
37
|
+
const text = (el && 'textContent' in el) ? (el.textContent ?? '') : '';
|
|
38
|
+
return typeof text === 'string' ? text : String(text ?? '');
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
})()`,
|
|
43
|
+
}, unifiedApiUrl).then((res) => res?.result || res?.data?.result || null);
|
|
44
|
+
return typeof value === 'string' ? value : null;
|
|
45
|
+
}
|
|
46
|
+
async function readActiveInputValue(profile, unifiedApiUrl) {
|
|
47
|
+
const value = await controllerAction('browser:execute', {
|
|
48
|
+
profile,
|
|
49
|
+
script: `(() => {
|
|
50
|
+
const el = document.activeElement;
|
|
51
|
+
if (!el) return null;
|
|
52
|
+
const input = (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)
|
|
53
|
+
? el
|
|
54
|
+
: (el.querySelector?.('input, textarea') || el);
|
|
55
|
+
if (input && 'value' in input) {
|
|
56
|
+
// @ts-ignore
|
|
57
|
+
const v = input.value;
|
|
58
|
+
return typeof v === 'string' ? v : String(v ?? '');
|
|
59
|
+
}
|
|
60
|
+
const text = (input && 'textContent' in input) ? (input.textContent ?? '') : '';
|
|
61
|
+
return typeof text === 'string' ? text : String(text ?? '');
|
|
62
|
+
})()`,
|
|
63
|
+
}, unifiedApiUrl).then((res) => res?.result || res?.data?.result || null);
|
|
64
|
+
return typeof value === 'string' ? value : null;
|
|
65
|
+
}
|
|
66
|
+
async function probeSearchInputState(profile, unifiedApiUrl, keyword) {
|
|
67
|
+
const state = await controllerAction('browser:execute', {
|
|
68
|
+
profile,
|
|
69
|
+
script: `(() => {
|
|
70
|
+
const norm = (v) => (typeof v === 'string' ? v.trim() : String(v ?? '').trim());
|
|
71
|
+
const editableSelector = "input, textarea, [contenteditable='true'], [contenteditable=''], [role='textbox']";
|
|
72
|
+
const toValue = (el) => {
|
|
73
|
+
try {
|
|
74
|
+
if (!el) return '';
|
|
75
|
+
if ('value' in el) {
|
|
76
|
+
const v = el.value;
|
|
77
|
+
return typeof v === 'string' ? v : String(v ?? '');
|
|
78
|
+
}
|
|
79
|
+
const t = el.textContent ?? '';
|
|
80
|
+
return typeof t === 'string' ? t : String(t ?? '');
|
|
81
|
+
} catch {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const isVisible = (el) => {
|
|
86
|
+
try {
|
|
87
|
+
if (!el || !el.getBoundingClientRect) return false;
|
|
88
|
+
const r = el.getBoundingClientRect();
|
|
89
|
+
if (!(r.width > 0 && r.height > 0)) return false;
|
|
90
|
+
return r.bottom >= 0 && r.right >= 0 && r.top <= window.innerHeight && r.left <= window.innerWidth;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const descriptor = (el) => {
|
|
96
|
+
const bits = [
|
|
97
|
+
el?.id || '',
|
|
98
|
+
el?.className || '',
|
|
99
|
+
el?.getAttribute?.('name') || '',
|
|
100
|
+
el?.getAttribute?.('placeholder') || '',
|
|
101
|
+
el?.getAttribute?.('aria-label') || '',
|
|
102
|
+
el?.getAttribute?.('data-placeholder') || '',
|
|
103
|
+
]
|
|
104
|
+
.map((x) => String(x || '').toLowerCase())
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
return bits.join(' ');
|
|
107
|
+
};
|
|
108
|
+
const hasSearchHint = (el) => /search|搜索|关键/.test(descriptor(el));
|
|
109
|
+
const activeElement = document.activeElement;
|
|
110
|
+
const activeEditable = activeElement && (activeElement.matches?.(editableSelector)
|
|
111
|
+
? activeElement
|
|
112
|
+
: activeElement.querySelector?.(editableSelector));
|
|
113
|
+
|
|
114
|
+
/** @type {Array<{value: string, source: string, score: number}>} */
|
|
115
|
+
const candidates = [];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
const pushCandidate = (el, source) => {
|
|
118
|
+
if (!el || seen.has(el)) return;
|
|
119
|
+
seen.add(el);
|
|
120
|
+
if (!isVisible(el)) return;
|
|
121
|
+
const value = toValue(el);
|
|
122
|
+
const rect = el.getBoundingClientRect?.() || { top: 9999, width: 0 };
|
|
123
|
+
const score =
|
|
124
|
+
(el === activeEditable ? 80 : 0) +
|
|
125
|
+
(hasSearchHint(el) ? 40 : 0) +
|
|
126
|
+
(rect.top < 180 ? 20 : 0) +
|
|
127
|
+
(rect.width > 120 ? 10 : 0);
|
|
128
|
+
candidates.push({ value: String(value || ''), source, score });
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (activeEditable) pushCandidate(activeEditable, 'active');
|
|
132
|
+
|
|
133
|
+
const root = document.querySelector('#search-input');
|
|
134
|
+
if (root) {
|
|
135
|
+
const nested = root.matches?.(editableSelector)
|
|
136
|
+
? root
|
|
137
|
+
: root.querySelector?.(editableSelector);
|
|
138
|
+
if (nested) pushCandidate(nested, 'search_root');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const primary = document.querySelectorAll(
|
|
142
|
+
"#search-input, input[type='search'], input[placeholder*='搜索'], input[placeholder*='关键字'], input[aria-label*='搜索'], input[name*='search']",
|
|
143
|
+
);
|
|
144
|
+
primary.forEach((node, idx) => {
|
|
145
|
+
const el = node.matches?.(editableSelector)
|
|
146
|
+
? node
|
|
147
|
+
: node.querySelector?.(editableSelector);
|
|
148
|
+
if (el) pushCandidate(el, \`primary_\${idx + 1}\`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const editable = document.querySelectorAll(editableSelector);
|
|
152
|
+
editable.forEach((node, idx) => {
|
|
153
|
+
pushCandidate(node, \`editable_\${idx + 1}\`);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
157
|
+
const normalizedKeyword = norm(keyword);
|
|
158
|
+
const exact = candidates.find((c) => norm(c.value) === normalizedKeyword) || null;
|
|
159
|
+
const nonEmpty = candidates.find((c) => norm(c.value).length > 0) || null;
|
|
160
|
+
const best = exact || nonEmpty || candidates[0] || null;
|
|
161
|
+
const activeValue = activeEditable ? toValue(activeEditable) : null;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
ok: !!exact,
|
|
165
|
+
value: best ? String(best.value || '') : null,
|
|
166
|
+
source: best ? String(best.source || '') : null,
|
|
167
|
+
activeValue: typeof activeValue === 'string' ? activeValue : (activeValue == null ? null : String(activeValue)),
|
|
168
|
+
candidates: candidates.slice(0, 5).map((c) => \`\${c.source}:\${String(c.value || '').slice(0, 60)}\`),
|
|
169
|
+
};
|
|
170
|
+
})()`,
|
|
171
|
+
}, unifiedApiUrl).then((res) => res?.result || res?.data?.result || null);
|
|
172
|
+
return {
|
|
173
|
+
ok: Boolean(state?.ok),
|
|
174
|
+
value: typeof state?.value === 'string' ? state.value : null,
|
|
175
|
+
source: typeof state?.source === 'string' ? state.source : null,
|
|
176
|
+
activeValue: typeof state?.activeValue === 'string' ? state.activeValue : null,
|
|
177
|
+
candidates: Array.isArray(state?.candidates) ? state.candidates.map((x) => String(x)) : [],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async function systemFillSearchInputValue(profile, unifiedApiUrl, keyword) {
|
|
181
|
+
// System-level requirement: prefer keyboard/mouse. `browser:execute` is JS mutation and should be avoided.
|
|
182
|
+
// We implement a system-level "fill" as: select-all + delete + type (with retries).
|
|
183
|
+
const trySelectAllDeleteType = async (modifier) => {
|
|
184
|
+
await controllerAction('keyboard:down', { profileId: profile, key: modifier }, unifiedApiUrl).catch(() => { });
|
|
185
|
+
await controllerAction('keyboard:press', { profileId: profile, key: 'A' }, unifiedApiUrl).catch(() => { });
|
|
186
|
+
await controllerAction('keyboard:up', { profileId: profile, key: modifier }, unifiedApiUrl).catch(() => { });
|
|
187
|
+
await delay(60);
|
|
188
|
+
await controllerAction('keyboard:press', { profileId: profile, key: 'Backspace' }, unifiedApiUrl).catch(() => { });
|
|
189
|
+
await delay(60);
|
|
190
|
+
await controllerAction('keyboard:type', { profileId: profile, text: keyword, delay: 70 }, unifiedApiUrl).catch(() => { });
|
|
191
|
+
};
|
|
192
|
+
const checkBoth = async () => {
|
|
193
|
+
const probe = await probeSearchInputState(profile, unifiedApiUrl, keyword).catch(() => null);
|
|
194
|
+
if (probe?.ok) {
|
|
195
|
+
return { ok: true, source: probe.source || 'probeSearchInputState', value: probe.value || '' };
|
|
196
|
+
}
|
|
197
|
+
const v1 = await readSearchInputValue(profile, unifiedApiUrl).catch(() => null);
|
|
198
|
+
if (v1 && v1.trim() === keyword)
|
|
199
|
+
return { ok: true, source: 'readSearchInputValue', value: v1 };
|
|
200
|
+
const v2 = await readActiveInputValue(profile, unifiedApiUrl).catch(() => null);
|
|
201
|
+
if (v2 && v2.trim() === keyword)
|
|
202
|
+
return { ok: true, source: 'readActiveInputValue', value: v2 };
|
|
203
|
+
return { ok: false, value: probe?.value || v1 || v2 || '' };
|
|
204
|
+
};
|
|
205
|
+
// Try a few times; Camoufox can drop events if focus is flaky.
|
|
206
|
+
for (let i = 0; i < 3; i++) {
|
|
207
|
+
await trySelectAllDeleteType('Meta');
|
|
208
|
+
await delay(250);
|
|
209
|
+
const c1 = await checkBoth();
|
|
210
|
+
if (c1.ok)
|
|
211
|
+
return { ok: true, method: 'meta', source: c1.source };
|
|
212
|
+
await trySelectAllDeleteType('Control');
|
|
213
|
+
await delay(250);
|
|
214
|
+
const c2 = await checkBoth();
|
|
215
|
+
if (c2.ok)
|
|
216
|
+
return { ok: true, method: 'control', source: c2.source };
|
|
217
|
+
}
|
|
218
|
+
const finalCheck = await checkBoth();
|
|
219
|
+
return { ok: false, reason: `mismatch:${String(finalCheck.value ?? '')}` };
|
|
220
|
+
}
|
|
221
|
+
async function submitHomeSearchViaContainer(profile, unifiedApiUrl) {
|
|
222
|
+
// Fallback only: do NOT type keyword again (it can append duplicated text).
|
|
223
|
+
// Just click search button once.
|
|
224
|
+
try {
|
|
225
|
+
await controllerAction('container:operation', { containerId: 'xiaohongshu_home.search_button', operationId: 'click', sessionId: profile, timeoutMs: 10000 }, unifiedApiUrl).catch(() => { });
|
|
226
|
+
console.log('[Phase2Search] submit via click fallback');
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function canSubmitSearch(profile, unifiedApiUrl, keyword) {
|
|
234
|
+
const probe = await probeSearchInputState(profile, unifiedApiUrl, keyword).catch(() => null);
|
|
235
|
+
if (probe?.ok) {
|
|
236
|
+
return {
|
|
237
|
+
ok: true,
|
|
238
|
+
value: probe.value ?? null,
|
|
239
|
+
source: probe.source ?? 'probeSearchInputState',
|
|
240
|
+
activeValue: probe.activeValue ?? null,
|
|
241
|
+
candidates: Array.isArray(probe.candidates) ? probe.candidates : [],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const value = await readSearchInputValue(profile, unifiedApiUrl).catch(() => null);
|
|
245
|
+
if (typeof value === 'string' && value.trim() === keyword)
|
|
246
|
+
return { ok: true, value, source: 'readSearchInputValue', activeValue: null, candidates: [] };
|
|
247
|
+
const activeValue = await readActiveInputValue(profile, unifiedApiUrl).catch(() => null);
|
|
248
|
+
if (typeof activeValue === 'string' && activeValue.trim() === keyword)
|
|
249
|
+
return { ok: true, value: activeValue, source: 'readActiveInputValue', activeValue, candidates: [] };
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
value: (probe?.value ?? value ?? activeValue ?? null),
|
|
253
|
+
source: (probe?.source ?? null),
|
|
254
|
+
activeValue: (probe?.activeValue ?? activeValue ?? null),
|
|
255
|
+
candidates: Array.isArray(probe?.candidates) ? probe.candidates : [],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function browserFillSearchInputValue(profile, unifiedApiUrl, keyword, searchInputContainerId) {
|
|
259
|
+
const selector = "#search-input input, #search-input textarea, #search-input [contenteditable='true'], input[type='search'], input[placeholder*='鎼滅储'], input[placeholder*='鍏抽敭'], input[aria-label*='鎼滅储']";
|
|
260
|
+
const fillRes = await controllerAction('browser:fill', { profile, selector, text: keyword }, unifiedApiUrl).catch((e) => ({ success: false, error: String(e?.message || e || 'browser_fill_failed') }));
|
|
261
|
+
const fillOk = Boolean(fillRes?.success ?? fillRes?.ok ?? false);
|
|
262
|
+
const fillErr = String(fillRes?.error || '').trim();
|
|
263
|
+
console.log(`[Phase2Search] protocol fill: selector="${selector}" success=${fillOk}${fillErr ? ` error=${fillErr}` : ''}`);
|
|
264
|
+
if (fillOk) {
|
|
265
|
+
return { success: true, mode: 'browser:fill', selector };
|
|
266
|
+
}
|
|
267
|
+
if (searchInputContainerId !== 'dom_fallback_search_input') {
|
|
268
|
+
const typeRes = await controllerAction('container:operation', {
|
|
269
|
+
containerId: searchInputContainerId,
|
|
270
|
+
operationId: 'type',
|
|
271
|
+
sessionId: profile,
|
|
272
|
+
config: {
|
|
273
|
+
text: keyword,
|
|
274
|
+
value: keyword,
|
|
275
|
+
clear_first: true,
|
|
276
|
+
human_typing: true,
|
|
277
|
+
pause_after: 300,
|
|
278
|
+
},
|
|
279
|
+
}, unifiedApiUrl).catch((e) => ({ success: false, error: String(e?.message || e || 'container_type_failed') }));
|
|
280
|
+
const typeOk = Boolean(typeRes?.success ?? typeRes?.ok ?? false);
|
|
281
|
+
const typeErr = String(typeRes?.error || '').trim();
|
|
282
|
+
console.log(`[Phase2Search] protocol input: container_type success=${typeOk}${typeErr ? ` error=${typeErr}` : ''}`);
|
|
283
|
+
if (typeOk) {
|
|
284
|
+
const ok = await canSubmitSearch(profile, unifiedApiUrl, keyword).catch(() => ({ ok: false }));
|
|
285
|
+
if (ok?.ok)
|
|
286
|
+
return { success: true, mode: 'container:type', selector };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Fallback: system-level input
|
|
290
|
+
if (searchInputContainerId !== 'dom_fallback_search_input') {
|
|
291
|
+
await controllerAction('container:operation', { containerId: searchInputContainerId, operationId: 'click', sessionId: profile }, unifiedApiUrl).catch(() => { });
|
|
292
|
+
}
|
|
293
|
+
await delay(200);
|
|
294
|
+
await clearSearchInput(profile, unifiedApiUrl);
|
|
295
|
+
await delay(120);
|
|
296
|
+
await controllerAction('keyboard:type', { profileId: profile, text: keyword }, unifiedApiUrl).catch(() => { });
|
|
297
|
+
await delay(200);
|
|
298
|
+
const ok = await canSubmitSearch(profile, unifiedApiUrl, keyword).catch(() => ({ ok: false }));
|
|
299
|
+
return { success: !!ok?.ok, mode: 'keyboard:type', selector };
|
|
300
|
+
}
|
|
301
|
+
async function clearSearchInput(profile, unifiedApiUrl) {
|
|
302
|
+
// Step 1: JS-based clear first (most reliable for Camoufox/Firefox)
|
|
303
|
+
const jsCleared = await controllerAction('browser:execute', {
|
|
304
|
+
profile,
|
|
305
|
+
script: `(() => {
|
|
306
|
+
const root = document.querySelector('#search-input') ||
|
|
307
|
+
document.querySelector("input[type='search']") ||
|
|
308
|
+
document.querySelector("input[placeholder*='搜索'], input[placeholder*='关键字']");
|
|
309
|
+
if (!root) return false;
|
|
310
|
+
root.value = '';
|
|
311
|
+
root.dispatchEvent(new Event('input', { bubbles: true }));
|
|
312
|
+
root.dispatchEvent(new Event('change', { bubbles: true }));
|
|
313
|
+
return true;
|
|
314
|
+
})()`,
|
|
315
|
+
}, unifiedApiUrl).then(res => res?.result || res?.data?.result || false).catch(() => false);
|
|
316
|
+
await delay(100);
|
|
317
|
+
const v1 = await readSearchInputValue(profile, unifiedApiUrl);
|
|
318
|
+
if (!v1 || !v1.trim())
|
|
319
|
+
return;
|
|
320
|
+
// Step 2+: Fallback to system keyboard if needed
|
|
321
|
+
const tryCombo = async (combo) => {
|
|
322
|
+
const mod = combo.startsWith('Meta') ? 'Meta' : 'Control';
|
|
323
|
+
await controllerAction('keyboard:down', { profileId: profile, key: mod }, unifiedApiUrl).catch(() => { });
|
|
324
|
+
await controllerAction('keyboard:press', { profileId: profile, key: 'A' }, unifiedApiUrl).catch(() => { });
|
|
325
|
+
await controllerAction('keyboard:up', { profileId: profile, key: mod }, unifiedApiUrl).catch(() => { });
|
|
326
|
+
await delay(80);
|
|
327
|
+
// Use both Delete and Backspace to cover different input implementations.
|
|
328
|
+
await controllerAction('keyboard:press', { profileId: profile, key: 'Backspace' }, unifiedApiUrl).catch(() => { });
|
|
329
|
+
await controllerAction('keyboard:press', { profileId: profile, key: 'Delete' }, unifiedApiUrl).catch(() => { });
|
|
330
|
+
};
|
|
331
|
+
// 1) Cmd+A (Mac) then delete
|
|
332
|
+
await tryCombo('Meta+A');
|
|
333
|
+
await delay(120);
|
|
334
|
+
let v = await readSearchInputValue(profile, unifiedApiUrl);
|
|
335
|
+
if (!v || !v.trim())
|
|
336
|
+
return;
|
|
337
|
+
// 2) Ctrl+A then delete
|
|
338
|
+
await tryCombo('Control+A');
|
|
339
|
+
await delay(120);
|
|
340
|
+
v = await readSearchInputValue(profile, unifiedApiUrl);
|
|
341
|
+
if (!v || !v.trim())
|
|
342
|
+
return;
|
|
343
|
+
// 3) Fallback: repeated Backspace
|
|
344
|
+
for (let i = 0; i < 80; i++) {
|
|
345
|
+
await controllerAction('keyboard:press', { profileId: profile, key: 'Backspace' }, unifiedApiUrl).catch(() => { });
|
|
346
|
+
if (i % 10 === 0) {
|
|
347
|
+
await delay(40);
|
|
348
|
+
v = await readSearchInputValue(profile, unifiedApiUrl);
|
|
349
|
+
if (!v || !v.trim())
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
export async function execute(input) {
|
|
355
|
+
const { keyword, profile = 'xiaohongshu_fresh', unifiedApiUrl = 'http://127.0.0.1:7701', } = input;
|
|
356
|
+
const debugArtifactsEnabled = isDebugArtifactsEnabled();
|
|
357
|
+
console.log(`[Phase2Search] 执行搜索(容器驱动): ${keyword}`);
|
|
358
|
+
// Ensure we are in a safe starting state (home/search). Recover from detail/comments if needed.
|
|
359
|
+
const ensureRes = await ensureXhsCheckpoint({
|
|
360
|
+
sessionId: profile,
|
|
361
|
+
target: 'search_ready',
|
|
362
|
+
serviceUrl: unifiedApiUrl,
|
|
363
|
+
timeoutMs: 15000,
|
|
364
|
+
allowOneLevelUpFallback: true,
|
|
365
|
+
});
|
|
366
|
+
if (!ensureRes.success && ensureRes.reached !== 'home_ready' && ensureRes.reached !== 'search_ready') {
|
|
367
|
+
throw new Error(`[Phase2Search] ensure checkpoint failed: reached=${ensureRes.reached} url=${ensureRes.url}`);
|
|
368
|
+
}
|
|
369
|
+
let currentUrl = await controllerAction('browser:execute', { profile, script: 'window.location.href' }, unifiedApiUrl).then((res) => res?.result || res?.data?.result || '');
|
|
370
|
+
// 开发期硬门禁:每个大环节开始先定位,不做容错兜底。
|
|
371
|
+
const det = await detectXhsCheckpoint({ sessionId: profile, serviceUrl: unifiedApiUrl });
|
|
372
|
+
console.log(`[Phase2Search] locate: checkpoint=${det.checkpoint} url=${det.url}`);
|
|
373
|
+
if (det.checkpoint === 'risk_control' || det.checkpoint === 'login_guard' || det.checkpoint === 'offsite') {
|
|
374
|
+
throw new Error(`[Phase2Search] hard_stop checkpoint=${det.checkpoint} url=${det.url}`);
|
|
375
|
+
}
|
|
376
|
+
async function waitCheckpoint(maxWaitMs) {
|
|
377
|
+
const start = Date.now();
|
|
378
|
+
let last = det;
|
|
379
|
+
while (Date.now() - start < maxWaitMs) {
|
|
380
|
+
await delay(500);
|
|
381
|
+
last = await detectXhsCheckpoint({ sessionId: profile, serviceUrl: unifiedApiUrl });
|
|
382
|
+
if (last.checkpoint !== 'detail_ready' && last.checkpoint !== 'comments_ready')
|
|
383
|
+
return last;
|
|
384
|
+
}
|
|
385
|
+
return last;
|
|
386
|
+
}
|
|
387
|
+
async function exitDetailOrCommentsState() {
|
|
388
|
+
let d = await detectXhsCheckpoint({ sessionId: profile, serviceUrl: unifiedApiUrl });
|
|
389
|
+
if (d.checkpoint !== 'detail_ready' && d.checkpoint !== 'comments_ready')
|
|
390
|
+
return d;
|
|
391
|
+
// 详情/评论态:先尝试点击关闭按钮(更贴近用户行为),然后等待状态变化。
|
|
392
|
+
console.log('[Phase2Search] 处于详情/评论态,尝试点击关闭按钮关闭详情页...');
|
|
393
|
+
try {
|
|
394
|
+
const r = await controllerAction('container:operation', { containerId: 'xiaohongshu_detail.close_button', operationId: 'click', sessionId: profile, timeoutMs: 15000 }, unifiedApiUrl);
|
|
395
|
+
console.log(`[Phase2Search] close_button click: success=${Boolean(r?.success !== false)}`);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
console.log('[Phase2Search] close_button click failed (ignored)');
|
|
399
|
+
}
|
|
400
|
+
d = await waitCheckpoint(8000);
|
|
401
|
+
if (d.checkpoint !== 'detail_ready' && d.checkpoint !== 'comments_ready')
|
|
402
|
+
return d;
|
|
403
|
+
// 若仍在详情/评论态:使用 ESC 退出(系统级),每次后等待状态稳定。
|
|
404
|
+
for (let i = 0; i < 2; i += 1) {
|
|
405
|
+
console.log(`[Phase2Search] still in ${d.checkpoint}, press ESC to exit (round=${i + 1})`);
|
|
406
|
+
await controllerAction('keyboard:press', { profileId: profile, key: 'Escape' }, unifiedApiUrl);
|
|
407
|
+
d = await waitCheckpoint(8000);
|
|
408
|
+
if (d.checkpoint !== 'detail_ready' && d.checkpoint !== 'comments_ready')
|
|
409
|
+
return d;
|
|
410
|
+
}
|
|
411
|
+
// 最终仍失败:留证据(截图长度),停止(不刷新)。
|
|
412
|
+
let shotLen = 0;
|
|
413
|
+
if (isDebugArtifactsEnabled()) {
|
|
414
|
+
const shot = await controllerAction('browser:screenshot', { profileId: profile, fullPage: false }, unifiedApiUrl).then((res) => res?.data || res?.result || res?.data?.data || '');
|
|
415
|
+
shotLen = typeof shot === 'string' ? shot.length : 0;
|
|
416
|
+
}
|
|
417
|
+
throw new Error(`[Phase2Search] 关闭详情页后仍未回到搜索/首页(checkpoint=${d.checkpoint})。停止(避免刷新)。URL=${d.url} screenshot_len=${shotLen}`);
|
|
418
|
+
}
|
|
419
|
+
// If starting from detail/comments state, exit to a stable checkpoint before doing anything else.
|
|
420
|
+
if (det.checkpoint === 'detail_ready' || det.checkpoint === 'comments_ready') {
|
|
421
|
+
await exitDetailOrCommentsState();
|
|
422
|
+
}
|
|
423
|
+
// 检查当前页面是否可搜索(优先用 DOM signals,禁止任何刷新/导航)
|
|
424
|
+
const domCheck = await controllerAction('browser:execute', {
|
|
425
|
+
profile,
|
|
426
|
+
script: `(function(){
|
|
427
|
+
const root =
|
|
428
|
+
document.querySelector('#search-input') ||
|
|
429
|
+
document.querySelector('input[type="search"]') ||
|
|
430
|
+
document.querySelector('input[placeholder*="搜索"], input[placeholder*="关键字"]');
|
|
431
|
+
const el = root && ('value' in root)
|
|
432
|
+
? root
|
|
433
|
+
: (root ? (root.querySelector('input, textarea, [contenteditable="true"], [contenteditable=""]') || root) : null);
|
|
434
|
+
const rect = el && el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
435
|
+
const hasSearchInput = !!el && !!rect && rect.width > 0 && rect.height > 0;
|
|
436
|
+
const hasDetailMask = !!document.querySelector('.detail-mask, .note-detail-mask, .content-mask');
|
|
437
|
+
return {
|
|
438
|
+
hasSearchInput,
|
|
439
|
+
hasDetailMask,
|
|
440
|
+
rect: rect ? { x1: rect.left, y1: rect.top, x2: rect.right, y2: rect.bottom } : null,
|
|
441
|
+
url: window.location.href,
|
|
442
|
+
};
|
|
443
|
+
})()`,
|
|
444
|
+
}, unifiedApiUrl).then(res => res?.result || res?.data?.result || {});
|
|
445
|
+
const hasSearchInput = Boolean(domCheck?.hasSearchInput);
|
|
446
|
+
const hasDetailMask = Boolean(domCheck?.hasDetailMask);
|
|
447
|
+
const domRect = domCheck?.rect || null;
|
|
448
|
+
// Priority 1: If detail mask exists or we're still in detail/comments, close the modal.
|
|
449
|
+
// NOTE: On XHS, ESC is unreliable in Camoufox; prefer clicking the close button (system click via container).
|
|
450
|
+
if (hasDetailMask || det.checkpoint === 'detail_ready' || det.checkpoint === 'comments_ready') {
|
|
451
|
+
console.log('[Phase2Search] 处于详情/评论态,尝试点击关闭按钮关闭详情页...');
|
|
452
|
+
// Best-effort click the close button by container operation (system-level click).
|
|
453
|
+
// If container is missing (layout changed), we fail fast (no refresh) with evidence.
|
|
454
|
+
const clickRes = await controllerAction('container:operation', { containerId: 'xiaohongshu_detail.modal_shell', operationId: 'click', sessionId: profile }, unifiedApiUrl);
|
|
455
|
+
console.log(`[Phase2Search] close_button click: success=${Boolean(clickRes?.success)}`);
|
|
456
|
+
await delay(1500);
|
|
457
|
+
const detAfterClose = await detectXhsCheckpoint({ sessionId: profile, serviceUrl: unifiedApiUrl });
|
|
458
|
+
if (detAfterClose.checkpoint !== 'search_ready' && detAfterClose.checkpoint !== 'home_ready') {
|
|
459
|
+
throw new Error(`[Phase2Search] 关闭详情页后仍未回到搜索/首页(checkpoint=${detAfterClose.checkpoint})。停止(避免刷新)。URL=${currentUrl}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Priority 2: Without search input we cannot search
|
|
463
|
+
if (!hasSearchInput) {
|
|
464
|
+
throw new Error(`[Phase2Search] 未检测到搜索输入框,无法执行搜索。URL=${currentUrl}`);
|
|
465
|
+
}
|
|
466
|
+
const probeHighlight = async (containerId) => {
|
|
467
|
+
try {
|
|
468
|
+
const res = await controllerAction('container:operation', { containerId, operationId: 'highlight', sessionId: profile }, unifiedApiUrl);
|
|
469
|
+
return res?.success ? res : null;
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
// Do not infer page type from URL (XHS can keep /explore/<id> while showing search UI).
|
|
476
|
+
// Instead, probe containers first; if all fail, fallback to DOM detection (no refresh/goto).
|
|
477
|
+
let searchInputContainerId = '';
|
|
478
|
+
let highlightResult = null;
|
|
479
|
+
let useDomFallback = false;
|
|
480
|
+
const preferredOrder = det.checkpoint === 'search_ready'
|
|
481
|
+
? ['xiaohongshu_search.search_bar', 'xiaohongshu_home.search_input']
|
|
482
|
+
: ['xiaohongshu_home.search_input', 'xiaohongshu_search.search_bar'];
|
|
483
|
+
for (const candidate of preferredOrder) {
|
|
484
|
+
highlightResult = await probeHighlight(candidate);
|
|
485
|
+
if (highlightResult) {
|
|
486
|
+
searchInputContainerId = candidate;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Fallback: if container highlight fails, use DOM rect (no refresh/goto)
|
|
491
|
+
if (!searchInputContainerId && domRect) {
|
|
492
|
+
useDomFallback = true;
|
|
493
|
+
searchInputContainerId = 'dom_fallback_search_input';
|
|
494
|
+
highlightResult = { success: true, data: { rect: domRect } };
|
|
495
|
+
console.log('[Phase2Search] 容器 highlight 失败,使用 DOM rect 作为搜索框');
|
|
496
|
+
}
|
|
497
|
+
if (!searchInputContainerId) {
|
|
498
|
+
throw new Error(`[Phase2Search] 未识别页面状态,无法定位搜索框。当前 URL: ${currentUrl}`);
|
|
499
|
+
}
|
|
500
|
+
const isSearchResult = searchInputContainerId === 'xiaohongshu_search.search_bar';
|
|
501
|
+
const isHome = searchInputContainerId === 'xiaohongshu_home.search_input' || searchInputContainerId === 'dom_fallback_search_input';
|
|
502
|
+
console.log(`[Phase2Search] 当前页面: ${isSearchResult ? 'search_result' : 'home'},使用容器 ${searchInputContainerId}`);
|
|
503
|
+
// 验证搜索框可用性(先高亮确认)
|
|
504
|
+
if (!highlightResult) {
|
|
505
|
+
console.log(`[Phase2Search] highlight start: ${searchInputContainerId}`);
|
|
506
|
+
highlightResult = await controllerAction('container:operation', { containerId: searchInputContainerId, operationId: 'highlight', sessionId: profile }, unifiedApiUrl);
|
|
507
|
+
}
|
|
508
|
+
console.log(`[Phase2Search] highlight done: success=${Boolean(highlightResult?.success)}`);
|
|
509
|
+
if (!highlightResult?.success) {
|
|
510
|
+
throw new Error(`[Phase2Search] 搜索框不可用: ${searchInputContainerId}`);
|
|
511
|
+
}
|
|
512
|
+
await delay(500);
|
|
513
|
+
// Camoufox: prefer system-level coordinate click to reliably focus inputs.
|
|
514
|
+
// Using container:operation click can hang in some cases.
|
|
515
|
+
const anchor = highlightResult?.data || highlightResult;
|
|
516
|
+
const rect = anchor?.rect;
|
|
517
|
+
if (rect?.x1 !== undefined && rect?.y1 !== undefined && rect?.x2 !== undefined && rect?.y2 !== undefined) {
|
|
518
|
+
const width = Number(rect.x2) - Number(rect.x1);
|
|
519
|
+
const height = Number(rect.y2) - Number(rect.y1);
|
|
520
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width < 8 || height < 8) {
|
|
521
|
+
console.log(`[Phase2Search] rect invalid for coordinate click (w=${width}, h=${height}), fallback to container click`);
|
|
522
|
+
if (!useDomFallback) {
|
|
523
|
+
await controllerAction('container:operation', { containerId: searchInputContainerId, operationId: 'click', sessionId: profile, timeoutMs: 10000 }, unifiedApiUrl).catch(() => { });
|
|
524
|
+
}
|
|
525
|
+
await delay(260);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
const cx = (Number(rect.x1) + Number(rect.x2)) / 2;
|
|
529
|
+
const cy = (Number(rect.y1) + Number(rect.y2)) / 2;
|
|
530
|
+
console.log(`[Phase2Search] mouse:click at (${cx.toFixed(1)}, ${cy.toFixed(1)})`);
|
|
531
|
+
await controllerAction('mouse:click', { profileId: profile, x: cx, y: cy, clicks: 1 }, unifiedApiUrl);
|
|
532
|
+
// Give the input time to receive focus in Camoufox.
|
|
533
|
+
await delay(300);
|
|
534
|
+
// Try selecting text via mouse (more reliable than keyboard shortcuts on some builds).
|
|
535
|
+
await controllerAction('mouse:click', { profileId: profile, x: cx, y: cy, clicks: 2 }, unifiedApiUrl).catch(() => { });
|
|
536
|
+
await delay(200);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
console.warn(`[Phase2Search] missing rect for system click, fallback to container click`);
|
|
541
|
+
if (!useDomFallback) {
|
|
542
|
+
await controllerAction('container:operation', { containerId: searchInputContainerId, operationId: 'click', sessionId: profile, timeoutMs: 10000 }, unifiedApiUrl).catch(() => { });
|
|
543
|
+
await delay(260);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// If the input already contains the same keyword, do not force clearing.
|
|
547
|
+
// Camoufox sometimes ignores deletion events in certain states; treat "already correct" as success.
|
|
548
|
+
const beforeClear = await readSearchInputValue(profile, unifiedApiUrl).catch(() => null);
|
|
549
|
+
const inputAlreadyMatches = typeof beforeClear === 'string' && beforeClear.trim() === keyword;
|
|
550
|
+
if (inputAlreadyMatches) {
|
|
551
|
+
console.log('[Phase2Search] input already matches keyword, will clear and retype to ensure focus');
|
|
552
|
+
}
|
|
553
|
+
// Always clear and type to ensure focus and input state
|
|
554
|
+
// Force clear via JS first (most reliable)
|
|
555
|
+
await controllerAction('browser:execute', {
|
|
556
|
+
profile,
|
|
557
|
+
script: `(() => {
|
|
558
|
+
const root = document.querySelector('#search-input') ||
|
|
559
|
+
document.querySelector("input[type='search']") ||
|
|
560
|
+
document.querySelector("input[placeholder*='搜索'], input[placeholder*='关键字']");
|
|
561
|
+
if (root) {
|
|
562
|
+
root.value = '';
|
|
563
|
+
root.dispatchEvent(new Event('input', { bubbles: true }));
|
|
564
|
+
}
|
|
565
|
+
return true;
|
|
566
|
+
})()`,
|
|
567
|
+
}, unifiedApiUrl).catch(() => { });
|
|
568
|
+
await delay(100);
|
|
569
|
+
await clearSearchInput(profile, unifiedApiUrl);
|
|
570
|
+
const clearedValue = await readSearchInputValue(profile, unifiedApiUrl);
|
|
571
|
+
if (typeof clearedValue === 'string' && clearedValue.trim() && clearedValue.trim() !== keyword) {
|
|
572
|
+
let shotLen = 0;
|
|
573
|
+
if (debugArtifactsEnabled) {
|
|
574
|
+
const shot = await controllerAction('browser:screenshot', { profileId: profile, fullPage: false }, unifiedApiUrl).then((res) => res?.data || res?.result || res?.data?.data || '');
|
|
575
|
+
shotLen = typeof shot === 'string' ? shot.length : 0;
|
|
576
|
+
}
|
|
577
|
+
throw new Error(`[Phase2Search] 清空输入框失败(组合键 + 退格后仍有残留)。value="${clearedValue}" screenshot_len=${shotLen}`);
|
|
578
|
+
}
|
|
579
|
+
// Camoufox: keyboard typing can be flaky (focus/IME). Use a fill-style set + input/change events.
|
|
580
|
+
const fillRes = await browserFillSearchInputValue(profile, unifiedApiUrl, keyword, searchInputContainerId);
|
|
581
|
+
const fillSuccess = Boolean(fillRes?.success !== false);
|
|
582
|
+
console.log(`[Phase2Search] browser:fill done: success=${fillSuccess}`);
|
|
583
|
+
if (!fillSuccess) {
|
|
584
|
+
const fallback = await systemFillSearchInputValue(profile, unifiedApiUrl, keyword);
|
|
585
|
+
console.log(`[Phase2Search] keyboard fill fallback: ok=${Boolean(fallback?.ok)} reason=${fallback?.reason || ''}`);
|
|
586
|
+
}
|
|
587
|
+
await delay(450);
|
|
588
|
+
// If we skipped typing, the input already contains keyword; proceed to submit.
|
|
589
|
+
// 强制验证:提交前必须确认 input 值等于 keyword,否则直接失败(不点击搜索按钮)
|
|
590
|
+
const canSubmit = await canSubmitSearch(profile, unifiedApiUrl, keyword);
|
|
591
|
+
const beforeSubmitValue = await readSearchInputValue(profile, unifiedApiUrl).catch(() => null);
|
|
592
|
+
const activeBeforeSubmitValue = await readActiveInputValue(profile, unifiedApiUrl).catch(() => null);
|
|
593
|
+
console.log(`[Phase2Search] Before submit: input value="${String(beforeSubmitValue)}" active="${String(activeBeforeSubmitValue)}" keyword="${keyword}" probe=${String(canSubmit.source)} candidates=${(canSubmit.candidates || []).join(' | ')}`);
|
|
594
|
+
if (String(canSubmit.activeValue || activeBeforeSubmitValue || '') !== String(keyword) && searchInputContainerId !== 'dom_fallback_search_input') {
|
|
595
|
+
await controllerAction('container:operation', { containerId: searchInputContainerId, operationId: 'click', sessionId: profile }, unifiedApiUrl).catch(() => { });
|
|
596
|
+
await delay(300);
|
|
597
|
+
}
|
|
598
|
+
if (!canSubmit.ok) {
|
|
599
|
+
let shotLen = 0;
|
|
600
|
+
if (debugArtifactsEnabled) {
|
|
601
|
+
const shot = await controllerAction('browser:screenshot', { profileId: profile, fullPage: false }, unifiedApiUrl).then((res) => res?.data || res?.result || res?.data?.data || '');
|
|
602
|
+
shotLen = typeof shot === 'string' ? shot.length : 0;
|
|
603
|
+
}
|
|
604
|
+
throw new Error(`[Phase2Search] 提交前 input 值不等于关键字:expected="${keyword}" actual="${String(canSubmit.value)}" source=${String(canSubmit.source)} active="${String(canSubmit.activeValue)}" candidates=${(canSubmit.candidates || []).join(' | ')} screenshot_len=${shotLen}。停止执行(不点击搜索按钮)。`);
|
|
605
|
+
}
|
|
606
|
+
if (isHome) {
|
|
607
|
+
// Trigger Enter first; fallback to click only when Enter does not navigate to search result.
|
|
608
|
+
console.log('[Phase2Search] submit via Enter');
|
|
609
|
+
await controllerAction('keyboard:press', { profileId: profile, key: 'Enter' }, unifiedApiUrl).catch(() => { });
|
|
610
|
+
await delay(1200);
|
|
611
|
+
const afterEnter = await detectXhsCheckpoint({ sessionId: profile, serviceUrl: unifiedApiUrl });
|
|
612
|
+
if (afterEnter.checkpoint !== 'search_ready') {
|
|
613
|
+
const ok = await submitHomeSearchViaContainer(profile, unifiedApiUrl);
|
|
614
|
+
if (!ok) {
|
|
615
|
+
console.log('[Phase2Search] submit via search_button failed');
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
console.log('[Phase2Search] Enter submit accepted, skip click fallback');
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
// search_result:single Enter submit only
|
|
624
|
+
await controllerAction('keyboard:press', { profileId: profile, key: 'Enter' }, unifiedApiUrl).catch(() => { });
|
|
625
|
+
}
|
|
626
|
+
// 等待搜索结果页加载:最多 15 秒。
|
|
627
|
+
// 注意:XHS 在 Camoufox 下可能出现“URL 仍停留 /explore/<id>,但 DOM 已是搜索结果页”的壳页行为。
|
|
628
|
+
// 这里仅做节奏控制,最终成功判定仍以 DOM/容器信号为准。
|
|
629
|
+
await delay(15000);
|
|
630
|
+
// 验证是否到达搜索结果页(不依赖 URL,使用 DOM + checkpoint),失败则最多重试 3 次 Enter 提交
|
|
631
|
+
let finalUrl = currentUrl;
|
|
632
|
+
let success = false;
|
|
633
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
634
|
+
const checkpoint = await detectXhsCheckpoint({ sessionId: profile, serviceUrl: unifiedApiUrl });
|
|
635
|
+
const pageCheck = await controllerAction('browser:execute', {
|
|
636
|
+
profile,
|
|
637
|
+
script: `(function(){
|
|
638
|
+
const hasTabs = !!document.querySelector('.tabs, .filter-tabs, [role="tablist"]');
|
|
639
|
+
const hasResultList = !!document.querySelector('.feeds-container, .search-result-list, .note-list');
|
|
640
|
+
const hasSearchInput = !!document.querySelector('#search-input, input[type="search"]');
|
|
641
|
+
return {
|
|
642
|
+
hasTabs,
|
|
643
|
+
hasResultList,
|
|
644
|
+
hasSearchInput,
|
|
645
|
+
url: window.location.href,
|
|
646
|
+
};
|
|
647
|
+
})()`,
|
|
648
|
+
}, unifiedApiUrl).then(res => res?.result || res?.data?.result || null);
|
|
649
|
+
finalUrl = pageCheck?.url || finalUrl;
|
|
650
|
+
const urlStr = String(finalUrl || '');
|
|
651
|
+
const urlLooksSearch = urlStr.includes('/search_result') || urlStr.includes('search_result');
|
|
652
|
+
success = checkpoint.checkpoint === 'search_ready' && Boolean(pageCheck?.hasSearchInput) && (pageCheck?.hasTabs || pageCheck?.hasResultList || urlLooksSearch);
|
|
653
|
+
console.log(`[Phase2Search] 完成: success=${success} checkpoint=${checkpoint.checkpoint} url=${finalUrl} hasTabs=${pageCheck?.hasTabs} hasResultList=${pageCheck?.hasResultList} attempt=${attempt}`);
|
|
654
|
+
if (success)
|
|
655
|
+
break;
|
|
656
|
+
if (attempt < 3) {
|
|
657
|
+
console.log(`[Phase2Search] retry search submit (attempt=${attempt + 1})`);
|
|
658
|
+
// 重新聚焦输入框(系统级点击)再输入关键字,避免焦点丢失
|
|
659
|
+
try {
|
|
660
|
+
const rect = await controllerAction('browser:execute', {
|
|
661
|
+
profile,
|
|
662
|
+
script: `(function(){
|
|
663
|
+
const el = document.querySelector('#search-input') || document.querySelector('input[type="search"]') || document.querySelector('input[placeholder*="搜索"], input[placeholder*="关键字"]');
|
|
664
|
+
if (!el) return null;
|
|
665
|
+
const r = el.getBoundingClientRect();
|
|
666
|
+
return { x1: r.left, y1: r.top, x2: r.right, y2: r.bottom };
|
|
667
|
+
})()`,
|
|
668
|
+
}, unifiedApiUrl).then(res => res?.result || res?.data?.result || null);
|
|
669
|
+
if (rect && rect.x1 !== undefined) {
|
|
670
|
+
const cx = (Number(rect.x1) + Number(rect.x2)) / 2;
|
|
671
|
+
const cy = (Number(rect.y1) + Number(rect.y2)) / 2;
|
|
672
|
+
await controllerAction('mouse:click', { profileId: profile, x: cx, y: cy, clicks: 1 }, unifiedApiUrl).catch(() => { });
|
|
673
|
+
await delay(200);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
// ignore refocus errors
|
|
678
|
+
}
|
|
679
|
+
await systemFillSearchInputValue(profile, unifiedApiUrl, keyword).catch(() => { });
|
|
680
|
+
await delay(300);
|
|
681
|
+
await controllerAction('keyboard:press', { profileId: profile, key: 'Enter' }, unifiedApiUrl);
|
|
682
|
+
await delay(5000);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (!success) {
|
|
686
|
+
const ensureFinal = await ensureXhsCheckpoint({
|
|
687
|
+
sessionId: profile,
|
|
688
|
+
target: 'search_ready',
|
|
689
|
+
serviceUrl: unifiedApiUrl,
|
|
690
|
+
timeoutMs: 12000,
|
|
691
|
+
allowOneLevelUpFallback: false,
|
|
692
|
+
});
|
|
693
|
+
success = ensureFinal.success && ensureFinal.reached === 'search_ready';
|
|
694
|
+
finalUrl = ensureFinal.url || finalUrl;
|
|
695
|
+
console.log(`[Phase2Search] ensure final: success=${success} reached=${ensureFinal.reached} url=${finalUrl}`);
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
success,
|
|
699
|
+
finalUrl,
|
|
700
|
+
keyword,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
//# sourceMappingURL=Phase2SearchBlock.js.map
|