berry-agent 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/.api-emit.stamp +1 -0
- package/dist/.build-meta.json +6 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/loop.js +139 -0
- package/dist/agent/queue.js +119 -0
- package/dist/agent/stream.js +62 -0
- package/dist/agent/tools-batch.js +221 -0
- package/dist/agent/types.js +8 -0
- package/dist/api/berry-agent-llm.d.ts +18 -0
- package/dist/api/berry-agent.d.ts +9 -0
- package/dist/api/surface.json +1632 -0
- package/dist/api/tsconfig.paths.json +12 -0
- package/dist/api/typebox-compile.d.ts +2 -0
- package/dist/api/typebox-value.d.ts +2 -0
- package/dist/api/typebox.d.ts +2 -0
- package/dist/browser/cdp.js +256 -0
- package/dist/browser/codes.js +31 -0
- package/dist/browser/discover.js +107 -0
- package/dist/browser/engine.js +169 -0
- package/dist/browser/index.js +17 -0
- package/dist/browser/install.js +342 -0
- package/dist/browser/page.js +321 -0
- package/dist/browser/service.js +158 -0
- package/dist/browser/tools.js +198 -0
- package/dist/browser/types.js +81 -0
- package/dist/channels/ask-queue.js +82 -0
- package/dist/channels/commands.js +129 -0
- package/dist/channels/engine/cell.js +143 -0
- package/dist/channels/engine/diff.js +143 -0
- package/dist/channels/engine/engine.js +416 -0
- package/dist/channels/engine/index.js +8 -0
- package/dist/channels/engine/input-keys.js +86 -0
- package/dist/channels/engine/input.js +596 -0
- package/dist/channels/engine/memory-io.js +71 -0
- package/dist/channels/engine/process-io.js +38 -0
- package/dist/channels/engine/types.js +17 -0
- package/dist/channels/engine/width.js +122 -0
- package/dist/channels/index.js +15 -0
- package/dist/channels/registry.js +56 -0
- package/dist/channels/sdk/admit.js +29 -0
- package/dist/channels/sdk/backend.js +102 -0
- package/dist/channels/sdk/cursor.js +56 -0
- package/dist/channels/sdk/index.js +20 -0
- package/dist/channels/sdk/jsonl.js +208 -0
- package/dist/channels/sdk/protocol.js +24 -0
- package/dist/channels/sdk/schema.js +91 -0
- package/dist/channels/sdk/wire-core.js +446 -0
- package/dist/channels/service.js +169 -0
- package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
- package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
- package/dist/channels/tui/autocomplete/popup.js +148 -0
- package/dist/channels/tui/autocomplete/provider.js +7 -0
- package/dist/channels/tui/autocomplete/token.js +68 -0
- package/dist/channels/tui/backend/ansi-rows.js +211 -0
- package/dist/channels/tui/backend/main-screen.js +207 -0
- package/dist/channels/tui/backend/osc.js +91 -0
- package/dist/channels/tui/backend/transcript.js +230 -0
- package/dist/channels/tui/backend/tui-backend.js +915 -0
- package/dist/channels/tui/editor/editor-model.js +543 -0
- package/dist/channels/tui/editor/editor-view.js +140 -0
- package/dist/channels/tui/editor/editor.js +275 -0
- package/dist/channels/tui/editor/undo-stack.js +35 -0
- package/dist/channels/tui/editor/visual-lines.js +126 -0
- package/dist/channels/tui/editor/word-nav.js +106 -0
- package/dist/channels/tui/history/history-viewer.js +403 -0
- package/dist/channels/tui/index.js +3 -0
- package/dist/channels/tui/layout.js +167 -0
- package/dist/channels/tui/markdown/blocks.js +111 -0
- package/dist/channels/tui/markdown/inline.js +75 -0
- package/dist/channels/tui/markdown/markdown.js +182 -0
- package/dist/channels/tui/memory/memory-viewer.js +540 -0
- package/dist/channels/tui/overlay/alt-screen.js +105 -0
- package/dist/channels/tui/overlay/overlay.js +57 -0
- package/dist/channels/tui/overlay/select-confirm.js +156 -0
- package/dist/channels/tui/panels/todo-panel.js +54 -0
- package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
- package/dist/channels/tui/scroll/scroll-view.js +254 -0
- package/dist/channels/tui/status/status-line.js +76 -0
- package/dist/channels/tui/text.js +38 -0
- package/dist/channels/tui/theme.js +41 -0
- package/dist/channels/types.js +11 -0
- package/dist/channels/ui-core.js +209 -0
- package/dist/checkpoint/capture.js +45 -0
- package/dist/checkpoint/codes.js +33 -0
- package/dist/checkpoint/command.js +82 -0
- package/dist/checkpoint/gate.js +48 -0
- package/dist/checkpoint/index.js +20 -0
- package/dist/checkpoint/restore.js +172 -0
- package/dist/checkpoint/store.js +247 -0
- package/dist/checkpoint/types.js +4 -0
- package/dist/checkpoint/walk.js +146 -0
- package/dist/compaction/ccr-tools.js +112 -0
- package/dist/compaction/ccr.js +84 -0
- package/dist/compaction/codes.js +21 -0
- package/dist/compaction/index.js +18 -0
- package/dist/compaction/policy.js +200 -0
- package/dist/compaction/service.js +422 -0
- package/dist/compaction/slots.js +121 -0
- package/dist/compaction/types.js +10 -0
- package/dist/context/codes.js +40 -0
- package/dist/context/events.js +160 -0
- package/dist/context/index.js +13 -0
- package/dist/context/logger.js +136 -0
- package/dist/context/scope.js +182 -0
- package/dist/context/workspace.js +104 -0
- package/dist/contracts/agent-events.d.ts +73 -0
- package/dist/contracts/agent-events.js +16 -0
- package/dist/contracts/api.d.ts +249 -0
- package/dist/contracts/api.js +299 -0
- package/dist/contracts/approval.d.ts +37 -0
- package/dist/contracts/approval.js +9 -0
- package/dist/contracts/env-ref.d.ts +41 -0
- package/dist/contracts/env-ref.js +32 -0
- package/dist/contracts/errors.d.ts +56 -0
- package/dist/contracts/errors.js +266 -0
- package/dist/contracts/events.d.ts +70 -0
- package/dist/contracts/events.js +329 -0
- package/dist/contracts/index.d.ts +32 -0
- package/dist/contracts/index.js +26 -0
- package/dist/contracts/llm.d.ts +275 -0
- package/dist/contracts/llm.js +12 -0
- package/dist/contracts/messages.d.ts +63 -0
- package/dist/contracts/messages.js +65 -0
- package/dist/contracts/redact.d.ts +46 -0
- package/dist/contracts/redact.js +223 -0
- package/dist/contracts/tools.d.ts +197 -0
- package/dist/contracts/tools.js +33 -0
- package/dist/contracts/types.d.ts +302 -0
- package/dist/contracts/types.js +50 -0
- package/dist/contracts/ui.d.ts +122 -0
- package/dist/contracts/ui.js +15 -0
- package/dist/conversation/agent-service.js +61 -0
- package/dist/conversation/approval-wiring.js +82 -0
- package/dist/conversation/backoff.js +38 -0
- package/dist/conversation/codes.js +47 -0
- package/dist/conversation/control-tools.js +102 -0
- package/dist/conversation/control.js +210 -0
- package/dist/conversation/driver.js +922 -0
- package/dist/conversation/index.js +12 -0
- package/dist/conversation/model-visible.js +169 -0
- package/dist/conversation/open-tools.js +126 -0
- package/dist/conversation/reseed.js +132 -0
- package/dist/conversation/sessions.js +215 -0
- package/dist/conversation/todo.js +203 -0
- package/dist/conversation/types.js +35 -0
- package/dist/conversation/wiring.js +204 -0
- package/dist/credentials/codes.js +59 -0
- package/dist/credentials/commands.js +186 -0
- package/dist/credentials/env-ref.js +34 -0
- package/dist/credentials/index.js +28 -0
- package/dist/credentials/migration.js +18 -0
- package/dist/credentials/oauth.js +266 -0
- package/dist/credentials/refresh.js +152 -0
- package/dist/credentials/secrets.js +118 -0
- package/dist/credentials/types.js +30 -0
- package/dist/exec/bash.js +279 -0
- package/dist/exec/codes.js +37 -0
- package/dist/exec/env.js +99 -0
- package/dist/exec/environment.js +39 -0
- package/dist/exec/git-guard.js +623 -0
- package/dist/exec/index.js +24 -0
- package/dist/exec/registry.js +120 -0
- package/dist/exec/spawn.js +281 -0
- package/dist/exec/tail.js +77 -0
- package/dist/exec/types.js +5 -0
- package/dist/goal/codes.js +33 -0
- package/dist/goal/command.js +175 -0
- package/dist/goal/fold.js +121 -0
- package/dist/goal/gates.js +122 -0
- package/dist/goal/index.js +22 -0
- package/dist/goal/migration.js +48 -0
- package/dist/goal/service.js +501 -0
- package/dist/goal/todo-tool.js +174 -0
- package/dist/goal/types.js +1 -0
- package/dist/goal/update-tool.js +46 -0
- package/dist/host/approval-cmd.js +276 -0
- package/dist/host/assembly.js +1146 -0
- package/dist/host/boot-failures.js +104 -0
- package/dist/host/budget-advisory.js +74 -0
- package/dist/host/budget-broadcast.js +50 -0
- package/dist/host/builtins.js +54 -0
- package/dist/host/cli.js +544 -0
- package/dist/host/codes.js +120 -0
- package/dist/host/config-schema.js +195 -0
- package/dist/host/conversation-stack.js +654 -0
- package/dist/host/core-plugins.js +1685 -0
- package/dist/host/credentials-cmd.js +42 -0
- package/dist/host/disclosure.js +30 -0
- package/dist/host/dispatch.js +105 -0
- package/dist/host/doors-cmd.js +154 -0
- package/dist/host/dump-config.js +44 -0
- package/dist/host/hook-dispatch-guard.js +27 -0
- package/dist/host/import-gate.js +153 -0
- package/dist/host/index.js +54 -0
- package/dist/host/issue-session.js +241 -0
- package/dist/host/loader.js +372 -0
- package/dist/host/main.js +161 -0
- package/dist/host/manifest.js +381 -0
- package/dist/host/mcp-entry.js +53 -0
- package/dist/host/plugin-boot.js +952 -0
- package/dist/host/plugin-context.js +672 -0
- package/dist/host/plugin-install.js +480 -0
- package/dist/host/plugin-reload.js +124 -0
- package/dist/host/plugin-store.js +462 -0
- package/dist/host/plugin-tools.js +359 -0
- package/dist/host/plugin-uninstall.js +257 -0
- package/dist/host/plugins-cmd.js +302 -0
- package/dist/host/plugins-command.js +130 -0
- package/dist/host/plugins-config.js +195 -0
- package/dist/host/prompt-sections.js +133 -0
- package/dist/host/run-entry.js +544 -0
- package/dist/host/runtime.js +209 -0
- package/dist/host/scheduler-tick.js +294 -0
- package/dist/host/serve-daemon.js +501 -0
- package/dist/host/serve-entry.js +279 -0
- package/dist/host/session-anchor.js +41 -0
- package/dist/host/sessions-cmd.js +219 -0
- package/dist/host/sessions-face.js +47 -0
- package/dist/host/settings-store.js +112 -0
- package/dist/host/signals.js +69 -0
- package/dist/host/single-instance.js +105 -0
- package/dist/host/subagent-factory.js +291 -0
- package/dist/host/testkit/harness.js +283 -0
- package/dist/host/testkit/index.js +14 -0
- package/dist/host/testkit/matrix.js +302 -0
- package/dist/host/tool-policy-store.js +177 -0
- package/dist/host/triggers.js +212 -0
- package/dist/host/tui-entry.js +216 -0
- package/dist/host/webui-bridge.js +165 -0
- package/dist/issue/codes.js +38 -0
- package/dist/issue/filter.js +146 -0
- package/dist/issue/github.js +116 -0
- package/dist/issue/index.js +17 -0
- package/dist/issue/mount.js +70 -0
- package/dist/issue/poll.js +58 -0
- package/dist/issue/service.js +341 -0
- package/dist/issue/tools.js +70 -0
- package/dist/issue/types.js +34 -0
- package/dist/issue/webhook.js +126 -0
- package/dist/llm/codes.js +77 -0
- package/dist/llm/complete.js +196 -0
- package/dist/llm/events.js +15 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/inflight.js +65 -0
- package/dist/llm/model-id.js +58 -0
- package/dist/llm/provider-face.d.ts +32 -0
- package/dist/llm/provider-face.js +34 -0
- package/dist/llm/recovery.js +146 -0
- package/dist/llm/runtime.js +46 -0
- package/dist/llm/stream-fn.js +147 -0
- package/dist/lsp/codes.js +31 -0
- package/dist/lsp/connection.js +147 -0
- package/dist/lsp/frame.js +110 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/inject.js +58 -0
- package/dist/lsp/instance.js +264 -0
- package/dist/lsp/service.js +522 -0
- package/dist/lsp/tools.js +53 -0
- package/dist/lsp/types.js +201 -0
- package/dist/mcp/bridge.js +222 -0
- package/dist/mcp/codes.js +21 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/jsonrpc.js +230 -0
- package/dist/mcp/service.js +125 -0
- package/dist/mcp/tools.js +145 -0
- package/dist/mcp/types.js +108 -0
- package/dist/memory/cite.js +131 -0
- package/dist/memory/codes.js +50 -0
- package/dist/memory/command.js +71 -0
- package/dist/memory/consolidate.js +219 -0
- package/dist/memory/cycle.js +109 -0
- package/dist/memory/dao.js +878 -0
- package/dist/memory/diff.js +238 -0
- package/dist/memory/extract.js +181 -0
- package/dist/memory/fts.js +58 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/inject.js +354 -0
- package/dist/memory/merge.js +178 -0
- package/dist/memory/migration.js +132 -0
- package/dist/memory/pollution.js +59 -0
- package/dist/memory/port.js +385 -0
- package/dist/memory/review.js +201 -0
- package/dist/memory/scan.js +89 -0
- package/dist/memory/tools.js +508 -0
- package/dist/memory/types.js +140 -0
- package/dist/obs/codes.js +38 -0
- package/dist/obs/db.js +115 -0
- package/dist/obs/index.js +20 -0
- package/dist/obs/rollup.js +61 -0
- package/dist/obs/service.js +363 -0
- package/dist/obs/session-tools.js +282 -0
- package/dist/obs/session-view.js +225 -0
- package/dist/obs/tool.js +97 -0
- package/dist/obs/types.js +1 -0
- package/dist/persist/audit.js +63 -0
- package/dist/persist/aux.js +39 -0
- package/dist/persist/codes.js +35 -0
- package/dist/persist/index.js +30 -0
- package/dist/persist/load-history.js +50 -0
- package/dist/persist/migrations.js +37 -0
- package/dist/persist/paths.js +108 -0
- package/dist/persist/persistence.js +229 -0
- package/dist/persist/schema.js +99 -0
- package/dist/persist/secret-box.js +95 -0
- package/dist/persist/store.js +706 -0
- package/dist/persist/write-behind.js +238 -0
- package/dist/safety/approval.js +171 -0
- package/dist/safety/bwrap.js +123 -0
- package/dist/safety/codes.js +57 -0
- package/dist/safety/danger.js +505 -0
- package/dist/safety/gate.js +226 -0
- package/dist/safety/index.js +24 -0
- package/dist/safety/presets.js +73 -0
- package/dist/safety/roots.js +139 -0
- package/dist/safety/sandbox.js +214 -0
- package/dist/safety/seatbelt.js +88 -0
- package/dist/safety/sensitive.js +34 -0
- package/dist/safety/tool-policy.js +203 -0
- package/dist/safety/types.js +13 -0
- package/dist/scheduler/codes.js +54 -0
- package/dist/scheduler/cron-backend.js +135 -0
- package/dist/scheduler/engine.js +280 -0
- package/dist/scheduler/gates.js +60 -0
- package/dist/scheduler/index.js +25 -0
- package/dist/scheduler/migration.js +22 -0
- package/dist/scheduler/runner.js +171 -0
- package/dist/scheduler/schedule.js +193 -0
- package/dist/scheduler/service.js +283 -0
- package/dist/scheduler/tick.js +139 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/sdk/http.js +865 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/mcp.js +289 -0
- package/dist/sdk/plugin-route-registry.js +126 -0
- package/dist/sdk/plugin-routes.js +149 -0
- package/dist/sdk/security.js +116 -0
- package/dist/sdk/types.js +32 -0
- package/dist/session/budget.js +159 -0
- package/dist/session/codes.js +40 -0
- package/dist/session/derive.js +180 -0
- package/dist/session/event-data.js +1 -0
- package/dist/session/fork.js +72 -0
- package/dist/session/import-gates.js +148 -0
- package/dist/session/index.js +18 -0
- package/dist/session/recover.js +103 -0
- package/dist/session/session.js +279 -0
- package/dist/session/snapshot.js +107 -0
- package/dist/skills/agents.js +242 -0
- package/dist/skills/codes.js +51 -0
- package/dist/skills/discovery.js +266 -0
- package/dist/skills/frontmatter.js +215 -0
- package/dist/skills/index.js +28 -0
- package/dist/skills/load.js +118 -0
- package/dist/skills/manage.js +201 -0
- package/dist/skills/registry.js +138 -0
- package/dist/skills/render.js +124 -0
- package/dist/skills/sections.js +171 -0
- package/dist/skills/types.js +21 -0
- package/dist/subagent/codes.js +50 -0
- package/dist/subagent/declarative.js +55 -0
- package/dist/subagent/index.js +19 -0
- package/dist/subagent/notify.js +51 -0
- package/dist/subagent/provide.js +7 -0
- package/dist/subagent/registry.js +149 -0
- package/dist/subagent/service.js +338 -0
- package/dist/subagent/surface.js +44 -0
- package/dist/subagent/tool.js +153 -0
- package/dist/subagent/types.js +25 -0
- package/dist/tools/apply-patch.js +164 -0
- package/dist/tools/codes.js +109 -0
- package/dist/tools/fs.js +501 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/observed.js +99 -0
- package/dist/tools/pipeline.js +176 -0
- package/dist/tools/protected-read.js +54 -0
- package/dist/tools/registry.js +250 -0
- package/dist/tools/search.js +451 -0
- package/dist/tools/worktree.js +327 -0
- package/dist/web/codes.js +37 -0
- package/dist/web/gate.js +43 -0
- package/dist/web/hygiene.js +195 -0
- package/dist/web/index.js +16 -0
- package/dist/web/service.js +207 -0
- package/dist/web/ssrf-guard.js +44 -0
- package/dist/web/tool.js +76 -0
- package/dist/web/types.js +10 -0
- package/dist/webui/assets/index-B-t2O8gh.js +22 -0
- package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
- package/dist/webui/index.html +14 -0
- package/dist/webui/index.js +14 -0
- package/dist/webui/server.js +549 -0
- package/dist/webui/types.js +38 -0
- package/examples/README.md +26 -0
- package/examples/minimal-code-plugin/entry.js +43 -0
- package/examples/minimal-code-plugin/package.json +12 -0
- package/examples/pure-skill-pack/package.json +14 -0
- package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
- package/package.json +86 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "插件侧 tsconfig paths 模板(API 治理公开面)——拷进插件 tsconfig 的 compilerOptions.paths;相对段按插件 tsconfig 所在目录调整(示例假插件与宿主包同级安装);typebox 三键经插件自身 dependencies 解析(类型面同 npm 包,运行时仍走加载器注入防双实例),三行映射是等价兜底非必需;berry-agent/sqlite 键行随 SqliteFace 落码批补(届时见 COMPATIBILITY.md 版本注记)。",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"paths": {
|
|
5
|
+
"berry-agent": ["../node_modules/berry-agent/dist/api/berry-agent.d.ts"],
|
|
6
|
+
"berry-agent/llm": ["../node_modules/berry-agent/dist/api/berry-agent-llm.d.ts"],
|
|
7
|
+
"typebox": ["../node_modules/berry-agent/dist/api/typebox.d.ts"],
|
|
8
|
+
"typebox/value": ["../node_modules/berry-agent/dist/api/typebox-value.d.ts"],
|
|
9
|
+
"typebox/compile": ["../node_modules/berry-agent/dist/api/typebox-compile.d.ts"]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP 连接件(03 §10.3 形态条款——手写 CDP 桥,零新增依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 线形:browser 级单连接上的 JSON 文本帧(Chrome DevTools Protocol)——
|
|
5
|
+
* 命令帧 {id, method, params, sessionId?} / 应答帧 {id, result|error} /
|
|
6
|
+
* 事件帧 {method, params, sessionId?}。flat 模式下 per-session 命令带
|
|
7
|
+
* sessionId 字段、事件按 sessionId 分流路由(Target.attachToTarget
|
|
8
|
+
* {flatten:true} 产物)。
|
|
9
|
+
*
|
|
10
|
+
* 失败语义分账:连接级失败(ws 关闭/建立失败)= BROWSER_CONNECT_FAILED
|
|
11
|
+
* (在途全结清 + onDown 通知);单命令超时 = 普通 Error(连接仍活,调用方
|
|
12
|
+
* 自决重试)——两分防「慢命令」误杀活连接。
|
|
13
|
+
*/
|
|
14
|
+
import { BaseError } from '../contracts/index.js';
|
|
15
|
+
import { BROWSER_CDP_COMMAND_TIMEOUT_MS } from './types.js';
|
|
16
|
+
/** CDP 命令应答错误(wire error 对象) */
|
|
17
|
+
export class CdpCommandError extends Error {
|
|
18
|
+
cdpCode;
|
|
19
|
+
cdpMessage;
|
|
20
|
+
constructor(cdpCode, cdpMessage) {
|
|
21
|
+
super(`CDP 命令被拒(code ${cdpCode}):${cdpMessage}`);
|
|
22
|
+
this.cdpCode = cdpCode;
|
|
23
|
+
this.cdpMessage = cdpMessage;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 组 CDP 连接(ws 面之上——请求关联表 + 事件分流路由)。
|
|
28
|
+
* ws.opened 由调用方(引擎层)先行 await——本件只消费已建立的连接。
|
|
29
|
+
*/
|
|
30
|
+
export function createCdpConnection(ws, deps = {}) {
|
|
31
|
+
const logger = deps.logger;
|
|
32
|
+
const commandTimeout = deps.commandTimeoutMs ?? BROWSER_CDP_COMMAND_TIMEOUT_MS;
|
|
33
|
+
const pending = new Map();
|
|
34
|
+
/** 事件等待账(`${method}\0${sessionId ?? ''}` → 等待者队列) */
|
|
35
|
+
const eventWaiters = new Map();
|
|
36
|
+
const eventListeners = new Set();
|
|
37
|
+
/** 已注册的 onDown 监听者(settleDown 快照送达 + 迟到订阅即回调共用账) */
|
|
38
|
+
const downListeners = new Set();
|
|
39
|
+
let downGate = false; // 幂等闸(true = 已收口)
|
|
40
|
+
let deadReason = '';
|
|
41
|
+
let nextId = 1;
|
|
42
|
+
const isDead = () => downGate;
|
|
43
|
+
/** 连接级收口(幂等闸):在途命令 + 事件等待全结清 + onDown 一次性送达 */
|
|
44
|
+
const settleDown = (reason) => {
|
|
45
|
+
if (downGate)
|
|
46
|
+
return;
|
|
47
|
+
downGate = true;
|
|
48
|
+
deadReason = reason;
|
|
49
|
+
for (const entry of pending.values()) {
|
|
50
|
+
clearTimeout(entry.timer);
|
|
51
|
+
entry.reject(new BaseError('BROWSER_CONNECT_FAILED', `CDP 连接已断(${reason})——在途命令 ${entry.method} 结清`));
|
|
52
|
+
}
|
|
53
|
+
pending.clear();
|
|
54
|
+
for (const queue of eventWaiters.values()) {
|
|
55
|
+
for (const waiter of queue) {
|
|
56
|
+
clearTimeout(waiter.timer);
|
|
57
|
+
// 事件流随连接死而终结(普通 Error——单事件等待非连接码域)
|
|
58
|
+
waiter.reject(new Error(`CDP 连接已断(${reason})——事件等待结清`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
eventWaiters.clear();
|
|
62
|
+
for (const listener of downListeners)
|
|
63
|
+
listener(deadReason);
|
|
64
|
+
};
|
|
65
|
+
/** 事件分发:等待账优先(配对即清)→ 广播监听者 */
|
|
66
|
+
const dispatchEvent = (event) => {
|
|
67
|
+
const key = `${event.method}\0${event.sessionId ?? ''}`;
|
|
68
|
+
const queue = eventWaiters.get(key);
|
|
69
|
+
if (queue !== undefined && queue.length > 0) {
|
|
70
|
+
const waiter = queue.shift();
|
|
71
|
+
clearTimeout(waiter.timer);
|
|
72
|
+
waiter.resolve(event.params);
|
|
73
|
+
if (queue.length === 0)
|
|
74
|
+
eventWaiters.delete(key);
|
|
75
|
+
return; // 配对等待者独占(loadEventFired 一类单发事件不容双消费)
|
|
76
|
+
}
|
|
77
|
+
for (const listener of eventListeners) {
|
|
78
|
+
try {
|
|
79
|
+
listener(event);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
logger?.warn?.(`CDP 事件监听者抛错(contained 吞):${err instanceof Error ? err.message : String(err)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/** 入站文本帧路由:应答帧(带 id)→ 关联表;事件帧(带 method)→ 分发 */
|
|
87
|
+
ws.onMessage((text) => {
|
|
88
|
+
let msg;
|
|
89
|
+
try {
|
|
90
|
+
msg = JSON.parse(text);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
logger?.warn?.(`CDP 入站帧非 JSON(丢弃):${text.slice(0, 120)}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (typeof msg.id === 'number') {
|
|
97
|
+
const entry = pending.get(msg.id);
|
|
98
|
+
if (entry === undefined)
|
|
99
|
+
return; // 迟到应答(超时已结清)——静默
|
|
100
|
+
pending.delete(msg.id);
|
|
101
|
+
clearTimeout(entry.timer);
|
|
102
|
+
if (msg.error !== null && msg.error !== undefined) {
|
|
103
|
+
const err = msg.error;
|
|
104
|
+
entry.reject(new CdpCommandError(typeof err.code === 'number' ? err.code : 0, typeof err.message === 'string' ? err.message : '未知错误'));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
entry.resolve(msg.result);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (typeof msg.method === 'string') {
|
|
112
|
+
dispatchEvent({
|
|
113
|
+
method: msg.method,
|
|
114
|
+
params: msg.params,
|
|
115
|
+
...(typeof msg.sessionId === 'string' ? { sessionId: msg.sessionId } : {}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// 既无 id 又无 method——协议面外噪声,静默
|
|
119
|
+
});
|
|
120
|
+
ws.onClose(() => settleDown('WebSocket 已关闭'));
|
|
121
|
+
const conn = {
|
|
122
|
+
get isDead() {
|
|
123
|
+
return isDead();
|
|
124
|
+
},
|
|
125
|
+
onDown(listener) {
|
|
126
|
+
if (downGate) {
|
|
127
|
+
listener(deadReason); // 迟到订阅即回调(mcp/lsp onExit 同律)
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
downListeners.add(listener);
|
|
131
|
+
},
|
|
132
|
+
onEvent(listener) {
|
|
133
|
+
eventListeners.add(listener);
|
|
134
|
+
return () => eventListeners.delete(listener);
|
|
135
|
+
},
|
|
136
|
+
waitEvent(method, sessionId, timeoutMs) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
if (isDead()) {
|
|
139
|
+
reject(new BaseError('BROWSER_CONNECT_FAILED', `CDP 连接已断(${deadReason})——等待 ${method} 不可达`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const key = `${method}\0${sessionId ?? ''}`;
|
|
143
|
+
const waiter = {
|
|
144
|
+
resolve,
|
|
145
|
+
reject,
|
|
146
|
+
timer: setTimeout(() => {
|
|
147
|
+
const queue = eventWaiters.get(key);
|
|
148
|
+
if (queue !== undefined) {
|
|
149
|
+
const idx = queue.indexOf(waiter);
|
|
150
|
+
if (idx >= 0)
|
|
151
|
+
queue.splice(idx, 1);
|
|
152
|
+
if (queue.length === 0)
|
|
153
|
+
eventWaiters.delete(key);
|
|
154
|
+
}
|
|
155
|
+
reject(new Error(`CDP 事件 ${method} 等待超时(${timeoutMs}ms)`));
|
|
156
|
+
}, timeoutMs),
|
|
157
|
+
};
|
|
158
|
+
const queue = eventWaiters.get(key);
|
|
159
|
+
if (queue !== undefined)
|
|
160
|
+
queue.push(waiter);
|
|
161
|
+
else
|
|
162
|
+
eventWaiters.set(key, [waiter]);
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
send(method, params = {}, opts = {}) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
if (isDead()) {
|
|
168
|
+
reject(new BaseError('BROWSER_CONNECT_FAILED', `CDP 连接已断(${deadReason})——命令 ${method} 快拒`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const id = nextId++;
|
|
172
|
+
const timeoutMs = opts.timeoutMs ?? commandTimeout;
|
|
173
|
+
const entry = {
|
|
174
|
+
resolve,
|
|
175
|
+
reject,
|
|
176
|
+
method,
|
|
177
|
+
timer: setTimeout(() => {
|
|
178
|
+
pending.delete(id);
|
|
179
|
+
reject(new Error(`CDP 命令 ${method} 超时(${timeoutMs}ms)——连接仍在,单命令结清`));
|
|
180
|
+
}, timeoutMs),
|
|
181
|
+
};
|
|
182
|
+
pending.set(id, entry);
|
|
183
|
+
const frame = { id, method, params };
|
|
184
|
+
if (opts.sessionId !== undefined)
|
|
185
|
+
frame.sessionId = opts.sessionId;
|
|
186
|
+
try {
|
|
187
|
+
ws.send(JSON.stringify(frame));
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
pending.delete(id);
|
|
191
|
+
clearTimeout(entry.timer);
|
|
192
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
close() {
|
|
197
|
+
settleDown('主动关闭');
|
|
198
|
+
ws.close();
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
return conn;
|
|
202
|
+
}
|
|
203
|
+
/* ---------------- 生产 ws 面(Node ≥22 原生 WebSocket 包装) ---------------- */
|
|
204
|
+
/**
|
|
205
|
+
* 缺省 ws 面:全局 WebSocket(undici 产物,Node ≥22 免 flag)。只暴露桥所需
|
|
206
|
+
* 最小子集——opened 建立门 + 文本帧收发 + 关闭挂线。
|
|
207
|
+
*/
|
|
208
|
+
export function defaultWsFace() {
|
|
209
|
+
return {
|
|
210
|
+
connect(url) {
|
|
211
|
+
const socket = new WebSocket(url);
|
|
212
|
+
let messageListener;
|
|
213
|
+
let closeListener;
|
|
214
|
+
let closed = false;
|
|
215
|
+
socket.addEventListener('message', (event) => {
|
|
216
|
+
// CDP 线形恒文本帧;二进制帧(协议面外)丢弃
|
|
217
|
+
if (typeof event.data === 'string')
|
|
218
|
+
messageListener?.(event.data);
|
|
219
|
+
});
|
|
220
|
+
socket.addEventListener('close', () => {
|
|
221
|
+
if (closed)
|
|
222
|
+
return;
|
|
223
|
+
closed = true;
|
|
224
|
+
closeListener?.();
|
|
225
|
+
});
|
|
226
|
+
socket.addEventListener('error', () => {
|
|
227
|
+
// error 不单独语义化:close 必随后到(undici 生命周期)——关联回归一处
|
|
228
|
+
});
|
|
229
|
+
const connection = {
|
|
230
|
+
send: (text) => socket.send(text),
|
|
231
|
+
close: () => {
|
|
232
|
+
if (closed)
|
|
233
|
+
return;
|
|
234
|
+
try {
|
|
235
|
+
socket.close();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
/* CLOSING 期重复 close——吞 */
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
onMessage: (listener) => {
|
|
242
|
+
messageListener = listener;
|
|
243
|
+
},
|
|
244
|
+
onClose: (listener) => {
|
|
245
|
+
closeListener = listener;
|
|
246
|
+
},
|
|
247
|
+
opened: new Promise((resolve, reject) => {
|
|
248
|
+
socket.addEventListener('open', () => resolve());
|
|
249
|
+
socket.addEventListener('close', () => reject(new BaseError('BROWSER_CONNECT_FAILED', `WebSocket 建立失败(连接即关闭):${url}`)));
|
|
250
|
+
socket.addEventListener('error', () => reject(new BaseError('BROWSER_CONNECT_FAILED', `WebSocket 建立失败:${url}`)));
|
|
251
|
+
}),
|
|
252
|
+
};
|
|
253
|
+
return connection;
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser 桥域错误码注册(03 §10.3——BROWSER_ 前缀族首批)。
|
|
3
|
+
*
|
|
4
|
+
* 前缀族明列见 contracts/errors.ts ERROR_CODE_PREFIXES(02 §5.3 #1);码名清
|
|
5
|
+
* 单与语义真源 = 03 篇 §10.3 引擎发现序/生命周期条 + 02 §5.3 BROWSER_ 族四
|
|
6
|
+
* 码。本文件由模块公开面 index.ts 引入(注册纪律:写入点文件必须实际 import
|
|
7
|
+
* 本文件注册才发生——与 mcp/lsp 先例同款)。
|
|
8
|
+
*/
|
|
9
|
+
import { registerErrorCodes } from '../contracts/index.js';
|
|
10
|
+
registerErrorCodes([
|
|
11
|
+
{
|
|
12
|
+
code: 'BROWSER_ENGINE_NOT_FOUND',
|
|
13
|
+
module: 'browser',
|
|
14
|
+
description: '引擎发现序四步全缺席(显式覆盖 / 系统 Chrome 知名位 + PATH / 数据目录专用引擎)——工具调用诚实缺席拒附安装指引,不自动下载;显式覆盖指名路径缺席同码(显式误配 fail-loud 不静默回退)',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
code: 'BROWSER_CONNECT_FAILED',
|
|
18
|
+
module: 'browser',
|
|
19
|
+
description: '引擎 spawn 后 CDP 载体级失败(DevTools 侦听行缺席/启动超时、WebSocket 建立失败、连接中断)——结清在途请求 + 引擎降级(下用再试),不炸宿主',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
code: 'BROWSER_DIGEST_MISMATCH',
|
|
23
|
+
module: 'browser',
|
|
24
|
+
description: '/browser install 同版本重装摘要不符即拒执行(TOFU 锚定执法——账本既有指纹 vs 本次实收指纹),既有引擎不动',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: 'BROWSER_CONFIG_INVALID',
|
|
28
|
+
module: 'browser',
|
|
29
|
+
description: 'config.browser 行载荷坏(executablePath 非空字符串形 / providers 非数组形——装载时刻响亮拒)',
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 引擎发现序(03 §10.3——四步:显式覆盖 → 系统 Chrome 知名位 + PATH →
|
|
3
|
+
* 数据目录专用引擎 → 诚实缺席)。
|
|
4
|
+
*
|
|
5
|
+
* 全缺席 = 诚实缺席:抛 BROWSER_ENGINE_NOT_FOUND 附安装指引——**不自动下载**
|
|
6
|
+
* (150MB 级惊喜下载不可接受;下载 = 显式命令 /browser install,归 install 件)。
|
|
7
|
+
* 显式覆盖(config executablePath / `BERRY_AGENT_BROWSER_PATH`)指名路径缺席
|
|
8
|
+
* 同码 fail-loud——显式误配不静默回退(回退会掩盖配置错误)。
|
|
9
|
+
*/
|
|
10
|
+
import { BaseError } from '../contracts/index.js';
|
|
11
|
+
/** macOS 系统 Chrome 知名位(系统级 + 用户级两形) */
|
|
12
|
+
function wellKnownDarwin(homeDir) {
|
|
13
|
+
return [
|
|
14
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
15
|
+
`${homeDir}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
/** Linux 系统 Chrome 知名位(发行版包名族 + snap 形) */
|
|
19
|
+
function wellKnownLinux() {
|
|
20
|
+
return [
|
|
21
|
+
'/usr/bin/google-chrome',
|
|
22
|
+
'/usr/bin/google-chrome-stable',
|
|
23
|
+
'/usr/bin/google-chrome-beta',
|
|
24
|
+
'/usr/bin/chromium',
|
|
25
|
+
'/usr/bin/chromium-browser',
|
|
26
|
+
'/snap/bin/chromium',
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
/** 系统 Chrome 知名位(win32 v1 诚实缺席——03 §10.3 明裁) */
|
|
30
|
+
export function wellKnownPaths(platform, homeDir) {
|
|
31
|
+
if (platform === 'darwin')
|
|
32
|
+
return wellKnownDarwin(homeDir);
|
|
33
|
+
if (platform === 'linux')
|
|
34
|
+
return wellKnownLinux();
|
|
35
|
+
return []; // win32:知名位 v1 诚实缺席(PATH 探测腿仍活)
|
|
36
|
+
}
|
|
37
|
+
/** PATH 探测可执行名(win32 走 .exe;其余四名通扫) */
|
|
38
|
+
function pathProbeNames(platform) {
|
|
39
|
+
if (platform === 'win32')
|
|
40
|
+
return ['chrome.exe'];
|
|
41
|
+
return ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser'];
|
|
42
|
+
}
|
|
43
|
+
/** PATH 分隔符(win32 ';' 其余 ':') */
|
|
44
|
+
function pathDelimiter(platform) {
|
|
45
|
+
return platform === 'win32' ? ';' : ':';
|
|
46
|
+
}
|
|
47
|
+
/** 存在且为普通文件(access + stat isFile 双腿——目录形不算引擎) */
|
|
48
|
+
async function isFileAt(fs, path) {
|
|
49
|
+
try {
|
|
50
|
+
const st = await fs.stat(path);
|
|
51
|
+
return st.isFile();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 四步发现序。返回首个命中步的绝对路径;全缺席抛 BROWSER_ENGINE_NOT_FOUND。
|
|
59
|
+
*/
|
|
60
|
+
export async function discoverEngine(deps) {
|
|
61
|
+
const { fs, readEnv, platform, homeDir, dataDir } = deps;
|
|
62
|
+
// ① 显式覆盖:行 config executablePath 或 BERRY_AGENT_BROWSER_PATH(config
|
|
63
|
+
// 优先——行面覆盖环境面)。指名即验:缺席不回退(显式误配 fail-loud)。
|
|
64
|
+
const explicit = deps.executablePath ?? readEnv('BERRY_AGENT_BROWSER_PATH');
|
|
65
|
+
if (explicit !== undefined && explicit !== '') {
|
|
66
|
+
if (await isFileAt(fs, explicit)) {
|
|
67
|
+
return { path: explicit, source: deps.executablePath !== undefined ? 'config' : 'env' };
|
|
68
|
+
}
|
|
69
|
+
throw new BaseError('BROWSER_ENGINE_NOT_FOUND', `显式指定的浏览器引擎路径不存在:${explicit}(config browser.executablePath / BERRY_AGENT_BROWSER_PATH 显式误配不静默回退——请修正路径或移除显式配置)`);
|
|
70
|
+
}
|
|
71
|
+
// ② 系统 Chrome 知名位(macOS/Linux;win32 v1 诚实缺席)→ PATH 探测
|
|
72
|
+
for (const candidate of wellKnownPaths(platform, homeDir)) {
|
|
73
|
+
if (await isFileAt(fs, candidate))
|
|
74
|
+
return { path: candidate, source: 'wellknown' };
|
|
75
|
+
}
|
|
76
|
+
const pathEnv = readEnv('PATH');
|
|
77
|
+
if (pathEnv !== undefined && pathEnv !== '') {
|
|
78
|
+
for (const dir of pathEnv.split(pathDelimiter(platform))) {
|
|
79
|
+
if (dir === '')
|
|
80
|
+
continue;
|
|
81
|
+
for (const name of pathProbeNames(platform)) {
|
|
82
|
+
const candidate = `${dir.endsWith('/') ? dir.slice(0, -1) : dir}/${name}`;
|
|
83
|
+
if (await isFileAt(fs, candidate))
|
|
84
|
+
return { path: candidate, source: 'path' };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ③ 数据目录专用引擎(/browser install 产物——账本记相对可执行位)
|
|
89
|
+
const ledgerPath = `${dataDir}/browser/engine/ledger.json`;
|
|
90
|
+
try {
|
|
91
|
+
const raw = await fs.readFile(ledgerPath, 'utf8');
|
|
92
|
+
const ledger = JSON.parse(raw);
|
|
93
|
+
if (typeof ledger.executableRel === 'string' && ledger.executableRel !== '') {
|
|
94
|
+
const candidate = `${dataDir}/browser/engine/${ledger.executableRel}`;
|
|
95
|
+
if (await isFileAt(fs, candidate))
|
|
96
|
+
return { path: candidate, source: 'dataDir' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// 账本缺席/坏形——install 未跑过即无专用引擎,静默落第④步(诚实缺席)
|
|
101
|
+
}
|
|
102
|
+
// ④ 全缺席 = 诚实缺席(附安装指引——不自动下载)
|
|
103
|
+
throw new BaseError('BROWSER_ENGINE_NOT_FOUND', '未发现可用的浏览器引擎(系统 Chrome 知名位 / PATH / 数据目录专用引擎均缺席)。' +
|
|
104
|
+
'安装路径三选一:① 在会话中执行 /browser install 下载 Chromium for Testing 到数据目录;' +
|
|
105
|
+
'② 安装系统 Chrome(macOS/Linux 知名位或 PATH 可见即自动发现);' +
|
|
106
|
+
'③ 经 config browser.executablePath 或 BERRY_AGENT_BROWSER_PATH 显式指定引擎路径');
|
|
107
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 引擎生命周期件(03 §10.3——惰性首用 spawn + DevTools 侦听行发现 + CDP 建立)。
|
|
3
|
+
*
|
|
4
|
+
* 编舞:发现序(discoverEngine)→ spawn(`--headless=new` +
|
|
5
|
+
* `--remote-debugging-port=0`〔自动分配零端口竞态〕+ `--user-data-dir` 钉数据
|
|
6
|
+
* 目录不碰用户日常档)→ stderr 侦听行解析(port=0 形态下引擎在 stderr 报
|
|
7
|
+
* `DevTools listening on ws://127.0.0.1:<port>/devtools/browser/<guid>`——
|
|
8
|
+
* 零端口即唯一出口)→ ws 建立(browser 级单连接)→ Browser.getVersion 握手
|
|
9
|
+
* 校验。全程同罩启动预算钟。
|
|
10
|
+
*
|
|
11
|
+
* 失败收口:任何一步失败 = BROWSER_CONNECT_FAILED + 子进程树杀(不留孤儿)。
|
|
12
|
+
* 运行期降级:ws 关闭/进程退出 → onDown 一次性收口(在途结清归 cdp 件)。
|
|
13
|
+
* 协议化关停 close():Browser.close 命令 → 宽限等自退 → kill 兜底(幂等)。
|
|
14
|
+
*/
|
|
15
|
+
import { BaseError } from '../contracts/index.js';
|
|
16
|
+
import { createCdpConnection } from './cdp.js';
|
|
17
|
+
import { discoverEngine } from './discover.js';
|
|
18
|
+
import { BROWSER_CLOSE_GRACE_MS, BROWSER_ENGINE_ENV_ALLOW, BROWSER_ENGINE_OWNER, BROWSER_STARTUP_TIMEOUT_MS, DEVTOOLS_LISTENING_RE, } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* 启动引擎(惰性首用的执行体——发现 → spawn → 侦听行 → ws → 握手)。
|
|
21
|
+
* 任何一步失败:kill 子进程 + 抛 BROWSER_ENGINE_NOT_FOUND(发现序缺席)或
|
|
22
|
+
* BROWSER_CONNECT_FAILED(载体级失败)。
|
|
23
|
+
*/
|
|
24
|
+
export async function launchBrowserEngine(deps) {
|
|
25
|
+
const logger = deps.logger;
|
|
26
|
+
const startupTimeoutMs = deps.startupTimeoutMs ?? BROWSER_STARTUP_TIMEOUT_MS;
|
|
27
|
+
const closeGraceMs = deps.closeGraceMs ?? BROWSER_CLOSE_GRACE_MS;
|
|
28
|
+
// 发现序(第①~④步——缺席即 BROWSER_ENGINE_NOT_FOUND 附安装指引)
|
|
29
|
+
const discovered = await discoverEngine({
|
|
30
|
+
fs: deps.fs,
|
|
31
|
+
readEnv: deps.readEnv,
|
|
32
|
+
platform: deps.platform,
|
|
33
|
+
homeDir: deps.homeDir,
|
|
34
|
+
dataDir: deps.dataDir,
|
|
35
|
+
...(deps.config.executablePath !== undefined ? { executablePath: deps.config.executablePath } : {}),
|
|
36
|
+
});
|
|
37
|
+
logger?.info?.(`browser 引擎发现命中(${discovered.source}):${discovered.path}`);
|
|
38
|
+
// profile 目录就位(--user-data-dir 钉数据目录——不碰用户日常档)
|
|
39
|
+
const profileDir = `${deps.dataDir}/browser/profile`;
|
|
40
|
+
await deps.fs.mkdir(profileDir, { recursive: true });
|
|
41
|
+
const child = deps.spawn.spawnInteractive({
|
|
42
|
+
argv: [
|
|
43
|
+
discovered.path,
|
|
44
|
+
'--headless=new',
|
|
45
|
+
'--remote-debugging-port=0',
|
|
46
|
+
`--user-data-dir=${profileDir}`,
|
|
47
|
+
'--no-first-run',
|
|
48
|
+
'--no-default-browser-check',
|
|
49
|
+
'about:blank',
|
|
50
|
+
],
|
|
51
|
+
env: { allow: BROWSER_ENGINE_ENV_ALLOW },
|
|
52
|
+
owner: BROWSER_ENGINE_OWNER,
|
|
53
|
+
});
|
|
54
|
+
// 启动竞速收口旗:先到者封(握手持/进程退/预算钟三源)
|
|
55
|
+
let settled = false;
|
|
56
|
+
// stderr 累积 + 侦听行扫描(port=0 的唯一出口)。失败(预算钟尽/进程先退)
|
|
57
|
+
// = 树杀不留孤儿——已退场景 kill 无害,未退场景(超时腿)不杀即漏孤儿。
|
|
58
|
+
let wsUrl;
|
|
59
|
+
try {
|
|
60
|
+
wsUrl = await new Promise((resolve, reject) => {
|
|
61
|
+
let buffer = '';
|
|
62
|
+
const finish = (fn) => {
|
|
63
|
+
if (settled)
|
|
64
|
+
return;
|
|
65
|
+
settled = true;
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
fn();
|
|
68
|
+
};
|
|
69
|
+
const timer = setTimeout(() => finish(() => reject(new BaseError('BROWSER_CONNECT_FAILED', `引擎启动超时(${startupTimeoutMs}ms)未报 DevTools 侦听行——stderr 累积:${buffer.slice(-400)}`))), startupTimeoutMs);
|
|
70
|
+
child.stderr.on('data', (chunk) => {
|
|
71
|
+
buffer += chunk.toString('utf8');
|
|
72
|
+
const match = DEVTOOLS_LISTENING_RE.exec(buffer);
|
|
73
|
+
if (match !== null) {
|
|
74
|
+
const url = match[1] ?? '';
|
|
75
|
+
finish(() => resolve(url));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
child.onExit((info) => finish(() => reject(new BaseError('BROWSER_CONNECT_FAILED', `引擎进程在 DevTools 侦听行产出前退出(${info.spawnError !== undefined ? `spawn 失败:${info.spawnError.message}` : `exit ${String(info.code)}`})——路径:${discovered.path}`))));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
child.kill();
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
// ws 建立(browser 级单连接)——失败即树杀不留孤儿
|
|
86
|
+
const wsConn = deps.ws.connect(wsUrl);
|
|
87
|
+
try {
|
|
88
|
+
await wsConn.opened;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
child.kill();
|
|
92
|
+
throw err instanceof BaseError
|
|
93
|
+
? err
|
|
94
|
+
: new BaseError('BROWSER_CONNECT_FAILED', `WebSocket 建立失败:${err instanceof Error ? err.message : String(err)}`);
|
|
95
|
+
}
|
|
96
|
+
const conn = createCdpConnection(wsConn, { logger });
|
|
97
|
+
try {
|
|
98
|
+
// 握手校验(Browser.getVersion——CDP 线形活性验证)
|
|
99
|
+
await conn.send('Browser.getVersion', {}, { timeoutMs: startupTimeoutMs });
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
conn.close();
|
|
103
|
+
child.kill();
|
|
104
|
+
throw err instanceof BaseError
|
|
105
|
+
? err
|
|
106
|
+
: new BaseError('BROWSER_CONNECT_FAILED', `CDP 握手(Browser.getVersion)失败:${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
}
|
|
108
|
+
// 运行期降级双源:进程退出 / ws 关闭(后者 cdp 件内已收口)——任一先到,
|
|
109
|
+
// 引擎侧标死 + onDown 一次性送达。进程退也兜底关 ws(幂等)。
|
|
110
|
+
let downGate = false;
|
|
111
|
+
let downReason = '';
|
|
112
|
+
const downListeners = new Set();
|
|
113
|
+
const markDown = (reason) => {
|
|
114
|
+
if (downGate)
|
|
115
|
+
return;
|
|
116
|
+
downGate = true;
|
|
117
|
+
downReason = reason;
|
|
118
|
+
conn.close(); // 在途结清 + 后续快拒(幂等)
|
|
119
|
+
for (const listener of downListeners)
|
|
120
|
+
listener(reason);
|
|
121
|
+
};
|
|
122
|
+
conn.onDown((reason) => markDown(`连接关闭:${reason}`));
|
|
123
|
+
child.onExit((info) => markDown(info.spawnError !== undefined ? `进程退出(spawn 失败)` : `进程退出(code ${String(info.code)})`));
|
|
124
|
+
let closeGate = false;
|
|
125
|
+
const handle = {
|
|
126
|
+
conn,
|
|
127
|
+
discovered,
|
|
128
|
+
get alive() {
|
|
129
|
+
return !downGate;
|
|
130
|
+
},
|
|
131
|
+
onDown(listener) {
|
|
132
|
+
if (downGate) {
|
|
133
|
+
listener(downReason);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
downListeners.add(listener);
|
|
137
|
+
},
|
|
138
|
+
close() {
|
|
139
|
+
if (closeGate)
|
|
140
|
+
return Promise.resolve();
|
|
141
|
+
closeGate = true;
|
|
142
|
+
return (async () => {
|
|
143
|
+
// 告别命令先行(markDown 会结清在途并关 ws——顺序不可倒);坏引擎
|
|
144
|
+
// 不响应即超时落宽限腿,不悬挂
|
|
145
|
+
if (!downGate) {
|
|
146
|
+
try {
|
|
147
|
+
await conn.send('Browser.close', {}, { timeoutMs: closeGraceMs });
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
/* 协议告别失败走宽限兜底 */
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
markDown('协议化关停');
|
|
154
|
+
// 宽限等自退 → kill 兜底(不因坏引擎悬挂)
|
|
155
|
+
await new Promise((resolve) => {
|
|
156
|
+
const timer = setTimeout(() => {
|
|
157
|
+
child.kill();
|
|
158
|
+
resolve();
|
|
159
|
+
}, closeGraceMs);
|
|
160
|
+
child.onExit(() => {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
})();
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
return handle;
|
|
169
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser 模块公开面(L3 core: 官方插件 15 之 #7——02 §4.1 席 19 / 03 §10.3
|
|
3
|
+
* browser 桥件章:CDP 手写最小桥 + 引擎发现序 + 工具面十件)。
|
|
4
|
+
*
|
|
5
|
+
* 批 17b-3 工具面+编排腿:页面上下文 + 工具面十件 + 编排(惰性引擎/会话
|
|
6
|
+
* 路由/两级闲置回收)。批 17b-4 install 件:/browser install 下载原语
|
|
7
|
+
* (CfT 元数据 + 流式下载 + 手写 unzip + 摘要账本 TOFU 锚定)已入本面。
|
|
8
|
+
*/
|
|
9
|
+
import './codes.js';
|
|
10
|
+
export { BROWSER_STARTUP_TIMEOUT_MS, BROWSER_CDP_COMMAND_TIMEOUT_MS, BROWSER_IDLE_MS, BROWSER_SWEEP_INTERVAL_MS, BROWSER_CLOSE_GRACE_MS, BROWSER_NAV_TIMEOUT_MS, BROWSER_CONSOLE_RING_CAP, BROWSER_SCREENSHOT_KEEP, BROWSER_SCREENSHOT_DIRNAME, BROWSER_ENGINE_OWNER, BROWSER_ENGINE_ENV_ALLOW, normalizeBrowserConfig, } from './types.js';
|
|
11
|
+
export { wellKnownPaths, discoverEngine } from './discover.js';
|
|
12
|
+
export { createCdpConnection, defaultWsFace, CdpCommandError } from './cdp.js';
|
|
13
|
+
export { launchBrowserEngine } from './engine.js';
|
|
14
|
+
export { createBrowserPage } from './page.js';
|
|
15
|
+
export { buildBrowserTools } from './tools.js';
|
|
16
|
+
export { createBrowserService } from './service.js';
|
|
17
|
+
export { BROWSER_METADATA_URL, BROWSER_DOWNLOAD_HOSTS, BROWSER_ENGINE_DIRNAME, BROWSER_LEDGER_NAME, BROWSER_ZIP_MAX_BYTES, platformArtifact, defaultDownloadFace, unzipTo, installBrowserEngine, } from './install.js';
|