@xiaobuyu/nodesign 0.0.1 → 0.0.3

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 (358) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +42 -22
  9. package/server/api/assets/notes.js +194 -144
  10. package/server/api/assets.js +891 -896
  11. package/server/api/board.js +134 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +465 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1035 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +351 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +152 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/edit-sketch.js +167 -0
  120. package/server/engine/mcp/tools/explain-style.js +237 -237
  121. package/server/engine/mcp/tools/export-handoff.js +133 -133
  122. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  123. package/server/engine/mcp/tools/generate-image.js +842 -842
  124. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  125. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  126. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  127. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  128. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  129. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  130. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  131. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  132. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  133. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  134. package/server/engine/mcp/tools/highlight.js +68 -68
  135. package/server/engine/mcp/tools/list-pages.js +222 -222
  136. package/server/engine/mcp/tools/look-at-board.js +124 -0
  137. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  138. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  139. package/server/engine/mcp/tools/organize-board.js +64 -64
  140. package/server/engine/mcp/tools/paint-still.js +434 -434
  141. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  142. package/server/engine/mcp/tools/preview-deck.js +130 -130
  143. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  144. package/server/engine/mcp/tools/publish-site.js +102 -102
  145. package/server/engine/mcp/tools/query-elements.js +195 -195
  146. package/server/engine/mcp/tools/read-board.js +190 -103
  147. package/server/engine/mcp/tools/read-document.js +93 -93
  148. package/server/engine/mcp/tools/read-page.js +264 -264
  149. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  150. package/server/engine/mcp/tools/read-user-view.js +59 -0
  151. package/server/engine/mcp/tools/record-decision.js +140 -140
  152. package/server/engine/mcp/tools/relate-on-board.js +112 -110
  153. package/server/engine/mcp/tools/report-issue.js +115 -115
  154. package/server/engine/mcp/tools/roll-film.js +266 -266
  155. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  156. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  157. package/server/engine/mcp/tools/screenshot.js +583 -583
  158. package/server/engine/mcp/tools/sketch-on-board.js +337 -0
  159. package/server/engine/mcp/tools/tier-gate.js +96 -96
  160. package/server/engine/mcp/tools/trace-motion.js +215 -215
  161. package/server/engine/mcp/tools/web-search.js +548 -548
  162. package/server/engine/mcp/tools/write-on-board.js +130 -0
  163. package/server/engine/motion/inventory.js +349 -349
  164. package/server/engine/perception/session.js +235 -235
  165. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  174. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
  175. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  176. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  177. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
  178. 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 -78
  179. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  180. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
  181. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  182. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  183. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  185. package/server/engine/runs/active-runs.js +698 -698
  186. package/server/engine/runs/board-tasklist.js +130 -0
  187. package/server/engine/runs/live-turn.js +252 -252
  188. package/server/engine/runs/store.js +364 -364
  189. package/server/engine/runs/turn-relay.js +217 -217
  190. package/server/engine/runtime/workspace.js +164 -164
  191. package/server/index.js +3 -0
  192. package/server/lib/artifact-file-path.js +80 -80
  193. package/server/lib/artifact-target.js +284 -284
  194. package/server/lib/asset-refs.js +239 -239
  195. package/server/lib/async-queue.js +104 -104
  196. package/server/lib/auto-relations.js +130 -130
  197. package/server/lib/binding-types.js +72 -60
  198. package/server/lib/board-graph-export.js +191 -0
  199. package/server/lib/board-groups.js +167 -0
  200. package/server/lib/board-hero.js +76 -0
  201. package/server/lib/board-kind-sizes.js +85 -62
  202. package/server/lib/board-relations.js +133 -133
  203. package/server/lib/browse-proxy.js +180 -180
  204. package/server/lib/canvas-id.js +36 -36
  205. package/server/lib/chalk.js +121 -0
  206. package/server/lib/cover.js +227 -227
  207. package/server/lib/danbooru-tags.js +291 -291
  208. package/server/lib/doc-extract.js +156 -156
  209. package/server/lib/docx/build-from-source.js +260 -260
  210. package/server/lib/docx/build.js +490 -490
  211. package/server/lib/docx/dump-styles.js +287 -287
  212. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  213. package/server/lib/docx/merge-runs.js +129 -129
  214. package/server/lib/docx/numbering.js +218 -218
  215. package/server/lib/docx/order.js +152 -152
  216. package/server/lib/docx/rawzip.js +172 -172
  217. package/server/lib/docx/render.js +92 -92
  218. package/server/lib/docx/text-lint.js +225 -225
  219. package/server/lib/docx/tokens.js +335 -335
  220. package/server/lib/docx/units.js +29 -29
  221. package/server/lib/docx/xml.js +291 -291
  222. package/server/lib/docx-pages.js +187 -187
  223. package/server/lib/export-collect.js +289 -289
  224. package/server/lib/export-package.js +210 -210
  225. package/server/lib/html-srcset.js +145 -145
  226. package/server/lib/image-variant.js +460 -460
  227. package/server/lib/ingress/forward-openai-chat.js +305 -305
  228. package/server/lib/ingress/openai-chat.js +491 -491
  229. package/server/lib/ingress/session-notice.js +66 -66
  230. package/server/lib/ingress/session-routes.js +68 -68
  231. package/server/lib/ingress/slot-probe.js +130 -130
  232. package/server/lib/ingress/upstream-billing.js +52 -52
  233. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  234. package/server/lib/ingress/upstream-truncation.js +48 -48
  235. package/server/lib/issues-store.js +182 -182
  236. package/server/lib/kinds/deck.js +72 -72
  237. package/server/lib/kinds/docx.js +283 -283
  238. package/server/lib/kinds/file-kinds.js +113 -113
  239. package/server/lib/kinds/index.js +202 -202
  240. package/server/lib/kinds/site.js +161 -161
  241. package/server/lib/model-ingress.js +543 -543
  242. package/server/lib/moderation.js +255 -255
  243. package/server/lib/notice-store.js +103 -103
  244. package/server/lib/plugin-install.js +104 -104
  245. package/server/lib/plugin-validator.js +721 -721
  246. package/server/lib/publish-store.js +115 -115
  247. package/server/lib/quick-summary.js +52 -52
  248. package/server/lib/quota.js +270 -270
  249. package/server/lib/rate-window.js +29 -29
  250. package/server/lib/reference-assets.js +92 -92
  251. package/server/lib/region-shot.js +135 -135
  252. package/server/lib/safe-path.js +73 -73
  253. package/server/lib/sdk-session.js +33 -33
  254. package/server/lib/showcase-store.js +92 -92
  255. package/server/lib/site-publish.js +465 -465
  256. package/server/lib/sketch-layout.js +277 -0
  257. package/server/lib/ssrf-guard.js +432 -432
  258. package/server/lib/task-scan.js +158 -158
  259. package/server/lib/tavern-json.js +154 -154
  260. package/server/lib/video-variant.js +237 -237
  261. package/server/lib/workspace-path.js +33 -33
  262. package/server/ops/fix-sdk-musl.mjs +40 -40
  263. package/server/ops/install-macos-fonts.sh +114 -114
  264. package/server/ops/macos-fonts.conf +336 -336
  265. package/server/ops/sandbox-shim/bwrap +57 -57
  266. package/server/projects/assets-summary.js +119 -119
  267. package/server/projects/auto-name.js +53 -53
  268. package/server/projects/board-limits.js +12 -0
  269. package/server/projects/board-sanitize.js +252 -0
  270. package/server/projects/board-store.js +507 -640
  271. package/server/projects/move-entry.js +103 -103
  272. package/server/projects/store.js +270 -270
  273. package/server/projects/ui-config.js +60 -54
  274. package/server/projects/viewpoint-store.js +78 -0
  275. package/server/projects/workspace-templates.js +53 -53
  276. package/server/projects/workspace.js +1025 -1025
  277. package/server/runtime/local-config.js +177 -177
  278. package/server/runtime/profile.js +72 -72
  279. package/server/services/rembg-service.py +334 -334
  280. package/server/shared/README.md +25 -25
  281. package/server/shared/deck.js +54 -54
  282. package/server/shared/time.js +49 -49
  283. package/server/style-pipeline/README.md +53 -53
  284. package/server/ws/broker.js +39 -36
  285. package/server/ws/browse-channel.js +177 -177
  286. package/server/ws/index.js +386 -386
  287. package/web/README.md +89 -89
  288. package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
  289. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
  290. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
  291. package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
  292. package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
  293. package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
  294. package/web/dist/assets/arc-D_khH_xN.js +1 -0
  295. package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
  296. package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
  297. package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
  298. package/web/dist/assets/channel-DGkccMO1.js +1 -0
  299. package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
  300. package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
  301. package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
  302. package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
  303. package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
  304. package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
  305. package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
  306. package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
  307. package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
  308. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
  309. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
  310. package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
  311. package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
  312. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
  313. package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
  314. package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
  315. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  316. package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
  317. package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
  318. package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
  319. package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
  320. package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
  321. package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
  322. package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
  323. package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
  324. package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
  325. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
  326. package/web/dist/assets/index-GAamh1p4.js +2152 -0
  327. package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
  328. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  329. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
  330. package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
  331. package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
  332. package/web/dist/assets/layout-CnXiF_NB.js +1 -0
  333. package/web/dist/assets/linear-BMW0u6G6.js +1 -0
  334. package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
  335. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  336. package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
  337. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
  338. package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
  339. package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
  340. package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
  341. package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
  342. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
  343. package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
  344. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
  345. package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
  346. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
  347. package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
  348. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
  349. package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
  350. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
  351. package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
  352. package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
  353. package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
  354. package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
  355. package/web/dist/index.html +17 -17
  356. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
  357. package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
  358. package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
