@xopcai/xopc 0.0.368 → 0.0.369
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/dist/browser-ext/_locales/en/messages.json +30 -1
- package/dist/browser-ext/_locales/zh_CN/messages.json +30 -1
- package/dist/browser-ext/dist/assets/{auth-beGtbLPS.js → page-context-BeworyNB.js} +1331 -1355
- package/dist/browser-ext/dist/assets/{sidepanel-BT0ojEzG.css → sidepanel-Ccl0eSa1.css} +43 -0
- package/dist/browser-ext/dist/background.js +680 -182
- package/dist/browser-ext/dist/sidepanel.html +2 -2
- package/dist/browser-ext/dist/sidepanel.js +372 -193
- package/dist/browser-ext/manifest.json +2 -8
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{activity-store-D9ZMh5ZR.js → activity-store-BQ3PMp2q.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-Bj3rvcTm.js → agent-avatar-dicebear-Stj17S9u.js} +1 -1
- package/dist/gateway/static/root/assets/agent-defaults-settings-panel-CCRfvuov.js +1 -0
- package/dist/gateway/static/root/assets/{agent-plugin-dialog-BjdgKCnk.js → agent-plugin-dialog-CRlzd8El.js} +1 -1
- package/dist/gateway/static/root/assets/agents-_ug2LCQb.js +66 -0
- package/dist/gateway/static/root/assets/{agents-admin-api-B5QDepbW.js → agents-admin-api-HBZvVyzF.js} +1 -1
- package/dist/gateway/static/root/assets/{app-management-settings-panel-2rTEtB_X.js → app-management-settings-panel-pJFe6M6c.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-BPVBS80k.js → appearance-settings-BrxSpBWa.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-1FuKlZYP.js → apps-page-DegSl5Lh.js} +1 -1
- package/dist/gateway/static/root/assets/{archive-plugin-BbT1O7oK.js → archive-plugin-BO3I9zRP.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-DjwP_h8h.js → automations-page-BZQ_VHjp.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace-BQywN6nG.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-gS7Wg68v.js → binary-plugins-gFD7vH9R.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-DbwMaivP.js → block-editor-3XjFv3kK.js} +3 -3
- package/dist/gateway/static/root/assets/browser-automation-inputs-DxRoKFun.js +1 -0
- package/dist/gateway/static/root/assets/browser-automations-page-yiEuG1v7.js +3 -0
- package/dist/gateway/static/root/assets/browser-settings-page-ch2_bQF0.js +3 -0
- package/dist/gateway/static/root/assets/{capabilities-Dd1PZRQw.js → capabilities-B48r0DDa.js} +1 -1
- package/dist/gateway/static/root/assets/{capabilities-Lswa0hv8.js → capabilities-DO-vfNKI.js} +1 -1
- package/dist/gateway/static/root/assets/{capabilities-page-djBsC_tG.js → capabilities-page-BEnFt_dj.js} +2 -2
- package/dist/gateway/static/root/assets/{capabilities-settings-panel-DTHR1DrJ.js → capabilities-settings-panel-NckMsJ5N.js} +4 -4
- package/dist/gateway/static/root/assets/{capability-header-actions-494dnJsj.js → capability-header-actions-CY8KCO9j.js} +1 -1
- package/dist/gateway/static/root/assets/channels-CHOS2CGa.js +1 -0
- package/dist/gateway/static/root/assets/{chat-terminal-dock-Bl7-Zk-x.js → chat-terminal-dock-Bwknnb4c.js} +2 -2
- package/dist/gateway/static/root/assets/{computer-settings-page-ByExKr_6.js → computer-settings-page-B2xeFCSt.js} +1 -1
- package/dist/gateway/static/root/assets/{connector-service-page-BK_5aFxO.js → connector-service-page-C83DnAqK.js} +1 -1
- package/dist/gateway/static/root/assets/connectors-page-D6wTmWA6.js +2 -0
- package/dist/gateway/static/root/assets/{date-picker-BXJv6472.js → date-picker-Bq9GR-DU.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-CWbbhKZa.js → dependency-picker-BTERkA9X.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-DWNEC3mk.js → desktop-pet-BwsbQw8V.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-BYwlxGiN.js → desktop-pet-settings-sL9i44pP.js} +1 -1
- package/dist/gateway/static/root/assets/device-pairing-wizard-DI2bqaKO.js +1 -0
- package/dist/gateway/static/root/assets/{directory-picker-path-field-D26tSY8Z.js → directory-picker-path-field-BFlDmsS5.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-xYIGNegB.js → extension-debug-page-7Z1Tp7-w.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-BoQ1-3AW.js → extension-page-uGqvb8QX.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-provider-Cw_JU6RR.js → extension-provider-xlSyz4Z6.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-BH5TFGPe.js → extension-settings-page-CKHgrjOs.js} +1 -1
- package/dist/gateway/static/root/assets/{fallback-models-04Wqmw14.js → fallback-models-BwQg0G1D.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-swr-CNf0ZeRw.js → gateway-config-swr-7RKqgU_U.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-realtime-C4KtwMY3.js → gateway-realtime-DDWymi-o.js} +1 -1
- package/dist/gateway/static/root/assets/gateway-realtime-store-BheKaO6d.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-settings-BqvCJVUC.js → gateway-settings-BAQnSEMJ.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-startup-retry-DIxHacUw.js → gateway-startup-retry-jiV5sbEX.js} +1 -1
- package/dist/gateway/static/root/assets/home-page-BZDjvwet.js +2 -0
- package/dist/gateway/static/root/assets/{image-generation-api-CS0q0Kto.js → image-generation-api-DO4mXb6I.js} +1 -1
- package/dist/gateway/static/root/assets/{imports-page-n_rSLo6b.js → imports-page-re1_Nh64.js} +1 -1
- package/dist/gateway/static/root/assets/index-BB1z8QxA.js +106 -0
- package/dist/gateway/static/root/assets/index-D9R6tU7r.css +1 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-tVkrHbYF.js → keyboard-shortcuts-settings-B_xja9LN.js} +1 -1
- package/dist/gateway/static/root/assets/{local-app-workbench-page-L8tYuas0.js → local-app-workbench-page-CErdCrPX.js} +1 -1
- package/dist/gateway/static/root/assets/{local-apps-page-SSJ9B1bH.js → local-apps-page-BPL2RXdK.js} +1 -1
- package/dist/gateway/static/root/assets/{local-session-drafts-B_McurIj.js → local-session-drafts-DRPsDCj1.js} +1 -1
- package/dist/gateway/static/root/assets/{locale-store-D6_rF-qT.js → locale-store-COVscAqC.js} +1 -1
- package/dist/gateway/static/root/assets/{logs-page-BWD63eg1.js → logs-page-DL-BM0xq.js} +2 -2
- package/dist/gateway/static/root/assets/management-settings-BOVRHhXk.js +1 -0
- package/dist/gateway/static/root/assets/{markdown-view-_xwUuKce.js → markdown-view-B_qnAcW2.js} +1 -1
- package/dist/gateway/static/root/assets/{media-plugins-C631CDu4.js → media-plugins-C0Pc-wSY.js} +1 -1
- package/dist/gateway/static/root/assets/messages-Bb2GK-Nj.js +3 -0
- package/dist/gateway/static/root/assets/models-hub-cache-DyPPksMP.js +1 -0
- package/dist/gateway/static/root/assets/note-detail-panel-DJJK2yL4.js +5 -0
- package/dist/gateway/static/root/assets/{notes-api-vIZWQ9Yt.js → notes-api-sBgBcyLW.js} +1 -1
- package/dist/gateway/static/root/assets/notes-page-oCxO7ZTv.js +8 -0
- package/dist/gateway/static/root/assets/{page-context-capture-button-BlPDM7GM.js → page-context-capture-button-tf77xIz3.js} +1 -1
- package/dist/gateway/static/root/assets/personal-page-DOk9gUK5.js +1 -0
- package/dist/gateway/static/root/assets/{product-open-page-BJjWvoER.js → product-open-page-Dc3Y-Y7J.js} +1 -1
- package/dist/gateway/static/root/assets/{product-preview-content-CEd4E7Gf.js → product-preview-content-MG_hmVA3.js} +1 -1
- package/dist/gateway/static/root/assets/{project-detail-page-DM4_VLNX.js → project-detail-page-3uAzbW4T.js} +1 -1
- package/dist/gateway/static/root/assets/{projects-page-lszV8HuS.js → projects-page-MllfNLGA.js} +1 -1
- package/dist/gateway/static/root/assets/{registry-api-uJnjqTcn.js → registry-api-Bh9UBb-f.js} +1 -1
- package/dist/gateway/static/root/assets/{remote-access-hub-O6j9CWX0.js → remote-access-hub-Bs-C70el.js} +1 -1
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-CjnNQYW8.js → runtime-tools-settings-panel-DpJ5jNzR.js} +1 -1
- package/dist/gateway/static/root/assets/{scenes-page-B3T3dDOR.js → scenes-page-Dpxamkw6.js} +2 -2
- package/dist/gateway/static/root/assets/{schema-form-DT3c4Vd_.js → schema-form-DUeyQ1zh.js} +1 -1
- package/dist/gateway/static/root/assets/{send-session-input-CICkGwn-.js → send-session-input-Dgn4BRk_.js} +1 -1
- package/dist/gateway/static/root/assets/{session-api-BcQodoAu.js → session-api-yjpEz4pP.js} +1 -1
- package/dist/gateway/static/root/assets/{session-manager-B0tOE4qz.js → session-manager-LInoZ_Yi.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-BCVlNHtq.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-Bz-ue9fZ.js +1 -0
- package/dist/gateway/static/root/assets/{settings-page-D-PZjcss.js → settings-page-DouHuBXt.js} +1 -1
- package/dist/gateway/static/root/assets/{setup-status-panel-Ce3gTbT3.js → setup-status-panel-B5BoZ-1y.js} +1 -1
- package/dist/gateway/static/root/assets/{share-link-dialog-CWS8EkJS.js → share-link-dialog-DJTabLAM.js} +1 -1
- package/dist/gateway/static/root/assets/{share-preview-page-23NJAKiq.js → share-preview-page-jw7h4t_B.js} +1 -1
- package/dist/gateway/static/root/assets/{shares-api-iKhHbvBD.js → shares-api-c0dfWz6h.js} +1 -1
- package/dist/gateway/static/root/assets/shares-settings-BlnlzGjH.js +1 -0
- package/dist/gateway/static/root/assets/{skill-api-C-MWdQwl.js → skill-api-CfyX_0zN.js} +1 -1
- package/dist/gateway/static/root/assets/{skill-reload-api-iy02aB13.js → skill-reload-api-Ge5XymHa.js} +1 -1
- package/dist/gateway/static/root/assets/skills-page-DjdTeYzm.js +2 -0
- package/dist/gateway/static/root/assets/system-settings-panel-DWf256TE.js +1 -0
- package/dist/gateway/static/root/assets/{task-detail-page-B2SIEyhZ.js → task-detail-page-DE6N1ZQe.js} +2 -2
- package/dist/gateway/static/root/assets/{task-expandable-content-D2LRR4BC.js → task-expandable-content-hYcgCEE3.js} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-BJfhz-Zq.js → theme-store-BOfCKgl6.js} +1 -1
- package/dist/gateway/static/root/assets/{tracing-settings-page-aHD56iaS.js → tracing-settings-page-DDWFXcST.js} +1 -1
- package/dist/gateway/static/root/assets/{tunnel-api-BCf8CfTC.js → tunnel-api-BBm9esrY.js} +1 -1
- package/dist/gateway/static/root/assets/{usage-page-B4XUkuJd.js → usage-page-C93x89uW.js} +1 -1
- package/dist/gateway/static/root/assets/{use-autosave-BnnTw9RE.js → use-autosave-CSckcjFx.js} +1 -1
- package/dist/gateway/static/root/assets/{use-composer-attachments-Itczg0y1.js → use-composer-attachments-C7Gaaai1.js} +1 -1
- package/dist/gateway/static/root/assets/{use-file-preview-expanded-DJ5vyrvM.js → use-file-preview-expanded-Db2VYg1b.js} +2 -2
- package/dist/gateway/static/root/assets/user-model-page-B74LkVbp.js +2 -0
- package/dist/gateway/static/root/assets/{voice-api-key-field-BphJku8x.js → voice-api-key-field-GEw5x2fU.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-config-api-B4u4coFj.js → voice-config-api-3yyVwXln.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-jgOEP1xw.js → work-discovery-overlay-Bs2MKiTv.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-N69crzRu.js → workflow-run-setup-panel-BR_U_snV.js} +2 -2
- package/dist/gateway/static/root/assets/workflows-page-Crk4SxtC.js +6 -0
- package/dist/gateway/static/root/assets/{workspace-api-Lzv6GDO8.js → workspace-api-Si2o3inw.js} +2 -2
- package/dist/gateway/static/root/index.html +27 -27
- package/dist/package.js +1 -1
- package/dist/packages/browser-control-contract/src/automation.js +213 -0
- package/dist/packages/browser-control-contract/src/index.js +43 -12
- package/dist/packages/gateway-contract/src/gateway-scopes.js +3 -1
- package/dist/packages/gateway-contract/src/notifications.js +1 -0
- package/dist/packages/realtime-protocol/src/voice.js +9 -3
- package/dist/src/agent/coding/repository-instructions.js +8 -1
- package/dist/src/agent/tool-manuals/browser.d.ts +1 -1
- package/dist/src/agent/tool-manuals/browser.js +3 -3
- package/dist/src/agent/tools/browser/tool/browser-use-tool.js +1 -1
- package/dist/src/agent/tools/browser/tool/schemas.d.ts +0 -13
- package/dist/src/agent/tools/browser/tool/schemas.js +0 -4
- package/dist/src/agent/tools/browser-automation-tool.d.ts +74 -1
- package/dist/src/agent/tools/browser-automation-tool.js +52 -6
- package/dist/src/automations/capabilities/write.d.ts +2 -0
- package/dist/src/automations/domain/types.d.ts +11 -0
- package/dist/src/automations/domain/validation.d.ts +4 -0
- package/dist/src/automations/domain/validation.js +1 -0
- package/dist/src/automations/draft/automation-draft-validator.d.ts +1 -0
- package/dist/src/automations/service/action-executor.js +11 -3
- package/dist/src/automations/service/automation-service.d.ts +1 -0
- package/dist/src/automations/service/automation-service.js +13 -1
- package/dist/src/bootstrap/application-state.js +1 -1
- package/dist/src/browser/automations/repository.d.ts +4 -0
- package/dist/src/browser/automations/repository.js +27 -6
- package/dist/src/browser/automations/runner.js +101 -9
- package/dist/src/browser/automations/runtime-service.js +4 -3
- package/dist/src/browser/automations/schema.d.ts +84 -1
- package/dist/src/browser/automations/schema.js +68 -2
- package/dist/src/browser/automations/service.d.ts +78 -1
- package/dist/src/browser/automations/service.js +36 -4
- package/dist/src/browser/automations/types.d.ts +19 -4
- package/dist/src/browser/drivers/extension-driver.d.ts +5 -5
- package/dist/src/browser/drivers/extension-driver.js +10 -10
- package/dist/src/browser/drivers/playwright-driver.js +46 -0
- package/dist/src/browser/index.d.ts +1 -2
- package/dist/src/browser/index.js +2 -3
- package/dist/src/browser/policy/browser-policy.d.ts +0 -1
- package/dist/src/browser/policy/browser-policy.js +1 -4
- package/dist/src/browser/providers/index.d.ts +1 -1
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/providers/realtime-extension.d.ts +1 -1
- package/dist/src/browser/providers/realtime-extension.js +5 -1
- package/dist/src/browser/recordings/service.d.ts +22 -0
- package/dist/src/browser/recordings/service.js +160 -0
- package/dist/src/browser/runtime/browser-runtime.d.ts +1 -1
- package/dist/src/browser/runtime/browser-runtime.js +19 -56
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/computer/runtime.js +2 -2
- package/dist/src/config/schema.d.ts +12 -12
- package/dist/src/config/schema.js +12 -12
- package/dist/src/endpoint-tools/policy.js +4 -3
- package/dist/src/gateway/hono/routes/browser-automations.js +23 -7
- package/dist/src/gateway/hono/routes/browser-recordings.d.ts +3 -0
- package/dist/src/gateway/hono/routes/browser-recordings.js +78 -0
- package/dist/src/gateway/hono/routes/browser.js +1 -34
- package/dist/src/gateway/hono/routes/device-push.js +1 -0
- package/dist/src/gateway/hono/routes/home.js +1 -1
- package/dist/src/gateway/hono/routes/lazy-bundles.js +8 -0
- package/dist/src/gateway/hono/routes/personal-agent.js +35 -0
- package/dist/src/gateway/hono/routes/voice.js +5 -2
- package/dist/src/gateway/security/gateway-scopes.js +3 -1
- package/dist/src/gateway/service/agent-runner.js +1 -0
- package/dist/src/home-intelligence/generator.d.ts +2 -0
- package/dist/src/home-intelligence/generator.js +53 -4
- package/dist/src/home-intelligence/host.d.ts +2 -1
- package/dist/src/home-intelligence/host.js +23 -3
- package/dist/src/migrations/cloud-public-models.js +1 -0
- package/dist/src/notifications/clarification.d.ts +5 -0
- package/dist/src/notifications/clarification.js +66 -0
- package/dist/src/notifications/device-store.js +1 -0
- package/dist/src/notifications/harmony-push.d.ts +1 -0
- package/dist/src/notifications/harmony-push.js +3 -1
- package/dist/src/notifications/planner.d.ts +1 -0
- package/dist/src/notifications/service.d.ts +2 -2
- package/dist/src/notifications/service.js +20 -4
- package/dist/src/notifications/store.d.ts +2 -0
- package/dist/src/notifications/store.js +7 -1
- package/dist/src/notifications/types.d.ts +1 -0
- package/dist/src/notifications/types.js +1 -0
- package/dist/src/storage/sqlite/clarification-wait-repository.js +1 -0
- package/dist/src/storage/sqlite/migrations/238_browser_record_replay.sql +45 -0
- package/dist/src/storage/sqlite/migrations/239_clarification_notifications.sql +26 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
- package/dist/src/storage/sqlite/migrations/runner.js +3 -3
- package/dist/src/storage/sqlite/schema.js +1 -1
- package/dist/src/tasks/home-query-service.d.ts +1 -1
- package/dist/src/tasks/home-query-service.js +6 -2
- package/dist/src/voice/dashscope/streaming-tts-stream.d.ts +2 -1
- package/dist/src/voice/dashscope/streaming-tts-stream.js +135 -79
- package/dist/src/voice/realtime/agentEngine.js +78 -1
- package/dist/src/voice/realtime/speakable-segmenter.d.ts +2 -0
- package/dist/src/voice/realtime/speakable-segmenter.js +30 -0
- package/dist/src/voice/tts/providers/alibaba-speech.js +17 -1
- package/dist/src/voice/tts/speak-core.d.ts +3 -0
- package/dist/src/voice/tts/speak-core.js +21 -1
- package/dist/src/voice/tts/speech-provider-types.d.ts +8 -0
- package/package.json +3 -3
- package/dist/gateway/static/root/assets/agent-defaults-settings-panel-D0tD3kCe.js +0 -1
- package/dist/gateway/static/root/assets/agents-CWXKzX5L.js +0 -66
- package/dist/gateway/static/root/assets/automations-workspace-C9ZT6TAU.js +0 -6
- package/dist/gateway/static/root/assets/browser-automation-inputs-CXG_JveC.js +0 -1
- package/dist/gateway/static/root/assets/browser-automations-page-CJc7OKR3.js +0 -3
- package/dist/gateway/static/root/assets/browser-settings-page-BjaTEUR8.js +0 -3
- package/dist/gateway/static/root/assets/channels-BaYRJtz3.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-DyqRwSH2.js +0 -2
- package/dist/gateway/static/root/assets/device-pairing-wizard-BAc4F35b.js +0 -1
- package/dist/gateway/static/root/assets/gateway-realtime-store-ChZUzVbe.js +0 -1
- package/dist/gateway/static/root/assets/home-page-DhPPLbdD.js +0 -2
- package/dist/gateway/static/root/assets/index-CK4O2xMa.css +0 -1
- package/dist/gateway/static/root/assets/index-DpLD-7Hc.js +0 -106
- package/dist/gateway/static/root/assets/management-settings-aKSibklM.js +0 -1
- package/dist/gateway/static/root/assets/messages-Bnx3GMDB.js +0 -3
- package/dist/gateway/static/root/assets/models-hub-cache-Di83uIPw.js +0 -1
- package/dist/gateway/static/root/assets/note-detail-panel-B_fve3_2.js +0 -5
- package/dist/gateway/static/root/assets/notes-page-DnChyp8S.js +0 -8
- package/dist/gateway/static/root/assets/personal-page-BM2qAgg_.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-BIixHXlQ.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-49SUEWPh.js +0 -1
- package/dist/gateway/static/root/assets/shares-settings-CNEZjF66.js +0 -1
- package/dist/gateway/static/root/assets/skills-page-BndNdYsJ.js +0 -2
- package/dist/gateway/static/root/assets/system-settings-panel-EpZeMlvp.js +0 -1
- package/dist/gateway/static/root/assets/user-model-page-Bz5lkFG9.js +0 -2
- package/dist/gateway/static/root/assets/workflows-page-CPCgOfHh.js +0 -6
- package/dist/src/browser/policy/approval-store.d.ts +0 -17
- package/dist/src/browser/policy/approval-store.js +0 -62
- package/dist/src/browser/providers/extension-ws-acquire.d.ts +0 -19
- package/dist/src/browser/providers/extension-ws-acquire.js +0 -78
- package/dist/src/browser/providers/extension.d.ts +0 -61
- package/dist/src/browser/providers/extension.js +0 -378
|
@@ -1,4 +1,343 @@
|
|
|
1
|
-
import { n as endpointToolContentSchema, E as ENDPOINT_PROTOCOL_VERSION, g as canonicalJson, T as TAB_BINDING_PREFIX, h as captureContextMenuSelection, P as PENDING_CONTEXT_KEY, H as t,
|
|
1
|
+
import { y as readProfile, p as gatewayFetch, n as endpointToolContentSchema, E as ENDPOINT_PROTOCOL_VERSION, g as canonicalJson, T as TAB_BINDING_PREFIX, h as captureContextMenuSelection, P as PENDING_CONTEXT_KEY, H as t, z as registerBrowserEndpoint, q as getAccessProfile, a as RealtimeClient, j as createBrowserEndpointHello, R as REALTIME_PROTOCOL_VERSION } from './assets/page-context-BeworyNB.js';
|
|
2
|
+
|
|
3
|
+
const LOG_COLORS = {
|
|
4
|
+
debug: "color: #888",
|
|
5
|
+
info: "color: #3b82f6",
|
|
6
|
+
warn: "color: #f59e0b",
|
|
7
|
+
error: "color: #ef4444"
|
|
8
|
+
};
|
|
9
|
+
function createLogger(prefix) {
|
|
10
|
+
function log(level, message, data) {
|
|
11
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
12
|
+
const formatted = `[${timestamp}] [${prefix}] ${message}`;
|
|
13
|
+
if (data && Object.keys(data).length > 0) {
|
|
14
|
+
console[level](`%c${formatted}`, LOG_COLORS[level], data);
|
|
15
|
+
} else {
|
|
16
|
+
console[level](`%c${formatted}`, LOG_COLORS[level]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
debug: (msg, data) => log("debug", msg, data),
|
|
21
|
+
info: (msg, data) => log("info", msg, data),
|
|
22
|
+
warn: (msg, data) => log("warn", msg, data),
|
|
23
|
+
error: (msg, data) => log("error", msg, data)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const log$2 = createLogger("SessionManager");
|
|
28
|
+
const XOPC_TAB_GROUP_TITLE = "xopc";
|
|
29
|
+
const busySessions = /* @__PURE__ */ new Set();
|
|
30
|
+
function holdAutomationSession(id) {
|
|
31
|
+
busySessions.add(id);
|
|
32
|
+
}
|
|
33
|
+
function releaseAutomationSession(id) {
|
|
34
|
+
busySessions.delete(id);
|
|
35
|
+
resetWindowIdleTimer(id);
|
|
36
|
+
}
|
|
37
|
+
const automationSessions = /* @__PURE__ */ new Map();
|
|
38
|
+
const WINDOW_IDLE_TIMEOUT = 12e4;
|
|
39
|
+
const DEFAULT_WORKSPACE = "default";
|
|
40
|
+
function resetWindowIdleTimer(workspace = DEFAULT_WORKSPACE) {
|
|
41
|
+
const session = automationSessions.get(workspace);
|
|
42
|
+
if (!session) return;
|
|
43
|
+
if (session.idleTimer) clearTimeout(session.idleTimer);
|
|
44
|
+
session.idleDeadlineAt = Date.now() + WINDOW_IDLE_TIMEOUT;
|
|
45
|
+
session.idleTimer = setTimeout(async () => {
|
|
46
|
+
const current = automationSessions.get(workspace);
|
|
47
|
+
if (!current) return;
|
|
48
|
+
if (busySessions.has(workspace)) {
|
|
49
|
+
resetWindowIdleTimer(workspace);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const tabs = await chrome.tabs.query({ groupId: current.groupId });
|
|
54
|
+
for (const t of tabs) {
|
|
55
|
+
if (t.id != null) await chrome.tabs.remove(t.id).catch(() => {
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
automationSessions.delete(workspace);
|
|
61
|
+
log$2.info("Automation tab group cleared due to idle timeout", { workspace });
|
|
62
|
+
}, WINDOW_IDLE_TIMEOUT);
|
|
63
|
+
}
|
|
64
|
+
async function waitForTabLoad(tabId, timeoutMs = 3e4) {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const timeout = setTimeout(resolve, timeoutMs);
|
|
67
|
+
const listener = (id, info) => {
|
|
68
|
+
if (id === tabId && info.status === "complete") {
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
71
|
+
resolve();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
75
|
+
chrome.tabs.get(tabId).then((tab) => {
|
|
76
|
+
if (tab.status === "complete") {
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
79
|
+
resolve();
|
|
80
|
+
}
|
|
81
|
+
}).catch(() => {
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
84
|
+
resolve();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function resolveTargetWindowId() {
|
|
89
|
+
const w = await chrome.windows.getLastFocused({ windowTypes: ["normal"] });
|
|
90
|
+
if (w?.id != null) return w.id;
|
|
91
|
+
const all = await chrome.windows.getAll({ windowTypes: ["normal"] });
|
|
92
|
+
const first = all.find((x) => x.id != null);
|
|
93
|
+
if (first?.id != null) return first.id;
|
|
94
|
+
throw new Error("No normal browser window — open a Chrome window, then run automation again.");
|
|
95
|
+
}
|
|
96
|
+
async function getOrCreateAutomationWindow(workspace = DEFAULT_WORKSPACE) {
|
|
97
|
+
const existing = automationSessions.get(workspace);
|
|
98
|
+
if (existing) {
|
|
99
|
+
try {
|
|
100
|
+
await chrome.tabGroups.get(existing.groupId);
|
|
101
|
+
const tabsInGroup = await chrome.tabs.query({ groupId: existing.groupId });
|
|
102
|
+
if (tabsInGroup.length === 0) throw new Error("empty group");
|
|
103
|
+
existing.tabIds = tabsInGroup.map((t) => t.id).filter(Boolean);
|
|
104
|
+
const active = tabsInGroup.find((t) => t.active);
|
|
105
|
+
existing.activeTabId = active?.id ?? tabsInGroup[0]?.id ?? null;
|
|
106
|
+
resetWindowIdleTimer(workspace);
|
|
107
|
+
return existing;
|
|
108
|
+
} catch {
|
|
109
|
+
automationSessions.delete(workspace);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const windowId = await resolveTargetWindowId();
|
|
113
|
+
const tab = await chrome.tabs.create({
|
|
114
|
+
windowId,
|
|
115
|
+
url: "about:blank",
|
|
116
|
+
active: false
|
|
117
|
+
});
|
|
118
|
+
if (tab.id == null) throw new Error("Failed to create automation tab");
|
|
119
|
+
const groupId = await chrome.tabs.group({
|
|
120
|
+
tabIds: [tab.id],
|
|
121
|
+
createProperties: { windowId }
|
|
122
|
+
});
|
|
123
|
+
await chrome.tabGroups.update(groupId, {
|
|
124
|
+
title: XOPC_TAB_GROUP_TITLE,
|
|
125
|
+
collapsed: false,
|
|
126
|
+
color: "blue"
|
|
127
|
+
});
|
|
128
|
+
const session = {
|
|
129
|
+
windowId,
|
|
130
|
+
groupId,
|
|
131
|
+
tabIds: [tab.id],
|
|
132
|
+
activeTabId: tab.id,
|
|
133
|
+
idleTimer: null,
|
|
134
|
+
idleDeadlineAt: 0,
|
|
135
|
+
createdAt: Date.now()
|
|
136
|
+
};
|
|
137
|
+
automationSessions.set(workspace, session);
|
|
138
|
+
resetWindowIdleTimer(workspace);
|
|
139
|
+
log$2.info("Created automation tab group", { workspace, windowId, groupId, tabId: tab.id });
|
|
140
|
+
return session;
|
|
141
|
+
}
|
|
142
|
+
async function getActiveTabId(workspace = DEFAULT_WORKSPACE) {
|
|
143
|
+
const session = await getOrCreateAutomationWindow(workspace);
|
|
144
|
+
if (session.activeTabId != null) {
|
|
145
|
+
const ok = await chrome.tabs.get(session.activeTabId).catch(() => null);
|
|
146
|
+
if (ok) return session.activeTabId;
|
|
147
|
+
}
|
|
148
|
+
const tabs = await chrome.tabs.query({ groupId: session.groupId });
|
|
149
|
+
const tabId = tabs.find((t) => t.active)?.id ?? tabs[0]?.id;
|
|
150
|
+
if (tabId == null) throw new Error("No tab in xopc automation group");
|
|
151
|
+
session.activeTabId = tabId;
|
|
152
|
+
return tabId;
|
|
153
|
+
}
|
|
154
|
+
async function addTabToAutomationGroup(url, workspace = DEFAULT_WORKSPACE) {
|
|
155
|
+
const session = await getOrCreateAutomationWindow(workspace);
|
|
156
|
+
const tab = await chrome.tabs.create({
|
|
157
|
+
windowId: session.windowId,
|
|
158
|
+
url,
|
|
159
|
+
active: false
|
|
160
|
+
});
|
|
161
|
+
if (tab.id == null) throw new Error("Failed to create tab");
|
|
162
|
+
const inGroup = await chrome.tabs.query({ groupId: session.groupId });
|
|
163
|
+
const mergedIds = [...inGroup.map((t) => t.id).filter(Boolean), tab.id];
|
|
164
|
+
await chrome.tabs.group({ tabIds: mergedIds, groupId: session.groupId });
|
|
165
|
+
session.activeTabId = tab.id;
|
|
166
|
+
session.tabIds = mergedIds;
|
|
167
|
+
resetWindowIdleTimer(workspace);
|
|
168
|
+
return { tabId: tab.id, session };
|
|
169
|
+
}
|
|
170
|
+
async function closeSession(workspace = DEFAULT_WORKSPACE) {
|
|
171
|
+
const session = automationSessions.get(workspace);
|
|
172
|
+
if (!session) return;
|
|
173
|
+
if (session.idleTimer) clearTimeout(session.idleTimer);
|
|
174
|
+
const tabs = await chrome.tabs.query({ groupId: session.groupId }).catch(() => []);
|
|
175
|
+
for (const t of tabs) {
|
|
176
|
+
if (t.id != null) await chrome.tabs.remove(t.id).catch(() => {
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
automationSessions.delete(workspace);
|
|
180
|
+
log$2.info("Closed automation session", { workspace });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function browserDom(input) {
|
|
184
|
+
const root = globalThis;
|
|
185
|
+
const doc = root.document;
|
|
186
|
+
const normalize = (value) => String(value || "").trim().replace(/\s+/g, " ").slice(0, 240);
|
|
187
|
+
const sensitive = (element) => element.type === "password" || element.type === "file" || /cc-|one-time-code|transaction-|password|token|secret|otp|cvv|cvc|card.?number|验证码|密码|银行卡|卡号/i.test([
|
|
188
|
+
element.autocomplete,
|
|
189
|
+
element.name,
|
|
190
|
+
element.id,
|
|
191
|
+
element.getAttribute("aria-label"),
|
|
192
|
+
name(element)
|
|
193
|
+
].join(" "));
|
|
194
|
+
const role = (element) => element.getAttribute("role") || {
|
|
195
|
+
A: "link",
|
|
196
|
+
BUTTON: "button",
|
|
197
|
+
SUMMARY: "button",
|
|
198
|
+
SELECT: "combobox",
|
|
199
|
+
TEXTAREA: "textbox",
|
|
200
|
+
FORM: "form",
|
|
201
|
+
FIELDSET: "group",
|
|
202
|
+
SECTION: "region"
|
|
203
|
+
}[element.tagName] || (element.tagName === "INPUT" ? element.type === "checkbox" ? "checkbox" : element.type === "radio" ? "radio" : ["submit", "button", "reset"].includes(element.type) ? "button" : "textbox" : "text");
|
|
204
|
+
const name = (element) => normalize(
|
|
205
|
+
(element.getAttribute("aria-labelledby") || "").split(/\s+/).map((id) => doc.getElementById(id)?.textContent || "").join(" ") || element.getAttribute("aria-label") || element.labels?.[0]?.textContent || (element.tagName === "FIELDSET" ? element.querySelector("legend")?.textContent : "") || element.getAttribute("alt") || element.getAttribute("placeholder") || (["submit", "button", "reset"].includes(element.type) ? element.value : "") || element.innerText || element.getAttribute("title") || ""
|
|
206
|
+
);
|
|
207
|
+
const describe = (element) => {
|
|
208
|
+
const container = element.parentElement?.closest('form,fieldset,section,[role="region"],[role="group"],[role="dialog"]');
|
|
209
|
+
return {
|
|
210
|
+
role: role(element),
|
|
211
|
+
name: name(element),
|
|
212
|
+
...element.getAttribute("data-testid") ? { testId: element.getAttribute("data-testid") } : {},
|
|
213
|
+
...container && name(container) ? { scope: { role: role(container), name: name(container) } } : {}
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const selector = 'a[href],button,input:not([type="hidden"]),textarea,select,summary,[role],[data-testid],label,p,span,h1,h2,h3,td,output';
|
|
217
|
+
if (input.operation === "resolve") {
|
|
218
|
+
const target = input.target;
|
|
219
|
+
const matches = Array.from(doc.querySelectorAll(selector)).filter((element2) => {
|
|
220
|
+
const rect = element2.getBoundingClientRect();
|
|
221
|
+
const style = root.getComputedStyle(element2);
|
|
222
|
+
if (!rect.width || !rect.height || style.display === "none" || style.visibility === "hidden") return false;
|
|
223
|
+
const descriptor = describe(element2);
|
|
224
|
+
return descriptor.role === target.role && (!target.name || descriptor.name === target.name) && (!target.nameIncludes || descriptor.name?.includes(target.nameIncludes)) && (!target.testId || descriptor.testId === target.testId) && (!target.scope || descriptor.scope?.role === target.scope.role && descriptor.scope?.name === target.scope.name);
|
|
225
|
+
});
|
|
226
|
+
if (matches.length !== 1) return { matches: matches.length };
|
|
227
|
+
const element = matches[0];
|
|
228
|
+
const ref = `xopc-e${++root.__xopcRefCounter || (root.__xopcRefCounter = 1)}`;
|
|
229
|
+
element.setAttribute("data-xopc-ref", ref);
|
|
230
|
+
return { matches: 1, node: {
|
|
231
|
+
ref,
|
|
232
|
+
role: role(element),
|
|
233
|
+
name: name(element),
|
|
234
|
+
value: !sensitive(element) && "value" in element ? String(element.value).slice(0, 4096) : void 0,
|
|
235
|
+
description: !sensitive(element) ? normalize(element.textContent) : void 0,
|
|
236
|
+
href: element.tagName === "A" ? element.href : void 0,
|
|
237
|
+
states: [sensitive(element) ? "sensitive" : "", element.disabled ? "disabled" : "", element.checked ? "checked" : "", ["BUTTON", "INPUT"].includes(element.tagName) && element.type === "submit" ? "submit" : ""].filter(Boolean)
|
|
238
|
+
} };
|
|
239
|
+
}
|
|
240
|
+
const prior = root.__xopcRecorder;
|
|
241
|
+
if (input.operation === "record" && prior?.recordingId === input.recordingId && prior?.generation === input.generation) return { recording: true };
|
|
242
|
+
if (prior) {
|
|
243
|
+
await prior.stop();
|
|
244
|
+
delete root.__xopcRecorder;
|
|
245
|
+
}
|
|
246
|
+
if (input.operation === "stop") return { stopped: true };
|
|
247
|
+
let sourceSeq = 0;
|
|
248
|
+
const pending = /* @__PURE__ */ new Set();
|
|
249
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
250
|
+
const lastValues = /* @__PURE__ */ new WeakMap();
|
|
251
|
+
const listeners = [];
|
|
252
|
+
const cleanUrl = () => {
|
|
253
|
+
const url = new URL(root.location.href);
|
|
254
|
+
url.username = "";
|
|
255
|
+
url.password = "";
|
|
256
|
+
url.hash = "";
|
|
257
|
+
for (const key of [...url.searchParams.keys()]) if (/token|secret|password|key|code/i.test(key)) url.searchParams.delete(key);
|
|
258
|
+
return url.toString();
|
|
259
|
+
};
|
|
260
|
+
const documentId = `${root.performance.timeOrigin}:${input.generation}`;
|
|
261
|
+
let failed = false;
|
|
262
|
+
let ended = false;
|
|
263
|
+
let enterTarget;
|
|
264
|
+
let enterAt = 0;
|
|
265
|
+
const emit = (event) => {
|
|
266
|
+
const task = root.chrome.runtime.sendMessage({
|
|
267
|
+
type: "browser/recording-event",
|
|
268
|
+
recordingId: input.recordingId,
|
|
269
|
+
generation: input.generation,
|
|
270
|
+
event: { ...event, id: root.crypto.randomUUID(), sourceSeq: ++sourceSeq, documentId, url: cleanUrl() }
|
|
271
|
+
}).then((ack) => {
|
|
272
|
+
if (!ack?.ok) failed = true;
|
|
273
|
+
}).catch(() => {
|
|
274
|
+
failed = true;
|
|
275
|
+
});
|
|
276
|
+
pending.add(task);
|
|
277
|
+
void task.finally(() => pending.delete(task));
|
|
278
|
+
};
|
|
279
|
+
const flush = (element) => {
|
|
280
|
+
if (!dirty.delete(element) || sensitive(element)) return;
|
|
281
|
+
const value = String(element.value || "").slice(0, 4096);
|
|
282
|
+
if (lastValues.get(element) === value) return;
|
|
283
|
+
lastValues.set(element, value);
|
|
284
|
+
emit({ action: element.tagName === "SELECT" ? "select" : "fill", target: describe(element), value });
|
|
285
|
+
};
|
|
286
|
+
const listen = (type, handler) => {
|
|
287
|
+
listeners.push([type, handler]);
|
|
288
|
+
(type === "pagehide" ? root : doc).addEventListener(type, handler, true);
|
|
289
|
+
};
|
|
290
|
+
listen("input", (event) => {
|
|
291
|
+
if (event.isTrusted && !sensitive(event.target) && !["checkbox", "radio"].includes(event.target.type) && "value" in event.target) dirty.add(event.target);
|
|
292
|
+
});
|
|
293
|
+
listen("dragstart", (event) => {
|
|
294
|
+
if (event.isTrusted) emit({ action: "unsupported", value: "Drag actions are not supported by the recorder." });
|
|
295
|
+
});
|
|
296
|
+
listen("change", (event) => {
|
|
297
|
+
if (!event.isTrusted || sensitive(event.target)) return;
|
|
298
|
+
if (["checkbox", "radio"].includes(event.target.type)) emit({ action: "check", target: describe(event.target), checked: !!event.target.checked });
|
|
299
|
+
else {
|
|
300
|
+
dirty.add(event.target);
|
|
301
|
+
flush(event.target);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
listen("focusout", (event) => flush(event.target));
|
|
305
|
+
listen("click", (event) => {
|
|
306
|
+
if (!event.isTrusted) return;
|
|
307
|
+
for (const element2 of dirty) flush(element2);
|
|
308
|
+
const element = event.target.closest('a[href],button,summary,[role="button"],input[type="submit"],input[type="button"]');
|
|
309
|
+
if (element && event.detail === 0 && Date.now() - enterAt < 500 && (element === enterTarget || element.form && element.form === enterTarget?.form)) return;
|
|
310
|
+
if (element && !sensitive(element)) emit({ action: "click", target: describe(element) });
|
|
311
|
+
else if (event.target.closest('canvas,[contenteditable="true"],input[type="file"]')) emit({ action: "unsupported", value: "Canvas, rich text and file actions require an assistant to complete the workflow." });
|
|
312
|
+
});
|
|
313
|
+
listen("pagehide", () => {
|
|
314
|
+
for (const element of dirty) flush(element);
|
|
315
|
+
emit({ action: "checkpoint" });
|
|
316
|
+
ended = true;
|
|
317
|
+
for (const [type, handler] of listeners) (type === "pagehide" ? root : doc).removeEventListener(type, handler, true);
|
|
318
|
+
});
|
|
319
|
+
listen("keydown", (event) => {
|
|
320
|
+
if (!event.isTrusted || event.isComposing || sensitive(event.target) || !["Enter", "Escape"].includes(event.key)) return;
|
|
321
|
+
flush(event.target);
|
|
322
|
+
if (event.key === "Enter") {
|
|
323
|
+
enterTarget = event.target;
|
|
324
|
+
enterAt = Date.now();
|
|
325
|
+
}
|
|
326
|
+
emit({ action: "press", target: describe(event.target), key: event.key });
|
|
327
|
+
});
|
|
328
|
+
root.__xopcRecorder = { recordingId: input.recordingId, generation: input.generation, stop: async () => {
|
|
329
|
+
for (const [type, handler] of listeners) (type === "pagehide" ? root : doc).removeEventListener(type, handler, true);
|
|
330
|
+
for (const element of dirty) flush(element);
|
|
331
|
+
if (!ended) {
|
|
332
|
+
emit({ action: "checkpoint" });
|
|
333
|
+
ended = true;
|
|
334
|
+
}
|
|
335
|
+
await Promise.all([...pending]);
|
|
336
|
+
if (failed) throw new Error("Recording events could not be saved.");
|
|
337
|
+
} };
|
|
338
|
+
emit({ action: "navigate" });
|
|
339
|
+
return { recording: true, documentId };
|
|
340
|
+
}
|
|
2
341
|
|
|
3
342
|
const BROWSER_EXTENSION_PROTOCOL_VERSION = 5;
|
|
4
343
|
const BROWSER_CONTROL_ENDPOINT_TOOL_NAME = "browser.control";
|
|
@@ -44,6 +383,285 @@ const BROWSER_CONTROL_ENDPOINT_DESCRIPTOR = {
|
|
|
44
383
|
idempotent: false,
|
|
45
384
|
resultKinds: ["json"]
|
|
46
385
|
};
|
|
386
|
+
const BROWSER_RECORDING_ENDPOINT_INPUT_SCHEMA = {
|
|
387
|
+
type: "object",
|
|
388
|
+
additionalProperties: false,
|
|
389
|
+
required: ["operation"],
|
|
390
|
+
properties: { operation: { type: "string", enum: ["start", "pause", "resume", "finish", "status"] } }
|
|
391
|
+
};
|
|
392
|
+
const BROWSER_RECORDING_ENDPOINT_DESCRIPTOR = {
|
|
393
|
+
...BROWSER_CONTROL_ENDPOINT_DESCRIPTOR,
|
|
394
|
+
name: "browser.recording",
|
|
395
|
+
title: "Browser workflow recording",
|
|
396
|
+
description: "Record a workflow in the active browser tab and save it as an automation.",
|
|
397
|
+
inputSchema: BROWSER_RECORDING_ENDPOINT_INPUT_SCHEMA,
|
|
398
|
+
maxConcurrency: 1
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
function open() {
|
|
402
|
+
return new Promise((resolve, reject) => {
|
|
403
|
+
const request2 = indexedDB.open("xopc-browser-recordings", 1);
|
|
404
|
+
request2.onupgradeneeded = () => {
|
|
405
|
+
request2.result.createObjectStore("sessions", { keyPath: "id" });
|
|
406
|
+
request2.result.createObjectStore("events", { keyPath: ["recordingId", "seq"] });
|
|
407
|
+
};
|
|
408
|
+
request2.onsuccess = () => resolve(request2.result);
|
|
409
|
+
request2.onerror = () => reject(request2.error);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function request(value) {
|
|
413
|
+
return new Promise((resolve, reject) => {
|
|
414
|
+
value.onsuccess = () => resolve(value.result);
|
|
415
|
+
value.onerror = () => reject(value.error);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async function transaction(mode, work) {
|
|
419
|
+
const db = await open();
|
|
420
|
+
const tx = db.transaction(["sessions", "events"], mode);
|
|
421
|
+
const done = new Promise((resolve, reject) => {
|
|
422
|
+
tx.oncomplete = () => resolve();
|
|
423
|
+
tx.onerror = () => reject(tx.error);
|
|
424
|
+
tx.onabort = () => reject(tx.error);
|
|
425
|
+
});
|
|
426
|
+
try {
|
|
427
|
+
const result = await work(tx);
|
|
428
|
+
await done;
|
|
429
|
+
return result;
|
|
430
|
+
} catch (error) {
|
|
431
|
+
try {
|
|
432
|
+
tx.abort();
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
await done.catch(() => {
|
|
436
|
+
});
|
|
437
|
+
throw error;
|
|
438
|
+
} finally {
|
|
439
|
+
db.close();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function recordings() {
|
|
443
|
+
return transaction("readonly", async (tx) => {
|
|
444
|
+
const rows = await request(tx.objectStore("sessions").getAll());
|
|
445
|
+
return rows.sort((a, b) => a.createdAtMs - b.createdAtMs);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function saveRecording(session) {
|
|
449
|
+
return transaction("readwrite", async (tx) => {
|
|
450
|
+
await request(tx.objectStore("sessions").put(session));
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
function recordingEvents(id, after = 0) {
|
|
454
|
+
return transaction("readonly", async (tx) => {
|
|
455
|
+
const rows = await request(tx.objectStore("events").getAll(IDBKeyRange.bound([id, after + 1], [id, Number.MAX_SAFE_INTEGER]), 100));
|
|
456
|
+
return rows.map(({ recordingId: _recordingId, ...event }) => event);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function appendRecordingEvent(id, generation, tabId, event) {
|
|
460
|
+
return transaction("readwrite", async (tx) => {
|
|
461
|
+
const sessions = tx.objectStore("sessions");
|
|
462
|
+
const session = await request(sessions.get(id));
|
|
463
|
+
if (!session || session.state !== "recording" || session.generation !== generation || session.tabId !== tabId) throw new Error("Recording is no longer active.");
|
|
464
|
+
if (session.count >= 1e4) throw new Error("Recording event limit reached. Finish this recording.");
|
|
465
|
+
const bytes = session.bytes + new TextEncoder().encode(JSON.stringify(event)).length;
|
|
466
|
+
if (bytes > 20 * 1024 * 1024) throw new Error("Recording storage limit reached.");
|
|
467
|
+
const seq = session.count + 1;
|
|
468
|
+
await request(tx.objectStore("events").add({ ...event, recordingId: id, seq }));
|
|
469
|
+
await request(sessions.put({ ...session, count: seq, bytes }));
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
function deleteRecordingEvents(id, through) {
|
|
473
|
+
return transaction("readwrite", async (tx) => {
|
|
474
|
+
await request(tx.objectStore("events").delete(IDBKeyRange.bound([id, 1], [id, through])));
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
function patchRecording(id, patch) {
|
|
478
|
+
return transaction("readwrite", async (tx) => {
|
|
479
|
+
const store = tx.objectStore("sessions");
|
|
480
|
+
const current = await request(store.get(id));
|
|
481
|
+
if (!current) throw new Error("Recording not found.");
|
|
482
|
+
await request(store.put({ ...current, ...patch }));
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let uploadTask;
|
|
487
|
+
let resyncRequested = false;
|
|
488
|
+
let controlTask;
|
|
489
|
+
let claim;
|
|
490
|
+
function setRecordingClaim(value) {
|
|
491
|
+
claim = value;
|
|
492
|
+
void syncRecordings();
|
|
493
|
+
}
|
|
494
|
+
async function inject(tabId, input) {
|
|
495
|
+
const result = await chrome.scripting.executeScript({ target: { tabId }, func: browserDom, args: [input] });
|
|
496
|
+
return result[0]?.result;
|
|
497
|
+
}
|
|
498
|
+
async function recordingCommand(operation, tabId) {
|
|
499
|
+
if (operation === "status") return recordings();
|
|
500
|
+
if (controlTask) throw new Error("A recording action is already in progress.");
|
|
501
|
+
controlTask = control(operation, tabId);
|
|
502
|
+
try {
|
|
503
|
+
return await controlTask;
|
|
504
|
+
} finally {
|
|
505
|
+
controlTask = void 0;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function control(operation, tabId) {
|
|
509
|
+
const all = await recordings();
|
|
510
|
+
const active = all.find((item) => item.state === "recording" || item.state === "paused");
|
|
511
|
+
if (operation === "start") {
|
|
512
|
+
if (active) return active;
|
|
513
|
+
const profile = await readProfile();
|
|
514
|
+
if (!profile) throw new Error("Connect to your Gateway first.");
|
|
515
|
+
if (tabId !== void 0 && (!Number.isInteger(tabId) || tabId <= 0)) throw new Error("Invalid recording tab.");
|
|
516
|
+
const tab = tabId !== void 0 ? await chrome.tabs.get(tabId) : (await chrome.tabs.query({ active: true, lastFocusedWindow: true }))[0];
|
|
517
|
+
if (!tab?.id || !tab.url?.match(/^https?:\/\//)) throw new Error("RECORDING_PAGE_UNAVAILABLE");
|
|
518
|
+
if ([...automationSessions.values()].some((session3) => session3.tabIds.includes(tab.id))) throw new Error("This page belongs to a running browser task. Select your workflow page to record.");
|
|
519
|
+
const session2 = {
|
|
520
|
+
id: crypto.randomUUID(),
|
|
521
|
+
state: "recording",
|
|
522
|
+
tabId: tab.id,
|
|
523
|
+
gatewayId: profile.gatewayId,
|
|
524
|
+
deviceId: profile.deviceId,
|
|
525
|
+
generation: 1,
|
|
526
|
+
count: 0,
|
|
527
|
+
ack: 0,
|
|
528
|
+
bytes: 0,
|
|
529
|
+
createdAtMs: Date.now()
|
|
530
|
+
};
|
|
531
|
+
await saveRecording(session2);
|
|
532
|
+
try {
|
|
533
|
+
await inject(tab.id, { operation: "record", recordingId: session2.id, generation: session2.generation });
|
|
534
|
+
} catch (error) {
|
|
535
|
+
await patchRecording(session2.id, { state: "interrupted", error: String(error) });
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
await chrome.action.setBadgeText({ text: "REC" });
|
|
539
|
+
return session2;
|
|
540
|
+
}
|
|
541
|
+
if (!active) throw new Error("There is no active recording.");
|
|
542
|
+
if (operation === "resume") {
|
|
543
|
+
if (active.state !== "paused") return active;
|
|
544
|
+
const session2 = { ...active, state: "recording", generation: active.generation + 1 };
|
|
545
|
+
await patchRecording(session2.id, { state: session2.state, generation: session2.generation });
|
|
546
|
+
try {
|
|
547
|
+
await inject(session2.tabId, { operation: "record", recordingId: session2.id, generation: session2.generation });
|
|
548
|
+
} catch (error) {
|
|
549
|
+
await patchRecording(session2.id, { state: "interrupted", error: String(error) });
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
await chrome.action.setBadgeText({ text: "REC" });
|
|
553
|
+
return session2;
|
|
554
|
+
}
|
|
555
|
+
if (operation !== "pause" && operation !== "finish") throw new Error("Unknown recording operation.");
|
|
556
|
+
try {
|
|
557
|
+
await inject(active.tabId, { operation: "stop" });
|
|
558
|
+
} catch (error) {
|
|
559
|
+
const current2 = (await recordings()).find((item) => item.id === active.id);
|
|
560
|
+
await patchRecording(current2.id, { state: "interrupted", error: `Recording may be incomplete: ${String(error)}` });
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
const current = (await recordings()).find((item) => item.id === active.id);
|
|
564
|
+
const session = { ...current, state: operation === "pause" ? "paused" : "stopped" };
|
|
565
|
+
await patchRecording(session.id, { state: session.state });
|
|
566
|
+
await chrome.action.setBadgeText({ text: operation === "pause" ? "Ⅱ" : "" });
|
|
567
|
+
void syncRecordings();
|
|
568
|
+
return session;
|
|
569
|
+
}
|
|
570
|
+
async function acceptRecordingEvent(message, sender) {
|
|
571
|
+
if (sender.id !== chrome.runtime.id || !sender.tab?.id || sender.frameId !== 0) throw new Error("Invalid recording source.");
|
|
572
|
+
const event = message.event;
|
|
573
|
+
if (!event || JSON.stringify(event).length > 16384 || !["navigate", "click", "fill", "select", "check", "press", "checkpoint", "unsupported"].includes(event.action)) throw new Error("Invalid recording event.");
|
|
574
|
+
try {
|
|
575
|
+
await appendRecordingEvent(message.recordingId, message.generation, sender.tab.id, event);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (/limit reached/.test(String(error))) {
|
|
578
|
+
await patchRecording(message.recordingId, { state: "interrupted", error: String(error) });
|
|
579
|
+
void inject(sender.tab.id, { operation: "stop" }).catch(() => void 0);
|
|
580
|
+
await chrome.action.setBadgeText({ text: "" });
|
|
581
|
+
}
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
void syncRecordings();
|
|
585
|
+
}
|
|
586
|
+
function syncRecordings() {
|
|
587
|
+
if (uploadTask) {
|
|
588
|
+
resyncRequested = true;
|
|
589
|
+
return uploadTask;
|
|
590
|
+
}
|
|
591
|
+
uploadTask = upload().catch(() => void 0).finally(() => {
|
|
592
|
+
uploadTask = void 0;
|
|
593
|
+
if (resyncRequested) {
|
|
594
|
+
resyncRequested = false;
|
|
595
|
+
void syncRecordings();
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
return uploadTask;
|
|
599
|
+
}
|
|
600
|
+
async function upload() {
|
|
601
|
+
if (!claim) return;
|
|
602
|
+
const profile = await readProfile();
|
|
603
|
+
if (!profile) return;
|
|
604
|
+
for (const session of await recordings()) {
|
|
605
|
+
if (session.gatewayId !== profile.gatewayId || session.deviceId !== profile.deviceId || session.automationId) continue;
|
|
606
|
+
try {
|
|
607
|
+
const post = async (operation, body) => {
|
|
608
|
+
const response = await gatewayFetch(`/api/browser/recordings/${session.id}/${operation}`, {
|
|
609
|
+
method: "POST",
|
|
610
|
+
headers: { "Content-Type": "application/json", "x-endpoint-id": claim.endpointId, "x-endpoint-claim": claim.token },
|
|
611
|
+
body: JSON.stringify(body)
|
|
612
|
+
}, profile);
|
|
613
|
+
const data = await response.json();
|
|
614
|
+
if (!response.ok) throw new Error(data.error || "Recording could not be synchronized.");
|
|
615
|
+
return data;
|
|
616
|
+
};
|
|
617
|
+
let ack = session.ack;
|
|
618
|
+
let events = await recordingEvents(session.id, ack);
|
|
619
|
+
while (events.length) {
|
|
620
|
+
while (JSON.stringify({ events }).length > 24e4) events.pop();
|
|
621
|
+
const data = await post("events", { events });
|
|
622
|
+
if (data.ackThrough <= ack) throw new Error("Recording synchronization did not advance.");
|
|
623
|
+
ack = data.ackThrough;
|
|
624
|
+
const current2 = (await recordings()).find((item) => item.id === session.id);
|
|
625
|
+
await patchRecording(current2.id, { ack, error: void 0 });
|
|
626
|
+
events = await recordingEvents(session.id, ack);
|
|
627
|
+
}
|
|
628
|
+
const current = (await recordings()).find((item) => item.id === session.id);
|
|
629
|
+
if (current.state === "stopped" && current.count === ack) {
|
|
630
|
+
const data = await post("finish", { finalSeq: current.count });
|
|
631
|
+
await patchRecording(current.id, { ack, automationId: data.automation.id, error: void 0 });
|
|
632
|
+
await deleteRecordingEvents(session.id, current.count);
|
|
633
|
+
}
|
|
634
|
+
} catch (error) {
|
|
635
|
+
const current = (await recordings()).find((item) => item.id === session.id);
|
|
636
|
+
await patchRecording(current.id, { error: error instanceof Error ? error.message : String(error) });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
chrome.tabs.onUpdated.addListener((tabId, info) => {
|
|
641
|
+
if (info.status !== "complete") return;
|
|
642
|
+
void recordings().then(async (all) => {
|
|
643
|
+
const session = all.find((item) => item.tabId === tabId && item.state === "recording");
|
|
644
|
+
if (!session) return;
|
|
645
|
+
try {
|
|
646
|
+
await patchRecording(session.id, { generation: session.generation + 1 });
|
|
647
|
+
await inject(tabId, { operation: "record", recordingId: session.id, generation: session.generation + 1 });
|
|
648
|
+
} catch (error) {
|
|
649
|
+
await patchRecording(session.id, { state: "interrupted", error: String(error) });
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
654
|
+
void recordings().then(async (all) => {
|
|
655
|
+
for (const session of all.filter((item) => item.tabId === tabId && ["recording", "paused"].includes(item.state))) {
|
|
656
|
+
await patchRecording(session.id, { state: "interrupted", error: "The recorded tab was closed. The partial recording is saved locally." });
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
661
|
+
void recordings().then(async (all) => {
|
|
662
|
+
for (const session of all.filter((item) => item.state === "recording")) await patchRecording(session.id, { state: "interrupted", error: "Browser restarted. The recording was stopped." });
|
|
663
|
+
});
|
|
664
|
+
});
|
|
47
665
|
|
|
48
666
|
/**
|
|
49
667
|
* Utilities for hex, bytes, CSPRNG.
|
|
@@ -594,31 +1212,7 @@ class EndpointToolHostController {
|
|
|
594
1212
|
}
|
|
595
1213
|
}
|
|
596
1214
|
|
|
597
|
-
const
|
|
598
|
-
debug: "color: #888",
|
|
599
|
-
info: "color: #3b82f6",
|
|
600
|
-
warn: "color: #f59e0b",
|
|
601
|
-
error: "color: #ef4444"
|
|
602
|
-
};
|
|
603
|
-
function createLogger(prefix) {
|
|
604
|
-
function log(level, message, data) {
|
|
605
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
606
|
-
const formatted = `[${timestamp}] [${prefix}] ${message}`;
|
|
607
|
-
if (data && Object.keys(data).length > 0) {
|
|
608
|
-
console[level](`%c${formatted}`, LOG_COLORS[level], data);
|
|
609
|
-
} else {
|
|
610
|
-
console[level](`%c${formatted}`, LOG_COLORS[level]);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return {
|
|
614
|
-
debug: (msg, data) => log("debug", msg, data),
|
|
615
|
-
info: (msg, data) => log("info", msg, data),
|
|
616
|
-
warn: (msg, data) => log("warn", msg, data),
|
|
617
|
-
error: (msg, data) => log("error", msg, data)
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const log$2 = createLogger("CDP");
|
|
1215
|
+
const log$1 = createLogger("CDP");
|
|
622
1216
|
const attached = /* @__PURE__ */ new Set();
|
|
623
1217
|
function isDebuggableUrl(url) {
|
|
624
1218
|
if (!url) return true;
|
|
@@ -660,7 +1254,7 @@ async function ensureAttached(tabId) {
|
|
|
660
1254
|
attached.add(tabId);
|
|
661
1255
|
await chrome.debugger.sendCommand({ tabId }, "Runtime.enable", {});
|
|
662
1256
|
await chrome.debugger.sendCommand({ tabId }, "Page.enable", {});
|
|
663
|
-
log$
|
|
1257
|
+
log$1.debug("Attached to tab", { tabId, attempt });
|
|
664
1258
|
return;
|
|
665
1259
|
} catch (e) {
|
|
666
1260
|
lastError = e instanceof Error ? e.message : String(e);
|
|
@@ -701,151 +1295,8 @@ chrome.debugger.onDetach.addListener((source) => {
|
|
|
701
1295
|
if (source.tabId) attached.delete(source.tabId);
|
|
702
1296
|
});
|
|
703
1297
|
|
|
704
|
-
const log$1 = createLogger("SessionManager");
|
|
705
|
-
const XOPC_TAB_GROUP_TITLE = "xopc";
|
|
706
|
-
const automationSessions = /* @__PURE__ */ new Map();
|
|
707
|
-
const WINDOW_IDLE_TIMEOUT = 12e4;
|
|
708
|
-
const DEFAULT_WORKSPACE = "default";
|
|
709
|
-
function resetWindowIdleTimer(workspace = DEFAULT_WORKSPACE) {
|
|
710
|
-
const session = automationSessions.get(workspace);
|
|
711
|
-
if (!session) return;
|
|
712
|
-
if (session.idleTimer) clearTimeout(session.idleTimer);
|
|
713
|
-
session.idleDeadlineAt = Date.now() + WINDOW_IDLE_TIMEOUT;
|
|
714
|
-
session.idleTimer = setTimeout(async () => {
|
|
715
|
-
const current = automationSessions.get(workspace);
|
|
716
|
-
if (!current) return;
|
|
717
|
-
try {
|
|
718
|
-
const tabs = await chrome.tabs.query({ groupId: current.groupId });
|
|
719
|
-
for (const t of tabs) {
|
|
720
|
-
if (t.id != null) await chrome.tabs.remove(t.id).catch(() => {
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
} catch {
|
|
724
|
-
}
|
|
725
|
-
automationSessions.delete(workspace);
|
|
726
|
-
log$1.info("Automation tab group cleared due to idle timeout", { workspace });
|
|
727
|
-
}, WINDOW_IDLE_TIMEOUT);
|
|
728
|
-
}
|
|
729
|
-
async function waitForTabLoad(tabId, timeoutMs = 3e4) {
|
|
730
|
-
return new Promise((resolve) => {
|
|
731
|
-
const timeout = setTimeout(resolve, timeoutMs);
|
|
732
|
-
const listener = (id, info) => {
|
|
733
|
-
if (id === tabId && info.status === "complete") {
|
|
734
|
-
clearTimeout(timeout);
|
|
735
|
-
chrome.tabs.onUpdated.removeListener(listener);
|
|
736
|
-
resolve();
|
|
737
|
-
}
|
|
738
|
-
};
|
|
739
|
-
chrome.tabs.onUpdated.addListener(listener);
|
|
740
|
-
chrome.tabs.get(tabId).then((tab) => {
|
|
741
|
-
if (tab.status === "complete") {
|
|
742
|
-
clearTimeout(timeout);
|
|
743
|
-
chrome.tabs.onUpdated.removeListener(listener);
|
|
744
|
-
resolve();
|
|
745
|
-
}
|
|
746
|
-
}).catch(() => {
|
|
747
|
-
clearTimeout(timeout);
|
|
748
|
-
chrome.tabs.onUpdated.removeListener(listener);
|
|
749
|
-
resolve();
|
|
750
|
-
});
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
async function resolveTargetWindowId() {
|
|
754
|
-
const w = await chrome.windows.getLastFocused({ windowTypes: ["normal"] });
|
|
755
|
-
if (w?.id != null) return w.id;
|
|
756
|
-
const all = await chrome.windows.getAll({ windowTypes: ["normal"] });
|
|
757
|
-
const first = all.find((x) => x.id != null);
|
|
758
|
-
if (first?.id != null) return first.id;
|
|
759
|
-
throw new Error("No normal browser window — open a Chrome window, then run automation again.");
|
|
760
|
-
}
|
|
761
|
-
async function getOrCreateAutomationWindow(workspace = DEFAULT_WORKSPACE) {
|
|
762
|
-
const existing = automationSessions.get(workspace);
|
|
763
|
-
if (existing) {
|
|
764
|
-
try {
|
|
765
|
-
await chrome.tabGroups.get(existing.groupId);
|
|
766
|
-
const tabsInGroup = await chrome.tabs.query({ groupId: existing.groupId });
|
|
767
|
-
if (tabsInGroup.length === 0) throw new Error("empty group");
|
|
768
|
-
existing.tabIds = tabsInGroup.map((t) => t.id).filter(Boolean);
|
|
769
|
-
const active = tabsInGroup.find((t) => t.active);
|
|
770
|
-
existing.activeTabId = active?.id ?? tabsInGroup[0]?.id ?? null;
|
|
771
|
-
resetWindowIdleTimer(workspace);
|
|
772
|
-
return existing;
|
|
773
|
-
} catch {
|
|
774
|
-
automationSessions.delete(workspace);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
const windowId = await resolveTargetWindowId();
|
|
778
|
-
const tab = await chrome.tabs.create({
|
|
779
|
-
windowId,
|
|
780
|
-
url: "about:blank",
|
|
781
|
-
active: false
|
|
782
|
-
});
|
|
783
|
-
if (tab.id == null) throw new Error("Failed to create automation tab");
|
|
784
|
-
const groupId = await chrome.tabs.group({
|
|
785
|
-
tabIds: [tab.id],
|
|
786
|
-
createProperties: { windowId }
|
|
787
|
-
});
|
|
788
|
-
await chrome.tabGroups.update(groupId, {
|
|
789
|
-
title: XOPC_TAB_GROUP_TITLE,
|
|
790
|
-
collapsed: false,
|
|
791
|
-
color: "blue"
|
|
792
|
-
});
|
|
793
|
-
const session = {
|
|
794
|
-
windowId,
|
|
795
|
-
groupId,
|
|
796
|
-
tabIds: [tab.id],
|
|
797
|
-
activeTabId: tab.id,
|
|
798
|
-
idleTimer: null,
|
|
799
|
-
idleDeadlineAt: 0,
|
|
800
|
-
createdAt: Date.now()
|
|
801
|
-
};
|
|
802
|
-
automationSessions.set(workspace, session);
|
|
803
|
-
resetWindowIdleTimer(workspace);
|
|
804
|
-
log$1.info("Created automation tab group", { workspace, windowId, groupId, tabId: tab.id });
|
|
805
|
-
return session;
|
|
806
|
-
}
|
|
807
|
-
async function getActiveTabId(workspace = DEFAULT_WORKSPACE) {
|
|
808
|
-
const session = await getOrCreateAutomationWindow(workspace);
|
|
809
|
-
if (session.activeTabId != null) {
|
|
810
|
-
const ok = await chrome.tabs.get(session.activeTabId).catch(() => null);
|
|
811
|
-
if (ok) return session.activeTabId;
|
|
812
|
-
}
|
|
813
|
-
const tabs = await chrome.tabs.query({ groupId: session.groupId });
|
|
814
|
-
const tabId = tabs.find((t) => t.active)?.id ?? tabs[0]?.id;
|
|
815
|
-
if (tabId == null) throw new Error("No tab in xopc automation group");
|
|
816
|
-
session.activeTabId = tabId;
|
|
817
|
-
return tabId;
|
|
818
|
-
}
|
|
819
|
-
async function addTabToAutomationGroup(url, workspace = DEFAULT_WORKSPACE) {
|
|
820
|
-
const session = await getOrCreateAutomationWindow(workspace);
|
|
821
|
-
const tab = await chrome.tabs.create({
|
|
822
|
-
windowId: session.windowId,
|
|
823
|
-
url,
|
|
824
|
-
active: false
|
|
825
|
-
});
|
|
826
|
-
if (tab.id == null) throw new Error("Failed to create tab");
|
|
827
|
-
const inGroup = await chrome.tabs.query({ groupId: session.groupId });
|
|
828
|
-
const mergedIds = [...inGroup.map((t) => t.id).filter(Boolean), tab.id];
|
|
829
|
-
await chrome.tabs.group({ tabIds: mergedIds, groupId: session.groupId });
|
|
830
|
-
session.activeTabId = tab.id;
|
|
831
|
-
session.tabIds = mergedIds;
|
|
832
|
-
resetWindowIdleTimer(workspace);
|
|
833
|
-
return { tabId: tab.id, session };
|
|
834
|
-
}
|
|
835
|
-
async function closeSession(workspace = DEFAULT_WORKSPACE) {
|
|
836
|
-
const session = automationSessions.get(workspace);
|
|
837
|
-
if (!session) return;
|
|
838
|
-
if (session.idleTimer) clearTimeout(session.idleTimer);
|
|
839
|
-
const tabs = await chrome.tabs.query({ groupId: session.groupId }).catch(() => []);
|
|
840
|
-
for (const t of tabs) {
|
|
841
|
-
if (t.id != null) await chrome.tabs.remove(t.id).catch(() => {
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
automationSessions.delete(workspace);
|
|
845
|
-
log$1.info("Closed automation session", { workspace });
|
|
846
|
-
}
|
|
847
|
-
|
|
848
1298
|
const observations = /* @__PURE__ */ new Map();
|
|
1299
|
+
const activeCommands = /* @__PURE__ */ new Set();
|
|
849
1300
|
async function markerDigest(tabId) {
|
|
850
1301
|
const marker = await evaluate(tabId, `({ url: location.href, timeOrigin: performance.timeOrigin })`);
|
|
851
1302
|
const url = new URL(marker.url);
|
|
@@ -867,9 +1318,9 @@ async function resolveAttachedTarget(input) {
|
|
|
867
1318
|
if (input.action === "navigate" || input.action === "tabs" || input.action === "sequence") {
|
|
868
1319
|
return fail("INVALID_INPUT", `${input.action} is unavailable for an attached tab.`);
|
|
869
1320
|
}
|
|
870
|
-
const needsAct = !["observe", "wait", "close"].includes(input.action);
|
|
1321
|
+
const needsAct = !["observe", "resolve", "wait", "close"].includes(input.action);
|
|
871
1322
|
if (needsAct && binding.mode !== "act") {
|
|
872
|
-
return fail("
|
|
1323
|
+
return fail("INVALID_INPUT", "This tab is attached in read-only mode. Enable Control tab in the side panel.");
|
|
873
1324
|
}
|
|
874
1325
|
const tabId = Number(binding.tabId);
|
|
875
1326
|
const tab = Number.isInteger(tabId) ? await chrome.tabs.get(tabId).catch(() => null) : null;
|
|
@@ -890,10 +1341,15 @@ async function resolveAttachedTarget(input) {
|
|
|
890
1341
|
}
|
|
891
1342
|
return { binding, tabId, stateKey: `attached:${binding.id}` };
|
|
892
1343
|
}
|
|
893
|
-
async function executeBrowserCommand(command) {
|
|
1344
|
+
async function executeBrowserCommand(command, signal) {
|
|
894
1345
|
const startedAt = Date.now();
|
|
1346
|
+
const lockKey = command.input.target?.kind === "attached_tab" ? command.input.target.bindingId : command.input.sessionId;
|
|
1347
|
+
if (activeCommands.has(lockKey)) return { id: command.id, connectionId: command.connectionId, result: fail("INVALID_INPUT", "This browser page is already executing an action.") };
|
|
1348
|
+
activeCommands.add(lockKey);
|
|
1349
|
+
holdAutomationSession(command.input.sessionId);
|
|
895
1350
|
try {
|
|
896
|
-
|
|
1351
|
+
if (signal?.aborted) return { id: command.id, connectionId: command.connectionId, result: fail("ABORTED", "Operation was cancelled.") };
|
|
1352
|
+
const result = await execute(command.input, command.timeoutMs, command.visualFallback, startedAt, signal);
|
|
897
1353
|
return { id: command.id, connectionId: command.connectionId, result };
|
|
898
1354
|
} catch (error) {
|
|
899
1355
|
return {
|
|
@@ -902,18 +1358,26 @@ async function executeBrowserCommand(command) {
|
|
|
902
1358
|
result: {
|
|
903
1359
|
ok: false,
|
|
904
1360
|
error: {
|
|
905
|
-
code: /timeout/i.test(String(error)) ? "TIMEOUT" : "DRIVER_UNAVAILABLE",
|
|
1361
|
+
code: signal?.aborted ? "ABORTED" : /timeout/i.test(String(error)) ? "TIMEOUT" : "DRIVER_UNAVAILABLE",
|
|
906
1362
|
message: error instanceof Error ? error.message : String(error)
|
|
907
1363
|
}
|
|
908
1364
|
}
|
|
909
1365
|
};
|
|
1366
|
+
} finally {
|
|
1367
|
+
activeCommands.delete(lockKey);
|
|
1368
|
+
releaseAutomationSession(command.input.sessionId);
|
|
910
1369
|
}
|
|
911
1370
|
}
|
|
912
|
-
async function execute(input, timeoutMs, visualFallback, startedAt) {
|
|
1371
|
+
async function execute(input, timeoutMs, visualFallback, startedAt, signal) {
|
|
913
1372
|
const sessionId = input.sessionId;
|
|
914
1373
|
if (!sessionId) return fail("INVALID_INPUT", "Extension actions require a session id.");
|
|
915
1374
|
const attached = await resolveAttachedTarget(input);
|
|
916
1375
|
if (attached && !("binding" in attached)) return attached;
|
|
1376
|
+
if (attached && !["observe", "resolve", "wait", "close"].includes(input.action)) {
|
|
1377
|
+
const activeRecording = (await recordings()).some((item) => item.tabId === attached.tabId && ["recording", "paused"].includes(item.state));
|
|
1378
|
+
if (activeRecording) return fail("INVALID_INPUT", "This page is being recorded. Finish recording before running an automation.");
|
|
1379
|
+
}
|
|
1380
|
+
if (signal?.aborted) return fail("ABORTED", "Operation was cancelled.");
|
|
917
1381
|
const stateKey = attached?.stateKey ?? sessionId;
|
|
918
1382
|
if (input.action === "close") {
|
|
919
1383
|
if (!attached) await closeSession(sessionId);
|
|
@@ -935,6 +1399,15 @@ async function execute(input, timeoutMs, visualFallback, startedAt) {
|
|
|
935
1399
|
}
|
|
936
1400
|
if (input.action === "tabs") return tabs(sessionId, input, startedAt, timeoutMs);
|
|
937
1401
|
if (input.action === "sequence") return fail("INVALID_INPUT", "Sequences are expanded by the browser runtime.");
|
|
1402
|
+
if (input.action === "resolve") {
|
|
1403
|
+
const tabId2 = attached?.tabId ?? await getActiveTabId(sessionId);
|
|
1404
|
+
const resolved = await evaluate(tabId2, `(${browserDom.toString()})(${JSON.stringify({ operation: "resolve", target: input.semanticTarget })})`);
|
|
1405
|
+
if (resolved.matches !== 1 || !resolved.node) return fail("TARGET_NOT_FOUND", `Semantic target must match exactly one element; matched ${resolved.matches}.`);
|
|
1406
|
+
const observation2 = await observe(sessionId, "never", attached);
|
|
1407
|
+
observation2.nodes = [resolved.node];
|
|
1408
|
+
observations.get(stateKey).nodes = observation2.nodes;
|
|
1409
|
+
return success("resolve", "read", startedAt, observation2);
|
|
1410
|
+
}
|
|
938
1411
|
const current = observations.get(stateKey);
|
|
939
1412
|
if (!current || current.revision !== input.revision) {
|
|
940
1413
|
return fail("STALE_OBSERVATION", "The page changed. Observe it again.", await observe(sessionId, "never", attached));
|
|
@@ -965,6 +1438,13 @@ async function execute(input, timeoutMs, visualFallback, startedAt) {
|
|
|
965
1438
|
}`, input.value);
|
|
966
1439
|
if (!changed) return fail("TARGET_NOT_FOUND", `Target ${input.ref} was not found.`);
|
|
967
1440
|
if (input.submit) await pressKey(tabId, "Enter");
|
|
1441
|
+
} else if (input.action === "check") {
|
|
1442
|
+
const changed = await evaluateRef(tabId, input.ref, `(element, checked) => {
|
|
1443
|
+
if (!['checkbox', 'radio'].includes(element.type)) return false;
|
|
1444
|
+
if (element.checked !== checked) element.click();
|
|
1445
|
+
return element.checked === checked;
|
|
1446
|
+
}`, input.checked);
|
|
1447
|
+
if (!changed) return fail("INVALID_INPUT", "The checkbox could not be set to the requested state.");
|
|
968
1448
|
} else if (input.action === "select") {
|
|
969
1449
|
const changed = await evaluateRef(tabId, input.ref, `(element, value) => {
|
|
970
1450
|
element.value = value;
|
|
@@ -983,7 +1463,7 @@ async function execute(input, timeoutMs, visualFallback, startedAt) {
|
|
|
983
1463
|
await dispatchInput(tabId, "dispatchMouseEvent", { type: "mouseWheel", x: 640, y: 360, deltaX: 0, deltaY: input.deltaY });
|
|
984
1464
|
}
|
|
985
1465
|
} else if (input.action === "wait") {
|
|
986
|
-
await wait(tabId, input, timeoutMs);
|
|
1466
|
+
await wait(tabId, input, timeoutMs, signal);
|
|
987
1467
|
} else if (input.action === "upload") {
|
|
988
1468
|
const document = await sendCommand(tabId, "DOM.getDocument", { depth: 1 });
|
|
989
1469
|
const query = await sendCommand(tabId, "DOM.querySelectorAll", {
|
|
@@ -1089,10 +1569,11 @@ async function tabs(sessionId, input, startedAt, timeoutMs) {
|
|
|
1089
1569
|
}
|
|
1090
1570
|
};
|
|
1091
1571
|
}
|
|
1092
|
-
async function wait(tabId, input, defaultTimeout) {
|
|
1572
|
+
async function wait(tabId, input, defaultTimeout, signal) {
|
|
1093
1573
|
const timeout = input.timeoutMs ?? defaultTimeout;
|
|
1094
1574
|
const deadline = Date.now() + timeout;
|
|
1095
1575
|
while (Date.now() < deadline) {
|
|
1576
|
+
if (signal?.aborted) throw new Error("Operation aborted.");
|
|
1096
1577
|
if (input.condition === "page_idle") {
|
|
1097
1578
|
const tab = await chrome.tabs.get(tabId);
|
|
1098
1579
|
if (tab.status === "complete") return;
|
|
@@ -1228,7 +1709,7 @@ function scheduleReconnectAlarm() {
|
|
|
1228
1709
|
}
|
|
1229
1710
|
const browserToolRegistry = new EndpointToolRegistry([{
|
|
1230
1711
|
descriptor: BROWSER_CONTROL_ENDPOINT_DESCRIPTOR,
|
|
1231
|
-
execute: async (args) => {
|
|
1712
|
+
execute: async (args, context) => {
|
|
1232
1713
|
const input = args.input;
|
|
1233
1714
|
if (!input || typeof input !== "object") throw new TypeError("Browser control input is required");
|
|
1234
1715
|
const result = await executeBrowserCommand({
|
|
@@ -1238,9 +1719,12 @@ const browserToolRegistry = new EndpointToolRegistry([{
|
|
|
1238
1719
|
input,
|
|
1239
1720
|
timeoutMs: typeof args.timeoutMs === "number" ? args.timeoutMs : 3e4,
|
|
1240
1721
|
visualFallback: args.visualFallback !== false
|
|
1241
|
-
});
|
|
1722
|
+
}, context.signal);
|
|
1242
1723
|
return { content: [{ type: "json", value: fitBrowserControlResultToFrame(result.result) }] };
|
|
1243
1724
|
}
|
|
1725
|
+
}, {
|
|
1726
|
+
descriptor: BROWSER_RECORDING_ENDPOINT_DESCRIPTOR,
|
|
1727
|
+
execute: async (args) => ({ content: [{ type: "json", value: await recordingCommand(String(args.operation)) }] })
|
|
1244
1728
|
}]);
|
|
1245
1729
|
const endpointHost = new EndpointToolHostController({
|
|
1246
1730
|
registry: browserToolRegistry,
|
|
@@ -1300,6 +1784,7 @@ async function connect(generation) {
|
|
|
1300
1784
|
createHello: () => createBrowserEndpointHello(browserToolRegistry.descriptors()),
|
|
1301
1785
|
onReady: ({ endpointId, turnToken }) => {
|
|
1302
1786
|
endpointClaim = { endpointId, token: turnToken };
|
|
1787
|
+
setRecordingClaim(endpointClaim);
|
|
1303
1788
|
lastConnectionError = void 0;
|
|
1304
1789
|
clearReconnectAlarm();
|
|
1305
1790
|
endpointHost.connect((message) => client.sendEndpointMessage(message));
|
|
@@ -1311,6 +1796,7 @@ async function connect(generation) {
|
|
|
1311
1796
|
},
|
|
1312
1797
|
onDisconnected: () => {
|
|
1313
1798
|
endpointClaim = void 0;
|
|
1799
|
+
setRecordingClaim(void 0);
|
|
1314
1800
|
endpointHost.disconnect();
|
|
1315
1801
|
scheduleReconnectAlarm();
|
|
1316
1802
|
}
|
|
@@ -1339,7 +1825,15 @@ function connectWithLogging() {
|
|
|
1339
1825
|
});
|
|
1340
1826
|
}
|
|
1341
1827
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
1342
|
-
if (message.type === "browser/
|
|
1828
|
+
if (message.type === "browser/recording-event") {
|
|
1829
|
+
void acceptRecordingEvent(message, sender).then(() => sendResponse({ ok: true })).catch((error) => sendResponse({ ok: false, error: String(error) }));
|
|
1830
|
+
} else if (message.type === "browser/recording-command") {
|
|
1831
|
+
if (!sender.url?.startsWith(`chrome-extension://${chrome.runtime.id}/`) || sender.tab) {
|
|
1832
|
+
sendResponse({ ok: false, error: "Invalid recording control source." });
|
|
1833
|
+
return true;
|
|
1834
|
+
}
|
|
1835
|
+
void recordingCommand(message.operation ?? "status", message.tabId).then((value) => sendResponse({ ok: true, value })).catch((error) => sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
1836
|
+
} else if (message.type === "browser/get-status") {
|
|
1343
1837
|
sendResponse({ connected: Boolean(endpointClaim), transport: "gateway-realtime", error: lastConnectionError });
|
|
1344
1838
|
} else if (message.type === "browser/get-endpoint-claim") {
|
|
1345
1839
|
sendResponse({ claim: endpointClaim, error: lastConnectionError });
|
|
@@ -1402,3 +1896,7 @@ chrome.storage.onChanged.addListener((changes, area) => {
|
|
|
1402
1896
|
});
|
|
1403
1897
|
void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
|
1404
1898
|
connectWithLogging();
|
|
1899
|
+
chrome.alarms.create("xopc-recording-sync", { periodInMinutes: 0.5 });
|
|
1900
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
1901
|
+
if (alarm.name === "xopc-recording-sync") void syncRecordings();
|
|
1902
|
+
});
|