@xiaobuyu/nodesign 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +140 -0
  3. package/bin/nodesign.js +176 -0
  4. package/package.json +104 -0
  5. package/server/api/README.md +45 -0
  6. package/server/api/_guard.js +75 -0
  7. package/server/api/admin.js +187 -0
  8. package/server/api/assets/docx-page.js +103 -0
  9. package/server/api/assets/helpers.js +22 -0
  10. package/server/api/assets/notes.js +144 -0
  11. package/server/api/assets.js +896 -0
  12. package/server/api/board.js +57 -0
  13. package/server/api/browse.js +138 -0
  14. package/server/api/canvas.js +444 -0
  15. package/server/api/chatai.js +324 -0
  16. package/server/api/exports/build-standalone.js +980 -0
  17. package/server/api/exports/cards.js +118 -0
  18. package/server/api/exports/docx-pdf.js +46 -0
  19. package/server/api/exports/export-page.js +142 -0
  20. package/server/api/exports/handoff.js +147 -0
  21. package/server/api/exports.js +690 -0
  22. package/server/api/instruction.js +75 -0
  23. package/server/api/local.js +115 -0
  24. package/server/api/me.js +116 -0
  25. package/server/api/memory.js +185 -0
  26. package/server/api/pending-changes.js +366 -0
  27. package/server/api/plugins.js +164 -0
  28. package/server/api/projects.js +220 -0
  29. package/server/api/publish.js +64 -0
  30. package/server/api/recent.js +73 -0
  31. package/server/api/sessions.js +589 -0
  32. package/server/api/skills.js +77 -0
  33. package/server/api/standalone-fit.js +168 -0
  34. package/server/api/turn-compose.js +180 -0
  35. package/server/api/turn-inflight.js +44 -0
  36. package/server/api/turn.js +669 -0
  37. package/server/auth/README.md +17 -0
  38. package/server/auth/middleware.js +138 -0
  39. package/server/auth/origin-guard.js +71 -0
  40. package/server/auth/session.js +108 -0
  41. package/server/auth/tier.js +113 -0
  42. package/server/auth/users-store.js +348 -0
  43. package/server/edit/README.md +29 -0
  44. package/server/engine/README.md +113 -0
  45. package/server/engine/agent/agent-shared.js +550 -0
  46. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
  47. package/server/engine/agent/auto-mode-rules.js +78 -0
  48. package/server/engine/agent/context.js +418 -0
  49. package/server/engine/agent/events.js +464 -0
  50. package/server/engine/agent/hooks/canvas-validate.js +199 -0
  51. package/server/engine/agent/hooks/failure.js +120 -0
  52. package/server/engine/agent/hooks/file-events.js +75 -0
  53. package/server/engine/agent/hooks/lifecycle.js +233 -0
  54. package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
  55. package/server/engine/agent/hooks/post-guidance.js +206 -0
  56. package/server/engine/agent/hooks/post-subagent-report.js +104 -0
  57. package/server/engine/agent/hooks/post-trim.js +58 -0
  58. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
  59. package/server/engine/agent/hooks/pre-defaults.js +84 -0
  60. package/server/engine/agent/hooks/pre-injectors.js +277 -0
  61. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
  62. package/server/engine/agent/hooks/pre-starter-files.js +61 -0
  63. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
  64. package/server/engine/agent/hooks/site-validate.js +94 -0
  65. package/server/engine/agent/hooks/tool-prompts.js +31 -0
  66. package/server/engine/agent/hooks/turn-state-memory.js +56 -0
  67. package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
  68. package/server/engine/agent/hooks.js +354 -0
  69. package/server/engine/agent/init-contract.js +69 -0
  70. package/server/engine/agent/isolation.js +159 -0
  71. package/server/engine/agent/model-context.js +359 -0
  72. package/server/engine/agent/model-table.js +292 -0
  73. package/server/engine/agent/plugin-loader.js +266 -0
  74. package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
  75. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
  76. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
  77. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
  78. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
  79. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
  80. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
  81. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
  82. package/server/engine/agent/prompts/tools/site-reference.md +106 -0
  83. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
  84. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
  85. package/server/engine/agent/session-loop.js +1024 -0
  86. package/server/engine/agent/session-model.js +164 -0
  87. package/server/engine/agent/skill.js +204 -0
  88. package/server/engine/agent/system-prompts.js +86 -0
  89. package/server/engine/agent/task-events.js +78 -0
  90. package/server/engine/agents/ds-extractor.md +132 -0
  91. package/server/engine/agents/explorer.md +190 -0
  92. package/server/engine/agents/index.js +230 -0
  93. package/server/engine/agents/schemas/design-system.json +156 -0
  94. package/server/engine/agents/schemas/tweak-schema.json +113 -0
  95. package/server/engine/agents/tweak-proposer.md +127 -0
  96. package/server/engine/agents/vision-checker.md +254 -0
  97. package/server/engine/browse/capture.js +422 -0
  98. package/server/engine/browse/card.js +137 -0
  99. package/server/engine/browse/handover.js +59 -0
  100. package/server/engine/browse/page-digest.js +119 -0
  101. package/server/engine/browse/refs.js +196 -0
  102. package/server/engine/browse/registry.js +307 -0
  103. package/server/engine/browse/screencast.js +170 -0
  104. package/server/engine/browse/state.js +121 -0
  105. package/server/engine/chatai/chat-log.js +79 -0
  106. package/server/engine/chatai/index.js +193 -0
  107. package/server/engine/chatai/openai-compat.js +152 -0
  108. package/server/engine/chatai/orchestrate.js +309 -0
  109. package/server/engine/chatai/perform.js +40 -0
  110. package/server/engine/chatai/summarize.js +79 -0
  111. package/server/engine/mcp/capability-gate.js +52 -0
  112. package/server/engine/mcp/index.js +339 -0
  113. package/server/engine/mcp/param-sanitizer.js +142 -0
  114. package/server/engine/mcp/tools/arrange-on-board.js +124 -0
  115. package/server/engine/mcp/tools/artifact-session.js +278 -0
  116. package/server/engine/mcp/tools/browse-computer.js +421 -0
  117. package/server/engine/mcp/tools/browse-find-batch.js +183 -0
  118. package/server/engine/mcp/tools/browse-screenshot.js +157 -0
  119. package/server/engine/mcp/tools/browse.js +480 -0
  120. package/server/engine/mcp/tools/build-docx.js +101 -0
  121. package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
  122. package/server/engine/mcp/tools/create-on-board.js +155 -0
  123. package/server/engine/mcp/tools/crystallize-skill.js +168 -0
  124. package/server/engine/mcp/tools/deliver-files.js +0 -0
  125. package/server/engine/mcp/tools/explain-style.js +237 -0
  126. package/server/engine/mcp/tools/export-handoff.js +133 -0
  127. package/server/engine/mcp/tools/expose-tweaks.js +149 -0
  128. package/server/engine/mcp/tools/generate-image.js +842 -0
  129. package/server/engine/mcp/tools/get-computed-styles.js +164 -0
  130. package/server/engine/mcp/tools/get-pending-changes.js +205 -0
  131. package/server/engine/mcp/tools/h3box-ssh.js +84 -0
  132. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
  133. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
  134. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
  135. package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
  136. package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
  137. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
  138. package/server/engine/mcp/tools/helpers/rembg.js +273 -0
  139. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
  140. package/server/engine/mcp/tools/highlight.js +68 -0
  141. package/server/engine/mcp/tools/list-pages.js +222 -0
  142. package/server/engine/mcp/tools/lookup-tags.js +59 -0
  143. package/server/engine/mcp/tools/navigate-to-page.js +60 -0
  144. package/server/engine/mcp/tools/organize-board.js +64 -0
  145. package/server/engine/mcp/tools/paint-still.js +434 -0
  146. package/server/engine/mcp/tools/pin-to-board.js +130 -0
  147. package/server/engine/mcp/tools/preview-deck.js +130 -0
  148. package/server/engine/mcp/tools/profile-scroll.js +305 -0
  149. package/server/engine/mcp/tools/publish-site.js +102 -0
  150. package/server/engine/mcp/tools/query-elements.js +195 -0
  151. package/server/engine/mcp/tools/read-board.js +103 -0
  152. package/server/engine/mcp/tools/read-document.js +93 -0
  153. package/server/engine/mcp/tools/read-page.js +264 -0
  154. package/server/engine/mcp/tools/read-tavern-json.js +143 -0
  155. package/server/engine/mcp/tools/record-decision.js +140 -0
  156. package/server/engine/mcp/tools/relate-on-board.js +110 -0
  157. package/server/engine/mcp/tools/remove-background.js +391 -0
  158. package/server/engine/mcp/tools/report-issue.js +115 -0
  159. package/server/engine/mcp/tools/roll-film.js +266 -0
  160. package/server/engine/mcp/tools/screenshot-docx.js +138 -0
  161. package/server/engine/mcp/tools/screenshot-url.js +195 -0
  162. package/server/engine/mcp/tools/screenshot.js +583 -0
  163. package/server/engine/mcp/tools/tier-gate.js +96 -0
  164. package/server/engine/mcp/tools/trace-motion.js +215 -0
  165. package/server/engine/mcp/tools/web-search.js +548 -0
  166. package/server/engine/motion/inventory.js +349 -0
  167. package/server/engine/perception/session.js +235 -0
  168. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
  174. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
  175. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
  176. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
  177. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
  178. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
  179. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
  180. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
  181. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -0
  182. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
  183. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
  185. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
  186. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
  187. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
  188. package/server/engine/runs/active-runs.js +698 -0
  189. package/server/engine/runs/live-turn.js +252 -0
  190. package/server/engine/runs/store.js +364 -0
  191. package/server/engine/runs/turn-relay.js +217 -0
  192. package/server/engine/runtime/workspace.js +164 -0
  193. package/server/index.js +235 -0
  194. package/server/lib/artifact-file-path.js +80 -0
  195. package/server/lib/artifact-target.js +284 -0
  196. package/server/lib/asset-refs.js +239 -0
  197. package/server/lib/async-queue.js +104 -0
  198. package/server/lib/auto-relations.js +130 -0
  199. package/server/lib/binding-types.js +60 -0
  200. package/server/lib/board-kind-sizes.js +62 -0
  201. package/server/lib/board-relations.js +133 -0
  202. package/server/lib/browse-proxy.js +180 -0
  203. package/server/lib/canvas-id.js +36 -0
  204. package/server/lib/cover.js +227 -0
  205. package/server/lib/danbooru-tags.js +291 -0
  206. package/server/lib/doc-extract.js +156 -0
  207. package/server/lib/docx/build-from-source.js +260 -0
  208. package/server/lib/docx/build.js +490 -0
  209. package/server/lib/docx/dump-styles.js +287 -0
  210. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
  211. package/server/lib/docx/merge-runs.js +129 -0
  212. package/server/lib/docx/numbering.js +218 -0
  213. package/server/lib/docx/order.js +152 -0
  214. package/server/lib/docx/rawzip.js +172 -0
  215. package/server/lib/docx/render.js +92 -0
  216. package/server/lib/docx/text-lint.js +225 -0
  217. package/server/lib/docx/tokens.js +335 -0
  218. package/server/lib/docx/units.js +29 -0
  219. package/server/lib/docx/xml.js +291 -0
  220. package/server/lib/docx-pages.js +187 -0
  221. package/server/lib/export-collect.js +289 -0
  222. package/server/lib/export-package.js +210 -0
  223. package/server/lib/html-srcset.js +145 -0
  224. package/server/lib/image-variant.js +460 -0
  225. package/server/lib/ingress/forward-openai-chat.js +305 -0
  226. package/server/lib/ingress/openai-chat.js +491 -0
  227. package/server/lib/ingress/session-notice.js +66 -0
  228. package/server/lib/ingress/session-routes.js +68 -0
  229. package/server/lib/ingress/slot-probe.js +130 -0
  230. package/server/lib/ingress/upstream-billing.js +52 -0
  231. package/server/lib/ingress/upstream-fail-streak.js +72 -0
  232. package/server/lib/ingress/upstream-truncation.js +48 -0
  233. package/server/lib/issues-store.js +182 -0
  234. package/server/lib/kinds/deck.js +72 -0
  235. package/server/lib/kinds/docx.js +283 -0
  236. package/server/lib/kinds/file-kinds.js +113 -0
  237. package/server/lib/kinds/index.js +202 -0
  238. package/server/lib/kinds/site.js +161 -0
  239. package/server/lib/model-ingress.js +543 -0
  240. package/server/lib/moderation.js +255 -0
  241. package/server/lib/notice-store.js +103 -0
  242. package/server/lib/plugin-install.js +104 -0
  243. package/server/lib/plugin-validator.js +721 -0
  244. package/server/lib/publish-store.js +115 -0
  245. package/server/lib/quick-summary.js +52 -0
  246. package/server/lib/quota.js +270 -0
  247. package/server/lib/rate-window.js +29 -0
  248. package/server/lib/reference-assets.js +92 -0
  249. package/server/lib/region-shot.js +135 -0
  250. package/server/lib/safe-path.js +73 -0
  251. package/server/lib/sdk-session.js +33 -0
  252. package/server/lib/showcase-store.js +92 -0
  253. package/server/lib/site-publish.js +465 -0
  254. package/server/lib/ssrf-guard.js +432 -0
  255. package/server/lib/task-scan.js +158 -0
  256. package/server/lib/tavern-json.js +154 -0
  257. package/server/lib/video-variant.js +237 -0
  258. package/server/lib/workspace-path.js +33 -0
  259. package/server/ops/fix-sdk-musl.mjs +40 -0
  260. package/server/ops/install-macos-fonts.sh +114 -0
  261. package/server/ops/macos-fonts.conf +336 -0
  262. package/server/ops/sandbox-shim/bwrap +57 -0
  263. package/server/projects/assets-summary.js +119 -0
  264. package/server/projects/auto-name.js +53 -0
  265. package/server/projects/board-store.js +640 -0
  266. package/server/projects/move-entry.js +103 -0
  267. package/server/projects/store.js +270 -0
  268. package/server/projects/ui-config.js +54 -0
  269. package/server/projects/workspace-templates.js +53 -0
  270. package/server/projects/workspace.js +1025 -0
  271. package/server/runtime/capabilities.js +153 -0
  272. package/server/runtime/local-config.js +177 -0
  273. package/server/runtime/local-env.js +88 -0
  274. package/server/runtime/platform.js +342 -0
  275. package/server/runtime/profile.js +72 -0
  276. package/server/services/rembg-launcher.js +313 -0
  277. package/server/services/rembg-service.py +334 -0
  278. package/server/shared/README.md +25 -0
  279. package/server/shared/deck.js +54 -0
  280. package/server/shared/time.js +49 -0
  281. package/server/style-pipeline/README.md +53 -0
  282. package/server/ws/broker.js +36 -0
  283. package/server/ws/browse-channel.js +177 -0
  284. package/server/ws/index.js +386 -0
  285. package/web/README.md +89 -0
  286. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
  287. package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
  288. package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
  289. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  290. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  291. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  292. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  293. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  294. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  295. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  296. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  297. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  298. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  299. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  300. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  301. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  302. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  303. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  304. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  305. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  306. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  307. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  308. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  309. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  310. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  311. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  312. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  313. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  314. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  315. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  316. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  317. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  318. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  319. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  320. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  321. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  322. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  323. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  324. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  325. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  326. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  327. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  328. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  329. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  330. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  331. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  332. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  333. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  334. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  335. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  336. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  337. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  338. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  339. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  340. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  341. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  342. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  343. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  344. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  345. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  346. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  347. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  348. package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
  349. package/web/dist/assets/again-D8tfgKKO.webp +0 -0
  350. package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
  351. package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
  352. package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
  353. package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
  354. package/web/dist/assets/index-C0KUf6Wd.css +1 -0
  355. package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
  356. package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
  357. package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
  358. package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
  359. package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
  360. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  361. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  362. package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
  363. package/web/dist/assets/question-8H4HSqxh.webp +0 -0
  364. package/web/dist/assets/reject-BaPJqElj.webp +0 -0
  365. package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
  366. package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
  367. package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
  368. package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
  369. package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
  370. package/web/dist/assets/thisone-BylZKglV.webp +0 -0
  371. package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
  372. package/web/dist/index.html +18 -0
