@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,543 @@
1
+ /**
2
+ * server/lib/model-ingress.js — Anthropic 范式通用模型入口(2026-08-19,
3
+ * 前身 binary-fixup-proxy.js)。
4
+ *
5
+ * ## 定位
6
+ *
7
+ * SDK binary 永远说 Anthropic Messages 协议;所有非订阅模型都从这里进。
8
+ * 入口按 **请求 body.model 查表**(model-context.js 的 resolveWireModel)
9
+ * 决定发往哪个上游、换哪把钥匙、开哪些修补 —— 无会话状态、无进程级
10
+ * "当前模型" env(旧版的 NODESIGN_CURRENT_APP_MODEL 在多会话不同模型时
11
+ * 会互相覆盖,已随 NoDesk 包装一起退役)。
12
+ *
13
+ * ## 每请求流水线
14
+ *
15
+ * 1. 剥 /__nd/<sessionId> 路径前缀(只用于日志归属)
16
+ * 2. body.model 反查路由(appModel / sdkAlias / 剥[1m]的 alias 都认);
17
+ * 查不到 → 502 fail-loud,绝不盲转发(盲转发 = 烧错通路的钱且无人知晓)。
18
+ * 注册过的 API 会话只认自己那行 + 自己的 fast 行,其它名字改道 fast 兜底
19
+ * (未知名 = SDK helper;在表里但属于别的行 = 撞名雷,见 resolveSessionWire)
20
+ * 3. count_tokens:上游没有该端点(表里标了 / 404 实测过)→ 本地估算短路
21
+ * 4. 修补:model 还原成上游真名 · thinking strip/enabled8k · tool_result
22
+ * 图片提升到顶层(Kimi 与 Gemini 桥都丢 tool_result 图,08-19 探针实锤)·
23
+ * 超长边图片下采样
24
+ * 5. 换钥匙:上游钥匙按条目取(外部插槽 .key 内联 > env UPSTREAMS[].keyEnv),按 authStyle 发;
25
+ * binary 带来的 ANTHROPIC_API_KEY 是占位符,一律丢弃
26
+ * 6. 转发(SSE 直接 pipe,不解析响应流)
27
+ *
28
+ * ## 已知缺口(记录在案,别当没有)
29
+ *
30
+ * - 反重力通道流式 stop_reason 恒=end_turn 的病(tool_use 块在、收尾说错话)
31
+ * 需要解析改写 SSE 才能修 —— 那族通道只当玩具,不为它上复杂度。
32
+ * - prompt cache 不过桥(探针实测 cache 字段恒 0),长会话经济性靠模型价差硬扛。
33
+ */
34
+
35
+ import http from 'node:http';
36
+ import https from 'node:https';
37
+ import sharp from 'sharp';
38
+ import { resolveWireModel, UPSTREAMS } from '../engine/agent/model-context.js';
39
+ import { resolveSessionWire, fallbackLogged } from './ingress/session-routes.js';
40
+ import { forwardOpenAIChat } from './ingress/forward-openai-chat.js';
41
+ import { failStreaks, exhaustedErrorBody } from './ingress/upstream-fail-streak.js';
42
+ import { noteUpstreamBilling } from './ingress/upstream-billing.js';
43
+ import { noteUpstreamTruncation } from './ingress/upstream-truncation.js';
44
+ import { noticeSession } from './ingress/session-notice.js';
45
+
46
+ // ── 出站连接池 ──
47
+ // ⛔ 08-20 生产真踩:本地盒子(authStyle 'none' = 环回 SSH 隧道)走 Node 默认
48
+ // globalAgent,而它 **keepAlive 默认开着**。llama-server 几秒就关掉空闲连接,
49
+ // 那个 FIN 要先穿过 SSH 隧道才到 Node —— 窗口里 Node 会从池子里挑一条**其实已经
50
+ // 死了的 socket** 发请求,结果就是 `ECONNRESET: socket hang up` 刷屏(一分钟几百条),
51
+ // 而盒子那头 `ss` 看到的连接数是 0(两边对同一条连接的死活认知不一致,是识别信号)。
52
+ // 隧道内建连接本来就便宜(没有 TLS 握手),所以本地上游一律不复用连接。
53
+ // 远端上游(中转站/HTTPS)保持 keep-alive:那边握手贵,且没有隧道拖慢 FIN。
54
+ const AGENTS = new Map();
55
+ function agentFor(wire, useHttps) {
56
+ if (wire.upstream.authStyle !== 'none') return undefined; // 远端:用默认 globalAgent
57
+ const key = useHttps ? 'https' : 'http';
58
+ if (!AGENTS.has(key)) AGENTS.set(key, new (useHttps ? https : http).Agent({ keepAlive: false }));
59
+ return AGENTS.get(key);
60
+ }
61
+
62
+ let _instance = null;
63
+
64
+ const PREFIX_RE = /^\/__nd\/([^/]+)(\/.*)$/;
65
+
66
+ // Vision 下采样阈值(长边像素)。1568 = Anthropic token 优化阈值,也是多数
67
+ // 中转网关安全线(mili-logo 2500×2500 曾触发网关 400)。
68
+ const VISION_MAX_DIM = (() => {
69
+ const n = Number(process.env.NODESIGN_VISION_MAX_DIM);
70
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1568;
71
+ })();
72
+
73
+ // count_tokens capability 运行时缓存:upstreamId → false(404 实测确认没有)。
74
+ // 表里 countTokens:false 的上游直接短路,true 的先转发、404 后降级并记住。
75
+ const countTokensDead = new Set();
76
+
77
+ // 会话级路由(注册 / 注销 / 决策)住 ingress/session-routes.js;这里转出口给 session-loop 与测试
78
+ export { registerIngressSession, unregisterIngressSession, resolveSessionWire } from './ingress/session-routes.js';
79
+
80
+ /**
81
+ * 启动入口(幂等单例;路由是 per-request 的,单例不再锁死目标)。
82
+ * @returns {Promise<{ baseUrl: string, close: () => Promise<void> }>}
83
+ */
84
+ export async function getOrStartIngress() {
85
+ if (_instance) return _instance;
86
+
87
+ const server = http.createServer((req, res) => {
88
+ const chunks = [];
89
+ req.on('data', (c) => chunks.push(c));
90
+ req.on('end', async () => {
91
+ try {
92
+ await handleRequest(req, res, Buffer.concat(chunks));
93
+ } catch (err) {
94
+ console.error(`[model-ingress] handler error: ${err?.stack || err?.message || err}`);
95
+ try { res.writeHead(502); res.end(`ingress handler error: ${err?.message || 'unknown'}`); } catch { /* ignore */ }
96
+ }
97
+ });
98
+ req.on('error', (err) => {
99
+ console.error(`[model-ingress] request error: ${err.message}`);
100
+ try { res.writeHead(400); res.end(); } catch { /* ignore */ }
101
+ });
102
+ });
103
+
104
+ await new Promise((resolve, reject) => {
105
+ const onError = (err) => { server.removeListener('listening', onListening); reject(err); };
106
+ const onListening = () => { server.removeListener('error', onError); resolve(); };
107
+ server.once('error', onError);
108
+ server.once('listening', onListening);
109
+ server.listen(0, '127.0.0.1');
110
+ });
111
+
112
+ const baseUrl = `http://127.0.0.1:${server.address().port}`;
113
+ console.log(`[model-ingress] listening on ${baseUrl} (upstreams: ${Object.keys(UPSTREAMS).join(', ')})`);
114
+ _instance = { baseUrl, close: () => new Promise((r) => server.close(() => r())), server };
115
+ return _instance;
116
+ }
117
+
118
+ export async function stopIngress() {
119
+ if (!_instance) return;
120
+ await _instance.close();
121
+ _instance = null;
122
+ }
123
+
124
+ async function handleRequest(req, res, bodyBuf) {
125
+ // 剥 /__nd/<sessionId> 前缀(日志归属用)
126
+ let sessionTag = null;
127
+ let origPath = req.url;
128
+ const m = PREFIX_RE.exec(req.url);
129
+ if (m) { sessionTag = decodeURIComponent(m[1]); origPath = m[2]; }
130
+ const sidShort = sessionTag ? sessionTag.slice(0, 8) : '-';
131
+
132
+ if (!(req.method === 'POST' && /^\/v1\/messages\b/.test(origPath))) {
133
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
134
+ res.end(`model-ingress: unsupported ${req.method} ${origPath}`);
135
+ return;
136
+ }
137
+
138
+ let parsed;
139
+ try {
140
+ parsed = JSON.parse(bodyBuf.toString('utf8'));
141
+ } catch {
142
+ // 没有 model 就没法路由 —— fail-loud 而不是盲转发
143
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
144
+ res.end('model-ingress: body is not JSON, cannot route');
145
+ return;
146
+ }
147
+
148
+ const routed = resolveSessionWire(parsed?.model, sessionTag);
149
+ const wire = routed.wire;
150
+ if (wire && routed.reason !== 'table') {
151
+ const logKey = `${sessionTag}:${parsed?.model}`;
152
+ if (!fallbackLogged.has(logKey)) {
153
+ fallbackLogged.add(logKey);
154
+ if (routed.reason === 'collision') {
155
+ console.warn(`[model-ingress] sid=${sidShort} ⛔ 撞名:model '${parsed?.model}' 属于别的行(${routed.collidesWith}),本会话是 ${routed.sessionModel} —— 不跨行转发,改道会话 fast(${routed.fastModel})`);
156
+ } else {
157
+ console.warn(`[model-ingress] sid=${sidShort} 未知 model '${parsed?.model}' → 会话 fast 兜底(${routed.fastModel})`);
158
+ }
159
+ }
160
+ }
161
+ if (!wire) {
162
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
163
+ res.end(`model-ingress: no route for model '${parsed?.model}' — 该名字不在 model-context.js 的 API 表里`);
164
+ console.warn(`[model-ingress] sid=${sidShort} 未知 model '${parsed?.model}',已拒绝(fail-loud)`);
165
+ return;
166
+ }
167
+
168
+ // authStyle 'none' = 无鉴权上游(本地 llama-server 走环回隧道),不需要钥匙
169
+ const needKey = wire.upstream.authStyle !== 'none';
170
+ // 钥匙取用点(全入口唯一一处):外部插槽(runtime/local-config.js)把钥匙写在条目里;内置行走 env
171
+ const key = needKey ? (wire.upstream.key || (wire.upstream.keyEnv ? process.env[wire.upstream.keyEnv] : null)) : null;
172
+ if (needKey && !key) {
173
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
174
+ res.end(`model-ingress: 上游钥匙未配置(${wire.upstream.keyEnv ? `env ${wire.upstream.keyEnv} 为空` : `上游 ${wire.upstreamId} 没填 key`})`);
175
+ return;
176
+ }
177
+
178
+ const isCountTokens = /^\/v1\/messages\/count_tokens\b/.test(origPath);
179
+ // CLI 在**回合之间**也用主模型打请求(猜你想问 / 标题生成),ingress 分不出角色(都是 main)。
180
+ // 它们的半截不该触发续接:那是给用户的建议词,不是 agent 的回答,接下去也没意义,
181
+ // 而且标记会串到下一个回合头上(fable 评审 P2;真路径探针里 SUGGESTION MODE 每回合都来一发)。
182
+ // 认的是 CLI 自己写死的提示词开头,认不出来最多是少一道保险(还有回合内新鲜度判据兜底)。
183
+ const isSuggestionQuery = (() => {
184
+ try {
185
+ const last = parsed?.messages?.[parsed.messages.length - 1];
186
+ const txt = typeof last?.content === 'string' ? last.content : JSON.stringify(last?.content || '');
187
+ return txt.includes('SUGGESTION MODE');
188
+ } catch { return false; }
189
+ })();
190
+
191
+ // 会话连续失败上限(upstream-fail-streak.js,僵尸 run 案):上游持续死时 CLI 对 5xx 无上限退避重试,
192
+ // 一个回合能挂一小时。到上限就回 400(CLI 不重试、回合以 is_error 收场、文案到用户、会话不死),
193
+ // 计数归零让用户下次再发有新机会。count_tokens 不参与(它不是循环的燃料)。
194
+ // 计数按 sid+角色分桶:helper(标题/分类器)一次成功不能把主行攒的计数清零(08-21 评审抓的洞)
195
+ const streakKey = sessionTag ? `${sessionTag}:${routed.role || 'main'}` : null;
196
+ if (!isCountTokens && failStreaks.exhausted(streakKey)) {
197
+ const { n, reason } = failStreaks.consume(streakKey);
198
+ const body = JSON.stringify(exhaustedErrorBody({ label: wire.upstream?.label || wire.upstreamId, n, reason }));
199
+ console.warn(`[model-ingress] sid=${sidShort} role=${routed.role} upstream=${wire.upstreamId} 连续失败 ${n} 次(${reason})→ 400 止损,计数归零`);
200
+ res.writeHead(400, { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(body)) });
201
+ res.end(body);
202
+ return;
203
+ }
204
+ const noteOutcome = (ok, reason) => {
205
+ if (isCountTokens) return;
206
+ const n = failStreaks.note(streakKey, ok, reason);
207
+ // 上游抖的时候会话里什么都不动(CLI 在退避重试,一次 503 能挂 50~140 秒)—— 推一句人话,
208
+ // 让用户知道是在重试而不是卡死,等不及可以自己点停止。节流住在 session-notice.js。
209
+ if (!ok && routed.role !== 'helper') {
210
+ const label = wire.upstream?.label || wire.upstreamId;
211
+ noticeSession(sessionTag, {
212
+ key: 'upstream_retry',
213
+ text: `${label} 上游繁忙(${reason || '未知'}),正在自动重试第 ${n} 次;等不及可以点停止。`,
214
+ priority: 'warn',
215
+ });
216
+ }
217
+ };
218
+
219
+ // count_tokens:上游没有该端点 → 本地估算短路(SDK 内部窗口计数要有数,
220
+ // 否则 ContextUsageBar 永远"等待"、80% 预警从不触发 —— DMXAPI 时代真踩过)
221
+ const upstreamHasCount = wire.upstream.countTokens !== false && !countTokensDead.has(wire.upstreamId);
222
+ if (isCountTokens && !upstreamHasCount) {
223
+ const respBody = JSON.stringify({ input_tokens: estimateInputTokens(parsed) });
224
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(respBody)) });
225
+ res.end(respBody);
226
+ return;
227
+ }
228
+
229
+ // 修补流水线(count_tokens 也要 model 还原,其余修补无害)
230
+ await transformForUpstream(parsed, wire);
231
+
232
+ // 协议分岔:上游说 OpenAI chat(Zen 等)→ 转换层;其余透传 Anthropic
233
+ if (wire.protocol === 'openai-chat') {
234
+ const target = new URL(wire.upstream.baseUrl);
235
+ // helper 请求降档:主行想多少归主行,helper 一句话的活用 helperReasoningEffort(默认 low)
236
+ const wireFwd = routed.role === 'helper' && wire.helperReasoningEffort ? { ...wire, reasoningEffort: wire.helperReasoningEffort } : wire;
237
+ forwardOpenAIChat({ parsed, wire: wireFwd, key, res, sidShort, target, path: joinPath(target.pathname, '/chat/completions'), agent: agentFor(wire, target.protocol === 'https:'), onOutcome: noteOutcome,
238
+ // 上游自报费用按会话 × appModel 累加(helper 请求记到 helper 行头上),session-loop 结账时取走
239
+ onBilling: (info) => noteUpstreamBilling(sessionTag, wireFwd.appModel, info),
240
+ // 「说到一半被掐」标记:session-loop 收到 result 时取走,有标记就自动续接一轮(upstream-truncation.js)。
241
+ // ⚠️ helper 一律不报(连"收得完整"也不报):它就一句话的活不值得续接,而报 null 会把主行刚记下的
242
+ // 标记清掉(标题/摘要那发常常紧跟在主回合后面)。
243
+ onTruncated: (reason) => {
244
+ if (routed.role === 'helper' || isCountTokens || isSuggestionQuery) return;
245
+ noteUpstreamTruncation(sessionTag, reason, { appModel: wireFwd.appModel });
246
+ },
247
+ // 就地重发时说一声:那期间会话里什么都不动,用户只看到一个转不停的绿点(跟"上游繁忙"同一条通道)
248
+ onNotice: (text) => {
249
+ if (routed.role === 'helper') return; // helper 的重发用户不需要知道
250
+ noticeSession(sessionTag, { key: 'upstream_retry', text, priority: 'warn' });
251
+ } });
252
+ return;
253
+ }
254
+ const outBody = Buffer.from(JSON.stringify(parsed), 'utf8');
255
+
256
+ // 换钥匙 + 头处理
257
+ const target = new URL(wire.upstream.baseUrl);
258
+ const useHttps = target.protocol === 'https:';
259
+ const headers = { ...req.headers, host: target.hostname };
260
+ delete headers['x-api-key'];
261
+ delete headers['authorization'];
262
+ if (wire.upstream.authStyle === 'bearer') headers['authorization'] = `Bearer ${key}`;
263
+ else if (needKey) headers['x-api-key'] = key;
264
+ headers['content-length'] = String(outBody.length);
265
+
266
+ const proxyReq = (useHttps ? https : http).request({
267
+ hostname: target.hostname,
268
+ port: target.port || (useHttps ? 443 : 80),
269
+ path: joinPath(target.pathname, origPath),
270
+ method: 'POST',
271
+ headers,
272
+ agent: agentFor(wire, useHttps),
273
+ }, (proxyRes) => {
274
+ // count_tokens 404 → 降级本地估算并记住这个上游没有该端点
275
+ if (isCountTokens && (proxyRes.statusCode === 404 || proxyRes.statusCode === 405)) {
276
+ countTokensDead.add(wire.upstreamId);
277
+ proxyRes.resume(); // 丢弃上游响应体
278
+ const respBody = JSON.stringify({ input_tokens: estimateInputTokens(parsed) });
279
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(respBody)) });
280
+ res.end(respBody);
281
+ console.warn(`[model-ingress] ${wire.upstreamId} 没有 count_tokens(404),已降级本地估算并缓存`);
282
+ return;
283
+ }
284
+ // 上游 4xx/5xx:console.warn 一行 status + model + body 前 200 字,PM2 日志直接看到根因
285
+ if (proxyRes.statusCode >= 400) {
286
+ const respChunks = [];
287
+ proxyRes.on('data', (c) => respChunks.push(c));
288
+ proxyRes.on('end', () => {
289
+ const preview = Buffer.concat(respChunks).slice(0, 200).toString('utf8').replace(/\s+/g, ' ');
290
+ console.warn(`[model-ingress] sid=${sidShort} upstream=${wire.upstreamId} ${proxyRes.statusCode} model=${wire.wireModel} body=${preview}`);
291
+ });
292
+ // 透传路:5xx 记一次失败(4xx 是请求本身的问题,CLI 不重试,不记);2xx 算成功
293
+ if (proxyRes.statusCode >= 500) noteOutcome(false, `HTTP ${proxyRes.statusCode}`);
294
+ } else {
295
+ noteOutcome(true);
296
+ }
297
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
298
+ proxyRes.pipe(res);
299
+ });
300
+
301
+ proxyReq.on('error', (err) => {
302
+ const detail = err.code ? `${err.code}: ${err.message}` : err.message;
303
+ console.error(`[model-ingress] forward error (${wire.upstreamId}): ${detail}`);
304
+ noteOutcome(false, `forward: ${detail}`);
305
+ try { res.writeHead(502); res.end(`ingress forward error: ${detail}`); } catch { /* ignore */ }
306
+ });
307
+
308
+ proxyReq.write(outBody);
309
+ proxyReq.end();
310
+ }
311
+
312
+ /**
313
+ * 按路由行修补请求 body(原地改)。导出供单测直接喂假 body 验证。
314
+ *
315
+ * @param {object} parsed Anthropic Messages body
316
+ * @param {object} wire resolveWireModel 的返回
317
+ * @returns {Promise<boolean>} 是否有改动
318
+ */
319
+ export async function transformForUpstream(parsed, wire) {
320
+ let mutated = false;
321
+
322
+ // model 还原成上游真名(SDK 发的是 spoof alias 或 appModel)
323
+ if (parsed.model !== wire.wireModel) {
324
+ parsed.model = wire.wireModel;
325
+ mutated = true;
326
+ }
327
+
328
+ // thinking:'strip' = 删字段让上游自决(Gemini thinking 关不掉、参数过桥行为
329
+ // 未知);'enabled8k' = adaptive 改写成 enabled+budget(Kimi 实测 adaptive =
330
+ // 0 thinking blocks)
331
+ if (wire.thinking === 'strip') {
332
+ if ('thinking' in parsed) { delete parsed.thinking; mutated = true; }
333
+ } else if (wire.thinking === 'enabled8k') {
334
+ if (parsed.thinking?.type === 'adaptive') {
335
+ parsed.thinking = { type: 'enabled', budget_tokens: 8192 };
336
+ mutated = true;
337
+ }
338
+ }
339
+
340
+ // tool_result 里的图提升到 user message 顶层(丢图桥的必修 shim)
341
+ if (wire.liftImages && Array.isArray(parsed.messages)) {
342
+ if (liftImagesFromToolResult(parsed.messages)) mutated = true;
343
+ }
344
+
345
+ // 图片归一:长边超限下采样(所有上游统一做,安全无害)+ 把上游解不开的格式
346
+ // 转码过去(只有声明了 imageFormats 的上游才做,见 model-context 那张表)
347
+ if (Array.isArray(parsed.messages)) {
348
+ const allowed = wire.upstream?.imageFormats || null;
349
+ if (await normalizeImages(parsed.messages, VISION_MAX_DIM, allowed)) mutated = true;
350
+ }
351
+
352
+ return mutated;
353
+ }
354
+
355
+ /**
356
+ * 把 tool_result.content 里的 image block 提到外层 user message content 顶层;
357
+ * 原位置替换为占位文本。Kimi S8 时代写的,08-19 探针证明 Gemini 桥同样需要。
358
+ */
359
+ export function liftImagesFromToolResult(messages) {
360
+ let mutated = false;
361
+ for (const msg of messages) {
362
+ if (msg?.role !== 'user' || !Array.isArray(msg.content)) continue;
363
+ const liftedImages = [];
364
+ for (const block of msg.content) {
365
+ if (block?.type !== 'tool_result' || !Array.isArray(block.content)) continue;
366
+ block.content = block.content.map((inner) => {
367
+ if (inner?.type === 'image' && inner.source?.data) {
368
+ liftedImages.push({ ...inner });
369
+ mutated = true;
370
+ return {
371
+ type: 'text',
372
+ text: '[image lifted to end of this message for upstream vision compat — see image block below]',
373
+ };
374
+ }
375
+ return inner;
376
+ });
377
+ }
378
+ if (liftedImages.length > 0) msg.content.push(...liftedImages);
379
+ }
380
+ return mutated;
381
+ }
382
+
383
+ /**
384
+ * 图片归一:扫所有 image block(user msg 顶层 + tool_result 内嵌),按需下采样
385
+ * 和/或转码。两件事共用一次遍历也共用一条 sharp 管线 —— 拆成两个函数就是把这段
386
+ * 嵌套遍历抄第二份,而它已经是"顶层 + tool_result 内嵌"两层的形状了。
387
+ *
388
+ * fail-soft:单张图 sharp 抛错 → warn 后保留原图透传,不阻断整 turn。
389
+ *
390
+ * @param {number} maxDim 长边上限,超了才 resize
391
+ * @param {string[]|null} allowed 上游解得开的 media_type 白名单;null = 不限
392
+ */
393
+ async function normalizeImages(messages, maxDim, allowed) {
394
+ let mutated = false;
395
+ for (const msg of messages) {
396
+ if (!Array.isArray(msg?.content)) continue;
397
+ for (let i = 0; i < msg.content.length; i++) {
398
+ const block = msg.content[i];
399
+ if (block?.type === 'image') {
400
+ const replaced = await maybeNormalizeImageBlock(block, maxDim, allowed);
401
+ if (replaced) { msg.content[i] = replaced; mutated = true; }
402
+ }
403
+ if (block?.type === 'tool_result' && Array.isArray(block.content)) {
404
+ for (let j = 0; j < block.content.length; j++) {
405
+ const inner = block.content[j];
406
+ if (inner?.type === 'image') {
407
+ const replaced = await maybeNormalizeImageBlock(inner, maxDim, allowed);
408
+ if (replaced) { block.content[j] = replaced; mutated = true; }
409
+ }
410
+ }
411
+ }
412
+ }
413
+ }
414
+ return mutated;
415
+ }
416
+
417
+ const IMAGE_WRITERS = {
418
+ 'image/png': (p) => p.png({ compressionLevel: 9 }),
419
+ 'image/jpeg': (p) => p.jpeg({ quality: 90, mozjpeg: true }),
420
+ 'image/webp': (p) => p.webp({ quality: 85 }),
421
+ };
422
+
423
+ /**
424
+ * 单图归一。两个触发条件,满足任一才动(都不满足返 null 原样透传):
425
+ * - 长边超过 maxDim → 下采样
426
+ * - media_type 不在上游的 imageFormats 里 → 转码
427
+ *
428
+ * 转码目标按有没有 alpha 分:有 alpha 走 png(保住透明),没有走 jpeg(照片和
429
+ * 渲染图转 png 会胖好几倍,本地盒子这条链路还要过 SSH 隧道)。**只在 allowed
430
+ * 里挑**,上游声明什么就往什么转。
431
+ *
432
+ * gif 不在 IMAGE_WRITERS 的键里,所以动图永远不进这条路(重 encode 会丢帧)——
433
+ * 代价是 gif 遇到解不开它的上游仍旧失败,那个另说,别在这儿悄悄压成一张静图。
434
+ */
435
+ async function maybeNormalizeImageBlock(block, maxDim, allowed) {
436
+ const src = block?.source;
437
+ if (!src || src.type !== 'base64' || !src.data) return null;
438
+ if (!IMAGE_WRITERS[src.media_type]) return null;
439
+
440
+ const needsTranscode = Array.isArray(allowed) && !allowed.includes(src.media_type);
441
+
442
+ try {
443
+ const inputBuf = Buffer.from(src.data, 'base64');
444
+ const meta = await sharp(inputBuf).metadata();
445
+ const w = meta.width || 0;
446
+ const h = meta.height || 0;
447
+ const needsResize = Math.max(w, h) > maxDim;
448
+ if (!needsResize && !needsTranscode) return null;
449
+
450
+ // 转码时目标格式从 allowed 里挑;不转码就还用原格式
451
+ let outType = src.media_type;
452
+ if (needsTranscode) {
453
+ const prefer = meta.hasAlpha ? ['image/png', 'image/jpeg'] : ['image/jpeg', 'image/png'];
454
+ outType = prefer.find((t) => allowed.includes(t) && IMAGE_WRITERS[t])
455
+ || allowed.find((t) => IMAGE_WRITERS[t]);
456
+ if (!outType) {
457
+ console.warn(`[model-ingress] 上游 imageFormats 里没有能写的格式,原样透传 ${src.media_type}`);
458
+ return null;
459
+ }
460
+ }
461
+
462
+ let pipeline = sharp(inputBuf);
463
+ if (needsResize) {
464
+ pipeline = pipeline.resize({
465
+ width: w >= h ? maxDim : null,
466
+ height: h > w ? maxDim : null,
467
+ fit: 'inside',
468
+ withoutEnlargement: true,
469
+ });
470
+ }
471
+ // 两种情况去 alpha:① 落到 jpeg(它没有 alpha 通道,不 flatten 出来是黑底);
472
+ // ② 走了 resize —— 这条是原有行为,理由是"部分 vision 网关对 RGBA 不友好",
473
+ // 别因为加转码顺手把它改掉(中转站那两个上游一直吃的是 flatten 过的图)。
474
+ if (meta.hasAlpha && (needsResize || outType === 'image/jpeg')) {
475
+ pipeline = pipeline.flatten({ background: '#ffffff' });
476
+ }
477
+ const outBuf = await IMAGE_WRITERS[outType](pipeline).toBuffer();
478
+ return {
479
+ ...block,
480
+ source: { type: 'base64', media_type: outType, data: outBuf.toString('base64') },
481
+ };
482
+ } catch (err) {
483
+ console.warn(`[model-ingress] image normalize failed (passthrough): ${err?.message || err}`);
484
+ return null;
485
+ }
486
+ }
487
+
488
+ /**
489
+ * CJK 字符范围(token 估算分流):平/片假名 + CJK 基础汉字 + 韩文音节。
490
+ * 系数:CJK 1 字 ≈ 1.3 token,英文 BPE ~4 char/token。
491
+ */
492
+ const CJK_REGEX = /[぀-鿿가-힯]/g;
493
+
494
+ function estimateText(s) {
495
+ if (typeof s !== 'string') return 0;
496
+ const cjk = s.match(CJK_REGEX)?.length || 0;
497
+ return cjk * 1.3 + (s.length - cjk) / 4;
498
+ }
499
+
500
+ /**
501
+ * 粗估 messages body 的 input token 数(count_tokens 本地降级用)。
502
+ * 实测误差:纯英文 ±10%,纯中文 ±15%,混合 ±20% —— 够 UI 进度条与预警分档。
503
+ */
504
+ export function estimateInputTokens(parsed) {
505
+ try {
506
+ let total = 0;
507
+ const addText = (s) => { total += estimateText(s); };
508
+ const addBlock = (b) => {
509
+ if (!b) return;
510
+ if (b.type === 'text' && b.text) addText(b.text);
511
+ else if (b.type === 'image' && b.source?.data) total += b.source.data.length / 4;
512
+ else if (b.type === 'tool_use') { addText(b.name || ''); addText(JSON.stringify(b.input || {})); }
513
+ else if (b.type === 'tool_result') {
514
+ if (typeof b.content === 'string') addText(b.content);
515
+ else if (Array.isArray(b.content)) b.content.forEach(addBlock);
516
+ } else if (b.type === 'thinking' && b.thinking) addText(b.thinking);
517
+ };
518
+ if (Array.isArray(parsed.messages)) {
519
+ for (const msg of parsed.messages) {
520
+ if (typeof msg.content === 'string') addText(msg.content);
521
+ else if (Array.isArray(msg.content)) msg.content.forEach(addBlock);
522
+ }
523
+ }
524
+ if (typeof parsed.system === 'string') addText(parsed.system);
525
+ else if (Array.isArray(parsed.system)) parsed.system.forEach(addBlock);
526
+ if (Array.isArray(parsed.tools)) {
527
+ for (const t of parsed.tools) {
528
+ addText(t.name || '');
529
+ addText(t.description || '');
530
+ addText(JSON.stringify(t.input_schema || {}));
531
+ }
532
+ }
533
+ return Math.max(1, Math.round(total));
534
+ } catch {
535
+ return 50000; // 保守值 ≈ 1/5 上下文,误触 80% 警告概率低
536
+ }
537
+ }
538
+
539
+ /** URL path 拼接(避免重复 / 或丢段)。moonshot base 自带 /anthropic 段靠它保住 */
540
+ function joinPath(base, reqPath) {
541
+ const cleanBase = (base || '').replace(/\/$/, '');
542
+ return cleanBase + (reqPath.startsWith('/') ? reqPath : '/' + reqPath);
543
+ }