minimal-agent 0.13.0 → 0.15.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.
Files changed (474) hide show
  1. package/README.md +6 -5
  2. package/package.json +60 -4
  3. package/plugins/computer_use/.claude-plugin/plugin.json +1 -1
  4. package/plugins/cron-runner/src/commands.js +4 -4
  5. package/plugins/cron-runner/src/commands.ts +4 -4
  6. package/plugins/workflow-runner/.claude-plugin/plugin.json +2 -2
  7. package/plugins/workflow-runner/commands/workflow.md +57 -90
  8. package/plugins/workflow-runner/commands/workflows.md +4 -4
  9. package/plugins/workflow-runner/plugin.js +24 -13
  10. package/plugins/workflow-runner/plugin.ts +28 -13
  11. package/plugins/workflow-runner/src/discovery.js +160 -0
  12. package/plugins/workflow-runner/src/discovery.ts +194 -0
  13. package/plugins/workflow-runner/src/goalAnchor.js +53 -0
  14. package/plugins/workflow-runner/src/goalAnchor.ts +59 -0
  15. package/plugins/workflow-runner/src/index.js +197 -206
  16. package/plugins/workflow-runner/src/index.ts +248 -249
  17. package/plugins/workflow-runner/src/inputCoercion.js +40 -0
  18. package/plugins/workflow-runner/src/inputCoercion.ts +43 -0
  19. package/plugins/workflow-runner/src/journal.js +224 -0
  20. package/plugins/workflow-runner/src/journal.ts +274 -0
  21. package/plugins/workflow-runner/src/jsonSchema.js +132 -0
  22. package/plugins/workflow-runner/src/jsonSchema.ts +162 -0
  23. package/plugins/workflow-runner/src/moduleResolution.js +64 -0
  24. package/plugins/workflow-runner/src/moduleResolution.ts +71 -0
  25. package/plugins/workflow-runner/src/resourceInputs.js +220 -0
  26. package/plugins/workflow-runner/src/resourceInputs.ts +300 -0
  27. package/plugins/workflow-runner/src/runner.js +400 -489
  28. package/plugins/workflow-runner/src/runner.ts +488 -560
  29. package/plugins/workflow-runner/src/runtime/config.js +46 -0
  30. package/plugins/workflow-runner/src/runtime/config.ts +69 -0
  31. package/plugins/workflow-runner/src/runtime/context.js +1304 -0
  32. package/plugins/workflow-runner/src/runtime/context.ts +1786 -0
  33. package/plugins/workflow-runner/src/runtime/exec.js +34 -0
  34. package/plugins/workflow-runner/src/runtime/exec.ts +53 -0
  35. package/plugins/workflow-runner/src/runtime/flow.js +8 -0
  36. package/plugins/workflow-runner/src/runtime/flow.ts +12 -0
  37. package/plugins/workflow-runner/src/runtime/fs.js +27 -0
  38. package/plugins/workflow-runner/src/runtime/fs.ts +41 -0
  39. package/plugins/workflow-runner/src/runtime/model.js +6 -0
  40. package/plugins/workflow-runner/src/runtime/model.ts +8 -0
  41. package/plugins/workflow-runner/src/runtime/output.js +7 -0
  42. package/plugins/workflow-runner/src/runtime/output.ts +21 -0
  43. package/plugins/workflow-runner/src/runtime/scheduler.js +141 -0
  44. package/plugins/workflow-runner/src/runtime/scheduler.ts +166 -0
  45. package/plugins/workflow-runner/src/runtime/state.js +16 -0
  46. package/plugins/workflow-runner/src/runtime/state.ts +19 -0
  47. package/plugins/workflow-runner/src/runtime/tools.js +3 -0
  48. package/plugins/workflow-runner/src/runtime/tools.ts +6 -0
  49. package/plugins/workflow-runner/src/sandbox.js +141 -0
  50. package/plugins/workflow-runner/src/sandbox.ts +182 -0
  51. package/plugins/workflow-runner/src/scriptGuard.d.mts +38 -0
  52. package/plugins/workflow-runner/src/scriptGuard.mjs +449 -0
  53. package/plugins/workflow-runner/src/taskIdentity.js +151 -307
  54. package/plugins/workflow-runner/src/taskIdentity.ts +207 -355
  55. package/plugins/workflow-runner/src/toolRuntime.js +29 -8
  56. package/plugins/workflow-runner/src/toolRuntime.ts +36 -11
  57. package/plugins/workflow-runner/src/types.js +7 -211
  58. package/plugins/workflow-runner/src/types.ts +154 -623
  59. package/plugins/workflow-runner/src/util.js +47 -0
  60. package/plugins/workflow-runner/src/util.ts +51 -0
  61. package/plugins/workflow-runner/src/workflowContext.js +20 -0
  62. package/plugins/workflow-runner/src/workflowContext.ts +58 -0
  63. package/plugins/workflow-runner/src/workflowDependencies.js +36 -0
  64. package/plugins/workflow-runner/src/workflowDependencies.ts +59 -0
  65. package/plugins/workflow-runner/src/workflowFingerprint.js +118 -0
  66. package/plugins/workflow-runner/src/workflowFingerprint.ts +122 -0
  67. package/plugins/workflow-runner/src/workflowState.js +474 -802
  68. package/plugins/workflow-runner/src/workflowState.ts +575 -970
  69. package/plugins/workflow-runner/test/api.test.ts +223 -0
  70. package/plugins/workflow-runner/test/apiCapabilities.test.ts +614 -0
  71. package/plugins/workflow-runner/test/apiConcurrencyGate.test.ts +104 -0
  72. package/plugins/workflow-runner/test/apiEcho.test.ts +210 -0
  73. package/plugins/workflow-runner/test/apiReadText.test.ts +178 -0
  74. package/plugins/workflow-runner/test/apiResourceLedger.test.ts +184 -0
  75. package/plugins/workflow-runner/test/childWorkflow.test.ts +304 -0
  76. package/plugins/workflow-runner/test/discovery.test.ts +234 -0
  77. package/plugins/workflow-runner/test/goalAnchor.test.ts +106 -0
  78. package/plugins/workflow-runner/test/goalController.test.ts +317 -0
  79. package/plugins/workflow-runner/test/journalLanes.test.ts +198 -0
  80. package/plugins/workflow-runner/test/listResources.test.ts +106 -0
  81. package/plugins/workflow-runner/test/resourceInputs.test.ts +294 -0
  82. package/plugins/workflow-runner/test/sandbox.test.ts +126 -0
  83. package/plugins/workflow-runner/test/scheduler.test.ts +70 -0
  84. package/plugins/workflow-runner/test/scriptGuard.test.ts +276 -0
  85. package/plugins/workflow-runner/test/skillDocSync.test.ts +78 -0
  86. package/plugins/workflow-runner/test/taskIdentity.test.ts +288 -179
  87. package/plugins/workflow-runner/test/workflowFingerprint.test.ts +106 -0
  88. package/plugins/workflow-runner/test/workflowState.test.ts +429 -0
  89. package/skills/pptx/examples/assets/cover-bg.svg +23 -0
  90. package/skills/pptx/examples/assets/icon-spark.svg +5 -0
  91. package/skills/ui-design/SKILL.md +5 -2
  92. package/skills/ui-design/references/layout-composition.md +1 -1
  93. package/skills/workflow-creator/SKILL.md +344 -0
  94. package/skills/workflow-creator/assets/template/workflow.mjs +48 -0
  95. package/skills/workflow-creator/references/api.md +216 -0
  96. package/skills/workflow-creator/references/migration.md +103 -0
  97. package/skills/workflow-creator/references/patterns.md +779 -0
  98. package/skills/workflow-creator/scripts/list-resources.mjs +205 -0
  99. package/skills/workflow-creator/scripts/scaffold.mjs +148 -0
  100. package/skills/workflow-creator/scripts/validate.mjs +275 -0
  101. package/skills/workflow-creator/scripts/workflow-creator.test.mjs +82 -0
  102. package/src/advisor/advisorCoordinator.js +536 -0
  103. package/src/advisor/advisorCoordinator.ts +693 -0
  104. package/src/cli/configMenu.tsx +0 -1
  105. package/src/cli/print.js +27 -6
  106. package/src/cli/print.ts +48 -4
  107. package/src/cli/quickModelSetup.js +12 -1
  108. package/src/cli/quickModelSetup.tsx +15 -3
  109. package/src/cli/streamJson.js +5 -6
  110. package/src/cli/streamJson.ts +8 -4
  111. package/src/cli/wizards/advisorWizard.js +4 -2
  112. package/src/cli/wizards/advisorWizard.tsx +8 -2
  113. package/src/cli/wizards/contextAdvisorWizard.js +5 -4
  114. package/src/cli/wizards/contextAdvisorWizard.tsx +7 -6
  115. package/src/cli/wizards/providerPresets.js +18 -8
  116. package/src/cli/wizards/providerPresets.ts +18 -8
  117. package/src/collaboration/taskFlow.js +115 -0
  118. package/src/collaboration/taskFlow.ts +169 -0
  119. package/src/config/configDefaults.js +41 -1
  120. package/src/config/configDefaults.ts +43 -1
  121. package/src/config/configDiagnostics.ts +2 -2
  122. package/src/config/configFile.js +40 -9
  123. package/src/config/configFile.ts +60 -11
  124. package/src/config/configService.js +419 -74
  125. package/src/config/configService.ts +703 -145
  126. package/src/config/numericLimits.js +22 -0
  127. package/src/config/numericLimits.ts +24 -0
  128. package/src/config.js +1 -0
  129. package/src/config.ts +3 -2
  130. package/src/context/budgetPlan.js +1 -1
  131. package/src/context/budgetPlan.ts +1 -1
  132. package/src/context/compact.js +7 -2
  133. package/src/context/compact.ts +9 -2
  134. package/src/context/microCompactLite.js +212 -13
  135. package/src/context/microCompactLite.ts +261 -12
  136. package/src/context/sessionRegistry.js +8 -1
  137. package/src/context/sessionRegistry.ts +18 -5
  138. package/src/context/toolResultArchive.js +72 -0
  139. package/src/context/toolResultArchive.ts +95 -0
  140. package/src/goal/errors.js +8 -0
  141. package/src/goal/errors.ts +18 -0
  142. package/src/goal/goalRuntime.js +344 -0
  143. package/src/goal/goalRuntime.ts +466 -0
  144. package/src/goal/goalStore.js +200 -0
  145. package/src/goal/goalStore.ts +219 -0
  146. package/src/goal/index.js +4 -0
  147. package/src/goal/index.ts +4 -0
  148. package/src/goal/statusMapping.js +71 -0
  149. package/src/goal/statusMapping.ts +95 -0
  150. package/src/goal/types.js +7 -0
  151. package/src/goal/types.ts +179 -0
  152. package/src/goal/workflowController.js +306 -0
  153. package/src/goal/workflowController.ts +360 -0
  154. package/src/harness/evidenceLedger.js +2 -0
  155. package/src/harness/evidenceLedger.ts +1 -0
  156. package/src/harness/stallGuard.js +9 -3
  157. package/src/harness/stallGuard.ts +7 -3
  158. package/src/images/imageResolver.js +161 -0
  159. package/src/images/imageResolver.ts +189 -0
  160. package/src/infra/verification.js +34 -5
  161. package/src/infra/verification.ts +39 -4
  162. package/src/llm/client.js +79 -21
  163. package/src/llm/client.ts +182 -35
  164. package/src/llm/gatewayClient.js +201 -0
  165. package/src/llm/gatewayClient.ts +299 -0
  166. package/src/llm/messageProvenance.js +46 -0
  167. package/src/llm/messageProvenance.ts +58 -0
  168. package/src/llm/modelProvider.js +1 -0
  169. package/src/llm/modelProvider.ts +22 -0
  170. package/src/llm/providers/anthropicFormat.js +108 -1
  171. package/src/llm/providers/anthropicFormat.ts +115 -4
  172. package/src/llm/providers/openaiFormat.js +9 -0
  173. package/src/llm/providers/openaiFormat.ts +12 -3
  174. package/src/loop.js +328 -55
  175. package/src/loop.ts +382 -58
  176. package/src/main.js +4 -1
  177. package/src/main.tsx +4 -1
  178. package/src/managedExecution/index.js +2 -0
  179. package/src/managedExecution/index.ts +2 -0
  180. package/src/managedExecution/registry.js +15 -0
  181. package/src/managedExecution/registry.ts +20 -0
  182. package/src/managedExecution/types.js +1 -0
  183. package/src/managedExecution/types.ts +86 -0
  184. package/src/modelRef.js +9 -0
  185. package/src/modelRef.ts +31 -0
  186. package/src/multimodal.js +14 -28
  187. package/src/multimodal.ts +17 -33
  188. package/src/net/lanDiscovery.js +65 -0
  189. package/src/net/lanDiscovery.ts +82 -0
  190. package/src/plugin-sdk.js +28 -3
  191. package/src/plugin-sdk.ts +39 -6
  192. package/src/plugins/pluginApi.ts +16 -1
  193. package/src/plugins/pluginLoader.js +4 -0
  194. package/src/plugins/pluginLoader.ts +6 -0
  195. package/src/plugins/pluginRunner.js +139 -7
  196. package/src/plugins/pluginRunner.ts +173 -9
  197. package/src/plugins/slashFallback.js +113 -0
  198. package/src/plugins/slashFallback.ts +137 -0
  199. package/src/prompts/policyRegistry.js +12 -32
  200. package/src/prompts/policyRegistry.ts +15 -32
  201. package/src/prompts/promptAssembly.js +103 -12
  202. package/src/prompts/promptAssembly.ts +114 -12
  203. package/src/prompts/skillList.js +21 -3
  204. package/src/prompts/skillList.ts +27 -2
  205. package/src/prompts/stableSystemPrompt.js +18 -0
  206. package/src/prompts/stableSystemPrompt.ts +33 -0
  207. package/src/runtime/agentRuntimeContext.js +8 -2
  208. package/src/runtime/agentRuntimeContext.ts +17 -4
  209. package/src/session/runAgentTurn.js +78 -12
  210. package/src/session/runAgentTurn.ts +105 -23
  211. package/src/session/runTurn.js +10 -9
  212. package/src/session/runTurn.ts +17 -8
  213. package/src/session/sessionState.ts +2 -0
  214. package/src/team/auth.js +186 -0
  215. package/src/team/auth.ts +298 -0
  216. package/src/team/cli.js +105 -0
  217. package/src/team/cli.ts +122 -0
  218. package/src/team/onlineToolContract.js +64 -0
  219. package/src/team/onlineToolContract.ts +109 -0
  220. package/src/team/toolPolicy.js +36 -0
  221. package/src/team/toolPolicy.ts +75 -0
  222. package/src/tools/advisor/advisor.js +115 -50
  223. package/src/tools/advisor/advisor.ts +124 -51
  224. package/src/tools/advisor/advisorDecision.js +71 -0
  225. package/src/tools/advisor/advisorDecision.ts +85 -0
  226. package/src/tools/agent/agent.js +18 -5
  227. package/src/tools/agent/agent.ts +21 -7
  228. package/src/tools/agent/agentControl.js +246 -0
  229. package/src/tools/agent/agentControl.ts +284 -0
  230. package/src/tools/agent/agentTaskService.js +40 -1
  231. package/src/tools/agent/agentTaskService.ts +44 -1
  232. package/src/tools/agent/agentTypes.js +14 -2
  233. package/src/tools/agent/agentTypes.ts +15 -2
  234. package/src/tools/applypatch/applyPatch.js +362 -0
  235. package/src/tools/applypatch/applyPatch.ts +428 -0
  236. package/src/tools/bash/bash.js +22 -15
  237. package/src/tools/bash/bash.ts +23 -17
  238. package/src/tools/capabilities.js +10 -5
  239. package/src/tools/capabilities.ts +10 -5
  240. package/src/tools/computeruse/computerIntent.js +32 -0
  241. package/src/tools/computeruse/computerIntent.ts +46 -0
  242. package/src/tools/computeruse/telemetryLog.js +12 -0
  243. package/src/tools/computeruse/telemetryLog.ts +16 -0
  244. package/src/tools/computeruse/vision/telemetry.js +1 -0
  245. package/src/tools/computeruse/vision/telemetry.ts +1 -0
  246. package/src/tools/effectiveToolSet.js +4 -0
  247. package/src/tools/effectiveToolSet.ts +4 -0
  248. package/src/tools/index.js +21 -2
  249. package/src/tools/index.ts +33 -2
  250. package/src/tools/presentresult/presentresult.js +6 -15
  251. package/src/tools/presentresult/presentresult.ts +6 -15
  252. package/src/tools/read/read.js +62 -9
  253. package/src/tools/read/read.ts +63 -9
  254. package/src/tools/shared/fileUtils.js +1 -2
  255. package/src/tools/shared/fileUtils.ts +1 -2
  256. package/src/tools/shared/toolOutputStore.js +23 -1
  257. package/src/tools/shared/toolOutputStore.ts +22 -1
  258. package/src/tools/skill/skill.js +9 -3
  259. package/src/tools/skill/skill.ts +8 -4
  260. package/src/tools/toolPromptGuides.js +125 -0
  261. package/src/tools/toolPromptGuides.ts +146 -0
  262. package/src/tools/webbrowser/agentBrowserPath.js +19 -9
  263. package/src/tools/webbrowser/agentBrowserPath.ts +70 -28
  264. package/src/tools/webbrowser/browser.js +27 -11
  265. package/src/tools/webbrowser/browser.ts +68 -24
  266. package/src/tools/webbrowser/browserAutomation.js +329 -0
  267. package/src/tools/webbrowser/browserAutomation.ts +467 -0
  268. package/src/tools/webbrowser/browserAxState.js +97 -0
  269. package/src/tools/webbrowser/browserAxState.ts +142 -0
  270. package/src/tools/webbrowser/browserBatch.js +1578 -0
  271. package/src/tools/webbrowser/browserBatch.ts +2382 -0
  272. package/src/tools/webbrowser/browserCdp.js +208 -0
  273. package/src/tools/webbrowser/browserCdp.ts +255 -0
  274. package/src/tools/webbrowser/browserConditions.js +24 -0
  275. package/src/tools/webbrowser/browserConditions.ts +45 -0
  276. package/src/tools/webbrowser/browserContent.js +281 -0
  277. package/src/tools/webbrowser/browserContent.ts +438 -0
  278. package/src/tools/webbrowser/browserControl.js +131 -34
  279. package/src/tools/webbrowser/browserControl.ts +210 -56
  280. package/src/tools/webbrowser/browserDaemon.js +22 -14
  281. package/src/tools/webbrowser/browserDaemon.ts +54 -21
  282. package/src/tools/webbrowser/browserFrames.js +47 -0
  283. package/src/tools/webbrowser/browserFrames.ts +54 -0
  284. package/src/tools/webbrowser/browserInteractionResolver.js +17 -0
  285. package/src/tools/webbrowser/browserInteractionResolver.ts +40 -0
  286. package/src/tools/webbrowser/browserLocator.js +140 -0
  287. package/src/tools/webbrowser/browserLocator.ts +194 -0
  288. package/src/tools/webbrowser/browserOutcome.js +10 -17
  289. package/src/tools/webbrowser/browserOutcome.ts +48 -31
  290. package/src/tools/webbrowser/browserRecovery.js +58 -0
  291. package/src/tools/webbrowser/browserRecovery.ts +83 -0
  292. package/src/tools/webbrowser/browserRunRegistry.js +35 -3
  293. package/src/tools/webbrowser/browserRunRegistry.ts +91 -15
  294. package/src/tools/webbrowser/browserService.js +28 -2
  295. package/src/tools/webbrowser/browserService.ts +49 -9
  296. package/src/tools/webbrowser/browserStream.js +132 -33
  297. package/src/tools/webbrowser/browserStream.ts +224 -50
  298. package/src/tools/webbrowser/browserTabs.js +48 -15
  299. package/src/tools/webbrowser/browserTabs.ts +178 -96
  300. package/src/tools/webbrowser/webbrowser.js +679 -144
  301. package/src/tools/webbrowser/webbrowser.ts +975 -236
  302. package/src/tools/webfetch/preapproved.js +0 -2
  303. package/src/tools/webfetch/preapproved.ts +0 -2
  304. package/src/tools/websearch/websearch.js +129 -8
  305. package/src/tools/websearch/websearch.ts +138 -6
  306. package/src/types.ts +459 -40
  307. package/src/ui/App.js +377 -58
  308. package/src/ui/App.tsx +576 -111
  309. package/src/ui/LiveArea.js +14 -8
  310. package/src/ui/LiveArea.tsx +14 -10
  311. package/src/ui/MessageList.js +63 -20
  312. package/src/ui/MessageList.tsx +101 -16
  313. package/src/ui/ModelPicker.js +7 -4
  314. package/src/ui/ModelPicker.tsx +55 -8
  315. package/src/ui/hooks/useChat.js +152 -163
  316. package/src/ui/hooks/useChat.ts +396 -334
  317. package/src/ui/loginCommand.js +56 -0
  318. package/src/ui/loginCommand.ts +63 -0
  319. package/src/ui/toolProgress.js +41 -0
  320. package/src/ui/toolProgress.ts +44 -0
  321. package/src/web/browserBridge.js +20 -2
  322. package/src/web/browserBridge.ts +20 -2
  323. package/src/web/protocol.ts +1897 -194
  324. package/workflows/adaptive-research/workflow.mjs +86 -0
  325. package/workflows/e2e-capabilities/workflow.mjs +102 -0
  326. package/workflows/e2e-llm/workflow.mjs +52 -0
  327. package/workflows/e2e-smoke/workflow.mjs +59 -0
  328. package/workflows/e2e-vision-schema/README.md +48 -0
  329. package/workflows/e2e-vision-schema/workflow.mjs +149 -0
  330. package/workflows/e2e-write-greet/workflow.mjs +37 -0
  331. package/workflows/file-census/README.md +14 -0
  332. package/workflows/file-census/scripts/.gitkeep +2 -0
  333. package/workflows/file-census/workflow.mjs +110 -0
  334. package/workflows/goal/README.md +166 -0
  335. package/workflows/goal/prompts/audit.md +57 -0
  336. package/workflows/goal/prompts/contract.md +56 -0
  337. package/workflows/goal/prompts/iterate.md +37 -0
  338. package/workflows/goal/workflow.mjs +501 -0
  339. package/workflows/illustrate/README.md +90 -0
  340. package/workflows/illustrate/assets/engines.json +20 -0
  341. package/workflows/illustrate/assets/extract-baidu.js +21 -0
  342. package/workflows/illustrate/assets/extract-bing.js +33 -0
  343. package/workflows/illustrate/assets/extract-google.js +31 -0
  344. package/workflows/illustrate/workflow.mjs +301 -0
  345. package/workflows/plan/workflow.mjs +389 -0
  346. package/workflows/question-crops/README.md +35 -0
  347. package/workflows/question-crops/prompts/inventory.md +23 -0
  348. package/workflows/question-crops/scripts/image-ops.mjs +305 -0
  349. package/workflows/question-crops/scripts/image-ops.test.mjs +95 -0
  350. package/workflows/question-crops/workflow.mjs +172 -0
  351. package/workflows/ralph-loop/workflow.mjs +162 -0
  352. package/workflows/research/README.md +74 -0
  353. package/workflows/research/prompts/s0-goal.md +43 -0
  354. package/workflows/research/prompts/s1-early-exit.md +22 -0
  355. package/workflows/research/prompts/s1-map.md +58 -0
  356. package/workflows/research/prompts/s2-options.md +53 -0
  357. package/workflows/research/prompts/s3-deep-dive.md +56 -0
  358. package/workflows/research/prompts/s4-verify.md +74 -0
  359. package/workflows/research/prompts/s5-synthesis.md +81 -0
  360. package/workflows/research/prompts/s5b-fill-gaps.md +32 -0
  361. package/workflows/research/prompts/s6-package.md +32 -0
  362. package/workflows/research/scripts/build-report.mjs +98 -0
  363. package/workflows/research/workflow.mjs +288 -0
  364. package/workflows/wechat-reading-shots/prompts/collect-params.md +16 -0
  365. package/workflows/wechat-reading-shots/prompts/evaluate.md +20 -0
  366. package/workflows/wechat-reading-shots/prompts/open-book.md +28 -0
  367. package/workflows/wechat-reading-shots/prompts/read-page.md +13 -0
  368. package/workflows/wechat-reading-shots/prompts/turn-batch.md +24 -0
  369. package/workflows/wechat-reading-shots/test/control-flow.test.ts +329 -0
  370. package/workflows/wechat-reading-shots/workflow.mjs +266 -0
  371. package/workflows/wrong-question-docx/README.md +55 -0
  372. package/workflows/wrong-question-docx/prompts/figure-qa.md +8 -0
  373. package/workflows/wrong-question-docx/prompts/transcribe.md +25 -0
  374. package/workflows/wrong-question-docx/scripts/workbook.mjs +857 -0
  375. package/workflows/wrong-question-docx/test/control-flow.test.ts +165 -0
  376. package/workflows/wrong-question-docx/test/workbook.test.mjs +303 -0
  377. package/workflows/wrong-question-docx/workflow.mjs +485 -0
  378. package/plugins/workflow-runner/src/agentCommon.js +0 -354
  379. package/plugins/workflow-runner/src/agentCommon.ts +0 -435
  380. package/plugins/workflow-runner/src/controlFlow/branch.js +0 -40
  381. package/plugins/workflow-runner/src/controlFlow/branch.ts +0 -46
  382. package/plugins/workflow-runner/src/controlFlow/loop.js +0 -211
  383. package/plugins/workflow-runner/src/controlFlow/loop.ts +0 -248
  384. package/plugins/workflow-runner/src/controlFlow/pause.js +0 -129
  385. package/plugins/workflow-runner/src/controlFlow/pause.ts +0 -141
  386. package/plugins/workflow-runner/src/controlFlow/signal.js +0 -21
  387. package/plugins/workflow-runner/src/controlFlow/signal.ts +0 -29
  388. package/plugins/workflow-runner/src/controlFlow/types.js +0 -18
  389. package/plugins/workflow-runner/src/controlFlow/types.ts +0 -63
  390. package/plugins/workflow-runner/src/dslManifest.js +0 -72
  391. package/plugins/workflow-runner/src/dslManifest.ts +0 -127
  392. package/plugins/workflow-runner/src/expressions.js +0 -639
  393. package/plugins/workflow-runner/src/expressions.ts +0 -640
  394. package/plugins/workflow-runner/src/loader.js +0 -573
  395. package/plugins/workflow-runner/src/loader.ts +0 -721
  396. package/plugins/workflow-runner/src/multimodal.js +0 -38
  397. package/plugins/workflow-runner/src/multimodal.ts +0 -64
  398. package/plugins/workflow-runner/src/recovery.js +0 -126
  399. package/plugins/workflow-runner/src/recovery.ts +0 -156
  400. package/plugins/workflow-runner/src/stepExecutors/agent.js +0 -173
  401. package/plugins/workflow-runner/src/stepExecutors/agent.ts +0 -234
  402. package/plugins/workflow-runner/src/stepExecutors/assert.js +0 -42
  403. package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -52
  404. package/plugins/workflow-runner/src/stepExecutors/echo.js +0 -31
  405. package/plugins/workflow-runner/src/stepExecutors/echo.ts +0 -38
  406. package/plugins/workflow-runner/src/stepExecutors/llm.js +0 -99
  407. package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -133
  408. package/plugins/workflow-runner/src/stepExecutors/set.js +0 -49
  409. package/plugins/workflow-runner/src/stepExecutors/set.ts +0 -56
  410. package/plugins/workflow-runner/src/stepExecutors/skill.js +0 -111
  411. package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -140
  412. package/plugins/workflow-runner/src/stepExecutors/tool.js +0 -227
  413. package/plugins/workflow-runner/src/stepExecutors/tool.ts +0 -274
  414. package/plugins/workflow-runner/src/stepExecutors/verify.js +0 -61
  415. package/plugins/workflow-runner/src/stepExecutors/verify.ts +0 -72
  416. package/plugins/workflow-runner/src/stepPipeline.js +0 -277
  417. package/plugins/workflow-runner/src/stepPipeline.ts +0 -366
  418. package/plugins/workflow-runner/test/agent.test.ts +0 -587
  419. package/plugins/workflow-runner/test/agentLlmError.test.ts +0 -373
  420. package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +0 -65
  421. package/plugins/workflow-runner/test/captureMisbind.test.ts +0 -99
  422. package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -167
  423. package/plugins/workflow-runner/test/context-files.test.ts +0 -320
  424. package/plugins/workflow-runner/test/e2e.test.ts +0 -370
  425. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +0 -314
  426. package/plugins/workflow-runner/test/engineRobustness.test.ts +0 -234
  427. package/plugins/workflow-runner/test/expressions.test.ts +0 -439
  428. package/plugins/workflow-runner/test/foundation.test.ts +0 -126
  429. package/plugins/workflow-runner/test/graceful.test.ts +0 -139
  430. package/plugins/workflow-runner/test/greedyInputs.load.test.ts +0 -47
  431. package/plugins/workflow-runner/test/hostCapability.test.ts +0 -108
  432. package/plugins/workflow-runner/test/illustrate.load.test.ts +0 -183
  433. package/plugins/workflow-runner/test/jsonPayload.test.ts +0 -83
  434. package/plugins/workflow-runner/test/loader.test.ts +0 -617
  435. package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +0 -33
  436. package/plugins/workflow-runner/test/multimodal.test.ts +0 -342
  437. package/plugins/workflow-runner/test/output-schema.test.ts +0 -213
  438. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +0 -106
  439. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -269
  440. package/plugins/workflow-runner/test/ppt.load.test.ts +0 -1220
  441. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +0 -221
  442. package/plugins/workflow-runner/test/recovery.test.ts +0 -265
  443. package/plugins/workflow-runner/test/research.load.test.ts +0 -328
  444. package/plugins/workflow-runner/test/runner.test.ts +0 -911
  445. package/plugins/workflow-runner/test/runnerPreflight.test.ts +0 -436
  446. package/plugins/workflow-runner/test/skillTurnDone.test.ts +0 -71
  447. package/plugins/workflow-runner/test/stateLifecycle.test.ts +0 -198
  448. package/plugins/workflow-runner/test/stepPipeline.test.ts +0 -114
  449. package/plugins/workflow-runner/test/taskArbitration.test.ts +0 -354
  450. package/plugins/workflow-runner/test/taskLifecycle.test.ts +0 -324
  451. package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +0 -221
  452. package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +0 -29
  453. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +0 -168
  454. package/plugins/workflow-runner/test/webppt.load.test.ts +0 -250
  455. package/plugins/workflow-runner/test/word.load.test.ts +0 -208
  456. package/plugins/workflow-runner/test/workflows.smoke.test.ts +0 -76
  457. package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +0 -207
  458. package/src/advisorState.js +0 -22
  459. package/src/cli/configWizard.js +0 -293
  460. package/src/tools/toolProgressBridge.js +0 -48
  461. package/workflows/adaptive-research.yaml +0 -50
  462. package/workflows/book-review-short.yaml +0 -125
  463. package/workflows/e2e-write-greet.yaml +0 -31
  464. package/workflows/illustrate.yaml +0 -227
  465. package/workflows/plan.yaml +0 -470
  466. package/workflows/ppt.yaml +0 -1670
  467. package/workflows/ralph-loop.yaml +0 -201
  468. package/workflows/research.yaml +0 -873
  469. package/workflows/schema.json +0 -176
  470. package/workflows/webppt.yaml +0 -496
  471. package/workflows/wechat-reading-shots.yaml +0 -315
  472. package/workflows/word.yaml +0 -720
  473. package/workflows/youtube-shorts.yaml +0 -202
  474. /package/workflows/{research.md → research/DESIGN.md} +0 -0
