@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,1202 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { WebSocketServer } from 'ws';
|
|
5
|
+
import { SESSION_CLOSED_EVENT } from './SessionManager.js';
|
|
6
|
+
import { ContainerMatcher } from './container-matcher.js';
|
|
7
|
+
import { ensurePageRuntime } from './pageRuntime.js';
|
|
8
|
+
import { logDebug } from '../../../../modules/logging/src/index.js';
|
|
9
|
+
const logsDir = path.join(os.homedir(), '.webauto', 'logs');
|
|
10
|
+
const domPickerLogPath = path.join(logsDir, 'dom-picker-debug.log');
|
|
11
|
+
const highlightLogPath = path.join(logsDir, 'highlight-debug.log');
|
|
12
|
+
function appendLog(target, event, payload = {}) {
|
|
13
|
+
try {
|
|
14
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
15
|
+
const line = JSON.stringify({
|
|
16
|
+
ts: new Date().toISOString(),
|
|
17
|
+
event,
|
|
18
|
+
...payload,
|
|
19
|
+
});
|
|
20
|
+
fs.appendFileSync(target, `${line}\n`, 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* ignore log errors */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const appendDomPickerLog = (event, payload = {}) => appendLog(domPickerLogPath, event, payload);
|
|
27
|
+
const appendHighlightLog = (event, payload = {}) => appendLog(highlightLogPath, event, payload);
|
|
28
|
+
export class BrowserWsServer {
|
|
29
|
+
options;
|
|
30
|
+
wss;
|
|
31
|
+
matcher = new ContainerMatcher();
|
|
32
|
+
capabilityMap = new Map();
|
|
33
|
+
subscriptions = new Map();
|
|
34
|
+
sessionSubscribers = new Map();
|
|
35
|
+
runtimeBridgeUnsub = new Map();
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.options = options;
|
|
38
|
+
process.on(SESSION_CLOSED_EVENT, (sessionId) => {
|
|
39
|
+
this.teardownRuntimeEventBridge(sessionId);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async start() {
|
|
43
|
+
if (this.wss)
|
|
44
|
+
return;
|
|
45
|
+
const host = this.options.host || '127.0.0.1';
|
|
46
|
+
const port = Number(this.options.port || 8765);
|
|
47
|
+
this.wss = new WebSocketServer({ host, port });
|
|
48
|
+
this.wss.on('connection', (socket) => {
|
|
49
|
+
this.subscriptions.set(socket, new Set());
|
|
50
|
+
socket.on('message', (data) => this.handleMessage(socket, data));
|
|
51
|
+
socket.on('close', () => {
|
|
52
|
+
this.handleSocketClose(socket);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
this.wss.on('listening', () => {
|
|
56
|
+
console.log(`[browser-ws] listening on ws://${host}:${port}`);
|
|
57
|
+
});
|
|
58
|
+
this.wss.on('error', (err) => {
|
|
59
|
+
console.error('[browser-ws] server error:', err);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async handleDomPick(session, parameters) {
|
|
63
|
+
const timeoutMs = Math.min(Math.max(Number(parameters?.timeout) || 25000, 3000), 60000);
|
|
64
|
+
const page = await session.ensurePage();
|
|
65
|
+
const sessionId = session?.id || 'unknown';
|
|
66
|
+
appendDomPickerLog('start', { sessionId, timeoutMs });
|
|
67
|
+
await ensurePageRuntime(page);
|
|
68
|
+
try {
|
|
69
|
+
await page.bringToFront();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* ignore */
|
|
73
|
+
}
|
|
74
|
+
const hasRuntime = await page.evaluate(() => {
|
|
75
|
+
const w = window;
|
|
76
|
+
return Boolean(w.__domPicker && typeof w.__domPicker.startSession === 'function' && typeof w.__domPicker.getLastState === 'function');
|
|
77
|
+
});
|
|
78
|
+
if (!hasRuntime) {
|
|
79
|
+
appendDomPickerLog('runtime-missing', { sessionId });
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: 'domPicker runtime unavailable',
|
|
83
|
+
cancelled: false,
|
|
84
|
+
timeout: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const rootSelector = parameters.root_selector || parameters.rootSelector || null;
|
|
88
|
+
await page.evaluate((opts) => {
|
|
89
|
+
const w = window;
|
|
90
|
+
try {
|
|
91
|
+
w.__domPicker.startSession({ mode: 'hover-select', timeoutMs: opts.timeoutMs, rootSelector: opts.rootSelector });
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// swallow, polling below will observe error/idle state
|
|
95
|
+
// eslint-disable-next-line no-console
|
|
96
|
+
console.warn('[dom-picker] startSession error', err);
|
|
97
|
+
}
|
|
98
|
+
}, { timeoutMs, rootSelector });
|
|
99
|
+
const startedState = await page.evaluate(() => {
|
|
100
|
+
const w = window;
|
|
101
|
+
return w.__domPicker ? w.__domPicker.getLastState() : null;
|
|
102
|
+
});
|
|
103
|
+
appendDomPickerLog('started', { sessionId, state: startedState });
|
|
104
|
+
const startedAt = Date.now();
|
|
105
|
+
const hardTimeout = timeoutMs + 2000;
|
|
106
|
+
// Poll state until selected / cancelled / timeout
|
|
107
|
+
// We keep polling even after logical timeoutMs so that page-side timeout can mark phase = 'timeout'.
|
|
108
|
+
while (true) {
|
|
109
|
+
const elapsed = Date.now() - startedAt;
|
|
110
|
+
if (elapsed > hardTimeout) {
|
|
111
|
+
appendDomPickerLog('hard-timeout', { sessionId, elapsed });
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: 'domPicker hard timeout',
|
|
115
|
+
cancelled: false,
|
|
116
|
+
timeout: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const state = await page.evaluate(() => {
|
|
120
|
+
const w = window;
|
|
121
|
+
return w.__domPicker ? w.__domPicker.getLastState() : null;
|
|
122
|
+
});
|
|
123
|
+
if (!state) {
|
|
124
|
+
appendDomPickerLog('state-missing', { sessionId });
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: 'domPicker state unavailable',
|
|
128
|
+
cancelled: false,
|
|
129
|
+
timeout: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const phase = state.phase;
|
|
133
|
+
if (phase === 'selected' && state.selection) {
|
|
134
|
+
const sel = state.selection;
|
|
135
|
+
appendDomPickerLog('selected', { sessionId, selection: sel });
|
|
136
|
+
this.broadcastEvent('dom.picker.result', sessionId, {
|
|
137
|
+
success: true,
|
|
138
|
+
dom_path: sel.path || '',
|
|
139
|
+
selector: sel.selector || '',
|
|
140
|
+
bounding_rect: sel.rect || { x: 0, y: 0, width: 0, height: 0 },
|
|
141
|
+
tag: sel.tag || '',
|
|
142
|
+
id: sel.id || null,
|
|
143
|
+
classes: Array.isArray(sel.classes) ? sel.classes : [],
|
|
144
|
+
text: sel.text || '',
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
dom_path: sel.path || '',
|
|
149
|
+
selector: sel.selector || '',
|
|
150
|
+
bounding_rect: sel.rect || { x: 0, y: 0, width: 0, height: 0 },
|
|
151
|
+
tag: sel.tag || '',
|
|
152
|
+
id: sel.id || null,
|
|
153
|
+
classes: Array.isArray(sel.classes) ? sel.classes : [],
|
|
154
|
+
text: sel.text || '',
|
|
155
|
+
cancelled: false,
|
|
156
|
+
timeout: false,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (phase === 'cancelled') {
|
|
160
|
+
appendDomPickerLog('cancelled', { sessionId });
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
error: state.error || 'cancelled',
|
|
164
|
+
dom_path: null,
|
|
165
|
+
selector: null,
|
|
166
|
+
bounding_rect: null,
|
|
167
|
+
tag: null,
|
|
168
|
+
id: null,
|
|
169
|
+
classes: [],
|
|
170
|
+
text: '',
|
|
171
|
+
cancelled: true,
|
|
172
|
+
timeout: false,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (phase === 'timeout') {
|
|
176
|
+
appendDomPickerLog('timeout', { sessionId });
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
error: state.error || 'timeout',
|
|
180
|
+
dom_path: null,
|
|
181
|
+
selector: null,
|
|
182
|
+
bounding_rect: null,
|
|
183
|
+
tag: null,
|
|
184
|
+
id: null,
|
|
185
|
+
classes: [],
|
|
186
|
+
text: '',
|
|
187
|
+
cancelled: false,
|
|
188
|
+
timeout: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async stop() {
|
|
195
|
+
if (!this.wss)
|
|
196
|
+
return;
|
|
197
|
+
await new Promise((resolve) => this.wss?.close(() => resolve()));
|
|
198
|
+
this.wss = undefined;
|
|
199
|
+
}
|
|
200
|
+
async handleDomPickerLoopback(session, parameters) {
|
|
201
|
+
const page = await session.ensurePage();
|
|
202
|
+
const selector = parameters.selector || 'body';
|
|
203
|
+
const timeoutMs = Math.min(Math.max(Number(parameters?.timeout) || 10000, 1000), 60000);
|
|
204
|
+
const settleMs = Math.min(Math.max(Number(parameters?.settle_ms) || 32, 0), 2000);
|
|
205
|
+
const sessionId = session?.id || 'unknown';
|
|
206
|
+
appendDomPickerLog('loopback_start', { sessionId, selector, timeoutMs, settleMs });
|
|
207
|
+
await ensurePageRuntime(page);
|
|
208
|
+
// Real loopback: compute element center, move the real browser mouse, then read picker state.
|
|
209
|
+
const prep = await page.evaluate((sel) => {
|
|
210
|
+
const runtime = window.__webautoRuntime;
|
|
211
|
+
const picker = window.__domPicker;
|
|
212
|
+
if (!runtime || !runtime.ready) {
|
|
213
|
+
return { ok: false, error: '__webautoRuntime not ready' };
|
|
214
|
+
}
|
|
215
|
+
if (!picker || typeof picker.startSession !== 'function' || typeof picker.getLastState !== 'function') {
|
|
216
|
+
return { ok: false, error: '__domPicker unavailable' };
|
|
217
|
+
}
|
|
218
|
+
const info = picker.findElementCenter ? picker.findElementCenter(sel) : null;
|
|
219
|
+
const el = typeof sel === 'string' ? document.querySelector(sel) : null;
|
|
220
|
+
if (!info || !info.found || !el) {
|
|
221
|
+
return { ok: false, error: 'selector_not_found' };
|
|
222
|
+
}
|
|
223
|
+
const point = { x: Math.round(info.x), y: Math.round(info.y) };
|
|
224
|
+
const rect = info.rect;
|
|
225
|
+
const buildPath = runtime?.dom?.buildPathForElement;
|
|
226
|
+
const targetPath = buildPath && el instanceof Element ? buildPath(el, null) : null;
|
|
227
|
+
const fromPoint = document.elementFromPoint(point.x, point.y);
|
|
228
|
+
const fromPointPath = buildPath && fromPoint instanceof Element ? buildPath(fromPoint, null) : null;
|
|
229
|
+
const before = picker.getLastState();
|
|
230
|
+
if (!before?.phase || before.phase === 'idle') {
|
|
231
|
+
picker.startSession({ timeoutMs: 8000 });
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
ok: true,
|
|
235
|
+
selector: sel,
|
|
236
|
+
point,
|
|
237
|
+
targetRect: rect,
|
|
238
|
+
targetPath,
|
|
239
|
+
fromPointPath,
|
|
240
|
+
stateBefore: before,
|
|
241
|
+
};
|
|
242
|
+
}, selector);
|
|
243
|
+
if (!prep?.ok) {
|
|
244
|
+
appendDomPickerLog('loopback_runtime_missing', { sessionId, selector, error: prep?.error });
|
|
245
|
+
return { success: false, error: prep?.error || 'loopback_prep_failed' };
|
|
246
|
+
}
|
|
247
|
+
await page.mouse.move(prep.point.x, prep.point.y);
|
|
248
|
+
if (settleMs > 0) {
|
|
249
|
+
await new Promise((r) => setTimeout(r, settleMs));
|
|
250
|
+
}
|
|
251
|
+
const after = await page.evaluate(() => {
|
|
252
|
+
const picker = window.__domPicker;
|
|
253
|
+
return picker?.getLastState?.() || null;
|
|
254
|
+
});
|
|
255
|
+
await page.evaluate((sel) => {
|
|
256
|
+
const runtime = window.__webautoRuntime;
|
|
257
|
+
runtime?.highlight?.highlightSelector?.(sel, { persistent: true, channel: 'dom-picker-loopback' });
|
|
258
|
+
}, prep.selector);
|
|
259
|
+
const result = {
|
|
260
|
+
selector: prep.selector,
|
|
261
|
+
point: prep.point,
|
|
262
|
+
targetRect: prep.targetRect,
|
|
263
|
+
hoveredPath: after?.selection?.path || after?.hovered?.path || after?.selected?.path || after?.path || null,
|
|
264
|
+
targetPath: prep.targetPath,
|
|
265
|
+
fromPointPath: prep.fromPointPath,
|
|
266
|
+
overlayRect: after?.selection?.rect || after?.hovered?.rect || after?.selected?.rect || after?.rect || null,
|
|
267
|
+
stateBefore: prep.stateBefore,
|
|
268
|
+
stateAfter: after,
|
|
269
|
+
matches: Boolean(prep.targetPath) &&
|
|
270
|
+
(after?.selection?.path || after?.hovered?.path || after?.selected?.path || after?.path) === prep.targetPath &&
|
|
271
|
+
Boolean(after?.selection?.rect || after?.hovered?.rect || after?.selected?.rect || after?.rect),
|
|
272
|
+
};
|
|
273
|
+
appendDomPickerLog('loopback_result', { sessionId, selector, result });
|
|
274
|
+
return {
|
|
275
|
+
success: true,
|
|
276
|
+
data: result,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async handleMessage(socket, raw) {
|
|
280
|
+
let payload;
|
|
281
|
+
try {
|
|
282
|
+
payload = JSON.parse(this.rawToString(raw));
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
this.send(socket, {
|
|
286
|
+
type: 'error',
|
|
287
|
+
message: `Invalid JSON payload: ${err.message}`,
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
logDebug('browser-service', 'wsMessage', { payload });
|
|
292
|
+
if (payload?.type === 'subscribe') {
|
|
293
|
+
await this.handleSubscribe(socket, payload);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (payload?.type === 'unsubscribe') {
|
|
297
|
+
await this.handleUnsubscribe(socket, payload);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (payload?.type !== 'command') {
|
|
301
|
+
this.send(socket, {
|
|
302
|
+
type: 'error',
|
|
303
|
+
message: 'Unsupported message type',
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const sessionId = String(payload.session_id || '');
|
|
308
|
+
const requestId = String(payload.request_id || '');
|
|
309
|
+
const command = payload.data || {};
|
|
310
|
+
logDebug('browser-service', 'ws-command', { type: 'command', request_id: requestId, session_id: sessionId, data: command });
|
|
311
|
+
try {
|
|
312
|
+
const data = await this.dispatchCommand(sessionId, command);
|
|
313
|
+
const response = {
|
|
314
|
+
type: 'response',
|
|
315
|
+
request_id: requestId,
|
|
316
|
+
session_id: sessionId,
|
|
317
|
+
data,
|
|
318
|
+
};
|
|
319
|
+
logDebug('browser-service', 'wsResponse', { response });
|
|
320
|
+
this.send(socket, response);
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
const errorResponse = {
|
|
324
|
+
type: 'response',
|
|
325
|
+
request_id: requestId,
|
|
326
|
+
session_id: sessionId,
|
|
327
|
+
data: {
|
|
328
|
+
success: false,
|
|
329
|
+
error: err.message,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
logDebug('browser-service', 'wsError', { errorResponse });
|
|
333
|
+
this.send(socket, errorResponse);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async dispatchCommand(sessionId, command) {
|
|
337
|
+
const type = command.command_type;
|
|
338
|
+
switch (type) {
|
|
339
|
+
case 'browser_state':
|
|
340
|
+
return this.handleSessionControl(sessionId, {
|
|
341
|
+
...command,
|
|
342
|
+
action: command.action || 'list',
|
|
343
|
+
});
|
|
344
|
+
case 'page_control':
|
|
345
|
+
return this.handlePageControl(sessionId, command);
|
|
346
|
+
case 'dom_operation':
|
|
347
|
+
return this.handleDomOperation(sessionId, command);
|
|
348
|
+
case 'user_action':
|
|
349
|
+
return this.handleUserAction(sessionId, command);
|
|
350
|
+
case 'highlight':
|
|
351
|
+
return this.handleHighlight(sessionId, command);
|
|
352
|
+
case 'session_control':
|
|
353
|
+
return this.handleSessionControl(sessionId, command);
|
|
354
|
+
case 'mode_switch':
|
|
355
|
+
return this.handleModeSwitch(sessionId, command);
|
|
356
|
+
case 'container_operation':
|
|
357
|
+
return this.handleContainerOperation(sessionId, command);
|
|
358
|
+
case 'node_execute':
|
|
359
|
+
return this.handleNodeExecute(sessionId, command);
|
|
360
|
+
case 'dev_control':
|
|
361
|
+
return this.handleDevControl(sessionId, command);
|
|
362
|
+
case 'dev_command':
|
|
363
|
+
return this.handleDevCommand(sessionId, command);
|
|
364
|
+
default:
|
|
365
|
+
throw new Error(`Unknown command_type: ${type}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async handleSubscribe(socket, payload) {
|
|
369
|
+
const requestId = String(payload.request_id || '');
|
|
370
|
+
const sessionId = String(payload.session_id || '');
|
|
371
|
+
const topics = Array.isArray(payload.data?.topics) ? payload.data.topics : [];
|
|
372
|
+
if (!sessionId) {
|
|
373
|
+
this.send(socket, {
|
|
374
|
+
type: 'error',
|
|
375
|
+
request_id: requestId,
|
|
376
|
+
message: 'session_id required for subscribe',
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const clientTopics = this.subscriptions.get(socket) || new Set();
|
|
381
|
+
const sessionClients = this.sessionSubscribers.get(sessionId) || new Set();
|
|
382
|
+
const requiresRuntimeBridge = topics.some((topic) => topic === 'browser.runtime.event' || topic.startsWith('browser.runtime.event.'));
|
|
383
|
+
topics.forEach((topic) => {
|
|
384
|
+
clientTopics.add(topic);
|
|
385
|
+
sessionClients.add(socket);
|
|
386
|
+
});
|
|
387
|
+
this.subscriptions.set(socket, clientTopics);
|
|
388
|
+
this.sessionSubscribers.set(sessionId, sessionClients);
|
|
389
|
+
if (requiresRuntimeBridge) {
|
|
390
|
+
this.ensureRuntimeEventBridge(sessionId);
|
|
391
|
+
}
|
|
392
|
+
this.send(socket, {
|
|
393
|
+
type: 'response',
|
|
394
|
+
request_id: requestId,
|
|
395
|
+
session_id: sessionId,
|
|
396
|
+
data: { success: true, subscribed: topics },
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
async handleUnsubscribe(socket, payload) {
|
|
400
|
+
const requestId = String(payload.request_id || '');
|
|
401
|
+
const sessionId = String(payload.session_id || '');
|
|
402
|
+
const topics = Array.isArray(payload.data?.topics) ? payload.data.topics : [];
|
|
403
|
+
const clientTopics = this.subscriptions.get(socket);
|
|
404
|
+
if (!clientTopics) {
|
|
405
|
+
this.send(socket, {
|
|
406
|
+
type: 'response',
|
|
407
|
+
request_id: requestId,
|
|
408
|
+
session_id: sessionId,
|
|
409
|
+
data: { success: true, unsubscribed: [] },
|
|
410
|
+
});
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
topics.forEach((topic) => clientTopics.delete(topic));
|
|
414
|
+
if (clientTopics.size === 0) {
|
|
415
|
+
this.subscriptions.delete(socket);
|
|
416
|
+
}
|
|
417
|
+
this.send(socket, {
|
|
418
|
+
type: 'response',
|
|
419
|
+
request_id: requestId,
|
|
420
|
+
session_id: sessionId,
|
|
421
|
+
data: { success: true, unsubscribed: topics },
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
handleSocketClose(socket) {
|
|
425
|
+
this.subscriptions.delete(socket);
|
|
426
|
+
for (const [sessionId, clients] of this.sessionSubscribers.entries()) {
|
|
427
|
+
clients.delete(socket);
|
|
428
|
+
if (clients.size === 0) {
|
|
429
|
+
this.sessionSubscribers.delete(sessionId);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
broadcastEvent(topic, sessionId, data) {
|
|
434
|
+
const clients = this.sessionSubscribers.get(sessionId);
|
|
435
|
+
if (!clients)
|
|
436
|
+
return;
|
|
437
|
+
const payload = {
|
|
438
|
+
type: 'event',
|
|
439
|
+
topic,
|
|
440
|
+
session_id: sessionId,
|
|
441
|
+
data,
|
|
442
|
+
};
|
|
443
|
+
logDebug('browser-service', 'runtimeEvent:broadcast', { topic, sessionId, listeners: clients.size });
|
|
444
|
+
clients.forEach((socket) => {
|
|
445
|
+
const clientTopics = this.subscriptions.get(socket);
|
|
446
|
+
if (clientTopics?.has(topic)) {
|
|
447
|
+
try {
|
|
448
|
+
socket.send(JSON.stringify(payload));
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
console.warn('[browser-ws] failed to broadcast event:', err);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
ensureRuntimeEventBridge(sessionId) {
|
|
457
|
+
if (this.runtimeBridgeUnsub.has(sessionId))
|
|
458
|
+
return;
|
|
459
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
460
|
+
if (!session)
|
|
461
|
+
return;
|
|
462
|
+
const unsub = session.addRuntimeEventObserver((event) => {
|
|
463
|
+
const data = { event, received_at: Date.now() };
|
|
464
|
+
this.broadcastEvent('browser.runtime.event', sessionId, data);
|
|
465
|
+
if (event?.type) {
|
|
466
|
+
this.broadcastEvent(this.formatRuntimeTopic(event.type), sessionId, data);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
this.runtimeBridgeUnsub.set(sessionId, unsub);
|
|
470
|
+
}
|
|
471
|
+
teardownRuntimeEventBridge(sessionId) {
|
|
472
|
+
const unsub = this.runtimeBridgeUnsub.get(sessionId);
|
|
473
|
+
if (unsub) {
|
|
474
|
+
try {
|
|
475
|
+
unsub();
|
|
476
|
+
}
|
|
477
|
+
catch { }
|
|
478
|
+
this.runtimeBridgeUnsub.delete(sessionId);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
formatRuntimeTopic(type) {
|
|
482
|
+
const safe = (type || 'unknown').replace(/[^a-zA-Z0-9_.-]/g, '_').toLowerCase();
|
|
483
|
+
return `browser.runtime.event.${safe}`;
|
|
484
|
+
}
|
|
485
|
+
async handlePageControl(sessionId, command) {
|
|
486
|
+
const action = command.action;
|
|
487
|
+
const parameters = command.parameters || {};
|
|
488
|
+
if (!sessionId)
|
|
489
|
+
throw new Error('session_id required');
|
|
490
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
491
|
+
if (!session) {
|
|
492
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
493
|
+
}
|
|
494
|
+
if (action === 'navigate') {
|
|
495
|
+
const url = parameters.url;
|
|
496
|
+
if (!url)
|
|
497
|
+
throw new Error('navigate requires url');
|
|
498
|
+
await session.goto(url);
|
|
499
|
+
return { success: true, data: { action: 'navigated', url } };
|
|
500
|
+
}
|
|
501
|
+
if (action === 'screenshot') {
|
|
502
|
+
const filename = parameters.filename || `screenshot_${Date.now()}.png`;
|
|
503
|
+
const fullPage = parameters.full_page !== false;
|
|
504
|
+
const dir = path.resolve(process.cwd(), 'screenshots');
|
|
505
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
506
|
+
const target = path.join(dir, filename);
|
|
507
|
+
const buffer = await session.screenshot(fullPage);
|
|
508
|
+
await fs.promises.writeFile(target, buffer);
|
|
509
|
+
return { success: true, data: { action: 'screenshot', screenshot_path: target, full_page: fullPage } };
|
|
510
|
+
}
|
|
511
|
+
throw new Error(`Unknown page_control action: ${action}`);
|
|
512
|
+
}
|
|
513
|
+
async handleDomOperation(sessionId, command) {
|
|
514
|
+
const action = command.action;
|
|
515
|
+
const parameters = command.parameters || {};
|
|
516
|
+
if (action === 'pick_dom') {
|
|
517
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
518
|
+
if (!session) {
|
|
519
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
520
|
+
}
|
|
521
|
+
return this.handleDomPick(session, parameters);
|
|
522
|
+
}
|
|
523
|
+
if (action === 'query') {
|
|
524
|
+
return this.handleNodeExecute(sessionId, { command_type: 'node_execute', node_type: 'query', parameters });
|
|
525
|
+
}
|
|
526
|
+
if (action === 'dom_full') {
|
|
527
|
+
return this.handleDomFull(sessionId, parameters);
|
|
528
|
+
}
|
|
529
|
+
if (action === 'dom_branch') {
|
|
530
|
+
return this.handleDomBranch(sessionId, parameters);
|
|
531
|
+
}
|
|
532
|
+
throw new Error(`Unknown dom_operation action: ${action}`);
|
|
533
|
+
}
|
|
534
|
+
async handleDomFull(sessionId, parameters) {
|
|
535
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
536
|
+
if (!session) {
|
|
537
|
+
return { success: false, error: 'Session ' + sessionId + ' not found' };
|
|
538
|
+
}
|
|
539
|
+
const page = await session.ensurePage();
|
|
540
|
+
const rootSelector = parameters.root_selector || parameters.rootSelector || null;
|
|
541
|
+
const maxDepth = Number(parameters.max_depth || parameters.maxDepth || 8);
|
|
542
|
+
await ensurePageRuntime(page);
|
|
543
|
+
const domTree = await page.evaluate((config) => {
|
|
544
|
+
const runtime = window.__webautoRuntime;
|
|
545
|
+
if (!runtime?.getDomBranch) {
|
|
546
|
+
throw new Error('runtime.getDomBranch unavailable');
|
|
547
|
+
}
|
|
548
|
+
return runtime.getDomBranch('root', {
|
|
549
|
+
rootSelector: config.rootSelector,
|
|
550
|
+
maxDepth: config.maxDepth,
|
|
551
|
+
maxChildren: 100,
|
|
552
|
+
});
|
|
553
|
+
}, { rootSelector, maxDepth });
|
|
554
|
+
const node = domTree.node || {};
|
|
555
|
+
const nodeCount = node.childCount || 0;
|
|
556
|
+
this.broadcastEvent('dom.updated', sessionId, {
|
|
557
|
+
root_path: domTree.path || 'root',
|
|
558
|
+
node_count: nodeCount,
|
|
559
|
+
});
|
|
560
|
+
return {
|
|
561
|
+
success: true,
|
|
562
|
+
data: {
|
|
563
|
+
root_path: domTree.path || 'root',
|
|
564
|
+
node_count: nodeCount,
|
|
565
|
+
snapshot: node,
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
async handleDomBranch(sessionId, parameters) {
|
|
570
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
571
|
+
if (!session) {
|
|
572
|
+
return { success: false, error: 'Session ' + sessionId + ' not found' };
|
|
573
|
+
}
|
|
574
|
+
const page = await session.ensurePage();
|
|
575
|
+
const domPath = String(parameters.dom_path || parameters.domPath || '');
|
|
576
|
+
const depth = Number(parameters.depth || 3);
|
|
577
|
+
const rootSelector = parameters.root_selector || parameters.rootSelector || null;
|
|
578
|
+
await ensurePageRuntime(page);
|
|
579
|
+
const snapshot = await page.evaluate((config) => {
|
|
580
|
+
const runtime = window.__webautoRuntime;
|
|
581
|
+
if (!runtime?.getDomBranch) {
|
|
582
|
+
throw new Error('runtime.getDomBranch unavailable');
|
|
583
|
+
}
|
|
584
|
+
return runtime.getDomBranch(config.domPath, {
|
|
585
|
+
rootSelector: config.rootSelector,
|
|
586
|
+
maxDepth: config.depth,
|
|
587
|
+
maxChildren: 100,
|
|
588
|
+
});
|
|
589
|
+
}, { domPath, depth, rootSelector });
|
|
590
|
+
const node = snapshot.node || {};
|
|
591
|
+
return {
|
|
592
|
+
success: true,
|
|
593
|
+
data: {
|
|
594
|
+
path: snapshot.path || domPath,
|
|
595
|
+
node_count: node.childCount || 0,
|
|
596
|
+
children: node.children || [],
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
async handleUserAction(sessionId, command) {
|
|
601
|
+
const action = command.action;
|
|
602
|
+
const parameters = command.parameters || {};
|
|
603
|
+
if (action !== 'operation') {
|
|
604
|
+
throw new Error(`Unknown user_action action: ${action}`);
|
|
605
|
+
}
|
|
606
|
+
const op = parameters.operation_type;
|
|
607
|
+
if (!op)
|
|
608
|
+
throw new Error('operation_type required');
|
|
609
|
+
if (op === 'click') {
|
|
610
|
+
return this.handleNodeExecute(sessionId, { command_type: 'node_execute', node_type: 'click', parameters: parameters.target || parameters });
|
|
611
|
+
}
|
|
612
|
+
if (op === 'type') {
|
|
613
|
+
return this.handleNodeExecute(sessionId, { command_type: 'node_execute', node_type: 'type', parameters: parameters.target || parameters });
|
|
614
|
+
}
|
|
615
|
+
if (op === 'scroll') {
|
|
616
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
617
|
+
if (!session) {
|
|
618
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
619
|
+
}
|
|
620
|
+
const page = await session.ensurePage();
|
|
621
|
+
const target = parameters.target || {};
|
|
622
|
+
const coordinates = target.coordinates || null;
|
|
623
|
+
const deltaY = Number(parameters.deltaY ?? target.deltaY ?? parameters.delta_y ?? target.delta_y ?? 0);
|
|
624
|
+
if (coordinates && typeof coordinates.x === 'number' && typeof coordinates.y === 'number') {
|
|
625
|
+
await page.mouse.move(coordinates.x, coordinates.y);
|
|
626
|
+
}
|
|
627
|
+
await page.mouse.wheel(0, deltaY);
|
|
628
|
+
this.broadcastEvent('user_action.completed', sessionId, {
|
|
629
|
+
action: 'scroll',
|
|
630
|
+
target: coordinates
|
|
631
|
+
? `coordinates(${coordinates.x}, ${coordinates.y})`
|
|
632
|
+
: '',
|
|
633
|
+
duration_ms: 0,
|
|
634
|
+
deltaY,
|
|
635
|
+
});
|
|
636
|
+
return {
|
|
637
|
+
success: true,
|
|
638
|
+
data: {
|
|
639
|
+
action: 'scroll',
|
|
640
|
+
deltaY,
|
|
641
|
+
...(coordinates ? { x: coordinates.x, y: coordinates.y } : {}),
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
if (op === 'move' || op === 'down' || op === 'up' || op === 'key') {
|
|
646
|
+
return this.handleExtendedUserAction(sessionId, op, parameters.target || {}, parameters);
|
|
647
|
+
}
|
|
648
|
+
throw new Error(`Unsupported operation_type: ${op}`);
|
|
649
|
+
}
|
|
650
|
+
async handleExtendedUserAction(sessionId, opType, target, params) {
|
|
651
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
652
|
+
if (!session) {
|
|
653
|
+
return { success: false, error: 'Session ' + sessionId + ' not found' };
|
|
654
|
+
}
|
|
655
|
+
const page = await session.ensurePage();
|
|
656
|
+
const startedAt = Date.now();
|
|
657
|
+
const offset = target.offset || { x: 0, y: 0 };
|
|
658
|
+
const coordinates = target.coordinates || null;
|
|
659
|
+
const domPath = target.dom_path || null;
|
|
660
|
+
const selector = target.selector || null;
|
|
661
|
+
let coords = null;
|
|
662
|
+
// Support direct coordinates
|
|
663
|
+
if (coordinates && typeof coordinates.x === 'number' && typeof coordinates.y === 'number') {
|
|
664
|
+
coords = { x: coordinates.x + offset.x, y: coordinates.y + offset.y };
|
|
665
|
+
}
|
|
666
|
+
else if (domPath) {
|
|
667
|
+
await ensurePageRuntime(page);
|
|
668
|
+
const result = await page.evaluate((config) => {
|
|
669
|
+
const runtime = window.__webautoRuntime;
|
|
670
|
+
if (!runtime?.dom?.resolveByPath)
|
|
671
|
+
return null;
|
|
672
|
+
const el = runtime.dom.resolveByPath(config.path, config.rootSelector);
|
|
673
|
+
if (!el)
|
|
674
|
+
return null;
|
|
675
|
+
const rect = el.getBoundingClientRect();
|
|
676
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
677
|
+
}, { path: domPath, rootSelector: null });
|
|
678
|
+
if (result) {
|
|
679
|
+
coords = { x: result.x + offset.x, y: result.y + offset.y };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else if (selector) {
|
|
683
|
+
const element = await page.$(selector);
|
|
684
|
+
if (element) {
|
|
685
|
+
const box = await element.boundingBox();
|
|
686
|
+
if (box) {
|
|
687
|
+
coords = { x: box.x + box.width / 2 + offset.x, y: box.y + box.height / 2 + offset.y };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (opType === 'move') {
|
|
692
|
+
if (coords) {
|
|
693
|
+
await page.mouse.move(coords.x, coords.y);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else if (opType === 'down') {
|
|
697
|
+
if (coords) {
|
|
698
|
+
await page.mouse.move(coords.x, coords.y);
|
|
699
|
+
await page.mouse.down();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
else if (opType === 'up') {
|
|
703
|
+
await page.mouse.up();
|
|
704
|
+
}
|
|
705
|
+
else if (opType === 'key') {
|
|
706
|
+
const key = params.key || '';
|
|
707
|
+
if (key) {
|
|
708
|
+
await page.keyboard.press(key);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const duration = Date.now() - startedAt;
|
|
712
|
+
this.broadcastEvent('user_action.completed', sessionId, {
|
|
713
|
+
action: opType,
|
|
714
|
+
target: domPath || selector || (coordinates ? `coordinates(${coordinates.x}, ${coordinates.y})` : ''),
|
|
715
|
+
duration_ms: duration,
|
|
716
|
+
...(coords ? { x: coords.x, y: coords.y } : {}),
|
|
717
|
+
});
|
|
718
|
+
return {
|
|
719
|
+
success: true,
|
|
720
|
+
data: {
|
|
721
|
+
action: opType,
|
|
722
|
+
target: domPath || selector || (coordinates ? `coordinates(${coordinates.x}, ${coordinates.y})` : ''),
|
|
723
|
+
duration_ms: duration,
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
async handleHighlight(sessionId, command) {
|
|
728
|
+
const action = command.action;
|
|
729
|
+
const parameters = command.parameters || {};
|
|
730
|
+
if (action === 'element') {
|
|
731
|
+
return this.handleDevCommand(sessionId, { command_type: 'dev_command', action: 'highlight_element', parameters });
|
|
732
|
+
}
|
|
733
|
+
if (action === 'dom_path') {
|
|
734
|
+
return this.handleDevCommand(sessionId, { command_type: 'dev_command', action: 'highlight_dom_path', parameters });
|
|
735
|
+
}
|
|
736
|
+
throw new Error(`Unknown highlight action: ${action}`);
|
|
737
|
+
}
|
|
738
|
+
async handleSessionControl(sessionId, command) {
|
|
739
|
+
const action = command.action;
|
|
740
|
+
if (action === 'create') {
|
|
741
|
+
const capabilities = command.capabilities || ['dom'];
|
|
742
|
+
const browserConfig = command.browser_config || {};
|
|
743
|
+
const profileId = browserConfig.profile_id || browserConfig.session_name || `session_${Date.now().toString(36)}`;
|
|
744
|
+
const headless = browserConfig.headless ?? false;
|
|
745
|
+
const viewport = browserConfig.viewport;
|
|
746
|
+
const userAgent = browserConfig.user_agent;
|
|
747
|
+
const initialUrl = browserConfig.initial_url || command.initial_url;
|
|
748
|
+
const result = await this.options.sessionManager.createSession({
|
|
749
|
+
profileId,
|
|
750
|
+
sessionName: browserConfig.session_name || profileId,
|
|
751
|
+
headless,
|
|
752
|
+
viewport,
|
|
753
|
+
userAgent,
|
|
754
|
+
initialUrl,
|
|
755
|
+
});
|
|
756
|
+
this.capabilityMap.set(result.sessionId, capabilities);
|
|
757
|
+
return {
|
|
758
|
+
success: true,
|
|
759
|
+
session_id: result.sessionId,
|
|
760
|
+
status: 'ready',
|
|
761
|
+
capabilities,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
if (action === 'list') {
|
|
765
|
+
const sessions = this.options.sessionManager.listSessions().map((session) => ({
|
|
766
|
+
session_id: session.session_id || session.profileId,
|
|
767
|
+
profileId: session.profileId,
|
|
768
|
+
current_url: session.current_url,
|
|
769
|
+
mode: session.mode,
|
|
770
|
+
status: 'ready',
|
|
771
|
+
capabilities: this.capabilityMap.get(session.profileId) || [],
|
|
772
|
+
}));
|
|
773
|
+
return {
|
|
774
|
+
success: true,
|
|
775
|
+
sessions,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
if (action === 'info') {
|
|
779
|
+
if (!sessionId) {
|
|
780
|
+
throw new Error('session_id required for info action');
|
|
781
|
+
}
|
|
782
|
+
const info = await this.options.sessionManager.getSessionInfo(sessionId);
|
|
783
|
+
if (!info) {
|
|
784
|
+
return {
|
|
785
|
+
success: false,
|
|
786
|
+
error: `Session ${sessionId} not found`,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
success: true,
|
|
791
|
+
session_info: {
|
|
792
|
+
...info,
|
|
793
|
+
capabilities: this.capabilityMap.get(sessionId) || [],
|
|
794
|
+
},
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
if (action === 'delete') {
|
|
798
|
+
if (!sessionId) {
|
|
799
|
+
throw new Error('session_id required for delete action');
|
|
800
|
+
}
|
|
801
|
+
const deleted = await this.options.sessionManager.deleteSession(sessionId);
|
|
802
|
+
this.capabilityMap.delete(sessionId);
|
|
803
|
+
return {
|
|
804
|
+
success: deleted,
|
|
805
|
+
session_id: sessionId,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
throw new Error(`Unknown session action: ${action}`);
|
|
809
|
+
}
|
|
810
|
+
async handleModeSwitch(sessionId, command) {
|
|
811
|
+
if (!sessionId) {
|
|
812
|
+
throw new Error('session_id required for mode switch');
|
|
813
|
+
}
|
|
814
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
815
|
+
if (!session) {
|
|
816
|
+
return {
|
|
817
|
+
success: false,
|
|
818
|
+
error: `Session ${sessionId} not found`,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
const target = command.target_mode || 'dev';
|
|
822
|
+
session.setMode(target);
|
|
823
|
+
return {
|
|
824
|
+
success: true,
|
|
825
|
+
session_id: sessionId,
|
|
826
|
+
new_mode: target,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
async handleContainerOperation(sessionId, command) {
|
|
830
|
+
if (!sessionId) {
|
|
831
|
+
throw new Error('session_id required for container operations');
|
|
832
|
+
}
|
|
833
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
834
|
+
if (!session) {
|
|
835
|
+
return {
|
|
836
|
+
success: false,
|
|
837
|
+
error: `Session ${sessionId} not found`,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
logDebug('browser-service', 'containerOperation', { sessionId, command });
|
|
841
|
+
const pageContext = command.page_context || {};
|
|
842
|
+
if (command.action === 'match_root') {
|
|
843
|
+
const match = await this.matcher.matchRoot(session, pageContext);
|
|
844
|
+
if (!match) {
|
|
845
|
+
return {
|
|
846
|
+
success: false,
|
|
847
|
+
error: 'No matching container found',
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
success: true,
|
|
852
|
+
data: {
|
|
853
|
+
container: match.container,
|
|
854
|
+
selector: match.container?.matched_selector || match.match_details?.selector,
|
|
855
|
+
domPath: match.match_details?.dom_path || null,
|
|
856
|
+
match_details: match.match_details,
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
if (command.action === 'inspect_tree') {
|
|
861
|
+
const snapshot = await this.matcher.inspectTree(session, pageContext, command.parameters || {});
|
|
862
|
+
return {
|
|
863
|
+
success: true,
|
|
864
|
+
data: snapshot,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
if (command.action === 'inspect_dom_branch') {
|
|
868
|
+
const branch = await this.matcher.inspectDomBranch(session, pageContext, command.parameters || {});
|
|
869
|
+
return {
|
|
870
|
+
success: true,
|
|
871
|
+
data: branch,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
throw new Error(`Unsupported container action: ${command.action}`);
|
|
875
|
+
}
|
|
876
|
+
async handleNodeExecute(sessionId, command) {
|
|
877
|
+
if (!sessionId) {
|
|
878
|
+
throw new Error('session_id required for node_execute');
|
|
879
|
+
}
|
|
880
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
881
|
+
if (!session) {
|
|
882
|
+
return {
|
|
883
|
+
success: false,
|
|
884
|
+
error: `Session ${sessionId} not found`,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
const nodeType = command.node_type;
|
|
888
|
+
const parameters = command.parameters || {};
|
|
889
|
+
switch (nodeType) {
|
|
890
|
+
case 'navigate': {
|
|
891
|
+
const url = parameters.url;
|
|
892
|
+
if (!url) {
|
|
893
|
+
throw new Error('Navigate node requires url');
|
|
894
|
+
}
|
|
895
|
+
await session.goto(url);
|
|
896
|
+
return {
|
|
897
|
+
success: true,
|
|
898
|
+
data: {
|
|
899
|
+
action: 'navigated',
|
|
900
|
+
url,
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
case 'click': {
|
|
905
|
+
const selector = parameters.selector;
|
|
906
|
+
if (!selector)
|
|
907
|
+
throw new Error('Click node requires selector');
|
|
908
|
+
await session.click(selector);
|
|
909
|
+
return {
|
|
910
|
+
success: true,
|
|
911
|
+
data: { action: 'clicked', selector },
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
case 'type': {
|
|
915
|
+
const selector = parameters.selector;
|
|
916
|
+
if (!selector)
|
|
917
|
+
throw new Error('Type node requires selector');
|
|
918
|
+
const text = parameters.text ?? parameters.value ?? '';
|
|
919
|
+
await session.fill(selector, String(text));
|
|
920
|
+
return {
|
|
921
|
+
success: true,
|
|
922
|
+
data: { action: 'typed', selector, text },
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
case 'screenshot': {
|
|
926
|
+
const filename = parameters.filename || `screenshot_${Date.now()}.png`;
|
|
927
|
+
const fullPage = parameters.full_page !== false;
|
|
928
|
+
const dir = path.resolve(process.cwd(), 'screenshots');
|
|
929
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
930
|
+
const target = path.join(dir, filename);
|
|
931
|
+
const buffer = await session.screenshot(fullPage);
|
|
932
|
+
await fs.promises.writeFile(target, buffer);
|
|
933
|
+
return {
|
|
934
|
+
success: true,
|
|
935
|
+
data: {
|
|
936
|
+
action: 'screenshot',
|
|
937
|
+
screenshot_path: target,
|
|
938
|
+
full_page: fullPage,
|
|
939
|
+
},
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
case 'query': {
|
|
943
|
+
const selector = parameters.selector;
|
|
944
|
+
if (!selector)
|
|
945
|
+
throw new Error('Query node requires selector');
|
|
946
|
+
const limit = Number(parameters.max_items || parameters.maxItems || 5);
|
|
947
|
+
const page = await session.ensurePage();
|
|
948
|
+
const result = await page.$$eval(selector, (els, lim) => {
|
|
949
|
+
const sample = [];
|
|
950
|
+
const max = Math.max(0, Number(lim) || 0);
|
|
951
|
+
for (let i = 0; i < Math.min(max, els.length); i++) {
|
|
952
|
+
const el = els[i];
|
|
953
|
+
sample.push({
|
|
954
|
+
tag: el.tagName,
|
|
955
|
+
id: el.id || null,
|
|
956
|
+
classes: Array.from(el.classList || []),
|
|
957
|
+
text: (el.textContent || '').trim().slice(0, 120),
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
count: els.length,
|
|
962
|
+
sample,
|
|
963
|
+
};
|
|
964
|
+
}, limit);
|
|
965
|
+
return {
|
|
966
|
+
success: true,
|
|
967
|
+
data: {
|
|
968
|
+
selector,
|
|
969
|
+
count: result.count,
|
|
970
|
+
sample: result.sample,
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
case 'dom_info': {
|
|
975
|
+
const page = await session.ensurePage();
|
|
976
|
+
const info = await page.evaluate(() => {
|
|
977
|
+
const doc = document;
|
|
978
|
+
const html = doc.documentElement;
|
|
979
|
+
const body = doc.body;
|
|
980
|
+
const serialize = (el) => {
|
|
981
|
+
if (!el)
|
|
982
|
+
return null;
|
|
983
|
+
return {
|
|
984
|
+
tag: el.tagName,
|
|
985
|
+
id: el.id || null,
|
|
986
|
+
classes: Array.from(el.classList || []),
|
|
987
|
+
};
|
|
988
|
+
};
|
|
989
|
+
const firstChildren = (el, limit = 8) => {
|
|
990
|
+
if (!el || !el.children)
|
|
991
|
+
return [];
|
|
992
|
+
return Array.from(el.children)
|
|
993
|
+
.slice(0, limit)
|
|
994
|
+
.map((child) => serialize(child));
|
|
995
|
+
};
|
|
996
|
+
return {
|
|
997
|
+
html: serialize(html),
|
|
998
|
+
body: serialize(body),
|
|
999
|
+
app: serialize(doc.getElementById('app')),
|
|
1000
|
+
appChildren: firstChildren(doc.getElementById('app')),
|
|
1001
|
+
bodyChildren: firstChildren(body),
|
|
1002
|
+
};
|
|
1003
|
+
});
|
|
1004
|
+
return {
|
|
1005
|
+
success: true,
|
|
1006
|
+
data: info,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
case 'eval':
|
|
1010
|
+
case 'evaluate':
|
|
1011
|
+
case 'evaluate_js': {
|
|
1012
|
+
const expression = parameters.expression || parameters.script;
|
|
1013
|
+
if (!expression) {
|
|
1014
|
+
throw new Error('Eval node requires expression');
|
|
1015
|
+
}
|
|
1016
|
+
const arg = parameters.arg;
|
|
1017
|
+
const result = await session.evaluate(expression, arg);
|
|
1018
|
+
return {
|
|
1019
|
+
success: true,
|
|
1020
|
+
data: { result },
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
case 'pick_dom': {
|
|
1024
|
+
const result = await this.handleDomPick(session, parameters);
|
|
1025
|
+
return {
|
|
1026
|
+
success: true,
|
|
1027
|
+
data: result,
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
case 'dom_pick_loopback': {
|
|
1031
|
+
const result = await this.handleDomPickerLoopback(session, parameters);
|
|
1032
|
+
return {
|
|
1033
|
+
success: true,
|
|
1034
|
+
data: result,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
default:
|
|
1038
|
+
throw new Error(`Unsupported node type: ${nodeType}`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
async handleDevControl(sessionId, command) {
|
|
1042
|
+
if (command.action === 'enable_overlay') {
|
|
1043
|
+
return {
|
|
1044
|
+
success: true,
|
|
1045
|
+
data: {
|
|
1046
|
+
enabled: true,
|
|
1047
|
+
message: 'Overlay not implemented in TS service yet',
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
success: false,
|
|
1053
|
+
error: `Unsupported dev control action: ${command.action}`,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
async handleDevCommand(sessionId, command) {
|
|
1057
|
+
if (!sessionId) {
|
|
1058
|
+
throw new Error('session_id required for dev_command');
|
|
1059
|
+
}
|
|
1060
|
+
const session = this.options.sessionManager.getSession(sessionId);
|
|
1061
|
+
if (!session) {
|
|
1062
|
+
return {
|
|
1063
|
+
success: false,
|
|
1064
|
+
error: `Session ${sessionId} not found`,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
const action = command.action;
|
|
1068
|
+
const parameters = command.parameters || {};
|
|
1069
|
+
switch (action) {
|
|
1070
|
+
case 'highlight_element': {
|
|
1071
|
+
const selector = (parameters.selector || '').trim();
|
|
1072
|
+
if (!selector) {
|
|
1073
|
+
return { success: false, error: 'selector required' };
|
|
1074
|
+
}
|
|
1075
|
+
const channel = (parameters.channel || 'ui-action').trim() || 'ui-action';
|
|
1076
|
+
const style = typeof parameters.style === 'string' ? parameters.style : undefined;
|
|
1077
|
+
const duration = typeof parameters.duration === 'number' ? parameters.duration : Number(parameters.duration || 0);
|
|
1078
|
+
const sticky = typeof parameters.sticky === 'boolean' ? parameters.sticky : Boolean(parameters.hold || false);
|
|
1079
|
+
const rootSelector = parameters.root_selector || parameters.rootSelector || null;
|
|
1080
|
+
appendHighlightLog('request', { sessionId, channel, selector, style, duration, sticky, rootSelector });
|
|
1081
|
+
const page = await session.ensurePage();
|
|
1082
|
+
const result = await page.evaluate((config) => {
|
|
1083
|
+
if (!window.__webautoRuntime?.highlight?.highlightSelector) {
|
|
1084
|
+
throw new Error('highlight runtime unavailable');
|
|
1085
|
+
}
|
|
1086
|
+
const res = window.__webautoRuntime.highlight.highlightSelector(config.selector, {
|
|
1087
|
+
channel: config.channel,
|
|
1088
|
+
...(config.style ? { style: config.style } : {}),
|
|
1089
|
+
...(Number.isFinite(config.duration) && config.duration > 0 ? { duration: config.duration } : {}),
|
|
1090
|
+
...(typeof config.sticky === 'boolean' ? { sticky: config.sticky } : {}),
|
|
1091
|
+
...(config.rootSelector ? { rootSelector: config.rootSelector } : {}),
|
|
1092
|
+
});
|
|
1093
|
+
const count = typeof res === 'number' ? res : Number(res?.count || res?.matched || 0);
|
|
1094
|
+
return { count: Number.isFinite(count) ? count : 0, channel: config.channel };
|
|
1095
|
+
}, { selector, channel, style, duration: Number.isFinite(duration) ? duration : 0, sticky, rootSelector });
|
|
1096
|
+
appendHighlightLog('result', { sessionId, channel, selector, count: result?.count || 0 });
|
|
1097
|
+
return { success: true, data: result };
|
|
1098
|
+
}
|
|
1099
|
+
case 'clear_highlight': {
|
|
1100
|
+
const channel = (parameters.channel || 'ui-action').trim() || 'ui-action';
|
|
1101
|
+
appendHighlightLog('clear', { sessionId, channel });
|
|
1102
|
+
const page = await session.ensurePage();
|
|
1103
|
+
await page.evaluate((ch) => {
|
|
1104
|
+
window.__webautoRuntime?.highlight?.clear?.(ch);
|
|
1105
|
+
}, channel);
|
|
1106
|
+
return {
|
|
1107
|
+
success: true,
|
|
1108
|
+
data: { cleared: true },
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
case 'highlight_dom_path': {
|
|
1112
|
+
const path = (parameters.path || parameters.dom_path || '').trim();
|
|
1113
|
+
if (!path) {
|
|
1114
|
+
return { success: false, error: 'path required' };
|
|
1115
|
+
}
|
|
1116
|
+
const channel = (parameters.channel || 'ui-action').trim() || 'ui-action';
|
|
1117
|
+
const style = typeof parameters.style === 'string' ? parameters.style : undefined;
|
|
1118
|
+
const duration = typeof parameters.duration === 'number' ? parameters.duration : Number(parameters.duration || 0);
|
|
1119
|
+
const sticky = typeof parameters.sticky === 'boolean' ? parameters.sticky : Boolean(parameters.hold || false);
|
|
1120
|
+
const rootSelector = parameters.root_selector || parameters.rootSelector || null;
|
|
1121
|
+
appendHighlightLog('request', { sessionId, channel, path, style, duration, sticky, rootSelector });
|
|
1122
|
+
const page = await session.ensurePage();
|
|
1123
|
+
const result = await page.evaluate((config) => {
|
|
1124
|
+
const runtime = window.__webautoRuntime;
|
|
1125
|
+
if (!runtime?.highlight?.highlightElements) {
|
|
1126
|
+
throw new Error('highlight runtime unavailable');
|
|
1127
|
+
}
|
|
1128
|
+
if (!runtime?.dom?.resolveByPath) {
|
|
1129
|
+
throw new Error('dom resolveByPath unavailable');
|
|
1130
|
+
}
|
|
1131
|
+
const node = runtime.dom.resolveByPath(config.path, config.rootSelector || null);
|
|
1132
|
+
if (!node)
|
|
1133
|
+
return { count: 0, channel: config.channel };
|
|
1134
|
+
runtime.highlight.highlightElements([node], {
|
|
1135
|
+
channel: config.channel,
|
|
1136
|
+
...(config.style ? { style: config.style } : {}),
|
|
1137
|
+
...(Number.isFinite(config.duration) && config.duration > 0 ? { duration: config.duration } : {}),
|
|
1138
|
+
...(typeof config.sticky === 'boolean' ? { sticky: config.sticky } : {}),
|
|
1139
|
+
...(config.rootSelector ? { rootSelector: config.rootSelector } : {}),
|
|
1140
|
+
});
|
|
1141
|
+
return { count: 1, channel: config.channel };
|
|
1142
|
+
}, { path, channel, style, duration: Number.isFinite(duration) ? duration : 0, sticky, rootSelector });
|
|
1143
|
+
appendHighlightLog('result', { sessionId, channel, path, count: result?.count || 0 });
|
|
1144
|
+
return { success: true, data: result };
|
|
1145
|
+
}
|
|
1146
|
+
case 'cancel_dom_pick': {
|
|
1147
|
+
const result = await this.cancelDomPicker(session);
|
|
1148
|
+
return {
|
|
1149
|
+
success: true,
|
|
1150
|
+
data: result,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
default:
|
|
1154
|
+
return {
|
|
1155
|
+
success: false,
|
|
1156
|
+
error: `Unsupported dev command: ${action}`,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async highlightViaRuntime() {
|
|
1161
|
+
return { count: 0 };
|
|
1162
|
+
}
|
|
1163
|
+
async clearHighlightOverlays() {
|
|
1164
|
+
return { cleared: 0 };
|
|
1165
|
+
}
|
|
1166
|
+
async cancelDomPicker(session) {
|
|
1167
|
+
const page = await session.ensurePage();
|
|
1168
|
+
return page.evaluate(() => {
|
|
1169
|
+
const cancel = window.__webautoDomPickerCancel;
|
|
1170
|
+
if (typeof cancel === 'function') {
|
|
1171
|
+
try {
|
|
1172
|
+
cancel();
|
|
1173
|
+
window.__webautoDomPickerCancel = null;
|
|
1174
|
+
return { cancelled: true };
|
|
1175
|
+
}
|
|
1176
|
+
catch (err) {
|
|
1177
|
+
return { cancelled: false, error: err.message };
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return { cancelled: false };
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
send(socket, payload) {
|
|
1184
|
+
try {
|
|
1185
|
+
socket.send(JSON.stringify(payload));
|
|
1186
|
+
}
|
|
1187
|
+
catch (err) {
|
|
1188
|
+
console.error('[browser-ws] failed to send message:', err);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
rawToString(data) {
|
|
1192
|
+
if (typeof data === 'string')
|
|
1193
|
+
return data;
|
|
1194
|
+
if (Buffer.isBuffer(data))
|
|
1195
|
+
return data.toString('utf-8');
|
|
1196
|
+
if (Array.isArray(data)) {
|
|
1197
|
+
return Buffer.concat(data).toString('utf-8');
|
|
1198
|
+
}
|
|
1199
|
+
return Buffer.from(data).toString('utf-8');
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
//# sourceMappingURL=ws-server.js.map
|