@@ -1,324 +1,324 @@
1
- /**
2
- * server/api/chatai.js —— 演出端点:页面 → 服务端 → 中转站。
3
- *
4
- * agent 写的演出前端(iframe 里的站点页)打这两条路由;chatai 的 key 在服务端
5
- * env,页面永远拿不到。页面靠同源 cookie 认证,不发任何新凭证。
6
- *
7
- * ⭐ 演出路**不过内容审查**(2026-08-15 拍板):chatai 无工具、无会话、改不了
8
- * 任何文件,出格也只是文本;审查只存在于 agent 侧。但不过审 ≠ 不设闸,四道闸
9
- * 全钉在这一个口上:
10
- *
11
- * 1. 通路批准制 —— auth/tier.js localGenApproved:档位有资格(admin/pro)且被站主逐人批过(同 roll_film 那套);basic 档不开。
12
- * 中转站计量单位不明,问清之前不对全量内测用户开。
13
- * 2. 金额闸门 —— checkQuota 同一个 USD 池:每轮花费按我们的单价表记进
14
- * runs + run_model_usage(skill_id='chatai'),跟 agent 会话共享日限/终身额度。
15
- * 3. 频率闸门 —— 滑动窗口每用户每分钟 N 轮(env NODESIGN_CHATAI_TURNS_PER_MIN,
16
- * 默认 10)。金额日限拦不住单日尖峰,这道闸才是 setInterval 烧钱机的对手。
17
- * 4. 单演出串行 —— 同一文件夹同时只跑一轮(保护 对话.jsonl 成对追加与摘要
18
- * 折叠),撞上返 409 而不是排队。
19
- *
20
- * 流式默认 SSE(data: {delta} … data: {done, usage, costUsd, 摘要});
21
- * body.stream=false 走整段 JSON。中转站分片本来就粗,别指望逐字。
22
- */
23
-
24
- import path from 'node:path';
25
- import fs from 'node:fs/promises';
26
- import express from 'express';
27
- import { guardProject } from './_guard.js';
28
- import { getWorkspaceRoot } from '../projects/workspace.js';
29
- import { performTurn } from '../engine/chatai/perform.js';
30
- import {
31
- loadOrchestration, normalizeOrchestration, serializeOrchestration,
32
- resolveInside, estTokens, CONFIG_FILE,
33
- } from '../engine/chatai/orchestrate.js';
34
- import { readLog, readSummary, SUMMARY_FILE } from '../engine/chatai/chat-log.js';
35
- import { modelCatalog } from '../engine/chatai/index.js';
36
- import { checkQuota, fmtUsd } from '../lib/quota.js';
37
- import { can, localGenApproved } from '../auth/tier.js';
38
- import { makeRateWindow } from '../lib/rate-window.js';
39
- import {
40
- createRun, markRunStarted, markRunSucceeded, markRunFailed,
41
- setRunMetrics, setRunModelUsage,
42
- } from '../engine/runs/store.js';
43
-
44
- const router = express.Router();
45
- export default router;
46
-
47
- const MAX_INPUT_CHARS = 8000;
48
-
49
- const rate = makeRateWindow({
50
- limit: Number(process.env.NODESIGN_CHATAI_TURNS_PER_MIN) || 10,
51
- windowMs: 60_000,
52
- });
53
-
54
- /** 正在演出的文件夹(abs path → true)。串行是保护 jsonl,不是并发额度。 */
55
- const playing = new Set();
56
-
57
- function gateApproved(req, res) {
58
- // 档位 + 逐人批准(auth/tier.js localGenApproved):basic 档不开,pro 档要被站主批过
59
- if (localGenApproved(req.user)) return true;
60
- res.status(403).json({ error: '演出通路尚未对这个账号开放' });
61
- return false;
62
- }
63
-
64
- /** 解析演出文件夹:相对工作区根,防逃逸,必须真是目录。失败时已发响应。 */
65
- async function resolvePerformanceDir(req, res) {
66
- const root = path.resolve(getWorkspaceRoot(req.params.pid));
67
- const rel = String(req.method === 'GET' ? req.query.dir ?? '' : req.body?.dir ?? '');
68
- const abs = path.resolve(root, rel);
69
- if (abs !== root && !abs.startsWith(root + path.sep)) {
70
- res.status(400).json({ error: 'dir 跑出了项目工作区' });
71
- return null;
72
- }
73
- try {
74
- if (!(await fs.stat(abs)).isDirectory()) throw new Error();
75
- } catch {
76
- res.status(404).json({ error: '演出文件夹不存在' });
77
- return null;
78
- }
79
- return { abs, rel };
80
- }
81
-
82
- function statusOfOrchError(err) {
83
- if (err?.code === 'ORCH_NO_CONFIG') return 404;
84
- if (err?.code === 'ORCH_INVALID') return 422;
85
- if (err?.code === 'CHATAI_UNCONFIGURED') return 503;
86
- if (err?.code === 'CHATAI_UPSTREAM') return 502;
87
- return 500;
88
- }
89
-
90
- /** 把一轮(正文 + 可能的摘要调用)的用量并成 run_model_usage 行 */
91
- function usageRows(out) {
92
- const rows = new Map();
93
- const add = (model, usage, costUsd) => {
94
- if (!usage) return;
95
- const key = model || 'unknown';
96
- const acc = rows.get(key) || {
97
- inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, costUsd: 0,
98
- };
99
- // 列口径对齐 agent 行:input 不含缓存读(openai 格式的 prompt_tokens 含,要拆);
100
- // 思考 token 按输出计(Gemini 口径,costOf 同款)
101
- acc.inputTokens += Math.max(0, (usage.inputTokens || 0) - (usage.cacheReadTokens || 0));
102
- acc.outputTokens += (usage.outputTokens || 0) + (usage.reasoningTokens || 0);
103
- acc.cacheReadTokens += usage.cacheReadTokens || 0;
104
- acc.costUsd += costUsd || 0;
105
- rows.set(key, acc);
106
- };
107
- add(out.model, out.usage, out.costUsd);
108
- if (out.摘要?.用量) add(out.摘要.模型, out.摘要.用量, out.摘要.花费);
109
- return rows;
110
- }
111
-
112
- function meterRun(runId, out) {
113
- const rows = usageRows(out);
114
- setRunModelUsage(runId, Object.fromEntries(rows));
115
- const total = [...rows.values()].reduce((a, r) => ({
116
- inputTokens: a.inputTokens + r.inputTokens,
117
- outputTokens: a.outputTokens + r.outputTokens,
118
- cacheReadTokens: a.cacheReadTokens + r.cacheReadTokens,
119
- totalCostUsd: a.totalCostUsd + r.costUsd,
120
- }), { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, totalCostUsd: 0 });
121
- setRunMetrics(runId, total);
122
- return total.totalCostUsd;
123
- }
124
-
125
- /**
126
- * GET /:pid/chatai/log?dir=… —— 页面打开时渲染既有对话。
127
- * 读不花钱不限频;只要项目归属过关就给。
128
- */
129
- router.get('/:pid/chatai/log', async (req, res, next) => {
130
- try {
131
- if (!guardProject(req, res)) return;
132
- const dir = await resolvePerformanceDir(req, res);
133
- if (!dir) return;
134
- let config;
135
- try {
136
- config = await loadOrchestration(dir.abs);
137
- } catch (err) {
138
- return res.status(statusOfOrchError(err)).json({ error: err.message });
139
- }
140
- const [records, summary] = await Promise.all([
141
- readLog(dir.abs, config), readSummary(dir.abs),
142
- ]);
143
- res.json({
144
- records,
145
- 摘要: summary ? { 至: summary.至, 内容: summary.内容 } : null,
146
- 配置: { 模型: config.模型, 最大输出: config.最大输出, 上下文预算: config.上下文预算 },
147
- });
148
- } catch (err) { next(err); }
149
- });
150
-
151
- /**
152
- * GET /:pid/chatai/config?dir=… —— 设置页的一次取齐:归一后的配置 +
153
- * 条目引用的文件内容 + 历史/摘要状况。token 估算前端自己算(同款下限口径)。
154
- */
155
- router.get('/:pid/chatai/config', async (req, res, next) => {
156
- try {
157
- if (!guardProject(req, res)) return;
158
- const dir = await resolvePerformanceDir(req, res);
159
- if (!dir) return;
160
- let config;
161
- try {
162
- config = await loadOrchestration(dir.abs);
163
- } catch (err) {
164
- return res.status(statusOfOrchError(err)).json({ error: err.message, code: err.code || null });
165
- }
166
- const 文件 = {};
167
- for (const e of [...config.系统层, ...config.尾部]) {
168
- if (e.文件 == null || e.文件 in 文件) continue;
169
- try {
170
- 文件[e.文件] = await fs.readFile(resolveInside(dir.abs, e.文件, `条目「${e.名字}」`), 'utf8');
171
- } catch { 文件[e.文件] = null; } // 引用悬空:设置页要红给用户看,不是 500
172
- }
173
- const [records, summary] = await Promise.all([readLog(dir.abs, config), readSummary(dir.abs)]);
174
- const boundary = summary?.至 ?? 0;
175
- res.json({
176
- 配置: config,
177
- 文件,
178
- // 模型/单价/思考档由服务端一张表说了算(MODEL_SPECS),前端只负责画——
179
- // 中转站改名或换档位时不用两头改(单一真相源)。
180
- 模型表: modelCatalog(),
181
- 状况: {
182
- 记录条数: records.length,
183
- 轮数: records.filter(r => r.role === 'user').length,
184
- 活历史tok: records.filter(r => r.seq > boundary).reduce((n, r) => n + estTokens(r.text), 0),
185
- 摘要: summary ? { 至: summary.至, 内容: summary.内容 } : null,
186
- },
187
- });
188
- } catch (err) { next(err); }
189
- });
190
-
191
- /**
192
- * PUT /:pid/chatai/config body { dir, 配置, 文件? }
193
- *
194
- * 校验(normalizeOrchestration,跟演出路同一份)→ 先写引用文件、再原子写
195
- * 编排.yaml。改动下一轮生效——正在跑的那轮已经编译完了,不追。
196
- * 文件表只许写文件夹内部,且不许碰 编排.yaml / 摘要.json / 对话记录本体。
197
- */
198
- router.put('/:pid/chatai/config', async (req, res, next) => {
199
- try {
200
- if (!guardProject(req, res)) return;
201
- const dir = await resolvePerformanceDir(req, res);
202
- if (!dir) return;
203
- let config;
204
- try {
205
- config = normalizeOrchestration(req.body?.配置);
206
- } catch (err) {
207
- return res.status(422).json({ error: err.message });
208
- }
209
- const files = req.body?.文件 && typeof req.body.文件 === 'object' ? req.body.文件 : {};
210
- const forbidden = new Set([CONFIG_FILE, SUMMARY_FILE, config.历史.文件]);
211
- for (const [rel, text] of Object.entries(files)) {
212
- if (forbidden.has(rel)) {
213
- return res.status(422).json({ error: `「${rel}」不归设置页写(配置/摘要/对话记录各有各的落盘路)` });
214
- }
215
- if (typeof text !== 'string') return res.status(422).json({ error: `文件「${rel}」的内容要是字符串` });
216
- let abs;
217
- try { abs = resolveInside(dir.abs, rel, '文件表'); } catch (err) {
218
- return res.status(422).json({ error: err.message });
219
- }
220
- await fs.mkdir(path.dirname(abs), { recursive: true });
221
- await fs.writeFile(abs, text, 'utf8');
222
- }
223
- const target = path.join(dir.abs, CONFIG_FILE);
224
- const tmp = `${target}.tmp`;
225
- await fs.writeFile(tmp, serializeOrchestration(config), 'utf8');
226
- await fs.rename(tmp, target);
227
- res.json({ ok: true, 配置: config });
228
- } catch (err) { next(err); }
229
- });
230
-
231
- /**
232
- * POST /:pid/chatai/turn body { dir, input, stream? }
233
- */
234
- router.post('/:pid/chatai/turn', async (req, res, next) => {
235
- try {
236
- if (!guardProject(req, res)) return;
237
- if (!gateApproved(req, res)) return;
238
-
239
- const input = String(req.body?.input ?? '').trim();
240
- if (!input) return res.status(422).json({ error: 'input 是空的' });
241
- if (input.length > MAX_INPUT_CHARS) {
242
- return res.status(422).json({ error: `input 超长(上限 ${MAX_INPUT_CHARS} 字符)` });
243
- }
244
-
245
- // 08-21 经营态:演出通路烧的是带钥匙的 API 钱,公开注册号(免费档)不开 —— 跟订阅 Claude 同一把资格
246
- if (!can(req.user, 'subscription')) {
247
- return res.status(403).json({ error: '演出模式仅限 Pro 档,暂未对外开放;当前档位请用设计会话', code: 'MODEL_LOCKED' });
248
- }
249
- const quota = checkQuota(req.user);
250
- if (!quota.ok) {
251
- const word = quota.kind === 'lifetime' ? '试用额度' : '今日额度';
252
- return res.status(429).json({ error: `${word}用完了(${fmtUsd(quota.used)} / ${fmtUsd(quota.limit)})` });
253
- }
254
- const r = rate.take(req.user?.id ?? 'anon');
255
- if (!r.ok) {
256
- res.setHeader('Retry-After', Math.ceil(r.retryAfterMs / 1000));
257
- return res.status(429).json({ error: '太快了,歇几秒再发' });
258
- }
259
-
260
- const dir = await resolvePerformanceDir(req, res);
261
- if (!dir) return;
262
- if (playing.has(dir.abs)) {
263
- return res.status(409).json({ error: '这场演出正有一轮在跑,等它回完' });
264
- }
265
-
266
- const stream = req.body?.stream !== false;
267
- const run = createRun({
268
- skillId: 'chatai',
269
- brief: input.slice(0, 120),
270
- projectId: req.params.pid,
271
- userId: req.user?.id ?? null,
272
- metadata: { dir: dir.rel, kind: 'chatai-turn' },
273
- });
274
- markRunStarted(run.id);
275
-
276
- // 客户端断开就掐上游:落盘在调用成功之后,掐掉的轮子不留半条记录
277
- const ctrl = new AbortController();
278
- req.on('close', () => { if (!res.writableEnded) ctrl.abort(new Error('客户端断开')); });
279
-
280
- playing.add(dir.abs);
281
- try {
282
- if (stream) {
283
- res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
284
- res.setHeader('Cache-Control', 'no-cache');
285
- res.setHeader('X-Accel-Buffering', 'no');
286
- res.flushHeaders?.();
287
- const send = (obj) => res.write(`data: ${JSON.stringify(obj)}\n\n`);
288
- try {
289
- const out = await performTurn({
290
- dir: dir.abs, userInput: input, signal: ctrl.signal,
291
- onDelta: (piece) => send({ delta: piece }),
292
- });
293
- const costUsd = meterRun(run.id, out);
294
- markRunSucceeded(run.id);
295
- send({
296
- done: true, text: out.text, usage: out.usage, costUsd,
297
- 模型: out.model, 档: out.档 ?? null, ...(out.降级 ? { 降级: out.降级 } : {}),
298
- 摘要: out.摘要 ? { 折叠: !out.摘要.失败, ...(out.摘要.失败 ? { 失败: out.摘要.失败 } : {}) } : null,
299
- });
300
- } catch (err) {
301
- markRunFailed(run.id, err.message);
302
- send({ error: err.message, code: err.code || null });
303
- }
304
- res.end();
305
- } else {
306
- try {
307
- const out = await performTurn({ dir: dir.abs, userInput: input, signal: ctrl.signal });
308
- const costUsd = meterRun(run.id, out);
309
- markRunSucceeded(run.id);
310
- res.json({
311
- text: out.text, usage: out.usage, costUsd, meta: out.meta,
312
- 模型: out.model, 档: out.档 ?? null, ...(out.降级 ? { 降级: out.降级 } : {}),
313
- 摘要: out.摘要 ? { 折叠: !out.摘要.失败, ...(out.摘要.失败 ? { 失败: out.摘要.失败 } : {}) } : null,
314
- });
315
- } catch (err) {
316
- markRunFailed(run.id, err.message);
317
- res.status(statusOfOrchError(err)).json({ error: err.message, code: err.code || null });
318
- }
319
- }
320
- } finally {
321
- playing.delete(dir.abs);
322
- }
323
- } catch (err) { next(err); }
324
- });
1
+ /**
2
+ * server/api/chatai.js —— 演出端点:页面 → 服务端 → 中转站。
3
+ *
4
+ * agent 写的演出前端(iframe 里的站点页)打这两条路由;chatai 的 key 在服务端
5
+ * env,页面永远拿不到。页面靠同源 cookie 认证,不发任何新凭证。
6
+ *
7
+ * ⭐ 演出路**不过内容审查**(2026-08-15 拍板):chatai 无工具、无会话、改不了
8
+ * 任何文件,出格也只是文本;审查只存在于 agent 侧。但不过审 ≠ 不设闸,四道闸
9
+ * 全钉在这一个口上:
10
+ *
11
+ * 1. 通路批准制 —— auth/tier.js localGenApproved:档位有资格(admin/pro)且被站主逐人批过(同 roll_film 那套);basic 档不开。
12
+ * 中转站计量单位不明,问清之前不对全量内测用户开。
13
+ * 2. 金额闸门 —— checkQuota 同一个 USD 池:每轮花费按我们的单价表记进
14
+ * runs + run_model_usage(skill_id='chatai'),跟 agent 会话共享日限/终身额度。
15
+ * 3. 频率闸门 —— 滑动窗口每用户每分钟 N 轮(env NODESIGN_CHATAI_TURNS_PER_MIN,
16
+ * 默认 10)。金额日限拦不住单日尖峰,这道闸才是 setInterval 烧钱机的对手。
17
+ * 4. 单演出串行 —— 同一文件夹同时只跑一轮(保护 对话.jsonl 成对追加与摘要
18
+ * 折叠),撞上返 409 而不是排队。
19
+ *
20
+ * 流式默认 SSE(data: {delta} … data: {done, usage, costUsd, 摘要});
21
+ * body.stream=false 走整段 JSON。中转站分片本来就粗,别指望逐字。
22
+ */
23
+
24
+ import path from 'node:path';
25
+ import fs from 'node:fs/promises';
26
+ import express from 'express';
27
+ import { guardProject } from './_guard.js';
28
+ import { getWorkspaceRoot } from '../projects/workspace.js';
29
+ import { performTurn } from '../engine/chatai/perform.js';
30
+ import {
31
+ loadOrchestration, normalizeOrchestration, serializeOrchestration,
32
+ resolveInside, estTokens, CONFIG_FILE,
33
+ } from '../engine/chatai/orchestrate.js';
34
+ import { readLog, readSummary, SUMMARY_FILE } from '../engine/chatai/chat-log.js';
35
+ import { modelCatalog } from '../engine/chatai/index.js';
36
+ import { checkQuota, fmtUsd } from '../lib/quota.js';
37
+ import { can, localGenApproved } from '../auth/tier.js';
38
+ import { makeRateWindow } from '../lib/rate-window.js';
39
+ import {
40
+ createRun, markRunStarted, markRunSucceeded, markRunFailed,
41
+ setRunMetrics, setRunModelUsage,
42
+ } from '../engine/runs/store.js';
43
+
44
+ const router = express.Router();
45
+ export default router;
46
+
47
+ const MAX_INPUT_CHARS = 8000;
48
+
49
+ const rate = makeRateWindow({
50
+ limit: Number(process.env.NODESIGN_CHATAI_TURNS_PER_MIN) || 10,
51
+ windowMs: 60_000,
52
+ });
53
+
54
+ /** 正在演出的文件夹(abs path → true)。串行是保护 jsonl,不是并发额度。 */
55
+ const playing = new Set();
56
+
57
+ function gateApproved(req, res) {
58
+ // 档位 + 逐人批准(auth/tier.js localGenApproved):basic 档不开,pro 档要被站主批过
59
+ if (localGenApproved(req.user)) return true;
60
+ res.status(403).json({ error: '演出通路尚未对这个账号开放' });
61
+ return false;
62
+ }
63
+
64
+ /** 解析演出文件夹:相对工作区根,防逃逸,必须真是目录。失败时已发响应。 */
65
+ async function resolvePerformanceDir(req, res) {
66
+ const root = path.resolve(getWorkspaceRoot(req.params.pid));
67
+ const rel = String(req.method === 'GET' ? req.query.dir ?? '' : req.body?.dir ?? '');
68
+ const abs = path.resolve(root, rel);
69
+ if (abs !== root && !abs.startsWith(root + path.sep)) {
70
+ res.status(400).json({ error: 'dir 跑出了项目工作区' });
71
+ return null;
72
+ }
73
+ try {
74
+ if (!(await fs.stat(abs)).isDirectory()) throw new Error();
75
+ } catch {
76
+ res.status(404).json({ error: '演出文件夹不存在' });
77
+ return null;
78
+ }
79
+ return { abs, rel };
80
+ }
81
+
82
+ function statusOfOrchError(err) {
83
+ if (err?.code === 'ORCH_NO_CONFIG') return 404;
84
+ if (err?.code === 'ORCH_INVALID') return 422;
85
+ if (err?.code === 'CHATAI_UNCONFIGURED') return 503;
86
+ if (err?.code === 'CHATAI_UPSTREAM') return 502;
87
+ return 500;
88
+ }
89
+
90
+ /** 把一轮(正文 + 可能的摘要调用)的用量并成 run_model_usage 行 */
91
+ function usageRows(out) {
92
+ const rows = new Map();
93
+ const add = (model, usage, costUsd) => {
94
+ if (!usage) return;
95
+ const key = model || 'unknown';
96
+ const acc = rows.get(key) || {
97
+ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, costUsd: 0,
98
+ };
99
+ // 列口径对齐 agent 行:input 不含缓存读(openai 格式的 prompt_tokens 含,要拆);
100
+ // 思考 token 按输出计(Gemini 口径,costOf 同款)
101
+ acc.inputTokens += Math.max(0, (usage.inputTokens || 0) - (usage.cacheReadTokens || 0));
102
+ acc.outputTokens += (usage.outputTokens || 0) + (usage.reasoningTokens || 0);
103
+ acc.cacheReadTokens += usage.cacheReadTokens || 0;
104
+ acc.costUsd += costUsd || 0;
105
+ rows.set(key, acc);
106
+ };
107
+ add(out.model, out.usage, out.costUsd);
108
+ if (out.摘要?.用量) add(out.摘要.模型, out.摘要.用量, out.摘要.花费);
109
+ return rows;
110
+ }
111
+
112
+ function meterRun(runId, out) {
113
+ const rows = usageRows(out);
114
+ setRunModelUsage(runId, Object.fromEntries(rows));
115
+ const total = [...rows.values()].reduce((a, r) => ({
116
+ inputTokens: a.inputTokens + r.inputTokens,
117
+ outputTokens: a.outputTokens + r.outputTokens,
118
+ cacheReadTokens: a.cacheReadTokens + r.cacheReadTokens,
119
+ totalCostUsd: a.totalCostUsd + r.costUsd,
120
+ }), { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, totalCostUsd: 0 });
121
+ setRunMetrics(runId, total);
122
+ return total.totalCostUsd;
123
+ }
124
+
125
+ /**
126
+ * GET /:pid/chatai/log?dir=… —— 页面打开时渲染既有对话。
127
+ * 读不花钱不限频;只要项目归属过关就给。
128
+ */
129
+ router.get('/:pid/chatai/log', async (req, res, next) => {
130
+ try {
131
+ if (!guardProject(req, res)) return;
132
+ const dir = await resolvePerformanceDir(req, res);
133
+ if (!dir) return;
134
+ let config;
135
+ try {
136
+ config = await loadOrchestration(dir.abs);
137
+ } catch (err) {
138
+ return res.status(statusOfOrchError(err)).json({ error: err.message });
139
+ }
140
+ const [records, summary] = await Promise.all([
141
+ readLog(dir.abs, config), readSummary(dir.abs),
142
+ ]);
143
+ res.json({
144
+ records,
145
+ 摘要: summary ? { 至: summary.至, 内容: summary.内容 } : null,
146
+ 配置: { 模型: config.模型, 最大输出: config.最大输出, 上下文预算: config.上下文预算 },
147
+ });
148
+ } catch (err) { next(err); }
149
+ });
150
+
151
+ /**
152
+ * GET /:pid/chatai/config?dir=… —— 设置页的一次取齐:归一后的配置 +
153
+ * 条目引用的文件内容 + 历史/摘要状况。token 估算前端自己算(同款下限口径)。
154
+ */
155
+ router.get('/:pid/chatai/config', async (req, res, next) => {
156
+ try {
157
+ if (!guardProject(req, res)) return;
158
+ const dir = await resolvePerformanceDir(req, res);
159
+ if (!dir) return;
160
+ let config;
161
+ try {
162
+ config = await loadOrchestration(dir.abs);
163
+ } catch (err) {
164
+ return res.status(statusOfOrchError(err)).json({ error: err.message, code: err.code || null });
165
+ }
166
+ const 文件 = {};
167
+ for (const e of [...config.系统层, ...config.尾部]) {
168
+ if (e.文件 == null || e.文件 in 文件) continue;
169
+ try {
170
+ 文件[e.文件] = await fs.readFile(resolveInside(dir.abs, e.文件, `条目「${e.名字}」`), 'utf8');
171
+ } catch { 文件[e.文件] = null; } // 引用悬空:设置页要红给用户看,不是 500
172
+ }
173
+ const [records, summary] = await Promise.all([readLog(dir.abs, config), readSummary(dir.abs)]);
174
+ const boundary = summary?.至 ?? 0;
175
+ res.json({
176
+ 配置: config,
177
+ 文件,
178
+ // 模型/单价/思考档由服务端一张表说了算(MODEL_SPECS),前端只负责画——
179
+ // 中转站改名或换档位时不用两头改(单一真相源)。
180
+ 模型表: modelCatalog(),
181
+ 状况: {
182
+ 记录条数: records.length,
183
+ 轮数: records.filter(r => r.role === 'user').length,
184
+ 活历史tok: records.filter(r => r.seq > boundary).reduce((n, r) => n + estTokens(r.text), 0),
185
+ 摘要: summary ? { 至: summary.至, 内容: summary.内容 } : null,
186
+ },
187
+ });
188
+ } catch (err) { next(err); }
189
+ });
190
+
191
+ /**
192
+ * PUT /:pid/chatai/config body { dir, 配置, 文件? }
193
+ *
194
+ * 校验(normalizeOrchestration,跟演出路同一份)→ 先写引用文件、再原子写
195
+ * 编排.yaml。改动下一轮生效——正在跑的那轮已经编译完了,不追。
196
+ * 文件表只许写文件夹内部,且不许碰 编排.yaml / 摘要.json / 对话记录本体。
197
+ */
198
+ router.put('/:pid/chatai/config', async (req, res, next) => {
199
+ try {
200
+ if (!guardProject(req, res)) return;
201
+ const dir = await resolvePerformanceDir(req, res);
202
+ if (!dir) return;
203
+ let config;
204
+ try {
205
+ config = normalizeOrchestration(req.body?.配置);
206
+ } catch (err) {
207
+ return res.status(422).json({ error: err.message });
208
+ }
209
+ const files = req.body?.文件 && typeof req.body.文件 === 'object' ? req.body.文件 : {};
210
+ const forbidden = new Set([CONFIG_FILE, SUMMARY_FILE, config.历史.文件]);
211
+ for (const [rel, text] of Object.entries(files)) {
212
+ if (forbidden.has(rel)) {
213
+ return res.status(422).json({ error: `「${rel}」不归设置页写(配置/摘要/对话记录各有各的落盘路)` });
214
+ }
215
+ if (typeof text !== 'string') return res.status(422).json({ error: `文件「${rel}」的内容要是字符串` });
216
+ let abs;
217
+ try { abs = resolveInside(dir.abs, rel, '文件表'); } catch (err) {
218
+ return res.status(422).json({ error: err.message });
219
+ }
220
+ await fs.mkdir(path.dirname(abs), { recursive: true });
221
+ await fs.writeFile(abs, text, 'utf8');
222
+ }
223
+ const target = path.join(dir.abs, CONFIG_FILE);
224
+ const tmp = `${target}.tmp`;
225
+ await fs.writeFile(tmp, serializeOrchestration(config), 'utf8');
226
+ await fs.rename(tmp, target);
227
+ res.json({ ok: true, 配置: config });
228
+ } catch (err) { next(err); }
229
+ });
230
+
231
+ /**
232
+ * POST /:pid/chatai/turn body { dir, input, stream? }
233
+ */
234
+ router.post('/:pid/chatai/turn', async (req, res, next) => {
235
+ try {
236
+ if (!guardProject(req, res)) return;
237
+ if (!gateApproved(req, res)) return;
238
+
239
+ const input = String(req.body?.input ?? '').trim();
240
+ if (!input) return res.status(422).json({ error: 'input 是空的' });
241
+ if (input.length > MAX_INPUT_CHARS) {
242
+ return res.status(422).json({ error: `input 超长(上限 ${MAX_INPUT_CHARS} 字符)` });
243
+ }
244
+
245
+ // 08-21 经营态:演出通路烧的是带钥匙的 API 钱,公开注册号(免费档)不开 —— 跟订阅 Claude 同一把资格
246
+ if (!can(req.user, 'subscription')) {
247
+ return res.status(403).json({ error: '演出模式仅限 Pro 档,暂未对外开放;当前档位请用设计会话', code: 'MODEL_LOCKED' });
248
+ }
249
+ const quota = checkQuota(req.user);
250
+ if (!quota.ok) {
251
+ const word = quota.kind === 'lifetime' ? '试用额度' : '今日额度';
252
+ return res.status(429).json({ error: `${word}用完了(${fmtUsd(quota.used)} / ${fmtUsd(quota.limit)})` });
253
+ }
254
+ const r = rate.take(req.user?.id ?? 'anon');
255
+ if (!r.ok) {
256
+ res.setHeader('Retry-After', Math.ceil(r.retryAfterMs / 1000));
257
+ return res.status(429).json({ error: '太快了,歇几秒再发' });
258
+ }
259
+
260
+ const dir = await resolvePerformanceDir(req, res);
261
+ if (!dir) return;
262
+ if (playing.has(dir.abs)) {
263
+ return res.status(409).json({ error: '这场演出正有一轮在跑,等它回完' });
264
+ }
265
+
266
+ const stream = req.body?.stream !== false;
267
+ const run = createRun({
268
+ skillId: 'chatai',
269
+ brief: input.slice(0, 120),
270
+ projectId: req.params.pid,
271
+ userId: req.user?.id ?? null,
272
+ metadata: { dir: dir.rel, kind: 'chatai-turn' },
273
+ });
274
+ markRunStarted(run.id);
275
+
276
+ // 客户端断开就掐上游:落盘在调用成功之后,掐掉的轮子不留半条记录
277
+ const ctrl = new AbortController();
278
+ req.on('close', () => { if (!res.writableEnded) ctrl.abort(new Error('客户端断开')); });
279
+
280
+ playing.add(dir.abs);
281
+ try {
282
+ if (stream) {
283
+ res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
284
+ res.setHeader('Cache-Control', 'no-cache');
285
+ res.setHeader('X-Accel-Buffering', 'no');
286
+ res.flushHeaders?.();
287
+ const send = (obj) => res.write(`data: ${JSON.stringify(obj)}\n\n`);
288
+ try {
289
+ const out = await performTurn({
290
+ dir: dir.abs, userInput: input, signal: ctrl.signal,
291
+ onDelta: (piece) => send({ delta: piece }),
292
+ });
293
+ const costUsd = meterRun(run.id, out);
294
+ markRunSucceeded(run.id);
295
+ send({
296
+ done: true, text: out.text, usage: out.usage, costUsd,
297
+ 模型: out.model, 档: out.档 ?? null, ...(out.降级 ? { 降级: out.降级 } : {}),
298
+ 摘要: out.摘要 ? { 折叠: !out.摘要.失败, ...(out.摘要.失败 ? { 失败: out.摘要.失败 } : {}) } : null,
299
+ });
300
+ } catch (err) {
301
+ markRunFailed(run.id, err.message);
302
+ send({ error: err.message, code: err.code || null });
303
+ }
304
+ res.end();
305
+ } else {
306
+ try {
307
+ const out = await performTurn({ dir: dir.abs, userInput: input, signal: ctrl.signal });
308
+ const costUsd = meterRun(run.id, out);
309
+ markRunSucceeded(run.id);
310
+ res.json({
311
+ text: out.text, usage: out.usage, costUsd, meta: out.meta,
312
+ 模型: out.model, 档: out.档 ?? null, ...(out.降级 ? { 降级: out.降级 } : {}),
313
+ 摘要: out.摘要 ? { 折叠: !out.摘要.失败, ...(out.摘要.失败 ? { 失败: out.摘要.失败 } : {}) } : null,
314
+ });
315
+ } catch (err) {
316
+ markRunFailed(run.id, err.message);
317
+ res.status(statusOfOrchError(err)).json({ error: err.message, code: err.code || null });
318
+ }
319
+ }
320
+ } finally {
321
+ playing.delete(dir.abs);
322
+ }
323
+ } catch (err) { next(err); }
324
+ });