minimal-agent 0.9.0 → 0.10.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 (467) hide show
  1. package/README.md +11 -8
  2. package/package.json +34 -1
  3. package/plugins/cron-runner/.claude-plugin/plugin.json +5 -0
  4. package/plugins/cron-runner/commands/cron.md +26 -0
  5. package/plugins/cron-runner/commands/crons.md +7 -0
  6. package/plugins/cron-runner/plugin.js +34 -0
  7. package/plugins/cron-runner/src/commands.js +348 -0
  8. package/plugins/cron-runner/src/cronTime.js +148 -0
  9. package/plugins/cron-runner/src/lock.js +68 -0
  10. package/plugins/cron-runner/src/runs.js +89 -0
  11. package/plugins/cron-runner/src/scheduleEngine.js +72 -0
  12. package/plugins/cron-runner/src/scheduler.js +355 -0
  13. package/plugins/cron-runner/src/schema.js +140 -0
  14. package/plugins/cron-runner/src/service.js +167 -0
  15. package/plugins/cron-runner/src/store.js +298 -0
  16. package/plugins/cron-runner/src/types.js +1 -0
  17. package/plugins/workflow-runner/commands/workflow.md +4 -4
  18. package/plugins/workflow-runner/commands/workflows.md +1 -1
  19. package/plugins/workflow-runner/plugin.js +2 -1
  20. package/plugins/workflow-runner/src/agentCommon.js +172 -9
  21. package/plugins/workflow-runner/src/dslManifest.js +72 -0
  22. package/plugins/workflow-runner/src/expressions.js +150 -13
  23. package/plugins/workflow-runner/src/index.js +39 -8
  24. package/plugins/workflow-runner/src/loader.js +216 -64
  25. package/plugins/workflow-runner/src/multimodal.js +37 -0
  26. package/plugins/workflow-runner/src/recovery.js +26 -24
  27. package/plugins/workflow-runner/src/runner.js +693 -287
  28. package/plugins/workflow-runner/src/stepExecutors/agent.js +126 -37
  29. package/plugins/workflow-runner/src/stepExecutors/assert.js +3 -2
  30. package/plugins/workflow-runner/src/stepExecutors/llm.js +38 -30
  31. package/plugins/workflow-runner/src/stepExecutors/skill.js +27 -38
  32. package/plugins/workflow-runner/src/stepExecutors/tool.js +56 -3
  33. package/plugins/workflow-runner/src/stepExecutors/verify.js +19 -10
  34. package/plugins/workflow-runner/src/taskIdentity.js +185 -30
  35. package/plugins/workflow-runner/src/types.js +18 -7
  36. package/plugins/workflow-runner/src/workflowState.js +355 -55
  37. package/skills/ai-image/SKILL.md +89 -0
  38. package/skills/ai-image/scripts/generate.mjs +497 -0
  39. package/skills/ai-image/scripts/generate_from_plan.mjs +469 -0
  40. package/skills/ai-image/scripts/generate_from_plan_test.mjs +223 -0
  41. package/skills/ai-image/scripts/generate_test.mjs +74 -0
  42. package/skills/browser-automation/SKILL.md +47 -0
  43. package/skills/docx/SKILL.md +25 -3
  44. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  45. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  46. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  47. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  48. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  49. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  50. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  51. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  52. package/skills/docx/scripts/opc_check.py +253 -0
  53. package/skills/image-gen-openrouter/SKILL.md +10 -2
  54. package/skills/image-stats/SKILL.md +65 -0
  55. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  56. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  57. package/skills/image-stats/scripts/image_stats.py +368 -0
  58. package/skills/image-stats/scripts/image_stats_test.py +181 -0
  59. package/skills/pptx/SKILL.md +123 -95
  60. package/skills/pptx/design.md +82 -0
  61. package/skills/pptx/examples/README.md +29 -0
  62. package/skills/pptx/examples/hybrid_example.mjs +107 -0
  63. package/skills/pptx/examples/hybrid_example.pptx +0 -0
  64. package/skills/pptx/pptxgenjs.md +41 -0
  65. package/skills/pptx/scripts/deck_helpers.mjs +224 -0
  66. package/skills/pptx/scripts/layout_lint.py +293 -0
  67. package/skills/pptx/scripts/layout_lint_test.py +160 -0
  68. package/skills/pptx/scripts/opc_check.py +339 -0
  69. package/skills/pptx/scripts/svg_fallback.mjs +145 -0
  70. package/skills/pptx/svg.md +67 -0
  71. package/skills/tts-zh/SKILL.md +19 -2
  72. package/skills/ui-design/SKILL.md +158 -0
  73. package/skills/ui-design/claude_design.md +309 -0
  74. package/skills/ui-design/evals/evals.json +254 -0
  75. package/skills/ui-design/references/aesthetic-direction.md +83 -0
  76. package/skills/ui-design/references/claude-design-protocol.md +36 -0
  77. package/skills/ui-design/references/design-context.md +53 -0
  78. package/skills/ui-design/references/design-execution-playbook.md +82 -0
  79. package/skills/ui-design/references/export-pptx.md +124 -0
  80. package/skills/ui-design/references/layout-composition.md +654 -0
  81. package/skills/ui-design/references/preview-feedback.md +37 -0
  82. package/skills/ui-design/references/source-image-layout.md +189 -0
  83. package/skills/ui-design/references/starter-components.md +78 -0
  84. package/skills/ui-design/references/vi-aegis-vector.md +144 -0
  85. package/skills/ui-design/references/vi-amber-prism.md +133 -0
  86. package/skills/ui-design/references/vi-aqua-glass.md +200 -0
  87. package/skills/ui-design/references/vi-comfort-vision.md +147 -0
  88. package/skills/ui-design/references/vi-ember-prism.md +140 -0
  89. package/skills/ui-design/references/vi-nebula-console.md +151 -0
  90. package/skills/ui-design/references/vi-obsidian-optics.md +139 -0
  91. package/skills/ui-design/references/vi-obsidian-xui.md +143 -0
  92. package/skills/ui-design/references/vi-precision-horizon-dark.md +117 -0
  93. package/skills/ui-design/references/vi-precision-horizon.md +215 -0
  94. package/skills/ui-design/references/vi-solar-relay.md +130 -0
  95. package/skills/ui-design/references/vi-template-workflow.md +48 -0
  96. package/skills/ui-design/references/vi-verdant-grid.md +141 -0
  97. package/skills/ui-design/references/vi-violet-meridian.md +172 -0
  98. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  99. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  100. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  101. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  102. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  103. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  104. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  105. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  106. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  107. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  108. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  109. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  110. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  111. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  112. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  113. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  114. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  115. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  116. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  117. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  118. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  119. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  120. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  121. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  122. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  123. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  124. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  125. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  126. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  127. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  128. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  129. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  130. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  131. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  132. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  133. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  134. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  135. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  136. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  137. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  138. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  139. package/skills/ui-design/scripts/assemble_material.py +217 -0
  140. package/skills/ui-design/scripts/assemble_material_test.py +149 -0
  141. package/skills/ui-design/scripts/browser_check_ui_artifact.mjs +221 -0
  142. package/skills/ui-design/scripts/chart_sanity_check.py +261 -0
  143. package/skills/ui-design/scripts/chart_sanity_check_test.py +165 -0
  144. package/skills/ui-design/scripts/check_language_policy.py +351 -0
  145. package/skills/ui-design/scripts/check_ui_artifact.py +435 -0
  146. package/skills/ui-design/scripts/check_ui_artifact_pptx_test.py +251 -0
  147. package/skills/ui-design/scripts/deck_pages_check.py +158 -0
  148. package/skills/ui-design/scripts/deck_pages_check_test.py +134 -0
  149. package/skills/ui-design/scripts/deck_visual_style_check.py +767 -0
  150. package/skills/ui-design/scripts/deck_visual_style_check_test.py +713 -0
  151. package/skills/ui-design/scripts/design_budget_check.py +271 -0
  152. package/skills/ui-design/scripts/design_budget_check_test.py +183 -0
  153. package/skills/ui-design/scripts/design_repair_hints.py +162 -0
  154. package/skills/ui-design/scripts/design_repair_hints_test.py +54 -0
  155. package/skills/ui-design/scripts/design_style_check.py +397 -0
  156. package/skills/ui-design/scripts/design_style_check_test.py +233 -0
  157. package/skills/ui-design/scripts/draft_coverage_check.py +184 -0
  158. package/skills/ui-design/scripts/draft_coverage_check_test.py +131 -0
  159. package/skills/ui-design/scripts/export_pptx_from_html.mjs +2367 -0
  160. package/skills/ui-design/scripts/extract_source_assets.py +607 -0
  161. package/skills/ui-design/scripts/extract_source_assets_test.py +256 -0
  162. package/skills/ui-design/scripts/generated_image_review_collect.py +206 -0
  163. package/skills/ui-design/scripts/generated_image_review_collect_test.py +91 -0
  164. package/skills/ui-design/scripts/image_plan_lint.py +571 -0
  165. package/skills/ui-design/scripts/image_plan_lint_test.py +515 -0
  166. package/skills/ui-design/scripts/image_prompt_rewrite.py +713 -0
  167. package/skills/ui-design/scripts/image_prompt_rewrite_test.py +966 -0
  168. package/skills/ui-design/scripts/layout_balance.py +881 -0
  169. package/skills/ui-design/scripts/layout_balance_test.py +927 -0
  170. package/skills/ui-design/scripts/normalize_design_json.py +272 -0
  171. package/skills/ui-design/scripts/normalize_design_json_test.py +119 -0
  172. package/skills/ui-design/scripts/plan_source_layout.py +1450 -0
  173. package/skills/ui-design/scripts/plan_source_layout_test.py +1136 -0
  174. package/skills/ui-design/scripts/resolve_source_input.py +270 -0
  175. package/skills/ui-design/scripts/resolve_source_input_test.py +98 -0
  176. package/skills/ui-design/scripts/scaffold_ui_artifact.py +377 -0
  177. package/skills/ui-design/scripts/source_usage_check.py +581 -0
  178. package/skills/ui-design/scripts/source_usage_check_test.py +427 -0
  179. package/skills/ui-design/scripts/text_readability_pixel.py +340 -0
  180. package/skills/ui-design/scripts/text_readability_pixel_test.py +219 -0
  181. package/skills/ui-design/scripts/visual_quality_gate.py +301 -0
  182. package/skills/ui-design/scripts/visual_quality_gate_test.py +233 -0
  183. package/skills/ui-design/tests/e2e/prompts/deck.md +19 -0
  184. package/skills/ui-design/tests/e2e/prompts/webpage.md +17 -0
  185. package/skills/ui-design/tests/e2e/render_check.mjs +101 -0
  186. package/skills/ui-design/tests/e2e/rubric.json +54 -0
  187. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/browser-check.json +49 -0
  188. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/language-check.txt +12 -0
  189. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/static-check.txt +15 -0
  190. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/outputs/index.html +837 -0
  191. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/prompt.md +12 -0
  192. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/review.md +21 -0
  193. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/desktop.png +0 -0
  194. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/mobile.png +0 -0
  195. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/homepage.html +373 -0
  196. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/platform.html +378 -0
  197. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/prompt.md +13 -0
  198. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/review.md +26 -0
  199. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  200. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  201. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  202. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  203. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  204. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  205. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/checks/static-check.txt +2 -0
  206. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/outputs/index.html +656 -0
  207. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/prompt.md +12 -0
  208. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/render_check.mjs +67 -0
  209. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/checks/static-check.txt +2 -0
  210. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/outputs/index.html +587 -0
  211. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/prompt.md +12 -0
  212. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/review.md +36 -0
  213. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/browser-check.json +49 -0
  214. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/language-check.txt +8 -0
  215. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/static-check.txt +16 -0
  216. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/outputs/index.html +739 -0
  217. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/prompt.md +11 -0
  218. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/review.md +22 -0
  219. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/desktop.png +0 -0
  220. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/mobile.png +0 -0
  221. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/checks/browser-check.json +52 -0
  222. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/outputs/index.html +641 -0
  223. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/desktop.png +0 -0
  224. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/mobile.png +0 -0
  225. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/outputs/index.html +467 -0
  226. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/browser-check.json +49 -0
  227. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/desktop.png +0 -0
  228. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/mobile.png +0 -0
  229. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/homepage.html +373 -0
  230. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/platform.html +378 -0
  231. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/prompt.md +13 -0
  232. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/review.md +26 -0
  233. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  234. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  235. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  236. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  237. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  238. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  239. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/browser-check.json +49 -0
  240. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/language-check.txt +4 -0
  241. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/static-check.txt +2 -0
  242. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/execution_notes.md +10 -0
  243. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/outputs/index.html +587 -0
  244. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/prompt.md +14 -0
  245. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/review.md +13 -0
  246. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/desktop.png +0 -0
  247. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/mobile.png +0 -0
  248. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/browser-check.json +24 -0
  249. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/static-check.txt +2 -0
  250. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/execution_notes.md +21 -0
  251. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/outputs/deck.html +451 -0
  252. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-01.png +0 -0
  253. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-02.png +0 -0
  254. package/skills/ui-design/tests/e2e/runs/iteration-1/review.md +79 -0
  255. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/browser-check.json +26 -0
  256. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/static-check.txt +2 -0
  257. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/execution_notes.md +20 -0
  258. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/outputs/index.html +534 -0
  259. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/desktop.png +0 -0
  260. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/mobile.png +0 -0
  261. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/browser-check.json +64 -0
  262. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/language-check.txt +7 -0
  263. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/quick-validate.txt +1 -0
  264. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-console.txt +2 -0
  265. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-homepage.txt +2 -0
  266. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/console.html +600 -0
  267. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/homepage.html +556 -0
  268. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/prompt.md +29 -0
  269. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/review.md +57 -0
  270. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-desktop.png +0 -0
  271. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-mobile.png +0 -0
  272. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-desktop.png +0 -0
  273. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-mobile.png +0 -0
  274. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/browser-check.json +33 -0
  275. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/language-check.txt +6 -0
  276. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/quick-validate.txt +1 -0
  277. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/static-check.txt +2 -0
  278. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/outputs/index.html +682 -0
  279. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/prompt.md +13 -0
  280. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/review.md +59 -0
  281. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/desktop.png +0 -0
  282. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/mobile.png +0 -0
  283. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-01.html +466 -0
  284. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-02.html +433 -0
  285. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/prompt.md +13 -0
  286. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/browser-check.json +49 -0
  287. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/desktop.png +0 -0
  288. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/mobile.png +0 -0
  289. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/browser-check.json +49 -0
  290. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/desktop.png +0 -0
  291. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/mobile.png +0 -0
  292. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/browser-check.json +49 -0
  293. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/language-check.txt +3 -0
  294. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/static-check.txt +2 -0
  295. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/execution_notes.md +24 -0
  296. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/outputs/index.html +723 -0
  297. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/prompt.md +15 -0
  298. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/review.md +26 -0
  299. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/desktop.png +0 -0
  300. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/mobile.png +0 -0
  301. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-dark.json +49 -0
  302. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-light.json +49 -0
  303. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/language-check.txt +11 -0
  304. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-dark.txt +2 -0
  305. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-light.txt +3 -0
  306. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/execution_notes.md +23 -0
  307. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/dark.html +392 -0
  308. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/light.html +476 -0
  309. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/prompt.md +23 -0
  310. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/review.md +41 -0
  311. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/desktop.png +0 -0
  312. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/mobile.png +0 -0
  313. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/desktop.png +0 -0
  314. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/mobile.png +0 -0
  315. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/browser-check.json +54 -0
  316. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/language-check.txt +5 -0
  317. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/quick-validate.txt +1 -0
  318. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/static-check.txt +2 -0
  319. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/outputs/index.html +765 -0
  320. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/prompt.md +15 -0
  321. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/review.md +59 -0
  322. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/desktop.png +0 -0
  323. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/mobile.png +0 -0
  324. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/outputs/index.html +508 -0
  325. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/browser-check.json +49 -0
  326. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/desktop.png +0 -0
  327. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/mobile.png +0 -0
  328. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/browser-check.json +54 -0
  329. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/language-check.txt +6 -0
  330. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/quick-validate.txt +1 -0
  331. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/static-check.txt +2 -0
  332. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/outputs/index.html +788 -0
  333. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/prompt.md +14 -0
  334. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/review.md +60 -0
  335. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/desktop.png +0 -0
  336. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/mobile.png +0 -0
  337. package/skills/video-compose/SKILL.md +8 -1
  338. package/skills/web-crawler/SKILL.md +70 -0
  339. package/skills/web-crawler/scripts/crawl.mjs +538 -0
  340. package/skills/web-crawler/scripts/crawl.test.mjs +204 -0
  341. package/skills/xlsx/SKILL.md +12 -0
  342. package/skills/xlsx/scripts/opc_check.py +253 -0
  343. package/src/advisorState.js +22 -0
  344. package/src/askBridge.js +87 -3
  345. package/src/bootstrap/workingDir.js +12 -1
  346. package/src/cli/args.js +17 -1
  347. package/src/cli/configMenu.js +236 -0
  348. package/src/cli/configWizard.js +7 -5
  349. package/src/cli/print.js +37 -4
  350. package/src/cli/streamJson.js +77 -24
  351. package/src/cli/wizards/advisorWizard.js +147 -0
  352. package/src/cli/wizards/fieldForm.js +149 -0
  353. package/src/cli/wizards/imageWizard.js +60 -0
  354. package/src/cli/wizards/openRouterWizard.js +23 -0
  355. package/src/cli/wizards/providerPresets.js +95 -0
  356. package/src/cli/wizards/providerWizard.js +219 -0
  357. package/src/cli/wizards/searchWizard.js +23 -0
  358. package/src/cli/wizards/ttsWizard.js +34 -0
  359. package/src/config/configFile.js +441 -83
  360. package/src/config/configService.js +446 -0
  361. package/src/config.js +24 -64
  362. package/src/context/agentHome.js +9 -0
  363. package/src/context/archive.js +3 -2
  364. package/src/context/compact.js +8 -5
  365. package/src/context/contextLock.js +81 -0
  366. package/src/context/persistContext.js +30 -11
  367. package/src/context/reactiveCompact.js +4 -2
  368. package/src/context/recentDirs.js +21 -61
  369. package/src/context/sessionPath.js +97 -23
  370. package/src/context/sessionRegistry.js +303 -24
  371. package/src/context/tokenCounter.js +18 -0
  372. package/src/context/workspaceIndex.js +731 -0
  373. package/src/infra/verification.js +3 -2
  374. package/src/llm/client.js +293 -206
  375. package/src/llm/providers/anthropicFormat.js +241 -0
  376. package/src/llm/providers/openaiFormat.js +190 -0
  377. package/src/loop.js +211 -27
  378. package/src/main.js +61 -11
  379. package/src/messageQueue.js +97 -0
  380. package/src/multimodal.js +203 -0
  381. package/src/plugin-sdk.js +15 -0
  382. package/src/plugins/commandRouter.js +3 -0
  383. package/src/plugins/hookEngine.js +5 -1
  384. package/src/plugins/pluginLifecycle.js +28 -0
  385. package/src/plugins/pluginRunner.js +40 -0
  386. package/src/plugins/transcript.js +4 -3
  387. package/src/prompts/pluginList.js +2 -2
  388. package/src/prompts/projectInstructions.js +3 -3
  389. package/src/prompts/promptAssembly.js +133 -109
  390. package/src/prompts/skillList.js +12 -22
  391. package/src/session/runAgentTurn.js +70 -0
  392. package/src/session/runTurn.js +41 -6
  393. package/src/toolGate.js +17 -0
  394. package/src/tools/advisor/advisor.js +379 -0
  395. package/src/tools/agent/agent.js +555 -0
  396. package/src/tools/agent/agentTypes.js +59 -0
  397. package/src/tools/agent/coordinator.js +93 -0
  398. package/src/tools/agent/worktree.js +270 -0
  399. package/src/tools/ask/ask.js +7 -2
  400. package/src/tools/bash/bash.js +112 -18
  401. package/src/tools/download/download.js +144 -0
  402. package/src/tools/edit/edit.js +1 -0
  403. package/src/tools/edit/multi-edit.js +1 -0
  404. package/src/tools/genimage/genimage.js +242 -0
  405. package/src/tools/genspeech/genspeech.js +67 -0
  406. package/src/tools/glob/glob.js +1 -0
  407. package/src/tools/grep/grep.js +3 -0
  408. package/src/tools/grep/rgPath.js +2 -0
  409. package/src/tools/index.js +25 -2
  410. package/src/tools/read/read.js +1 -0
  411. package/src/tools/shared/killTree.js +52 -0
  412. package/src/tools/toolProgressBridge.js +48 -0
  413. package/src/tools/webbrowser/agentBrowserPath.js +258 -0
  414. package/src/tools/webbrowser/browser.js +59 -42
  415. package/src/tools/webbrowser/browserControl.js +118 -0
  416. package/src/tools/webbrowser/browserDaemon.js +97 -0
  417. package/src/tools/webbrowser/browserService.js +27 -0
  418. package/src/tools/webbrowser/browserStream.js +119 -0
  419. package/src/tools/webbrowser/webbrowser.js +699 -130
  420. package/src/tools/webfetch/webfetch.js +1 -0
  421. package/src/tools/websearch/websearch.js +134 -51
  422. package/src/tools/write/write.js +1 -0
  423. package/src/ui/App.js +249 -22
  424. package/src/ui/AskQuestion.js +19 -17
  425. package/src/ui/InputBox.js +34 -9
  426. package/src/ui/LiveArea.js +112 -19
  427. package/src/ui/MessageList.js +100 -14
  428. package/src/ui/ModelPicker.js +56 -0
  429. package/src/ui/QueuedMessagesBanner.js +14 -0
  430. package/src/ui/Root.js +18 -10
  431. package/src/ui/SessionPicker.js +102 -0
  432. package/src/ui/StatusLine.js +13 -4
  433. package/src/ui/WorkspacePicker.js +55 -0
  434. package/src/ui/figures.js +44 -0
  435. package/src/ui/hooks/askSlot.js +82 -0
  436. package/src/ui/hooks/useChat.js +345 -64
  437. package/src/ui/hooks/usePasteHandler.js +11 -1
  438. package/src/ui/hooks/useTextBuffer.js +1 -3
  439. package/src/ui/liveViewport.js +5 -1
  440. package/src/ui/markdown.js +206 -0
  441. package/src/ui/textBuffer.js +0 -7
  442. package/src/ui/theme.js +32 -0
  443. package/src/utils/debugLog.js +0 -4
  444. package/src/utils/resourcePaths.js +2 -2
  445. package/src/utils/stripThink.js +50 -0
  446. package/src/web/browserBridge.js +447 -0
  447. package/src/web/protocol.js +19 -0
  448. package/workflows/adaptive-research.yaml +9 -2
  449. package/workflows/book-review-short.yaml +31 -7
  450. package/workflows/e2e-write-greet.yaml +3 -0
  451. package/workflows/illustrate.yaml +227 -0
  452. package/workflows/plan.yaml +437 -207
  453. package/workflows/ppt.yaml +1659 -0
  454. package/workflows/ralph-loop.yaml +142 -112
  455. package/workflows/schema.json +81 -23
  456. package/workflows/webppt.yaml +496 -0
  457. package/workflows/word.yaml +710 -0
  458. package/workflows/youtube-shorts.yaml +38 -10
  459. package/MINIMAL-TRACE.md +0 -77
  460. package/plugins/workflow-runner/src/voteHelpers.js +0 -110
  461. package/skills/config/SKILL.md +0 -123
  462. package/skills/frontend-design/LICENSE.txt +0 -177
  463. package/skills/frontend-design/SKILL.md +0 -42
  464. package/skills/pptx/scripts/office/__pycache__/soffice.cpython-314.pyc +0 -0
  465. package/src/ui/ProgressPanel.js +0 -54
  466. package/src/ui/StreamingBlock.js +0 -11
  467. package/src/ui/ToolStatus.js +0 -11
