@tea-agent/loop-agent 0.43.0-next.9 → 0.43.0
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/AGENTS.md +1 -1
- package/CHANGELOG.md +31 -2309
- package/README.md +7 -0
- package/dist/application/dag/args.js +8 -0
- package/dist/application/task-lifecycle/advance.js +38 -7
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +2 -0
- package/dist/cli/program.js +3 -1
- package/dist/cli/update/init-surface-notifier.js +2 -0
- package/dist/cli/update/policy.js +2 -0
- package/dist/commands/client-recovery.js +20 -55
- package/dist/commands/cursor-prompt.js +2 -0
- package/dist/commands/dag-approve.js +2 -0
- package/dist/commands/dag-final-verification.js +2 -0
- package/dist/commands/dag-init-hybrid.js +2 -0
- package/dist/commands/dag-reconcile-tasks.js +2 -0
- package/dist/commands/dag-reject.js +2 -0
- package/dist/commands/dag-report.js +2 -0
- package/dist/commands/dag-rerun.js +2 -0
- package/dist/commands/dag-resume.js +2 -0
- package/dist/commands/dag-workflow-compile.js +2 -0
- package/dist/commands/dag-workflow-plan.js +2 -0
- package/dist/commands/dag-workflow-validate.js +2 -0
- package/dist/commands/delegate.js +2 -0
- package/dist/commands/examples.js +2 -0
- package/dist/commands/import-prd.js +2 -0
- package/dist/commands/init-model-catalog.js +2 -0
- package/dist/commands/init.js +19 -13
- package/dist/commands/instructions.js +2 -0
- package/dist/commands/knowledge.js +2 -0
- package/dist/commands/loop-benchmark.js +2 -0
- package/dist/commands/pi-prompt.js +2 -0
- package/dist/commands/pi-reuse-benchmark.js +2 -0
- package/dist/commands/promote-run.js +2 -0
- package/dist/commands/stats.js +1 -1
- package/dist/commands/study-init.js +2 -0
- package/dist/commands/task-advance.js +34 -0
- package/dist/commands/task-source-prepare.js +2 -0
- package/dist/commands/worktree-create.js +2 -0
- package/dist/commands/worktree-remove.js +2 -0
- package/dist/executors/dag-pi-executor.js +1650 -198
- package/dist/executors/pi-executor.js +85 -3
- package/dist/executors/pi-sdk-executor.js +18 -0
- package/dist/executors/shell-executor.js +93 -7
- package/dist/executors/shell-verification.js +3 -0
- package/dist/executors/shell-write-guard.js +27 -32
- package/dist/governance/exec-plans.js +6 -3
- package/dist/infrastructure/console/app-data.js +6 -0
- package/dist/shared/artifacts-core.js +2 -0
- package/dist/shared/backend-dogfood-preflight.js +47 -0
- package/dist/shared/dag-failure-category.js +3 -0
- package/dist/shared/git-progress.js +2 -0
- package/dist/shared/one-shot-prompt-args.js +2 -0
- package/dist/shared/operator/capabilities.js +180 -0
- package/dist/shared/package-metadata.js +6 -4
- package/dist/shared/pi-context-pressure/extension.js +26 -16
- package/dist/shared/pi-context-pressure/sift-bridge.js +1 -1
- package/dist/shared/pi-provider-recovery.js +182 -0
- package/dist/shared/pi-retry-settings.js +40 -0
- package/dist/shared/reference-context.js +2 -0
- package/dist/task/config-types.js +29 -0
- package/dist/task/contract/project.js +9 -0
- package/dist/task/contract/recover.js +6 -0
- package/dist/task/contract/schema.js +17 -0
- package/dist/task/source-prepare/artifact-meta.js +9 -2
- package/dist/task/source-prepare/build-draft.js +60 -0
- package/dist/task/source-prepare/fragment-inventory.js +4 -1
- package/dist/task/source-prepare/parse-intent.js +36 -11
- package/dist/task/task-demand-routing.js +5 -2
- package/dist/worker/console/chat/browser-automation.js +523 -0
- package/dist/worker/console/chat/browser-policy.js +6 -36
- package/dist/worker/console/chat/browser-routes.js +188 -56
- package/dist/worker/console/chat/explore-tools.js +5 -2
- package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +184 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +94 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
- package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
- package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
- package/dist/worker/console/chat/pi-runtime.js +208 -1138
- package/dist/worker/console/chat/routes.js +27 -7
- package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
- package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
- package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +1 -1
- package/dist/worker/console/chat/session-mode-view.js +5 -4
- package/dist/worker/console/chat/session-mode.js +18 -12
- package/dist/worker/console/chat/session-store.js +42 -9
- package/dist/worker/console/chat/tool-preview.js +115 -0
- package/dist/worker/console/chat/tools.js +10 -0
- package/dist/worker/console/chat/turn-order.js +13 -0
- package/dist/worker/console/chat/turn-process.js +32 -30
- package/dist/worker/console/console-update-and-init.js +1 -19
- package/dist/worker/console/console-update-runtime.js +5 -20
- package/dist/worker/console/operator-actions.js +63 -1
- package/dist/worker/console/prd-intake-bridge.js +54 -1
- package/dist/worker/console/routes.js +8 -0
- package/dist/worker/console/scheduled-goal-host.js +98 -0
- package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
- package/dist/worker/console/server.js +24 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D_fNbWc_.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
- package/dist/worker/console/static/assets/{arc-_HuqLW6m.js → arc-eoQiMvuk.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Djy2qY1m.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-bB-1TSyY.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5Hmghrz.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Duyl39oy.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-x6mdnSyf.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-WlBuFj7W.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CTF429HX.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dwd6r-SF.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C-Br82D1.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CuGp0DeE.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CJhFX6Lh.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DpGOa_jT.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DXJaTynd.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CRBZWjE4.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BdXcxtlb.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-07UKsOts.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-D38p9__S.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-nSiBNNjH.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BFOTot5D.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DypKaZ6k.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-B6yoOQeW.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BHYS6viX.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-D2VD5Snj.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-E98mWxN1.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CT1zxhqP.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-6GcngNcy.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCL6NvKe.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
- package/dist/worker/console/static/assets/index-Cdkvw_H6.js +469 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C0627e-9.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DPDPv9HL.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D3OHdWiO.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DI88zD7n.js → linear-BDZ9riWi.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-p2l8IrWY.js → mermaid.core-7pKqYtpZ.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-B1bAX-oS.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DwOnWxdD.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-xRkTXnJC.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-_9UC5Zio.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CgEUirWJ.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Czbpwiyz.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-_j7_KiqY.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BNJa6Hyi.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DedpKtOc.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BBvthr7c.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Ds7umgEM.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKa-p1nF.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LoUNR56o.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bqbl6ym7.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B8dUg4lf.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +24 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
- package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +192 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +9 -1
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +15 -5
- package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +92 -38
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +145 -70
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +38 -55
- package/dist/worker/console/static-src/operator-chat/useComposer.js +182 -69
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +2 -0
- package/dist/worker/console/static-src/prd-file-import.js +3 -1
- package/dist/worker/console/workspace-context.js +22 -0
- package/dist/worker/console/workspace-initialization.js +209 -0
- package/dist/worker/delivery/git-transaction.js +2 -0
- package/dist/worker/feature/fullstack-validate.js +2 -0
- package/dist/worker/loop-agent/loop-agent-client.js +8 -2
- package/dist/worker/materialize/harness-task-materializer.js +4 -1
- package/dist/worker/metrics/projector.js +1 -1
- package/dist/worker/observability/event-store.js +4 -1
- package/dist/worker/observability/read-model.js +4 -1
- package/dist/worker/observe/node-transparency.js +23 -1
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/operator-chrome.js +3 -1
- package/dist/worker/observe/static/styles.css +7 -4
- package/dist/worker/observe/static/views/dag-inspector.js +5 -19
- package/dist/worker/outcomes/declared-artifacts.js +2 -0
- package/dist/worker/preflight.js +3 -0
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
- package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
- package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
- package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
- package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
- package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
- package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
- package/dist/worker/task-spec/validate.js +4 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
- package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
- package/dist/workflows/dag/dag-retry-schema.js +11 -0
- package/dist/workflows/dag/decision-envelope.js +2 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +2 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-committed-facts.js +461 -0
- package/dist/workflows/dag/frontend-durable-tools.js +15 -3
- package/dist/workflows/dag/frontend-implementation-contract.js +369 -12
- package/dist/workflows/dag/frontend-plan-canary.js +53 -0
- package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
- package/dist/workflows/dag/frontend-plan-render.js +0 -2
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
- package/dist/workflows/dag/frontend-recovery-run.js +57 -0
- package/dist/workflows/dag/frontend-repair.js +55 -76
- package/dist/workflows/dag/frontend-review-context.js +42 -68
- package/dist/workflows/dag/frontend-review-scopes.js +2 -2
- package/dist/workflows/dag/frontend-risk.js +92 -10
- package/dist/workflows/dag/frontend-session-budget.js +117 -3
- package/dist/workflows/dag/frontend-shape.js +11 -55
- package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
- package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
- package/dist/workflows/dag/frontend-verification-trace.js +33 -54
- package/dist/workflows/dag/frontend-writer-admission.js +3 -47
- package/dist/workflows/dag/frontend-writer-status.js +0 -23
- package/dist/workflows/dag/init-hybrid.js +47 -19
- package/dist/workflows/dag/interrupt-request.js +2 -0
- package/dist/workflows/dag/lifecycle.js +11 -2
- package/dist/workflows/dag/node-execution.js +50 -13
- package/dist/workflows/dag/reconcile-run.js +2 -0
- package/dist/workflows/dag/repair-artifact.js +3 -1
- package/dist/workflows/dag/report.js +2 -0
- package/dist/workflows/dag/rerun-plan.js +10 -0
- package/dist/workflows/dag/rerun-task.js +79 -1
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/scheduler.js +2 -4
- package/dist/workflows/dag/types.js +44 -6
- package/dist/workflows/dag/validate.js +4 -0
- package/docs/README.md +1 -0
- package/docs/architecture/runtime-boundaries.md +3 -3
- package/docs/governance/README.md +1 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +14 -10
- package/docs/templates/frontend-implementation-contract.schema.json +0 -7
- package/package.json +9 -3
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/references/contract-protocol.md +3 -1
- package/skills/frontend-plan/SKILL.md +8 -6
- package/skills/frontend-plan/references/decision-contract.md +3 -3
- package/skills/frontend-review/SKILL.md +18 -28
- package/skills/frontend-review/references/review-findings.md +8 -3
- package/dist/commands/new-task.js +0 -5
- package/dist/commands/status.js +0 -57
- package/dist/commands/task-contract.js +0 -269
- package/dist/shared/prompts.js +0 -26
- package/dist/task/config.js +0 -2
- package/dist/task/contract/validate-draft.js +0 -106
- package/dist/task/goal.js +0 -3
- package/dist/task/index.js +0 -12
- package/dist/task/lifecycle.js +0 -1
- package/dist/task/operator/capabilities.js +0 -6
- package/dist/task/operator/envelope.js +0 -2
- package/dist/task/operator/index.js +0 -5
- package/dist/task/operator/registry.js +0 -2
- package/dist/task/operator/types.js +0 -1
- package/dist/task/paths.js +0 -1
- package/dist/task/source-state.js +0 -1
- package/dist/task/subagent-guidance.js +0 -1
- package/dist/worker/console/chat/sift-bridge.js +0 -1
- package/dist/worker/console/index.js +0 -20
- package/dist/worker/console/static/assets/channel-C0A6UEIa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +0 -1
- package/dist/worker/console/static/assets/index-24jp3aBh.js +0 -468
- package/dist/worker/console/static/assets/index-CY6y6I8M.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +0 -8
- package/dist/worker/scheduler/index.js +0 -25
- package/dist/workflows/dag/facts.js +0 -4
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
- package/dist/workflows/dag/index.js +0 -6
- package/dist/workflows/dynamic/index.js +0 -7
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-scoped Playwright browser used by both Operator Chat tools and
|
|
3
|
+
* the Browser panel. The browser profile lives outside the repository and is
|
|
4
|
+
* reused across Console restarts, so login state and downloads remain
|
|
5
|
+
* isolated from the user's normal browser without contaminating the repo.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { mkdir } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { chromium, } from "playwright-core";
|
|
11
|
+
import { normalizeBrowserUrl } from "./browser-policy.js";
|
|
12
|
+
export const BROWSER_VIEWPORT_WIDTH = 1280;
|
|
13
|
+
export const BROWSER_VIEWPORT_HEIGHT = 900;
|
|
14
|
+
const SEARCH_INPUT_SELECTORS = [
|
|
15
|
+
"input[name='wd']",
|
|
16
|
+
"input[name='q']",
|
|
17
|
+
"input[name='query']",
|
|
18
|
+
"input[type='search']",
|
|
19
|
+
"input[type='text']",
|
|
20
|
+
];
|
|
21
|
+
const SEARCH_SUBMIT_SELECTORS = [
|
|
22
|
+
"button[type='submit']",
|
|
23
|
+
"input[type='submit']",
|
|
24
|
+
"button:not([type])",
|
|
25
|
+
];
|
|
26
|
+
const DEFAULT_SEARCH_ENGINE_URL = "https://www.baidu.com/";
|
|
27
|
+
const SEARCH_TYPING_DELAY_MS = 45;
|
|
28
|
+
const DEFAULT_ACTION_TIMEOUT_MS = 15_000;
|
|
29
|
+
const MAX_SNAPSHOT_CHARS = 24_000;
|
|
30
|
+
const MAX_SCREENSHOT_BYTES = 4 * 1024 * 1024;
|
|
31
|
+
export class BrowserAutomationService {
|
|
32
|
+
options;
|
|
33
|
+
browserPromise;
|
|
34
|
+
activePage;
|
|
35
|
+
tabs = new Map();
|
|
36
|
+
nextTabId = 1;
|
|
37
|
+
disposed = false;
|
|
38
|
+
dialogQueue = [];
|
|
39
|
+
downloadQueue = [];
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
}
|
|
43
|
+
async navigate(rawUrl) {
|
|
44
|
+
const normalized = normalizeBrowserUrl(rawUrl);
|
|
45
|
+
if (normalized.kind !== "ok") {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
code: normalized.kind === "blocked" ? "BAD_SCHEME" : "INVALID_URL",
|
|
49
|
+
error: normalized.kind === "blocked"
|
|
50
|
+
? "browser urls must use http(s)"
|
|
51
|
+
: "invalid browser url",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const page = await this.page();
|
|
56
|
+
await page.goto(normalized.url, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
57
|
+
const snapshot = await this.snapshot({ screenshot: false });
|
|
58
|
+
return { ok: true, page: snapshot };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return { ok: false, code: "NAVIGATION_FAILED", error: errorMessage(error) };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async reload() {
|
|
65
|
+
const page = await this.page();
|
|
66
|
+
await page.reload({ waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
67
|
+
return this.snapshot({ screenshot: false });
|
|
68
|
+
}
|
|
69
|
+
async goBack() {
|
|
70
|
+
const page = await this.page();
|
|
71
|
+
await page.goBack({ waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
72
|
+
return this.snapshot({ screenshot: false });
|
|
73
|
+
}
|
|
74
|
+
async goForward() {
|
|
75
|
+
const page = await this.page();
|
|
76
|
+
await page.goForward({ waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
77
|
+
return this.snapshot({ screenshot: false });
|
|
78
|
+
}
|
|
79
|
+
async snapshot(options) {
|
|
80
|
+
const page = await this.page();
|
|
81
|
+
const dialog = this.dialogFor(page);
|
|
82
|
+
if (dialog) {
|
|
83
|
+
return {
|
|
84
|
+
url: page.url(),
|
|
85
|
+
title: "浏览器弹窗",
|
|
86
|
+
tabs: await this.tabSummaries(),
|
|
87
|
+
dialog,
|
|
88
|
+
ariaSnapshot: "",
|
|
89
|
+
...(options?.screenshot === false
|
|
90
|
+
? {}
|
|
91
|
+
: { screenshot: await this.captureScreenshot(page) }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const [title, aria] = await Promise.all([
|
|
95
|
+
page.title(),
|
|
96
|
+
page.ariaSnapshot({ mode: "ai", timeout: 10_000 }),
|
|
97
|
+
]);
|
|
98
|
+
const download = await this.nextDownloadResult();
|
|
99
|
+
const screenshot = options?.screenshot === false
|
|
100
|
+
? undefined
|
|
101
|
+
: await this.captureScreenshot(page);
|
|
102
|
+
return {
|
|
103
|
+
url: page.url(),
|
|
104
|
+
title,
|
|
105
|
+
tabs: await this.tabSummaries(),
|
|
106
|
+
dialog,
|
|
107
|
+
ariaSnapshot: truncateText(aria, MAX_SNAPSHOT_CHARS),
|
|
108
|
+
...(screenshot ? { screenshot } : {}),
|
|
109
|
+
...(download ? { download } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Lightweight state for iframe-driven UI. Avoids aria capture and PNG
|
|
114
|
+
* encoding on every panel poll.
|
|
115
|
+
*/
|
|
116
|
+
async state() {
|
|
117
|
+
if (!this.browserPromise && !this.disposed)
|
|
118
|
+
return emptyBrowserState();
|
|
119
|
+
const page = await this.page();
|
|
120
|
+
const state = {
|
|
121
|
+
running: true,
|
|
122
|
+
url: page.url(),
|
|
123
|
+
title: await page.title().catch(() => ""),
|
|
124
|
+
tabs: await this.tabSummaries(),
|
|
125
|
+
dialog: this.dialogFor(page),
|
|
126
|
+
ariaSnapshot: "",
|
|
127
|
+
};
|
|
128
|
+
return state;
|
|
129
|
+
}
|
|
130
|
+
async search(rawQuery, rawUrl) {
|
|
131
|
+
const query = rawQuery.trim();
|
|
132
|
+
if (!query)
|
|
133
|
+
throw new BrowserAutomationError("EMPTY_QUERY", "search query is empty");
|
|
134
|
+
const target = rawUrl?.trim() || DEFAULT_SEARCH_ENGINE_URL;
|
|
135
|
+
const normalized = normalizeBrowserUrl(target);
|
|
136
|
+
if (normalized.kind !== "ok") {
|
|
137
|
+
throw new BrowserAutomationError("INVALID_URL", "search engine url must use http(s)");
|
|
138
|
+
}
|
|
139
|
+
const page = await this.page();
|
|
140
|
+
await page.bringToFront();
|
|
141
|
+
await page.goto(normalized.url, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
142
|
+
const input = await findSearchInput(page);
|
|
143
|
+
if (!input) {
|
|
144
|
+
throw new BrowserAutomationError("SEARCH_INPUT_NOT_FOUND", "the search page did not expose a recognizable search input");
|
|
145
|
+
}
|
|
146
|
+
await input.fill("", { timeout: DEFAULT_ACTION_TIMEOUT_MS });
|
|
147
|
+
await input.pressSequentially(query, {
|
|
148
|
+
delay: SEARCH_TYPING_DELAY_MS,
|
|
149
|
+
timeout: DEFAULT_ACTION_TIMEOUT_MS,
|
|
150
|
+
});
|
|
151
|
+
await Promise.all([
|
|
152
|
+
page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 30_000 }),
|
|
153
|
+
submitSearchForm(page, input),
|
|
154
|
+
]);
|
|
155
|
+
return this.snapshot({ screenshot: false });
|
|
156
|
+
}
|
|
157
|
+
async act(request) {
|
|
158
|
+
const page = await this.page();
|
|
159
|
+
const ref = request.ref.trim();
|
|
160
|
+
if (!/^(f\d+)?e\d+$/.test(ref)) {
|
|
161
|
+
throw new BrowserAutomationError("INVALID_REF", "element ref must come from a fresh browser snapshot");
|
|
162
|
+
}
|
|
163
|
+
const locator = page.locator(`aria-ref=${ref}`);
|
|
164
|
+
try {
|
|
165
|
+
if (request.type === "click") {
|
|
166
|
+
await locator.click({ timeout: DEFAULT_ACTION_TIMEOUT_MS });
|
|
167
|
+
}
|
|
168
|
+
else if (request.type === "fill") {
|
|
169
|
+
await locator.fill(request.value, { timeout: DEFAULT_ACTION_TIMEOUT_MS });
|
|
170
|
+
}
|
|
171
|
+
else if (request.type === "press") {
|
|
172
|
+
await locator.press(request.key, { timeout: DEFAULT_ACTION_TIMEOUT_MS });
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
await locator.hover({ timeout: DEFAULT_ACTION_TIMEOUT_MS });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (error instanceof Error && /ref .* not found/i.test(error.message)) {
|
|
180
|
+
throw new BrowserAutomationError("STALE_REF", "element ref is stale; take a fresh browser snapshot", { cause: error });
|
|
181
|
+
}
|
|
182
|
+
throw new BrowserAutomationError("ACTION_FAILED", errorMessage(error), { cause: error });
|
|
183
|
+
}
|
|
184
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 3_000 }).catch(() => undefined);
|
|
185
|
+
return { action: request.type, ref, page: await this.snapshot({ screenshot: false }) };
|
|
186
|
+
}
|
|
187
|
+
async scroll(input) {
|
|
188
|
+
const page = await this.page();
|
|
189
|
+
const amount = clamp(input.amount ?? 600, 50, 3000);
|
|
190
|
+
await page.mouse.wheel(0, input.direction === "down" ? amount : -amount);
|
|
191
|
+
await page.waitForTimeout(150);
|
|
192
|
+
return this.snapshot({ screenshot: false });
|
|
193
|
+
}
|
|
194
|
+
async pressKey(key) {
|
|
195
|
+
const page = await this.page();
|
|
196
|
+
await page.keyboard.press(key);
|
|
197
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 3_000 }).catch(() => undefined);
|
|
198
|
+
return this.snapshot({ screenshot: false });
|
|
199
|
+
}
|
|
200
|
+
async selectTab(tabId) {
|
|
201
|
+
const page = this.tabs.get(tabId);
|
|
202
|
+
if (!page)
|
|
203
|
+
throw new BrowserAutomationError("TAB_NOT_FOUND", `tab ${tabId} does not exist`);
|
|
204
|
+
this.activePage = page;
|
|
205
|
+
await page.bringToFront();
|
|
206
|
+
return this.snapshot({ screenshot: false });
|
|
207
|
+
}
|
|
208
|
+
async newTab(rawUrl) {
|
|
209
|
+
const context = await this.context();
|
|
210
|
+
const page = await context.newPage();
|
|
211
|
+
this.trackPage(page);
|
|
212
|
+
this.activePage = page;
|
|
213
|
+
if (rawUrl) {
|
|
214
|
+
const result = await this.navigate(rawUrl);
|
|
215
|
+
if (!result.ok)
|
|
216
|
+
throw new BrowserAutomationError(result.code, result.error);
|
|
217
|
+
return result.page;
|
|
218
|
+
}
|
|
219
|
+
return this.snapshot({ screenshot: false });
|
|
220
|
+
}
|
|
221
|
+
async closeTab(tabId) {
|
|
222
|
+
const page = tabId === undefined ? await this.page() : this.tabs.get(tabId);
|
|
223
|
+
if (!page)
|
|
224
|
+
throw new BrowserAutomationError("TAB_NOT_FOUND", `tab ${tabId} does not exist`);
|
|
225
|
+
this.tabs.delete(tabIdForValue(this.tabs, page) ?? 0);
|
|
226
|
+
await page.close();
|
|
227
|
+
if (this.activePage === page)
|
|
228
|
+
this.activePage = undefined;
|
|
229
|
+
return this.snapshot({ screenshot: false });
|
|
230
|
+
}
|
|
231
|
+
async handleDialog(action, promptText) {
|
|
232
|
+
const page = await this.page();
|
|
233
|
+
const index = this.dialogQueue.findIndex((entry) => entry.page === page);
|
|
234
|
+
const entry = index >= 0 ? this.dialogQueue[index] : undefined;
|
|
235
|
+
if (!entry)
|
|
236
|
+
throw new BrowserAutomationError("NO_DIALOG", "the active page has no pending dialog");
|
|
237
|
+
this.dialogQueue.splice(index, 1);
|
|
238
|
+
if (action === "accept")
|
|
239
|
+
await entry.dialog.accept(promptText);
|
|
240
|
+
else
|
|
241
|
+
await entry.dialog.dismiss();
|
|
242
|
+
return this.snapshot({ screenshot: false });
|
|
243
|
+
}
|
|
244
|
+
async dispatchPointer(input) {
|
|
245
|
+
const page = await this.page();
|
|
246
|
+
if (input.type === "mousemove")
|
|
247
|
+
await page.mouse.move(input.x, input.y);
|
|
248
|
+
else if (input.type === "wheel")
|
|
249
|
+
await page.mouse.wheel(input.deltaX, input.deltaY);
|
|
250
|
+
else if (input.type === "mousedown")
|
|
251
|
+
await page.mouse.down({ button: input.button ?? "left" });
|
|
252
|
+
else if (input.type === "mouseup")
|
|
253
|
+
await page.mouse.up({ button: input.button ?? "left" });
|
|
254
|
+
return { ok: true };
|
|
255
|
+
}
|
|
256
|
+
async dispatchKeyboard(input) {
|
|
257
|
+
const page = await this.page();
|
|
258
|
+
if (input.type === "keydown")
|
|
259
|
+
await page.keyboard.down(input.key);
|
|
260
|
+
else
|
|
261
|
+
await page.keyboard.up(input.key);
|
|
262
|
+
return { ok: true };
|
|
263
|
+
}
|
|
264
|
+
async inputText(text) {
|
|
265
|
+
const page = await this.page();
|
|
266
|
+
await page.keyboard.type(text);
|
|
267
|
+
return { ok: true };
|
|
268
|
+
}
|
|
269
|
+
async screenshot() {
|
|
270
|
+
return this.snapshot({ screenshot: true });
|
|
271
|
+
}
|
|
272
|
+
async focus() {
|
|
273
|
+
const page = await this.page();
|
|
274
|
+
await page.bringToFront();
|
|
275
|
+
return { ok: true };
|
|
276
|
+
}
|
|
277
|
+
async waitFor(ms) {
|
|
278
|
+
const page = await this.page();
|
|
279
|
+
await page.waitForTimeout(clamp(ms, 0, 10_000));
|
|
280
|
+
return this.snapshot({ screenshot: false });
|
|
281
|
+
}
|
|
282
|
+
async dispose() {
|
|
283
|
+
if (this.disposed)
|
|
284
|
+
return;
|
|
285
|
+
this.disposed = true;
|
|
286
|
+
const pending = this.browserPromise;
|
|
287
|
+
this.browserPromise = undefined;
|
|
288
|
+
this.tabs.clear();
|
|
289
|
+
this.activePage = undefined;
|
|
290
|
+
if (!pending)
|
|
291
|
+
return;
|
|
292
|
+
const context = await pending.catch(() => undefined);
|
|
293
|
+
await context?.close().catch(() => undefined);
|
|
294
|
+
}
|
|
295
|
+
async page() {
|
|
296
|
+
const context = await this.context();
|
|
297
|
+
if (this.activePage && !this.activePage.isClosed())
|
|
298
|
+
return this.activePage;
|
|
299
|
+
const existing = context.pages().find((page) => this.tabs.has(tabIdForValue(this.tabs, page) ?? -1));
|
|
300
|
+
if (existing) {
|
|
301
|
+
this.activePage = existing;
|
|
302
|
+
return existing;
|
|
303
|
+
}
|
|
304
|
+
const page = await context.newPage();
|
|
305
|
+
this.trackPage(page);
|
|
306
|
+
this.activePage = page;
|
|
307
|
+
return page;
|
|
308
|
+
}
|
|
309
|
+
async context() {
|
|
310
|
+
if (this.disposed)
|
|
311
|
+
throw new BrowserAutomationError("BROWSER_CLOSED", "browser service is disposed");
|
|
312
|
+
this.browserPromise ??= (async () => {
|
|
313
|
+
await Promise.all([
|
|
314
|
+
mkdir(this.options.profileDir, { recursive: true }),
|
|
315
|
+
mkdir(this.options.downloadsDir, { recursive: true }),
|
|
316
|
+
mkdir(this.options.screenshotsDir, { recursive: true }),
|
|
317
|
+
]);
|
|
318
|
+
const browser = await this.launchBrowser();
|
|
319
|
+
this.wireContext(browser);
|
|
320
|
+
const page = browser.pages()[0] ?? await browser.newPage();
|
|
321
|
+
this.trackPage(page);
|
|
322
|
+
this.activePage = page;
|
|
323
|
+
return browser;
|
|
324
|
+
})();
|
|
325
|
+
try {
|
|
326
|
+
return await this.browserPromise;
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
this.browserPromise = undefined;
|
|
330
|
+
throw new BrowserAutomationError("BROWSER_START_FAILED", errorMessage(error), {
|
|
331
|
+
cause: error,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
wireContext(context) {
|
|
336
|
+
context.on("page", (page) => {
|
|
337
|
+
this.trackPage(page);
|
|
338
|
+
if (!this.activePage || this.activePage.isClosed())
|
|
339
|
+
this.activePage = page;
|
|
340
|
+
});
|
|
341
|
+
context.on("close", () => {
|
|
342
|
+
if (this.browserPromise) {
|
|
343
|
+
void this.browserPromise.then((current) => {
|
|
344
|
+
if (current === context) {
|
|
345
|
+
this.browserPromise = undefined;
|
|
346
|
+
this.activePage = undefined;
|
|
347
|
+
this.tabs.clear();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
context.on("dialog", (dialog) => {
|
|
353
|
+
const page = dialog.page();
|
|
354
|
+
if (page)
|
|
355
|
+
this.dialogQueue.push({ page, dialog });
|
|
356
|
+
});
|
|
357
|
+
context.on("download", (download) => {
|
|
358
|
+
this.downloadQueue.push({ download, consumed: false });
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
async launchBrowser() {
|
|
362
|
+
const candidates = chromiumLaunchCandidates();
|
|
363
|
+
const launchOptions = chromiumLaunchOptions(this.options.headless);
|
|
364
|
+
let lastError;
|
|
365
|
+
for (const candidate of candidates) {
|
|
366
|
+
try {
|
|
367
|
+
return await chromium.launchPersistentContext(this.options.profileDir, { ...launchOptions, ...candidate });
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
lastError = error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (candidates.length === 0) {
|
|
374
|
+
return await chromium.launchPersistentContext(this.options.profileDir, launchOptions);
|
|
375
|
+
}
|
|
376
|
+
throw lastError instanceof Error
|
|
377
|
+
? lastError
|
|
378
|
+
: new Error("no Chromium executable was available");
|
|
379
|
+
}
|
|
380
|
+
trackPage(page) {
|
|
381
|
+
for (const [id, existing] of this.tabs) {
|
|
382
|
+
if (existing === page)
|
|
383
|
+
return id;
|
|
384
|
+
}
|
|
385
|
+
const id = this.nextTabId;
|
|
386
|
+
this.nextTabId += 1;
|
|
387
|
+
this.tabs.set(id, page);
|
|
388
|
+
page.on("close", () => {
|
|
389
|
+
this.tabs.delete(id);
|
|
390
|
+
if (this.activePage === page)
|
|
391
|
+
this.activePage = undefined;
|
|
392
|
+
});
|
|
393
|
+
return id;
|
|
394
|
+
}
|
|
395
|
+
dialogFor(page) {
|
|
396
|
+
const index = this.dialogQueue.findIndex((entry) => entry.page === page);
|
|
397
|
+
const dialog = index >= 0 ? this.dialogQueue[index]?.dialog : undefined;
|
|
398
|
+
if (!dialog)
|
|
399
|
+
return null;
|
|
400
|
+
return {
|
|
401
|
+
type: dialog.type(),
|
|
402
|
+
message: dialog.message(),
|
|
403
|
+
...(dialog.defaultValue() ? { defaultText: dialog.defaultValue() } : {}),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async tabSummaries() {
|
|
407
|
+
return Promise.all([...this.tabs.entries()].map(async ([id, page]) => ({
|
|
408
|
+
id,
|
|
409
|
+
url: page.url(),
|
|
410
|
+
title: page.isClosed() ? "" : await page.title().catch(() => ""),
|
|
411
|
+
active: !page.isClosed() && page === this.activePage,
|
|
412
|
+
})));
|
|
413
|
+
}
|
|
414
|
+
async captureScreenshot(page) {
|
|
415
|
+
const buffer = await page.screenshot({
|
|
416
|
+
type: "png",
|
|
417
|
+
fullPage: false,
|
|
418
|
+
timeout: 15_000,
|
|
419
|
+
});
|
|
420
|
+
if (buffer.byteLength > MAX_SCREENSHOT_BYTES)
|
|
421
|
+
return undefined;
|
|
422
|
+
return `data:image/png;base64,${buffer.toString("base64")}`;
|
|
423
|
+
}
|
|
424
|
+
async nextDownloadResult() {
|
|
425
|
+
const index = this.downloadQueue.findIndex((entry) => !entry.consumed);
|
|
426
|
+
if (index < 0)
|
|
427
|
+
return undefined;
|
|
428
|
+
const entry = this.downloadQueue[index];
|
|
429
|
+
if (!entry)
|
|
430
|
+
return undefined;
|
|
431
|
+
entry.consumed = true;
|
|
432
|
+
const download = entry.download;
|
|
433
|
+
const target = path.join(this.options.downloadsDir, `${randomUUID()}-${sanitizeFilename(download.suggestedFilename())}`);
|
|
434
|
+
try {
|
|
435
|
+
await download.saveAs(target);
|
|
436
|
+
this.downloadQueue.splice(index, 1);
|
|
437
|
+
return {
|
|
438
|
+
url: download.url(),
|
|
439
|
+
suggestedFilename: download.suggestedFilename(),
|
|
440
|
+
path: target,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
this.downloadQueue.splice(index, 1);
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function emptyBrowserState() {
|
|
450
|
+
return {
|
|
451
|
+
running: false,
|
|
452
|
+
url: "about:blank",
|
|
453
|
+
title: "",
|
|
454
|
+
tabs: [],
|
|
455
|
+
dialog: null,
|
|
456
|
+
ariaSnapshot: "",
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function chromiumLaunchOptions(headless) {
|
|
460
|
+
return {
|
|
461
|
+
headless: headless ?? false,
|
|
462
|
+
viewport: undefined,
|
|
463
|
+
acceptDownloads: true,
|
|
464
|
+
locale: "zh-CN",
|
|
465
|
+
timezoneId: "Asia/Shanghai",
|
|
466
|
+
args: ["--start-maximized", "--disable-blink-features=AutomationControlled"],
|
|
467
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async function findSearchInput(page) {
|
|
471
|
+
for (const selector of SEARCH_INPUT_SELECTORS) {
|
|
472
|
+
const locator = page.locator(selector).first();
|
|
473
|
+
if (await locator.isVisible().catch(() => false))
|
|
474
|
+
return locator;
|
|
475
|
+
}
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
async function submitSearchForm(page, input) {
|
|
479
|
+
for (const selector of SEARCH_SUBMIT_SELECTORS) {
|
|
480
|
+
const locator = page.locator(selector).first();
|
|
481
|
+
if (!await locator.isVisible().catch(() => false))
|
|
482
|
+
continue;
|
|
483
|
+
await locator.click({ timeout: DEFAULT_ACTION_TIMEOUT_MS });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
await input.press("Enter", { timeout: DEFAULT_ACTION_TIMEOUT_MS });
|
|
487
|
+
}
|
|
488
|
+
export class BrowserAutomationError extends Error {
|
|
489
|
+
code;
|
|
490
|
+
constructor(code, message, options) {
|
|
491
|
+
super(message, options);
|
|
492
|
+
this.code = code;
|
|
493
|
+
this.name = "BrowserAutomationError";
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
export function chromiumLaunchCandidates(env = process.env, platform = process.platform) {
|
|
497
|
+
const executablePath = env.LOOP_BROWSER_CHROMIUM_PATH?.trim();
|
|
498
|
+
if (executablePath)
|
|
499
|
+
return [{ executablePath }];
|
|
500
|
+
if (platform === "win32")
|
|
501
|
+
return [{ channel: "chrome" }, { channel: "msedge" }];
|
|
502
|
+
if (platform === "darwin")
|
|
503
|
+
return [{ channel: "chrome" }];
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
function tabIdForValue(map, page) {
|
|
507
|
+
for (const [id, value] of map)
|
|
508
|
+
if (value === page)
|
|
509
|
+
return id;
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
function truncateText(value, max) {
|
|
513
|
+
return value.length <= max ? value : `${value.slice(0, max)}\n... [snapshot truncated]`;
|
|
514
|
+
}
|
|
515
|
+
function clamp(value, min, max) {
|
|
516
|
+
return Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : min;
|
|
517
|
+
}
|
|
518
|
+
function sanitizeFilename(value) {
|
|
519
|
+
return value.replace(/[\\/:*?"<>|]/g, "_").slice(0, 180) || "download";
|
|
520
|
+
}
|
|
521
|
+
function errorMessage(error) {
|
|
522
|
+
return error instanceof Error ? error.message : String(error);
|
|
523
|
+
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure URL policy for the Operator Chat browser panel: normalize user input
|
|
3
|
-
* into an http(s) URL
|
|
4
|
-
* embed in the panel iframe. Dependency-free so it is unit-testable.
|
|
3
|
+
* into an http(s) URL. Dependency-free so it is unit-testable.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* services by user action. The Console's OWN origin is explicitly allowed —
|
|
11
|
-
* the sandbox still renders it in an opaque origin with no same-origin
|
|
12
|
-
* privileges, exactly like any other site.
|
|
5
|
+
* Only the scheme is gated: javascript:, data:, file: and friends must never
|
|
6
|
+
* reach the iframe. Local addresses (localhost / 127.0.0.1 / ::1 / LAN
|
|
7
|
+
* hosts) are deliberately allowed — the panel is a development console and
|
|
8
|
+
* its users routinely browse local dev servers.
|
|
13
9
|
*/
|
|
14
10
|
/**
|
|
15
11
|
* Extract the `frame-ancestors` source list of a Content-Security-Policy
|
|
@@ -47,16 +43,6 @@ export function embeddabilityOf(probe) {
|
|
|
47
43
|
}
|
|
48
44
|
return "embeddable";
|
|
49
45
|
}
|
|
50
|
-
/** A loopback hostname (localhost, IPv6 ::1, 127.0.0.0/8, 0.0.0.0). */
|
|
51
|
-
export function isLoopbackHostname(hostname) {
|
|
52
|
-
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
53
|
-
if (host === "localhost" || host === "::1" || host === "0.0.0.0")
|
|
54
|
-
return true;
|
|
55
|
-
const parts = host.split(".");
|
|
56
|
-
return (parts.length === 4 &&
|
|
57
|
-
parts[0] === "127" &&
|
|
58
|
-
parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255));
|
|
59
|
-
}
|
|
60
46
|
/**
|
|
61
47
|
* Schemes that must never reach the iframe even without `//` (javascript:,
|
|
62
48
|
* data:, file:, ...). Host:port lookalikes (example.com:8080) are NOT here —
|
|
@@ -88,12 +74,8 @@ const FORBIDDEN_SCHEMES = new Set([
|
|
|
88
74
|
/**
|
|
89
75
|
* Normalize one address-bar input against the navigation policy.
|
|
90
76
|
* @param input raw user text.
|
|
91
|
-
* @param selfOrigin the Console's own origin (window.location.origin). The
|
|
92
|
-
* Console itself may be browsed in the panel (the sandbox keeps it opaque),
|
|
93
|
-
* so it is let through BEFORE the loopback check — its host is normally
|
|
94
|
-
* loopback.
|
|
95
77
|
*/
|
|
96
|
-
export function normalizeBrowserUrl(input
|
|
78
|
+
export function normalizeBrowserUrl(input) {
|
|
97
79
|
const trimmed = input.trim();
|
|
98
80
|
if (trimmed === "")
|
|
99
81
|
return { kind: "invalid" };
|
|
@@ -127,17 +109,5 @@ export function normalizeBrowserUrl(input, selfOrigin) {
|
|
|
127
109
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
128
110
|
return { kind: "blocked", reason: "scheme" };
|
|
129
111
|
}
|
|
130
|
-
// The Console's own origin is allowed (browsed in an opaque origin like
|
|
131
|
-
// any other site); checked before the loopback gate because its host is
|
|
132
|
-
// normally loopback.
|
|
133
|
-
try {
|
|
134
|
-
if (url.origin === new URL(selfOrigin).origin)
|
|
135
|
-
return { kind: "ok", url: url.href };
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
// Unparsable selfOrigin (never in practice): fall through to the gate.
|
|
139
|
-
}
|
|
140
|
-
if (isLoopbackHostname(url.hostname))
|
|
141
|
-
return { kind: "blocked", reason: "loopback" };
|
|
142
112
|
return { kind: "ok", url: url.href };
|
|
143
113
|
}
|