@@ -0,0 +1,491 @@
1
+ /**
2
+ * lib/ingress/openai-chat.js — Anthropic Messages ⇄ OpenAI Chat Completions 协议转换(2026-08-21)
3
+ *
4
+ * ## 为什么有这层
5
+ *
6
+ * SDK binary 永远说 Anthropic Messages;model-ingress 以前只会**转发**(上游也说 Anthropic)。
7
+ * OpenCode Zen 的免费模型 Ox Alpha(x-preview-f-free)只有 OpenAI chat 格式能用工具
8
+ * (Zen 给它架的 /v1/messages 桥一带 tools 就 [1210],08-21 四种写法探死),newapi 中转站
9
+ * 同病。所以协议映射得自己做 —— 不上 gproxy(外部守护进程 + 四个已知洞 + 第二个 quirk
10
+ * 真相源),在 ingress 里按上游 `protocol: 'openai-chat'` 分岔,其余上游一字不动。
11
+ *
12
+ * ## 映射要点(都是探针实测逼出来的,不是抄规范)
13
+ *
14
+ * - tool_result 里的图一律提到紧随其后的 user 消息里:tool 角色消息里放 image_url 上游挂死 120s
15
+ * - tool_result 必须紧跟 assistant 的 tool_calls:Anthropic 一条 user 消息里 tool_result 与
16
+ * 文本混排 → 先吐 role:tool 条,再吐 role:user 条(文本 + 提出来的图)
17
+ * - thinking 块不回传(没有 signature 机制);assistant 历史里的 thinking 合成 reasoning_content
18
+ * (models.dev 标 interleaved.field=reasoning_content,回传给模型接着想)
19
+ * - Anthropic thinking 参数 → reasoning_effort(行内 reasoningEffort,Ox 三档 low|high|max)
20
+ * - 流式:OpenAI chunk → 合成 message_start / content_block_* / message_delta / message_stop;
21
+ * usage 在最后一个 chunk(stream_options.include_usage),Anthropic 口径 input 不含 cache 命中
22
+ * - stop_reason:tool_calls→tool_use · stop→end_turn · length→max_tokens;有 tool_calls 但
23
+ * finish 说 stop 也算 tool_use(CLI 认块不认 stop_reason,但别给它矛盾信号)
24
+ * - **上游私货 finish_reason**(08-21):Zen 会吐 `finish_reason:"network_error"`(它到模型
25
+ * 提供方那一跳断了),实测形态是挂 185 秒或快败 6~9 秒后零 delta 收场。这种值不在
26
+ * STOP_MAP 里,以前落 `|| 'end_turn'` = 把上游故障包装成"成功的空回合",CLI 只能补一句
27
+ * "你上一轮没有可见输出"再跑一整轮(真实代价:185s 空转 + 一整轮重来)。现在改成发
28
+ * error 事件 —— **假上游实验实测:CLI 收到流中 error 会在 0.2~0.4 秒内原样重发,用户
29
+ * 全程无感**,所以重试交给它,ingress 不自建(两层重试互不知情,只会让失败会话多占并发槽)
30
+ * - **refusal 字段**:OpenAI 的拒答走 `delta.refusal` / `message.refusal` 而不是 content,
31
+ * 我们以前整个没读 = 又一种"零可见输出的假成功"。当文本吐出去即可。
32
+ * ⚠️ 别顺手把 content_filter 映射成 Anthropic 的 `refusal` stop_reason(gproxy 那么写):
33
+ * 实测 CLI 见到 stop_reason=refusal 会弹「Start a new session」并丢弃随流正文,
34
+ * 会话直接判死 —— 现有的 content_filter→end_turn 在 Claude Code 语境下才是对的
35
+ */
36
+ import { Transform } from 'node:stream';
37
+
38
+ const STOP_MAP = { tool_calls: 'tool_use', stop: 'end_turn', length: 'max_tokens', content_filter: 'end_turn', function_call: 'tool_use' };
39
+
40
+ /** finish_reason 是不是上游私货(不在 STOP_MAP 里)。null/undefined 不算 —— 那是"没给收尾原因",另有分支 */
41
+ const isAlienFinish = (finish) => Boolean(finish) && !(finish in STOP_MAP);
42
+
43
+ /**
44
+ * 「半截」判据(08-21 晚,对齐 OpenCode 1.18.21 的 unknown-finish 续接):**已经说出正文、
45
+ * 却没有可信的收尾原因**(无 finish_reason = 上游把流掐了;私货 finish 如 network_error)。
46
+ * 这种响应我们照旧按 end_turn 交付(假上游实测:有可见输出后再发 error 事件 CLI **不重试**,
47
+ * 只会把半截 + "Server error mid-response" 一起交给用户并判 is_error),
48
+ * 但要标记出来让 session-loop 自动续接一轮 —— 否则半截答案就是最终答案。
49
+ *
50
+ * ⚠️ 出过 tool_call 的不算:那种半截 CLI 自己会治(坏 JSON → __unparsedToolInput →
51
+ * 本地合成 InputValidationError tool_result → 模型自己重来,实测 8s 内自愈)。
52
+ * 对它续接等于叠加,实测还会把回合拖进 max_turns。
53
+ *
54
+ * @returns {string|null} 原因串(进日志/审计),null = 不是半截
55
+ */
56
+ export function truncationReason({ finish, sawText, sawToolCall, doneSeen = false }) {
57
+ if (!sawText || sawToolCall) return null;
58
+ // ⭐ 上游好好地发了 `[DONE]` 只是末块没带 finish_reason(OpenAI 兼容实现里不罕见):
59
+ // 那是**收完了**,不是被掐。少这一条判据的话,换一家这种脾气的上游就会每一轮都平白
60
+ // 续接到封顶 —— 3 倍 token、3 倍延迟,外加一条冤枉用户的告警(fable 评审 P1,探针复现过)。
61
+ if (!finish && doneSeen) return null;
62
+ if (!finish) return 'no finish_reason';
63
+ if (isAlienFinish(finish)) return `finish_reason='${finish}'`;
64
+ return null;
65
+ }
66
+
67
+ function textOfBlocks(blocks) {
68
+ if (typeof blocks === 'string') return blocks;
69
+ if (!Array.isArray(blocks)) return '';
70
+ return blocks.filter(b => b?.type === 'text' && typeof b.text === 'string').map(b => b.text).join('\n');
71
+ }
72
+
73
+ function imagePart(block) {
74
+ const src = block?.source;
75
+ if (!src) return null;
76
+ if (src.type === 'base64' && src.data) return { type: 'image_url', image_url: { url: `data:${src.media_type || 'image/png'};base64,${src.data}` } };
77
+ if (src.type === 'url' && src.url) return { type: 'image_url', image_url: { url: src.url } };
78
+ return null;
79
+ }
80
+
81
+ /** tool_result.content → (text, images[])。图不留在 tool 消息里(上游挂死),拿出来给调用方放进 user 消息 */
82
+ function splitToolResult(block) {
83
+ const images = [];
84
+ let text = '';
85
+ if (typeof block.content === 'string') text = block.content;
86
+ else if (Array.isArray(block.content)) {
87
+ const parts = [];
88
+ for (const inner of block.content) {
89
+ if (inner?.type === 'text') parts.push(inner.text || '');
90
+ else if (inner?.type === 'image') { const p = imagePart(inner); if (p) { images.push(p); parts.push('[image: see the image attached to the following user message]'); } }
91
+ }
92
+ text = parts.join('\n');
93
+ }
94
+ if (block.is_error && text) text = `[tool error] ${text}`;
95
+ return { text, images };
96
+ }
97
+
98
+ /**
99
+ * @param {object} parsed Anthropic Messages body(已过 transformForUpstream:model 已是 wireModel)
100
+ * @param {{ reasoningEffort?: string, maxOutput?: number }} opts
101
+ * @returns {object} OpenAI chat.completions body
102
+ */
103
+ export function toOpenAIChatRequest(parsed, opts = {}) {
104
+ const out = { model: parsed.model, messages: [] };
105
+ const sys = textOfBlocks(parsed.system);
106
+ if (sys) out.messages.push({ role: 'system', content: sys });
107
+
108
+ for (const msg of parsed.messages || []) {
109
+ if (!msg) continue;
110
+ if (msg.role === 'assistant') {
111
+ const m = { role: 'assistant', content: '' };
112
+ if (typeof msg.content === 'string') m.content = msg.content;
113
+ else if (Array.isArray(msg.content)) {
114
+ const texts = []; const thoughts = []; const calls = [];
115
+ for (const b of msg.content) {
116
+ if (b?.type === 'text') texts.push(b.text || '');
117
+ else if (b?.type === 'thinking' && b.thinking) thoughts.push(b.thinking);
118
+ else if (b?.type === 'tool_use') calls.push({ id: b.id, type: 'function', function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } });
119
+ }
120
+ // 只有 thinking(被打断的回合)时 content 为空且无 tool_calls,部分 OpenAI 兼容后端会 400 —— 补个占位
121
+ m.content = texts.join('\n') || (calls.length ? '' : '(no text)');
122
+ if (thoughts.length) m.reasoning_content = thoughts.join('\n');
123
+ if (calls.length) m.tool_calls = calls;
124
+ }
125
+ out.messages.push(m);
126
+ continue;
127
+ }
128
+ // user:tool_result 先出(紧跟 tool_calls),其余文本/图合成一条 user
129
+ if (typeof msg.content === 'string') { out.messages.push({ role: 'user', content: msg.content }); continue; }
130
+ if (!Array.isArray(msg.content)) continue;
131
+ const toolMsgs = []; const parts = []; const lifted = [];
132
+ for (const b of msg.content) {
133
+ if (b?.type === 'tool_result') {
134
+ const { text, images } = splitToolResult(b);
135
+ toolMsgs.push({ role: 'tool', tool_call_id: b.tool_use_id, content: text || '(empty)' });
136
+ lifted.push(...images);
137
+ } else if (b?.type === 'text') parts.push({ type: 'text', text: b.text || '' });
138
+ else if (b?.type === 'image') { const p = imagePart(b); if (p) parts.push(p); }
139
+ else if (b?.type === 'document') parts.push({ type: 'text', text: '[document attachment omitted: upstream cannot read documents]' });
140
+ }
141
+ out.messages.push(...toolMsgs);
142
+ const all = [...parts, ...lifted];
143
+ if (all.length) {
144
+ const onlyText = all.every(p => p.type === 'text');
145
+ out.messages.push({ role: 'user', content: onlyText ? all.map(p => p.text).join('\n') : all });
146
+ }
147
+ }
148
+
149
+ if (Array.isArray(parsed.tools)) {
150
+ const fns = parsed.tools
151
+ .filter(t => t && t.name && (t.type === undefined || t.type === 'custom'))
152
+ .map(t => ({ type: 'function', function: { name: t.name, description: t.description || '', parameters: t.input_schema || { type: 'object', properties: {} } } }));
153
+ if (fns.length) out.tools = fns;
154
+ }
155
+ const tc = parsed.tool_choice;
156
+ if (tc && out.tools) {
157
+ if (tc.type === 'any') out.tool_choice = 'required';
158
+ else if (tc.type === 'none') out.tool_choice = 'none';
159
+ else if (tc.type === 'tool' && tc.name) out.tool_choice = { type: 'function', function: { name: tc.name } };
160
+ else out.tool_choice = 'auto';
161
+ }
162
+
163
+ const cap = opts.maxOutput || 131072;
164
+ out.max_tokens = Math.max(1, Math.min(Number(parsed.max_tokens) || cap, cap));
165
+ if (typeof parsed.temperature === 'number') out.temperature = parsed.temperature;
166
+ if (typeof parsed.top_p === 'number') out.top_p = parsed.top_p;
167
+ if (Array.isArray(parsed.stop_sequences) && parsed.stop_sequences.length) out.stop = parsed.stop_sequences.slice(0, 4);
168
+ if (parsed.stream) { out.stream = true; out.stream_options = { include_usage: true }; }
169
+ // 档位只看行内 reasoningEffort:Anthropic 的 thinking 字段在进到这里之前已被 transformForUpstream
170
+ // 按行内 thinking:'strip' 删掉(fable 评审抓的:以前以它存在为前提,档位从没发出去过)
171
+ if (opts.reasoningEffort && parsed.thinking?.type !== 'disabled') out.reasoning_effort = opts.reasoningEffort;
172
+ return out;
173
+ }
174
+
175
+ function usageFromOpenAI(u) {
176
+ if (!u) return { input_tokens: 0, output_tokens: 0 };
177
+ const cached = Number(u.prompt_tokens_details?.cached_tokens) || 0;
178
+ const prompt = Number(u.prompt_tokens) || 0;
179
+ return {
180
+ input_tokens: Math.max(0, prompt - cached),
181
+ output_tokens: Number(u.completion_tokens) || 0,
182
+ cache_read_input_tokens: cached,
183
+ cache_creation_input_tokens: 0,
184
+ };
185
+ }
186
+
187
+ function parseArgs(s) {
188
+ if (s == null || s === '') return {};
189
+ try { return JSON.parse(s); } catch { return { _raw_arguments: String(s) }; }
190
+ }
191
+
192
+ /**
193
+ * 非流式:OpenAI chat.completion → Anthropic message。
194
+ * 返回 null = 别包成成功回合,调用方回 502(CLI 会重试)。两种情况返 null:
195
+ * ① 没有 choices(上游 200 但给了错误体/空体)
196
+ * ② finish_reason 是上游私货且零可见输出(流式那条 `_finish` 的孪生洞,同一张 STOP_MAP)
197
+ */
198
+ export function fromOpenAIChatResponse(json) {
199
+ if (!json || !Array.isArray(json.choices) || !json.choices.length) return null;
200
+ const choice = json.choices[0] || {};
201
+ const m = choice.message || {};
202
+ const content = [];
203
+ if (m.reasoning_content) content.push({ type: 'thinking', thinking: String(m.reasoning_content), signature: '' });
204
+ if (m.content) content.push({ type: 'text', text: String(m.content) });
205
+ if (m.refusal) content.push({ type: 'text', text: String(m.refusal) });
206
+ for (const c of m.tool_calls || []) {
207
+ content.push({ type: 'tool_use', id: c.id || `call_${content.length}`, name: c.function?.name || '', input: parseArgs(c.function?.arguments) });
208
+ }
209
+ const hasTools = (m.tool_calls || []).length > 0;
210
+ const hasVisible = hasTools || Boolean(m.content) || Boolean(m.refusal);
211
+ if (isAlienFinish(choice.finish_reason) && !hasVisible) {
212
+ console.warn(`[ingress/openai-chat] upstream finish_reason='${choice.finish_reason}' with no visible output — failing the turn (non-stream)`);
213
+ return null;
214
+ }
215
+ if (!hasVisible) console.warn(`[ingress/openai-chat] finish_reason='${choice.finish_reason}' 收尾但零可见输出(thinking-only,非流式)—— CLI 会补一轮催促`);
216
+ const stop_reason = hasTools ? 'tool_use' : (STOP_MAP[choice.finish_reason] || 'end_turn');
217
+ return {
218
+ id: json?.id || `msg_${Date.now()}`,
219
+ type: 'message', role: 'assistant',
220
+ model: json?.model || '',
221
+ content, stop_reason, stop_sequence: null,
222
+ usage: usageFromOpenAI(json?.usage),
223
+ };
224
+ }
225
+
226
+ /** 上游错误体 → Anthropic 错误体(CLI 会把 message 原样显示) */
227
+ export function toAnthropicError(status, bodyText) {
228
+ let message = bodyText;
229
+ try { const j = JSON.parse(bodyText); message = j?.error?.message || j?.message || bodyText; } catch { /* 非 JSON */ }
230
+ const type = status === 401 || status === 403 ? 'authentication_error'
231
+ : status === 429 ? 'rate_limit_error'
232
+ : status >= 500 ? 'api_error' : 'invalid_request_error';
233
+ return { type: 'error', error: { type, message: String(message).slice(0, 2000) } };
234
+ }
235
+
236
+ /**
237
+ * 非流式响应的「半截」判定(流式那份住在 OpenAIToAnthropicSSE.truncated;两边同一张判据 truncationReason)。
238
+ * @returns {string|null} 原因串,null = 不是半截
239
+ */
240
+ export function truncationOfChatResponse(json) {
241
+ const choice = json?.choices?.[0];
242
+ if (!choice) return null;
243
+ const m = choice.message || {};
244
+ return truncationReason({
245
+ finish: choice.finish_reason,
246
+ sawText: Boolean(m.content) || Boolean(m.refusal),
247
+ sawToolCall: (m.tool_calls || []).length > 0,
248
+ });
249
+ }
250
+
251
+ /**
252
+ * 流式:OpenAI SSE chunk → Anthropic SSE 事件。Transform,直接 pipe。
253
+ * 状态机:当前打开的块(thinking/text/tool_use 之一)+ tool_calls 按 index 映射到块号。
254
+ */
255
+ export class OpenAIToAnthropicSSE extends Transform {
256
+ constructor({ model = '', label = '上游' } = {}) {
257
+ super();
258
+ this.model = model;
259
+ this.label = label; // 上游的人话名字(错误文案用;CLI 会把 message 原样显示给用户)
260
+ this.buf = '';
261
+ this.started = false;
262
+ this.done = false;
263
+ this.blockIndex = -1; // 最后分配的块号
264
+ this.open = null; // { kind: 'thinking'|'text'|'tool', index }
265
+ this.toolBlocks = new Map(); // openai tool_call index → block index
266
+ this.finish = null;
267
+ this.usage = null;
268
+ this.id = null;
269
+ this.sawToolCall = false;
270
+ this.sawText = false; // 有过可见正文(区分"只想没说"的早断流)
271
+ this.failReason = null; // 本次以 error 事件收场的原因(forward 层据此记会话失败计数;null = 正常收尾)
272
+ this.truncated = null; // 本次「半截」收场的原因(见 truncationReason;forward 层据此报给 session-loop 续接)
273
+ this.cost = null; // 上游报的本次费用(美元,/zen/go 在 [DONE] 后补的 cost 字段;null = 上游没报)
274
+ this.doneSeen = false; // 见过 [DONE]:之后只收 cost,收尾等 _flush
275
+ this.attempts = 1; // 这条 SSE 一共打了几发上游(就地重发会 ++)
276
+ this.attemptUsage = null; // 这一发的 usage
277
+ this.attemptCost = null; // 这一发的 cost
278
+ // ⭐ 一份数两个读者,口径不同,别混([[feedback-single-source-of-truth]]):
279
+ // this.usage = **最后一发**的 usage → 进 message_delta 发给 CLI。CLI 拿它算"我的上下文多大",
280
+ // 累加会让一次重发把 input 翻倍,长会话里提前触发压缩。
281
+ // this.usageTotal = **所有发**的累计 → 进 onBilling。记账问的是"真烧了多少",失败那发也烧了。
282
+ this.usageTotal = null;
283
+ this.sawStreamError = false; // 流中途发过 error 事件:这条流已经死了,不许再重发也不许再收尾
284
+ }
285
+ _emit(event, data) { this.push(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); }
286
+ _ensureStart(chunk) {
287
+ if (this.started) return;
288
+ this.started = true;
289
+ this.id = chunk?.id || `msg_${Date.now()}`;
290
+ if (chunk?.model) this.model = chunk.model;
291
+ this._emit('message_start', { type: 'message_start', message: { id: this.id, type: 'message', role: 'assistant', model: this.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } } });
292
+ }
293
+ _closeOpen() {
294
+ if (!this.open) return;
295
+ this._emit('content_block_stop', { type: 'content_block_stop', index: this.open.index });
296
+ this.open = null;
297
+ }
298
+ _openBlock(kind, block) {
299
+ this._closeOpen();
300
+ this.blockIndex += 1;
301
+ this.open = { kind, index: this.blockIndex };
302
+ this._emit('content_block_start', { type: 'content_block_start', index: this.blockIndex, content_block: block });
303
+ return this.blockIndex;
304
+ }
305
+ _handleChunk(chunk) {
306
+ this._ensureStart(chunk);
307
+ if (chunk.usage) this.attemptUsage = chunk.usage;
308
+ if (chunk.cost != null) this.attemptCost = Number(chunk.cost);
309
+ const choice = chunk.choices?.[0];
310
+ if (!choice) return;
311
+ const d = choice.delta || {};
312
+ if (d.reasoning_content) {
313
+ if (this.open?.kind !== 'thinking') this._openBlock('thinking', { type: 'thinking', thinking: '', signature: '' });
314
+ this._emit('content_block_delta', { type: 'content_block_delta', index: this.open.index, delta: { type: 'thinking_delta', thinking: String(d.reasoning_content) } });
315
+ }
316
+ // content 与 refusal 都是"可见正文",进同一个 text 块(拒答也是模型说的话,原样吐给用户)
317
+ for (const piece of [d.content, d.refusal]) {
318
+ if (!piece) continue;
319
+ this.sawText = true;
320
+ if (this.open?.kind !== 'text') this._openBlock('text', { type: 'text', text: '' });
321
+ this._emit('content_block_delta', { type: 'content_block_delta', index: this.open.index, delta: { type: 'text_delta', text: String(piece) } });
322
+ }
323
+ for (const tc of d.tool_calls || []) {
324
+ // 按 index 分块;上游不带 index 时:带 id 的是新调用,否则续上一个
325
+ const key = tc.index ?? (tc.id ? `id:${tc.id}` : this.lastToolKey);
326
+ this.lastToolKey = key;
327
+ this.sawToolCall = true;
328
+ if (!this.toolBlocks.has(key)) {
329
+ const idx = this._openBlock('tool', { type: 'tool_use', id: tc.id || `call_${key}`, name: tc.function?.name || '', input: {} });
330
+ this.toolBlocks.set(key, idx);
331
+ } else if (this.open?.kind !== 'tool' || this.open.index !== this.toolBlocks.get(key)) {
332
+ // 上游交错回到旧的 tool_call(少见):Anthropic 块一旦 stop 不能再开,只能并进当前块号
333
+ this._closeOpen();
334
+ this.open = { kind: 'tool', index: this.toolBlocks.get(key) };
335
+ }
336
+ const args = tc.function?.arguments;
337
+ if (args) this._emit('content_block_delta', { type: 'content_block_delta', index: this.toolBlocks.get(key), delta: { type: 'input_json_delta', partial_json: String(args) } });
338
+ }
339
+ if (choice.finish_reason) this.finish = choice.finish_reason;
340
+ }
341
+ /**
342
+ * 一次上游往返结束(流关了/断了)。**只结账 + 给判决,不发收尾事件** —— 因为判决可能是
343
+ * "这一发白跑了,再打一次",那时这条 SSE 还要继续用(forward 层的就地重发,见该文件)。
344
+ * @returns {{ kind: 'complete'|'truncated'|'empty', reason?: string }}
345
+ */
346
+ attemptEnd() {
347
+ this._foldAttemptTotals();
348
+ this._closeOpen(); // 失败那一发开着的 thinking 块也要闭合,否则块永远悬着
349
+ return this.verdict();
350
+ }
351
+
352
+ /**
353
+ * 这一发算什么:
354
+ * empty —— **零可见输出且收尾原因不可信**(没 finish / 私货 finish / 一个块都没开)。
355
+ * 这是"白跑一发",可以就地重发(跟 OpenCode 对 unknown finish 的做法一个意思)。
356
+ * truncated —— 说了一半被掐(判据见 truncationReason),照旧 end_turn 交付 + 标记续接。
357
+ * complete —— 正常收尾。⭐ 已知 finish(stop/length)+ 零可见输出**不算 empty**:
358
+ * 那是上游好好地告诉你"我就没话说",OpenCode 同样直接结束不重试。
359
+ */
360
+ verdict() {
361
+ // 已经往流里发过 error 事件:这条流按协议就结束了(CLI 实测收到流中 error 会 0.2 秒静默重发,
362
+ // 人早走了)。既不能重发上游(纯烧钱),也不该再补 message_stop(error 之后再接内容块是非法的)。
363
+ if (this.sawStreamError) return { kind: 'errored', reason: 'upstream sent an error mid-stream' };
364
+ if (!this.started || (this.blockIndex < 0 && !this.finish)) {
365
+ return { kind: 'empty', reason: 'empty response' };
366
+ }
367
+ const zeroVisible = !this.sawText && !this.sawToolCall;
368
+ if (zeroVisible && !this.finish) return { kind: 'empty', reason: 'stream ended before any visible output' };
369
+ if (zeroVisible && isAlienFinish(this.finish)) return { kind: 'empty', reason: `finish_reason='${this.finish}' with no visible output` };
370
+ const truncated = truncationReason({ finish: this.finish, sawText: this.sawText, sawToolCall: this.sawToolCall, doneSeen: this.doneSeen });
371
+ if (truncated) return { kind: 'truncated', reason: truncated };
372
+ return { kind: 'complete' };
373
+ }
374
+
375
+ /** 整条响应收尾:发 Anthropic 的收场事件(或 error 事件)。调一次就封口。 */
376
+ finalize(v = this.verdict()) {
377
+ if (this.done) return;
378
+ this.done = true;
379
+ if (v.kind === 'errored') { // error 事件已经发过了(块也已闭合),不再补任何事件
380
+ this.failReason = this.failReason || v.reason;
381
+ return;
382
+ }
383
+ if (v.kind === 'empty') {
384
+ // 零可见输出且收尾不可信 —— forward 层已经就地重发过(额度用完了才走到这),
385
+ // 别包装成"成功的空消息"让 CLI 当正常结束(那会触发它"你上一轮没有可见输出"的催促循环)
386
+ this._ensureStart(null);
387
+ this.failReason = v.reason;
388
+ const msg = v.reason === 'empty response'
389
+ ? `${this.label}返回了空响应,一个字都没有 —— 上游问题,已自动重发仍失败;稍后再发,或换个模型(upstream returned an empty response)`
390
+ : v.reason.startsWith('finish_reason=')
391
+ ? `${this.label}以 ${this.finish} 结束了这次请求,没有输出任何正文 —— 上游自己的链路出错,已自动重发仍失败;稍后再发,或换个模型(upstream ended with ${v.reason})`
392
+ : `${this.label}在模型还在思考、还没输出正文时就结束了响应 —— 上游问题,已自动重发仍失败;稍后再发,或换个模型/思考档(upstream stream ended before any visible output)`;
393
+ console.warn(`[ingress/openai-chat] 零可见输出收场(${v.reason})—— 重发额度已用完,这一轮判失败`);
394
+ this._emit('error', { type: 'error', error: { type: 'api_error', message: msg } });
395
+ return;
396
+ }
397
+ if (v.kind === 'truncated') {
398
+ this.truncated = v.reason;
399
+ console.warn(`[ingress/openai-chat] 半截收场(${v.reason})—— 按 end_turn 交付,标记待续接`);
400
+ } else if (isAlienFinish(this.finish)) {
401
+ console.warn(`[ingress/openai-chat] 未知 finish_reason='${this.finish}'(有可见输出);按 end_turn 收尾`);
402
+ } else if (this.finish && !this.sawText && !this.sawToolCall) {
403
+ // 已知 finish 但零可见块:上游明说自己收完了,不重发(跟 OpenCode 一致)。CLI 会补一轮催促
404
+ console.warn(`[ingress/openai-chat] finish_reason='${this.finish}' 收尾但零可见输出(thinking-only)—— CLI 会补一轮催促`);
405
+ }
406
+ this._closeOpen();
407
+ const stop_reason = this.sawToolCall ? 'tool_use' : (STOP_MAP[this.finish] || 'end_turn');
408
+ this._emit('message_delta', { type: 'message_delta', delta: { stop_reason, stop_sequence: null }, usage: usageFromOpenAI(this.usage) });
409
+ this._emit('message_stop', { type: 'message_stop' });
410
+ }
411
+
412
+ /**
413
+ * 就地失败收尾:发一条 error 事件就封口(重发那几发拿到 4xx/5xx 时用)。
414
+ * ⚠️ 必须走它而不是"手写 _emit + end":`done` 要在这里置上,否则 `_flush` 会再判决一次、
415
+ * 补第二条 error,还会把 failReason 从真因(限流/鉴权)盖成"零可见输出"。
416
+ */
417
+ failWith(message, reason) {
418
+ if (this.done) return;
419
+ this.done = true;
420
+ this.failReason = reason || 'upstream error';
421
+ this._ensureStart(null);
422
+ this._closeOpen();
423
+ this._emit('error', { type: 'error', error: { type: 'api_error', message: String(message).slice(0, 2000) } });
424
+ }
425
+
426
+ /**
427
+ * 开始新一发上游往返(就地重发时调)。**只重置"这一发"的状态**:
428
+ * 块号 / 已发出的块 / 见过正文没 / 累计 usage 与 cost 全部保留 —— 它们属于这条 SSE 而不是某一发。
429
+ */
430
+ beginAttempt() {
431
+ this.buf = '';
432
+ this.finish = null;
433
+ this.doneSeen = false;
434
+ this.attemptUsage = null;
435
+ this.toolBlocks = new Map(); // 新一发的 tool_call index 从 0 重来,别跟上一发的块号串了
436
+ this.lastToolKey = undefined;
437
+ this.attempts += 1;
438
+ }
439
+
440
+ /** 把这一发的 usage/cost 折进整条响应的累计(失败那一发也烧了上游的 token,账要算它) */
441
+ _foldAttemptTotals() {
442
+ const u = this.attemptUsage;
443
+ if (u) {
444
+ this.usage = u; // 给 CLI 的:最后一发的真实上下文大小
445
+ const t = this.usageTotal || { prompt_tokens: 0, completion_tokens: 0 };
446
+ this.usageTotal = {
447
+ prompt_tokens: (t.prompt_tokens || 0) + (u.prompt_tokens || 0),
448
+ completion_tokens: (t.completion_tokens || 0) + (u.completion_tokens || 0),
449
+ prompt_tokens_details: { cached_tokens: (t.prompt_tokens_details?.cached_tokens || 0) + (u.prompt_tokens_details?.cached_tokens || 0) },
450
+ completion_tokens_details: { reasoning_tokens: (t.completion_tokens_details?.reasoning_tokens || 0) + (u.completion_tokens_details?.reasoning_tokens || 0) },
451
+ };
452
+ this.attemptUsage = null;
453
+ }
454
+ if (this.attemptCost != null) {
455
+ this.cost = (this.cost || 0) + this.attemptCost;
456
+ this.attemptCost = null;
457
+ }
458
+ }
459
+
460
+ _transform(chunk, _enc, cb) {
461
+ this.buf += chunk.toString('utf8');
462
+ let nl;
463
+ while ((nl = this.buf.indexOf('\n')) >= 0) {
464
+ const line = this.buf.slice(0, nl).replace(/\r$/, '');
465
+ this.buf = this.buf.slice(nl + 1);
466
+ if (!line.startsWith('data:')) continue;
467
+ const payload = line.slice(5).trim();
468
+ if (!payload) continue;
469
+ // [DONE] 不立刻收尾:Zen(/zen/go 入口)在 [DONE] **之后**还补一条 {"choices":[],"cost":"0.00123"},
470
+ // 那是上游报的真实费用(记账要它)。[DONE] 后只认 cost,别的都忽略;真正收尾在 _flush(上游关流)
471
+ if (payload === '[DONE]') { this.doneSeen = true; continue; }
472
+ let j;
473
+ try { j = JSON.parse(payload); } catch { continue; }
474
+ if (this.doneSeen || this.done) { if (j && j.cost != null) this.attemptCost = Number(j.cost); continue; }
475
+ if (j?.error) { // 流中途的错误体:转成 Anthropic error 事件
476
+ this._ensureStart(j);
477
+ this._closeOpen(); // 先把开着的块闭合再发 error(顺序反了会出现 error 后面还跟 content_block_stop)
478
+ this.sawStreamError = true;
479
+ this._emit('error', { type: 'error', error: { type: 'api_error', message: String(j.error.message || j.error) } });
480
+ continue;
481
+ }
482
+ try { this._handleChunk(j); } catch (err) { console.warn('[ingress/openai-chat] chunk handling failed:', err.message); }
483
+ }
484
+ cb();
485
+ }
486
+ _flush(cb) {
487
+ // 单发场景(没人调 attemptEnd 手动收尾):走同一条判决 → 收尾的路,行为与从前一致
488
+ if (!this.done) this.finalize(this.attemptEnd());
489
+ cb();
490
+ }
491
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * lib/ingress/session-notice.js —— ingress 往「正在等的那个会话」推一句人话(08-21 晚)。
3
+ *
4
+ * 病:上游抖的时候(Zen 的 503 挂 50~140 秒才返回、network_error 慢败 185 秒),CLI 在退避重试,
5
+ * 会话里**什么都不动** —— 用户看到的是一个转了三五分钟的绿点,不知道是卡死了还是在重试,
6
+ * 也就不知道该等还是该点停止。
7
+ *
8
+ * 治法:ingress 每次上游失败就往会话推一条通知(前端弹 toast)。ingress 住 lib/、会话住 engine/,
9
+ * 为了不让 lib 反向依赖 engine(session-loop 已经 import model-ingress),这里只做一个回调注册表:
10
+ * session-loop 起会话时 register,结束时 unregister,ingress 只管 notice。
11
+ *
12
+ * 节流住在这里而不是调用方:一次失败 CLI 0.2 秒就重发,连着几发能刷出一串 toast。
13
+ * 同一会话 MIN_INTERVAL_MS 内只推一条,够用户知道"在重试",不至于变成噪音。
14
+ */
15
+
16
+ export const MIN_INTERVAL_MS = 20_000;
17
+
18
+ export class SessionNotices {
19
+ constructor({ minIntervalMs = MIN_INTERVAL_MS, now = () => Date.now() } = {}) {
20
+ this.handlers = new Map(); // sid → fn(payload)
21
+ this.lastAt = new Map(); // sid → ts(节流)
22
+ this.minIntervalMs = minIntervalMs;
23
+ this.now = now;
24
+ }
25
+ register(sid, fn) { if (sid && typeof fn === 'function') this.handlers.set(sid, fn); }
26
+ /**
27
+ * 注销。`expected` 给了就只在**当前登记的就是它**时才删(跟 unregisterQuerySession 的 token 比对同一招):
28
+ * 旧会话的 finally 跑在 `await commitWorkspace` 之后,可能已经隔了几秒,期间同 sid 的新会话
29
+ * 已经 register —— 不比对就会把新会话的通道删掉,新会话从此静默收不到上游提示。
30
+ */
31
+ unregister(sid, expected = null) {
32
+ if (!sid) return;
33
+ if (expected && this.handlers.get(sid) !== expected) return;
34
+ this.handlers.delete(sid);
35
+ this.lastAt.delete(sid);
36
+ }
37
+ /**
38
+ * 推一条。没有注册者(非会话请求 / 会话已结束)就静默丢弃。
39
+ * @param {string} sid
40
+ * @param {{ key?: string, text: string, priority?: string }} payload
41
+ * @param {{ throttle?: boolean }} [opts] throttle=false 强制推(默认 true)
42
+ * @returns {boolean} 真推出去了吗
43
+ */
44
+ notice(sid, payload, { throttle = true } = {}) {
45
+ const fn = sid ? this.handlers.get(sid) : null;
46
+ if (!fn || !payload?.text) return false;
47
+ const now = this.now();
48
+ if (throttle && this.lastAt.has(sid)) {
49
+ // ⚠️ 别写成 `(this.lastAt.get(sid) || 0)`:没推过时那是 0,真时钟下 now-0 巨大碰巧放行,
50
+ // 假时钟(测试里 now()=0)却把**第一条**吞掉 —— 同一段代码两种行为。
51
+ if (now - this.lastAt.get(sid) < this.minIntervalMs) return false;
52
+ }
53
+ this.lastAt.set(sid, now);
54
+ try { fn(payload); return true; } catch (err) {
55
+ console.warn(`[session-notice] handler failed for ${String(sid).slice(0, 8)}: ${err.message}`);
56
+ return false;
57
+ }
58
+ }
59
+ }
60
+
61
+ /** 进程内单例 */
62
+ export const sessionNotices = new SessionNotices();
63
+
64
+ export function registerSessionNotice(sid, fn) { sessionNotices.register(sid, fn); }
65
+ export function unregisterSessionNotice(sid, expected) { sessionNotices.unregister(sid, expected); }
66
+ export function noticeSession(sid, payload, opts) { return sessionNotices.notice(sid, payload, opts); }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * lib/ingress/session-routes.js — 会话级路由(08-22 从 model-ingress.js 拆出,行为见 resolveSessionWire)。
3
+ *
4
+ * SDK binary 的部分内部 helper 不看 ANTHROPIC_SMALL_FAST_MODEL,直接用它 config 目录里的默认 Claude 名
5
+ * 发请求(_ingress-check 实测抓到 claude-sonnet-5 重试 8 次)。旧基建里这类请求静默打错通路烧钱;
6
+ * fail-loud 之后它们会 502 = helper 功能死。这里给注册过的会话一条兜底:未知 model 名 → 本会话的 fast 模型。
7
+ * session-loop 在 API 会话起 query 前注册、finally 注销。
8
+ *
9
+ * ⛔ 撞名雷(2026-08-19 审计标出、08-20 封死):API 行的 sdkAlias 同时也是真实的 Claude 名。qwen 会话里的
10
+ * binary 若用这类名字发一发 helper 请求,按全表反查会**命中别的行**、带着别家钥匙静默转发——事前无警报、
11
+ * 成功转发不留日志。表级断言封不住(SDK 内部会用哪些名字没法枚举),所以是**会话级路由**:一个会话只认
12
+ * 自己那行和自己的 fast 行,其它一概改道 fast 兜底,绝不跨行。
13
+ *
14
+ * 08-22 起**会话优先**而不是全表优先:本地分发版的外部插槽共用同一个 sdkAlias(表内只剩一个空着的 1M 订阅名,
15
+ * 见 model-context.js EXTERNAL_SDK_ALIAS),全表反查分不出它们,只有会话知道自己是谁。先问会话、再查全表,
16
+ * 对内置行结果不变(自己的 alias 反查到的就是自己)。
17
+ */
18
+
19
+ import { resolveWireModel, resolveModelRoute, wireNamesOf } from '../../engine/agent/model-context.js';
20
+
21
+ const sessionRoutes = new Map(); // sessionId → { appModel, fastModel }
22
+ /** `${sid}:${model}` 只告一次,防日志洪水(model-ingress.js 读写) */
23
+ export const fallbackLogged = new Set();
24
+
25
+ export function registerIngressSession(sessionId, appModel) {
26
+ const route = resolveModelRoute(appModel);
27
+ if (route.mode === 'api') sessionRoutes.set(sessionId, { appModel: route.appModel, fastModel: route.fastModel });
28
+ }
29
+
30
+ export function unregisterIngressSession(sessionId) {
31
+ sessionRoutes.delete(sessionId);
32
+ for (const k of fallbackLogged) {
33
+ if (k.startsWith(sessionId + ':')) fallbackLogged.delete(k);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 会话级路由决策(纯函数,有单测)。
39
+ *
40
+ * - 没注册的会话 / 无会话前缀(探针、体检):全表反查,查不到就是 null(502)。
41
+ * - 注册过的 API 会话:只认**自己那行**和**自己的 fast 行**(按 wireNamesOf:id / alias / 剥 [1m] 的 alias)。
42
+ * 其它名字一律改道 fast 兜底——不在表里的是 SDK helper 默认名('fallback'),在表里但属于别的行的
43
+ * 就是撞名雷('collision'),后者尤其不能放过去(那是别家的钥匙、真钱)。
44
+ *
45
+ * role:'main' = 会话主行的请求(主 agent 一轮);'helper' = fast 行 / 兜底 / 撞名改道
46
+ * (标题、auto 分类器、摘要等一句话的活)。openai-chat 行按 role 选 reasoning_effort。
47
+ *
48
+ * @returns {{ wire: ReturnType<typeof resolveWireModel>, reason: 'table'|'fallback'|'collision'|'none', role: 'main'|'helper',
49
+ * fastModel?: string, sessionModel?: string, collidesWith?: string }}
50
+ */
51
+ export function resolveSessionWire(bodyModel, sessionTag) {
52
+ const sess = sessionTag ? sessionRoutes.get(sessionTag) : null;
53
+ if (!sess) {
54
+ const direct = resolveWireModel(bodyModel);
55
+ return { wire: direct, reason: direct ? 'table' : 'none', role: 'main' };
56
+ }
57
+ if (wireNamesOf(sess.appModel).includes(bodyModel)) return { wire: resolveWireModel(sess.appModel), reason: 'table', role: 'main' };
58
+ if (wireNamesOf(sess.fastModel).includes(bodyModel)) return { wire: resolveWireModel(sess.fastModel), reason: 'table', role: 'helper' };
59
+ const direct = resolveWireModel(bodyModel);
60
+ return {
61
+ wire: resolveWireModel(sess.fastModel),
62
+ role: 'helper',
63
+ reason: direct ? 'collision' : 'fallback',
64
+ fastModel: sess.fastModel,
65
+ sessionModel: sess.appModel,
66
+ ...(direct ? { collidesWith: direct.appModel } : {}),
67
+ };
68
+ }