package/src/llm/client.js CHANGED
@@ -1,45 +1,45 @@
1
1
  /**
2
2
  * ============================================================
3
- * src/llm/client.ts —— OpenAI 兼容 LLM 客户端(流式)
3
+ * src/llm/client.ts —— LLM 客户端(流式,双协议)
4
4
  * ------------------------------------------------------------
5
5
  * 这个文件做一件事:把"消息历史 + 可用工具"发给 LLM,把"流式响应"逐 chunk
6
6
  * yield 回上层。它是整个 agent 系统里唯一会 fetch() 远端 API 的地方。
7
7
  *
8
- * 我们用 OpenAI 的 Chat Completions 协议,因为:
9
- * - DeepSeek、月之暗面、智谱、火山、OpenRouter 都按它兼容
10
- * - 协议简单:POST 一个 JSON,回 SSE
8
+ * 支持两种协议格式(`provider.apiFormat`,缺省 'openai'):
9
+ * - OpenAI Chat Completions —— DeepSeek、月之暗面、智谱、火山、OpenRouter 都按它兼容
10
+ * - Anthropic Messages API —— ⚠️ 未经真实端点验证,见 providers/anthropicFormat.ts 头注
11
+ *
12
+ * 格式相关的请求体构造 + SSE chunk 解析拆到 `providers/openaiFormat.ts` /
13
+ * `providers/anthropicFormat.ts`;本文件只保留协议无关的共享骨架:重试/自适应退避/
14
+ * 空闲看门狗/中断处理——这部分历史上踩过真实事故(P0-1),两种格式共用同一份,
15
+ * 不重复实现以免两份retry逻辑逐渐漂移出不一致的行为。
11
16
  *
12
17
  * SSE(Server-Sent Events)是什么?
13
18
  * 一种很简单的流式协议:服务器逐行返回 "data: <json>\n\n",
14
- * 最后用 "data: [DONE]\n\n" 表示结束。
19
+ * OpenAI 用 "data: [DONE]\n\n" 表示结束;Anthropic 用一个 data payload 里
20
+ * `type:"message_stop"` 的事件表示结束(两者的 data 载荷都自解释,行拆分逻辑
21
+ * 不需要为格式差异特殊处理,见 anthropicFormat.ts 头注)。
15
22
  *
16
23
  * 本文件不依赖任何 npm 包(fetch 是 Bun/Node 20+ 内置)。
17
24
  * ============================================================
18
25
  */
