@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,1476 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { promises as fsPromises } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import WebSocket from 'ws';
|
|
7
|
+
import { logDebug } from '../../../modules/logging/src/index.js';
|
|
8
|
+
import { normalizePayload } from './payload-normalizer.js';
|
|
9
|
+
function normalizeInputMode(mode) {
|
|
10
|
+
const raw = String(mode || '').trim().toLowerCase();
|
|
11
|
+
return raw === 'protocol' ? 'protocol' : 'system';
|
|
12
|
+
}
|
|
13
|
+
export class UiController {
|
|
14
|
+
repoRoot;
|
|
15
|
+
messageBus;
|
|
16
|
+
userContainerRoot;
|
|
17
|
+
containerIndexPath;
|
|
18
|
+
cliTargets;
|
|
19
|
+
defaultWsHost;
|
|
20
|
+
defaultWsPort;
|
|
21
|
+
defaultHttpHost;
|
|
22
|
+
defaultHttpPort;
|
|
23
|
+
defaultHttpProtocol;
|
|
24
|
+
_containerIndexCache;
|
|
25
|
+
inputMode;
|
|
26
|
+
snapshotCache = new Map();
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.repoRoot = options.repoRoot || process.cwd();
|
|
29
|
+
this.messageBus = options.messageBus;
|
|
30
|
+
this.userContainerRoot = options.userContainerRoot || path.join(os.homedir(), '.webauto', 'container-lib');
|
|
31
|
+
this.containerIndexPath = options.containerIndexPath || path.join(this.repoRoot, 'apps/webauto/resources/container-library.index.json');
|
|
32
|
+
this.cliTargets = options.cliTargets || {};
|
|
33
|
+
this.defaultWsHost = options.defaultWsHost || '127.0.0.1';
|
|
34
|
+
this.defaultWsPort = Number(options.defaultWsPort || 8765);
|
|
35
|
+
this.defaultHttpHost = options.defaultHttpHost || '127.0.0.1';
|
|
36
|
+
this.defaultHttpPort = Number(options.defaultHttpPort || 7704);
|
|
37
|
+
this.defaultHttpProtocol = options.defaultHttpProtocol || 'http';
|
|
38
|
+
this._containerIndexCache = null;
|
|
39
|
+
this.inputMode = normalizeInputMode(process.env.WEBAUTO_INPUT_MODE);
|
|
40
|
+
this.cliTargets = options.cliTargets || {};
|
|
41
|
+
logDebug('controller', 'init', {
|
|
42
|
+
wsHost: this.defaultWsHost,
|
|
43
|
+
wsPort: this.defaultWsPort,
|
|
44
|
+
httpHost: this.defaultHttpHost,
|
|
45
|
+
httpPort: this.defaultHttpPort
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
getSnapshotCacheKey(options, targetUrl, sessionId) {
|
|
49
|
+
const maxDepth = options.maxDepth ?? 2;
|
|
50
|
+
const maxChildren = options.maxChildren ?? 5;
|
|
51
|
+
const rootSelector = options.rootSelector || '';
|
|
52
|
+
const containerId = options.containerId || options.rootContainerId || '';
|
|
53
|
+
return [sessionId, targetUrl, String(maxDepth), String(maxChildren), rootSelector, containerId].join('::');
|
|
54
|
+
}
|
|
55
|
+
async runCliCommand(moduleName, args) {
|
|
56
|
+
const scriptPath = this.cliTargets[moduleName];
|
|
57
|
+
if (!scriptPath) {
|
|
58
|
+
throw new Error(`Unknown module: ${moduleName}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(`[controller] runCliCommand ${moduleName} ${scriptPath} ${args.join(' ')}`);
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
// Check if scriptPath ends with .js, if so run with node directly
|
|
63
|
+
const isJs = scriptPath.endsWith('.js');
|
|
64
|
+
const cmd = isJs ? 'node' : 'npx';
|
|
65
|
+
const cmdArgs = isJs ? [scriptPath, ...args] : ['tsx', scriptPath, ...args];
|
|
66
|
+
const child = spawn(cmd, cmdArgs, {
|
|
67
|
+
cwd: this.repoRoot,
|
|
68
|
+
env: process.env,
|
|
69
|
+
windowsHide: true,
|
|
70
|
+
});
|
|
71
|
+
let stdout = '';
|
|
72
|
+
let stderr = '';
|
|
73
|
+
child.stdout.on('data', (data) => {
|
|
74
|
+
stdout += data.toString();
|
|
75
|
+
});
|
|
76
|
+
child.stderr.on('data', (data) => {
|
|
77
|
+
stderr += data.toString();
|
|
78
|
+
});
|
|
79
|
+
child.on('close', (code) => {
|
|
80
|
+
console.log(`[controller] cli exit ${moduleName} ${code}`);
|
|
81
|
+
if (code !== 0) {
|
|
82
|
+
console.error(`[controller] cli stderr ${moduleName}`, stderr);
|
|
83
|
+
reject(new Error(`Command failed with code ${code}: ${stderr}`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
console.log(`[controller] cli stdout ${moduleName}`, stdout.trim());
|
|
88
|
+
const result = JSON.parse(stdout.trim());
|
|
89
|
+
resolve(result);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
console.error(`[controller] cli parse error ${moduleName}`, e);
|
|
93
|
+
resolve({ success: true, raw: stdout.trim() });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
child.on('error', (err) => {
|
|
97
|
+
reject(err);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async handleAction(action, payload = {}) {
|
|
102
|
+
logDebug('controller', 'handleAction', { action, payload });
|
|
103
|
+
switch (action) {
|
|
104
|
+
case 'browser:status':
|
|
105
|
+
return this.fetchBrowserStatus();
|
|
106
|
+
case 'session:list':
|
|
107
|
+
return this.runCliCommand('session-manager', ['list']);
|
|
108
|
+
case 'session:create':
|
|
109
|
+
return this.handleSessionCreate(payload);
|
|
110
|
+
case 'session:delete':
|
|
111
|
+
return this.handleSessionDelete(payload);
|
|
112
|
+
case 'logs:stream':
|
|
113
|
+
return this.handleLogsStream(payload);
|
|
114
|
+
case 'operations:list':
|
|
115
|
+
return this.runCliCommand('operations', ['list']);
|
|
116
|
+
case 'operations:run':
|
|
117
|
+
return this.handleOperationRun(payload);
|
|
118
|
+
case 'containers:inspect':
|
|
119
|
+
case 'containers:get':
|
|
120
|
+
return this.handleContainerInspect(payload);
|
|
121
|
+
case 'containers:inspect-container':
|
|
122
|
+
case 'containers:get-container':
|
|
123
|
+
return this.handleContainerInspectContainer(payload);
|
|
124
|
+
case 'containers:inspect-branch':
|
|
125
|
+
return this.handleContainerInspectBranch(payload);
|
|
126
|
+
case 'containers:remap':
|
|
127
|
+
return this.handleContainerRemap(payload);
|
|
128
|
+
case 'containers:create-child':
|
|
129
|
+
return this.handleContainerCreateChild(payload);
|
|
130
|
+
case 'containers:update-alias':
|
|
131
|
+
return this.handleContainerUpdateAlias(payload);
|
|
132
|
+
case 'containers:update-operations':
|
|
133
|
+
return this.handleContainerUpdateOperations(payload);
|
|
134
|
+
case 'containers:match':
|
|
135
|
+
case 'containers:status':
|
|
136
|
+
return this.handleContainerMatch(payload);
|
|
137
|
+
case 'browser:highlight':
|
|
138
|
+
return this.handleBrowserHighlight(payload);
|
|
139
|
+
case 'browser:clear-highlight':
|
|
140
|
+
return this.handleBrowserClearHighlight(payload);
|
|
141
|
+
case 'browser:highlight-dom-path':
|
|
142
|
+
return this.handleBrowserHighlightDomPath(payload);
|
|
143
|
+
case 'browser:execute':
|
|
144
|
+
return this.handleBrowserExecute(payload);
|
|
145
|
+
case 'browser:screenshot':
|
|
146
|
+
return this.handleBrowserScreenshot(payload);
|
|
147
|
+
case 'browser:fill':
|
|
148
|
+
return this.handleBrowserFill(payload);
|
|
149
|
+
case 'browser:page:list':
|
|
150
|
+
return this.handleBrowserPageList(payload);
|
|
151
|
+
case 'browser:page:new':
|
|
152
|
+
return this.handleBrowserPageNew(payload);
|
|
153
|
+
case 'browser:page:switch':
|
|
154
|
+
return this.handleBrowserPageSwitch(payload);
|
|
155
|
+
case 'browser:page:close':
|
|
156
|
+
return this.handleBrowserPageClose(payload);
|
|
157
|
+
case 'browser:goto':
|
|
158
|
+
return this.handleBrowserGoto(payload);
|
|
159
|
+
case 'browser:set-viewport':
|
|
160
|
+
return this.handleBrowserSetViewport(payload);
|
|
161
|
+
case 'browser:set-viewport':
|
|
162
|
+
return this.handleBrowserSetViewport(payload);
|
|
163
|
+
case 'browser:cancel-pick':
|
|
164
|
+
return this.handleBrowserCancelDomPick(payload);
|
|
165
|
+
case 'browser:pick-dom':
|
|
166
|
+
return this.handleBrowserPickDom(payload);
|
|
167
|
+
case 'keyboard:press':
|
|
168
|
+
return this.handleKeyboardPress(payload);
|
|
169
|
+
case 'keyboard:type':
|
|
170
|
+
return this.handleKeyboardType(payload);
|
|
171
|
+
case 'system:shortcut':
|
|
172
|
+
return this.handleSystemShortcut(payload);
|
|
173
|
+
case 'system:input-mode:set':
|
|
174
|
+
return this.handleSystemInputModeSet(payload);
|
|
175
|
+
case 'system:input-mode:get':
|
|
176
|
+
return this.handleSystemInputModeGet();
|
|
177
|
+
case 'mouse:wheel':
|
|
178
|
+
return this.handleMouseWheel(payload);
|
|
179
|
+
case 'dom:branch:2':
|
|
180
|
+
return this.handleDomBranch2(payload);
|
|
181
|
+
case 'dom:pick:2':
|
|
182
|
+
return this.handleDomPick2(payload);
|
|
183
|
+
case 'container:operation':
|
|
184
|
+
return this.handleContainerOperation(payload);
|
|
185
|
+
default:
|
|
186
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async handleSessionCreate(payload = {}) {
|
|
190
|
+
const { profile, url, headless, keepOpen } = normalizePayload(payload, { required: ['profile'] });
|
|
191
|
+
logDebug('controller', 'session:create', { payload });
|
|
192
|
+
const args = ['create', '--profile', profile];
|
|
193
|
+
if (url)
|
|
194
|
+
args.push('--url', url);
|
|
195
|
+
if (headless !== undefined)
|
|
196
|
+
args.push('--headless', String(headless));
|
|
197
|
+
if (keepOpen !== undefined)
|
|
198
|
+
args.push('--keep-open', String(keepOpen));
|
|
199
|
+
return this.runCliCommand('session-manager', args);
|
|
200
|
+
}
|
|
201
|
+
async handleSessionDelete(payload = {}) {
|
|
202
|
+
const { profile } = normalizePayload(payload, { required: ['profile'] });
|
|
203
|
+
return this.runCliCommand('session-manager', ['delete', '--profile', profile]);
|
|
204
|
+
}
|
|
205
|
+
async handleLogsStream(payload = {}) {
|
|
206
|
+
const args = ['stream'];
|
|
207
|
+
if (payload.source)
|
|
208
|
+
args.push('--source', payload.source);
|
|
209
|
+
if (payload.session)
|
|
210
|
+
args.push('--session', payload.session);
|
|
211
|
+
if (payload.lines)
|
|
212
|
+
args.push('--lines', String(payload.lines));
|
|
213
|
+
return this.runCliCommand('logging', args);
|
|
214
|
+
}
|
|
215
|
+
async handleSystemShortcut(payload = {}) {
|
|
216
|
+
const shortcut = String(payload.shortcut || '').trim();
|
|
217
|
+
const app = String(payload.app || 'camoufox').trim();
|
|
218
|
+
if (!shortcut)
|
|
219
|
+
throw new Error('shortcut required');
|
|
220
|
+
if (process.platform === 'darwin') {
|
|
221
|
+
const { spawnSync } = await import('node:child_process');
|
|
222
|
+
spawnSync('osascript', ['-e', `tell application "${app}" to activate`]);
|
|
223
|
+
if (shortcut === 'new-tab') {
|
|
224
|
+
const res = spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "t" using command down']);
|
|
225
|
+
if (res.status != 0)
|
|
226
|
+
throw new Error('osascript new-tab failed');
|
|
227
|
+
return { success: true, data: { ok: true, shortcut } };
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`unsupported shortcut: ${shortcut}`);
|
|
230
|
+
}
|
|
231
|
+
if (process.platform === 'win32') {
|
|
232
|
+
const { spawnSync } = await import('node:child_process');
|
|
233
|
+
if (shortcut === 'new-tab') {
|
|
234
|
+
const script = 'Add-Type -AssemblyName System.Windows.Forms; $ws = New-Object -ComObject WScript.Shell; $ws.SendKeys("^t");';
|
|
235
|
+
const res = spawnSync('powershell', ['-NoProfile', '-Command', script], { windowsHide: true });
|
|
236
|
+
if (res.status != 0)
|
|
237
|
+
throw new Error('powershell new-tab failed');
|
|
238
|
+
return { success: true, data: { ok: true, shortcut } };
|
|
239
|
+
}
|
|
240
|
+
throw new Error(`unsupported shortcut: ${shortcut}`);
|
|
241
|
+
}
|
|
242
|
+
throw new Error('unsupported platform');
|
|
243
|
+
}
|
|
244
|
+
async handleSystemInputModeSet(payload = {}) {
|
|
245
|
+
const mode = normalizeInputMode(payload.mode);
|
|
246
|
+
this.inputMode = mode;
|
|
247
|
+
process.env.WEBAUTO_INPUT_MODE = mode;
|
|
248
|
+
logDebug('controller', 'input-mode:set', { mode });
|
|
249
|
+
return { success: true, data: { mode } };
|
|
250
|
+
}
|
|
251
|
+
async handleSystemInputModeGet() {
|
|
252
|
+
return { success: true, data: { mode: this.inputMode } };
|
|
253
|
+
}
|
|
254
|
+
async handleOperationRun(payload = {}) {
|
|
255
|
+
const op = payload.op || payload.operation || payload.id;
|
|
256
|
+
if (!op)
|
|
257
|
+
throw new Error('缺少操作 ID');
|
|
258
|
+
const args = ['run', '--op', op];
|
|
259
|
+
if (payload.config) {
|
|
260
|
+
args.push('--config', JSON.stringify(payload.config));
|
|
261
|
+
}
|
|
262
|
+
return this.runCliCommand('operations', args);
|
|
263
|
+
}
|
|
264
|
+
async handleContainerInspect(payload = {}) {
|
|
265
|
+
const { profile, url, maxDepth, maxChildren, containerId, rootSelector } = normalizePayload(payload, { required: ['profile'] });
|
|
266
|
+
logDebug('controller', 'containers:inspect', { profile, payload });
|
|
267
|
+
const context = await this.captureInspectorSnapshot({
|
|
268
|
+
profile,
|
|
269
|
+
url,
|
|
270
|
+
maxDepth,
|
|
271
|
+
maxChildren,
|
|
272
|
+
containerId,
|
|
273
|
+
rootSelector,
|
|
274
|
+
});
|
|
275
|
+
const snapshot = context.snapshot;
|
|
276
|
+
return {
|
|
277
|
+
success: true,
|
|
278
|
+
data: {
|
|
279
|
+
sessionId: context.sessionId,
|
|
280
|
+
profileId: context.profileId,
|
|
281
|
+
url: context.targetUrl,
|
|
282
|
+
snapshot,
|
|
283
|
+
containerSnapshot: snapshot,
|
|
284
|
+
domTree: snapshot?.dom_tree || null,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async handleContainerInspectContainer(payload = {}) {
|
|
289
|
+
const { profile, containerId, url, maxDepth, maxChildren, rootSelector } = normalizePayload(payload, { required: ['profile', 'containerId'] });
|
|
290
|
+
const context = await this.captureInspectorSnapshot({
|
|
291
|
+
profile,
|
|
292
|
+
url,
|
|
293
|
+
maxDepth,
|
|
294
|
+
maxChildren,
|
|
295
|
+
containerId,
|
|
296
|
+
rootSelector,
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
success: true,
|
|
300
|
+
data: {
|
|
301
|
+
sessionId: context.sessionId,
|
|
302
|
+
profileId: context.profileId,
|
|
303
|
+
url: context.targetUrl,
|
|
304
|
+
snapshot: context.snapshot,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async handleContainerInspectBranch(payload = {}) {
|
|
309
|
+
const { profile, path, url, maxDepth, maxChildren, rootSelector } = normalizePayload(payload, { required: ['profile', 'path'] });
|
|
310
|
+
logDebug('controller', 'containers:inspect:branch', { profile, path, payload });
|
|
311
|
+
const context = await this.captureInspectorBranch({
|
|
312
|
+
profile,
|
|
313
|
+
url,
|
|
314
|
+
path,
|
|
315
|
+
rootSelector,
|
|
316
|
+
maxDepth,
|
|
317
|
+
maxChildren,
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
data: {
|
|
322
|
+
sessionId: context.sessionId,
|
|
323
|
+
profileId: context.profileId,
|
|
324
|
+
url: context.targetUrl,
|
|
325
|
+
branch: context.branch,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async handleContainerRemap(payload = {}) {
|
|
330
|
+
const containerId = payload.containerId || payload.id;
|
|
331
|
+
const selector = (payload.selector || '').trim();
|
|
332
|
+
const definition = payload.definition || {};
|
|
333
|
+
if (!containerId) {
|
|
334
|
+
throw new Error('缺少容器 ID');
|
|
335
|
+
}
|
|
336
|
+
if (!selector) {
|
|
337
|
+
throw new Error('缺少新的 selector');
|
|
338
|
+
}
|
|
339
|
+
const siteKey = payload.siteKey ||
|
|
340
|
+
this.resolveSiteKeyFromUrl(payload.url) ||
|
|
341
|
+
this.inferSiteFromContainerId(containerId);
|
|
342
|
+
if (!siteKey) {
|
|
343
|
+
throw new Error('无法确定容器所属站点');
|
|
344
|
+
}
|
|
345
|
+
const normalizedDefinition = { ...definition, id: containerId };
|
|
346
|
+
const existingSelectors = Array.isArray(normalizedDefinition.selectors) ? normalizedDefinition.selectors : [];
|
|
347
|
+
const filtered = existingSelectors.filter((item) => (item?.css || '').trim() && (item.css || '').trim() !== selector);
|
|
348
|
+
normalizedDefinition.selectors = [{ css: selector, variant: 'primary', score: 1 }, ...filtered];
|
|
349
|
+
await this.writeUserContainerDefinition(siteKey, containerId, normalizedDefinition);
|
|
350
|
+
const { profile, url } = normalizePayload(payload, { required: ['profile'] });
|
|
351
|
+
return this.handleContainerInspect({ profile, url });
|
|
352
|
+
}
|
|
353
|
+
async handleContainerCreateChild(payload = {}) {
|
|
354
|
+
const parentId = payload.parentId || payload.parent_id;
|
|
355
|
+
const containerId = payload.containerId || payload.childId || payload.id;
|
|
356
|
+
if (!parentId) {
|
|
357
|
+
throw new Error('缺少父容器 ID');
|
|
358
|
+
}
|
|
359
|
+
if (!containerId) {
|
|
360
|
+
throw new Error('缺少子容器 ID');
|
|
361
|
+
}
|
|
362
|
+
const siteKey = payload.siteKey ||
|
|
363
|
+
this.resolveSiteKeyFromUrl(payload.url) ||
|
|
364
|
+
this.inferSiteFromContainerId(containerId) ||
|
|
365
|
+
this.inferSiteFromContainerId(parentId);
|
|
366
|
+
if (!siteKey) {
|
|
367
|
+
throw new Error('无法确定容器所属站点');
|
|
368
|
+
}
|
|
369
|
+
const selectorEntries = this.normalizeSelectors(payload.selectors || payload.selector || []) || [];
|
|
370
|
+
if (!selectorEntries.length) {
|
|
371
|
+
throw new Error('缺少 selector 定义');
|
|
372
|
+
}
|
|
373
|
+
const parentDefinition = (await this.readContainerDefinition(siteKey, parentId)) || { id: parentId, children: [] };
|
|
374
|
+
const normalizedChild = {
|
|
375
|
+
...(payload.definition || {}),
|
|
376
|
+
id: containerId,
|
|
377
|
+
selectors: selectorEntries,
|
|
378
|
+
name: payload.definition?.name || payload.alias || containerId,
|
|
379
|
+
type: payload.definition?.type || 'section',
|
|
380
|
+
capabilities: Array.isArray(payload.definition?.capabilities) && payload.definition.capabilities.length
|
|
381
|
+
? payload.definition.capabilities
|
|
382
|
+
: ['highlight', 'find-child', 'scroll'],
|
|
383
|
+
};
|
|
384
|
+
const alias = typeof payload.alias === 'string' ? payload.alias.trim() : '';
|
|
385
|
+
const metadata = { ...(normalizedChild.metadata || {}) };
|
|
386
|
+
if (alias) {
|
|
387
|
+
metadata.alias = alias;
|
|
388
|
+
normalizedChild.alias = alias;
|
|
389
|
+
normalizedChild.nickname = alias;
|
|
390
|
+
if (!normalizedChild.name) {
|
|
391
|
+
normalizedChild.name = alias;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
delete metadata.alias;
|
|
396
|
+
}
|
|
397
|
+
if (payload.domPath) {
|
|
398
|
+
metadata.source_dom_path = payload.domPath;
|
|
399
|
+
}
|
|
400
|
+
if (payload.domMeta && typeof payload.domMeta === 'object') {
|
|
401
|
+
metadata.source_dom_meta = payload.domMeta;
|
|
402
|
+
}
|
|
403
|
+
normalizedChild.metadata = metadata;
|
|
404
|
+
if (!normalizedChild.page_patterns || !normalizedChild.page_patterns.length) {
|
|
405
|
+
const parentPatterns = parentDefinition.page_patterns || parentDefinition.pagePatterns;
|
|
406
|
+
if (parentPatterns?.length) {
|
|
407
|
+
normalizedChild.page_patterns = parentPatterns;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const nextParent = { ...parentDefinition };
|
|
411
|
+
const childList = Array.isArray(nextParent.children) ? [...nextParent.children] : [];
|
|
412
|
+
if (!childList.includes(containerId)) {
|
|
413
|
+
childList.push(containerId);
|
|
414
|
+
}
|
|
415
|
+
nextParent.children = childList;
|
|
416
|
+
await this.writeUserContainerDefinition(siteKey, containerId, normalizedChild);
|
|
417
|
+
await this.writeUserContainerDefinition(siteKey, parentId, nextParent);
|
|
418
|
+
// 正确流程:写入定义后立即进行容器匹配,并通过 containers.matched 事件推送最新树
|
|
419
|
+
const { profile, url, maxDepth, maxChildren, rootSelector } = normalizePayload(payload, { required: ['profile'] });
|
|
420
|
+
return this.handleContainerMatch({
|
|
421
|
+
profile,
|
|
422
|
+
url,
|
|
423
|
+
maxDepth,
|
|
424
|
+
maxChildren,
|
|
425
|
+
rootSelector,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
async handleContainerUpdateAlias(payload = {}) {
|
|
429
|
+
const containerId = payload.containerId || payload.id;
|
|
430
|
+
if (!containerId) {
|
|
431
|
+
throw new Error('缺少容器 ID');
|
|
432
|
+
}
|
|
433
|
+
const alias = typeof payload.alias === 'string' ? payload.alias.trim() : '';
|
|
434
|
+
const siteKey = payload.siteKey ||
|
|
435
|
+
this.resolveSiteKeyFromUrl(payload.url) ||
|
|
436
|
+
this.inferSiteFromContainerId(containerId);
|
|
437
|
+
if (!siteKey) {
|
|
438
|
+
throw new Error('无法确定容器所属站点');
|
|
439
|
+
}
|
|
440
|
+
const baseDefinition = (await this.readContainerDefinition(siteKey, containerId)) || { id: containerId };
|
|
441
|
+
const metadata = { ...baseDefinition.metadata || {} };
|
|
442
|
+
if (alias) {
|
|
443
|
+
metadata.alias = alias;
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
delete metadata.alias;
|
|
447
|
+
}
|
|
448
|
+
const next = {
|
|
449
|
+
...baseDefinition,
|
|
450
|
+
name: baseDefinition.name || alias || containerId,
|
|
451
|
+
metadata,
|
|
452
|
+
};
|
|
453
|
+
if (alias) {
|
|
454
|
+
next.alias = alias;
|
|
455
|
+
next.nickname = alias;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
delete next.alias;
|
|
459
|
+
delete next.nickname;
|
|
460
|
+
}
|
|
461
|
+
await this.writeUserContainerDefinition(siteKey, containerId, next);
|
|
462
|
+
const { profile, url } = normalizePayload(payload, { required: ['profile'] });
|
|
463
|
+
return this.handleContainerInspect({ profile, url });
|
|
464
|
+
}
|
|
465
|
+
async handleContainerUpdateOperations(payload = {}) {
|
|
466
|
+
const containerId = payload.containerId || payload.id;
|
|
467
|
+
if (!containerId) {
|
|
468
|
+
throw new Error('缺少容器 ID');
|
|
469
|
+
}
|
|
470
|
+
const siteKey = payload.siteKey ||
|
|
471
|
+
this.resolveSiteKeyFromUrl(payload.url) ||
|
|
472
|
+
this.inferSiteFromContainerId(containerId);
|
|
473
|
+
if (!siteKey) {
|
|
474
|
+
throw new Error('无法确定容器所属站点');
|
|
475
|
+
}
|
|
476
|
+
const operations = Array.isArray(payload.operations) ? payload.operations : [];
|
|
477
|
+
const baseDefinition = (await this.readContainerDefinition(siteKey, containerId)) || { id: containerId };
|
|
478
|
+
const next = {
|
|
479
|
+
...baseDefinition,
|
|
480
|
+
operations,
|
|
481
|
+
};
|
|
482
|
+
await this.writeUserContainerDefinition(siteKey, containerId, next);
|
|
483
|
+
const { profile, url } = normalizePayload(payload, { required: ['profile'] });
|
|
484
|
+
return this.handleContainerInspect({ profile, url, containerId });
|
|
485
|
+
}
|
|
486
|
+
async handleContainerMatch(payload = {}) {
|
|
487
|
+
const { profile, url, maxDepth, maxChildren, rootSelector, cache, cacheTtlMs, invalidateCache } = normalizePayload(payload, { required: ['profile'] });
|
|
488
|
+
logDebug('controller', 'containers:match', { profile, url, payload });
|
|
489
|
+
try {
|
|
490
|
+
const context = await this.captureInspectorSnapshot({
|
|
491
|
+
profile,
|
|
492
|
+
url,
|
|
493
|
+
maxDepth: maxDepth || 2,
|
|
494
|
+
maxChildren: maxChildren || 5,
|
|
495
|
+
rootSelector,
|
|
496
|
+
cache,
|
|
497
|
+
cacheTtlMs,
|
|
498
|
+
invalidateCache,
|
|
499
|
+
});
|
|
500
|
+
const snapshot = context.snapshot;
|
|
501
|
+
const rootContainer = snapshot?.root_match?.container || snapshot?.container_tree?.container || snapshot?.container_tree?.containers?.[0];
|
|
502
|
+
const matchPayload = {
|
|
503
|
+
sessionId: context.sessionId,
|
|
504
|
+
profileId: context.profileId,
|
|
505
|
+
url: context.targetUrl,
|
|
506
|
+
matched: !!rootContainer,
|
|
507
|
+
container: rootContainer || null,
|
|
508
|
+
snapshot,
|
|
509
|
+
cache: context.cache || null,
|
|
510
|
+
};
|
|
511
|
+
this.messageBus?.publish?.('containers.matched', matchPayload);
|
|
512
|
+
if (matchPayload.container) {
|
|
513
|
+
this.emitContainerAppearEvents(snapshot.container_tree, matchPayload);
|
|
514
|
+
}
|
|
515
|
+
this.messageBus?.publish?.('handshake.status', {
|
|
516
|
+
status: matchPayload.matched ? 'ready' : 'pending',
|
|
517
|
+
profileId: matchPayload.profileId,
|
|
518
|
+
sessionId: matchPayload.sessionId,
|
|
519
|
+
url: matchPayload.url,
|
|
520
|
+
matched: matchPayload.matched,
|
|
521
|
+
containerId: matchPayload.container?.id || null,
|
|
522
|
+
source: 'containers:match',
|
|
523
|
+
ts: Date.now(),
|
|
524
|
+
});
|
|
525
|
+
logDebug('controller', 'containers.matched', {
|
|
526
|
+
profileId: matchPayload.profileId,
|
|
527
|
+
matched: matchPayload.matched,
|
|
528
|
+
containerId: matchPayload.container?.id,
|
|
529
|
+
childrenCount: matchPayload.container?.children?.length,
|
|
530
|
+
nodesCount: matchPayload.container?.match?.nodes?.length
|
|
531
|
+
});
|
|
532
|
+
return {
|
|
533
|
+
success: true,
|
|
534
|
+
data: matchPayload,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
throw new Error(`容器匹配失败: ${err?.message || String(err)}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async handleBrowserHighlight(payload = {}) {
|
|
542
|
+
const { profile, selector, style, duration, channel, sticky, maxMatches } = normalizePayload(payload, { required: ['profile', 'selector'] });
|
|
543
|
+
const highlightOpts = {
|
|
544
|
+
style,
|
|
545
|
+
duration,
|
|
546
|
+
channel,
|
|
547
|
+
sticky,
|
|
548
|
+
maxMatches,
|
|
549
|
+
};
|
|
550
|
+
try {
|
|
551
|
+
const result = await this.sendHighlightViaWs(profile, selector, highlightOpts);
|
|
552
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
553
|
+
success: true,
|
|
554
|
+
selector,
|
|
555
|
+
source: result?.source || 'unknown',
|
|
556
|
+
details: result?.details || null,
|
|
557
|
+
});
|
|
558
|
+
return { success: true, data: result };
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
const errorMessage = err?.message || '高亮请求失败';
|
|
562
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
563
|
+
success: false,
|
|
564
|
+
selector,
|
|
565
|
+
error: errorMessage,
|
|
566
|
+
});
|
|
567
|
+
throw err || new Error(errorMessage);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async handleBrowserClearHighlight(payload = {}) {
|
|
571
|
+
const { profile, channel } = normalizePayload(payload, { required: ['profile'] });
|
|
572
|
+
try {
|
|
573
|
+
const result = await this.sendClearHighlightViaWs(profile, channel || null);
|
|
574
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
575
|
+
success: true,
|
|
576
|
+
selector: null,
|
|
577
|
+
details: result,
|
|
578
|
+
});
|
|
579
|
+
return { success: true, data: result };
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
const message = err?.message || '清除高亮失败';
|
|
583
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
584
|
+
success: false,
|
|
585
|
+
selector: null,
|
|
586
|
+
error: message,
|
|
587
|
+
});
|
|
588
|
+
throw err;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async handleBrowserExecute(payload = {}) {
|
|
592
|
+
const { profile, script } = normalizePayload(payload, { required: ['profile', 'script'] });
|
|
593
|
+
try {
|
|
594
|
+
const result = await this.sendExecuteViaWs(profile, script);
|
|
595
|
+
return { success: true, data: result };
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
const errorMessage = err?.message || '执行脚本失败';
|
|
599
|
+
throw new Error(errorMessage);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async handleBrowserScreenshot(payload = {}) {
|
|
603
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
604
|
+
const fullPage = typeof payload.fullPage === 'boolean' ? payload.fullPage : Boolean(payload.fullPage);
|
|
605
|
+
// 截图在某些页面会更慢,放宽超时以保证调试证据可落盘
|
|
606
|
+
const result = await this.browserServiceCommand('screenshot', { profileId, fullPage }, { timeoutMs: 60000 });
|
|
607
|
+
return { success: true, data: result };
|
|
608
|
+
}
|
|
609
|
+
async handleBrowserFill(payload = {}) {
|
|
610
|
+
const { profile, selector, text } = normalizePayload(payload, { required: ['profile', 'selector', 'text'] });
|
|
611
|
+
logDebug('controller', 'browser:fill', { profile, selector, textLen: String(text || '').length });
|
|
612
|
+
// Use browser-service fill (Playwright page.fill) so it stays in system-level input pathway.
|
|
613
|
+
return this.browserServiceCommand('fill', { profileId: profile, selector, value: text }, { timeoutMs: 60000 });
|
|
614
|
+
}
|
|
615
|
+
async handleBrowserPageList(payload = {}) {
|
|
616
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
617
|
+
const result = await this.browserServiceCommand('page:list', { profileId }, { timeoutMs: 30000 });
|
|
618
|
+
return { success: true, data: result };
|
|
619
|
+
}
|
|
620
|
+
async handleBrowserPageNew(payload = {}) {
|
|
621
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
622
|
+
const url = payload.url ? String(payload.url) : undefined;
|
|
623
|
+
const result = await this.browserServiceCommand('page:new', { profileId, ...(url ? { url } : {}) }, { timeoutMs: 30000 });
|
|
624
|
+
const index = Number(result?.index ?? result?.data?.index);
|
|
625
|
+
if (Number.isFinite(index)) {
|
|
626
|
+
return { success: true, data: result };
|
|
627
|
+
}
|
|
628
|
+
const list = await this.browserServiceCommand('page:list', { profileId }, { timeoutMs: 30000 });
|
|
629
|
+
const activeIndexRaw = list?.activeIndex ?? list?.data?.activeIndex;
|
|
630
|
+
const activeIndex = Number(activeIndexRaw);
|
|
631
|
+
if (Number.isFinite(activeIndex)) {
|
|
632
|
+
return { success: true, data: { ...(result || {}), index: activeIndex, fallback: 'activeIndex' } };
|
|
633
|
+
}
|
|
634
|
+
return { success: true, data: result };
|
|
635
|
+
}
|
|
636
|
+
async handleBrowserPageSwitch(payload = {}) {
|
|
637
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
638
|
+
const index = Number(payload.index);
|
|
639
|
+
if (!Number.isFinite(index))
|
|
640
|
+
throw new Error('index required');
|
|
641
|
+
const result = await this.browserServiceCommand('page:switch', { profileId, index }, { timeoutMs: 30000 });
|
|
642
|
+
return { success: true, data: result };
|
|
643
|
+
}
|
|
644
|
+
async handleBrowserPageClose(payload = {}) {
|
|
645
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
646
|
+
const hasIndex = typeof payload.index !== 'undefined' && payload.index !== null;
|
|
647
|
+
const index = hasIndex ? Number(payload.index) : undefined;
|
|
648
|
+
const result = await this.browserServiceCommand('page:close', { profileId, ...(Number.isFinite(index) ? { index } : {}) }, { timeoutMs: 30000 });
|
|
649
|
+
return { success: true, data: result };
|
|
650
|
+
}
|
|
651
|
+
async fetchBrowserStatus() {
|
|
652
|
+
try {
|
|
653
|
+
const data = await this.browserServiceCommand('getStatus', {}, { timeoutMs: 10000 });
|
|
654
|
+
return { success: true, data };
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
return { success: false, error: err?.message || String(err) };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
getBrowserServiceHttpUrl() {
|
|
661
|
+
return `${this.defaultHttpProtocol}://${this.defaultHttpHost}:${this.defaultHttpPort}`;
|
|
662
|
+
}
|
|
663
|
+
async browserServiceCommand(action, args, options = {}) {
|
|
664
|
+
const timeoutMs = typeof options.timeoutMs === 'number' && options.timeoutMs > 0 ? options.timeoutMs : 20000;
|
|
665
|
+
const abortController1 = new AbortController();
|
|
666
|
+
const timeoutId1 = setTimeout(() => abortController1.abort(), timeoutMs);
|
|
667
|
+
const res = await fetch(`${this.getBrowserServiceHttpUrl()}/command`, {
|
|
668
|
+
method: 'POST',
|
|
669
|
+
headers: { 'Content-Type': 'application/json' },
|
|
670
|
+
body: JSON.stringify({ action, args }),
|
|
671
|
+
signal: abortController1.signal,
|
|
672
|
+
});
|
|
673
|
+
clearTimeout(timeoutId1);
|
|
674
|
+
const raw = await res.text();
|
|
675
|
+
let data = {};
|
|
676
|
+
try {
|
|
677
|
+
data = raw ? JSON.parse(raw) : {};
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
data = { raw };
|
|
681
|
+
}
|
|
682
|
+
if (!res.ok) {
|
|
683
|
+
throw new Error(data?.error || data?.body?.error || `browser-service command "${action}" HTTP ${res.status}`);
|
|
684
|
+
}
|
|
685
|
+
if (data && data.ok === false) {
|
|
686
|
+
throw new Error(data.error || `browser-service command "${action}" failed`);
|
|
687
|
+
}
|
|
688
|
+
if (data && data.error) {
|
|
689
|
+
throw new Error(data.error);
|
|
690
|
+
}
|
|
691
|
+
return data.body ?? data;
|
|
692
|
+
}
|
|
693
|
+
async handleBrowserGoto(payload = {}) {
|
|
694
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
695
|
+
const url = (payload.url || '').toString();
|
|
696
|
+
if (!url)
|
|
697
|
+
throw new Error('url required');
|
|
698
|
+
const result = await this.browserServiceCommand('goto', { profileId, url });
|
|
699
|
+
return { success: true, data: result };
|
|
700
|
+
}
|
|
701
|
+
async handleBrowserSetViewport(payload = {}) {
|
|
702
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
703
|
+
const width = Number(payload.width);
|
|
704
|
+
const height = Number(payload.height);
|
|
705
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) {
|
|
706
|
+
throw new Error('width/height required');
|
|
707
|
+
}
|
|
708
|
+
const result = await this.browserServiceCommand('page:setViewport', { profileId, width, height }, { timeoutMs: 30000 });
|
|
709
|
+
return { success: true, data: result };
|
|
710
|
+
}
|
|
711
|
+
async handleKeyboardPress(payload = {}) {
|
|
712
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
713
|
+
const key = (payload.key || 'Enter').toString();
|
|
714
|
+
const delay = typeof payload.delay === 'number' ? payload.delay : undefined;
|
|
715
|
+
const result = await this.browserServiceCommand('keyboard:press', { profileId, key, ...(delay ? { delay } : {}) });
|
|
716
|
+
return { success: true, data: result };
|
|
717
|
+
}
|
|
718
|
+
async handleKeyboardType(payload = {}) {
|
|
719
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
720
|
+
const text = (payload.text ?? '').toString();
|
|
721
|
+
const delay = typeof payload.delay === 'number' ? payload.delay : undefined;
|
|
722
|
+
const submit = typeof payload.submit === 'boolean' ? payload.submit : Boolean(payload.submit);
|
|
723
|
+
const result = await this.browserServiceCommand('keyboard:type', { profileId, text, ...(delay ? { delay } : {}), ...(submit ? { submit } : {}) });
|
|
724
|
+
return { success: true, data: result };
|
|
725
|
+
}
|
|
726
|
+
async handleMouseWheel(payload = {}) {
|
|
727
|
+
const profileId = (payload.profileId || payload.profile || payload.sessionId || 'default').toString();
|
|
728
|
+
const deltaY = Number(payload.deltaY ?? payload.y ?? payload.dy ?? 0) || 0;
|
|
729
|
+
const deltaX = Number(payload.deltaX ?? payload.x ?? payload.dx ?? 0) || 0;
|
|
730
|
+
const result = await this.browserServiceCommand('mouse:wheel', { profileId, deltaX, deltaY });
|
|
731
|
+
return { success: true, data: result };
|
|
732
|
+
}
|
|
733
|
+
async handleBrowserHighlightDomPath(payload = {}) {
|
|
734
|
+
const { profile, path, url, rootSelector, style, sticky, duration, channel } = normalizePayload(payload, { required: ['profile', 'path'] });
|
|
735
|
+
// 先加载 DOM 分支确保元素存在
|
|
736
|
+
if (path !== 'root' && url) {
|
|
737
|
+
try {
|
|
738
|
+
await this.fetchDomBranchFromService({
|
|
739
|
+
sessionId: profile,
|
|
740
|
+
url,
|
|
741
|
+
path,
|
|
742
|
+
rootSelector,
|
|
743
|
+
maxDepth: 2,
|
|
744
|
+
maxChildren: 10,
|
|
745
|
+
});
|
|
746
|
+
logDebug('controller', 'highlight-dom-path', { message: 'DOM branch loaded', path });
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
logDebug('controller', 'highlight-dom-path', { message: 'Failed to load DOM branch', path, error: err.message });
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const finalChannel = channel || 'hover-dom';
|
|
753
|
+
const finalStyle = style || '2px solid rgba(96, 165, 250, 0.95)';
|
|
754
|
+
const finalSticky = typeof sticky === 'boolean' ? sticky : true;
|
|
755
|
+
try {
|
|
756
|
+
const result = await this.sendHighlightDomPathViaWs(profile, path, {
|
|
757
|
+
channel: finalChannel,
|
|
758
|
+
style: finalStyle,
|
|
759
|
+
sticky: finalSticky,
|
|
760
|
+
duration,
|
|
761
|
+
rootSelector: rootSelector || null,
|
|
762
|
+
});
|
|
763
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
764
|
+
success: true,
|
|
765
|
+
selector: null,
|
|
766
|
+
details: result?.details || null,
|
|
767
|
+
});
|
|
768
|
+
return { success: true, data: result };
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
const errorMessage = err?.message || 'DOM 路径高亮失败';
|
|
772
|
+
this.messageBus?.publish?.('ui.highlight.result', {
|
|
773
|
+
success: false,
|
|
774
|
+
selector: null,
|
|
775
|
+
error: errorMessage,
|
|
776
|
+
});
|
|
777
|
+
throw err;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
async handleBrowserCancelDomPick(payload = {}) {
|
|
781
|
+
const { profile } = normalizePayload(payload, { required: ['profile'] });
|
|
782
|
+
try {
|
|
783
|
+
const data = await this.sendCancelDomPickViaWs(profile);
|
|
784
|
+
this.messageBus?.publish?.('ui.domPicker.result', {
|
|
785
|
+
success: false,
|
|
786
|
+
cancelled: true,
|
|
787
|
+
source: 'cancel-action',
|
|
788
|
+
details: data,
|
|
789
|
+
});
|
|
790
|
+
return { success: true, data };
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
const message = err?.message || '取消捕获失败';
|
|
794
|
+
this.messageBus?.publish?.('ui.domPicker.result', {
|
|
795
|
+
success: false,
|
|
796
|
+
cancelled: true,
|
|
797
|
+
error: message,
|
|
798
|
+
});
|
|
799
|
+
throw err;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async handleBrowserPickDom(payload = {}) {
|
|
803
|
+
const { profile, timeout, rootSelector } = normalizePayload(payload, { required: ['profile'] });
|
|
804
|
+
const finalTimeout = Math.min(Math.max(Number(timeout) || 25000, 3000), 60000);
|
|
805
|
+
const startedAt = Date.now();
|
|
806
|
+
try {
|
|
807
|
+
const result = await this.sendDomPickerViaWs(profile, {
|
|
808
|
+
timeout: finalTimeout,
|
|
809
|
+
rootSelector,
|
|
810
|
+
});
|
|
811
|
+
this.messageBus?.publish?.('ui.domPicker.result', {
|
|
812
|
+
success: true,
|
|
813
|
+
selector: result?.selector || null,
|
|
814
|
+
domPath: result?.dom_path || null,
|
|
815
|
+
durationMs: Date.now() - startedAt,
|
|
816
|
+
});
|
|
817
|
+
return { success: true, data: result };
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
const message = err?.message || '元素拾取失败';
|
|
821
|
+
this.messageBus?.publish?.('ui.domPicker.result', { success: false, error: message });
|
|
822
|
+
throw err;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// v2 DOM pick:直接暴露 dom_path + selector 给 UI
|
|
826
|
+
async handleDomPick2(payload = {}) {
|
|
827
|
+
const { profile, timeout, rootSelector } = normalizePayload(payload, { required: ['profile'] });
|
|
828
|
+
const finalTimeout = Math.min(Math.max(Number(timeout) || 25000, 3000), 60000);
|
|
829
|
+
const result = await this.sendDomPickerViaWs(profile, { timeout: finalTimeout, rootSelector });
|
|
830
|
+
// 统一输出结构:domPath + selector
|
|
831
|
+
return {
|
|
832
|
+
success: true,
|
|
833
|
+
data: {
|
|
834
|
+
domPath: result?.dom_path || null,
|
|
835
|
+
selector: result?.selector || null,
|
|
836
|
+
raw: result,
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
async sendHighlightViaWs(sessionId, selector, options = {}) {
|
|
841
|
+
const payload = {
|
|
842
|
+
type: 'command',
|
|
843
|
+
session_id: sessionId,
|
|
844
|
+
data: {
|
|
845
|
+
command_type: 'dev_command',
|
|
846
|
+
action: 'highlight_element',
|
|
847
|
+
parameters: {
|
|
848
|
+
selector,
|
|
849
|
+
...(options.style ? { style: options.style } : {}),
|
|
850
|
+
...(typeof options.duration === 'number' ? { duration: options.duration } : {}),
|
|
851
|
+
...(options.channel ? { channel: options.channel } : {}),
|
|
852
|
+
...(typeof options.sticky === 'boolean' ? { sticky: options.sticky } : {}),
|
|
853
|
+
...(typeof options.maxMatches === 'number' ? { max_matches: options.maxMatches } : {}),
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
};
|
|
857
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
|
|
858
|
+
const data = response?.data || response;
|
|
859
|
+
const success = data?.success !== false;
|
|
860
|
+
if (!success) {
|
|
861
|
+
const err = data?.error || response?.error;
|
|
862
|
+
throw new Error(err || 'highlight_element failed');
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
success: true,
|
|
866
|
+
source: 'ws',
|
|
867
|
+
details: data?.data || data,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
async sendHighlightDomPathViaWs(sessionId, domPath, options = {}) {
|
|
871
|
+
const payload = {
|
|
872
|
+
type: 'command',
|
|
873
|
+
session_id: sessionId,
|
|
874
|
+
data: {
|
|
875
|
+
command_type: 'dev_command',
|
|
876
|
+
action: 'highlight_dom_path',
|
|
877
|
+
parameters: {
|
|
878
|
+
path: domPath,
|
|
879
|
+
...(options.style ? { style: options.style } : {}),
|
|
880
|
+
...(typeof options.duration === 'number' ? { duration: options.duration } : {}),
|
|
881
|
+
...(options.channel ? { channel: options.channel } : {}),
|
|
882
|
+
...(typeof options.sticky === 'boolean' ? { sticky: options.sticky } : {}),
|
|
883
|
+
...(options.rootSelector ? { root_selector: options.rootSelector } : {}),
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
|
|
888
|
+
const data = response?.data || response;
|
|
889
|
+
const success = data?.success !== false;
|
|
890
|
+
if (!success) {
|
|
891
|
+
const err = data?.error || response?.error;
|
|
892
|
+
throw new Error(err || 'highlight_dom_path failed');
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
success: true,
|
|
896
|
+
source: 'ws',
|
|
897
|
+
details: data?.data || data,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
async sendClearHighlightViaWs(sessionId, channel = null) {
|
|
901
|
+
const payload = {
|
|
902
|
+
type: 'command',
|
|
903
|
+
session_id: sessionId,
|
|
904
|
+
data: {
|
|
905
|
+
command_type: 'dev_command',
|
|
906
|
+
action: 'clear_highlight',
|
|
907
|
+
parameters: channel ? { channel } : {},
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 10000);
|
|
911
|
+
const data = response?.data || response;
|
|
912
|
+
if (data?.success === false) {
|
|
913
|
+
throw new Error(data?.error || 'clear_highlight failed');
|
|
914
|
+
}
|
|
915
|
+
return data?.data || data || { removed: 0 };
|
|
916
|
+
}
|
|
917
|
+
async sendCancelDomPickViaWs(sessionId) {
|
|
918
|
+
const payload = {
|
|
919
|
+
type: 'command',
|
|
920
|
+
session_id: sessionId,
|
|
921
|
+
data: {
|
|
922
|
+
command_type: 'dev_command',
|
|
923
|
+
action: 'cancel_dom_pick',
|
|
924
|
+
parameters: {},
|
|
925
|
+
},
|
|
926
|
+
};
|
|
927
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 10000);
|
|
928
|
+
const data = response?.data || response;
|
|
929
|
+
if (data?.success === false) {
|
|
930
|
+
throw new Error(data?.error || 'cancel_dom_pick failed');
|
|
931
|
+
}
|
|
932
|
+
return data?.data || data || { cancelled: false };
|
|
933
|
+
}
|
|
934
|
+
async sendDomPickerViaWs(sessionId, options = {}) {
|
|
935
|
+
const timeout = Math.min(Math.max(Number(options.timeout) || 25000, 3000), 60000);
|
|
936
|
+
const payload = {
|
|
937
|
+
type: 'command',
|
|
938
|
+
session_id: sessionId,
|
|
939
|
+
data: {
|
|
940
|
+
command_type: 'node_execute',
|
|
941
|
+
node_type: 'pick_dom',
|
|
942
|
+
parameters: {
|
|
943
|
+
timeout,
|
|
944
|
+
...(options.rootSelector ? { root_selector: options.rootSelector } : {}),
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, timeout + 5000);
|
|
949
|
+
const data = response?.data;
|
|
950
|
+
if (data?.success === false) {
|
|
951
|
+
throw new Error(data?.error || 'pick_dom failed');
|
|
952
|
+
}
|
|
953
|
+
const result = data?.data || data;
|
|
954
|
+
if (!result) {
|
|
955
|
+
throw new Error('picker result missing');
|
|
956
|
+
}
|
|
957
|
+
return result;
|
|
958
|
+
}
|
|
959
|
+
async sendExecuteViaWs(sessionId, script) {
|
|
960
|
+
const payload = {
|
|
961
|
+
type: 'command',
|
|
962
|
+
session_id: sessionId,
|
|
963
|
+
data: {
|
|
964
|
+
command_type: 'node_execute',
|
|
965
|
+
node_type: 'evaluate',
|
|
966
|
+
parameters: {
|
|
967
|
+
script,
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 10000);
|
|
972
|
+
const data = response?.data || response;
|
|
973
|
+
if (data?.success === false) {
|
|
974
|
+
throw new Error(data?.error || 'execute failed');
|
|
975
|
+
}
|
|
976
|
+
return data?.data || data || { result: null };
|
|
977
|
+
}
|
|
978
|
+
async fetchContainerSnapshotFromService({ sessionId, url, maxDepth, maxChildren, rootContainerId, rootSelector }) {
|
|
979
|
+
if (!sessionId || !url) {
|
|
980
|
+
throw new Error('缺少 sessionId 或 URL');
|
|
981
|
+
}
|
|
982
|
+
const payload = {
|
|
983
|
+
type: 'command',
|
|
984
|
+
session_id: sessionId,
|
|
985
|
+
data: {
|
|
986
|
+
command_type: 'container_operation',
|
|
987
|
+
action: 'inspect_tree',
|
|
988
|
+
page_context: { url },
|
|
989
|
+
parameters: {
|
|
990
|
+
...(typeof maxDepth === 'number' ? { max_depth: maxDepth } : {}),
|
|
991
|
+
...(typeof maxChildren === 'number' ? { max_children: maxChildren } : {}),
|
|
992
|
+
...(rootContainerId ? { root_container_id: rootContainerId } : {}),
|
|
993
|
+
...(rootSelector ? { root_selector: rootSelector } : {}),
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
};
|
|
997
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
|
|
998
|
+
if (response?.data?.success) {
|
|
999
|
+
return response.data.data || response.data.snapshot || response.data;
|
|
1000
|
+
}
|
|
1001
|
+
throw new Error(response?.data?.error || response?.error || 'inspect_tree failed');
|
|
1002
|
+
}
|
|
1003
|
+
async fetchDomBranchFromService({ sessionId, url, path, rootSelector, maxDepth, maxChildren }) {
|
|
1004
|
+
if (!sessionId || !url || !path) {
|
|
1005
|
+
throw new Error('缺少 sessionId / URL / DOM 路径');
|
|
1006
|
+
}
|
|
1007
|
+
const payload = {
|
|
1008
|
+
type: 'command',
|
|
1009
|
+
session_id: sessionId,
|
|
1010
|
+
data: {
|
|
1011
|
+
command_type: 'container_operation',
|
|
1012
|
+
action: 'inspect_dom_branch',
|
|
1013
|
+
page_context: { url },
|
|
1014
|
+
parameters: {
|
|
1015
|
+
path,
|
|
1016
|
+
...(rootSelector ? { root_selector: rootSelector } : {}),
|
|
1017
|
+
...(typeof maxDepth === 'number' ? { max_depth: maxDepth } : {}),
|
|
1018
|
+
...(typeof maxChildren === 'number' ? { max_children: maxChildren } : {}),
|
|
1019
|
+
},
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
const response = await this.sendWsCommand(this.getBrowserWsUrl(), payload, 20000);
|
|
1023
|
+
if (response?.data?.success) {
|
|
1024
|
+
return response.data.data || response.data.snapshot || response.data;
|
|
1025
|
+
}
|
|
1026
|
+
throw new Error(response?.data?.error || response?.error || 'inspect_dom_branch failed');
|
|
1027
|
+
}
|
|
1028
|
+
// 下列方法尚未在 TS 版本实现,仅为保持编译通过的占位实现
|
|
1029
|
+
async handleDomBranch2(_payload = {}) {
|
|
1030
|
+
throw new Error('handleDomBranch2 is not implemented in controller.ts');
|
|
1031
|
+
}
|
|
1032
|
+
async fetchSessions() {
|
|
1033
|
+
try {
|
|
1034
|
+
const res = await this.runCliCommand('session-manager', ['list']);
|
|
1035
|
+
const sessions = res?.sessions || res?.data?.sessions || res?.data || [];
|
|
1036
|
+
return Array.isArray(sessions) ? sessions : [];
|
|
1037
|
+
}
|
|
1038
|
+
catch {
|
|
1039
|
+
return [];
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
findSessionByProfile(sessions, profile) {
|
|
1043
|
+
if (!profile)
|
|
1044
|
+
return null;
|
|
1045
|
+
return (sessions.find((session) => session?.profileId === profile ||
|
|
1046
|
+
session?.profile_id === profile ||
|
|
1047
|
+
session?.session_id === profile ||
|
|
1048
|
+
session?.sessionId === profile) || null);
|
|
1049
|
+
}
|
|
1050
|
+
focusSnapshotOnContainer(snapshot, containerId) {
|
|
1051
|
+
if (!containerId || !snapshot?.container_tree) {
|
|
1052
|
+
return snapshot;
|
|
1053
|
+
}
|
|
1054
|
+
const target = this.cloneContainerSubtree(snapshot.container_tree, containerId);
|
|
1055
|
+
if (!target) {
|
|
1056
|
+
return snapshot;
|
|
1057
|
+
}
|
|
1058
|
+
const nextSnapshot = {
|
|
1059
|
+
...snapshot,
|
|
1060
|
+
container_tree: target,
|
|
1061
|
+
metadata: {
|
|
1062
|
+
...(snapshot.metadata || {}),
|
|
1063
|
+
root_container_id: containerId,
|
|
1064
|
+
},
|
|
1065
|
+
};
|
|
1066
|
+
if (!nextSnapshot.root_match || nextSnapshot.root_match?.container?.id !== containerId) {
|
|
1067
|
+
nextSnapshot.root_match = {
|
|
1068
|
+
container: {
|
|
1069
|
+
id: containerId,
|
|
1070
|
+
...(target.name ? { name: target.name } : {}),
|
|
1071
|
+
},
|
|
1072
|
+
matched_selector: target.match?.matched_selector,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
return nextSnapshot;
|
|
1076
|
+
}
|
|
1077
|
+
cloneContainerSubtree(node, targetId) {
|
|
1078
|
+
if (!node)
|
|
1079
|
+
return null;
|
|
1080
|
+
if (node.id === targetId || node.container_id === targetId) {
|
|
1081
|
+
return this.deepClone(node);
|
|
1082
|
+
}
|
|
1083
|
+
if (Array.isArray(node.children)) {
|
|
1084
|
+
for (const child of node.children) {
|
|
1085
|
+
const match = this.cloneContainerSubtree(child, targetId);
|
|
1086
|
+
if (match)
|
|
1087
|
+
return match;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
deepClone(payload) {
|
|
1093
|
+
return JSON.parse(JSON.stringify(payload));
|
|
1094
|
+
}
|
|
1095
|
+
async captureInspectorSnapshot(options = {}) {
|
|
1096
|
+
const profile = options.profile;
|
|
1097
|
+
// 优先使用调用方提供的 URL,避免为了 containers:match 再额外跑一次 session-manager CLI
|
|
1098
|
+
// 只有在 URL 缺失时才回退到 CLI 查询当前会话列表。
|
|
1099
|
+
let targetSession = null;
|
|
1100
|
+
let sessions = [];
|
|
1101
|
+
if (!options.url) {
|
|
1102
|
+
sessions = await this.fetchSessions();
|
|
1103
|
+
targetSession = profile ? this.findSessionByProfile(sessions, profile) : sessions[0] || null;
|
|
1104
|
+
}
|
|
1105
|
+
const sessionId = profile || targetSession?.session_id || targetSession?.sessionId || null;
|
|
1106
|
+
const profileId = profile || targetSession?.profileId || targetSession?.profile_id || sessionId || null;
|
|
1107
|
+
const targetUrl = options.url || targetSession?.current_url || targetSession?.currentUrl;
|
|
1108
|
+
const requestedContainerId = options.containerId || options.rootContainerId;
|
|
1109
|
+
if (!targetUrl) {
|
|
1110
|
+
throw new Error('无法确定会话 URL,请先在浏览器中打开目标页面');
|
|
1111
|
+
}
|
|
1112
|
+
let liveError = null;
|
|
1113
|
+
let snapshot = null;
|
|
1114
|
+
let fromCache = false;
|
|
1115
|
+
let cacheAgeMs = null;
|
|
1116
|
+
const cacheEnabled = typeof options.cache === 'boolean'
|
|
1117
|
+
? options.cache
|
|
1118
|
+
: String(process.env.WEBAUTO_CONTAINER_SNAPSHOT_CACHE || '0') === '1';
|
|
1119
|
+
const cacheTtlMs = typeof options.cacheTtlMs === 'number' && Number.isFinite(options.cacheTtlMs)
|
|
1120
|
+
? Math.max(0, Math.floor(options.cacheTtlMs))
|
|
1121
|
+
: Number(process.env.WEBAUTO_CONTAINER_SNAPSHOT_CACHE_TTL_MS || 5000);
|
|
1122
|
+
if (sessionId && targetUrl && cacheEnabled) {
|
|
1123
|
+
const cacheKey = this.getSnapshotCacheKey(options, targetUrl, sessionId);
|
|
1124
|
+
if (options.invalidateCache) {
|
|
1125
|
+
this.snapshotCache.delete(cacheKey);
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
const cached = this.snapshotCache.get(cacheKey);
|
|
1129
|
+
if (cached) {
|
|
1130
|
+
const age = Date.now() - cached.ts;
|
|
1131
|
+
if (age <= cacheTtlMs) {
|
|
1132
|
+
snapshot = cached.snapshot;
|
|
1133
|
+
fromCache = true;
|
|
1134
|
+
cacheAgeMs = age;
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
this.snapshotCache.delete(cacheKey);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (sessionId) {
|
|
1143
|
+
try {
|
|
1144
|
+
if (!snapshot) {
|
|
1145
|
+
snapshot = await this.fetchContainerSnapshotFromService({
|
|
1146
|
+
sessionId,
|
|
1147
|
+
url: targetUrl,
|
|
1148
|
+
maxDepth: options.maxDepth,
|
|
1149
|
+
maxChildren: options.maxChildren,
|
|
1150
|
+
rootContainerId: requestedContainerId,
|
|
1151
|
+
rootSelector: options.rootSelector,
|
|
1152
|
+
});
|
|
1153
|
+
if (snapshot && cacheEnabled && sessionId && targetUrl) {
|
|
1154
|
+
const cacheKey = this.getSnapshotCacheKey(options, targetUrl, sessionId);
|
|
1155
|
+
this.snapshotCache.set(cacheKey, {
|
|
1156
|
+
ts: Date.now(),
|
|
1157
|
+
sessionId,
|
|
1158
|
+
profileId: profileId || 'default',
|
|
1159
|
+
url: targetUrl,
|
|
1160
|
+
snapshot,
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
catch (err) {
|
|
1166
|
+
liveError = err;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (!snapshot || !snapshot.container_tree) {
|
|
1170
|
+
const rootError = liveError || new Error('容器树为空,检查容器定义或选择器是否正确');
|
|
1171
|
+
throw rootError;
|
|
1172
|
+
}
|
|
1173
|
+
if (requestedContainerId) {
|
|
1174
|
+
snapshot = this.focusSnapshotOnContainer(snapshot, requestedContainerId);
|
|
1175
|
+
}
|
|
1176
|
+
return {
|
|
1177
|
+
sessionId: sessionId || profileId || 'unknown-session',
|
|
1178
|
+
profileId: profileId || 'default',
|
|
1179
|
+
targetUrl,
|
|
1180
|
+
snapshot,
|
|
1181
|
+
cache: {
|
|
1182
|
+
enabled: cacheEnabled,
|
|
1183
|
+
hit: fromCache,
|
|
1184
|
+
ageMs: cacheAgeMs,
|
|
1185
|
+
ttlMs: cacheTtlMs,
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
async captureInspectorBranch(options = {}) {
|
|
1190
|
+
const profile = options.profile;
|
|
1191
|
+
const domPath = options.path;
|
|
1192
|
+
if (!profile)
|
|
1193
|
+
throw new Error('缺少 profile');
|
|
1194
|
+
if (!domPath)
|
|
1195
|
+
throw new Error('缺少 DOM 路径');
|
|
1196
|
+
const sessions = await this.fetchSessions();
|
|
1197
|
+
const targetSession = profile ? this.findSessionByProfile(sessions, profile) : sessions[0] || null;
|
|
1198
|
+
const sessionId = targetSession?.session_id || targetSession?.sessionId || profile || null;
|
|
1199
|
+
const profileId = profile || targetSession?.profileId || targetSession?.profile_id || sessionId || null;
|
|
1200
|
+
const targetUrl = options.url || targetSession?.current_url || targetSession?.currentUrl;
|
|
1201
|
+
if (!targetUrl) {
|
|
1202
|
+
throw new Error('无法确定会话 URL');
|
|
1203
|
+
}
|
|
1204
|
+
let branch = null;
|
|
1205
|
+
let liveError = null;
|
|
1206
|
+
if (sessionId) {
|
|
1207
|
+
try {
|
|
1208
|
+
branch = await this.fetchDomBranchFromService({
|
|
1209
|
+
sessionId,
|
|
1210
|
+
url: targetUrl,
|
|
1211
|
+
path: domPath,
|
|
1212
|
+
rootSelector: options.rootSelector,
|
|
1213
|
+
maxDepth: options.maxDepth,
|
|
1214
|
+
maxChildren: options.maxChildren,
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
catch (err) {
|
|
1218
|
+
liveError = err;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (!branch?.node) {
|
|
1222
|
+
throw liveError || new Error('无法获取 DOM 分支');
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
sessionId: sessionId || profileId || 'unknown-session',
|
|
1226
|
+
profileId: profileId || 'default',
|
|
1227
|
+
targetUrl,
|
|
1228
|
+
branch,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
resolveSiteKeyFromUrl(url) {
|
|
1232
|
+
if (!url)
|
|
1233
|
+
return null;
|
|
1234
|
+
let host = '';
|
|
1235
|
+
try {
|
|
1236
|
+
host = new URL(url).hostname.toLowerCase();
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
const index = this.loadContainerIndex();
|
|
1242
|
+
let bestKey = null;
|
|
1243
|
+
let bestLen = -1;
|
|
1244
|
+
for (const [key, meta] of Object.entries(index)) {
|
|
1245
|
+
const domain = (meta?.website || '').toLowerCase();
|
|
1246
|
+
if (!domain)
|
|
1247
|
+
continue;
|
|
1248
|
+
if (host === domain || host.endsWith(`.${domain}`)) {
|
|
1249
|
+
if (domain.length > bestLen) {
|
|
1250
|
+
bestKey = key;
|
|
1251
|
+
bestLen = domain.length;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return bestKey;
|
|
1256
|
+
}
|
|
1257
|
+
loadContainerIndex() {
|
|
1258
|
+
if (this._containerIndexCache) {
|
|
1259
|
+
return this._containerIndexCache;
|
|
1260
|
+
}
|
|
1261
|
+
if (!fs.existsSync(this.containerIndexPath)) {
|
|
1262
|
+
this._containerIndexCache = {};
|
|
1263
|
+
return this._containerIndexCache;
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
this._containerIndexCache = JSON.parse(fs.readFileSync(this.containerIndexPath, 'utf-8'));
|
|
1267
|
+
}
|
|
1268
|
+
catch {
|
|
1269
|
+
this._containerIndexCache = {};
|
|
1270
|
+
}
|
|
1271
|
+
return this._containerIndexCache;
|
|
1272
|
+
}
|
|
1273
|
+
inferSiteFromContainerId(containerId) {
|
|
1274
|
+
if (!containerId)
|
|
1275
|
+
return null;
|
|
1276
|
+
const dotIdx = containerId.indexOf('.');
|
|
1277
|
+
if (dotIdx > 0) {
|
|
1278
|
+
return containerId.slice(0, dotIdx);
|
|
1279
|
+
}
|
|
1280
|
+
const underscoreIdx = containerId.indexOf('_');
|
|
1281
|
+
if (underscoreIdx > 0) {
|
|
1282
|
+
return containerId.slice(0, underscoreIdx);
|
|
1283
|
+
}
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
async writeUserContainerDefinition(siteKey, containerId, definition) {
|
|
1287
|
+
const parts = containerId.split('.').filter(Boolean);
|
|
1288
|
+
const targetDir = path.join(this.userContainerRoot, siteKey, ...parts);
|
|
1289
|
+
await fsPromises.mkdir(targetDir, { recursive: true });
|
|
1290
|
+
const filePath = path.join(targetDir, 'container.json');
|
|
1291
|
+
const payload = { ...definition, id: containerId };
|
|
1292
|
+
await fsPromises.writeFile(filePath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
1293
|
+
}
|
|
1294
|
+
normalizeSelectors(_selectors) {
|
|
1295
|
+
return _selectors;
|
|
1296
|
+
}
|
|
1297
|
+
async readContainerDefinition(siteKey, containerId) {
|
|
1298
|
+
const parts = containerId.split('.').filter(Boolean);
|
|
1299
|
+
const targetDir = path.join(this.userContainerRoot, siteKey, ...parts);
|
|
1300
|
+
const filePath = path.join(targetDir, 'container.json');
|
|
1301
|
+
if (!fs.existsSync(filePath)) {
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
try {
|
|
1305
|
+
const content = await fsPromises.readFile(filePath, 'utf-8');
|
|
1306
|
+
return JSON.parse(content);
|
|
1307
|
+
}
|
|
1308
|
+
catch {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
async sendWsCommand(_url, _payload, _timeout = 15000) {
|
|
1313
|
+
return new Promise((resolve, reject) => {
|
|
1314
|
+
const socket = new WebSocket(_url);
|
|
1315
|
+
let settled = false;
|
|
1316
|
+
const timeout = setTimeout(() => {
|
|
1317
|
+
if (settled)
|
|
1318
|
+
return;
|
|
1319
|
+
settled = true;
|
|
1320
|
+
socket.terminate();
|
|
1321
|
+
reject(new Error("WebSocket command timeout"));
|
|
1322
|
+
}, _timeout);
|
|
1323
|
+
const cleanup = () => {
|
|
1324
|
+
clearTimeout(timeout);
|
|
1325
|
+
socket.removeAllListeners();
|
|
1326
|
+
};
|
|
1327
|
+
socket.once("open", () => {
|
|
1328
|
+
try {
|
|
1329
|
+
socket.send(JSON.stringify(_payload));
|
|
1330
|
+
}
|
|
1331
|
+
catch (err) {
|
|
1332
|
+
cleanup();
|
|
1333
|
+
if (!settled) {
|
|
1334
|
+
settled = true;
|
|
1335
|
+
reject(err);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
socket.once("message", (data) => {
|
|
1340
|
+
cleanup();
|
|
1341
|
+
if (settled)
|
|
1342
|
+
return;
|
|
1343
|
+
settled = true;
|
|
1344
|
+
try {
|
|
1345
|
+
resolve(JSON.parse(data.toString("utf-8")));
|
|
1346
|
+
}
|
|
1347
|
+
catch (err) {
|
|
1348
|
+
reject(err);
|
|
1349
|
+
}
|
|
1350
|
+
finally {
|
|
1351
|
+
socket.close();
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
socket.once("error", (err) => {
|
|
1355
|
+
cleanup();
|
|
1356
|
+
if (settled)
|
|
1357
|
+
return;
|
|
1358
|
+
settled = true;
|
|
1359
|
+
reject(err);
|
|
1360
|
+
});
|
|
1361
|
+
socket.once("close", () => {
|
|
1362
|
+
cleanup();
|
|
1363
|
+
if (!settled) {
|
|
1364
|
+
settled = true;
|
|
1365
|
+
reject(new Error("WebSocket closed before response"));
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
getBrowserWsUrl() {
|
|
1371
|
+
return `ws://${this.defaultWsHost || '127.0.0.1'}:${this.defaultWsPort || 7701}/ws`;
|
|
1372
|
+
}
|
|
1373
|
+
async handleContainerOperation(payload = {}) {
|
|
1374
|
+
const { containerId, operationId, config, profile } = normalizePayload(payload, { required: ['containerId', 'operationId', 'profile'] });
|
|
1375
|
+
// Allow caller to override timeout; without this Phase2 can hang until the outer controllerAction aborts.
|
|
1376
|
+
const timeoutMs = typeof payload?.timeoutMs === 'number' && Number.isFinite(payload.timeoutMs) && payload.timeoutMs > 0
|
|
1377
|
+
? Math.floor(payload.timeoutMs)
|
|
1378
|
+
: 120000;
|
|
1379
|
+
// Determine target URL for HTTP post to container endpoint
|
|
1380
|
+
const port = process.env.WEBAUTO_UNIFIED_PORT || 7701;
|
|
1381
|
+
const host = '127.0.0.1';
|
|
1382
|
+
logDebug('controller', 'container:operation', { containerId, operationId, profile, timeoutMs });
|
|
1383
|
+
const abortController2 = new AbortController();
|
|
1384
|
+
const timeoutId2 = setTimeout(() => abortController2.abort(), timeoutMs);
|
|
1385
|
+
try {
|
|
1386
|
+
const mergedConfig = { ...(config || {}) };
|
|
1387
|
+
if (typeof mergedConfig.timeoutMs !== 'number') {
|
|
1388
|
+
mergedConfig.timeoutMs = timeoutMs;
|
|
1389
|
+
}
|
|
1390
|
+
// Global input mode switch: keeps API shape unchanged, only toggles execution strategy.
|
|
1391
|
+
if (['click', 'type', 'key', 'scroll'].includes(String(operationId))) {
|
|
1392
|
+
if (this.inputMode === 'protocol') {
|
|
1393
|
+
mergedConfig.useSystemMouse = false;
|
|
1394
|
+
}
|
|
1395
|
+
else if (typeof mergedConfig.useSystemMouse !== 'boolean') {
|
|
1396
|
+
mergedConfig.useSystemMouse = true;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
const response = await fetch(`http://${host}:${port}/v1/container/${containerId}/execute`, {
|
|
1400
|
+
method: 'POST',
|
|
1401
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1402
|
+
body: JSON.stringify({
|
|
1403
|
+
operationId,
|
|
1404
|
+
config: mergedConfig,
|
|
1405
|
+
sessionId: profile
|
|
1406
|
+
}),
|
|
1407
|
+
signal: abortController2.signal,
|
|
1408
|
+
});
|
|
1409
|
+
clearTimeout(timeoutId2);
|
|
1410
|
+
if (!response.ok) {
|
|
1411
|
+
return { success: false, error: await response.text() };
|
|
1412
|
+
}
|
|
1413
|
+
const result = await response.json();
|
|
1414
|
+
clearTimeout(timeoutId2);
|
|
1415
|
+
return result;
|
|
1416
|
+
}
|
|
1417
|
+
catch (error) {
|
|
1418
|
+
clearTimeout(timeoutId2);
|
|
1419
|
+
logDebug('controller', 'container:operation:error', { containerId, operationId, profile, timeoutMs, error: error?.message || String(error) });
|
|
1420
|
+
return { success: false, error: error.message };
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
emitContainerAppearEvents(containerTree, context) {
|
|
1424
|
+
if (!this.messageBus?.publish)
|
|
1425
|
+
return;
|
|
1426
|
+
if (!containerTree || !containerTree.id) {
|
|
1427
|
+
logDebug('controller', 'emitContainerAppearEvents', { message: 'No valid container tree provided' });
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const visited = new Set();
|
|
1431
|
+
// Emit for root container (the containerTree itself)
|
|
1432
|
+
this.emitSingleContainerAppear(containerTree, context, visited);
|
|
1433
|
+
// Emit for all child containers in tree
|
|
1434
|
+
if (containerTree.children && Array.isArray(containerTree.children)) {
|
|
1435
|
+
for (const child of containerTree.children) {
|
|
1436
|
+
this.emitTreeContainerAppear(child, context, visited);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
emitSingleContainerAppear(container, context, visited) {
|
|
1441
|
+
if (!container || !container.id)
|
|
1442
|
+
return;
|
|
1443
|
+
const containerId = String(container.id);
|
|
1444
|
+
if (visited.has(containerId))
|
|
1445
|
+
return;
|
|
1446
|
+
visited.add(containerId);
|
|
1447
|
+
const payload = {
|
|
1448
|
+
containerId,
|
|
1449
|
+
containerName: container.name || null,
|
|
1450
|
+
sessionId: context.sessionId,
|
|
1451
|
+
profileId: context.profileId,
|
|
1452
|
+
url: context.url,
|
|
1453
|
+
bbox: container.match?.bbox || container.bbox || null,
|
|
1454
|
+
visible: container.match?.visible ?? container.visible ?? null,
|
|
1455
|
+
score: container.match?.score ?? container.score ?? null,
|
|
1456
|
+
timestamp: Date.now(),
|
|
1457
|
+
source: 'containers:match',
|
|
1458
|
+
};
|
|
1459
|
+
this.messageBus?.publish?.('container:appear', payload);
|
|
1460
|
+
this.messageBus?.publish?.(`container:${containerId}:appear`, payload);
|
|
1461
|
+
logDebug('controller', 'container:appear', { containerId, containerName: container.name });
|
|
1462
|
+
}
|
|
1463
|
+
emitTreeContainerAppear(node, context, visited) {
|
|
1464
|
+
if (!node || !node.id)
|
|
1465
|
+
return;
|
|
1466
|
+
// Emit for this node (node itself is a container object)
|
|
1467
|
+
this.emitSingleContainerAppear(node, context, visited);
|
|
1468
|
+
// Recursively emit for children
|
|
1469
|
+
if (node.children && Array.isArray(node.children)) {
|
|
1470
|
+
for (const child of node.children) {
|
|
1471
|
+
this.emitTreeContainerAppear(child, context, visited);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
//# sourceMappingURL=controller.js.map
|