@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,669 @@
1
+ /**
2
+ * server/api/turn.js — 唯一 LLM 入口(agentic 设计的核心)
3
+ *
4
+ * POST /api/projects/:pid/turn
5
+ * body: {
6
+ * chat: string, // 用户文本
7
+ * attachments: [{ path, ... }], // 附件托盘里的内容(asset / anchor / comment)
8
+ * skillId?: string, // 默认走 project.skillId
9
+ * }
10
+ * return 202 { runId, sessionId }
11
+ *
12
+ * 行为(streamInput 重构后):
13
+ * 1. 校验 project + 解析 input
14
+ * 2. composeUserMessage:拼成 SDK content blocks(多模态 / system 提示注入)
15
+ * 3. createRun(pending) — per-turn record,前端按 runId 跟踪
16
+ * 4. 立即返回 runId + sessionId(agent 异步在后端跑)
17
+ * 5. 看 hasActiveQuerySession(sid):
18
+ * - 已有 → pushUserMessage 进 inputQueue,runSession 拉走处理(追加 / 续 chat)
19
+ * - 没有 → startNewRunSession 起新 long-running query handle,预 push 首条 message
20
+ * 6. setActiveSession:写 project.activeSessionId 让下次不带 sid 的 turn fallback
21
+ *
22
+ * 续 turn 不依赖 jsonl resume —— streamInput 模式 query 横跨整个 session,
23
+ * conversation state 在 SDK binary 内存里。
24
+ *
25
+ * 错误:
26
+ * - runSession throw → 已通过 EventBus 推 run.error;console 留痕
27
+ * - HTTP 已经 202 返回,不再回 5xx
28
+ */
29
+
30
+ import express from 'express';
31
+ import { promises as fs } from 'fs';
32
+ import path from 'path';
33
+ import { randomUUID } from 'crypto';
34
+ import { validateProjectId, getProject, setActiveSession } from '../projects/store.js';
35
+ import { guardProject, guardRunInProject, modelUserFor } from './_guard.js';
36
+ import {
37
+ ensureProjectWorkspace,
38
+ ensureSessionWorkspace,
39
+ validateSessionId,
40
+ getSessionMetaDir,
41
+ } from '../projects/workspace.js';
42
+ import { createRun } from '../engine/runs/store.js';
43
+ import { runSession } from '../engine/agent/session-loop.js';
44
+ import {
45
+ cancelRun, provideAnswer, getQuery, provideElicitation, hasActiveQuerySession, getQuerySession, closeQuerySession, setSessionPermissionMode, getSessionIdByRunId,
46
+ } from '../engine/runs/active-runs.js';
47
+ import { pushUserMessage, getQueueDepth } from '../engine/runs/turn-relay.js';
48
+ import { applySessionModel, resolveSessionModel } from '../engine/agent/session-model.js';
49
+ import { lruGet, lruPut, inflightTurns, INFLIGHT_RETENTION_MS } from './turn-inflight.js';
50
+ import { allowedModelsFor, isModelLockedFor, defaultModelFor, modelIsFree, resolveSdkSpoofModel, hasSubscriptionAccess, crossLaneSwitchReason } from '../engine/agent/model-context.js';
51
+ import { AsyncQueue } from '../lib/async-queue.js';
52
+ import { checkQuota, checkFreeQuota, checkConcurrency, fmtUsd } from '../lib/quota.js';
53
+ import { shouldModerate, moderateText, recordViolation, levelFor } from '../lib/moderation.js';
54
+ import { getProjectBus } from '../ws/broker.js';
55
+ import { Events } from '../engine/agent/events.js';
56
+ import { readPendingSummary } from './pending-changes.js';
57
+ import { pendingRewinds } from './sessions.js';
58
+ import { platform } from '../runtime/platform.js';
59
+ import { composeUserMessage } from './turn-compose.js';
60
+
61
+ const router = express.Router();
62
+
63
+ /**
64
+ * Emit run.permission_mode_changed —— 广播 SDK 实际 permissionMode 的变化。唯一的
65
+ * 切换路径(turn 入口 mode 校正)调完 setPermissionMode 后 emit 一次。
66
+ *
67
+ * 前端不镜像这个事件(plan mode 2026-08-21 整体移除后它只剩观测价值)。事件保留给
68
+ * 多 tab 观测和排障用 —— mode 是 SDK 真相的一部分,不该只活在服务端日志里。
69
+ *
70
+ * @param {string} pid
71
+ * @param {string} sid
72
+ * @param {string} mode - 'default' | 'acceptEdits' | 'bypassPermissions' | 'dontAsk' | 'auto'
73
+ */
74
+ export function emitPermissionModeChanged(pid, sid, mode) {
75
+ if (!pid || !sid || !mode) return;
76
+ try {
77
+ getProjectBus(pid).publish({
78
+ type: 'run.permission_mode_changed',
79
+ sessionId: sid,
80
+ mode,
81
+ ts: new Date().toISOString(),
82
+ });
83
+ } catch (err) {
84
+ console.warn(`[turn] emitPermissionModeChanged failed: ${err.message}`);
85
+ }
86
+ }
87
+
88
+ router.post('/:pid/turn', async (req, res, next) => {
89
+ // ⚠️ 必须在 try **外面**:catch 是 try 的**兄弟**作用域。写在里面的话 catch 那句
90
+ // `typeof inflightReject === 'function'` 够不到它,而 typeof 对未声明的名字不抛错
91
+ // 只返 'undefined' —— 那条 race 修复就此一声不响地失效(08-17 被 no-undef.lint 扫出)。
92
+ let inflightResolve = null;
93
+ let inflightReject = null;
94
+ try {
95
+ const project = guardProject(req, res);
96
+ if (!project) return;
97
+
98
+ const { chat, attachments, skillId, sessionId, permissionMode, requestId, raw } = req.body || {};
99
+ // 只发附件不打字也是一条完整消息(2026-08-17,issue #1 第 8 条):拖张参考图
100
+ // 进来就该能发,逼用户补一句"看看这个"是白要的动作。
101
+ // 空文字 **且** 空附件才是空消息 —— 那个仍然拦。
102
+ const chatText = typeof chat === 'string' ? chat : '';
103
+ const hasAttachments = Array.isArray(attachments) && attachments.length > 0;
104
+ if (!chatText.trim() && !hasAttachments) {
105
+ return res.status(400).json({ error: 'chat string required' });
106
+ }
107
+
108
+ // Phase A.6:requestId 命中 LRU → 直接返已存在的 run/session(弱网重发幂等)
109
+ // race 修复:① LRU 命中(已完成请求)→ 立即返;② in-flight 命中(正在跑)→
110
+ // await 第一 POST 的 result 后返 deduped;③ 都 miss → 注册 in-flight Promise
111
+ // 后继续走 createRun 路径,结尾 resolve / reject 通知后续等待者。
112
+ if (typeof requestId === 'string' && requestId) {
113
+ const cached = lruGet(requestId);
114
+ if (cached && cached.pid === project.id) {
115
+ return res.status(202).json({ runId: cached.runId, sessionId: cached.sessionId, deduped: true });
116
+ }
117
+ const inflight = inflightTurns.get(requestId);
118
+ if (inflight) {
119
+ try {
120
+ const r = await inflight;
121
+ if (r && r.pid === project.id) {
122
+ return res.status(202).json({ runId: r.runId, sessionId: r.sessionId, deduped: true });
123
+ }
124
+ } catch { /* first POST failed → fall through 让本 POST 重新跑 */ }
125
+ }
126
+ // 注册 in-flight Promise,后到的同 requestId POST 会 await 这个
127
+ const p = new Promise((rs, rj) => { inflightResolve = rs; inflightReject = rj; });
128
+ inflightTurns.set(requestId, p);
129
+ // 防 promise unhandled rejection 警告:失败时也 attach catch
130
+ p.catch(() => {});
131
+ }
132
+ // permissionMode 请求字段保留兼容老前端,但不再参与决定(plan mode 08-21 整体移除):
133
+ // 启动 mode 一律 platform 默认(生产 bypassPermissions / exp auto)。
134
+ const initialPermissionMode = platform.permissionModeDefault;
135
+
136
+ const finalSkillId = (typeof skillId === 'string' && skillId) || project.skillId;
137
+
138
+ // C4:先确定 sessionRoot,给 composeUserMessage 看 pending-changes buffer
139
+ // (sid 解析逻辑下面已写)—— 提早 ensure 一次让 buffer 检查能命中真路径。
140
+
141
+ // session id 解析逻辑(streamInput 模式):
142
+ // - body.sessionId === string → 用该 sid(已有活 query 就 push,没有就起新 runSession)
143
+ // - body.sessionId === null → 新建 session(前端"+新会话"显式触发)
144
+ // - body.sessionId 不传 → fallback project.activeSessionId(向后兼容)
145
+ //
146
+ // 新建场景用 randomUUID 预生成 sid,传给 SDK options.sessionId 让 SDK 用
147
+ // 我们的 sid(d.ts:1537 sessionId 单独可传)。cwd 提前切到 sessions/<sid>/,
148
+ // agent 一启动就在 session 沙盒里跑。
149
+ //
150
+ // 变量名仍叫 resumeSessionId 是历史遗留——streamInput 模式不真"resume jsonl",
151
+ // 它只是"当前要用的 sid"。未 rename 是因为有大量 callsite 兼容成本。
152
+ let resumeSessionId;
153
+ if ('sessionId' in (req.body || {})) {
154
+ resumeSessionId = sessionId || null;
155
+ } else {
156
+ resumeSessionId = project.activeSessionId;
157
+ }
158
+ const isNewSession = !resumeSessionId;
159
+ const sid = isNewSession ? randomUUID() : resumeSessionId;
160
+ validateSessionId(sid);
161
+
162
+ // 守卫:临时 rewind query 在跑时拒绝同 sid 新 turn —— 防止两个 SDK subprocess
163
+ // 同时写同一 jsonl。临时 query ~3-5s,用户重试一次就 OK。
164
+ if (pendingRewinds.has(sid)) {
165
+ return res.status(409).json({ error: 'rewind in progress, retry shortly', code: 'REWIND_BUSY' });
166
+ }
167
+
168
+ // ── 内测闸门(2026-07-30):必须在 202 之前同步判 ──
169
+ // 日配额:所有 turn 都扣(排队的稍后也烧钱)。口径是金额不是 token,
170
+ // 原因见 lib/quota.js 文件头 —— 简言之 token 数对缓存命中与否几乎无差别,
171
+ // 金额能差十倍,拿 token 当闸门等于没量到主项。
172
+ //
173
+ // 07-31 起只剩这一道:分模型限额撤了,因为金额天然让 opus 烧得更快,
174
+ // 不需要第二个数字表达同一个意图。
175
+ // 模型解析提前到配额之前(08-21,配额按是否免费分岔):body.model > 会话覆盖 > 默认(defaultModelFor)
176
+ const requestedModelEarly = typeof req.body?.model === 'string' && req.body.model.trim() ? req.body.model.trim() : null;
177
+ const sessionModelEarly = await resolveSessionModel(getSessionMetaDir(project.id, sid));
178
+ const modelUser = modelUserFor(req, project); // 资格按项目 owner 算(_guard.js):admin 代看 basic 项目时老会话不许落回订阅行,否则 202 后 INIT_FAILED
179
+ // 老用户没覆盖的老会话保持全局默认,不静默切 Ox;新会话/公开号 → defaultModelFor
180
+ const keepLegacyDefault = !isNewSession && !sessionModelEarly.override && hasSubscriptionAccess(modelUser);
181
+ const turnModel = requestedModelEarly || sessionModelEarly.override
182
+ || (keepLegacyDefault ? sessionModelEarly.model : defaultModelFor(modelUser)) || sessionModelEarly.model;
183
+ // 解析出来的模型一律过白名单(不只 body.model):旧覆盖/资格收回/无 select 裸名都在此拦(fable P0)
184
+ if (isModelLockedFor(modelUser, turnModel)) {
185
+ return res.status(403).json({
186
+ error: `这个模型(${turnModel})仅限 Pro 档,暂未对外开放。换成免费模型继续`,
187
+ code: 'MODEL_LOCKED', model: turnModel,
188
+ });
189
+ }
190
+ if (!allowedModelsFor(modelUser).some((m) => m.id === turnModel)) { // 清单整个为空(本地版没 key/没登录/没插槽)→ 指路设置页,别让人去选择器里找
191
+ return res.status(403).json(allowedModelsFor(modelUser).length ? { error: `这个会话指向的模型(${turnModel})现在不可用,请在模型选择器里换一个`, code: 'MODEL_NOT_ALLOWED', model: turnModel } : { error: '还没有可用的模型:到「设置」填 API Key(或本机 claude login),或者配一个模型插槽', code: 'NO_MODEL_CONFIGURED', model: turnModel });
192
+ }
193
+ if (requestedModelEarly && sessionModelEarly.override && crossLaneSwitchReason(sessionModelEarly.override, requestedModelEarly)) {
194
+ return res.status(409).json({ error: crossLaneSwitchReason(sessionModelEarly.override, requestedModelEarly), code: 'LANE_SWITCH' });
195
+ }
196
+ if (modelIsFree(turnModel)) {
197
+ const fq = checkFreeQuota(req.user);
198
+ if (!fq.ok) {
199
+ return res.status(429).json({ error: `今天的免费轮次用完了(${fq.used} / ${fq.limit}),明天零点刷新`, code: 'QUOTA_EXCEEDED', kind: fq.kind, used: fq.used, limit: fq.limit });
200
+ }
201
+ }
202
+ const quota = modelIsFree(turnModel) ? { ok: true } : checkQuota(req.user);
203
+ if (!quota.ok) {
204
+ return res.status(429).json({
205
+ // 试用号(终身口径)没有"明天刷新"可许诺,文案不能骗人
206
+ error: quota.kind === 'lifetime'
207
+ ? `试用额度已用完(${fmtUsd(quota.used)} / ${fmtUsd(quota.limit)}),感谢体验!想继续用可以联系站主`
208
+ : `今日额度已用完(${fmtUsd(quota.usedToday)} / ${fmtUsd(quota.limit)}),明天零点刷新`,
209
+ code: 'QUOTA_EXCEEDED',
210
+ kind: quota.kind,
211
+ used: quota.used,
212
+ usedToday: quota.usedToday,
213
+ limit: quota.limit,
214
+ });
215
+ }
216
+ // 并发:只拦"会立刻开跑"的 turn;session 正忙时这条消息进排队(既有串行语义,
217
+ // 不产生新并发)。免费行不受全局固定数限制,只受内存闸(quota.js)
218
+ if (!getQuerySession(sid)?.currentRunId) {
219
+ const gate = checkConcurrency(req.user, { free: modelIsFree(turnModel) });
220
+ if (!gate.ok) {
221
+ return res.status(429).json({ error: gate.message, code: gate.code });
222
+ }
223
+ }
224
+
225
+ // ── 内容外审(2026-08-02):消息先过分类器再进 agent。拦下 = 零成本,run 都不建。
226
+ // 强度按账号(users.moderation_level,站主在控制台调),判定 / 留证 / 连坐封禁的
227
+ // 口径全在 lib/moderation.js。
228
+ // 没有文字就没有可审的东西(附件本来就不审,见 lib/moderation.js)——
229
+ // 拿空串去问分类器只是白花一次调用和 1 秒。
230
+ //
231
+ // 08-20 起外审档按模型通路取旋钮(订阅 / 本地与中转各一个),所以这里要先知道
232
+ // 这条消息会落在哪个模型上:新会话带 body.model 就是它(白名单校验在下面那段,
233
+ // 这里只是读;非法名最终会 400),否则读该会话的 session-config(新会话没文件
234
+ // → 全局默认)。只读不写 —— 模型持久化仍在外审之后,拦下的消息不该改会话配置。
235
+ if (shouldModerate(req.user, turnModel) && chatText.trim()) {
236
+ const verdict = await moderateText(chatText, levelFor(req.user, turnModel));
237
+ if (!verdict.ok) {
238
+ const rec = recordViolation({
239
+ userId: req.user.id, projectId: project.id,
240
+ category: verdict.category, severity: verdict.severity,
241
+ reason: verdict.reason, excerpt: chatText, level: verdict.level,
242
+ });
243
+ // 上面两道闸的 429 是同步返回,弱网重发撞 in-flight 的窗口可以忽略;
244
+ // 这里 await 了 ~1s,窗口是真的 —— reject 让正在 await 的同 requestId
245
+ // POST fallthrough 自己重跑(然后再被拦一次),不能让它挂死。
246
+ if (typeof requestId === 'string' && requestId) {
247
+ try { if (typeof inflightReject === 'function') inflightReject(new Error('moderation blocked')); } catch { /* */ }
248
+ inflightTurns.delete(requestId);
249
+ }
250
+ return res.status(451).json({
251
+ error: rec?.disabled
252
+ ? '消息涉及违规内容,账号已停用。如有疑问请联系站主'
253
+ : '这条消息涉及违规内容,没有发给 agent。请调整后重发',
254
+ code: 'MODERATION_BLOCKED',
255
+ });
256
+ }
257
+ }
258
+
259
+ // 取 sessionRoot + workspace 主动提示:
260
+ // - pendingSummary(C4):用户在 chat 间隔做的直接编辑/评论 buffer
261
+ // (素材摘要 08-21 起由 UserPromptSubmit hook 注入,首轮全量之后只报变化)
262
+ await ensureProjectWorkspace(project.id);
263
+ const sessionRoot = await ensureSessionWorkspace(project.id, sid);
264
+
265
+ // 模型选择(可选 body.model):只用于**新建会话**时把首选模型带进来
266
+ // (首页 / Hub 那条路,会话还不存在,前端只有 localStorage 偏好)。
267
+ //
268
+ // 会话建起来之后模型的真相在 session-config.json,改它走 PUT /sessions/:sid/model。
269
+ // 这里之所以不再无脑接受 body.model:前端每条消息都带偏好的话,在另一台机器上
270
+ // 为这个会话选的模型会被本机的旧偏好悄悄改回去 —— 一次发消息顺带改配置,
271
+ // 用户完全看不见。
272
+ // 没带 body.model 且会话无覆盖 → 默认模型写进会话(否则 session-loop 吃 NODESIGN_MODEL,对公开号是锁着的订阅行)
273
+ const requestedModel = requestedModelEarly || ((!sessionModelEarly.override && !keepLegacyDefault) ? turnModel : null);
274
+ if (requestedModel) {
275
+ // 与 PUT /sessions/:sid/model 同一道闸(2026-08-19 评审抓的洞):这条路
276
+ // 以前不校验,等于绕过 picker 白名单的后门 —— model-ingress 上线后表里
277
+ // 有带真钥匙的 API 模型(gemini),裸 POST 就能替会话选中它烧上游的钱。
278
+ // 校验用 allowedModelsFor(不含 locked)—— locked 的在上面已经 403 过了。
279
+ if (!allowedModelsFor(modelUserFor(req, project)).some((m) => m.id === requestedModel)) {
280
+ return res.status(400).json({ error: `unknown model: ${requestedModel}`, code: 'UNKNOWN_MODEL' });
281
+ }
282
+ await applySessionModel(sid, getSessionMetaDir(project.id, sid), requestedModel, 'turn');
283
+ }
284
+
285
+ const pendingSummary = isNewSession ? { count: 0, summary: '' } : await readPendingSummary(sessionRoot);
286
+ // raw:纯文本直达 SDK,不加任何装饰块 —— 斜杠命令(/compact 等)要求消息
287
+ // 就是命令本身,多包一层 system 注入就不会被识别
288
+ const { displayText, blocks } = raw === true && chatText.trim()
289
+ ? { displayText: chatText.trim(), blocks: [{ type: 'text', text: chatText.trim() }] }
290
+ : await composeUserMessage(chatText, attachments, pendingSummary, sessionRoot);
291
+
292
+ // 上传/附件诊断:NODESIGN_DEBUG_TURN=1 时打印 blocks 概况,定位 image 体积/媒体类型
293
+ // 引发的 400/超 token 类问题(配合 binary-fixup-proxy 的 /tmp dump)
294
+ if (process.env.NODESIGN_DEBUG_TURN === '1') {
295
+ const summary = blocks.map((b) => {
296
+ if (b.type === 'image') {
297
+ const dataLen = b.source?.data?.length || 0;
298
+ return `image(${b.source?.media_type},${(dataLen / 1024).toFixed(1)}KB-base64)`;
299
+ }
300
+ return `${b.type}(${(b.text || '').length}c)`;
301
+ });
302
+ console.info(`[turn.compose] sid=${sid.slice(0, 8)} blocks=[${summary.join(', ')}]`);
303
+ }
304
+
305
+ // 创建 run(pending)— per-turn record,displayText 落 brief 字段做审计
306
+ const run = createRun({
307
+ skillId: finalSkillId, brief: displayText, projectId: project.id,
308
+ userId: req.user?.id ?? null, sessionId: sid,
309
+ });
310
+
311
+ // Phase A.6:写 LRU 让后续重试同 requestId 拿到一致 (runId, sid)
312
+ // 同时 resolve in-flight Promise 通知正在 await 的并发 POST,5s 后清 in-flight
313
+ // entry(让 LRU 接管后续 dedup 查询)。
314
+ if (typeof requestId === 'string' && requestId) {
315
+ lruPut(requestId, { pid: project.id, runId: run.id, sessionId: sid });
316
+ if (inflightResolve) {
317
+ inflightResolve({ pid: project.id, runId: run.id, sessionId: sid });
318
+ }
319
+ setTimeout(() => inflightTurns.delete(requestId), INFLIGHT_RETENTION_MS);
320
+ }
321
+
322
+ // 立即返回,agent 后台跑
323
+ res.status(202).json({ runId: run.id, sessionId: sid });
324
+ const bus = getProjectBus(project.id);
325
+
326
+ const sdkUserMessage = {
327
+ type: 'user',
328
+ message: { role: 'user', content: blocks },
329
+ parent_tool_use_id: null,
330
+ };
331
+
332
+ if (hasActiveQuerySession(sid)) {
333
+ // streamInput 模式:session 已有 long-running query 在跑 →
334
+ // push 这条 message 进 queue,由 runSession 的 for-await-of 拉走处理。
335
+ // 适用:① 续 chat(agent 已结束上一轮 idle 等)② 用户在 agent 跑时追加消息
336
+ //
337
+ // permissionMode 校正:请求带的 mode 和 SDK 当前 mode 不一致时(例如上一轮
338
+ // agent 自己进了 plan mode,用户直接又发了一条普通消息),pushUserMessage 路径
339
+ // 下 SDK 会按旧 mode 处理新 chat → canUseTool 拦 Write/Edit。这里在 push 前对齐。
340
+ // setPermissionMode 是 SDK 原生 API,可在 turn 边界外调;fail-soft 不阻塞。
341
+ const querySession = getQuerySession(sid);
342
+ const currentMode = querySession?.currentPermissionMode;
343
+ const desiredMode = initialPermissionMode;
344
+ if (currentMode && desiredMode && currentMode !== desiredMode && querySession?.query?.setPermissionMode) {
345
+ try {
346
+ await querySession.query.setPermissionMode(desiredMode);
347
+ setSessionPermissionMode(sid, desiredMode);
348
+ emitPermissionModeChanged(project.id, sid, desiredMode);
349
+ } catch (err) {
350
+ console.warn(`[turn] mode sync failed sid=${sid.slice(0, 8)} (${currentMode}→${desiredMode}): ${err.message}`);
351
+ }
352
+ }
353
+ const ok = pushUserMessage(sid, run.id, sdkUserMessage);
354
+ if (!ok) {
355
+ // race:刚 close 的 session(理论上极少)—— fallback 起新
356
+ console.warn(`[turn] pushUserMessage failed for ${sid.slice(0, 8)}, falling back to new session`);
357
+ startNewRunSession({ runId: run.id, sid, sessionRoot, blocks: sdkUserMessage, eventBus: bus, project, finalSkillId, chat, initialPermissionMode });
358
+ } else {
359
+ // push 后 emit 当前 queue 积压深度,前端显示"已排队 N 条"
360
+ // depth=0 表示 agent idle 立刻处理;depth>0 表示 agent 还在忙,要排队
361
+ const depth = getQueueDepth(sid);
362
+ bus.publish({ type: 'run.queue.depth', sessionId: sid, depth, ts: new Date().toISOString() });
363
+ }
364
+ } else {
365
+ // 没活跃 session → 起新 runSession(首条 message 提前 push 进 queue)
366
+ startNewRunSession({ runId: run.id, sid, sessionRoot, blocks: sdkUserMessage, eventBus: bus, project, finalSkillId, chat, initialPermissionMode });
367
+ }
368
+
369
+ // 写回 active_session_id(让下次不带 sessionId 的 turn fallback 续到这个)。
370
+ // setActiveSession **无条件**调:它顺带 bump projects.updated_at,首页
371
+ // 「我的项目」按这个排序 —— 只在指针变化时才写会让"同会话继续聊"不再
372
+ // 把项目顶到最前。
373
+ //
374
+ // E1a(2026-08-13):指针**实际变化**时广播 project.active_session ——
375
+ // 会话真相源收敛到服务端指针后,同项目其他标签页靠这条事件对齐自己。
376
+ // project 是本请求开头 guardProject 读的库快照,拿它比对够准(写这个
377
+ // 字段的只有 turn 和删会话两条路,都走 HTTP 串行到达)。
378
+ try {
379
+ const pointerChanged = project.activeSessionId !== sid;
380
+ setActiveSession(project.id, sid);
381
+ if (pointerChanged) {
382
+ bus.publish({ ...Events.projectActiveSession(sid), ts: new Date().toISOString() });
383
+ }
384
+ } catch { /* ignore */ }
385
+ } catch (err) {
386
+ // 处理失败时通知正在 await in-flight 的并发同 requestId POST:reject + 清 entry
387
+ // 让它们 fallthrough 自己跑(subagent 提的 race 修复完整闭环)。
388
+ try { if (typeof inflightReject === 'function') inflightReject(err); } catch { /* */ }
389
+ const rid = req.body?.requestId;
390
+ if (typeof rid === 'string' && rid) inflightTurns.delete(rid);
391
+ next(err);
392
+ }
393
+ });
394
+
395
+ /**
396
+ * 起一个新的 runSession(streamInput long-running query),并预 push 首条 user
397
+ * message 让 SDK 启动后立即处理。fire-and-forget — 不阻塞 HTTP response。
398
+ */
399
+ function startNewRunSession({ runId, sid, sessionRoot, blocks, eventBus, project, finalSkillId, chat, initialPermissionMode }) {
400
+ const inputQueue = new AsyncQueue();
401
+ inputQueue.push(blocks); // 直接 push 进 queue —— runSession 启动后用 initialRunId 关联
402
+
403
+ runSession({
404
+ sessionId: sid,
405
+ projectId: project.id,
406
+ ownerId: project.ownerId, // 订阅通路的资格断言在 session-loop 做 OAuth 决策那一行(auth/tier.js)
407
+ sessionWorkspaceRoot: sessionRoot,
408
+ eventBus,
409
+ inputQueue,
410
+ skillId: finalSkillId,
411
+ // 不再传 sessionTitle —— SDK doc:"Custom session title... skips automatic
412
+ // title generation"。让 SDK 用 ANTHROPIC_SMALL_FAST_MODEL(haiku)自动
413
+ // 总结对话生成标题,前端 run.done 后 refetch sessions 拉新 summary。
414
+ // 用户主动 rename(未来 ✏️ 按钮)走 SDK renameSession() 单独路径。
415
+ initialRunId: runId,
416
+ initialPermissionMode,
417
+ })
418
+ .then(() => {
419
+ console.info(`[turn] runSession ${sid.slice(0, 8)} ended cleanly`);
420
+ })
421
+ .catch((err) => {
422
+ // session 抛错:query 可能挂了,前端通过 run.error event 看到
423
+ console.error(`[turn] runSession ${sid.slice(0, 8)} failed:`, err.message);
424
+ });
425
+ }
426
+
427
+ /**
428
+ * POST /api/projects/:pid/runs/:runId/cancel
429
+ *
430
+ * 用户点"停止生成"按钮 → 触发活跃 run 的 AbortController.abort()。
431
+ * SDK 看到 abort signal → query 中断 → session-loop try/catch 走 aborted 路径
432
+ * → emit run.cancelled 事件给前端。
433
+ *
434
+ * 200 { ok: true } 成功 trigger abort
435
+ * 404 { error: 'run not active' } runId 不在 registry(已结束 / 不存在)
436
+ */
437
+ router.post('/:pid/runs/:runId/cancel', async (req, res, next) => {
438
+ try {
439
+ const project = guardProject(req, res);
440
+ if (!project) return;
441
+ if (!guardRunInProject(req, res)) return;
442
+
443
+ const { runId } = req.params;
444
+ const ok = cancelRun(runId, 'user_cancel');
445
+ if (!ok) {
446
+ return res.status(404).json({
447
+ error: 'run not active or already finished',
448
+ code: 'RUN_NOT_ACTIVE',
449
+ });
450
+ }
451
+ res.json({ ok: true });
452
+ } catch (err) { next(err); }
453
+ });
454
+
455
+ /**
456
+ * A4.2:POST /api/projects/:pid/runs/:runId/answer
457
+ *
458
+ * 用户在 AskUserQuestionView 卡片点选项 → 前端 POST 来这个 endpoint →
459
+ * provideAnswer resolve 对应 toolUseId 的 Promise → session-loop.js canUseTool
460
+ * 返回 { behavior: 'allow', updatedInput: { ...input, answers } } → SDK
461
+ * binary 调 tool.call → 模型看到 "User has answered: q1=A"。
462
+ *
463
+ * Body:
464
+ * {
465
+ * toolUseId: string, // run.ask_user_question 事件带的
466
+ * answers: { [questionText]: optionLabel } // multi-select 用 ", " 拼
467
+ * }
468
+ *
469
+ * 200 { ok: true } 成功 resolve
470
+ * 404 { error, code: 'NO_PENDING_QUESTION' } run/toolUseId 不在 pending
471
+ * (已答 / 已 cancel / 已结束)
472
+ * 400 { error } body 缺字段
473
+ */
474
+ router.post('/:pid/runs/:runId/answer', async (req, res, next) => {
475
+ try {
476
+ const project = guardProject(req, res);
477
+ if (!project) return;
478
+ if (!guardRunInProject(req, res)) return;
479
+
480
+ const { runId } = req.params;
481
+ const { toolUseId, answers } = req.body || {};
482
+ if (!toolUseId || typeof toolUseId !== 'string') {
483
+ return res.status(400).json({ error: 'toolUseId required (string)' });
484
+ }
485
+ if (!answers || typeof answers !== 'object') {
486
+ return res.status(400).json({ error: 'answers required (object: { [questionText]: label })' });
487
+ }
488
+
489
+ const ok = provideAnswer(runId, toolUseId, answers);
490
+ if (!ok) {
491
+ return res.status(404).json({
492
+ error: 'no pending question for this run/toolUseId',
493
+ code: 'NO_PENDING_QUESTION',
494
+ });
495
+ }
496
+ res.json({ ok: true });
497
+ } catch (err) { next(err); }
498
+ });
499
+
500
+ /**
501
+ * Phase B 批次 4:POST /api/projects/:pid/runs/:runId/elicit/:reqId/answer
502
+ *
503
+ * MCP 工具调 server.elicitInput() 时 SDK 触发 onElicitation 回调,回调 emit
504
+ * run.elicitation_request 事件让前端弹 Modal。用户填完后 POST 这个 endpoint
505
+ * → provideElicitation resolve session-loop.js 里 await 的 Promise
506
+ * → SDK 拿到 { action, content } 继续工具调用。
507
+ *
508
+ * Body:
509
+ * {
510
+ * action: 'accept' | 'decline' | 'cancel',
511
+ * content?: { [field]: any } // accept 时用户填的表单字段
512
+ * }
513
+ *
514
+ * 200 { ok: true }
515
+ * 404 { error, code: 'NO_PENDING_ELICITATION' }
516
+ * 400 { error }
517
+ */
518
+ router.post('/:pid/runs/:runId/elicit/:reqId/answer', async (req, res, next) => {
519
+ try {
520
+ const project = guardProject(req, res);
521
+ if (!project) return;
522
+ if (!guardRunInProject(req, res)) return;
523
+
524
+ const { runId, reqId } = req.params;
525
+ const { action, content } = req.body || {};
526
+ if (!action || !['accept', 'decline', 'cancel'].includes(action)) {
527
+ return res.status(400).json({ error: 'action required (accept|decline|cancel)' });
528
+ }
529
+
530
+ const ok = provideElicitation(runId, reqId, { action, content });
531
+ if (!ok) {
532
+ return res.status(404).json({
533
+ error: 'no pending elicitation for this run/reqId',
534
+ code: 'NO_PENDING_ELICITATION',
535
+ });
536
+ }
537
+ res.json({ ok: true });
538
+ } catch (err) { next(err); }
539
+ });
540
+
541
+ // ─────────────────────────────────────────────────────────────────────
542
+ // SDK Query control method endpoints(sdk.d.ts:2017 Query interface)
543
+ // 这些方法只在 streaming input/output 模式下可用 — session-loop.js 唯一入口
544
+ // 已让所有 run 走 AsyncIterable<SDKUserMessage>(buildUserMessageStream)满足前提。
545
+ // ─────────────────────────────────────────────────────────────────────
546
+
547
+ /**
548
+ * POST /api/projects/:pid/runs/:runId/rewind
549
+ *
550
+ * 配合 sdkOptions.enableFileCheckpointing —— 把 cwd 文件回滚到指定 user
551
+ * message 时点 + session JSONL 截断到该 message。前端 user message 旁的
552
+ * undo 按钮调这个 endpoint。
553
+ *
554
+ * Body: { messageId: string } - SDKAssistantMessage.uuid 或 user message uuid
555
+ *
556
+ * 注意:rewindFiles 不主动 git revert(git 不在 SDK 管辖)→ 用户用 undo 后
557
+ * 产物文件落后 git history 一步;前端可以 hint 或 host 端补 commit。
558
+ */
559
+ router.post('/:pid/runs/:runId/rewind', async (req, res, next) => {
560
+ try {
561
+ const project = guardProject(req, res);
562
+ if (!project) return;
563
+ if (!guardRunInProject(req, res)) return;
564
+
565
+ const { runId } = req.params;
566
+ const { messageId } = req.body || {};
567
+ if (!messageId || typeof messageId !== 'string') {
568
+ return res.status(400).json({ error: 'messageId required (string)' });
569
+ }
570
+
571
+ const query = getQuery(runId);
572
+ if (!query) {
573
+ return res.status(404).json({
574
+ error: 'run not active or query handle not yet attached',
575
+ code: 'RUN_NOT_ACTIVE',
576
+ });
577
+ }
578
+ if (typeof query.rewindFiles !== 'function') {
579
+ return res.status(501).json({
580
+ error: 'SDK query handle missing rewindFiles method (older SDK?)',
581
+ code: 'METHOD_NOT_AVAILABLE',
582
+ });
583
+ }
584
+
585
+ const result = await query.rewindFiles(messageId);
586
+ res.json({ ok: true, result });
587
+ } catch (err) { next(err); }
588
+ });
589
+
590
+ /**
591
+ * POST /api/projects/:pid/runs/:runId/model
592
+ *
593
+ * 运行中切 model(SDK query.setModel,当场对下一次 LLM 调用生效)。
594
+ *
595
+ * **目前只有 API 能到这里**:前端 picker 在 turn 运行中是禁用的,它走
596
+ * PUT /sessions/:sid/model(那条等空闲才重启 query)。这条留着是因为"turn 跑到
597
+ * 一半换模型"是它独有的能力,PUT 那条按设计做不到。前端那个没人调的 Turn.setModel
598
+ * 绑定已删(doc 里还写着 kimi 时代的 model 名,留着只会误导下一个人)。
599
+ *
600
+ * 2026-07-30:切完**必须同时落 session-config**。原来这条只改运行时不写文件,
601
+ * 于是"当前这轮是 Opus、下次 resume 变回 Sonnet",而且界面无从得知;跟另外两条
602
+ * 写模型的路加起来,同一个事实有三个互不知情的写者。现在统一走 applySessionModel,
603
+ * 它自己会判断要不要重启空闲 query(这里 query 正在跑,不会重启)。
604
+ *
605
+ * Body: { model: string | null } - null = 清掉覆盖回到全局默认
606
+ */
607
+ router.post('/:pid/runs/:runId/model', async (req, res, next) => {
608
+ try {
609
+ const project = guardProject(req, res);
610
+ if (!project) return;
611
+ if (!guardRunInProject(req, res)) return;
612
+
613
+ const { runId } = req.params;
614
+ const { model } = req.body || {};
615
+ if (model !== null && model !== undefined && typeof model !== 'string') {
616
+ return res.status(400).json({ error: 'model must be string or null' });
617
+ }
618
+
619
+ // 08-21:热切路以前不过白名单(任意字符串直达 SDK)。与 PUT /sessions/:sid/model 同口径
620
+ if (typeof model === 'string' && model.trim()) {
621
+ const modelUser = modelUserFor(req, project); // 资格按项目 owner 算(_guard.js)
622
+ if (isModelLockedFor(modelUser, model.trim())) {
623
+ return res.status(403).json({ error: '这个模型仅限 Pro 档,暂未对外开放', code: 'MODEL_LOCKED', model: model.trim() });
624
+ }
625
+ if (!allowedModelsFor(modelUser).some((m) => m.id === model.trim())) {
626
+ return res.status(400).json({ error: `unknown model: ${model}`, code: 'UNKNOWN_MODEL' });
627
+ }
628
+ const sidForLane = getSessionIdByRunId(runId);
629
+ if (sidForLane) {
630
+ const cur = await resolveSessionModel(getSessionMetaDir(project.id, sidForLane));
631
+ const why = crossLaneSwitchReason(cur.model, model.trim());
632
+ if (why) return res.status(409).json({ error: why, code: 'LANE_SWITCH' });
633
+ }
634
+ }
635
+
636
+ const query = getQuery(runId);
637
+ if (!query) {
638
+ return res.status(404).json({
639
+ error: 'run not active',
640
+ code: 'RUN_NOT_ACTIVE',
641
+ });
642
+ }
643
+ if (typeof query.setModel !== 'function') {
644
+ return res.status(501).json({
645
+ error: 'SDK query handle missing setModel method',
646
+ code: 'METHOD_NOT_AVAILABLE',
647
+ });
648
+ }
649
+
650
+ const sid = getSessionIdByRunId(runId);
651
+ // API 行喂 SDK 的是 spoof 名(session-loop 同款),appModel 本身 binary 不认识
652
+ await query.setModel(model ? resolveSdkSpoofModel(model.trim()) : undefined);
653
+ // 运行时切完再落盘:setModel 失败就不该留下"配置说切了"的假象
654
+ let persisted = null;
655
+ if (sid) {
656
+ await ensureSessionWorkspace(project.id, sid);
657
+ persisted = await applySessionModel(sid, getSessionMetaDir(project.id, sid), model ?? null, 'runtime');
658
+ }
659
+ res.json({ ok: true, model: persisted?.model ?? model, override: persisted?.override ?? null });
660
+ } catch (err) { next(err); }
661
+ });
662
+
663
+
664
+ // 注:POST /image-approval 路由已删除(2026-05-06)。原本配 ImageApprovalBanner
665
+ // 走 approve/regenerate/dismiss 三按钮 gate,但实际上 emit 完即返不阻塞 agent,
666
+ // 形同弹窗装饰。改为:generate_image 已在 CallToolResult 返 image content block,
667
+ // 前端自动渲染;agent 在 caption / 自然回话邀请反馈,下一轮用户 chat 即天然 gate。
668
+
669
+ export default router;