19
26
  import { dlog } from '../utils/debugLog.js';
27
+ import { buildOpenAIRequest, createOpenAISSEParser, isImageInputRejected, isMaxTokensRejected, isResponseFormatUnsupported, } from './providers/openaiFormat.js';
28
+ import { buildAnthropicRequest, createAnthropicSSEParser } from './providers/anthropicFormat.js';
20
29
  /**
21
30
  * 记住"这个 baseURL::model 不支持 response_format.json_schema",避免每次都先撞 400。
22
31
  * 单进程内有效;每个 -p 子进程是全新进程 → 首个结构化调用吃一次 400 再退化,可接受。
32
+ * 只对 openai 格式有意义(anthropic 无此就地退化概念,见下)。
23
33
  */
24
34
  const rfUnsupported = new Set();
25
- /** 调试日志用的请求自增号(vote 并发多 voter 时靠它在日志里区分是哪一路)。 */
26
- let reqCounter = 0;
27
35
  /**
28
- * 判断一个 400 是否在抱怨 response_format(json_schema)不被支持。
29
- * 各 provider 文案不一(火山 GLM:`json_schema is not supported by this model`),
30
- * 故宽松匹配:"提到 response_format" 且 "出现 not support / unsupported / not valid / invalid"。
36
+ * 记住"这个 baseURL::model 拒绝我们发的 max_tokens",避免每次都先撞 400。
37
+ * 与 rfUnsupported 同理,单进程内有效。绝大多数 provider 接受 max_tokens(实测豆包到
38
+ * ≥131072 仍正常),这只是给个别挑剔 provider 的兜底,命中后该进程不再发 max_tokens。
31
39
  */
