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.
Files changed (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/.api-emit.stamp +1 -0
  4. package/dist/.build-meta.json +6 -0
  5. package/dist/agent/events.js +1 -0
  6. package/dist/agent/index.js +2 -0
  7. package/dist/agent/loop.js +139 -0
  8. package/dist/agent/queue.js +119 -0
  9. package/dist/agent/stream.js +62 -0
  10. package/dist/agent/tools-batch.js +221 -0
  11. package/dist/agent/types.js +8 -0
  12. package/dist/api/berry-agent-llm.d.ts +18 -0
  13. package/dist/api/berry-agent.d.ts +9 -0
  14. package/dist/api/surface.json +1632 -0
  15. package/dist/api/tsconfig.paths.json +12 -0
  16. package/dist/api/typebox-compile.d.ts +2 -0
  17. package/dist/api/typebox-value.d.ts +2 -0
  18. package/dist/api/typebox.d.ts +2 -0
  19. package/dist/browser/cdp.js +256 -0
  20. package/dist/browser/codes.js +31 -0
  21. package/dist/browser/discover.js +107 -0
  22. package/dist/browser/engine.js +169 -0
  23. package/dist/browser/index.js +17 -0
  24. package/dist/browser/install.js +342 -0
  25. package/dist/browser/page.js +321 -0
  26. package/dist/browser/service.js +158 -0
  27. package/dist/browser/tools.js +198 -0
  28. package/dist/browser/types.js +81 -0
  29. package/dist/channels/ask-queue.js +82 -0
  30. package/dist/channels/commands.js +129 -0
  31. package/dist/channels/engine/cell.js +143 -0
  32. package/dist/channels/engine/diff.js +143 -0
  33. package/dist/channels/engine/engine.js +416 -0
  34. package/dist/channels/engine/index.js +8 -0
  35. package/dist/channels/engine/input-keys.js +86 -0
  36. package/dist/channels/engine/input.js +596 -0
  37. package/dist/channels/engine/memory-io.js +71 -0
  38. package/dist/channels/engine/process-io.js +38 -0
  39. package/dist/channels/engine/types.js +17 -0
  40. package/dist/channels/engine/width.js +122 -0
  41. package/dist/channels/index.js +15 -0
  42. package/dist/channels/registry.js +56 -0
  43. package/dist/channels/sdk/admit.js +29 -0
  44. package/dist/channels/sdk/backend.js +102 -0
  45. package/dist/channels/sdk/cursor.js +56 -0
  46. package/dist/channels/sdk/index.js +20 -0
  47. package/dist/channels/sdk/jsonl.js +208 -0
  48. package/dist/channels/sdk/protocol.js +24 -0
  49. package/dist/channels/sdk/schema.js +91 -0
  50. package/dist/channels/sdk/wire-core.js +446 -0
  51. package/dist/channels/service.js +169 -0
  52. package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
  53. package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
  54. package/dist/channels/tui/autocomplete/popup.js +148 -0
  55. package/dist/channels/tui/autocomplete/provider.js +7 -0
  56. package/dist/channels/tui/autocomplete/token.js +68 -0
  57. package/dist/channels/tui/backend/ansi-rows.js +211 -0
  58. package/dist/channels/tui/backend/main-screen.js +207 -0
  59. package/dist/channels/tui/backend/osc.js +91 -0
  60. package/dist/channels/tui/backend/transcript.js +230 -0
  61. package/dist/channels/tui/backend/tui-backend.js +915 -0
  62. package/dist/channels/tui/editor/editor-model.js +543 -0
  63. package/dist/channels/tui/editor/editor-view.js +140 -0
  64. package/dist/channels/tui/editor/editor.js +275 -0
  65. package/dist/channels/tui/editor/undo-stack.js +35 -0
  66. package/dist/channels/tui/editor/visual-lines.js +126 -0
  67. package/dist/channels/tui/editor/word-nav.js +106 -0
  68. package/dist/channels/tui/history/history-viewer.js +403 -0
  69. package/dist/channels/tui/index.js +3 -0
  70. package/dist/channels/tui/layout.js +167 -0
  71. package/dist/channels/tui/markdown/blocks.js +111 -0
  72. package/dist/channels/tui/markdown/inline.js +75 -0
  73. package/dist/channels/tui/markdown/markdown.js +182 -0
  74. package/dist/channels/tui/memory/memory-viewer.js +540 -0
  75. package/dist/channels/tui/overlay/alt-screen.js +105 -0
  76. package/dist/channels/tui/overlay/overlay.js +57 -0
  77. package/dist/channels/tui/overlay/select-confirm.js +156 -0
  78. package/dist/channels/tui/panels/todo-panel.js +54 -0
  79. package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
  80. package/dist/channels/tui/scroll/scroll-view.js +254 -0
  81. package/dist/channels/tui/status/status-line.js +76 -0
  82. package/dist/channels/tui/text.js +38 -0
  83. package/dist/channels/tui/theme.js +41 -0
  84. package/dist/channels/types.js +11 -0
  85. package/dist/channels/ui-core.js +209 -0
  86. package/dist/checkpoint/capture.js +45 -0
  87. package/dist/checkpoint/codes.js +33 -0
  88. package/dist/checkpoint/command.js +82 -0
  89. package/dist/checkpoint/gate.js +48 -0
  90. package/dist/checkpoint/index.js +20 -0
  91. package/dist/checkpoint/restore.js +172 -0
  92. package/dist/checkpoint/store.js +247 -0
  93. package/dist/checkpoint/types.js +4 -0
  94. package/dist/checkpoint/walk.js +146 -0
  95. package/dist/compaction/ccr-tools.js +112 -0
  96. package/dist/compaction/ccr.js +84 -0
  97. package/dist/compaction/codes.js +21 -0
  98. package/dist/compaction/index.js +18 -0
  99. package/dist/compaction/policy.js +200 -0
  100. package/dist/compaction/service.js +422 -0
  101. package/dist/compaction/slots.js +121 -0
  102. package/dist/compaction/types.js +10 -0
  103. package/dist/context/codes.js +40 -0
  104. package/dist/context/events.js +160 -0
  105. package/dist/context/index.js +13 -0
  106. package/dist/context/logger.js +136 -0
  107. package/dist/context/scope.js +182 -0
  108. package/dist/context/workspace.js +104 -0
  109. package/dist/contracts/agent-events.d.ts +73 -0
  110. package/dist/contracts/agent-events.js +16 -0
  111. package/dist/contracts/api.d.ts +249 -0
  112. package/dist/contracts/api.js +299 -0
  113. package/dist/contracts/approval.d.ts +37 -0
  114. package/dist/contracts/approval.js +9 -0
  115. package/dist/contracts/env-ref.d.ts +41 -0
  116. package/dist/contracts/env-ref.js +32 -0
  117. package/dist/contracts/errors.d.ts +56 -0
  118. package/dist/contracts/errors.js +266 -0
  119. package/dist/contracts/events.d.ts +70 -0
  120. package/dist/contracts/events.js +329 -0
  121. package/dist/contracts/index.d.ts +32 -0
  122. package/dist/contracts/index.js +26 -0
  123. package/dist/contracts/llm.d.ts +275 -0
  124. package/dist/contracts/llm.js +12 -0
  125. package/dist/contracts/messages.d.ts +63 -0
  126. package/dist/contracts/messages.js +65 -0
  127. package/dist/contracts/redact.d.ts +46 -0
  128. package/dist/contracts/redact.js +223 -0
  129. package/dist/contracts/tools.d.ts +197 -0
  130. package/dist/contracts/tools.js +33 -0
  131. package/dist/contracts/types.d.ts +302 -0
  132. package/dist/contracts/types.js +50 -0
  133. package/dist/contracts/ui.d.ts +122 -0
  134. package/dist/contracts/ui.js +15 -0
  135. package/dist/conversation/agent-service.js +61 -0
  136. package/dist/conversation/approval-wiring.js +82 -0
  137. package/dist/conversation/backoff.js +38 -0
  138. package/dist/conversation/codes.js +47 -0
  139. package/dist/conversation/control-tools.js +102 -0
  140. package/dist/conversation/control.js +210 -0
  141. package/dist/conversation/driver.js +922 -0
  142. package/dist/conversation/index.js +12 -0
  143. package/dist/conversation/model-visible.js +169 -0
  144. package/dist/conversation/open-tools.js +126 -0
  145. package/dist/conversation/reseed.js +132 -0
  146. package/dist/conversation/sessions.js +215 -0
  147. package/dist/conversation/todo.js +203 -0
  148. package/dist/conversation/types.js +35 -0
  149. package/dist/conversation/wiring.js +204 -0
  150. package/dist/credentials/codes.js +59 -0
  151. package/dist/credentials/commands.js +186 -0
  152. package/dist/credentials/env-ref.js +34 -0
  153. package/dist/credentials/index.js +28 -0
  154. package/dist/credentials/migration.js +18 -0
  155. package/dist/credentials/oauth.js +266 -0
  156. package/dist/credentials/refresh.js +152 -0
  157. package/dist/credentials/secrets.js +118 -0
  158. package/dist/credentials/types.js +30 -0
  159. package/dist/exec/bash.js +279 -0
  160. package/dist/exec/codes.js +37 -0
  161. package/dist/exec/env.js +99 -0
  162. package/dist/exec/environment.js +39 -0
  163. package/dist/exec/git-guard.js +623 -0
  164. package/dist/exec/index.js +24 -0
  165. package/dist/exec/registry.js +120 -0
  166. package/dist/exec/spawn.js +281 -0
  167. package/dist/exec/tail.js +77 -0
  168. package/dist/exec/types.js +5 -0
  169. package/dist/goal/codes.js +33 -0
  170. package/dist/goal/command.js +175 -0
  171. package/dist/goal/fold.js +121 -0
  172. package/dist/goal/gates.js +122 -0
  173. package/dist/goal/index.js +22 -0
  174. package/dist/goal/migration.js +48 -0
  175. package/dist/goal/service.js +501 -0
  176. package/dist/goal/todo-tool.js +174 -0
  177. package/dist/goal/types.js +1 -0
  178. package/dist/goal/update-tool.js +46 -0
  179. package/dist/host/approval-cmd.js +276 -0
  180. package/dist/host/assembly.js +1146 -0
  181. package/dist/host/boot-failures.js +104 -0
  182. package/dist/host/budget-advisory.js +74 -0
  183. package/dist/host/budget-broadcast.js +50 -0
  184. package/dist/host/builtins.js +54 -0
  185. package/dist/host/cli.js +544 -0
  186. package/dist/host/codes.js +120 -0
  187. package/dist/host/config-schema.js +195 -0
  188. package/dist/host/conversation-stack.js +654 -0
  189. package/dist/host/core-plugins.js +1685 -0
  190. package/dist/host/credentials-cmd.js +42 -0
  191. package/dist/host/disclosure.js +30 -0
  192. package/dist/host/dispatch.js +105 -0
  193. package/dist/host/doors-cmd.js +154 -0
  194. package/dist/host/dump-config.js +44 -0
  195. package/dist/host/hook-dispatch-guard.js +27 -0
  196. package/dist/host/import-gate.js +153 -0
  197. package/dist/host/index.js +54 -0
  198. package/dist/host/issue-session.js +241 -0
  199. package/dist/host/loader.js +372 -0
  200. package/dist/host/main.js +161 -0
  201. package/dist/host/manifest.js +381 -0
  202. package/dist/host/mcp-entry.js +53 -0
  203. package/dist/host/plugin-boot.js +952 -0
  204. package/dist/host/plugin-context.js +672 -0
  205. package/dist/host/plugin-install.js +480 -0
  206. package/dist/host/plugin-reload.js +124 -0
  207. package/dist/host/plugin-store.js +462 -0
  208. package/dist/host/plugin-tools.js +359 -0
  209. package/dist/host/plugin-uninstall.js +257 -0
  210. package/dist/host/plugins-cmd.js +302 -0
  211. package/dist/host/plugins-command.js +130 -0
  212. package/dist/host/plugins-config.js +195 -0
  213. package/dist/host/prompt-sections.js +133 -0
  214. package/dist/host/run-entry.js +544 -0
  215. package/dist/host/runtime.js +209 -0
  216. package/dist/host/scheduler-tick.js +294 -0
  217. package/dist/host/serve-daemon.js +501 -0
  218. package/dist/host/serve-entry.js +279 -0
  219. package/dist/host/session-anchor.js +41 -0
  220. package/dist/host/sessions-cmd.js +219 -0
  221. package/dist/host/sessions-face.js +47 -0
  222. package/dist/host/settings-store.js +112 -0
  223. package/dist/host/signals.js +69 -0
  224. package/dist/host/single-instance.js +105 -0
  225. package/dist/host/subagent-factory.js +291 -0
  226. package/dist/host/testkit/harness.js +283 -0
  227. package/dist/host/testkit/index.js +14 -0
  228. package/dist/host/testkit/matrix.js +302 -0
  229. package/dist/host/tool-policy-store.js +177 -0
  230. package/dist/host/triggers.js +212 -0
  231. package/dist/host/tui-entry.js +216 -0
  232. package/dist/host/webui-bridge.js +165 -0
  233. package/dist/issue/codes.js +38 -0
  234. package/dist/issue/filter.js +146 -0
  235. package/dist/issue/github.js +116 -0
  236. package/dist/issue/index.js +17 -0
  237. package/dist/issue/mount.js +70 -0
  238. package/dist/issue/poll.js +58 -0
  239. package/dist/issue/service.js +341 -0
  240. package/dist/issue/tools.js +70 -0
  241. package/dist/issue/types.js +34 -0
  242. package/dist/issue/webhook.js +126 -0
  243. package/dist/llm/codes.js +77 -0
  244. package/dist/llm/complete.js +196 -0
  245. package/dist/llm/events.js +15 -0
  246. package/dist/llm/index.js +24 -0
  247. package/dist/llm/inflight.js +65 -0
  248. package/dist/llm/model-id.js +58 -0
  249. package/dist/llm/provider-face.d.ts +32 -0
  250. package/dist/llm/provider-face.js +34 -0
  251. package/dist/llm/recovery.js +146 -0
  252. package/dist/llm/runtime.js +46 -0
  253. package/dist/llm/stream-fn.js +147 -0
  254. package/dist/lsp/codes.js +31 -0
  255. package/dist/lsp/connection.js +147 -0
  256. package/dist/lsp/frame.js +110 -0
  257. package/dist/lsp/index.js +16 -0
  258. package/dist/lsp/inject.js +58 -0
  259. package/dist/lsp/instance.js +264 -0
  260. package/dist/lsp/service.js +522 -0
  261. package/dist/lsp/tools.js +53 -0
  262. package/dist/lsp/types.js +201 -0
  263. package/dist/mcp/bridge.js +222 -0
  264. package/dist/mcp/codes.js +21 -0
  265. package/dist/mcp/index.js +19 -0
  266. package/dist/mcp/jsonrpc.js +230 -0
  267. package/dist/mcp/service.js +125 -0
  268. package/dist/mcp/tools.js +145 -0
  269. package/dist/mcp/types.js +108 -0
  270. package/dist/memory/cite.js +131 -0
  271. package/dist/memory/codes.js +50 -0
  272. package/dist/memory/command.js +71 -0
  273. package/dist/memory/consolidate.js +219 -0
  274. package/dist/memory/cycle.js +109 -0
  275. package/dist/memory/dao.js +878 -0
  276. package/dist/memory/diff.js +238 -0
  277. package/dist/memory/extract.js +181 -0
  278. package/dist/memory/fts.js +58 -0
  279. package/dist/memory/index.js +42 -0
  280. package/dist/memory/inject.js +354 -0
  281. package/dist/memory/merge.js +178 -0
  282. package/dist/memory/migration.js +132 -0
  283. package/dist/memory/pollution.js +59 -0
  284. package/dist/memory/port.js +385 -0
  285. package/dist/memory/review.js +201 -0
  286. package/dist/memory/scan.js +89 -0
  287. package/dist/memory/tools.js +508 -0
  288. package/dist/memory/types.js +140 -0
  289. package/dist/obs/codes.js +38 -0
  290. package/dist/obs/db.js +115 -0
  291. package/dist/obs/index.js +20 -0
  292. package/dist/obs/rollup.js +61 -0
  293. package/dist/obs/service.js +363 -0
  294. package/dist/obs/session-tools.js +282 -0
  295. package/dist/obs/session-view.js +225 -0
  296. package/dist/obs/tool.js +97 -0
  297. package/dist/obs/types.js +1 -0
  298. package/dist/persist/audit.js +63 -0
  299. package/dist/persist/aux.js +39 -0
  300. package/dist/persist/codes.js +35 -0
  301. package/dist/persist/index.js +30 -0
  302. package/dist/persist/load-history.js +50 -0
  303. package/dist/persist/migrations.js +37 -0
  304. package/dist/persist/paths.js +108 -0
  305. package/dist/persist/persistence.js +229 -0
  306. package/dist/persist/schema.js +99 -0
  307. package/dist/persist/secret-box.js +95 -0
  308. package/dist/persist/store.js +706 -0
  309. package/dist/persist/write-behind.js +238 -0
  310. package/dist/safety/approval.js +171 -0
  311. package/dist/safety/bwrap.js +123 -0
  312. package/dist/safety/codes.js +57 -0
  313. package/dist/safety/danger.js +505 -0
  314. package/dist/safety/gate.js +226 -0
  315. package/dist/safety/index.js +24 -0
  316. package/dist/safety/presets.js +73 -0
  317. package/dist/safety/roots.js +139 -0
  318. package/dist/safety/sandbox.js +214 -0
  319. package/dist/safety/seatbelt.js +88 -0
  320. package/dist/safety/sensitive.js +34 -0
  321. package/dist/safety/tool-policy.js +203 -0
  322. package/dist/safety/types.js +13 -0
  323. package/dist/scheduler/codes.js +54 -0
  324. package/dist/scheduler/cron-backend.js +135 -0
  325. package/dist/scheduler/engine.js +280 -0
  326. package/dist/scheduler/gates.js +60 -0
  327. package/dist/scheduler/index.js +25 -0
  328. package/dist/scheduler/migration.js +22 -0
  329. package/dist/scheduler/runner.js +171 -0
  330. package/dist/scheduler/schedule.js +193 -0
  331. package/dist/scheduler/service.js +283 -0
  332. package/dist/scheduler/tick.js +139 -0
  333. package/dist/scheduler/types.js +1 -0
  334. package/dist/sdk/http.js +865 -0
  335. package/dist/sdk/index.js +22 -0
  336. package/dist/sdk/mcp.js +289 -0
  337. package/dist/sdk/plugin-route-registry.js +126 -0
  338. package/dist/sdk/plugin-routes.js +149 -0
  339. package/dist/sdk/security.js +116 -0
  340. package/dist/sdk/types.js +32 -0
  341. package/dist/session/budget.js +159 -0
  342. package/dist/session/codes.js +40 -0
  343. package/dist/session/derive.js +180 -0
  344. package/dist/session/event-data.js +1 -0
  345. package/dist/session/fork.js +72 -0
  346. package/dist/session/import-gates.js +148 -0
  347. package/dist/session/index.js +18 -0
  348. package/dist/session/recover.js +103 -0
  349. package/dist/session/session.js +279 -0
  350. package/dist/session/snapshot.js +107 -0
  351. package/dist/skills/agents.js +242 -0
  352. package/dist/skills/codes.js +51 -0
  353. package/dist/skills/discovery.js +266 -0
  354. package/dist/skills/frontmatter.js +215 -0
  355. package/dist/skills/index.js +28 -0
  356. package/dist/skills/load.js +118 -0
  357. package/dist/skills/manage.js +201 -0
  358. package/dist/skills/registry.js +138 -0
  359. package/dist/skills/render.js +124 -0
  360. package/dist/skills/sections.js +171 -0
  361. package/dist/skills/types.js +21 -0
  362. package/dist/subagent/codes.js +50 -0
  363. package/dist/subagent/declarative.js +55 -0
  364. package/dist/subagent/index.js +19 -0
  365. package/dist/subagent/notify.js +51 -0
  366. package/dist/subagent/provide.js +7 -0
  367. package/dist/subagent/registry.js +149 -0
  368. package/dist/subagent/service.js +338 -0
  369. package/dist/subagent/surface.js +44 -0
  370. package/dist/subagent/tool.js +153 -0
  371. package/dist/subagent/types.js +25 -0
  372. package/dist/tools/apply-patch.js +164 -0
  373. package/dist/tools/codes.js +109 -0
  374. package/dist/tools/fs.js +501 -0
  375. package/dist/tools/index.js +17 -0
  376. package/dist/tools/observed.js +99 -0
  377. package/dist/tools/pipeline.js +176 -0
  378. package/dist/tools/protected-read.js +54 -0
  379. package/dist/tools/registry.js +250 -0
  380. package/dist/tools/search.js +451 -0
  381. package/dist/tools/worktree.js +327 -0
  382. package/dist/web/codes.js +37 -0
  383. package/dist/web/gate.js +43 -0
  384. package/dist/web/hygiene.js +195 -0
  385. package/dist/web/index.js +16 -0
  386. package/dist/web/service.js +207 -0
  387. package/dist/web/ssrf-guard.js +44 -0
  388. package/dist/web/tool.js +76 -0
  389. package/dist/web/types.js +10 -0
  390. package/dist/webui/assets/index-B-t2O8gh.js +22 -0
  391. package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
  392. package/dist/webui/index.html +14 -0
  393. package/dist/webui/index.js +14 -0
  394. package/dist/webui/server.js +549 -0
  395. package/dist/webui/types.js +38 -0
  396. package/examples/README.md +26 -0
  397. package/examples/minimal-code-plugin/entry.js +43 -0
  398. package/examples/minimal-code-plugin/package.json +12 -0
  399. package/examples/pure-skill-pack/package.json +14 -0
  400. package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
  401. package/package.json +86 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * 引擎下载件(03 §10.3 引擎发现序条——`/browser install` 的下载原语)。
3
+ *
4
+ * - **下载原语不进模型工具面**(spec 明裁):显式命令(TUI/CLI)专用,
5
+ * 模型不可触发 150MB 级下载。
6
+ * - **域白名单钉 Chromium for Testing 官方两域**:元数据
7
+ * googlechromelabs.github.io + 工件 storage.googleapis.com——元数据给出
8
+ * 的下载 URL 同样过白名单(元数据被改也出不了白名单域)。
9
+ * - **流式落盘 + 摘要账本 TOFU 锚定**:首装计算 zip sha256 锚进
10
+ * `dataDir/browser/engine/ledger.json`;同版本重装摘要不符即拒执行
11
+ * (BROWSER_DIGEST_MISMATCH——fail-loud 不动盘)。
12
+ * - **手写 unzip 零新增依赖**:EOCD 反扫 → 中央目录遍历 → 本地头跳位 →
13
+ * store/deflate 两法解压(zlib.inflateRawSync);zip-slip 防御(拒绝
14
+ * `..` 与绝对路径条目)+ unix 属性行 chmod(可执行位——macOS app 内
15
+ * helper 二进制族需要)。
16
+ * - 平台映射 v1 收三席:mac-arm64 / mac-x64 / linux64(win32 与其余
17
+ * 诚实缺席——同发现序 win32 知名位缺席同律,指路显式 executablePath)。
18
+ */
19
+ import { createHash } from 'node:crypto';
20
+ import { inflateRawSync } from 'node:zlib';
21
+ import { BaseError } from '../contracts/index.js';
22
+ /* ---------------- 常量(域白名单与元数据单源) ---------------- */
23
+ /** CfT「最后已知良好版本」元数据(Stable 通道 + 全平台下载位) */
24
+ export const BROWSER_METADATA_URL = 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json';
25
+ /** 下载域白名单(元数据域 + 工件域——两域之外一律拒) */
26
+ export const BROWSER_DOWNLOAD_HOSTS = ['googlechromelabs.github.io', 'storage.googleapis.com'];
27
+ /** 引擎安装根(数据目录下——发现序第③步消费) */
28
+ export const BROWSER_ENGINE_DIRNAME = 'engine';
29
+ /** 摘要账本文件名(发现序③与 TOFU 锚定共用) */
30
+ export const BROWSER_LEDGER_NAME = 'ledger.json';
31
+ /** zip 体积帽(1.5GiB——防元数据被改指向巨型载荷的 sanity 防线) */
32
+ export const BROWSER_ZIP_MAX_BYTES = 1.5 * 1024 * 1024 * 1024;
33
+ /* ---------------- 平台映射 ---------------- */
34
+ /** 平台 + 架构 → CfT 工件名(v1 三席;缺席 = 平台不支持显式下载) */
35
+ export function platformArtifact(platform, arch) {
36
+ if (platform === 'darwin') {
37
+ if (arch === 'arm64')
38
+ return 'mac-arm64';
39
+ if (arch === 'x64')
40
+ return 'mac-x64';
41
+ return undefined;
42
+ }
43
+ if (platform === 'linux' && arch === 'x64')
44
+ return 'linux64';
45
+ return undefined; // win32/其余:v1 诚实缺席(发现序同律——指路显式 executablePath)
46
+ }
47
+ /** 工件名 → zip 根目录内可执行相对位 */
48
+ function executableRelOf(artifact) {
49
+ if (artifact === 'linux64')
50
+ return 'chrome-linux64/chrome';
51
+ // macOS:app bundle 内主二进制(CfT 命名「Google Chrome for Testing」)
52
+ return `chrome-${artifact}/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing`;
53
+ }
54
+ /* ---------------- 下载面(生产适配) ---------------- */
55
+ /** 生产下载面(Node ≥22 全局 fetch——web 流转异步迭代) */
56
+ export function defaultDownloadFace() {
57
+ return {
58
+ async fetchBinary(url) {
59
+ const res = await fetch(url);
60
+ if (res.body === null) {
61
+ // 无体应答(3xx 等):空流形——状态码由调用方执法
62
+ return { status: res.status, bytes: (async function* () { })() };
63
+ }
64
+ const body = res.body;
65
+ return {
66
+ status: res.status,
67
+ // web ReadableStream 在 Node ≥22 可异步迭代(for await 直读)
68
+ bytes: (async function* () {
69
+ for await (const chunk of body)
70
+ yield chunk;
71
+ })(),
72
+ };
73
+ },
74
+ };
75
+ }
76
+ /** EOCD 签名与最小长(含注释最大反扫窗 64KiB + 22) */
77
+ const EOCD_SIG = 0x06054b50;
78
+ const EOCD_MIN = 22;
79
+ const EOCD_SCAN = 65_557;
80
+ const CD_SIG = 0x02014b50;
81
+ const LFH_SIG = 0x04034b50;
82
+ /** 小端读(Buffer.readUInt32LE 的直写形——偏移自校验由调用方保证) */
83
+ const u16 = (b, o) => b.readUInt16LE(o);
84
+ const u32 = (b, o) => b.readUInt32LE(o);
85
+ /** 解析 EOCD → {中央目录偏移, 条目数}(ZIP64 形 v1 诚实不支持) */
86
+ function parseEocd(zip) {
87
+ const scanStart = Math.max(0, zip.length - EOCD_SCAN);
88
+ for (let i = zip.length - EOCD_MIN; i >= scanStart; i--) {
89
+ if (u32(zip, i) !== EOCD_SIG)
90
+ continue;
91
+ const entryCount = u16(zip, i + 10);
92
+ const cdOffset = u32(zip, i + 16);
93
+ // 多卷/ZIP64 形拒收:盘号/目录盘非零,或条目数与目录偏移触 0xFFFF/
94
+ // 0xFFFFFFFF 上界标记(EOCD 布局——尾字段注释长是 u16,勿读 u32 越界)
95
+ if (u16(zip, i + 4) !== 0 || u16(zip, i + 6) !== 0 || entryCount === 0xffff || cdOffset === 0xffffffff) {
96
+ throw new Error('引擎 zip 含多卷/ZIP64 形——v1 不支持(Chromium for Testing 工件不应出现此形)');
97
+ }
98
+ return { cdOffset, entryCount };
99
+ }
100
+ throw new Error('引擎 zip 缺 EOCD(非 zip 形或截断)');
101
+ }
102
+ /** 遍历中央目录(条目子集抽取;签名不符即拒——坏形不猜) */
103
+ function readCentralDirectory(zip) {
104
+ const { cdOffset, entryCount } = parseEocd(zip);
105
+ const entries = [];
106
+ let off = cdOffset;
107
+ for (let i = 0; i < entryCount; i++) {
108
+ if (off + 46 > zip.length || u32(zip, off) !== CD_SIG) {
109
+ throw new Error(`引擎 zip 中央目录坏形(第 ${i + 1} 条签名不符)`);
110
+ }
111
+ const method = u16(zip, off + 10);
112
+ const compressedSize = u32(zip, off + 20);
113
+ const nameLen = u16(zip, off + 28);
114
+ const extraLen = u16(zip, off + 30);
115
+ const commentLen = u16(zip, off + 32);
116
+ const externalAttrs = u32(zip, off + 38);
117
+ const localHeaderOffset = u32(zip, off + 42);
118
+ const name = zip.subarray(off + 46, off + 46 + nameLen).toString('utf8');
119
+ entries.push({ name, method, compressedSize, externalAttrs, localHeaderOffset });
120
+ off += 46 + nameLen + extraLen + commentLen;
121
+ }
122
+ return entries;
123
+ }
124
+ /** zip-slip 防御:条目名须为相对净形(拒 `..` 段与绝对形) */
125
+ function safeEntryName(name) {
126
+ if (name.startsWith('/') || name.includes('\\')) {
127
+ throw new Error(`zip-slip 防御拒绝条目:${JSON.stringify(name)}(绝对形/反斜杠)`);
128
+ }
129
+ for (const seg of name.split('/')) {
130
+ if (seg === '..')
131
+ throw new Error(`zip-slip 防御拒绝条目:${JSON.stringify(name)}(.. 段)`);
132
+ }
133
+ return name;
134
+ }
135
+ /** 解压单条目(本地头跳位 → store/deflate → Buffer) */
136
+ function extractEntry(zip, entry) {
137
+ const off = entry.localHeaderOffset;
138
+ if (off + 30 > zip.length || u32(zip, off) !== LFH_SIG) {
139
+ throw new Error(`引擎 zip 本地头坏形:${entry.name}`);
140
+ }
141
+ const nameLen = u16(zip, off + 26);
142
+ const extraLen = u16(zip, off + 28);
143
+ const dataStart = off + 30 + nameLen + extraLen;
144
+ const raw = zip.subarray(dataStart, dataStart + entry.compressedSize);
145
+ if (entry.method === 0)
146
+ return Buffer.from(raw); // store
147
+ if (entry.method === 8)
148
+ return inflateRawSync(raw); // deflate(raw 形)
149
+ throw new Error(`引擎 zip 条目用不支持压缩法 ${entry.method}:${entry.name}`);
150
+ }
151
+ /**
152
+ * 解压引擎 zip 到目标目录(手写最小 unzip——零依赖条款)。
153
+ * @returns 解出的文件条目数(目录条目不计)
154
+ */
155
+ export async function unzipTo(zip, destDir, fs) {
156
+ let files = 0;
157
+ for (const entry of readCentralDirectory(zip)) {
158
+ const name = safeEntryName(entry.name);
159
+ const target = `${destDir}/${name}`;
160
+ if (name.endsWith('/')) {
161
+ await fs.mkdir(target, { recursive: true });
162
+ continue;
163
+ }
164
+ // 父目录链先建(中央目录目录条目不保证先行)
165
+ const parent = name.lastIndexOf('/') === -1 ? destDir : `${destDir}/${name.slice(0, name.lastIndexOf('/'))}`;
166
+ await fs.mkdir(parent, { recursive: true });
167
+ await fs.writeFile(target, extractEntry(zip, entry));
168
+ files += 1;
169
+ // unix 属性行 chmod(高 16 位 mode;常规文件位在场才算——可执行位保
170
+ // macOS app 内 helper 二进制族)。chmod 面缺席静默跳过(窄面可选)。
171
+ const attrs = entry.externalAttrs >>> 16;
172
+ const mode = attrs & 0o7777;
173
+ if (mode !== 0 && (attrs & 0o170000) === 0o100000) {
174
+ await fs.chmod?.(target, mode);
175
+ }
176
+ }
177
+ return files;
178
+ }
179
+ /* ---------------- 安装主流程 ---------------- */
180
+ /** URL → 域(形态防御:非 http(s) 直拒) */
181
+ function hostOf(url) {
182
+ try {
183
+ const parsed = new URL(url);
184
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
185
+ return '';
186
+ return parsed.host;
187
+ }
188
+ catch {
189
+ return '';
190
+ }
191
+ }
192
+ /** 读既有账本(缺席/坏形 → undefined——坏账本当未装过,重锚 TOFU) */
193
+ async function readLedger(fs, path) {
194
+ try {
195
+ const raw = await fs.readFile(path, 'utf8');
196
+ const v = JSON.parse(raw);
197
+ if (typeof v.version === 'string' &&
198
+ typeof v.zipSha256 === 'string' &&
199
+ typeof v.executableRel === 'string' &&
200
+ v.version !== '' &&
201
+ v.zipSha256 !== '' &&
202
+ v.executableRel !== '') {
203
+ return {
204
+ version: v.version,
205
+ zipSha256: v.zipSha256,
206
+ executableRel: v.executableRel,
207
+ artifact: v.artifact ?? '',
208
+ installedAt: v.installedAt ?? '',
209
+ bytes: v.bytes ?? 0,
210
+ };
211
+ }
212
+ return undefined;
213
+ }
214
+ catch {
215
+ return undefined;
216
+ }
217
+ }
218
+ /** 元数据 JSON → Stable 版本 + 本平台工件 URL(白名单外 URL 响亮拒) */
219
+ function artifactUrlFromMetadata(raw, artifact) {
220
+ let parsed;
221
+ try {
222
+ parsed = JSON.parse(raw);
223
+ }
224
+ catch {
225
+ throw new Error('CfT 元数据非 JSON——上游形变或被劫持');
226
+ }
227
+ const channels = parsed.channels;
228
+ const stable = channels?.Stable;
229
+ const version = stable?.version;
230
+ const list = stable?.downloads?.chrome;
231
+ if (typeof version !== 'string' || !Array.isArray(list)) {
232
+ throw new Error('CfT 元数据缺 Stable.chrome 下载面——上游形变');
233
+ }
234
+ for (const item of list) {
235
+ if (item?.platform === artifact && typeof item.url === 'string') {
236
+ return { version, url: item.url };
237
+ }
238
+ }
239
+ throw new Error(`CfT 元数据无 ${artifact} 工件——平台工件缺席`);
240
+ }
241
+ /**
242
+ * 安装引擎(显式命令专用原语——/browser install 的执行体)。
243
+ *
244
+ * TOFU 语义:首装锚摘要;同版本重装摘要不符 = BROWSER_DIGEST_MISMATCH 拒
245
+ * 执行(不动盘);同版本在装且可执行在场 = 幂等零下载。
246
+ */
247
+ export async function installBrowserEngine(deps) {
248
+ const artifact = platformArtifact(deps.platform, deps.arch);
249
+ if (artifact === undefined) {
250
+ throw new Error(`当前平台(${deps.platform}/${deps.arch})无 Chromium for Testing 工件——v1 显式下载缺席。` +
251
+ '替代路径:安装系统 Chrome(发现序②自动发现)或经 config browser.executablePath / BERRY_AGENT_BROWSER_PATH 显式指定');
252
+ }
253
+ const engineDir = `${deps.dataDir}/browser/${BROWSER_ENGINE_DIRNAME}`;
254
+ const ledgerPath = `${engineDir}/${BROWSER_LEDGER_NAME}`;
255
+ const executableRel = executableRelOf(artifact);
256
+ // 元数据取(域白名单第一执法位——常量本身钉白名单域)
257
+ const metadataUrl = deps.metadataUrl ?? BROWSER_METADATA_URL;
258
+ if (!BROWSER_DOWNLOAD_HOSTS.includes(hostOf(metadataUrl))) {
259
+ throw new Error(`元数据 URL 域不在白名单:${metadataUrl}`);
260
+ }
261
+ const metaRes = await deps.download.fetchBinary(metadataUrl);
262
+ if (metaRes.status !== 200)
263
+ throw new Error(`CfT 元数据取失败(HTTP ${metaRes.status})`);
264
+ let metaRaw = '';
265
+ for await (const chunk of metaRes.bytes)
266
+ metaRaw += Buffer.from(chunk).toString('utf8');
267
+ const { version, url } = artifactUrlFromMetadata(metaRaw, artifact);
268
+ // 元数据给出的工件 URL 同过白名单(元数据被改也出不了白名单域)
269
+ if (!BROWSER_DOWNLOAD_HOSTS.includes(hostOf(url))) {
270
+ throw new Error(`工件 URL 域不在白名单:${url}(元数据可疑)`);
271
+ }
272
+ // TOFU 预检:同版本在装且可执行在场 → 幂等回执(零下载零写盘)
273
+ const existing = await readLedger(deps.fs, ledgerPath);
274
+ if (existing !== undefined && existing.version === version) {
275
+ const installed = `${engineDir}/${existing.executableRel}`;
276
+ try {
277
+ if ((await deps.fs.stat(installed)).isFile()) {
278
+ return {
279
+ version,
280
+ zipSha256: existing.zipSha256,
281
+ executableRel: existing.executableRel,
282
+ executablePath: installed,
283
+ alreadyInstalled: true,
284
+ bytes: 0,
285
+ };
286
+ }
287
+ }
288
+ catch {
289
+ // 可执行缺席(文件被删)——落修复安装腿(下载重验摘要)
290
+ }
291
+ }
292
+ // 流式下载 + 增量摘要(帽执法防巨型载荷)
293
+ const maxBytes = deps.maxZipBytes ?? BROWSER_ZIP_MAX_BYTES;
294
+ const res = await deps.download.fetchBinary(url);
295
+ if (res.status !== 200)
296
+ throw new Error(`引擎工件取失败(HTTP ${res.status})`);
297
+ const hash = createHash('sha256');
298
+ const parts = [];
299
+ let bytes = 0;
300
+ for await (const chunk of res.bytes) {
301
+ const buf = Buffer.from(chunk);
302
+ bytes += buf.length;
303
+ if (bytes > maxBytes) {
304
+ throw new Error(`引擎 zip 超体积帽(> ${maxBytes} 字节)——中止下载`);
305
+ }
306
+ hash.update(buf);
307
+ parts.push(buf);
308
+ }
309
+ const zipSha256 = hash.digest('hex');
310
+ const zip = Buffer.concat(parts);
311
+ // TOFU 执法:同版本重装摘要不符 = 拒执行(fail-loud 不动盘)
312
+ if (existing !== undefined && existing.version === version && existing.zipSha256 !== zipSha256) {
313
+ throw new BaseError('BROWSER_DIGEST_MISMATCH', `同版本重装摘要不符(version ${version}:账本 ${existing.zipSha256} ≠ 实测 ${zipSha256})——` +
314
+ 'TOFU 锚定拒绝执行;既有安装未动。请核查网络通路或清除账本后重装(后果自担)');
315
+ }
316
+ // 落盘:解压 + 可执行位 + 账本
317
+ await deps.fs.mkdir(engineDir, { recursive: true });
318
+ const files = await unzipTo(zip, engineDir, deps.fs);
319
+ const executablePath = `${engineDir}/${executableRel}`;
320
+ let executableOk = false;
321
+ try {
322
+ executableOk = (await deps.fs.stat(executablePath)).isFile();
323
+ }
324
+ catch {
325
+ executableOk = false;
326
+ }
327
+ if (!executableOk) {
328
+ throw new Error(`解压后可执行缺席:${executablePath}(工件形与预期不符)`);
329
+ }
330
+ await deps.fs.chmod?.(executablePath, 0o755); // 主二进制显式可执行(属性行之外的双保险)
331
+ const ledger = {
332
+ version,
333
+ zipSha256,
334
+ executableRel,
335
+ artifact,
336
+ installedAt: new Date().toISOString(),
337
+ bytes,
338
+ };
339
+ await deps.fs.writeFile(ledgerPath, JSON.stringify(ledger, null, 2));
340
+ deps.logger?.info?.(`browser 引擎已安装:Chromium for Testing ${version}(${artifact},${files} 文件,${bytes} 字节)→ ${executablePath}`);
341
+ return { version, zipSha256, executableRel, executablePath, alreadyInstalled: false, bytes };
342
+ }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * 页面上下文件(03 §10.3——per-session BrowserContext 隔离 + 十操作面)。
3
+ *
4
+ * 会话层编舞:Target.createBrowserContext(cookies/storage/缓存会话间隔离,
5
+ * disposeOnDetach 兜底)→ createTarget(about:blank 挂入专属 context)→
6
+ * attachToTarget(flatten:true 取 sessionId)→ Page/Runtime enable(console
7
+ * 事件回流面)。事件按 sessionId 分流路由在 cdp 件,本件只筛本会话帧。
8
+ *
9
+ * a11y 快照 ref 模型:DOM.getDocument 全树行走采可交互节点,按文档序派
10
+ * `@eN` 锚——ref 表 per-snapshot(每次快照重建,过期 ref 查表即拒不猜);
11
+ * click/type 查表后先 `DOM.scrollIntoViewIfNeeded` 滚入视口(折叠线外静默
12
+ * 假成功防线)再取盒模型中心点派输入事件。
13
+ *
14
+ * 安全卫生(SSRF 红线):navigate 入口 URL 先经 web 件同一卫生单源预检
15
+ * (HEAD + consumer 'navigate'——第三消费位同一 execute 同一在飞门);
16
+ * BaseError(WEB_ 族拦截)直传 fail-closed,普通网络失败不拦——两载体
17
+ * 网络面不同源,HEAD 不通不代表浏览器到不了。
18
+ */
19
+ import { BaseError } from '../contracts/index.js';
20
+ import { BROWSER_CONSOLE_RING_CAP, BROWSER_NAV_TIMEOUT_MS, BROWSER_SCREENSHOT_DIRNAME, BROWSER_SCREENSHOT_KEEP, } from './types.js';
21
+ /** 可交互标签 → a11y 角色词(INPUT 按 type 分叉另判) */
22
+ const INTERACTIVE_ROLES = {
23
+ A: 'link',
24
+ BUTTON: 'button',
25
+ SELECT: 'combobox',
26
+ TEXTAREA: 'textbox',
27
+ SUMMARY: 'button',
28
+ OPTION: 'option',
29
+ };
30
+ /** INPUT type → 角色词(缺席/未知 type 折 textbox) */
31
+ const INPUT_ROLES = {
32
+ checkbox: 'checkbox',
33
+ radio: 'radio',
34
+ submit: 'button',
35
+ button: 'button',
36
+ reset: 'button',
37
+ };
38
+ /** 命名键定义(key/code/windowsVirtualKeyCode 三元组——Input.dispatchKeyEvent 派发用) */
39
+ const NAMED_KEYS = {
40
+ Enter: { code: 'Enter', keyCode: 13, text: '\r' },
41
+ Tab: { code: 'Tab', keyCode: 9 },
42
+ Escape: { code: 'Escape', keyCode: 27 },
43
+ Backspace: { code: 'Backspace', keyCode: 8 },
44
+ Delete: { code: 'Delete', keyCode: 46 },
45
+ ArrowUp: { code: 'ArrowUp', keyCode: 38 },
46
+ ArrowDown: { code: 'ArrowDown', keyCode: 40 },
47
+ ArrowLeft: { code: 'ArrowLeft', keyCode: 37 },
48
+ ArrowRight: { code: 'ArrowRight', keyCode: 39 },
49
+ Home: { code: 'Home', keyCode: 36 },
50
+ End: { code: 'End', keyCode: 35 },
51
+ PageUp: { code: 'PageUp', keyCode: 33 },
52
+ PageDown: { code: 'PageDown', keyCode: 34 },
53
+ Space: { code: 'Space', keyCode: 32, text: ' ' },
54
+ };
55
+ /** 标签文本截断帽(快照行宽纪律——超长文本截断加省略记号) */
56
+ const LABEL_CAP = 80;
57
+ /** 平铺属性数组取值(DOM attributes = [name, value, name, value, …]) */
58
+ function attrOf(attributes, name) {
59
+ if (attributes === undefined)
60
+ return undefined;
61
+ for (let i = 0; i + 1 < attributes.length; i += 2) {
62
+ if (attributes[i] === name)
63
+ return attributes[i + 1];
64
+ }
65
+ return undefined;
66
+ }
67
+ /** 子树文本拼合(text 节点 nodeValue 连接——标签面取材) */
68
+ function textOf(node, depth = 0) {
69
+ if (depth > 24)
70
+ return ''; // 病态深树防爆栈
71
+ let out = node.nodeName === '#text' ? (node.nodeValue ?? '') : '';
72
+ for (const child of node.children ?? [])
73
+ out += textOf(child, depth + 1);
74
+ return out;
75
+ }
76
+ /** 标签截断(超帽截断加省略记号) */
77
+ function clampLabel(text) {
78
+ const t = text.trim().replace(/\s+/g, ' ');
79
+ return t.length > LABEL_CAP ? `${t.slice(0, LABEL_CAP)}…` : t;
80
+ }
81
+ /**
82
+ * 组页面上下文(service 专用——不直接挂工具面)。失败即抛(引擎死态
83
+ * BROWSER_CONNECT_FAILED 由调用方决定降级重试)。
84
+ */
85
+ export async function createBrowserPage(deps) {
86
+ const logger = deps.logger;
87
+ const navTimeoutMs = deps.navTimeoutMs ?? BROWSER_NAV_TIMEOUT_MS;
88
+ const now = () => deps.clock?.() ?? Date.now();
89
+ // 会话层编舞:专属 BrowserContext → 目标挂入 → flat 附着 → 域使能
90
+ const browserContext = (await deps.conn.send('Target.createBrowserContext', { disposeOnDetach: true }));
91
+ const target = (await deps.conn.send('Target.createTarget', {
92
+ url: 'about:blank',
93
+ browserContextId: browserContext.browserContextId,
94
+ }));
95
+ const attached = (await deps.conn.send('Target.attachToTarget', { targetId: target.targetId, flatten: true }));
96
+ const sessionId = attached.sessionId;
97
+ const send = (method, params, timeoutMs) => deps.conn.send(method, params, { sessionId, ...(timeoutMs !== undefined ? { timeoutMs } : {}) });
98
+ await send('Page.enable');
99
+ await send('Runtime.enable');
100
+ /* ---------------- console 回流(环形缓冲) ---------------- */
101
+ const ring = [];
102
+ const pushRing = (entry) => {
103
+ ring.push(entry);
104
+ if (ring.length > BROWSER_CONSOLE_RING_CAP)
105
+ ring.shift();
106
+ };
107
+ /** RemoteObject 折串(value 直取,缺席用 description/type 兜底) */
108
+ const renderArg = (arg) => {
109
+ const a = arg;
110
+ if (a?.value !== undefined && a.value !== null) {
111
+ return typeof a.value === 'string' ? a.value : JSON.stringify(a.value);
112
+ }
113
+ return a?.description ?? `<${a?.type ?? 'unknown'}>`;
114
+ };
115
+ const unsubscribe = deps.conn.onEvent((event) => {
116
+ if (event.sessionId !== sessionId)
117
+ return; // 只筛本会话帧
118
+ const params = event.params;
119
+ if (event.method === 'Runtime.consoleAPICalled') {
120
+ pushRing({
121
+ kind: 'console',
122
+ level: params.type ?? 'log',
123
+ text: (params.args ?? []).map(renderArg).join(' '),
124
+ timestamp: params.timestamp ?? now(),
125
+ });
126
+ }
127
+ else if (event.method === 'Runtime.exceptionThrown') {
128
+ const d = params.exceptionDetails;
129
+ const text = d?.exception?.description ??
130
+ (d?.exception?.value !== undefined ? renderArg(d.exception) : (d?.text ?? '未知异常'));
131
+ pushRing({ kind: 'exception', level: 'error', text, timestamp: now() });
132
+ }
133
+ });
134
+ /* ---------------- ref 表(per-snapshot) ---------------- */
135
+ let refTable = new Map();
136
+ let snapshotSeq = 0;
137
+ /** ref 查表(快照过期/幽灵 ref 拒——不猜节点;模型误用面走普通错非码族) */
138
+ const requireRef = (ref) => {
139
+ const entry = refTable.get(ref);
140
+ if (entry === undefined) {
141
+ throw new Error(`ref ${ref} 不在当前快照(快照过期或未取——先调 snapshot 再用其回执的 @eN 锚)`);
142
+ }
143
+ return entry;
144
+ };
145
+ /** 滚入视口(折叠线外静默假成功防线——交互前置统一动作) */
146
+ const scrollIntoView = (backendNodeId) => send('DOM.scrollIntoViewIfNeeded', { backendNodeId });
147
+ /** 截图序号(同刻多截图不撞名) */
148
+ let shotSeq = 0;
149
+ const page = {
150
+ sessionId,
151
+ async navigate(url) {
152
+ // SSRF 卫生预检:web 件同一 execute 第三消费位(同在飞门同卫生件)
153
+ try {
154
+ await deps.web.fetch(url, { method: 'HEAD', consumer: 'navigate' });
155
+ }
156
+ catch (err) {
157
+ if (err instanceof BaseError)
158
+ throw err; // WEB_ 族拦截——fail-closed 直传
159
+ logger?.debug?.(`导航预检普通失败(放行浏览器载体):${err instanceof Error ? err.message : String(err)}`);
160
+ }
161
+ const waitLoad = deps.conn.waitEvent('Page.loadEventFired', sessionId, navTimeoutMs);
162
+ await send('Page.navigate', { url }, navTimeoutMs);
163
+ await waitLoad;
164
+ // 终点 URL 从导航史读(服务端重定向落点——不信任请求原值)
165
+ const hist = (await send('Page.getNavigationHistory'));
166
+ const entry = hist.entries[hist.currentIndex];
167
+ return { url: entry?.url ?? url };
168
+ },
169
+ async back() {
170
+ return goHistory(-1);
171
+ },
172
+ async forward() {
173
+ return goHistory(1);
174
+ },
175
+ async snapshot() {
176
+ const doc = (await send('DOM.getDocument', { depth: -1, pierce: true }));
177
+ const next = new Map();
178
+ const lines = [];
179
+ let counter = 0;
180
+ /** 深度行走:可交互节点派锚(文档序);病态深树防爆栈 */
181
+ const walk = (node, depth) => {
182
+ if (depth > 64)
183
+ return;
184
+ const tag = node.nodeName ?? '';
185
+ let role;
186
+ if (tag === 'INPUT') {
187
+ const type = attrOf(node.attributes, 'type') ?? 'text';
188
+ role = INPUT_ROLES[type] ?? 'textbox';
189
+ }
190
+ else {
191
+ role = INTERACTIVE_ROLES[tag];
192
+ }
193
+ if (role !== undefined && typeof node.backendNodeId === 'number') {
194
+ counter += 1;
195
+ const ref = `@e${counter}`;
196
+ const label = clampLabel(textOf(node) ||
197
+ attrOf(node.attributes, 'aria-label') ||
198
+ attrOf(node.attributes, 'placeholder') ||
199
+ attrOf(node.attributes, 'value') ||
200
+ '');
201
+ const extra = [];
202
+ const href = attrOf(node.attributes, 'href');
203
+ if (role === 'link' && href !== undefined)
204
+ extra.push(clampLabel(href));
205
+ const name = attrOf(node.attributes, 'name');
206
+ if (name !== undefined)
207
+ extra.push(`name=${clampLabel(name)}`);
208
+ lines.push(`- ${role} ${JSON.stringify(label)}${extra.length > 0 ? `(${extra.join(' ')})` : ''} [ref=${ref}]`);
209
+ next.set(ref, { backendNodeId: node.backendNodeId, label });
210
+ }
211
+ for (const child of node.children ?? [])
212
+ walk(child, depth + 1);
213
+ };
214
+ if (doc.root !== undefined)
215
+ walk(doc.root, 0);
216
+ refTable = next; // per-snapshot 重建(旧锚全数作废——不混表)
217
+ snapshotSeq += 1;
218
+ return [`# 页面快照 ${snapshotSeq}(可交互节点 ${counter} 个——交互用 [ref=@eN] 锚)`, ...lines].join('\n');
219
+ },
220
+ async click(ref) {
221
+ const entry = requireRef(ref);
222
+ await scrollIntoView(entry.backendNodeId);
223
+ const box = (await send('DOM.getBoxModel', { backendNodeId: entry.backendNodeId }));
224
+ const quad = box.model?.content;
225
+ if (quad === undefined || quad.length < 8) {
226
+ throw new Error(`ref ${ref}(${entry.label})取不到盒模型——节点不可见或已离树,重新 snapshot`);
227
+ }
228
+ // content 八角(x,y×4)取包围盒中心
229
+ const xs = [quad[0], quad[2], quad[4], quad[6]];
230
+ const ys = [quad[1], quad[3], quad[5], quad[7]];
231
+ const x = (Math.min(...xs) + Math.max(...xs)) / 2;
232
+ const y = (Math.min(...ys) + Math.max(...ys)) / 2;
233
+ await send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
234
+ await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
235
+ },
236
+ async type(ref, text) {
237
+ const entry = requireRef(ref);
238
+ await scrollIntoView(entry.backendNodeId);
239
+ await send('DOM.focus', { backendNodeId: entry.backendNodeId });
240
+ await send('Input.insertText', { text });
241
+ },
242
+ async press(key) {
243
+ if (key.length === 1) {
244
+ // 单字符:rawKeyDown(key/code 推导,VK 码取大写 ASCII——字母键惯例)→ char(text 面)→ keyUp
245
+ const vk = key.toUpperCase().charCodeAt(0);
246
+ const code = key === ' ' ? 'Space' : `Key${key.toUpperCase()}`;
247
+ await send('Input.dispatchKeyEvent', { type: 'rawKeyDown', key, code, windowsVirtualKeyCode: vk });
248
+ await send('Input.dispatchKeyEvent', { type: 'char', key, text: key });
249
+ await send('Input.dispatchKeyEvent', { type: 'keyUp', key, code, windowsVirtualKeyCode: vk });
250
+ return;
251
+ }
252
+ const def = NAMED_KEYS[key];
253
+ if (def === undefined) {
254
+ throw new Error(`不支持的按键 ${JSON.stringify(key)}——命名键词表或单字符`); // schema 已收词表——此为防御位
255
+ }
256
+ await send('Input.dispatchKeyEvent', {
257
+ type: 'rawKeyDown',
258
+ key,
259
+ code: def.code,
260
+ windowsVirtualKeyCode: def.keyCode,
261
+ });
262
+ if (def.text !== undefined)
263
+ await send('Input.dispatchKeyEvent', { type: 'char', key, text: def.text });
264
+ await send('Input.dispatchKeyEvent', { type: 'keyUp', key, code: def.code, windowsVirtualKeyCode: def.keyCode });
265
+ },
266
+ async scroll(direction, amountPx) {
267
+ const metrics = (await send('Page.getLayoutMetrics'));
268
+ const vp = metrics.cssVisualViewport;
269
+ const x = (vp?.width ?? 1280) / 2;
270
+ const y = (vp?.height ?? 720) / 2;
271
+ // yDistance 正数向下(Chrome 惯例——synthesizeScrollGesture 滚动量)
272
+ await send('Input.synthesizeScrollGesture', {
273
+ x,
274
+ y,
275
+ yDistance: direction === 'down' ? amountPx : -amountPx,
276
+ speed: 800,
277
+ });
278
+ },
279
+ async screenshot() {
280
+ const shot = (await send('Page.captureScreenshot', { format: 'png' }));
281
+ const data = shot.data ?? '';
282
+ const bytes = Buffer.byteLength(data, 'base64');
283
+ const dir = `${deps.dataDir}/browser/${BROWSER_SCREENSHOT_DIRNAME}`;
284
+ await deps.fs.mkdir(dir, { recursive: true });
285
+ shotSeq += 1;
286
+ const path = `${dir}/shot-${now()}-${shotSeq}.png`;
287
+ await deps.fs.writeFile(path, Buffer.from(data, 'base64'));
288
+ // 滚动清理:超保留量弃最旧(文件名时序序——字典序即时间序)
289
+ const names = (await deps.fs.readdir(dir)).filter((n) => n.endsWith('.png')).sort();
290
+ for (const victim of names.slice(0, Math.max(0, names.length - BROWSER_SCREENSHOT_KEEP))) {
291
+ await deps.fs.unlink(`${dir}/${victim}`).catch(() => { }); // 清理失败不阻回执
292
+ }
293
+ logger?.debug?.(`browser 截图落盘(${bytes} 字节):${path}`);
294
+ return { path, bytes };
295
+ },
296
+ async consoleLog(limit) {
297
+ return [...ring].slice(-limit);
298
+ },
299
+ async close() {
300
+ unsubscribe();
301
+ // 关停幂等吞错:引擎死态 send 快拒 BROWSER_CONNECT_FAILED——页面级
302
+ // 收口不因引擎已死而炸调用方(引擎级降级归 service)
303
+ await send('Target.closeTarget', { targetId: target.targetId }).catch(() => { });
304
+ await deps.conn
305
+ .send('Target.disposeBrowserContext', { browserContextId: browserContext.browserContextId })
306
+ .catch(() => { });
307
+ },
308
+ };
309
+ /** 导航史邻步(back/forward 共体——无邻步诚实 moved:false) */
310
+ async function goHistory(delta) {
311
+ const hist = (await send('Page.getNavigationHistory'));
312
+ const next = hist.entries[hist.currentIndex + delta];
313
+ if (next === undefined)
314
+ return { moved: false };
315
+ const waitLoad = deps.conn.waitEvent('Page.loadEventFired', sessionId, navTimeoutMs);
316
+ await send('Page.navigateToHistoryEntry', { entryId: next.id }, navTimeoutMs);
317
+ await waitLoad;
318
+ return { moved: true, url: next.url };
319
+ }
320
+ return page;
321
+ }