@@ -0,0 +1,1578 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { copyFile, readdir, realpath, rename, rm, stat, writeFile, } from 'node:fs/promises';
4
+ import { basename, extname, isAbsolute, join, relative } from 'node:path';
5
+ import { z } from 'zod';
6
+ import { getWorkingDir } from '../../bootstrap/workingDir.js';
7
+ import { configService } from '../../config/configService.js';
8
+ import { toToolParameters } from '../../utils/zodToJson.js';
9
+ import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
10
+ import { resolveAgentBrowserPath } from './agentBrowserPath.js';
11
+ import { BROWSER_SESSION, browserArtifactDir, createArtifactPath, sessionArgs, spawnRunner, } from './browser.js';
12
+ import { BROWSER_TAB_MARKER_KEY, configureDownloads, getBrowserAutomation, } from './browserAutomation.js';
13
+ import { conciseAxTree, diffAxTree, normalizeAxTree, } from './browserAxState.js';
14
+ import { conditionToScript, } from './browserConditions.js';
15
+ import { CLIPBOARD_READ_EXPRESSION, CLIPBOARD_READ_TEXT_EXPRESSION, PAGE_ASSETS_EXPRESSION, browserAssetFetchExpression, bundlePageAssets, clipboardWriteExpression, clipboardWriteTextExpression, googleWorkspaceExportExpression, markdownFromPageContent, pageContentExpression, readUrlsInBackground, } from './browserContent.js';
16
+ import { detectBrowserChallenge } from './browserControl.js';
17
+ import { ensureDaemon } from './browserDaemon.js';
18
+ import { normalizeFrameInfo, normalizeFrameTree, } from './browserFrames.js';
19
+ import { resolveInteractionStrategy } from './browserInteractionResolver.js';
20
+ import { buildLocatorExpression, buildTargetExpression, } from './browserLocator.js';
21
+ import { withBrowserRecovery } from './browserRecovery.js';
22
+ import { browserRunRegistry } from './browserRunRegistry.js';
23
+ import { browserService } from './browserService.js';
24
+ import { getBrowserStream } from './browserStream.js';
25
+ import { claimConversationTabById, ensureConversationTab, parseTabList, } from './browserTabs.js';
26
+ const locatorLeafSchema = z.object({
27
+ by: z.enum([
28
+ 'css',
29
+ 'ax',
30
+ 'role',
31
+ 'text',
32
+ 'label',
33
+ 'placeholder',
34
+ 'alt',
35
+ 'title',
36
+ 'testid',
37
+ ]),
38
+ value: z.string(),
39
+ name: z.string().optional(),
40
+ exact: z.boolean().optional(),
41
+ strict: z.boolean().optional(),
42
+ nth: z.number().int().nonnegative().optional(),
43
+ has_text: z.string().optional(),
44
+ generation: z.number().int().positive().optional(),
45
+ frameId: z.string().min(1).optional(),
46
+ frame_id: z.string().min(1).optional(),
47
+ });
48
+ // Keep the public JSON Schema finite. A single nested scope and a finite frame
49
+ // chain cover the browser pages we support while avoiding recursive-schema
50
+ // fallbacks that turn the model contract into `any`.
51
+ const locatorSchema = z.object({
52
+ ...locatorLeafSchema.shape,
53
+ within: locatorLeafSchema.optional(),
54
+ frame: z.array(locatorLeafSchema).optional(),
55
+ framePath: z.array(locatorLeafSchema).optional(),
56
+ frame_path: z.array(locatorLeafSchema).optional(),
57
+ });
58
+ const expectationSchema = z.object({
59
+ navigation: z
60
+ .object({
61
+ url: z.string().optional(),
62
+ wait_until: z.enum(['commit', 'domcontentloaded', 'load']).optional(),
63
+ })
64
+ .optional(),
65
+ new_tab: z.object({ url: z.string().optional() }).optional(),
66
+ download: z.object({ filename: z.string().optional() }).optional(),
67
+ file_chooser: z.object({ files: z.array(z.string()).optional() }).optional(),
68
+ conditions: z
69
+ .array(z.union([
70
+ z.object({ type: z.literal('url'), pattern: z.string() }),
71
+ z.object({
72
+ type: z.literal('text'),
73
+ value: z.string(),
74
+ exact: z.boolean().optional(),
75
+ }),
76
+ z.object({
77
+ type: z.literal('element-visible'),
78
+ locator: locatorSchema.optional(),
79
+ selector: z.string().optional(),
80
+ }),
81
+ z.object({
82
+ type: z.literal('element-hidden'),
83
+ locator: locatorSchema.optional(),
84
+ selector: z.string().optional(),
85
+ }),
86
+ z.object({
87
+ type: z.literal('load-state'),
88
+ state: z.enum(['domcontentloaded', 'load', 'networkidle']),
89
+ }),
90
+ z.object({ type: z.literal('page-change') }),
91
+ z.object({
92
+ type: z.literal('download'),
93
+ filename: z.string().optional(),
94
+ }),
95
+ z.object({ type: z.literal('new-tab'), url: z.string().optional() }),
96
+ z.object({
97
+ type: z.literal('dialog'),
98
+ dialogType: z
99
+ .enum(['alert', 'confirm', 'prompt', 'beforeunload'])
100
+ .optional(),
101
+ }),
102
+ ]))
103
+ .max(8)
104
+ .optional(),
105
+ });
106
+ const stepSchema = z.object({
107
+ id: z.string().min(1).max(100).optional(),
108
+ action: z.enum([
109
+ 'navigate',
110
+ 'click',
111
+ 'doubleClick',
112
+ 'fill',
113
+ 'type',
114
+ 'hover',
115
+ 'press',
116
+ 'check',
117
+ 'uncheck',
118
+ 'setChecked',
119
+ 'getState',
120
+ 'drag',
121
+ 'wait',
122
+ 'waitForURL',
123
+ 'waitForDownload',
124
+ 'waitForFileChooser',
125
+ 'snapshot',
126
+ 'frameInfo',
127
+ 'visibleDom',
128
+ 'clickNode',
129
+ 'typeNode',
130
+ 'scrollNode',
131
+ 'upload',
132
+ 'pageAssets',
133
+ 'listPageAssets',
134
+ 'bundlePageAssets',
135
+ 'exportContent',
136
+ 'readUrls',
137
+ 'clipboardReadText',
138
+ 'clipboardWriteText',
139
+ 'clipboardRead',
140
+ 'clipboardWrite',
141
+ ]),
142
+ selector: z.string().optional(),
143
+ locator: locatorSchema.optional(),
144
+ trigger_selector: z.string().optional(),
145
+ trigger_locator: locatorSchema.optional(),
146
+ value: z.string().optional(),
147
+ files: z.array(z.string()).optional(),
148
+ key: z.string().optional(),
149
+ node_id: z.number().int().nonnegative().optional(),
150
+ checked: z.boolean().optional(),
151
+ url: z.string().optional(),
152
+ wait_ms: z.number().int().min(1).max(120_000).optional(),
153
+ drag_path: z
154
+ .array(z.object({ x: z.number(), y: z.number() }))
155
+ .min(2)
156
+ .optional(),
157
+ expect: expectationSchema.optional(),
158
+ intent: z.enum(['normal', 'submit']).optional(),
159
+ export_format: z
160
+ .enum([
161
+ 'text',
162
+ 'txt',
163
+ 'markdown',
164
+ 'md',
165
+ 'html',
166
+ 'json',
167
+ 'page',
168
+ 'google-workspace',
169
+ 'youtube-transcript',
170
+ 'pdf',
171
+ 'docx',
172
+ 'xlsx',
173
+ 'csv',
174
+ 'pptx',
175
+ ])
176
+ .optional(),
177
+ urls: z.array(z.string().url()).max(20).optional(),
178
+ concurrency: z.number().int().min(1).max(8).optional(),
179
+ content_type: z.enum(['text', 'html', 'domSnapshot']).optional(),
180
+ bundle_dir: z.string().optional(),
181
+ bundle_assets: z.boolean().optional(),
182
+ clipboard_text: z.string().optional(),
183
+ clipboard_items: z
184
+ .array(z.object({ type: z.string().min(1), base64: z.string() }))
185
+ .optional(),
186
+ is_multiple: z.boolean().optional(),
187
+ artifact: z
188
+ .object({
189
+ save_as: z.string().min(1).max(255),
190
+ label: z.string().max(200).optional(),
191
+ })
192
+ .optional(),
193
+ });
194
+ const inputSchema = z.object({
195
+ tab_id: z.string().optional(),
196
+ retention: z
197
+ .enum(['ephemeral', 'conversation', 'handoff', 'deliverable'])
198
+ .optional(),
199
+ tab_title: z.string().max(200).optional(),
200
+ steps: z.array(stepSchema).min(1).max(50),
201
+ stop_on_error: z.boolean().default(true),
202
+ timeout_ms: z.number().int().min(1_000).max(300_000).default(120_000),
203
+ final_snapshot: z.enum(['none', 'auto', 'full', 'diff']).default('auto'),
204
+ capture: z.enum(['none', 'state', 'screenshot', 'both']).default('state'),
205
+ });
206
+ const snapshots = new Map();
207
+ const MAX_SNAPSHOTS = 256;
208
+ const SNAPSHOT_TTL_MS = 15 * 60_000;
209
+ function jsString(value) {
210
+ return JSON.stringify(value);
211
+ }
212
+ const DOWNLOAD_MIME_BY_EXT = {
213
+ '.csv': 'text/csv',
214
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
215
+ '.pdf': 'application/pdf',
216
+ '.png': 'image/png',
217
+ '.jpg': 'image/jpeg',
218
+ '.jpeg': 'image/jpeg',
219
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
220
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
221
+ '.zip': 'application/zip',
222
+ };
223
+ async function hashFile(path) {
224
+ const hash = createHash('sha256');
225
+ for await (const chunk of createReadStream(path))
226
+ hash.update(chunk);
227
+ return hash.digest('hex');
228
+ }
229
+ async function materializeDownloadArtifact(sourcePath, saveAs) {
230
+ const name = basename(saveAs);
231
+ if (!name || name !== saveAs || name === '.' || name === '..')
232
+ throw new Error('artifact.save_as 只能是工作区根目录下的文件名');
233
+ const root = await realpath(getWorkingDir()).catch(() => getWorkingDir());
234
+ const destination = join(root, name);
235
+ const source = await realpath(sourcePath);
236
+ await stat(source);
237
+ const temporary = join(root, `.${name}.${randomUUID()}.partial`);
238
+ try {
239
+ await copyFile(source, temporary);
240
+ await rename(temporary, destination);
241
+ }
242
+ finally {
243
+ await rm(temporary, { force: true }).catch(() => undefined);
244
+ }
245
+ const info = await stat(destination);
246
+ const relativePath = name;
247
+ return {
248
+ status: 'final',
249
+ summary: `已下载并提交 ${name}`,
250
+ files: [
251
+ {
252
+ relativePath,
253
+ sha256: await hashFile(destination),
254
+ mime: DOWNLOAD_MIME_BY_EXT[extname(name).toLowerCase()] ??
255
+ 'application/octet-stream',
256
+ size: info.size,
257
+ modifiedAt: info.mtimeMs,
258
+ verification: 'not_run',
259
+ purpose: 'deliverable',
260
+ },
261
+ ],
262
+ warnings: [],
263
+ };
264
+ }
265
+ async function waitForDownloadedFile(directory, item, timeout, signal) {
266
+ const deadline = Date.now() + timeout;
267
+ let previousPath = '';
268
+ let previousSize = -1;
269
+ let stableSamples = 0;
270
+ while (Date.now() < deadline) {
271
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
272
+ const candidates = await Promise.all(entries
273
+ .filter((entry) => entry.isFile() && !entry.name.endsWith('.crdownload'))
274
+ .map(async (entry) => {
275
+ const path = join(directory, entry.name);
276
+ const info = await stat(path).catch(() => null);
277
+ return info
278
+ ? { path, name: entry.name, mtimeMs: info.mtimeMs, size: info.size }
279
+ : null;
280
+ }));
281
+ const usable = candidates
282
+ .filter((candidate) => candidate !== null)
283
+ .filter((candidate) => candidate.mtimeMs >= item.startedAt - 1_000)
284
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
285
+ const exact = usable.find((candidate) => candidate.name === item.filename);
286
+ const candidate = exact ?? usable[0];
287
+ if (candidate) {
288
+ const expectedSize = item.totalBytes;
289
+ const sizeMatches = expectedSize === undefined ||
290
+ expectedSize <= 0 ||
291
+ candidate.size === expectedSize;
292
+ if (sizeMatches) {
293
+ if (candidate.path === previousPath && candidate.size === previousSize)
294
+ stableSamples += 1;
295
+ else {
296
+ previousPath = candidate.path;
297
+ previousSize = candidate.size;
298
+ stableSamples = 1;
299
+ }
300
+ if (stableSamples >= 2)
301
+ return candidate.path;
302
+ }
303
+ }
304
+ await sleepWithSignal(100, signal);
305
+ }
306
+ throw new Error(`下载已完成但文件未稳定落盘:${item.filename}`);
307
+ }
308
+ async function materializeExportArtifact(value, format, saveAs) {
309
+ const name = basename(saveAs);
310
+ if (!name || name !== saveAs || name === '.' || name === '..')
311
+ throw new Error('artifact.save_as 只能是工作区根目录下的文件名');
312
+ const root = await realpath(getWorkingDir()).catch(() => getWorkingDir());
313
+ const destination = join(root, name);
314
+ let bytes;
315
+ if (value &&
316
+ typeof value === 'object' &&
317
+ '__browser_pdf_base64' in value &&
318
+ typeof value
319
+ .__browser_pdf_base64 === 'string') {
320
+ bytes = Buffer.from(value.__browser_pdf_base64, 'base64');
321
+ }
322
+ else if (value &&
323
+ typeof value === 'object' &&
324
+ '__browser_binary_base64' in value &&
325
+ typeof value
326
+ .__browser_binary_base64 === 'string') {
327
+ bytes = Buffer.from(value.__browser_binary_base64, 'base64');
328
+ }
329
+ else if (format === 'html' && value && typeof value === 'object') {
330
+ bytes = Buffer.from(String(value.html ?? ''), 'utf8');
331
+ }
332
+ else if (format === 'json') {
333
+ bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
334
+ }
335
+ else if (typeof value === 'string') {
336
+ bytes = Buffer.from(value.endsWith('\n') ? value : `${value}\n`, 'utf8');
337
+ }
338
+ else if (value && typeof value === 'object' && 'text' in value) {
339
+ bytes = Buffer.from(`${String(value.text ?? '')}\n`, 'utf8');
340
+ }
341
+ else {
342
+ bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
343
+ }
344
+ const temporary = join(root, `.${name}.${randomUUID()}.partial`);
345
+ try {
346
+ await writeFile(temporary, bytes, { mode: 0o600 });
347
+ await rename(temporary, destination);
348
+ }
349
+ finally {
350
+ await rm(temporary, { force: true }).catch(() => undefined);
351
+ }
352
+ const info = await stat(destination);
353
+ return {
354
+ status: 'final',
355
+ summary: `已导出并提交 ${name}`,
356
+ files: [
357
+ {
358
+ relativePath: name,
359
+ sha256: await hashFile(destination),
360
+ mime: DOWNLOAD_MIME_BY_EXT[extname(name).toLowerCase()] ??
361
+ 'application/octet-stream',
362
+ size: info.size,
363
+ modifiedAt: info.mtimeMs,
364
+ verification: 'not_run',
365
+ purpose: 'deliverable',
366
+ },
367
+ ],
368
+ warnings: [],
369
+ };
370
+ }
371
+ export function mergeArtifactManifests(current, next) {
372
+ if (!current)
373
+ return next;
374
+ const files = new Map();
375
+ for (const file of [...current.files, ...next.files]) {
376
+ // A repeated save_as represents the latest version of the same artifact;
377
+ // distinct paths remain distinct formal deliverables.
378
+ files.set(file.relativePath, file);
379
+ }
380
+ const links = new Map();
381
+ for (const link of [...(current.links ?? []), ...(next.links ?? [])])
382
+ links.set(link.url, link);
383
+ const allFiles = [...files.values()];
384
+ const warnings = [...new Set([...current.warnings, ...next.warnings])];
385
+ return {
386
+ status: current.status === 'final' && next.status === 'final' && !warnings.length
387
+ ? 'final'
388
+ : 'draft',
389
+ summary: `已提交 ${allFiles.length} 个成果文件`,
390
+ files: allFiles,
391
+ ...(links.size ? { links: [...links.values()] } : {}),
392
+ warnings,
393
+ };
394
+ }
395
+ async function evaluate(cdp, expression, signal, frameId) {
396
+ if (frameId) {
397
+ const result = (await cdp.evaluateInFrame(frameId, expression, signal));
398
+ return result.result?.value ?? result.result?.description;
399
+ }
400
+ const result = (await cdp.send('Runtime.evaluate', {
401
+ expression,
402
+ returnByValue: true,
403
+ awaitPromise: true,
404
+ }, undefined, 30_000, signal));
405
+ return result.result?.value ?? result.result?.description;
406
+ }
407
+ async function waitFor(cdp, predicate, timeout, afterSequence = 0, signal) {
408
+ const pageEvent = (event) => {
409
+ // Browser.* events are browser-scoped and intentionally remain visible
410
+ // (downloads do not carry a page session in all Chromium versions).
411
+ // Page/DOM/Input/etc. events must belong to this connection's attached
412
+ // page, otherwise another tab can satisfy a navigation or dialog wait.
413
+ const pageDomain = /^(Accessibility|DOM|Input|Log|Network|Page|Performance|Runtime|Security)\./;
414
+ return (!pageDomain.test(event.method) ||
415
+ event.sessionId === cdp.attachedPageSessionId);
416
+ };
417
+ return new Promise((resolve, reject) => {
418
+ if (signal?.aborted) {
419
+ reject(signal.reason instanceof Error
420
+ ? signal.reason
421
+ : new Error('Browser event wait aborted'));
422
+ return;
423
+ }
424
+ const existing = cdp
425
+ .read(afterSequence)
426
+ .find((event) => pageEvent(event) && predicate(event));
427
+ if (existing)
428
+ return resolve(existing);
429
+ let settled = false;
430
+ let off = () => { };
431
+ const cleanup = () => {
432
+ clearTimeout(timer);
433
+ off();
434
+ signal?.removeEventListener('abort', onAbort);
435
+ };
436
+ const finish = (callback) => {
437
+ if (settled)
438
+ return;
439
+ settled = true;
440
+ cleanup();
441
+ callback();
442
+ };
443
+ const onAbort = () => finish(() => reject(signal?.reason instanceof Error
444
+ ? signal.reason
445
+ : new Error('Browser event wait aborted')));
446
+ const timer = setTimeout(() => finish(() => reject(new Error('Browser event wait timed out'))), timeout);
447
+ off = cdp.subscribe((event) => {
448
+ if (pageEvent(event) && predicate(event))
449
+ finish(() => resolve(event));
450
+ });
451
+ signal?.addEventListener('abort', onAbort, { once: true });
452
+ // Close the tiny read/subscribe race without changing the event cursor contract.
453
+ const raced = cdp
454
+ .read(afterSequence)
455
+ .find((event) => pageEvent(event) && predicate(event));
456
+ if (raced)
457
+ finish(() => resolve(raced));
458
+ });
459
+ }
460
+ // Some agent-browser/CDP endpoints expose Runtime/Page but not the optional
461
+ // Accessibility domain. Keep a deterministic DOM-semantic fallback so a
462
+ // missing AX domain does not make an otherwise usable supervised browser fail.
463
+ const domSemanticSnapshotExpression = `(() => Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6,p,li,a,button,input,textarea,select,[role],[contenteditable="true"]')).map((el,index) => { const r=el.getBoundingClientRect(); const s=getComputedStyle(el); const visible=!!(r.width||r.height)&&s.display!=='none'&&s.visibility!=='hidden'&&s.pointerEvents!=='none'; const tag=el.tagName.toLowerCase(); const explicit=el.getAttribute('role'); const role=explicit||(['button','a','textarea','select'].includes(tag)?({button:'button',a:'link',textarea:'textbox',select:'combobox'})[tag]:(tag==='input'?(['checkbox','radio','button','submit','reset'].includes((el.getAttribute('type')||'text').toLowerCase())?(el.getAttribute('type')||'text').toLowerCase():'textbox'):tag)); const name=(el.getAttribute('aria-label')||el.textContent||el.getAttribute('placeholder')||'').trim().replace(/\\s+/g,' ').slice(0,240); const value=el instanceof HTMLInputElement||el instanceof HTMLTextAreaElement||el instanceof HTMLSelectElement?el.value:undefined; return {node_id:index,role,text:name,value,visible,x:r.x,y:r.y,width:r.width,height:r.height}; }).filter((node) => node.visible).slice(0,500))()`;
464
+ async function readBrowserAxTree(cdp, signal) {
465
+ try {
466
+ return normalizeAxTree(await cdp.send('Accessibility.getFullAXTree', {}, undefined, 30_000, signal));
467
+ }
468
+ catch (error) {
469
+ const message = error instanceof Error ? error.message : String(error);
470
+ if (!/-32601|not found|unknown method/i.test(message))
471
+ throw error;
472
+ const domNodes = await evaluate(cdp, domSemanticSnapshotExpression, signal);
473
+ if (!Array.isArray(domNodes))
474
+ return [];
475
+ return normalizeAxTree({
476
+ nodes: domNodes.map((value) => {
477
+ const node = value && typeof value === 'object'
478
+ ? value
479
+ : {};
480
+ const number = (key) => typeof node[key] === 'number' ? node[key] : undefined;
481
+ return {
482
+ nodeId: `dom-${String(node.node_id ?? '')}`,
483
+ role: { value: String(node.role ?? 'generic') },
484
+ name: { value: String(node.text ?? '') },
485
+ ...(node.value !== undefined
486
+ ? { properties: [{ name: 'value', value: { value: node.value } }] }
487
+ : {}),
488
+ bounds: {
489
+ x: number('x') ?? 0,
490
+ y: number('y') ?? 0,
491
+ width: number('width') ?? 0,
492
+ height: number('height') ?? 0,
493
+ },
494
+ };
495
+ }),
496
+ });
497
+ }
498
+ }
499
+ async function snapshot(cdp, key, mode, signal) {
500
+ const now = Date.now();
501
+ for (const [staleKey, stale] of snapshots) {
502
+ if (now - stale.updatedAt > SNAPSHOT_TTL_MS)
503
+ snapshots.delete(staleKey);
504
+ }
505
+ const tree = await readBrowserAxTree(cdp, signal);
506
+ const url = String(await evaluate(cdp, 'location.href', signal).catch(() => ''));
507
+ const frameResult = (await cdp
508
+ .send('Page.getFrameTree', {}, undefined, 30_000, signal)
509
+ .catch(() => ({})));
510
+ const frames = normalizeFrameTree(frameResult.frameTree);
511
+ const prior = snapshots.get(key);
512
+ const revision = (prior?.revision ?? 0) + 1;
513
+ const id = randomUUID();
514
+ const generation = prior && prior.url !== url
515
+ ? prior.generation + 1
516
+ : (prior?.generation ?? 1);
517
+ const samePageNodes = prior && prior.url === url ? prior.nodes : undefined;
518
+ const diff = diffAxTree(samePageNodes, tree, generation, revision, mode === 'full' || (!samePageNodes && mode !== 'diff'));
519
+ snapshots.set(key, {
520
+ id,
521
+ revision,
522
+ generation,
523
+ url,
524
+ text: JSON.stringify(tree),
525
+ nodes: new Map(tree.map((node) => [node.id, node])),
526
+ updatedAt: now,
527
+ });
528
+ if (snapshots.size > MAX_SNAPSHOTS) {
529
+ const stale = [...snapshots.entries()]
530
+ .sort((left, right) => left[1].updatedAt - right[1].updatedAt)
531
+ .slice(0, snapshots.size - MAX_SNAPSHOTS);
532
+ for (const [staleKey] of stale)
533
+ snapshots.delete(staleKey);
534
+ }
535
+ const body = mode === 'full' || !samePageNodes ? { nodes: tree, ...diff } : diff;
536
+ const compact = JSON.stringify(body);
537
+ return {
538
+ mode: diff.full ? 'full' : 'diff',
539
+ snapshot_id: id,
540
+ ...(prior ? { base_snapshot_id: prior.id } : {}),
541
+ page_revision: revision,
542
+ generation,
543
+ tree: (mode === 'full' || !prior ? conciseAxTree(tree) : compact).slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS),
544
+ frames,
545
+ };
546
+ }
547
+ async function resolveAxLocator(cdp, locator, snapshotKey, signal) {
548
+ if (!locator || locator.by !== 'ax')
549
+ return locator;
550
+ const current = await readBrowserAxTree(cdp, signal);
551
+ const prior = snapshots.get(snapshotKey);
552
+ if (locator.generation !== undefined && !prior) {
553
+ throw new Error('AX 引用缺少对应 snapshot;请先 snapshot 后再使用 generation');
554
+ }
555
+ if (locator.generation !== undefined &&
556
+ prior &&
557
+ locator.generation !== prior.generation) {
558
+ throw new Error(`AX 引用已过期:引用 generation=${locator.generation},当前页面 generation=${prior.generation};请重新 snapshot`);
559
+ }
560
+ if (locator.generation !== undefined && prior) {
561
+ const currentUrl = String(await evaluate(cdp, 'location.href', signal).catch(() => ''));
562
+ if (currentUrl !== prior.url) {
563
+ throw new Error(`AX 引用已过期:页面已从 ${prior.url} 导航到 ${currentUrl};请重新 snapshot`);
564
+ }
565
+ }
566
+ const indexMatch = /^index:(\d+)$/.exec(locator.value);
567
+ const numeric = indexMatch ? Number(indexMatch[1]) : Number(locator.value);
568
+ const node = current.find((item) => item.id === locator.value) ??
569
+ (Number.isInteger(numeric) && numeric >= 0 && numeric < current.length
570
+ ? current[numeric]
571
+ : undefined);
572
+ if (!node)
573
+ throw new Error(`AX 引用未找到:${locator.value};请重新 snapshot`);
574
+ if (!node.name && !node.role)
575
+ throw new Error(`AX 引用没有可执行的语义信息:${locator.value}`);
576
+ const resolved = node.name
577
+ ? {
578
+ by: 'role',
579
+ value: node.role ?? 'generic',
580
+ name: node.name,
581
+ exact: true,
582
+ strict: true,
583
+ }
584
+ : { by: 'text', value: node.value ?? '', exact: true, strict: true };
585
+ const framePath = locator.frame ?? locator.framePath ?? locator.frame_path;
586
+ const frameId = locator.frameId ?? locator.frame_id;
587
+ return {
588
+ ...resolved,
589
+ ...(framePath?.length ? { frame: framePath } : {}),
590
+ ...(frameId ? { frameId } : {}),
591
+ };
592
+ }
593
+ function eventMatchesCondition(condition, event) {
594
+ if (condition.type === 'download') {
595
+ return (event.method === 'Browser.downloadWillBegin' &&
596
+ (!condition.filename ||
597
+ String(event.params.suggestedFilename ?? '') === condition.filename));
598
+ }
599
+ if (condition.type === 'new-tab') {
600
+ return (event.method === 'Target.targetCreated' &&
601
+ (!condition.url || JSON.stringify(event.params).includes(condition.url)));
602
+ }
603
+ if (condition.type === 'dialog') {
604
+ return (event.method === 'Page.javascriptDialogOpening' &&
605
+ (!condition.dialogType ||
606
+ String(event.params.type ?? '') === condition.dialogType));
607
+ }
608
+ return false;
609
+ }
610
+ const visibleDomExpression = `(() => Array.from(document.querySelectorAll('a,button,input,textarea,select,[role],[contenteditable="true"]')).map((el, index) => { const r=el.getBoundingClientRect(); const s=getComputedStyle(el); const visible=!!(r.width||r.height)&&s.display!=='none'&&s.visibility!=='hidden'&&s.pointerEvents!=='none'; const label=el.labels?.length?Array.from(el.labels).map((item)=>String(item.textContent||'').trim()).filter(Boolean).join(' '):undefined; const role=el.getAttribute('role')||el.tagName.toLowerCase(); const editable=el instanceof HTMLInputElement||el instanceof HTMLTextAreaElement||el.isContentEditable; const clickable=el instanceof HTMLButtonElement||el instanceof HTMLAnchorElement||role==='button'||typeof el.onclick==='function'; return {node_id:index,tag:el.tagName.toLowerCase(),role,text:(el.innerText||el.getAttribute('aria-label')||label||el.getAttribute('placeholder')||'').trim().slice(0,240),label,placeholder:el.getAttribute('placeholder')||undefined,testId:el.getAttribute('data-testid')||undefined,cssCandidates:[el.id?'#'+CSS.escape(el.id):'',el.getAttribute('data-testid')?'[data-testid="'+CSS.escape(el.getAttribute('data-testid'))+'"]':''].filter(Boolean),framePath:[],x:r.x,y:r.y,width:r.width,height:r.height,visible,enabled:!el.disabled,editable,clickable,confidence:el.id||el.getAttribute('data-testid')?'high':'medium'}; }).filter((node) => node.visible).slice(0,500))()`;
611
+ export const visibleDomTarget = (nodeId) => `(() => { const nodes=${visibleDomExpression}; const n=nodes[${nodeId}]; if(!n) throw new Error('visible DOM node not found'); const all=Array.from(document.querySelectorAll('a,button,input,textarea,select,[role],[contenteditable="true"]')); const e=all[n.node_id]; if(!e) throw new Error('visible DOM node is stale'); return e; })()`;
612
+ async function waitForCondition(cdp, condition, timeout, baseline, signal) {
613
+ const deadline = Date.now() + timeout;
614
+ const script = conditionToScript(condition, (locator, selector) => buildTargetExpression(locator, selector));
615
+ let lastValue;
616
+ while (Date.now() < deadline) {
617
+ if (signal?.aborted)
618
+ throw signal.reason instanceof Error
619
+ ? signal.reason
620
+ : new Error('Browser condition wait aborted');
621
+ const challenge = await readBrowserChallenge(cdp, signal);
622
+ if (challenge)
623
+ throw new Error(`页面需要人工验证:${challenge.summary}`);
624
+ const value = await evaluate(cdp, script, signal);
625
+ lastValue = value;
626
+ if (condition.type === 'page-change' ? value !== baseline : value === true)
627
+ return value;
628
+ await new Promise((resolve, reject) => {
629
+ const timer = setTimeout(() => {
630
+ signal?.removeEventListener('abort', onAbort);
631
+ resolve();
632
+ }, 100);
633
+ const onAbort = () => {
634
+ clearTimeout(timer);
635
+ reject(signal?.reason instanceof Error
636
+ ? signal.reason
637
+ : new Error('Browser condition wait aborted'));
638
+ };
639
+ if (signal?.aborted)
640
+ onAbort();
641
+ else
642
+ signal?.addEventListener('abort', onAbort, { once: true });
643
+ });
644
+ }
645
+ throw new Error(`Browser condition timed out: ${condition.type} (last=${JSON.stringify(lastValue)})`);
646
+ }
647
+ async function readBrowserChallenge(cdp, signal) {
648
+ const text = String(await evaluate(cdp, `location.href + '\\n' + (document.body?.innerText || '').slice(0, 20000)`, signal).catch(() => ''));
649
+ return detectBrowserChallenge(text);
650
+ }
651
+ async function waitForBrowserChallenge(cdp, timeout, signal) {
652
+ const deadline = Date.now() + timeout;
653
+ while (Date.now() < deadline && !signal?.aborted) {
654
+ const challenge = await readBrowserChallenge(cdp, signal);
655
+ if (challenge)
656
+ return challenge;
657
+ await sleepWithSignal(250, signal).catch(() => undefined);
658
+ }
659
+ return null;
660
+ }
661
+ async function waitForEventOrBrowserChallenge(cdp, predicate, timeout, afterSequence = 0, signal) {
662
+ const challengeAbort = new AbortController();
663
+ const challengeRace = waitForBrowserChallenge(cdp, timeout, challengeAbort.signal).then((challenge) => {
664
+ if (challenge)
665
+ throw new Error(`页面需要人工验证:${challenge.summary}`);
666
+ return new Promise(() => undefined);
667
+ });
668
+ try {
669
+ return await Promise.race([
670
+ waitFor(cdp, predicate, timeout, afterSequence, signal),
671
+ challengeRace,
672
+ ]);
673
+ }
674
+ finally {
675
+ challengeAbort.abort();
676
+ }
677
+ }
678
+ async function elementPoint(cdp, target, framePath = [], signal, frameId) {
679
+ if (frameId) {
680
+ const value = await evaluate(cdp, `(() => { const e=${target}; e.scrollIntoView({block:'center',inline:'center'}); const r=e.getBoundingClientRect(); const s=getComputedStyle(e); if (!r.width || !r.height || s.visibility==='hidden' || s.pointerEvents==='none') throw new Error('Locator matched a non-actionable element'); return {x:r.left+r.width/2,y:r.top+r.height/2}; })()`, signal, frameId);
681
+ if (!value || typeof value !== 'object')
682
+ throw new Error('无法取得浏览器元素坐标');
683
+ const point = value;
684
+ if (typeof point.x !== 'number' || typeof point.y !== 'number')
685
+ throw new Error('浏览器元素坐标无效');
686
+ const offset = await cdp.frameOwnerOffset(frameId, signal);
687
+ return { x: point.x + offset.x, y: point.y + offset.y };
688
+ }
689
+ let frameOffsets = '';
690
+ let frameRoot = 'document';
691
+ for (const [index, frame] of framePath.entries()) {
692
+ const frameExpression = buildLocatorExpression(frame, undefined, frameRoot);
693
+ frameOffsets += `const __frame${index}=(${frameExpression})[0]; if(!__frame${index}||!__frame${index}.contentDocument) throw new Error('frameLocator could not resolve a same-origin iframe'); const __frameRect${index}=__frame${index}.getBoundingClientRect(); __offsetX+=__frameRect${index}.left; __offsetY+=__frameRect${index}.top; __frameRoot=__frame${index}.contentDocument;`;
694
+ frameRoot = '__frameRoot';
695
+ }
696
+ const value = await evaluate(cdp, `(() => { let __offsetX=0,__offsetY=0,__frameRoot=document; ${frameOffsets} const e=${target}; e.scrollIntoView({block:'center',inline:'center'}); const r=e.getBoundingClientRect(); const s=getComputedStyle(e); if (!r.width || !r.height || s.visibility==='hidden' || s.pointerEvents==='none') throw new Error('Locator matched a non-actionable element'); return {x:r.left+r.width/2+__offsetX,y:r.top+r.height/2+__offsetY}; })()`, signal);
697
+ if (!value || typeof value !== 'object')
698
+ throw new Error('无法取得浏览器元素坐标');
699
+ const point = value;
700
+ if (typeof point.x !== 'number' || typeof point.y !== 'number')
701
+ throw new Error('浏览器元素坐标无效');
702
+ return { x: point.x, y: point.y };
703
+ }
704
+ async function trustedClick(cdp, target, framePath = [], signal, clickCount = 1, frameId) {
705
+ const point = await elementPoint(cdp, target, framePath, signal, frameId);
706
+ await cdp.send('Input.dispatchMouseEvent', {
707
+ type: 'mouseMoved',
708
+ x: point.x,
709
+ y: point.y,
710
+ }, undefined, 30_000, signal);
711
+ await cdp.send('Input.dispatchMouseEvent', {
712
+ type: 'mousePressed',
713
+ x: point.x,
714
+ y: point.y,
715
+ button: 'left',
716
+ buttons: 1,
717
+ clickCount,
718
+ }, undefined, 30_000, signal);
719
+ await cdp.send('Input.dispatchMouseEvent', {
720
+ type: 'mouseReleased',
721
+ x: point.x,
722
+ y: point.y,
723
+ button: 'left',
724
+ buttons: 0,
725
+ clickCount,
726
+ }, undefined, 30_000, signal);
727
+ }
728
+ async function focusTarget(cdp, target, signal, frameId) {
729
+ await evaluate(cdp, `(()=>{const e=${target};e.scrollIntoView({block:'center',inline:'nearest'});e.focus();})()`, signal, frameId);
730
+ }
731
+ async function insertText(cdp, target, value, replace, signal, frameId) {
732
+ await focusTarget(cdp, target, signal, frameId);
733
+ if (replace) {
734
+ const mac = await evaluate(cdp, `navigator.platform.includes('Mac')`, signal, frameId);
735
+ const modifiers = mac === true ? 4 : 2;
736
+ await cdp.send('Input.dispatchKeyEvent', {
737
+ type: 'keyDown',
738
+ key: 'a',
739
+ code: 'KeyA',
740
+ modifiers,
741
+ }, undefined, 30_000, signal);
742
+ await cdp.send('Input.dispatchKeyEvent', {
743
+ type: 'keyUp',
744
+ key: 'a',
745
+ code: 'KeyA',
746
+ modifiers,
747
+ }, undefined, 30_000, signal);
748
+ }
749
+ await cdp.send('Input.insertText', { text: value }, undefined, 30_000, signal);
750
+ }
751
+ function sleepWithSignal(ms, signal) {
752
+ return new Promise((resolve, reject) => {
753
+ if (signal?.aborted) {
754
+ reject(signal.reason instanceof Error
755
+ ? signal.reason
756
+ : new Error('Browser step aborted'));
757
+ return;
758
+ }
759
+ const timer = setTimeout(() => {
760
+ signal?.removeEventListener('abort', onAbort);
761
+ resolve();
762
+ }, ms);
763
+ const onAbort = () => {
764
+ clearTimeout(timer);
765
+ reject(signal?.reason instanceof Error
766
+ ? signal.reason
767
+ : new Error('Browser step aborted'));
768
+ };
769
+ signal?.addEventListener('abort', onAbort, { once: true });
770
+ });
771
+ }
772
+ async function resumeBatchAfterHuman(stream, ctx, signal, summary) {
773
+ ctx.reportProgress?.(`${summary},等待你在浏览器中处理`);
774
+ const resolution = await stream.waitForHuman(signal);
775
+ if (resolution === 'resumed')
776
+ ctx.reportProgress?.('人工处理完成,正在重新读取页面状态并继续批量任务');
777
+ return resolution;
778
+ }
779
+ async function executeStep(cdp, step, timeout, afterSequence = 0, snapshotKey = 'step', signal) {
780
+ await cdp
781
+ .send('Page.enable', {}, undefined, 30_000, signal)
782
+ .catch(() => undefined);
783
+ const resolvedLocator = await resolveAxLocator(cdp, step.locator, snapshotKey, signal);
784
+ const frameId = resolvedLocator?.frameId ?? resolvedLocator?.frame_id;
785
+ const targetLocator = resolvedLocator && frameId
786
+ ? {
787
+ ...resolvedLocator,
788
+ frame: undefined,
789
+ framePath: undefined,
790
+ frame_path: undefined,
791
+ }
792
+ : resolvedLocator;
793
+ const target = () => {
794
+ if (targetLocator && step.selector) {
795
+ const semantic = buildTargetExpression(targetLocator, undefined);
796
+ const cssFallback = buildTargetExpression(undefined, step.selector);
797
+ return `(() => { try { return ${semantic}; } catch { return ${cssFallback}; } })()`;
798
+ }
799
+ return buildTargetExpression(targetLocator, step.selector);
800
+ };
801
+ switch (step.action) {
802
+ case 'navigate':
803
+ if (!step.url)
804
+ throw new Error('navigate requires url');
805
+ return cdp.send('Page.navigate', { url: step.url }, undefined, 30_000, signal);
806
+ case 'click':
807
+ return trustedClick(cdp, target(), resolvedLocator?.frame ??
808
+ resolvedLocator?.framePath ??
809
+ resolvedLocator?.frame_path ??
810
+ [], signal, 1, frameId);
811
+ case 'doubleClick':
812
+ return trustedClick(cdp, target(), resolvedLocator?.frame ??
813
+ resolvedLocator?.framePath ??
814
+ resolvedLocator?.frame_path ??
815
+ [], signal, 2, frameId);
816
+ case 'fill':
817
+ if (step.value === undefined)
818
+ throw new Error('fill requires value');
819
+ return insertText(cdp, target(), step.value, true, signal, frameId);
820
+ case 'type':
821
+ if (step.value === undefined)
822
+ throw new Error('type requires value');
823
+ return insertText(cdp, target(), step.value, false, signal, frameId);
824
+ case 'hover': {
825
+ const point = await elementPoint(cdp, target(), resolvedLocator?.frame ??
826
+ resolvedLocator?.framePath ??
827
+ resolvedLocator?.frame_path ??
828
+ [], signal, frameId);
829
+ return cdp.send('Input.dispatchMouseEvent', {
830
+ type: 'mouseMoved',
831
+ x: point.x,
832
+ y: point.y,
833
+ }, undefined, 30_000, signal);
834
+ }
835
+ case 'press':
836
+ if (!step.key)
837
+ throw new Error('press requires key');
838
+ if (step.locator || step.selector)
839
+ await focusTarget(cdp, target(), signal, frameId);
840
+ return cdp
841
+ .send('Input.dispatchKeyEvent', { type: 'keyDown', key: step.key }, undefined, 30_000, signal)
842
+ .then(() => cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: step.key }, undefined, 30_000, signal));
843
+ case 'check':
844
+ return evaluate(cdp, `Boolean(${target()}.checked)`, signal, frameId).then((checked) => checked
845
+ ? undefined
846
+ : trustedClick(cdp, target(), resolvedLocator?.frame ??
847
+ resolvedLocator?.framePath ??
848
+ resolvedLocator?.frame_path ??
849
+ [], signal, 1, frameId));
850
+ case 'uncheck':
851
+ return evaluate(cdp, `Boolean(${target()}.checked)`, signal, frameId).then((checked) => checked
852
+ ? trustedClick(cdp, target(), resolvedLocator?.frame ??
853
+ resolvedLocator?.framePath ??
854
+ resolvedLocator?.frame_path ??
855
+ [], signal, 1, frameId)
856
+ : undefined);
857
+ case 'setChecked':
858
+ if (step.checked === undefined)
859
+ throw new Error('setChecked requires checked');
860
+ return evaluate(cdp, `Boolean(${target()}.checked)`, signal, frameId).then((checked) => checked === step.checked
861
+ ? undefined
862
+ : trustedClick(cdp, target(), resolvedLocator?.frame ??
863
+ resolvedLocator?.framePath ??
864
+ resolvedLocator?.frame_path ??
865
+ [], signal, 1, frameId));
866
+ case 'getState':
867
+ return evaluate(cdp, `(()=>{const e=${target()};const s=getComputedStyle(e);return {visible:!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)&&s.visibility!=='hidden',enabled:!e.disabled,checked:!!e.checked}})()`, signal, frameId);
868
+ case 'drag': {
869
+ const points = step.drag_path;
870
+ if (!points?.length)
871
+ throw new Error('drag requires drag_path');
872
+ for (const [index, point] of points.entries())
873
+ await cdp.send('Input.dispatchMouseEvent', {
874
+ type: index === 0
875
+ ? 'mousePressed'
876
+ : index === points.length - 1
877
+ ? 'mouseReleased'
878
+ : 'mouseMoved',
879
+ x: point.x,
880
+ y: point.y,
881
+ button: 'left',
882
+ buttons: index === 0 ? 1 : index === points.length - 1 ? 0 : 1,
883
+ }, undefined, 30_000, signal);
884
+ return { points: points.length };
885
+ }
886
+ case 'wait':
887
+ await sleepWithSignal(step.wait_ms ?? 250, signal);
888
+ return { waited_ms: step.wait_ms ?? 250 };
889
+ case 'waitForURL':
890
+ if (!step.url)
891
+ throw new Error('waitForURL requires url');
892
+ return waitFor(cdp, (event) => /Page\.frame(Navigated|StartedLoading)/.test(event.method) &&
893
+ JSON.stringify(event.params).includes(step.url), timeout, afterSequence, signal);
894
+ case 'waitForDownload':
895
+ return waitFor(cdp, (event) => event.method === 'Browser.downloadWillBegin' ||
896
+ event.method === 'Browser.downloadProgress', timeout, afterSequence, signal);
897
+ case 'waitForFileChooser':
898
+ return waitFor(cdp, (event) => event.method === 'Page.fileChooserOpened', timeout, afterSequence, signal);
899
+ case 'snapshot':
900
+ return snapshot(cdp, snapshotKey, 'full', signal);
901
+ case 'frameInfo':
902
+ return normalizeFrameInfo(normalizeFrameTree((await cdp.send('Page.getFrameTree', {}, undefined, 30_000, signal)).frameTree));
903
+ case 'visibleDom':
904
+ return evaluate(cdp, visibleDomExpression, signal, frameId);
905
+ case 'clickNode':
906
+ if (step.node_id === undefined)
907
+ throw new Error('clickNode requires node_id');
908
+ return trustedClick(cdp, visibleDomTarget(step.node_id), [], signal);
909
+ case 'typeNode':
910
+ if (step.node_id === undefined || step.value === undefined)
911
+ throw new Error('typeNode requires node_id and value');
912
+ return insertText(cdp, visibleDomTarget(step.node_id), step.value, false, signal, frameId);
913
+ case 'scrollNode':
914
+ if (step.node_id === undefined)
915
+ throw new Error('scrollNode requires node_id');
916
+ return evaluate(cdp, `(()=>{const e=${visibleDomTarget(step.node_id)};e.scrollIntoView({block:'center',inline:'nearest'});return {node_id:${step.node_id}}})()`, signal, frameId);
917
+ case 'upload': {
918
+ if (!step.files?.length)
919
+ throw new Error('upload requires files');
920
+ if (step.trigger_selector || step.trigger_locator) {
921
+ const trigger = buildTargetExpression(step.trigger_locator, step.trigger_selector);
922
+ await trustedClick(cdp, trigger, [], signal);
923
+ return { files: step.files, chooser_triggered: true };
924
+ }
925
+ if (!step.selector)
926
+ throw new Error('upload requires selector or trigger_selector');
927
+ const documentResult = (await cdp.send('DOM.getDocument', {
928
+ depth: -1,
929
+ }, undefined, 30_000, signal));
930
+ if (!documentResult.root?.nodeId)
931
+ throw new Error('无法取得 DOM 根节点');
932
+ const found = (await cdp.send('DOM.querySelector', {
933
+ nodeId: documentResult.root.nodeId,
934
+ selector: step.selector,
935
+ }, undefined, 30_000, signal));
936
+ if (!found.nodeId)
937
+ throw new Error('upload selector matched no input');
938
+ await cdp.send('DOM.setFileInputFiles', {
939
+ files: step.files,
940
+ nodeId: found.nodeId,
941
+ }, undefined, 30_000, signal);
942
+ return { files: step.files, uploaded: true };
943
+ }
944
+ case 'pageAssets':
945
+ case 'listPageAssets':
946
+ case 'bundlePageAssets': {
947
+ const assets = await evaluate(cdp, PAGE_ASSETS_EXPRESSION, signal);
948
+ if (!Array.isArray(assets))
949
+ throw new Error('页面资产清单格式无效');
950
+ if (step.bundle_assets || step.action === 'bundlePageAssets') {
951
+ if (!step.bundle_dir)
952
+ throw new Error('bundle_assets requires bundle_dir');
953
+ const bundle = await bundlePageAssets(assets, step.bundle_dir, async (url) => {
954
+ const fetched = await evaluate(cdp, browserAssetFetchExpression(url), signal);
955
+ if (!fetched ||
956
+ typeof fetched !== 'object' ||
957
+ fetched.ok !== true ||
958
+ typeof fetched.base64 !== 'string') {
959
+ const status = fetched && typeof fetched === 'object'
960
+ ? String(fetched.status ?? 'unknown')
961
+ : 'unknown';
962
+ throw new Error(`浏览器上下文抓取资源失败 HTTP ${status}`);
963
+ }
964
+ const value = fetched;
965
+ return new Response(Buffer.from(value.base64, 'base64'), {
966
+ status: 200,
967
+ headers: value.type ? { 'content-type': value.type } : undefined,
968
+ });
969
+ });
970
+ return { assets, bundle };
971
+ }
972
+ return { assets };
973
+ }
974
+ case 'exportContent': {
975
+ const format = step.export_format ?? 'page';
976
+ if (format === 'pdf') {
977
+ const pdf = (await cdp.send('Page.printToPDF', {
978
+ printBackground: true,
979
+ preferCSSPageSize: true,
980
+ }, undefined, 30_000, signal));
981
+ if (!pdf.data)
982
+ throw new Error('浏览器未返回 PDF 内容');
983
+ return { __browser_pdf_base64: pdf.data };
984
+ }
985
+ if (['docx', 'xlsx', 'csv', 'pptx'].includes(format)) {
986
+ const exported = await evaluate(cdp, googleWorkspaceExportExpression(format), signal);
987
+ if (exported && typeof exported === 'object' && 'error' in exported)
988
+ throw new Error(String(exported.error));
989
+ if (!exported ||
990
+ typeof exported !== 'object' ||
991
+ typeof exported
992
+ .__browser_binary_base64 !== 'string')
993
+ throw new Error('Workspace 未返回可保存的文件内容');
994
+ return exported;
995
+ }
996
+ const content = await evaluate(cdp, pageContentExpression(format, step.selector), signal);
997
+ if (!content || typeof content !== 'object')
998
+ throw new Error('页面内容导出失败');
999
+ if (format === 'markdown' || format === 'md')
1000
+ return markdownFromPageContent(content);
1001
+ return content;
1002
+ }
1003
+ case 'readUrls': {
1004
+ if (!step.urls?.length)
1005
+ throw new Error('readUrls requires urls');
1006
+ return readUrlsInBackground(cdp, step.urls, timeout, step.content_type, step.concurrency, signal);
1007
+ }
1008
+ case 'clipboardReadText':
1009
+ return evaluate(cdp, CLIPBOARD_READ_TEXT_EXPRESSION, signal);
1010
+ case 'clipboardWriteText':
1011
+ if (step.clipboard_text === undefined)
1012
+ throw new Error('clipboardWriteText requires clipboard_text');
1013
+ return evaluate(cdp, clipboardWriteTextExpression(step.clipboard_text), signal);
1014
+ case 'clipboardRead':
1015
+ return evaluate(cdp, CLIPBOARD_READ_EXPRESSION, signal);
1016
+ case 'clipboardWrite':
1017
+ if (!step.clipboard_items?.length)
1018
+ throw new Error('clipboardWrite requires clipboard_items');
1019
+ return evaluate(cdp, clipboardWriteExpression(step.clipboard_items), signal);
1020
+ }
1021
+ }
1022
+ async function call(input, signal, ctx = {}) {
1023
+ // Tool runners normally apply the Zod default, but callWithContext is also
1024
+ // used by the local runtime and deterministic E2E harnesses directly. Keep
1025
+ // the execution core safe when that adapter is intentionally bypassed.
1026
+ const timeoutMs = input.timeout_ms ?? 120_000;
1027
+ const stopOnError = input.stop_on_error ?? true;
1028
+ const finalSnapshotMode = input.final_snapshot ?? 'auto';
1029
+ const captureMode = input.capture ?? 'state';
1030
+ if (!(await configService.browser()).enabled)
1031
+ return { ok: false, error: '内置浏览器已在设置中关闭' };
1032
+ if (ctx.browserRun?.supervised !== true)
1033
+ return {
1034
+ ok: false,
1035
+ error: 'BrowserBatch 仅可在 Desktop 的受监督浏览器会话中使用',
1036
+ };
1037
+ const bin = await resolveAgentBrowserPath();
1038
+ if (!bin)
1039
+ return { ok: false, error: '内置浏览器不可用:未找到 agent-browser' };
1040
+ const runId = ctx.browserRun?.runId ?? ctx.toolCallId ?? randomUUID();
1041
+ const conversationKey = ctx.browserRun?.conversationKey ?? `headless:${process.pid}`;
1042
+ const run = browserRunRegistry.ensure({
1043
+ conversationKey,
1044
+ runId,
1045
+ grants: ctx.browserRun?.grants,
1046
+ allowedUploadRoots: ctx.browserRun?.allowedUploadRoots,
1047
+ });
1048
+ try {
1049
+ browserRunRegistry.requireGrant(run, 'interact');
1050
+ if (input.steps.some((step) => step.intent === 'submit'))
1051
+ browserRunRegistry.requireGrant(run, 'submit');
1052
+ const uploadSteps = input.steps.filter((step) => step.action === 'upload');
1053
+ if (uploadSteps.length) {
1054
+ browserRunRegistry.requireGrant(run, 'upload');
1055
+ const roots = await Promise.all(run.allowedUploadRoots.map(async (root) => {
1056
+ try {
1057
+ return await realpath(root);
1058
+ }
1059
+ catch {
1060
+ return root;
1061
+ }
1062
+ }));
1063
+ if (!roots.length)
1064
+ throw new Error('当前浏览器任务没有获批的文件上传范围');
1065
+ for (const file of uploadSteps.flatMap((step) => step.files ?? [])) {
1066
+ const canonical = await realpath(file).catch(() => '');
1067
+ if (!canonical)
1068
+ throw new Error(`上传文件不存在或不可读取:${file}`);
1069
+ const allowed = roots.some((root) => {
1070
+ const child = relative(root, canonical);
1071
+ return (child === '' || (!child.startsWith('..') && !isAbsolute(child)));
1072
+ });
1073
+ if (!allowed)
1074
+ throw new Error(`上传文件超出当前任务批准范围:${file}`);
1075
+ }
1076
+ }
1077
+ const bundleDirs = input.steps
1078
+ .filter((step) => ['pageAssets', 'bundlePageAssets'].includes(step.action) &&
1079
+ (step.bundle_assets || step.action === 'bundlePageAssets'))
1080
+ .map((step) => step.bundle_dir)
1081
+ .filter((value) => Boolean(value));
1082
+ for (const bundleDir of bundleDirs) {
1083
+ const roots = [browserArtifactDir(), getWorkingDir()];
1084
+ const target = await realpath(bundleDir).catch(() => bundleDir);
1085
+ const allowed = roots.some((root) => {
1086
+ const child = relative(root, target);
1087
+ return child === '' || (!child.startsWith('..') && !isAbsolute(child));
1088
+ });
1089
+ if (!allowed)
1090
+ throw new Error(`资产打包目录不在浏览器产物或工作区范围内:${bundleDir}`);
1091
+ }
1092
+ }
1093
+ catch (error) {
1094
+ return {
1095
+ ok: false,
1096
+ error: error instanceof Error ? error.message : String(error),
1097
+ };
1098
+ }
1099
+ try {
1100
+ let claimedTabId = input.tab_id
1101
+ ? await claimConversationTabById({
1102
+ context: run,
1103
+ sessionName: BROWSER_SESSION,
1104
+ bin,
1105
+ globals: sessionArgs(BROWSER_SESSION),
1106
+ runner: spawnRunner,
1107
+ signal,
1108
+ }, input.tab_id)
1109
+ : await ensureConversationTab({
1110
+ context: run,
1111
+ sessionName: BROWSER_SESSION,
1112
+ bin,
1113
+ globals: sessionArgs(BROWSER_SESSION),
1114
+ runner: spawnRunner,
1115
+ signal,
1116
+ });
1117
+ if (input.retention) {
1118
+ browserRunRegistry.markTab(run, claimedTabId, {
1119
+ retention: input.retention,
1120
+ ...(input.tab_title ? { title: input.tab_title } : {}),
1121
+ });
1122
+ }
1123
+ const serviceInput = {
1124
+ sessionName: BROWSER_SESSION,
1125
+ bin,
1126
+ runner: spawnRunner,
1127
+ signal,
1128
+ };
1129
+ const runExclusive = (task) => browserService.exclusive(serviceInput, async () => task());
1130
+ // Do not hold the process-wide browser mutation lock while waiting for a
1131
+ // human. Desktop must be able to click/type during waiting_human; only
1132
+ // each setup/action/final-observation critical section is serialized.
1133
+ return await (async () => {
1134
+ const tabMarker = `${conversationKey}:${runId}:${randomUUID()}`;
1135
+ const markActiveTab = async (markerSignal) => {
1136
+ const marked = await spawnRunner(bin, [
1137
+ ...sessionArgs(BROWSER_SESSION),
1138
+ 'eval',
1139
+ `globalThis[${JSON.stringify(BROWSER_TAB_MARKER_KEY)}]=${JSON.stringify(tabMarker)}`,
1140
+ ], markerSignal);
1141
+ if (marked.code !== 0)
1142
+ throw new Error(marked.stderr || '无法标记当前浏览器会话标签');
1143
+ };
1144
+ // Reuse the same owned tab policy as WebBrowser before opening CDP. This
1145
+ // makes a batch one atomic lease instead of an accidental shared tab.
1146
+ // Setup is serialized, but this lock is released before any human wait.
1147
+ const setup = await runExclusive(async () => {
1148
+ const switched = await spawnRunner(bin, [...sessionArgs(BROWSER_SESSION), 'tab', claimedTabId], signal);
1149
+ if (switched.code !== 0)
1150
+ throw new Error(switched.stderr || '无法切换到已认领的浏览器标签页');
1151
+ await markActiveTab(signal);
1152
+ // The daemon exposes a browser-level CDP endpoint. Prefer the URL of
1153
+ // the tab we just claimed when attaching a page target, otherwise a
1154
+ // multi-tab session could silently bind the last unrelated tab.
1155
+ const currentPage = await spawnRunner(bin, [...sessionArgs(BROWSER_SESSION), 'get', 'url'], signal);
1156
+ const lastKnownUrl = currentPage.code === 0 ? currentPage.stdout.trim() : undefined;
1157
+ const cdp = await getBrowserAutomation(BROWSER_SESSION, bin, spawnRunner, signal, lastKnownUrl, tabMarker);
1158
+ return { cdp, lastKnownUrl };
1159
+ });
1160
+ let { cdp, lastKnownUrl } = setup;
1161
+ const snapshotKey = () => `${conversationKey}:${runId}:${claimedTabId}`;
1162
+ /**
1163
+ * Rebind both sides of the tab lease after a daemon/page restart.
1164
+ * A CDP reconnect alone is insufficient: the old tab id may have
1165
+ * disappeared with the browser process, and attaching the last page
1166
+ * would silently send the next action to another conversation.
1167
+ */
1168
+ const rebindTab = async (recoverySignal) => {
1169
+ const rebound = await spawnRunner(bin, [...sessionArgs(BROWSER_SESSION), 'tab', claimedTabId], recoverySignal);
1170
+ if (rebound.code === 0) {
1171
+ const active = await spawnRunner(bin, [...sessionArgs(BROWSER_SESSION), 'get', 'url'], recoverySignal);
1172
+ if (active.code === 0)
1173
+ lastKnownUrl = active.stdout.trim();
1174
+ }
1175
+ else {
1176
+ const beforeResult = await spawnRunner(bin, [...sessionArgs(BROWSER_SESSION), 'tab', 'list', '--json'], recoverySignal);
1177
+ if (beforeResult.code !== 0)
1178
+ throw new Error(beforeResult.stderr || '浏览器重启后无法读取标签列表');
1179
+ const beforeIds = new Set(parseTabList(beforeResult.stdout).map((tab) => tab.id));
1180
+ const created = await spawnRunner(bin, [
1181
+ ...sessionArgs(BROWSER_SESSION),
1182
+ 'tab',
1183
+ 'new',
1184
+ lastKnownUrl || 'about:blank',
1185
+ ], recoverySignal);
1186
+ if (created.code !== 0)
1187
+ throw new Error(created.stderr || '浏览器重启后无法创建恢复标签');
1188
+ const afterResult = await spawnRunner(bin, [...sessionArgs(BROWSER_SESSION), 'tab', 'list', '--json'], recoverySignal);
1189
+ if (afterResult.code !== 0)
1190
+ throw new Error(afterResult.stderr || '浏览器重启后无法确认恢复标签');
1191
+ const added = parseTabList(afterResult.stdout).filter((tab) => !beforeIds.has(tab.id));
1192
+ const replacement = added.find((tab) => tab.active) ?? added.at(-1);
1193
+ if (!replacement)
1194
+ throw new Error('浏览器重启后未返回新的恢复标签 ID');
1195
+ browserRunRegistry.releaseTab(run.conversationKey, claimedTabId);
1196
+ browserRunRegistry.claimTab(run, replacement.id);
1197
+ claimedTabId = replacement.id;
1198
+ }
1199
+ const active = await spawnRunner(bin, [...sessionArgs(BROWSER_SESSION), 'get', 'url'], recoverySignal);
1200
+ if (active.code === 0)
1201
+ lastKnownUrl = active.stdout.trim();
1202
+ await markActiveTab(recoverySignal);
1203
+ };
1204
+ const rebindTabAndPage = async (recoverySignal) => {
1205
+ await rebindTab(recoverySignal);
1206
+ await cdp.attachToPage(lastKnownUrl, true, tabMarker);
1207
+ };
1208
+ const downloadDir = await runExclusive(() => configureDownloads(cdp, conversationKey, runId));
1209
+ const stream = getBrowserStream(BROWSER_SESSION);
1210
+ let currentStepAbort;
1211
+ let offControl;
1212
+ let offDownload;
1213
+ // ⚠️ 取锁必须是 try 的第一行,释放必须在同一个 finally 内——这把锁是进程级
1214
+ // 单例,唯一释放路径是下面的 finally。此前 beginAgentAction 与
1215
+ // stream.finishAgentAction 分属两个不同的 try 块(中间横跨整段初始化 +
1216
+ // 整个逐步执行循环),ctx.reportProgress(宿主注入回调)等任何一步抛错都会
1217
+ // 跳过 finishAgentAction,锁永久不释放——对齐 webbrowser.ts 的写法(见其头注)。
1218
+ try {
1219
+ stream.beginAgentAction({ runId, conversationKey, action: 'batch' });
1220
+ offControl = stream.subscribeControl((state) => {
1221
+ if (state.status === 'waiting_human')
1222
+ currentStepAbort?.abort(new Error('浏览器已交给用户处理'));
1223
+ });
1224
+ stream.setAgentActing(true, 'batch', `${input.steps.length} 个步骤`);
1225
+ ctx.reportProgress?.(`开始浏览器批量任务,共 ${input.steps.length} 个步骤`);
1226
+ const downloads = new Map();
1227
+ const subscribeDownloadEvents = (connection) => connection.subscribe((event) => {
1228
+ if (event.method === 'Browser.downloadWillBegin') {
1229
+ const id = String(event.params.guid ?? randomUUID());
1230
+ const item = {
1231
+ id,
1232
+ url: String(event.params.url ?? ''),
1233
+ filename: basename(String(event.params.suggestedFilename ?? 'download')) || 'download',
1234
+ path: join(downloadDir, basename(String(event.params.suggestedFilename ?? 'download')) || 'download'),
1235
+ state: 'start',
1236
+ startedAt: Date.now(),
1237
+ };
1238
+ downloads.set(id, item);
1239
+ stream.publish({ type: 'download', ...item, state: 'start' });
1240
+ }
1241
+ if (event.method === 'Browser.downloadProgress') {
1242
+ const id = String(event.params.guid ?? '');
1243
+ const item = downloads.get(id);
1244
+ if (!item)
1245
+ return;
1246
+ const state = event.params.state === 'completed'
1247
+ ? 'complete'
1248
+ : event.params.state === 'canceled'
1249
+ ? 'canceled'
1250
+ : 'progress';
1251
+ item.state = state;
1252
+ item.receivedBytes =
1253
+ Number(event.params.receivedBytes ?? 0) || undefined;
1254
+ item.totalBytes = Number(event.params.totalBytes ?? 0) || undefined;
1255
+ item.error =
1256
+ typeof event.params.error === 'string'
1257
+ ? event.params.error
1258
+ : undefined;
1259
+ stream.publish({
1260
+ type: 'download',
1261
+ ...item,
1262
+ state,
1263
+ receivedBytes: item.receivedBytes,
1264
+ totalBytes: item.totalBytes,
1265
+ ...(item.error ? { error: item.error } : {}),
1266
+ });
1267
+ }
1268
+ });
1269
+ offDownload = subscribeDownloadEvents(cdp);
1270
+ const results = [];
1271
+ let artifactManifest;
1272
+ for (const [index, step] of input.steps.entries()) {
1273
+ const started = Date.now();
1274
+ const stepAbort = new AbortController();
1275
+ currentStepAbort = stepAbort;
1276
+ const abortFromParent = () => stepAbort.abort(signal?.reason);
1277
+ if (signal?.aborted)
1278
+ stepAbort.abort(signal.reason);
1279
+ else
1280
+ signal?.addEventListener('abort', abortFromParent, { once: true });
1281
+ const waits = [];
1282
+ try {
1283
+ ctx.reportProgress?.(`浏览器批量任务 ${index + 1}/${input.steps.length}:${step.action}`);
1284
+ const conditions = step.expect?.conditions ?? [];
1285
+ const preHuman = await runExclusive(async () => {
1286
+ const eventCursor = cdp.read(0).at(-1)?.sequence ?? 0;
1287
+ const baseline = await evaluate(cdp, `location.href + '|' + (document.body?.innerText || '').slice(0, 4000)`, stepAbort.signal).catch(() => undefined);
1288
+ if (step.expect?.navigation)
1289
+ waits.push(waitFor(cdp, (e) => e.method.startsWith('Page.frame') &&
1290
+ (!step.expect?.navigation?.url ||
1291
+ JSON.stringify(e.params).includes(step.expect.navigation.url)), timeoutMs, eventCursor, stepAbort.signal));
1292
+ if (step.expect?.new_tab)
1293
+ waits.push(waitFor(cdp, (e) => e.method === 'Target.targetCreated' &&
1294
+ (!step.expect?.new_tab?.url ||
1295
+ JSON.stringify(e.params).includes(step.expect.new_tab.url)), timeoutMs, eventCursor, stepAbort.signal));
1296
+ if (step.expect?.download)
1297
+ waits.push(waitFor(cdp, (e) => e.method === 'Browser.downloadWillBegin' &&
1298
+ (!step.expect?.download?.filename ||
1299
+ String(e.params.suggestedFilename ?? '').includes(step.expect.download.filename)), timeoutMs, eventCursor, stepAbort.signal));
1300
+ const uploadChooser = step.action === 'upload' &&
1301
+ (step.trigger_selector || step.trigger_locator)
1302
+ ? { files: step.files }
1303
+ : undefined;
1304
+ if (step.expect?.file_chooser || uploadChooser)
1305
+ waits.push(waitFor(cdp, (e) => e.method === 'Page.fileChooserOpened', timeoutMs, eventCursor, stepAbort.signal));
1306
+ for (const condition of step.expect?.conditions ?? []) {
1307
+ if (condition.type === 'download' ||
1308
+ condition.type === 'new-tab' ||
1309
+ condition.type === 'dialog') {
1310
+ waits.push(waitFor(cdp, (event) => eventMatchesCondition(condition, event), timeoutMs, eventCursor, stepAbort.signal));
1311
+ }
1312
+ }
1313
+ const value = await withBrowserRecovery(() => executeStep(cdp, step, timeoutMs, eventCursor, snapshotKey(), stepAbort.signal), {
1314
+ signal: stepAbort.signal,
1315
+ reconnect: async () => {
1316
+ await ensureDaemon(BROWSER_SESSION, {
1317
+ signal: stepAbort.signal,
1318
+ forceRestart: true,
1319
+ });
1320
+ browserRunRegistry.invalidateAllTabs();
1321
+ // A daemon restart can publish a new browser-level WebSocket
1322
+ // URL. Rebind the tab first, then mark that exact tab, and
1323
+ // only then create a fresh page-scoped CDP connection.
1324
+ await rebindTab(stepAbort.signal);
1325
+ const previousCdp = cdp;
1326
+ offDownload?.();
1327
+ cdp = await getBrowserAutomation(BROWSER_SESSION, bin, spawnRunner, stepAbort.signal, lastKnownUrl, tabMarker);
1328
+ offDownload = subscribeDownloadEvents(cdp);
1329
+ previousCdp.close();
1330
+ },
1331
+ rebindTab: async () => {
1332
+ await rebindTabAndPage(stepAbort.signal);
1333
+ },
1334
+ });
1335
+ if (step.action === 'navigate' && step.url)
1336
+ lastKnownUrl = step.url;
1337
+ let events = [];
1338
+ if (waits.length) {
1339
+ const challengeAbort = new AbortController();
1340
+ const challengeRace = waitForBrowserChallenge(cdp, timeoutMs, challengeAbort.signal).then((challenge) => {
1341
+ if (challenge)
1342
+ throw new Error(`页面需要人工验证:${challenge.summary}`);
1343
+ return new Promise(() => undefined);
1344
+ });
1345
+ try {
1346
+ events = await Promise.race([
1347
+ Promise.all(waits),
1348
+ challengeRace,
1349
+ ]);
1350
+ }
1351
+ finally {
1352
+ challengeAbort.abort();
1353
+ }
1354
+ }
1355
+ if (step.artifact && step.action === 'exportContent') {
1356
+ const nextManifest = await materializeExportArtifact(value, step.export_format ?? 'page', step.artifact.save_as);
1357
+ if (step.artifact.label)
1358
+ nextManifest.files[0].label = step.artifact.label;
1359
+ artifactManifest = mergeArtifactManifests(artifactManifest, nextManifest);
1360
+ }
1361
+ else if (step.artifact) {
1362
+ const startedDownload = events.find((event) => event.method === 'Browser.downloadWillBegin');
1363
+ const downloadId = String(startedDownload?.params.guid ?? '');
1364
+ const item = downloadId
1365
+ ? downloads.get(downloadId)
1366
+ : [...downloads.values()].at(-1);
1367
+ if (!item)
1368
+ throw new Error('未捕获到要保存为成果的下载');
1369
+ if (item.state !== 'complete') {
1370
+ const completed = await waitForEventOrBrowserChallenge(cdp, (event) => event.method === 'Browser.downloadProgress' &&
1371
+ String(event.params.guid ?? '') === item.id &&
1372
+ ['completed', 'canceled'].includes(String(event.params.state ?? '')), timeoutMs, eventCursor, stepAbort.signal);
1373
+ if (completed.params.state !== 'completed')
1374
+ throw new Error(`下载未完成:${String(completed.params.state ?? 'unknown')}`);
1375
+ item.state = 'complete';
1376
+ }
1377
+ item.path = await waitForDownloadedFile(downloadDir, item, timeoutMs, stepAbort.signal);
1378
+ const nextManifest = await materializeDownloadArtifact(item.path, step.artifact.save_as);
1379
+ if (step.artifact.label) {
1380
+ nextManifest.files[0].label = step.artifact.label;
1381
+ }
1382
+ artifactManifest = mergeArtifactManifests(artifactManifest, nextManifest);
1383
+ }
1384
+ const chooser = step.expect?.file_chooser ?? uploadChooser;
1385
+ if (chooser?.files?.length) {
1386
+ const opened = events.find((event) => event.method === 'Page.fileChooserOpened');
1387
+ const backendNodeId = Number(opened?.params.backendNodeId ?? 0);
1388
+ if (!backendNodeId)
1389
+ throw new Error('Browser did not provide the file chooser node');
1390
+ await cdp.send('DOM.setFileInputFiles', {
1391
+ files: chooser.files,
1392
+ backendNodeId,
1393
+ });
1394
+ }
1395
+ for (const condition of conditions) {
1396
+ if (condition.type === 'download' ||
1397
+ condition.type === 'new-tab' ||
1398
+ condition.type === 'dialog')
1399
+ continue;
1400
+ await waitForCondition(cdp, condition, timeoutMs, baseline, stepAbort.signal);
1401
+ }
1402
+ const challenge = await readBrowserChallenge(cdp, stepAbort.signal);
1403
+ return { value, events, challenge };
1404
+ });
1405
+ const { value, events, challenge } = preHuman;
1406
+ let humanSnapshot;
1407
+ if (challenge) {
1408
+ stream.requestHuman(challenge.reason, {
1409
+ runId,
1410
+ conversationKey,
1411
+ ownerId: 'local-owner',
1412
+ summary: challenge.summary,
1413
+ });
1414
+ const resolution = await resumeBatchAfterHuman(stream, ctx, signal, challenge.summary);
1415
+ if (resolution === 'stopped')
1416
+ throw new Error('浏览器批量任务已由用户停止');
1417
+ humanSnapshot = await runExclusive(() => snapshot(cdp, snapshotKey(), 'full', signal));
1418
+ }
1419
+ results.push({
1420
+ id: step.id ?? String(index + 1),
1421
+ ok: true,
1422
+ duration_ms: Date.now() - started,
1423
+ result: value,
1424
+ resolution_strategy: resolveInteractionStrategy(step),
1425
+ events,
1426
+ ...(humanSnapshot
1427
+ ? {
1428
+ human_intervention: 'resumed',
1429
+ post_intervention_snapshot: humanSnapshot,
1430
+ }
1431
+ : {}),
1432
+ ...(conditions.length
1433
+ ? {
1434
+ conditions_satisfied: conditions.map((condition) => condition.type),
1435
+ }
1436
+ : {}),
1437
+ });
1438
+ }
1439
+ catch (error) {
1440
+ await Promise.allSettled(waits);
1441
+ let errorMessage = error instanceof Error ? error.message : String(error);
1442
+ if (stream.getControlState().status !== 'waiting_human') {
1443
+ const challenge = await runExclusive(() => readBrowserChallenge(cdp).catch(() => null));
1444
+ if (challenge) {
1445
+ stream.requestHuman(challenge.reason, {
1446
+ runId,
1447
+ conversationKey,
1448
+ ownerId: 'local-owner',
1449
+ summary: challenge.summary,
1450
+ });
1451
+ }
1452
+ }
1453
+ if (stream.getControlState().status === 'waiting_human') {
1454
+ const resolution = await resumeBatchAfterHuman(stream, ctx, signal, '浏览器批量步骤已暂停');
1455
+ if (resolution === 'resumed') {
1456
+ const recovered = await runExclusive(() => snapshot(cdp, snapshotKey(), 'full', signal));
1457
+ results.push({
1458
+ id: step.id ?? String(index + 1),
1459
+ ok: false,
1460
+ duration_ms: Date.now() - started,
1461
+ resolution_strategy: resolveInteractionStrategy(step),
1462
+ error: '人工接管中断了当前步骤;浏览器已恢复,但该步骤是否完成无法自动确认。请依据最新快照重新确认后再继续。',
1463
+ result: {
1464
+ human_intervention: 'resumed_unverified',
1465
+ post_intervention_snapshot: recovered,
1466
+ },
1467
+ });
1468
+ // Never continue subsequent steps with an unknown side effect:
1469
+ // a click may have landed before the handoff, while a form
1470
+ // submission may still be pending. The next model turn can
1471
+ // inspect the fresh snapshot and deliberately resume.
1472
+ break;
1473
+ }
1474
+ errorMessage = '浏览器批量任务已由用户停止';
1475
+ }
1476
+ results.push({
1477
+ id: step.id ?? String(index + 1),
1478
+ ok: false,
1479
+ duration_ms: Date.now() - started,
1480
+ resolution_strategy: resolveInteractionStrategy(step),
1481
+ error: errorMessage,
1482
+ });
1483
+ if (stopOnError)
1484
+ break;
1485
+ }
1486
+ finally {
1487
+ stepAbort.abort();
1488
+ signal?.removeEventListener('abort', abortFromParent);
1489
+ if (currentStepAbort === stepAbort)
1490
+ currentStepAbort = undefined;
1491
+ }
1492
+ }
1493
+ let final;
1494
+ let finalScreenshot;
1495
+ await runExclusive(async () => {
1496
+ final =
1497
+ finalSnapshotMode === 'none'
1498
+ ? undefined
1499
+ : await snapshot(cdp, `${conversationKey}:${runId}:${claimedTabId}`, finalSnapshotMode, signal);
1500
+ if (captureMode === 'screenshot' || captureMode === 'both') {
1501
+ const path = await createArtifactPath('browser-batch-final', 'png');
1502
+ const captured = (await cdp.send('Page.captureScreenshot', {
1503
+ format: 'png',
1504
+ fromSurface: true,
1505
+ }));
1506
+ if (!captured.data)
1507
+ throw new Error('浏览器未返回最终截图');
1508
+ await writeFile(path, Buffer.from(captured.data, 'base64'), {
1509
+ mode: 0o600,
1510
+ });
1511
+ finalScreenshot = path;
1512
+ }
1513
+ });
1514
+ const failed = results.some((result) => result.ok === false);
1515
+ const content = JSON.stringify({
1516
+ tab_id: claimedTabId,
1517
+ download_dir: downloadDir,
1518
+ results,
1519
+ ...(final ? { final_snapshot: final } : {}),
1520
+ ...(finalScreenshot ? { final_screenshot: finalScreenshot } : {}),
1521
+ });
1522
+ // A direct export (PDF/MD/Workspace formats) has no Browser.download*
1523
+ // event, but it is still a formal browser deliverable. Keep one
1524
+ // metadata envelope for the Desktop delivery ledger and allow an empty
1525
+ // downloads list for that case.
1526
+ const metadata = downloads.size || artifactManifest
1527
+ ? {
1528
+ kind: 'browser_download',
1529
+ downloads: [...downloads.values()],
1530
+ ...(artifactManifest ? { artifact: artifactManifest } : {}),
1531
+ }
1532
+ : undefined;
1533
+ return failed
1534
+ ? { ok: false, error: content, ...(metadata ? { metadata } : {}) }
1535
+ : { ok: true, content, ...(metadata ? { metadata } : {}) };
1536
+ }
1537
+ finally {
1538
+ offDownload?.();
1539
+ offControl?.();
1540
+ currentStepAbort?.abort();
1541
+ currentStepAbort = undefined;
1542
+ cdp.close();
1543
+ stream.finishAgentAction(runId);
1544
+ try {
1545
+ stream.setAgentActing(false);
1546
+ }
1547
+ catch {
1548
+ // UI decoration failure must not hide the batch result or leak control state.
1549
+ }
1550
+ }
1551
+ })();
1552
+ }
1553
+ catch (error) {
1554
+ return {
1555
+ ok: false,
1556
+ error: error instanceof Error ? error.message : String(error),
1557
+ };
1558
+ }
1559
+ finally {
1560
+ // AX snapshots are part of the BrowserBatch contract: the model may use
1561
+ // the returned generation in the next tool call. Keep them behind the
1562
+ // bounded TTL/capacity eviction in captureFinalSnapshot instead of
1563
+ // invalidating the reference as soon as this call returns.
1564
+ browserRunRegistry.finish(conversationKey, runId);
1565
+ }
1566
+ }
1567
+ // biome-ignore lint/suspicious/noExplicitAny: Tool's schema parser supplies the inferred output type.
1568
+ export const browserBatchTool = {
1569
+ name: 'BrowserBatch',
1570
+ description: 'Executes multiple visible-browser actions under one tab claim and one persistent CDP connection. Supports semantic locators, event expectations, downloads, waits and final snapshots.',
1571
+ inputSchema,
1572
+ parameters: toToolParameters(inputSchema),
1573
+ isReadOnly: false,
1574
+ isConcurrencySafe: false,
1575
+ maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1576
+ call,
1577
+ callWithContext: call,
1578
+ };