32
- function isResponseFormatUnsupported(status, errText) {
33
- if (status !== 400)
34
- return false;
35
- const t = errText.toLowerCase();
36
- if (!t.includes('response_format'))
37
- return false;
38
- return (t.includes('not support') || // 命中 "not supported"
39
- t.includes('unsupported') ||
40
- t.includes('not valid') ||
41
- t.includes('invalid'));
42
- }
40
+ const maxTokensUnsupported = new Set();
41
+ /** 调试日志用的请求自增号(vote 并发多 voter 时靠它在日志里区分是哪一路)。 */
42
+ let reqCounter = 0;
43
43
  // ============================================================
44
44
  // 重试 + 超时(无网络重试 → 整条 IM 链卡死;这是 P0-1 的根治)
45
45
  // ------------------------------------------------------------
@@ -49,21 +49,86 @@ function isResponseFormatUnsupported(status, errText) {
49
49
  // 子进程挂起会整条 IM 通道 dir 锁卡死。
50
50
  //
51
51
  // 设计:
52
- // - 指数退避 200ms → 400 → 800 → 1600 → 3200(封顶 5 次)
52
+ // - 自适应退避(拥塞控制式):进程级档位 0.5→1→2→5→10→30→60s,失败逐级抬高、
53
+ // 成功(拿到 200)立即回落到最小值;封顶 60s(超 1min 就不是拥塞、是真死,无谓再等)。
54
+ // 跨请求记忆当前拥塞程度——连续失败不会从头傻试,刚成功又不会被旧拥塞拖慢。
53
55
  // - 5xx / 408 / 425 / 429 / 网络错误全部重试
54
56
  // - 401 / 403 / 404 等 4xx 不重试(重试无意义)+ 给清晰"key 失效"提示
55
57
  // - 429 优先解析 Retry-After 头(秒),封顶 60s
56
- // - AbortSignal.timeout 120s 兜底单次 fetch 时长
58
+ // - 空闲 watchdog 兜底卡死(语义见 IDLE_TIMEOUT_MS 注释——是【空闲】不是【总时长】)
57
59
  // - 用户 signal 触发的 abort 立即退出,不重试
58
60
  // - 流式响应中途中断(reader.read reject)走同一个错误路径
59
61
  // ============================================================
60
- /** 单一 fetch 请求的超时(毫秒)。流式响应中途不发新 chunk 也算超时。 */
61
- const FETCH_TIMEOUT_MS = 120_000;
62
+ /**
63
+ * 流式空闲超时(毫秒):自上一个字节到达起算,超过仍无任何新数据才判死。
64
+ * 可用 env `MINIMAL_AGENT_IDLE_TIMEOUT_MS` 覆盖。
65
+ *
66
+ * ⚠️ 语义是【空闲】而非【总时长】:重思考模型(MiniMax-M3/DeepSeek-R1 等)
67
+ * 带大上下文的流式响应总时长可以远超 2 分钟,但只要还在持续吐 chunk
68
+ * (reasoning_delta 也算)就不该掐。v0.9.x 之前用 AbortSignal.timeout 对
69
+ * 整个请求硬超时 120s——design 类长步骤被恰好 120s 掐死、重试必然同败,是 bug。
70
+ */
71
+ const IDLE_TIMEOUT_MS = (() => {
72
+ const v = Number(process.env.MINIMAL_AGENT_IDLE_TIMEOUT_MS);
73
+ return Number.isFinite(v) && v > 0 ? v : 120_000;
74
+ })();
75
+ /**
76
+ * 网络/5xx/429 的重试预算(单次请求最多尝试几次)。配合下面的自适应档位,
77
+ * 默认 6 次足以「骑过一次网络拥塞波」:实测某些 provider 端点(如 api.minimax.chat)
78
+ * 经路存在分钟级拥塞波——TLS 握手在 0.15s 与 6.8s 间剧烈摆动,坏的时候连接直接 RST
79
+ * (Bun fetch 报 "Unable to connect")。不稳网可用 env `MINIMAL_AGENT_LLM_MAX_RETRIES`
80
+ * 加码(如 10)。注意:现在「等多久」由自适应档位决定,本值只管「试几次」。
81
+ */
62
82
  const DEFAULT_RETRY = {
63
- maxAttempts: 5,
64
- baseDelayMs: 200,
65
- maxDelayMs: 3200,
83
+ maxAttempts: (() => {
84
+ const v = Number(process.env.MINIMAL_AGENT_LLM_MAX_RETRIES);
85
+ return Number.isFinite(v) && v >= 1 ? Math.floor(v) : 6;
86
+ })(),
66
87
  };
88
+ // ------------------------------------------------------------
89
+ // 自适应退避(拥塞控制式 AIMD 的简化版)
90
+ // ------------------------------------------------------------
91
+ // 动机:各家模型的 token plan 现在普遍会拥堵。固定指数退避有两个毛病:
92
+ // ① 每个新请求都从最小值重来 → 端点正堵时仍一上来就猛打、徒增失败;
93
+ // ② 退避封顶才 8s → 真遇上分钟级拥塞波,整段重试落在同一坏波里仍败。
94
+ // 对策:一个【进程级档位】记忆「当前网络有多堵」:
95
+ // - 每次重试前取当前档位延时(带抖动)并抬高一档(失败 = 乘性增);
96
+ // - 任一请求拿到 200 → 档位立即清零回最小值(成功 = 立即恢复,不永久拉长);
97
+ // - 档位封顶 60s:超过 1 分钟就不是「拥塞波」而是端点真死,再等无意义,让其 honest 失败。
98
+ // 跨请求共享:A 请求把档位顶到 30s,紧接着 B 请求重试就从 30s 起(别再傻试),
99
+ // 直到某次成功才回落。-p 子进程各自全新 → 从最小档位起,互不影响。
100
+ /** 退避档位(毫秒):0.5→1→2→5→10→30→60s。近似 log 曲线的「几个档位」,封顶 60s。 */
101
+ const BACKOFF_TIERS_MS = [500, 1000, 2000, 5000, 10_000, 30_000, 60_000];
102
+ /** 进程级当前档位索引(跨请求记忆当前拥塞程度;成功清零,失败抬高)。 */
103
+ let backoffTier = 0;
104
+ /**
105
+ * 取「这次重试该等多久」:当前档位延时 + ±15% 抖动(防 vote/parallel 并发请求
106
+ * 同步重试踩同一波),随后把档位抬高一档(封顶最后一档)。
107
+ * retryAfterMs 有值(429 的 Retry-After 头)时听服务器的(仍封顶 60s、仍抬档)。
108
+ */
109
+ function nextRetryDelayMs(retryAfterMs) {
110
+ const tierMs = BACKOFF_TIERS_MS[Math.min(backoffTier, BACKOFF_TIERS_MS.length - 1)];
111
+ backoffTier = Math.min(backoffTier + 1, BACKOFF_TIERS_MS.length - 1);
112
+ if (retryAfterMs !== undefined)
113
+ return Math.min(retryAfterMs, 60_000);
114
+ const jittered = tierMs * (0.85 + Math.random() * 0.3);
115
+ return Math.min(Math.round(jittered), 60_000);
116
+ }
117
+ /** 任一请求成功(拿到 200)→ 拥塞已缓解,档位立即回落到最小值。 */
118
+ function resetBackoff() {
119
+ backoffTier = 0;
120
+ }
121
+ /** 仅供测试:读取/重置进程级档位(生产代码勿调)。 */
122
+ export function _getBackoffTier() {
123
+ return backoffTier;
124
+ }
125
+ export function _resetBackoffTier() {
126
+ backoffTier = 0;
127
+ }
128
+ /** 仅供测试:暴露纯退避计算,供单测验证档位曲线/封顶/Retry-After(不触网)。 */
129
+ export function _nextRetryDelayMs(retryAfterMs) {
130
+ return nextRetryDelayMs(retryAfterMs);
131
+ }
67
132
  /** 可重试的 HTTP 状态码。4xx 除 408/425/429 外不重试(重试无意义)。 */
68
133
  function isRetryableStatus(status) {
69
134
  return status === 408 || status === 425 || status === 429 || (status >= 500 && status < 600);
@@ -82,11 +147,25 @@ function parseRetryAfter(resp) {
82
147
  return undefined;
83
148
  return Math.min(sec * 1000, 60_000);
84
149
  }
150
+ /**
151
+ * 构造带正确 .name 的中断错误。
152
+ *
153
+ * 为什么需要这个而不是直接 `new Error('aborted')`:loop.ts 靠 `e.name === 'AbortError'`
154
+ * 判断"这是用户主动中断",走优雅的 interrupted 收尾(不显示红色错误)。普通 `new Error(...)`
155
+ * 的 `.name` 是 `'Error'`,会让 loop.ts 把真实的用户中断误判成普通 LLM 报错
156
+ * (`code:'llm_error'`,红色错误)。本文件里所有"确认是用户 signal 触发的 abort"都必须
157
+ * 用这个构造,保持 `.name` 与真实 DOMException AbortError 一致。
158
+ */
159
+ function makeAbortError(message = 'aborted') {
160
+ const err = new Error(message);
161
+ err.name = 'AbortError';
162
+ return err;
163
+ }
85
164
  /** 可中断的 sleep:用户 signal 触发 abort 立刻 reject,不阻塞退出。 */
86
165
  function sleep(ms, signal) {
87
166
  return new Promise((resolve, reject) => {
88
167
  if (signal?.aborted) {
89
- reject(new Error('aborted'));
168
+ reject(makeAbortError());
90
169
  return;
91
170
  }
92
171
  const timer = setTimeout(() => {
@@ -95,7 +174,7 @@ function sleep(ms, signal) {
95
174
  }, ms);
96
175
  const onAbort = () => {
97
176
  clearTimeout(timer);
98
- reject(new Error('aborted'));
177
+ reject(makeAbortError());
99
178
  };
100
179
  signal?.addEventListener('abort', onAbort, { once: true });
101
180
  });
@@ -106,25 +185,46 @@ function buildChatError(resp, body) {
106
185
  const head = `[${status} ${resp.statusText}]`;
107
186
  if (isClientAuthError(status)) {
108
187
  return new Error(`LLM 鉴权失败 ${head}:API key 可能失效、过期或无权访问 model "${resp.url.split('/').pop()}".\n` +
109
- `请检查 .env / ~/.minimal-agent/config.json 的 MINIMAL_AGENT_API_KEY 与 provider 配置。\n` +
188
+ `请检查 .env / ~/.minimal-agent/config.json 的模型配置。\n` +
110
189
  `原始错误:${body.slice(0, 500)}`);
111
190
  }
112
191
  return new Error(`LLM 请求失败 ${head}:${body.slice(0, 500)}`);
113
192
  }
114
193
  /**
115
- * 把用户 signal 与超时 signal 组合,任一触发即 abort。
116
- * AbortSignal.any 是 Node 20.3+ / Bun 内置,project engines.node=">=20" 满足。
194
+ * 空闲 watchdog:kick() 重置计时;IDLE_TIMEOUT_MS 内没有任何 kick 才 abort。
195
+ * 与用户 signal 组合(AbortSignal.any,Node 20.3+/Bun 内置),任一触发即中断。
196
+ * 调用方契约:fetch 等待期自动计时;流式期间每收到一个 chunk 调一次 kick();
197
+ * 结束(无论成败)调 dispose() 清掉定时器。
117
198
  */
118
- function combinedSignal(userSignal) {
119
- const timeout = AbortSignal.timeout(FETCH_TIMEOUT_MS);
120
- if (!userSignal)
121
- return timeout;
122
- return AbortSignal.any([userSignal, timeout]);
199
+ export function createIdleWatchdog(userSignal, timeoutMs = IDLE_TIMEOUT_MS) {
200
+ const ctrl = new AbortController();
201
+ let timer = null;
202
+ const arm = () => {
203
+ if (timer)
204
+ clearTimeout(timer);
205
+ timer = setTimeout(() => {
206
+ ctrl.abort(new Error(`LLM 流式响应空闲超时:${timeoutMs}ms 内未收到任何新数据(总时长不限,只判空闲;可用 MINIMAL_AGENT_IDLE_TIMEOUT_MS 调整)`));
207
+ }, timeoutMs);
208
+ // 不阻止进程退出(Bun/Node 均支持 unref)
209
+ timer.unref?.();
210
+ };
211
+ arm();
212
+ return {
213
+ signal: userSignal ? AbortSignal.any([userSignal, ctrl.signal]) : ctrl.signal,
214
+ kick: arm,
215
+ dispose: () => {
216
+ if (timer)
217
+ clearTimeout(timer);
218
+ timer = null;
219
+ },
220
+ };
123
221
  }
124
222
  /**
125
- * 调用 LLM 并以流的形式 yield 事件。
223
+ * 调用 LLM 并以流的形式 yield 事件。协议格式由 `provider.apiFormat` 决定
224
+ * (缺省 'openai');两种格式共用本函数的重试/退避/看门狗骨架,请求体构造和
225
+ * SSE 解析分别委托给 providers/openaiFormat.ts / providers/anthropicFormat.ts。
126
226
  *
127
- * @param provider 选好的 provider(含 baseURL / apiKey / model)
227
+ * @param provider 选好的 provider(含 baseURL / apiKey / model / apiFormat)
128
228
  * @param messages 完整的对话历史
129
229
  * @param tools 工具列表(每个工具需要带 jsonSchema 才能给 LLM)
130
230
  * @param signal AbortSignal,UI 按 Ctrl+C 时中断
@@ -140,104 +240,115 @@ function combinedSignal(userSignal) {
140
240
  */
141
241
  export async function* chat(args) {
142
242
  const { provider, messages, tools, signal, responseFormat } = args;
143
- // 1. 把 Tool 数组转成 OpenAI 协议的 tools 字段
144
- const openaiTools = tools.map((t) => ({
145
- type: 'function',
146
- function: {
147
- name: t.name,
148
- description: typeof t.description === 'function' ? t.description() : t.description,
149
- parameters: t.parameters,
150
- },
151
- }));
152
- // 2. 过滤并清洁 messages 数组,剥离 timestamp, id, _ephemeral 等本地渲染元数据
153
- const cleanedMessages = messages
154
- .filter((m) => {
155
- // 过滤掉 _ephemeral=true 的临时 UI 提示消息,防止其污染 LLM 核心上下文
156
- if (m.role === 'system' && m._ephemeral === true) {
157
- return false;
158
- }
159
- return true;
160
- })
243
+ const format = provider.apiFormat === 'anthropic' ? 'anthropic' : 'openai';
244
+ if (format === 'anthropic' && responseFormat) {
245
+ throw new Error('output_schema 结构化输出目前不支持 Anthropic Messages 格式的 provider' +
246
+ '(Anthropic 的强制工具输出模式与 OpenAI 的 response_format.json_schema 形状不同,尚未实现映射)。' +
247
+ '请改用 OpenAI 格式的 provider,或去掉这一步的 output_schema。');
248
+ }
249
+ // 过滤并清洁 messages 数组,剥离 timestamp, id, _ephemeral 等本地渲染元数据。
250
+ // 两种格式共用同一份清洗后的内部形状——Anthropic 分支的翻译(system 摘除/
251
+ // tool_calls→tool_use 等)在 buildAnthropicRequest 内部做,不在这里做。
252
+ let cleanedMessages = messages
253
+ .filter((m) => !(m.role === 'system' && m._ephemeral === true))
161
254
  .map((m) => {
162
- // 结构解构以去除本地专属渲染元数据
163
- const { id, timestamp, ...rest } = m;
255
+ const { id, timestamp, attachments, display_metadata, ...rest } = m;
164
256
  return rest;
165
257
  });
166
- // 3. 构造请求体
167
- // 逃生开关:极个别 provider 不认 stream_options 会 400,可用 MINIMAL_AGENT_DISABLE_USAGE=1 关掉。
168
258
  const includeUsage = process.env.MINIMAL_AGENT_DISABLE_USAGE !== '1';
169
- const url = `${provider.baseURL.replace(/\/$/, '')}/chat/completions`;
170
- // response_format 能力探测:部分 OpenAI 兼容 provider(如火山 GLM 系)不支持
171
- // response_format.type=json_schema,会回 400。已知不支持的 model 直接跳过,
172
- // 撞到 400 则退化重试(去掉 response_format,靠 prompt 内嵌 schema + 上层 extractJsonPayload 容错解析)。
173
259
  const rfKey = `${provider.baseURL}::${provider.model}`;
174
- let wantResponseFormat = responseFormat !== undefined && !rfUnsupported.has(rfKey);
175
- const postChat = (withResponseFormat) => fetch(url, {
176
- method: 'POST',
177
- headers: {
178
- 'Content-Type': 'application/json',
179
- Authorization: `Bearer ${provider.apiKey}`,
180
- },
181
- body: JSON.stringify({
182
- model: provider.model,
183
- messages: cleanedMessages,
184
- tools: openaiTools.length > 0 ? openaiTools : undefined,
185
- stream: true,
186
- // stream_options.include_usage:让 provider 在流末附带真实 prompt_tokens,
187
- // 供上层自动压缩判断用;provider 不支持时 usagePromptTokens 保持 undefined,上层 fallback 本地估算。
188
- ...(includeUsage ? { stream_options: { include_usage: true } } : {}),
189
- // tool_choice: 'auto' 是默认值,可不写;某些 provider 必须显式
190
- tool_choice: openaiTools.length > 0 ? 'auto' : undefined,
191
- // ADR-05: structured output;缺省 undefined 不出现在 body(JSON.stringify 自动剥离)
192
- response_format: withResponseFormat ? responseFormat : undefined,
193
- }),
194
- // 用组合 signal:用户 signal 触发的 abort + 120s fetch 超时,任意一个先到即中断。
195
- // (之前裸传 signal,fetch 永远不超时,gateway 桥接子进程挂死时 IM 链卡死——P0-1 根因)
196
- signal: combinedSignal(signal),
260
+ // OpenAI 分支专属的「就地退化」状态;Anthropic 无此概念,两个 flag 恒为 false,
261
+ // 下面的退化 if 分支天然不会进入(不需要额外的 format 判断散落各处)。
262
+ let wantResponseFormat = format === 'openai' && responseFormat !== undefined && !rfUnsupported.has(rfKey);
263
+ let wantMaxTokens = format === 'openai' &&
264
+ typeof provider.maxTokens === 'number' &&
265
+ provider.maxTokens > 0 &&
266
+ !maxTokensUnsupported.has(rfKey);
267
+ let imagesStripped = false;
268
+ const stripImagesFromMessages = (msgs) => msgs.map((m) => {
269
+ const c = m.content;
270
+ if (!Array.isArray(c))
271
+ return m;
272
+ const textOnly = c.filter((p) => p.type === 'text');
273
+ const newContent = textOnly.length > 0
274
+ ? textOnly.map((p) => ({ type: 'text', text: p.text }))
275
+ : [{ type: 'text', text: '[图片内容未发送:当前模型不支持图片输入]' }];
276
+ return { ...m, content: newContent };
197
277
  });
198
- // 3. 发请求(fetch 在 Bun/Node 20+ 内置),带重试 + 超时
278
+ // 空闲 watchdog:每个 attempt 一只(重试前换新);fetch 等待期自动计时,
279
+ // 流式期间每个 chunk kick 一次。用户 signal 与 watchdog 任一触发即中断。
280
+ let watchdog = createIdleWatchdog(signal);
281
+ const postChat = () => {
282
+ const req = format === 'anthropic'
283
+ ? buildAnthropicRequest({ provider, cleanedMessages, tools })
284
+ : buildOpenAIRequest({
285
+ provider,
286
+ cleanedMessages,
287
+ tools,
288
+ withResponseFormat: wantResponseFormat,
289
+ withMaxTokens: wantMaxTokens,
290
+ includeUsage,
291
+ responseFormat,
292
+ });
293
+ return fetch(req.url, { method: 'POST', headers: req.headers, body: req.body, signal: watchdog.signal });
294
+ };
199
295
  const reqId = ++reqCounter;
200
296
  dlog('llm', `req#${reqId} POST →`, {
201
297
  model: provider.model,
298
+ format,
202
299
  rf: wantResponseFormat,
300
+ maxTokens: wantMaxTokens ? provider.maxTokens : undefined,
203
301
  msgs: cleanedMessages.length,
204
- tools: openaiTools.length,
302
+ tools: tools.length,
205
303
  });
206
- // 3a. 重试循环:每个 attempt 内部可能触发"response_format 不支持 → 去掉"的本地退化
207
- // (本地退化不算重试,仍属同一 attempt 的"拿到 200"路径的一部分)
208
- // 只有"网络/超时/5xx/429"才跨 attempt 重试,4xx 业务错误直接抛
304
+ // 重试循环:每个 attempt 内部可能触发"response_format 不支持 → 去掉"的本地退化
305
+ // (本地退化不算重试,仍属同一 attempt 的"拿到 200"路径的一部分;只 openai 分支会真的
306
+ // 进入这两个退化 if)。只有"网络/超时/5xx/429"才跨 attempt 重试,4xx 业务错误直接抛
209
307
  // (401/403 走 buildChatError 的专属文案,不浪费 5 次重试)
210
308
  let resp;
211
309
  let lastErr;
310
+ // 网络错误统一收尾(主路径 + 两条退化路径共用,防退避策略三处漂移):
311
+ // 用户 abort → 抛;置 lastErr + dlog;额度用尽 → 'break';否则退避后 'retry'。
312
+ const handleNetworkError = async (e, attempt, label) => {
313
+ if (signal?.aborted)
314
+ throw makeAbortError();
315
+ const err = e;
316
+ lastErr = new Error(`LLM 网络错误 [${err.name ?? 'NetworkError'}]:${err.message}`);
317
+ dlog('llm', `req#${reqId} attempt ${attempt} ${label}:`, err.message);
318
+ if (attempt >= DEFAULT_RETRY.maxAttempts)
319
+ return 'break';
320
+ const delay = nextRetryDelayMs();
321
+ dlog('llm', `req#${reqId} retry in ${delay}ms (attempt ${attempt + 1}/${DEFAULT_RETRY.maxAttempts}, tier=${backoffTier})`);
322
+ await sleep(delay, signal);
323
+ return 'retry';
324
+ };
212
325
  for (let attempt = 1; attempt <= DEFAULT_RETRY.maxAttempts; attempt++) {
213
326
  // 用户按 ESC/Ctrl+C 触发的 abort:立刻退出,不重试
214
327
  if (signal?.aborted) {
215
- throw new Error('aborted');
328
+ throw makeAbortError();
216
329
  }
330
+ // 每个 attempt 换一只新 watchdog(上一只可能已超时 abort,不能复用)
331
+ watchdog.dispose();
332
+ watchdog = createIdleWatchdog(signal);
217
333
  try {
218
- resp = await postChat(wantResponseFormat);
334
+ resp = await postChat();
335
+ watchdog.kick(); // 响应头到达 = 有进展
219
336
  }
220
337
  catch (e) {
221
- // 1) 用户主动 abort(signal.aborted 触发 AbortError)→ 立刻抛
222
- if (signal?.aborted)
223
- throw new Error('aborted');
224
- // 2) AbortSignal.timeout 触发的超时(Fetch 120s 还没回)
338
+ // 1) 用户主动 abort → handleNetworkError 里立刻抛
339
+ // 2) AbortSignal.timeout 触发的超时(Fetch 空闲超时还没回)
225
340
  // 3) 其它网络错误(ECONNRESET / fetch failed / DNS 失败)→ 全部按"可重试"处理
226
- const err = e;
227
- lastErr = new Error(`LLM 网络错误 [${err.name ?? 'NetworkError'}]:${err.message}`);
228
- dlog('llm', `req#${reqId} attempt ${attempt} failed (network):`, err.message);
229
- if (attempt >= DEFAULT_RETRY.maxAttempts)
341
+ if ((await handleNetworkError(e, attempt, 'failed (network)')) === 'break')
230
342
  break;
231
- const delay = Math.min(DEFAULT_RETRY.baseDelayMs * 2 ** (attempt - 1), DEFAULT_RETRY.maxDelayMs);
232
- dlog('llm', `req#${reqId} retry in ${delay}ms (attempt ${attempt + 1}/${DEFAULT_RETRY.maxAttempts})`);
233
- await sleep(delay, signal);
234
343
  continue;
235
344
  }
236
345
  // resp.ok = true 直接跳出循环
237
346
  if (resp.ok)
238
347
  break;
239
- // resp !ok:先看是不是 json_schema 不支持(本地退化路径,不算重试)
240
- const errText = await resp.text().catch(() => '');
348
+ // resp !ok:本地退化路径(response_format / max_tokens 不被支持 → 去掉重发,不算重试;
349
+ // 仅 openai 分支——wantResponseFormat/wantMaxTokens 在 anthropic 分支恒为 false)
350
+ let errText = await resp.text().catch(() => '');
351
+ // 退化①:response_format.json_schema 不支持
241
352
  if (wantResponseFormat && isResponseFormatUnsupported(resp.status, errText)) {
242
353
  // 记住该 model 不支持,单进程内后续结构化调用直接跳过 response_format(不再撞 400)。
243
354
  rfUnsupported.add(rfKey);
@@ -246,25 +357,64 @@ export async function* chat(args) {
246
357
  wantResponseFormat = false; // 本 attempt 内继续,关闭 response_format
247
358
  // 在同一 attempt 内再发一次(不计重试)
248
359
  try {
249
- resp = await postChat(false);
360
+ resp = await postChat();
361
+ watchdog.kick();
250
362
  }
251
363
  catch (e) {
252
364
  // 退化二次 fetch 也网络错:把错误外提到外层重试循环
253
- const err = e;
254
- lastErr = new Error(`LLM 网络错误 [${err.name ?? 'NetworkError'}]:${err.message}`);
255
- dlog('llm', `req#${reqId} attempt ${attempt} fallback fetch failed:`, err.message);
256
- if (signal?.aborted)
257
- throw new Error('aborted');
258
- if (attempt >= DEFAULT_RETRY.maxAttempts)
365
+ if ((await handleNetworkError(e, attempt, 'fallback fetch failed')) === 'break')
259
366
  break;
260
- const delay = Math.min(DEFAULT_RETRY.baseDelayMs * 2 ** (attempt - 1), DEFAULT_RETRY.maxDelayMs);
261
- dlog('llm', `req#${reqId} retry in ${delay}ms (attempt ${attempt + 1}/${DEFAULT_RETRY.maxAttempts})`);
262
- await sleep(delay, signal);
263
367
  continue;
264
368
  }
265
- // 退化后 OK 或 仍 !ok 都走下面统一判断
266
369
  if (resp.ok)
267
370
  break;
371
+ // 仍 !ok:刷新 errText,让下面 max_tokens 退化看到最新报错(两种退化可叠加)
372
+ errText = await resp.text().catch(() => '');
373
+ }
374
+ // 退化②:max_tokens 不被该 provider 接受(极少数;豆包实测接受到 ≥131072)→ 去掉重发
375
+ if (wantMaxTokens && isMaxTokensRejected(resp.status, errText)) {
376
+ maxTokensUnsupported.add(rfKey);
377
+ console.warn(`[minimal-agent] provider model "${provider.model}" 拒绝 max_tokens=${provider.maxTokens},` +
378
+ `已去掉 max_tokens 重发(回落该 provider 默认输出上限,长输出可能被截断)。`);
379
+ wantMaxTokens = false; // 本 attempt 内继续,关闭 max_tokens
380
+ try {
381
+ resp = await postChat();
382
+ watchdog.kick();
383
+ }
384
+ catch (e) {
385
+ if ((await handleNetworkError(e, attempt, 'fallback fetch failed')) === 'break')
386
+ break;
387
+ continue;
388
+ }
389
+ if (resp.ok)
390
+ break;
391
+ errText = await resp.text().catch(() => '');
392
+ }
393
+ // 退化③:模型不支持图片输入 → 剥离所有 image_url/video_url 重发(自动降级为纯文本)。
394
+ // 适用于新旧格式(openai/anthropic),仅触发一次,避免无限循环。
395
+ if (!imagesStripped && isImageInputRejected(resp.status, errText)) {
396
+ const hasImages = cleanedMessages.some((m) => {
397
+ const c = m.content;
398
+ return Array.isArray(c) && c.some((p) => p.type === 'image_url' || p.type === 'video_url');
399
+ });
400
+ if (hasImages) {
401
+ console.warn(`[minimal-agent] provider model "${provider.model}" 不支持图片输入,` +
402
+ `已剥离图片重发(仅保留文本内容)。如该模型实际支持视觉,请在模型设置中确认。`);
403
+ cleanedMessages = stripImagesFromMessages(cleanedMessages);
404
+ imagesStripped = true;
405
+ try {
406
+ resp = await postChat();
407
+ watchdog.kick();
408
+ }
409
+ catch (e) {
410
+ if ((await handleNetworkError(e, attempt, 'fallback fetch failed')) === 'break')
411
+ break;
412
+ continue;
413
+ }
414
+ if (resp.ok)
415
+ break;
416
+ errText = await resp.text().catch(() => '');
417
+ }
268
418
  }
269
419
  // 4xx 业务错误:不重试,直接抛(401/403 走 buildChatError 专属文案)
270
420
  if (!isRetryableStatus(resp.status)) {
@@ -275,32 +425,31 @@ export async function* chat(args) {
275
425
  dlog('llm', `req#${reqId} attempt ${attempt} failed [${resp.status}]:`, errText.slice(0, 200));
276
426
  if (attempt >= DEFAULT_RETRY.maxAttempts)
277
427
  break;
278
- // 429 优先用 Retry-After 头,否则走指数退避
428
+ // 429 优先用 Retry-After 头,否则走自适应档位退避
279
429
  const retryAfterMs = parseRetryAfter(resp);
280
- const delay = retryAfterMs !== undefined
281
- ? retryAfterMs
282
- : Math.min(DEFAULT_RETRY.baseDelayMs * 2 ** (attempt - 1), DEFAULT_RETRY.maxDelayMs);
283
- dlog('llm', `req#${reqId} retry in ${delay}ms (attempt ${attempt + 1}/${DEFAULT_RETRY.maxAttempts}, retryAfter=${retryAfterMs ?? 'exp'})`);
430
+ const delay = nextRetryDelayMs(retryAfterMs);
431
+ dlog('llm', `req#${reqId} retry in ${delay}ms (attempt ${attempt + 1}/${DEFAULT_RETRY.maxAttempts}, tier=${backoffTier}, retryAfter=${retryAfterMs ?? 'tier'})`);
284
432
  await sleep(delay, signal);
285
433
  }
286
- // 5 次都没拿到 200:把最后一次的 Error 抛出去
434
+ // 重试预算用尽仍没拿到 200:把最后一次的 Error 抛出去(注意不重置档位——还在堵)
287
435
  if (!resp || !resp.ok) {
288
436
  throw lastErr ?? new Error('LLM 请求失败:超过最大重试次数');
289
437
  }
438
+ // 拿到 200 = 端点可达、拥塞已缓解 → 档位立即回落到最小值(成功就恢复,不永久拉长)。
439
+ resetBackoff();
290
440
  if (!resp.body) {
291
441
  throw new Error('LLM 响应没有 body(provider 可能不支持流式?)');
292
442
  }
293
443
  // fetch 已返回且 ok:若日志卡在这条之前 = fetch 阶段挂起(无超时);卡在这条之后无 done = 流中途挂起。
294
444
  dlog('llm', `req#${reqId} streaming begin (status ${resp.status})`);
295
- // 5. SSE 解析:把 ReadableStream 切成一行行 "data: ..." 然后 JSON.parse
445
+ // SSE 解析:把 ReadableStream 切成一行行 "data: ..." 交给 format 对应的 parser。
446
+ // 两种格式的 data 载荷都自解释(OpenAI 用字面量 "[DONE]";Anthropic 的 JSON payload
447
+ // 自带 type 字段),行拆分逻辑本身不需要为格式差异分叉。
296
448
  const reader = resp.body.getReader();
297
449
  const decoder = new TextDecoder();
298
450
  /** 临时缓冲:HTTP chunk 不一定按行切,要自己拼 */
299
451
  let buffer = '';
300
- /** 累积的 stop_reason,最后 yield done 时用 */
301
- let stopReason = 'unknown';
302
- /** 真实 prompt_tokens(从 usage chunk 读取);provider 不返回时保持 undefined */
303
- let usagePromptTokens;
452
+ const parser = format === 'anthropic' ? createAnthropicSSEParser() : createOpenAISSEParser();
304
453
  try {
305
454
  while (true) {
306
455
  // 每次 read 前检查 abort:用户按 ESC/Ctrl+C 时 signal.aborted 为 true。
@@ -310,6 +459,7 @@ export async function* chat(args) {
310
459
  return;
311
460
  }
312
461
  const { value, done } = await reader.read();
462
+ watchdog.kick(); // 有新数据(reasoning/text/心跳都算进展)→ 重置空闲计时
313
463
  if (done)
314
464
  break;
315
465
  // 每个 chunk 处理完后也检查 abort,让快速连按能及时退出。
@@ -326,87 +476,24 @@ export async function* chat(args) {
326
476
  if (!line || !line.startsWith('data:'))
327
477
  continue;
328
478
  const dataStr = line.slice(5).trim(); // 去掉 "data:" 前缀
329
- if (dataStr === '[DONE]') {
330
- // 流结束标记;带出真实 prompt_tokens(provider 不返回时为 undefined,上层 fallback 本地估算)
331
- dlog('llm', `req#${reqId} done [DONE]`, { stopReason, promptTokens: usagePromptTokens });
332
- yield { type: 'done', stopReason, promptTokens: usagePromptTokens };
333
- return;
334
- }
335
- // 解析一个 chunk JSON
336
- let chunk;
337
- try {
338
- chunk = JSON.parse(dataStr);
339
- }
340
- catch {
341
- // 罕见:某些 provider 会发空行或心跳,忽略
342
- continue;
343
- }
344
- // usage chunk:include_usage 开启时,provider 通常在流末单独发一帧,
345
- // 此帧 choices 为空(没有 delta),必须在 `if (!delta) continue` 之前读取,
346
- // 否则会被 continue 跳过,永远拿不到真实 prompt_tokens。
347
- if (chunk.usage && typeof chunk.usage.prompt_tokens === 'number') {
348
- usagePromptTokens = chunk.usage.prompt_tokens;
349
- }
350
- const delta = chunk.choices?.[0]?.delta;
351
- const finish = chunk.choices?.[0]?.finish_reason;
352
- if (finish) {
353
- stopReason =
354
- finish === 'stop' || finish === 'tool_calls' || finish === 'length'
355
- ? finish
356
- : 'unknown';
357
- }
358
- if (!delta)
359
- continue;
360
- // 5a. 文本增量
361
- // 注意:MiniMax 等模型默认会把 <think>...</think> 标签直接放在
362
- // content 字段里 —— 我们原样累积,不剥离,确保下一轮工具调用时
363
- // 思维链完整回传。
364
- if (typeof delta.content === 'string' && delta.content.length > 0) {
365
- yield { type: 'text_delta', delta: delta.content };
366
- }
367
- // 5b. tool_calls 增量(OpenAI 协议下 tool_calls 也是分片的)
368
- if (Array.isArray(delta.tool_calls)) {
369
- for (const tc of delta.tool_calls) {
370
- yield {
371
- type: 'tool_call_delta',
372
- index: tc.index ?? 0,
373
- id: tc.id,
374
- name: tc.function?.name,
375
- argumentsDelta: tc.function?.arguments,
376
- };
479
+ const events = parser.handleData(dataStr);
480
+ for (const ev of events) {
481
+ yield ev;
482
+ if (ev.type === 'done') {
483
+ dlog('llm', `req#${reqId} done`, ev);
484
+ return;
377
485
  }
378
486
  }
379
- // 5c. 思维链增量(DeepSeek-R1 / MiniMax reasoning_split / OpenRouter)
380
- // 三种字段名都见过:reasoning_content / reasoning / reasoning_details
381
- // reasoning_details 是数组,每片是个对象;其它两个是字符串增量。
382
- if (typeof delta.reasoning_content === 'string' && delta.reasoning_content.length > 0) {
383
- yield {
384
- type: 'reasoning_delta',
385
- field: 'reasoning_content',
386
- delta: delta.reasoning_content,
387
- };
388
- }
389
- if (typeof delta.reasoning === 'string' && delta.reasoning.length > 0) {
390
- yield {
391
- type: 'reasoning_delta',
392
- field: 'reasoning',
393
- delta: delta.reasoning,
394
- };
395
- }
396
- if (Array.isArray(delta.reasoning_details) && delta.reasoning_details.length > 0) {
397
- yield {
398
- type: 'reasoning_delta',
399
- field: 'reasoning_details',
400
- items: delta.reasoning_details,
401
- };
402
- }
403
487
  }
404
488
  }
405
- // 如果流没显式发 [DONE],也补一个 done;同样带出 promptTokens
406
- dlog('llm', `req#${reqId} done (stream end, no [DONE])`, { stopReason, promptTokens: usagePromptTokens });
407
- yield { type: 'done', stopReason, promptTokens: usagePromptTokens };
489
+ // 流没有显式发终止事件(OpenAI [DONE] / Anthropic message_stop)就结束了——
490
+ // 用 parser 累积的兜底状态补一个 done。
491
+ const fallback = parser.buildFallbackDone();
492
+ dlog('llm', `req#${reqId} done (stream end, no terminal event)`, fallback);
493
+ yield fallback;
408
494
  }
409
495
  finally {
496
+ watchdog.dispose();
410
497
  reader.releaseLock?.();
411
498
  }
412
499
  }