@xiaobuyu/nodesign 0.0.6 → 0.0.8

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 (383) hide show
  1. package/README.md +266 -79
  2. package/README.zh-CN.md +254 -67
  3. package/package.json +2 -2
  4. package/server/api/admin.js +7 -0
  5. package/server/api/assets/docx-page.js +5 -4
  6. package/server/api/assets/helpers.js +26 -0
  7. package/server/api/assets/notes.js +50 -0
  8. package/server/api/assets.js +31 -38
  9. package/server/api/board.js +29 -1
  10. package/server/api/browse.js +3 -2
  11. package/server/api/chatai.js +9 -8
  12. package/server/api/exports/cards.js +7 -6
  13. package/server/api/exports/docx-pdf.js +4 -1
  14. package/server/api/instruction.js +3 -3
  15. package/server/api/local.js +10 -8
  16. package/server/api/projects.js +19 -3
  17. package/server/api/publish.js +3 -2
  18. package/server/api/sessions-rewind.js +220 -0
  19. package/server/api/sessions.js +58 -194
  20. package/server/api/stage.js +35 -0
  21. package/server/api/turn-model-switch.js +109 -0
  22. package/server/api/turn.js +52 -88
  23. package/server/auth/middleware.js +29 -6
  24. package/server/auth/tier.js +27 -7
  25. package/server/auth/users-store.js +14 -1
  26. package/server/engine/agent/actor-trail.js +110 -0
  27. package/server/engine/agent/agent-shared.js +22 -52
  28. package/server/engine/agent/beat-state.js +61 -0
  29. package/server/engine/agent/cast.js +322 -0
  30. package/server/engine/agent/events.js +18 -1
  31. package/server/engine/agent/hooks/beat-gate.js +73 -0
  32. package/server/engine/agent/hooks/failure.js +11 -1
  33. package/server/engine/agent/hooks/lifecycle.js +16 -41
  34. package/server/engine/agent/hooks/post-guidance.js +4 -38
  35. package/server/engine/agent/hooks/post-trim.js +1 -0
  36. package/server/engine/agent/hooks/pre-board-dirty.js +29 -0
  37. package/server/engine/agent/hooks/pre-defaults.js +206 -2
  38. package/server/engine/agent/hooks/pre-injectors.js +19 -2
  39. package/server/engine/agent/hooks/pre-peer-guard.js +88 -0
  40. package/server/engine/agent/hooks/pre-unknown-params.js +82 -0
  41. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +33 -0
  42. package/server/engine/agent/hooks/resident-role-lifecycle.js +100 -0
  43. package/server/engine/agent/hooks/slot-alias.js +30 -0
  44. package/server/engine/agent/hooks/user-prompt-submit.js +208 -47
  45. package/server/engine/agent/hooks.js +80 -14
  46. package/server/engine/agent/isolation.js +25 -2
  47. package/server/engine/agent/memory-config.js +87 -0
  48. package/server/engine/agent/model-context.js +102 -18
  49. package/server/engine/agent/model-table.js +361 -58
  50. package/server/engine/agent/prompts/nodesign-prelude.md +579 -70
  51. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +1 -1
  52. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +26 -9
  53. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +1 -1
  54. package/server/engine/agent/prompts/tools/site-reference.md +15 -4
  55. package/server/engine/agent/role-card.js +175 -0
  56. package/server/engine/agent/rp-mode.js +17 -0
  57. package/server/engine/agent/session-loop.js +55 -32
  58. package/server/engine/agent/stage-status.js +73 -0
  59. package/server/engine/agent/subagent-flight.js +73 -0
  60. package/server/engine/agent/system-prompts.js +41 -0
  61. package/server/engine/agent/tool-input-stream.js +165 -0
  62. package/server/engine/agents/explorer.md +1 -1
  63. package/server/engine/agents/index.js +1 -1
  64. package/server/engine/mcp/actor.js +59 -0
  65. package/server/engine/mcp/capability-gate.js +21 -2
  66. package/server/engine/mcp/index.js +118 -33
  67. package/server/engine/mcp/mode-profile.js +139 -0
  68. package/server/engine/mcp/server-name.js +19 -0
  69. package/server/engine/mcp/tools/artifact-session.js +2 -1
  70. package/server/engine/mcp/tools/browse-computer.js +2 -1
  71. package/server/engine/mcp/tools/browse-find-batch.js +95 -16
  72. package/server/engine/mcp/tools/browse.js +25 -3
  73. package/server/engine/mcp/tools/cast-role.js +167 -0
  74. package/server/engine/mcp/tools/crystallize-skill.js +1 -1
  75. package/server/engine/mcp/tools/draw-trend.js +104 -0
  76. package/server/engine/mcp/tools/edit-board-schema.js +56 -0
  77. package/server/engine/mcp/tools/edit-board.js +569 -0
  78. package/server/engine/mcp/tools/generate-image.js +50 -70
  79. package/server/engine/mcp/tools/helpers/codex-imagegen.js +139 -0
  80. package/server/engine/mcp/tools/helpers/rembg.js +7 -2
  81. package/server/engine/mcp/tools/helpers/shot-pipeline.js +109 -0
  82. package/server/engine/mcp/tools/list-pages.js +11 -5
  83. package/server/engine/mcp/tools/look-at-board.js +4 -3
  84. package/server/engine/mcp/tools/open-sheet.js +242 -0
  85. package/server/engine/mcp/tools/organize-board.js +45 -6
  86. package/server/engine/mcp/tools/paint-still.js +1 -1
  87. package/server/engine/mcp/tools/pin-to-board.js +103 -19
  88. package/server/engine/mcp/tools/read-board.js +123 -16
  89. package/server/engine/mcp/tools/read-tavern-json.js +87 -19
  90. package/server/engine/mcp/tools/report-issue.js +15 -7
  91. package/server/engine/mcp/tools/role-memory.js +83 -0
  92. package/server/engine/mcp/tools/roll-dice.js +82 -0
  93. package/server/engine/mcp/tools/roll-film.js +1 -1
  94. package/server/engine/mcp/tools/screenshot.js +19 -9
  95. package/server/engine/mcp/tools/set-vars.js +150 -0
  96. package/server/engine/mcp/tools/sheet-replan.js +46 -0
  97. package/server/engine/mcp/tools/web-search.js +12 -16
  98. package/server/engine/mcp/tools/write-on-board-flow.js +108 -0
  99. package/server/engine/mcp/tools/write-on-board-place.js +211 -0
  100. package/server/engine/mcp/tools/write-on-board-role-anchor.js +48 -0
  101. package/server/engine/mcp/tools/write-on-board-schema.js +125 -0
  102. package/server/engine/mcp/tools/write-on-board.js +568 -99
  103. package/server/engine/plugins/nodesign/skills/blackboard-rp/SKILL.md +215 -0
  104. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +14 -7
  105. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +44 -5
  106. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +16 -0
  107. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +24 -8
  108. package/server/engine/plugins/nodesign/skills/story-craft/SKILL.md +94 -0
  109. package/server/engine/plugins/nodesign/skills/story-import/SKILL.md +112 -0
  110. package/server/engine/plugins/nodesign/skills/story-intimacy/SKILL.md +72 -0
  111. package/server/engine/plugins/nodesign/skills/story-voice/SKILL.md +66 -0
  112. package/server/engine/plugins/nodesign/skills/story-voice/voices//345/206/267/347/241/254.md +33 -0
  113. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/203/205/346/204/237/347/233/264/347/273/231.md +42 -0
  114. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/204/217/350/257/206/346/265/201.md +42 -0
  115. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/255/246/344/276/240/347/231/275/350/257/235.md +34 -0
  116. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/224/237/346/264/273/346/265/201.md +35 -0
  117. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/231/275/346/217/217.md +37 -0
  118. package/server/engine/plugins/nodesign/skills/story-voice/voices//350/210/236/345/217/260/345/211/247.md +32 -0
  119. package/server/engine/plugins/nodesign/skills/story-voice/voices//351/230/266/346/256/265/345/210/207/346/215/242.md +42 -0
  120. package/server/engine/runs/board-seater.js +272 -0
  121. package/server/engine/runs/board-tasklist.js +90 -48
  122. package/server/engine/runs/turn-relay.js +24 -2
  123. package/server/engine/runs/user-chalk-echo.js +44 -0
  124. package/server/index.js +2 -2
  125. package/server/lib/artifact-target.js +31 -14
  126. package/server/lib/board-anchor.js +49 -0
  127. package/server/lib/board-dirty.js +93 -0
  128. package/server/lib/board-excerpt.js +80 -0
  129. package/server/lib/board-follow.js +111 -0
  130. package/server/lib/board-groups.js +1 -5
  131. package/server/lib/board-hero.js +3 -1
  132. package/server/lib/board-kind-sizes.js +38 -3
  133. package/server/lib/board-lanes.js +58 -0
  134. package/server/lib/board-obstacles.js +80 -0
  135. package/server/lib/board-relations.js +5 -1
  136. package/server/lib/board-sheets.js +467 -0
  137. package/server/lib/board-shelf.js +86 -0
  138. package/server/lib/board-transform.js +62 -0
  139. package/server/lib/canvas-id.js +46 -4
  140. package/server/lib/chalk-flow.js +88 -0
  141. package/server/lib/chalk-rewrite.js +63 -0
  142. package/server/lib/chalk-size-pref.js +48 -0
  143. package/server/lib/chalk.js +42 -3
  144. package/server/lib/export-collect.js +3 -1
  145. package/server/lib/ingress/forward-openai-chat.js +16 -4
  146. package/server/lib/ingress/openai-chat.js +23 -12
  147. package/server/lib/ingress/session-routes.js +3 -3
  148. package/server/lib/ingress/stream-watchdog.js +53 -0
  149. package/server/lib/ingress/upstream-billing.js +15 -0
  150. package/server/lib/ingress/upstream-error-hints.js +36 -0
  151. package/server/lib/issues-store.js +3 -1
  152. package/server/lib/json-preview.js +58 -0
  153. package/server/lib/kinds/docx.js +11 -4
  154. package/server/lib/kinds/index.js +6 -16
  155. package/server/lib/kinds/site.js +40 -7
  156. package/server/lib/model-ingress.js +26 -1
  157. package/server/lib/moderation.js +10 -5
  158. package/server/lib/quota.js +26 -6
  159. package/server/lib/rect.js +50 -0
  160. package/server/lib/rewrite-refs.js +110 -0
  161. package/server/lib/screen.js +36 -0
  162. package/server/lib/sheet-state.js +19 -0
  163. package/server/lib/sketch-array.js +118 -0
  164. package/server/lib/sketch-hatch.js +99 -0
  165. package/server/lib/sketch-layout.js +269 -69
  166. package/server/lib/sketch-shapes.js +152 -0
  167. package/server/lib/sketch-stencils.js +170 -0
  168. package/server/lib/ssrf-guard.js +9 -0
  169. package/server/lib/state-table.js +237 -0
  170. package/server/lib/state-trend.js +94 -0
  171. package/server/lib/state-triggers.js +199 -0
  172. package/server/lib/task-scan.js +18 -0
  173. package/server/lib/tavern-json.js +48 -0
  174. package/server/lib/user-uploads.js +88 -0
  175. package/server/projects/board-limits.js +5 -3
  176. package/server/projects/board-sanitize.js +220 -10
  177. package/server/projects/board-store.js +137 -7
  178. package/server/projects/memory-migration.js +85 -0
  179. package/server/projects/move-entry.js +11 -1
  180. package/server/projects/session-jsonl.js +110 -0
  181. package/server/projects/store.js +56 -3
  182. package/server/projects/ui-config.js +2 -1
  183. package/server/projects/viewpoint-store.js +35 -3
  184. package/server/projects/workspace-slots.js +30 -0
  185. package/server/projects/workspace-templates.js +42 -10
  186. package/server/projects/workspace.js +25 -26
  187. package/server/runtime/capabilities.js +6 -2
  188. package/server/services/rembg-launcher.js +5 -2
  189. package/server/shared/locales.js +70 -0
  190. package/server/shared/messages-en.js +69 -0
  191. package/server/shared/messages.js +65 -0
  192. package/server/ws/broker.js +3 -0
  193. package/server/ws/index.js +43 -3
  194. package/web/README.md +1 -1
  195. package/web/dist/assets/{BrowserWindow-P_vIdh-G.js → BrowserWindow-CcaGRWz5.js} +1 -1
  196. package/web/dist/assets/DeckWindow-BIr2KGFT.js +6 -0
  197. package/web/dist/assets/{DocxWindow-DSAU1-tD.js → DocxWindow-B9KZmU5z.js} +2 -2
  198. package/web/dist/assets/{MermaidBlock-DR1GikCv.js → MermaidBlock-DrfRnz8E.js} +43 -43
  199. package/web/dist/assets/{SiteWindow-92uBuRRR.js → SiteWindow-CJOjCF7d.js} +6 -6
  200. package/web/dist/assets/{abnfDiagram-VCTEODGH-uA8Esix4.js → abnfDiagram-VCTEODGH-CUZbQGIg.js} +1 -1
  201. package/web/dist/assets/{arc-D_khH_xN.js → arc-C1-HSUny.js} +1 -1
  202. package/web/dist/assets/{architectureDiagram-5GKGNRK7-DydJHqOt.js → architectureDiagram-5GKGNRK7-B5nwvu60.js} +2 -2
  203. package/web/dist/assets/{blockDiagram-NRAW4CY4-B7wBbKiW.js → blockDiagram-NRAW4CY4-uPPuFLwR.js} +1 -1
  204. package/web/dist/assets/{c4Diagram-UCG6FXSJ-6mrH0x7Y.js → c4Diagram-UCG6FXSJ-C0mKBFlz.js} +1 -1
  205. package/web/dist/assets/channel-BxpWNOGU.js +1 -0
  206. package/web/dist/assets/{chunk-2Q5K7J3B-Cuikho5Q.js → chunk-2Q5K7J3B-Cs0rlZ4q.js} +1 -1
  207. package/web/dist/assets/{chunk-5VM5RSS4-3-KZ5AUz.js → chunk-5VM5RSS4-DtR63oYb.js} +1 -1
  208. package/web/dist/assets/{chunk-F27PBJKO-Bkwq2gGU.js → chunk-F27PBJKO-B3MRdezJ.js} +1 -1
  209. package/web/dist/assets/{chunk-G27WJ6UU-BLyQzdGE.js → chunk-G27WJ6UU-BtwntwpS.js} +1 -1
  210. package/web/dist/assets/{chunk-JWPE2WC7-B7HxxQds.js → chunk-JWPE2WC7-BHsTe5PD.js} +1 -1
  211. package/web/dist/assets/{chunk-LCL6LL3I-CtXrnUwF.js → chunk-LCL6LL3I-knY15Qdm.js} +1 -1
  212. package/web/dist/assets/{chunk-POPQ4Y6H-dxddZlXt.js → chunk-POPQ4Y6H-C51tgK_I.js} +1 -1
  213. package/web/dist/assets/{chunk-SVP7TREG-DpkmrgHn.js → chunk-SVP7TREG-CnwRZmF_.js} +1 -1
  214. package/web/dist/assets/{chunk-XXDRQBXY-BwZ7RbpQ.js → chunk-XXDRQBXY-BtXzVF-_.js} +1 -1
  215. package/web/dist/assets/classDiagram-DTDB5LWJ-DUEKmDfo.js +1 -0
  216. package/web/dist/assets/classDiagram-v2-JRS7N3AN-DUEKmDfo.js +1 -0
  217. package/web/dist/assets/{cose-bilkent-JH36ORCC-DGFOsMUt.js → cose-bilkent-JH36ORCC-CzWlGP6i.js} +1 -1
  218. package/web/dist/assets/{cynefin-OW5HDTMX-DUwn3ehS.js → cynefin-OW5HDTMX-vK64EGSv.js} +1 -1
  219. package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js → cynefinDiagram-5FMLGOSQ-Bs3g1Gvh.js} +1 -1
  220. package/web/dist/assets/{dagre-3AP2YEHR-CyourLfz.js → dagre-3AP2YEHR-BhgkYe8V.js} +1 -1
  221. package/web/dist/assets/{diagram-S7CK7UJ4-BTMbx2np.js → diagram-S7CK7UJ4-C_6mvD2V.js} +1 -1
  222. package/web/dist/assets/{diagram-UQ7AKVKN-UK94_1-P.js → diagram-UQ7AKVKN-qW7tssUW.js} +1 -1
  223. package/web/dist/assets/{diagram-VSXAHHWV-C7zS-v9v.js → diagram-VSXAHHWV-txJPHpvG.js} +1 -1
  224. package/web/dist/assets/{diagram-VX7I27RA-DKJRa6CK.js → diagram-VX7I27RA-CQTMsCnL.js} +1 -1
  225. package/web/dist/assets/{diagram-Z3DM3KII-kUbAfGAp.js → diagram-Z3DM3KII--cb1hoE6.js} +1 -1
  226. package/web/dist/assets/{ebnfDiagram-PWID7BFC-CVW16_vb.js → ebnfDiagram-PWID7BFC-Da6lDU77.js} +1 -1
  227. package/web/dist/assets/{erDiagram-SSCWMZ5O-BRQMFQ5y.js → erDiagram-SSCWMZ5O-DGCMrsQn.js} +1 -1
  228. package/web/dist/assets/{flowDiagram-A5DVABFB-S34LpLtK.js → flowDiagram-A5DVABFB-GdOL_qno.js} +1 -1
  229. package/web/dist/assets/{ganttDiagram-EL5Y4UJY-BFiqk1-G.js → ganttDiagram-EL5Y4UJY-Cx1F_jBK.js} +4 -4
  230. package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-LZsrdyyv.js → gitGraphDiagram-WWUBYQGX-B5_6JhR3.js} +1 -1
  231. package/web/dist/assets/hand-CuvZZQGW.webp +0 -0
  232. package/web/dist/assets/index-BYKfMnmv.js +2759 -0
  233. package/web/dist/assets/index-CW7VsrVg.css +1 -0
  234. package/web/dist/assets/{infoDiagram-RXCK75RN-0DvN6KIp.js → infoDiagram-RXCK75RN-CjvKpSPA.js} +1 -1
  235. package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CaY3A-Bx.js → ishikawaDiagram-5VMMS53U-CxLoAPhx.js} +1 -1
  236. package/web/dist/assets/{journeyDiagram-EYS64GPL-BBeiaCrj.js → journeyDiagram-EYS64GPL-BfvS1upJ.js} +1 -1
  237. package/web/dist/assets/{kanban-definition-3QL26DDD-ux42DRXQ.js → kanban-definition-3QL26DDD-DKPPu0w4.js} +1 -1
  238. package/web/dist/assets/{layout-CnXiF_NB.js → layout-Dhgck-Zo.js} +1 -1
  239. package/web/dist/assets/{linear-BMW0u6G6.js → linear-DMSQ-aUw.js} +1 -1
  240. package/web/dist/assets/lxgw-nd-app-bold-D1pKA1bR.woff2 +0 -0
  241. package/web/dist/assets/lxgw-nd-app-regular-BDe3d26v.woff2 +0 -0
  242. package/web/dist/assets/lxgw-nd-bold-Bufvlg03.woff2 +0 -0
  243. package/web/dist/assets/lxgw-nd-regular-DQwoI4OP.woff2 +0 -0
  244. package/web/dist/assets/lxgwwenkaiscreen-subset-100-DLPe7tEt.woff2 +0 -0
  245. package/web/dist/assets/lxgwwenkaiscreen-subset-101-d3VsZKWT.woff2 +0 -0
  246. package/web/dist/assets/lxgwwenkaiscreen-subset-102-ClzJuLq6.woff2 +0 -0
  247. package/web/dist/assets/lxgwwenkaiscreen-subset-103-DuDnQ6K-.woff2 +0 -0
  248. package/web/dist/assets/lxgwwenkaiscreen-subset-104-kPGW3Eem.woff2 +0 -0
  249. package/web/dist/assets/lxgwwenkaiscreen-subset-105-BzD54h-0.woff2 +0 -0
  250. package/web/dist/assets/lxgwwenkaiscreen-subset-106-BQ7fIzlx.woff2 +0 -0
  251. package/web/dist/assets/lxgwwenkaiscreen-subset-107-D8tDfFHI.woff2 +0 -0
  252. package/web/dist/assets/lxgwwenkaiscreen-subset-108-tGhkpwep.woff2 +0 -0
  253. package/web/dist/assets/lxgwwenkaiscreen-subset-109-7SMVSrly.woff2 +0 -0
  254. package/web/dist/assets/lxgwwenkaiscreen-subset-110-_uDKrFed.woff2 +0 -0
  255. package/web/dist/assets/lxgwwenkaiscreen-subset-111-Bio825RE.woff2 +0 -0
  256. package/web/dist/assets/lxgwwenkaiscreen-subset-112-D2ee0i-C.woff2 +0 -0
  257. package/web/dist/assets/lxgwwenkaiscreen-subset-113-JPFVDrga.woff2 +0 -0
  258. package/web/dist/assets/lxgwwenkaiscreen-subset-114-BcfxYA4w.woff2 +0 -0
  259. package/web/dist/assets/lxgwwenkaiscreen-subset-115-B1HeeOsd.woff2 +0 -0
  260. package/web/dist/assets/lxgwwenkaiscreen-subset-116-hY12tXpq.woff2 +0 -0
  261. package/web/dist/assets/lxgwwenkaiscreen-subset-117-BvuZwmre.woff2 +0 -0
  262. package/web/dist/assets/lxgwwenkaiscreen-subset-118-tzvs_H7j.woff2 +0 -0
  263. package/web/dist/assets/lxgwwenkaiscreen-subset-119-J2DWuYDN.woff2 +0 -0
  264. package/web/dist/assets/lxgwwenkaiscreen-subset-21-pDubCJV5.woff2 +0 -0
  265. package/web/dist/assets/lxgwwenkaiscreen-subset-22-ChAPMpqp.woff2 +0 -0
  266. package/web/dist/assets/lxgwwenkaiscreen-subset-23-BBx3oNpT.woff2 +0 -0
  267. package/web/dist/assets/lxgwwenkaiscreen-subset-24-CYt-Oagk.woff2 +0 -0
  268. package/web/dist/assets/lxgwwenkaiscreen-subset-25-Cx3j-ZUv.woff2 +0 -0
  269. package/web/dist/assets/lxgwwenkaiscreen-subset-26-B9AfZ3LL.woff2 +0 -0
  270. package/web/dist/assets/lxgwwenkaiscreen-subset-27-Bh2s9MNE.woff2 +0 -0
  271. package/web/dist/assets/lxgwwenkaiscreen-subset-28-C02JPoKP.woff2 +0 -0
  272. package/web/dist/assets/lxgwwenkaiscreen-subset-29-T_tbUGhk.woff2 +0 -0
  273. package/web/dist/assets/lxgwwenkaiscreen-subset-30-DMfyNzhz.woff2 +0 -0
  274. package/web/dist/assets/lxgwwenkaiscreen-subset-31-DFfdzbkg.woff2 +0 -0
  275. package/web/dist/assets/lxgwwenkaiscreen-subset-32-Bs1fz2iW.woff2 +0 -0
  276. package/web/dist/assets/lxgwwenkaiscreen-subset-33-cpB96OkZ.woff2 +0 -0
  277. package/web/dist/assets/lxgwwenkaiscreen-subset-34-BhinPYWX.woff2 +0 -0
  278. package/web/dist/assets/lxgwwenkaiscreen-subset-35-D_njRQqA.woff2 +0 -0
  279. package/web/dist/assets/lxgwwenkaiscreen-subset-36-DqXlLE3t.woff2 +0 -0
  280. package/web/dist/assets/lxgwwenkaiscreen-subset-37-j5LbvMph.woff2 +0 -0
  281. package/web/dist/assets/lxgwwenkaiscreen-subset-38-oO4jWdKm.woff2 +0 -0
  282. package/web/dist/assets/lxgwwenkaiscreen-subset-39-CabsX4rw.woff2 +0 -0
  283. package/web/dist/assets/lxgwwenkaiscreen-subset-4-PatS3YHh.woff2 +0 -0
  284. package/web/dist/assets/lxgwwenkaiscreen-subset-40-yrLg3C3e.woff2 +0 -0
  285. package/web/dist/assets/lxgwwenkaiscreen-subset-41-DBXmCQhc.woff2 +0 -0
  286. package/web/dist/assets/lxgwwenkaiscreen-subset-42-Bqij_UiQ.woff2 +0 -0
  287. package/web/dist/assets/lxgwwenkaiscreen-subset-43-D3kp0Ntc.woff2 +0 -0
  288. package/web/dist/assets/lxgwwenkaiscreen-subset-44-B_BNGvK4.woff2 +0 -0
  289. package/web/dist/assets/lxgwwenkaiscreen-subset-45-CZc0cQWs.woff2 +0 -0
  290. package/web/dist/assets/lxgwwenkaiscreen-subset-46-CHQX5Kd9.woff2 +0 -0
  291. package/web/dist/assets/lxgwwenkaiscreen-subset-47-Ci8gLrHA.woff2 +0 -0
  292. package/web/dist/assets/lxgwwenkaiscreen-subset-48-run3ugD3.woff2 +0 -0
  293. package/web/dist/assets/lxgwwenkaiscreen-subset-49-DgfiUb6u.woff2 +0 -0
  294. package/web/dist/assets/lxgwwenkaiscreen-subset-5-B0CtGbX_.woff2 +0 -0
  295. package/web/dist/assets/lxgwwenkaiscreen-subset-50-CdCG-POr.woff2 +0 -0
  296. package/web/dist/assets/lxgwwenkaiscreen-subset-51-L2okeQZ2.woff2 +0 -0
  297. package/web/dist/assets/lxgwwenkaiscreen-subset-52-BZAqyGJW.woff2 +0 -0
  298. package/web/dist/assets/lxgwwenkaiscreen-subset-53-B0xWqLeU.woff2 +0 -0
  299. package/web/dist/assets/lxgwwenkaiscreen-subset-54-BZVQX-EA.woff2 +0 -0
  300. package/web/dist/assets/lxgwwenkaiscreen-subset-55-093mkIUJ.woff2 +0 -0
  301. package/web/dist/assets/lxgwwenkaiscreen-subset-56-CbhWG8G-.woff2 +0 -0
  302. package/web/dist/assets/lxgwwenkaiscreen-subset-57-pLQuvo6c.woff2 +0 -0
  303. package/web/dist/assets/lxgwwenkaiscreen-subset-58-BHhoY50x.woff2 +0 -0
  304. package/web/dist/assets/lxgwwenkaiscreen-subset-59-D1_6DuIQ.woff2 +0 -0
  305. package/web/dist/assets/lxgwwenkaiscreen-subset-6-BnAelmYU.woff2 +0 -0
  306. package/web/dist/assets/lxgwwenkaiscreen-subset-60-HDBIuLTZ.woff2 +0 -0
  307. package/web/dist/assets/lxgwwenkaiscreen-subset-61-C-c13SeU.woff2 +0 -0
  308. package/web/dist/assets/lxgwwenkaiscreen-subset-62-kHRVOhZ2.woff2 +0 -0
  309. package/web/dist/assets/lxgwwenkaiscreen-subset-63-C0MCsvFv.woff2 +0 -0
  310. package/web/dist/assets/lxgwwenkaiscreen-subset-64-DZekguCj.woff2 +0 -0
  311. package/web/dist/assets/lxgwwenkaiscreen-subset-65-CtP6c3IU.woff2 +0 -0
  312. package/web/dist/assets/lxgwwenkaiscreen-subset-66-DLVWoMSf.woff2 +0 -0
  313. package/web/dist/assets/lxgwwenkaiscreen-subset-67-BVuOnayH.woff2 +0 -0
  314. package/web/dist/assets/lxgwwenkaiscreen-subset-68-DpxQ6Mnx.woff2 +0 -0
  315. package/web/dist/assets/lxgwwenkaiscreen-subset-69-qesBxLmC.woff2 +0 -0
  316. package/web/dist/assets/lxgwwenkaiscreen-subset-70-BBt1xN7h.woff2 +0 -0
  317. package/web/dist/assets/lxgwwenkaiscreen-subset-71-BWqZacwB.woff2 +0 -0
  318. package/web/dist/assets/lxgwwenkaiscreen-subset-72-B1wP_Yst.woff2 +0 -0
  319. package/web/dist/assets/lxgwwenkaiscreen-subset-73-C99T7jNZ.woff2 +0 -0
  320. package/web/dist/assets/lxgwwenkaiscreen-subset-74-DrUTsWBe.woff2 +0 -0
  321. package/web/dist/assets/lxgwwenkaiscreen-subset-75-C4M5XXQe.woff2 +0 -0
  322. package/web/dist/assets/lxgwwenkaiscreen-subset-76-Bq8spVWF.woff2 +0 -0
  323. package/web/dist/assets/lxgwwenkaiscreen-subset-77-D9vNupE2.woff2 +0 -0
  324. package/web/dist/assets/lxgwwenkaiscreen-subset-78-D6th7Gnu.woff2 +0 -0
  325. package/web/dist/assets/lxgwwenkaiscreen-subset-79-bezM85cS.woff2 +0 -0
  326. package/web/dist/assets/lxgwwenkaiscreen-subset-80-DD8pnhG2.woff2 +0 -0
  327. package/web/dist/assets/lxgwwenkaiscreen-subset-81-N6jtSyBs.woff2 +0 -0
  328. package/web/dist/assets/lxgwwenkaiscreen-subset-82-D6c180bZ.woff2 +0 -0
  329. package/web/dist/assets/lxgwwenkaiscreen-subset-83-em17AadM.woff2 +0 -0
  330. package/web/dist/assets/lxgwwenkaiscreen-subset-84-CJ8gdrMK.woff2 +0 -0
  331. package/web/dist/assets/lxgwwenkaiscreen-subset-85-9PPGO1eB.woff2 +0 -0
  332. package/web/dist/assets/lxgwwenkaiscreen-subset-86-CjWPf-u5.woff2 +0 -0
  333. package/web/dist/assets/lxgwwenkaiscreen-subset-87-8D_X_yOC.woff2 +0 -0
  334. package/web/dist/assets/lxgwwenkaiscreen-subset-88-9vtKFPBx.woff2 +0 -0
  335. package/web/dist/assets/lxgwwenkaiscreen-subset-89-BhE5jAKX.woff2 +0 -0
  336. package/web/dist/assets/lxgwwenkaiscreen-subset-90-D2Npfm5E.woff2 +0 -0
  337. package/web/dist/assets/lxgwwenkaiscreen-subset-91-C6Ft1-Eu.woff2 +0 -0
  338. package/web/dist/assets/lxgwwenkaiscreen-subset-97-CaExyQP9.woff2 +0 -0
  339. package/web/dist/assets/lxgwwenkaiscreen-subset-98-CPoVbsUd.woff2 +0 -0
  340. package/web/dist/assets/lxgwwenkaiscreen-subset-99-ZUQZGVgH.woff2 +0 -0
  341. package/web/dist/assets/{mindmap-definition-FBJOCRG2-hUxSpdKn.js → mindmap-definition-FBJOCRG2-CalfL_Kn.js} +1 -1
  342. package/web/dist/assets/{pegDiagram-XKGWAZYB-aCaQPH0w.js → pegDiagram-XKGWAZYB-D9mVWz7l.js} +1 -1
  343. package/web/dist/assets/pending-edit-apply-kwMFWsUe.js +31 -0
  344. package/web/dist/assets/{pieDiagram-E7YTZNPT-CmOEZ8zg.js → pieDiagram-E7YTZNPT-DELzbZNS.js} +1 -1
  345. package/web/dist/assets/{quadrantDiagram-AXDQQJYC-VW6fAhsV.js → quadrantDiagram-AXDQQJYC-TZqp4mfA.js} +1 -1
  346. package/web/dist/assets/{railroadDiagram-O6MQD6OU-CKJ7Ba5K.js → railroadDiagram-O6MQD6OU-DxX59f0i.js} +1 -1
  347. package/web/dist/assets/{requirementDiagram-EFPCY7ZU-pNGkq9Vw.js → requirementDiagram-EFPCY7ZU-DFdsJLIe.js} +1 -1
  348. package/web/dist/assets/{rotate-cw-KAfChbGV.js → rotate-cw-62liz7LX.js} +1 -1
  349. package/web/dist/assets/{sankeyDiagram-P5KCCOFB-BIMmDTfZ.js → sankeyDiagram-P5KCCOFB-ByYcd3fG.js} +1 -1
  350. package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-BBW7wzKY.js → sequenceDiagram-WJ2MYXX4-Bj-ml1jS.js} +1 -1
  351. package/web/dist/assets/{sizeCapture-X5ZJPWSS-DdkXmLYR.js → sizeCapture-X5ZJPWSS-KhsXThyD.js} +1 -1
  352. package/web/dist/assets/{square-dashed-mouse-pointer-CDUlhUP7.js → square-dashed-mouse-pointer-D_VrqVpy.js} +1 -1
  353. package/web/dist/assets/{stateDiagram-HBIQ2CUA-W313Uz16.js → stateDiagram-HBIQ2CUA-BJlpHfWU.js} +1 -1
  354. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-CR2kYAH2.js +1 -0
  355. package/web/dist/assets/{swimlanes-XN3QIQJK-C29TL26K.js → swimlanes-XN3QIQJK-DUB9OJVs.js} +1 -1
  356. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BitOxnG2.js +8 -0
  357. package/web/dist/assets/{timeline-definition-24CTP7MA-CF0JMObX.js → timeline-definition-24CTP7MA-C03NX82A.js} +1 -1
  358. package/web/dist/assets/{vennDiagram-4TSXK5OY-DrDFsaH3.js → vennDiagram-4TSXK5OY-B9pjzngp.js} +1 -1
  359. package/web/dist/assets/{wardleyDiagram-VM6X3IG4-CVrnRIS_.js → wardleyDiagram-VM6X3IG4-UqWq_mcT.js} +1 -1
  360. package/web/dist/assets/{xychartDiagram-S5SC5T6Z-BvkN1zsT.js → xychartDiagram-S5SC5T6Z-D2pDKtOv.js} +1 -1
  361. package/web/dist/index.html +2 -2
  362. package/server/api/memory.js +0 -185
  363. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +0 -583
  364. package/server/engine/mcp/tools/arrange-on-board.js +0 -124
  365. package/server/engine/mcp/tools/create-on-board.js +0 -152
  366. package/server/engine/mcp/tools/edit-sketch.js +0 -167
  367. package/server/engine/mcp/tools/record-decision.js +0 -140
  368. package/server/engine/mcp/tools/relate-on-board.js +0 -112
  369. package/server/engine/mcp/tools/sketch-on-board.js +0 -337
  370. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +0 -235
  371. 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 +0 -78
  372. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +0 -268
  373. package/web/dist/assets/DeckWindow-BpLv5082.js +0 -16
  374. package/web/dist/assets/channel-DGkccMO1.js +0 -1
  375. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +0 -1
  376. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +0 -1
  377. package/web/dist/assets/index-C0KUf6Wd.css +0 -1
  378. package/web/dist/assets/index-GAamh1p4.js +0 -2152
  379. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  380. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  381. package/web/dist/assets/pending-edit-apply-AcdOQ9Ij.js +0 -31
  382. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +0 -1
  383. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +0 -8
@@ -0,0 +1,110 @@
1
+ /**
2
+ * server/projects/session-jsonl.js — SDK 会话转录(jsonl)的文件层(2026-08-30 拆出)。
3
+ *
4
+ * 这里只有"那个文件在哪、在不在、怎么安全地截短"这三件事,跟任何业务无关。
5
+ * 分开住是因为读它的人不止一个:回退要截断它,模型切换要靠它判断"这个会话跑过没有"
6
+ * (没跑过的会话没有历史,跨通路闸不该拦),以后想做分支浏览的也得从它读。
7
+ *
8
+ * 转录的位置由 SDK 定:`<CLAUDE_CONFIG_DIR>/projects/<encodeCwdForSDK(cwd)>/<sid>.jsonl`。
9
+ * 扁平化之后同一个项目所有会话共用一个 cwd,所以它们的 jsonl 都在同一个目录下、按 sid 分文件。
10
+ */
11
+
12
+ import path from 'path';
13
+ import { promises as fs } from 'fs';
14
+ import { encodeCwdForSDK } from './workspace.js';
15
+ import { platform } from '../runtime/platform.js';
16
+
17
+ const GLOBAL_CLAUDE_CONFIG_DIR = platform.claudeConfigDir;
18
+
19
+ /** 这个会话的转录文件路径。 */
20
+ export function sessionJsonlPath(sessionRoot, sid) {
21
+ return path.join(GLOBAL_CLAUDE_CONFIG_DIR, 'projects', encodeCwdForSDK(sessionRoot), `${sid}.jsonl`);
22
+ }
23
+
24
+ /** 转录在不在 —— 等价于"这个会话跑过至少一轮"。 */
25
+ export async function jsonlExistsForSession(sessionRoot, sid) {
26
+ try {
27
+ await fs.access(sessionJsonlPath(sessionRoot, sid));
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 对话层回滚(2026-08-08):把 jsonl 截断到 userMessageId 那条之前(含它与其后全部)。
36
+ * jsonl 是追加式日志,截到 prefix = 它历史上真实存在过的状态,resume 天然自洽;
37
+ * 之后的 file-history-snapshot 属于被撤销的编辑,一并丢弃是正确语义。
38
+ * 原子写(tmp+rename)。找不到该 uuid 返 null(fail-soft:文件回滚仍算成功)。
39
+ */
40
+ export async function truncateJsonlAtMessage(sessionRoot, sid, userMessageId) {
41
+ const jsonlPath = sessionJsonlPath(sessionRoot, sid);
42
+ try {
43
+ const raw = await fs.readFile(jsonlPath, 'utf8');
44
+ const lines = raw.split('\n');
45
+ let cut = -1;
46
+ for (let i = 0; i < lines.length; i++) {
47
+ if (!lines[i] || !lines[i].includes(userMessageId)) continue;
48
+ try { if (JSON.parse(lines[i]).uuid === userMessageId) { cut = i; break; } } catch { /* 非 JSON 行跳过 */ }
49
+ }
50
+ if (cut < 0) {
51
+ console.warn(`[sessions.rewind] uuid ${userMessageId.slice(0, 8)} 不在 jsonl 里,跳过对话截断`);
52
+ return null;
53
+ }
54
+ const kept = lines.slice(0, cut).join('\n');
55
+ const tmp = `${jsonlPath}.tmp-rewind`;
56
+ await fs.writeFile(tmp, kept ? `${kept}\n` : '');
57
+ await fs.rename(tmp, jsonlPath);
58
+ return lines.filter(Boolean).length - lines.slice(0, cut).filter(Boolean).length;
59
+ } catch (err) {
60
+ console.warn(`[sessions.rewind] jsonl 截断失败(不影响文件回滚):${err.message}`);
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 砍掉转录末尾的那条用户消息(连同它之后的一切)。
67
+ *
68
+ * 专给 fork 用。SDK 的 `forkSession(upToMessageId)` 是**含**那条的(08-30 探针实测:
69
+ * fork 到第二条用户消息,新转录里那条还在),而「从这里分叉」用户点的是**自己那句
70
+ * 说错的话** —— 含着它 fork 出来等于什么都没改。所以 fork 完再补这一刀。
71
+ *
72
+ * 为什么不能拿原来那个 uuid 去截:fork 会 **remap 每条消息的 uuid**(SDK 文档原话
73
+ * "remapping every message UUID"),原 id 在新文件里根本不存在。而 fork 既然截止在
74
+ * 那条消息,它必然就是新转录里最后一条**真**用户消息 —— tool_result 也顶着
75
+ * type='user' 的壳,所以要把那种排掉,认的是带 text block(或纯字符串正文)的那条。
76
+ *
77
+ * @returns {Promise<number|null>} 删掉的行数;没找到用户消息返回 null(fail-soft)
78
+ */
79
+ export async function truncateJsonlAtLastUserMessage(sessionRoot, sid) {
80
+ const jsonlPath = sessionJsonlPath(sessionRoot, sid);
81
+ try {
82
+ const raw = await fs.readFile(jsonlPath, 'utf8');
83
+ const lines = raw.split('\n');
84
+ let cut = -1;
85
+ for (let i = lines.length - 1; i >= 0; i--) {
86
+ if (!lines[i]) continue;
87
+ let o;
88
+ try { o = JSON.parse(lines[i]); } catch { continue; }
89
+ if (o.type !== 'user') continue;
90
+ const c = o.message?.content;
91
+ const isRealUserText = typeof c === 'string'
92
+ || (Array.isArray(c) && c.some((b) => b?.type === 'text'));
93
+ if (!isRealUserText) continue; // tool_result 也是 type='user'
94
+ cut = i;
95
+ break;
96
+ }
97
+ if (cut < 0) {
98
+ console.warn(`[fork] ${sid.slice(0, 8)} 的转录里找不到用户消息,跳过补刀`);
99
+ return null;
100
+ }
101
+ const kept = lines.slice(0, cut).join('\n');
102
+ const tmp = `${jsonlPath}.tmp-fork`;
103
+ await fs.writeFile(tmp, kept ? `${kept}\n` : '');
104
+ await fs.rename(tmp, jsonlPath);
105
+ return lines.filter(Boolean).length - lines.slice(0, cut).filter(Boolean).length;
106
+ } catch (err) {
107
+ console.warn(`[fork] 转录补刀失败:${err.message}`);
108
+ return null;
109
+ }
110
+ }
@@ -82,6 +82,15 @@ if (!projectsColNames.has('auto_named')) {
82
82
  console.log('[projects/store] projects.auto_named column added');
83
83
  }
84
84
 
85
+ // 模式分离(2026-08-27):design(设计工作台,现状)vs rp(演出:常驻角色演故事)。
86
+ // 两种模式共用全部基础设施(画布/黑板/会话/精灵),差异只在提示词分区与工具面
87
+ // (对照表在 engine/mcp/mode-profile.js 一份)。默认 'design' 让全部存量项目零行为变化。
88
+ // 会话启动时读一次 —— 切模式**下个会话生效**,跟"改人设下次上场生效"同一节奏。
89
+ if (!projectsColNames.has('mode')) {
90
+ db.exec("ALTER TABLE projects ADD COLUMN mode TEXT NOT NULL DEFAULT 'design'");
91
+ console.log('[projects/store] projects.mode column added (default design)');
92
+ }
93
+
85
94
  // 多用户内测(2026-07-30):项目归属。NULL 的存量行由 bootstrapAuth() 回填 admin
86
95
  if (!projectsColNames.has('owner_id')) {
87
96
  db.exec('ALTER TABLE projects ADD COLUMN owner_id TEXT');
@@ -151,6 +160,7 @@ function rowToProject(row) {
151
160
  skillId: row.skill_id,
152
161
  description: row.description || null,
153
162
  kind: row.kind || 'project',
163
+ mode: row.mode || 'design',
154
164
  autoNamed: !!row.auto_named,
155
165
  ownerId: row.owner_id || null,
156
166
  activeSessionId: row.active_session_id,
@@ -204,6 +214,7 @@ export function createProject({
204
214
  skillId = 'deskskill-engine-mini',
205
215
  description = null,
206
216
  kind = 'project',
217
+ mode = 'design',
207
218
  autoNamed = false,
208
219
  ownerId = null,
209
220
  }) {
@@ -211,15 +222,18 @@ export function createProject({
211
222
  if (kind !== 'project' && kind !== 'quick') {
212
223
  throw new Error(`createProject: kind 非法 (${kind})`);
213
224
  }
225
+ if (mode !== 'design' && mode !== 'rp') {
226
+ throw new Error(`createProject: mode 非法 (${mode})`);
227
+ }
214
228
  const id = newProjectId();
215
229
  const desc = (typeof description === 'string' && description.trim()) ? description.trim() : null;
216
230
  db.prepare(
217
- `INSERT INTO projects (id, name, skill_id, description, kind, auto_named, owner_id) VALUES (?, ?, ?, ?, ?, ?, ?)`,
218
- ).run(id, name.trim(), skillId, desc, kind, autoNamed ? 1 : 0, ownerId);
231
+ `INSERT INTO projects (id, name, skill_id, description, kind, mode, auto_named, owner_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
232
+ ).run(id, name.trim(), skillId, desc, kind, mode, autoNamed ? 1 : 0, ownerId);
219
233
  return getProject(id);
220
234
  }
221
235
 
222
- /** 更新(仅允许 name / skill_id / description / active_session_id / kind / autoNamed) */
236
+ /** 更新(仅允许 name / skill_id / description / active_session_id / kind / mode / autoNamed) */
223
237
  export function updateProject(id, patch) {
224
238
  validateProjectId(id);
225
239
  // 用户显式改名 = 这名字他自己定了,系统不再拿摘要覆盖
@@ -228,12 +242,16 @@ export function updateProject(id, patch) {
228
242
  if ('kind' in patch && patch.kind !== 'project' && patch.kind !== 'quick') {
229
243
  throw new Error(`updateProject: kind 非法 (${patch.kind})`);
230
244
  }
245
+ if ('mode' in patch && patch.mode !== 'design' && patch.mode !== 'rp') {
246
+ throw new Error(`updateProject: mode 非法 (${patch.mode})`);
247
+ }
231
248
  const map = {
232
249
  name: 'name',
233
250
  skillId: 'skill_id',
234
251
  description: 'description',
235
252
  activeSessionId: 'active_session_id',
236
253
  kind: 'kind',
254
+ mode: 'mode',
237
255
  autoNamed: 'auto_named',
238
256
  };
239
257
  const sets = [];
@@ -264,6 +282,41 @@ export function setActiveSession(projectId, sessionId) {
264
282
  }
265
283
 
266
284
  /** 列 project 关联的 runs(用于级联清理) */
285
+ /**
286
+ * 两个模式各自的用量(控制台那两张卡)。
287
+ *
288
+ * ⛔ **排掉站主自己**:他一个人的设计项目就比所有用户加起来还多,混进去这两个数
289
+ * 就只是在量他自己。同仓口径,见 feedback-creative-tool-metrics。
290
+ *
291
+ * ⚠️ 两条查询分开跑不合并:runs 要 join run_model_usage 才拿得到钱,而那张表
292
+ * 一个 run 有多行(一轮里换过几个模型就几行),并到项目那条里会把 projects
293
+ * 乘出好几份。COUNT(DISTINCT) 能救回来,但读的人得先看出这里有个陷阱。
294
+ *
295
+ * @returns {Array<{mode, projects, users, runs, costUsd, firstAt}>}
296
+ */
297
+ export function modeStats() {
298
+ const HUMANS = "p.owner_id IN (SELECT id FROM users WHERE role <> 'admin')";
299
+ const out = new Map();
300
+ for (const r of db.prepare(
301
+ `SELECT p.mode AS mode, COUNT(*) AS projects, COUNT(DISTINCT p.owner_id) AS users,
302
+ MIN(p.created_at) AS firstAt
303
+ FROM projects p WHERE ${HUMANS} GROUP BY p.mode`,
304
+ ).all()) out.set(r.mode, { ...r, runs: 0, costUsd: 0 });
305
+
306
+ for (const r of db.prepare(
307
+ `SELECT p.mode AS mode, COUNT(DISTINCT r.id) AS runs,
308
+ COALESCE(SUM(m.cost_usd), 0) AS costUsd
309
+ FROM runs r
310
+ JOIN projects p ON p.id = r.project_id
311
+ LEFT JOIN run_model_usage m ON m.run_id = r.id
312
+ WHERE ${HUMANS} GROUP BY p.mode`,
313
+ ).all()) {
314
+ const e = out.get(r.mode) || { mode: r.mode, projects: 0, users: 0, firstAt: null };
315
+ out.set(r.mode, { ...e, runs: r.runs, costUsd: r.costUsd });
316
+ }
317
+ return [...out.values()];
318
+ }
319
+
267
320
  export function listRunsForProject(projectId) {
268
321
  validateProjectId(projectId);
269
322
  return db.prepare('SELECT id FROM runs WHERE project_id = ? ORDER BY created_at DESC').all(projectId);
@@ -26,8 +26,9 @@ export const DEFAULT_UI_CONFIG = Object.freeze({
26
26
  * 黑板模式(2026-08-23):用户专注头脑风暴时,画布取代侧栏成为主交互窗口 ——
27
27
  * agent 每轮的主体内容落画布(sketch_on_board / edit_sketch),聊天里只留一两句;
28
28
  * 草图落定时服务端广播 board.focus,前端把镜头带过去。
29
+ * 2026-08-24 用户拍板默认开(老项目没写过这个键的也按开算;关过的尊重文件里的 false)。
29
30
  */
30
- blackboard_mode: false,
31
+ blackboard_mode: true,
31
32
  });
32
33
 
33
34
  /**
@@ -30,9 +30,27 @@ function sanitizeViewpoint(raw, userId) {
30
30
  } : null;
31
31
  if (camera && Object.values(camera).some(v => v === null)) return null;
32
32
  const str = (v, n) => (typeof v === 'string' && v.length <= n ? v : null);
33
+ /**
34
+ * 设备档(2026-08-28 移动端第二轮)。**浏览器判好了报上来**,这边只验不算 ——
35
+ * 真屏幕多大、是不是手指,只有浏览器知道;服务端手里只有相机矩形和缩放,
36
+ * 反推屏幕要除以缩放,而且分不清「390 宽的手机」和「拖窄了的桌面窗口」。
37
+ * 认不出的档一律当桌面:宁可给手机用户一份桌面版式(能用,只是要缩放),
38
+ * 也别给桌面用户一份 342 宽的窄条(那是坏的)。
39
+ */
40
+ const dc = raw.device && typeof raw.device === 'object' ? raw.device : null;
41
+ const dw = num(dc?.w, 200, 8000); const dh = num(dc?.h, 200, 8000);
42
+ // ⚠️ 夹持在这儿是错的判法:一个夹出来的 8000 会变成 8000 宽的版式建议。
43
+ // 尺寸不在真实屏幕的范围里就**整个丢掉 device**,退回相机×缩放那条老路。
44
+ const device = (dc && dw !== null && dh !== null && dw === Number(dc.w) && dh === Number(dc.h)) ? {
45
+ class: (dc.class === 'phone' || dc.class === 'tablet') ? dc.class : 'desktop',
46
+ w: dw, h: dh,
47
+ dpr: num(dc.dpr, 0.5, 8),
48
+ coarse: !!dc.coarse,
49
+ } : null;
33
50
  return {
34
51
  userId: userId || null,
35
52
  camera,
53
+ device,
36
54
  zoom: num(raw.zoom, 0.01, 100),
37
55
  layer: str(raw.layer, 300) || '',
38
56
  openWindow: str(raw.openWindow, 300),
@@ -56,18 +74,32 @@ export function getViewpoint(pid) {
56
74
  return v;
57
75
  }
58
76
 
77
+ /** 档位的人话(注入和工具共用一份措辞;桌面不说 —— 那是默认情况,说了是噪音) */
78
+ export function describeDevice(v) {
79
+ const d = v?.device;
80
+ if (!d || d.class === 'desktop') return null;
81
+ const name = d.class === 'phone' ? '手机' : '平板';
82
+ return `${name}(屏幕 ${d.w}x${d.h}px,触屏)`;
83
+ }
84
+
59
85
  /** 一行人话(注入/工具共用一份措辞)。rects 可选:给了就报视口里有什么 */
60
86
  export function describeViewpoint(v, rects = null) {
61
87
  if (!v) return null;
62
88
  const bits = [];
89
+ const dev = describeDevice(v);
90
+ if (dev) bits.push(`用的是${dev}`);
63
91
  if (v.layer) bits.push(`在文件夹「${v.layer}」里`);
64
92
  if (v.camera) {
65
93
  bits.push(`视口 (${Math.round(v.camera.x)},${Math.round(v.camera.y)}) ${Math.round(v.camera.w)}x${Math.round(v.camera.h)}${v.zoom ? ` 缩放 ${Number(v.zoom).toFixed(2)}` : ''}`);
66
94
  if (rects) {
67
95
  const c = v.camera;
68
- const inside = rects.filter(r => !(r.x + r.w < c.x || r.x > c.x + c.w || r.y + r.h < c.y || r.y > c.y + c.h)).map(r => r.id);
69
- if (inside.length) bits.push(`视口里有 ${inside.length} 件:${inside.slice(0, 10).join('、')}${inside.length > 10 ? ' 等' : ''}`);
70
- else bits.push('视口里是空地');
96
+ const inside = rects.filter(r => !(r.x + r.w < c.x || r.x > c.x + c.w || r.y + r.h < c.y || r.y > c.y + c.h));
97
+ // 带坐标和占位(08-27 用户提):agent 摆放要知道视口里谁占了哪,不该再专门调工具去问
98
+ if (inside.length) {
99
+ bits.push(`视口里 ${inside.length} 件(id@(x,y)宽x高):${inside.slice(0, 12)
100
+ .map(r => `${r.id}@(${Math.round(r.x)},${Math.round(r.y)})${Math.round(r.w)}x${Math.round(r.h)}`)
101
+ .join('、')}${inside.length > 12 ? ' 等' : ''}`);
102
+ } else bits.push('视口里是空地');
71
103
  }
72
104
  }
73
105
  if (v.openWindow) bits.push(`开着窗:${v.openWindow}${v.openPage ? `(${v.openPage})` : ''}`);
@@ -0,0 +1,30 @@
1
+ /**
2
+ * projects/workspace-slots.js —— 角色位铺装(2026-08-28 建;08-29 收成一个位)
3
+ *
4
+ * 预注册的角色子代理定义(rp-role)**必须在会话启动前就在盘上**:会话中途写入的
5
+ * agent 定义只有当时活着的那个 CLI 进程认得,换进程后永久 not found(08-28 三场
6
+ * 真会话勘定,机制见 engine/agent/cast.js)。所以铺装挂在 ensureProjectWorkspace 里,
7
+ * 跟着每次 ensure 走。
8
+ *
9
+ * 内容以代码为准:教义升级后重启即生效,也顺手把模型对定义体的手改抹平
10
+ * (判据不建在模型可写的东西上 —— 派发闸照读文件核 tools,这里保证文件是正版)。
11
+ *
12
+ * 退役的旧位(rp-actor / rp-narrator)在这里**删文件**:留着的话老名字仍然派得出去,
13
+ * 派出来的还是旧分工(写场面的那支笔已经收回主持人手里了)。
14
+ */
15
+ import { promises as fs } from 'fs';
16
+ import path from 'path';
17
+ import { ROLE_SLOT, RETIRED_SLOTS, slotAgentFile } from '../engine/agent/cast.js';
18
+ import { MCP_SERVER_NAME } from '../engine/mcp/server-name.js';
19
+
20
+ export async function ensureActorSlots(root) {
21
+ const dir = path.join(root, '.claude', 'agents');
22
+ const file = path.join(dir, `${ROLE_SLOT}.md`);
23
+ const want = slotAgentFile(MCP_SERVER_NAME);
24
+ let have = null;
25
+ try { have = await fs.readFile(file, 'utf8'); } catch { /* 还没有 */ }
26
+ if (have !== want) await fs.writeFile(file, want, 'utf8');
27
+ for (const old of RETIRED_SLOTS) {
28
+ try { await fs.unlink(path.join(dir, `${old}.md`)); } catch { /* 本来就没有 */ }
29
+ }
30
+ }
@@ -36,18 +36,50 @@ export const DEFAULT_SPEC_JSON = JSON.stringify(
36
36
  null, 2,
37
37
  ) + '\n';
38
38
 
39
- export const DEFAULT_CLAUDE_MD = `# Project Instructions
39
+ // 2026-08-24 记忆体系改版:CLAUDE.md 挪到工作区根(画布可见),承载三类
40
+ // **人工筛选过的**长期事实——项目指引 / 风格档案 / 用户习惯。每会话被 SDK
41
+ // 确定性全量注入(settingSources 'project' 原生行为,二进制实证根目录也读)。
42
+ // 生长中的记忆(agent 自动记)住 记忆/,两层各干各的。
43
+ export const DEFAULT_CLAUDE_MD = `# 项目档案
40
44
 
41
- This file is read by the AI agent at the start of every session as part of its
42
- system prompt. Write project-specific guidance here — design intent,
43
- constraints, vocabulary, must-do / must-not-do.
45
+ 这份文件每次会话都会完整进入 agent 的上下文。放**定了就不常变**的东西;
46
+ 会生长的记忆 agent 自己记在 记忆/ 里。改这份文件里的硬约束要经用户点头。
44
47
 
45
- The agent will see this verbatim. Keep it concise and actionable.
48
+ ## 项目指引
49
+ (设计意图、硬约束、必须做/不许做。例:主色永远不用红色。)
46
50
 
47
- ## Examples
48
- - Design tone: minimal, editorial, generous whitespace
49
- - Hard constraints: never use red as a primary color
50
- - Vocabulary: refer to the user as "the team"
51
+ ## 风格档案
52
+ (定案的视觉锚:调色板 / 字体 / 材质 / 艺术方向。)
51
53
 
52
- (Edit this file from the NoDesign UI — the agent picks up changes on next session.)
54
+ ## 用户习惯
55
+ (称呼、语气、工作方式偏好。)
53
56
  `;
57
+
58
+ // rp(演出)项目的档案模板(2026-08-28;08-30 随角色子代理停用改口径)。
59
+ // ⚠️ 原注:这份文件**每个角色子代理也强制吃**(项目 CLAUDE.md 进所有子代理上下文,
60
+ // omitClaudeMd 透不过去 —— 08-26 实测)。08-30 起主 agent 代演全场,暂时没有子代理
61
+ // 在读它,但这条约束等角色回来还成立,别按"只有一个读者"重排栏目。
62
+ // 详细设定别塞 —— 放世界书文件按需 grep,这份是每轮都重发的常驻开销。
63
+ export const DEFAULT_CLAUDE_MD_RP = `# 演出档案
64
+
65
+ 这份文件每次会话都完整进入 agent 的上下文。放**定了就不常变**的东西;
66
+ 会生长的记忆 agent 自己记在 记忆/ 里。详细设定放世界书文件(用到时 grep),
67
+ 别塞进这里 —— 这份是每轮都重发的常驻开销。改硬约束要经用户点头。
68
+
69
+ ## 世界观锚
70
+ (一句话定调:什么世界、什么时代、什么基调。例:低魔剑与魔法,冒险者公会体系。
71
+ 从酒馆世界书搬来的常驻条目也放这,十条以内 —— 其余降级成触发文件。)
72
+
73
+ ## 台面规矩
74
+ (这一场怎么玩:想跟谁演(一对一 / 一群人)/ 难度档(爽·写实·严酷)/
75
+ 代笔档(全代入·大纲)/ 骰子规则 / 文风与叙事禁令 / 禁区题材。)
76
+
77
+ ## 人物
78
+ (在场人物一览:每人三行核心 —— 是谁、什么关系、绝不做什么。这里是名录;
79
+ 每个人完整的人设在 角色/<名>/角色卡.md(cast_role 写的那张),写他的话之前先读。)
80
+
81
+ ## 称呼与习惯
82
+ (用户扮演谁、怎么称呼;语气与节奏偏好。)
83
+ `;
84
+ // ⚠️ 上面四节的节名是「演出档案骨架」的唯一真相 —— blackboard-rp SKILL 的酒馆导入
85
+ // 落点表按同一套节名指路(08-28 对账:此前两处各写一套骨架,字段对不上)。改节名两处同改。
@@ -6,12 +6,9 @@
6
6
  * 结构:
7
7
  * <PROJECTS_DATA_ROOT>/<projectId>/
8
8
  * ├── shared/ ← **项目工作区**:agent 的 cwd,产物的家
9
- * │ ├── .claude/
10
- * │ ├── CLAUDE.md 项目指引(用户写)
11
- * │ ├── settings.json 项目 SDK config
12
- * │ │ ├── skills/ agents/ 项目级 skill / 子代理
13
- * │ │ ├── agent-memory/ 长期记忆(agent 写)
14
- * │ │ └── projects/ SDK 转录落点(.gitignore)
9
+ * │ ├── CLAUDE.md 项目档案(指引/风格/习惯;08-24 挪到根,画布可见)
10
+ * │ ├── 记忆/ SDK auto-memory(08-24 起,画布可见)
11
+ * │ ├── .claude/ settings.json · skills/ agents/ · projects/(SDK 转录)
15
12
  * │ ├── assets/ 上传素材 + 生成图
16
13
  * │ ├── .nd/<sid>/ 会话私档(spec.json / design-plan.md)
17
14
  * │ ├── .git/ 项目历史
@@ -45,18 +42,16 @@ import { spawn } from 'child_process';
45
42
  import path from 'path';
46
43
  import { fileURLToPath } from 'url';
47
44
  import { mutex } from 'async-mutex-lite';
48
- import { validateProjectId } from './store.js';
45
+ import { validateProjectId, getProject } from './store.js';
49
46
  import { resolveModelContextWindow } from '../engine/agent/model-context.js';
47
+ import { ensureActorSlots } from './workspace-slots.js';
50
48
 
51
49
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
52
50
 
53
51
  /**
54
- * Per-sessionRoot read-modify-write 串行 utility
55
- *
56
- * 解决:record_decision / expose_tweaks / PostCompact 3 处都 readFile→mutate→writeFile
57
- * 同 spec.json,无锁 → 后写者 silent 覆盖前写者全量内容("刚 record 的 decision 没了"
58
- * 类 bug)。三处统一通过本 helper 写,async-mutex-lite 是 module-singleton,所有
59
- * import 共享 lock map,按 key 串行。
52
+ * Per-sessionRoot read-modify-write 串行 utility(spec.json)。
53
+ * 08-24 起唯一写入方 = 暂退役的 expose_tweaks(决策贴/PostCompact 摘要已拆);
54
+ * 锁留着 —— tweaks 升级回归时并发结构不变。
60
55
  *
61
56
  * @param {string} workspaceRoot - sessions/<sid>/ 路径
62
57
  * @param {(spec: object) => (object|void|Promise<object|void>)} mutator
@@ -93,8 +88,9 @@ export function validateSessionId(sid) {
93
88
  }
94
89
 
95
90
  import {
96
- DEFAULT_GITIGNORE, DEFAULT_SPEC_JSON, DEFAULT_CLAUDE_MD,
91
+ DEFAULT_GITIGNORE, DEFAULT_CLAUDE_MD, DEFAULT_CLAUDE_MD_RP,
97
92
  } from './workspace-templates.js';
93
+ import { migrateMemoryLayout } from './memory-migration.js';
98
94
 
99
95
  /**
100
96
  * NoDesign 全局默认 settings.json — 代码是 source of truth。
@@ -224,16 +220,11 @@ export function getSessionMetaDir(projectId, sessionId) {
224
220
  // ── ensure ──
225
221
 
226
222
  /**
227
- * 创建项目工作区(幂等)。完成后保证:
228
- * - .claude/{CLAUDE.md, settings.json} 模板写入(仅不存在时)
229
- * - .claude/{skills, agents, agent-memory} 目录存在
230
- * - assets/ 存在、.gitignore 写入
231
- * - 旧的三层结构(tasks/ + per-session 沙盒)已扁平化
232
- * - .git 存在(项目级历史;扁平化前是 per-session 的)
233
- *
234
- * 扁平化跟着 ensure 走而不是单独一次性脚本:跟 removeRootLegacyArtifacts 同一个
235
- * 范式 —— 幂等、按项目惰性触发、跑过一次之后是 no-op。这样线上不需要停机窗口,
236
- * 也不存在"迁移脚本漏了哪个项目"。
223
+ * 创建项目工作区(幂等)。完成后保证:.claude/{CLAUDE.md, settings.json} 模板
224
+ * (仅不存在时)、.claude/{skills, agents, agent-memory} 与 assets/ 目录、演员位
225
+ * 定义、.gitignore、旧三层结构已扁平化、.git 存在(项目级历史)。
226
+ * 扁平化跟着 ensure 走而不是一次性脚本:幂等、按项目惰性触发、跑过即 no-op ——
227
+ * 线上不需要停机窗口,也不存在"迁移脚本漏了哪个项目"。
237
228
  */
238
229
  export async function ensureProjectWorkspace(projectId) {
239
230
  await removeRootLegacyArtifacts(projectId);
@@ -244,11 +235,19 @@ export async function ensureProjectWorkspace(projectId) {
244
235
  await fs.mkdir(path.join(root, '.claude', 'agent-memory'), { recursive: true });
245
236
  await fs.mkdir(path.join(root, 'assets'), { recursive: true });
246
237
 
238
+ // 演员位(2026-08-28 重构):预注册 RP 子代理定义,必须先于任何会话在盘上(拆件见文件)
239
+ await ensureActorSlots(root);
240
+
247
241
  // .gitignore 每次比对而不是"不存在才写":扁平化新增了 .claude/projects/ 和
248
242
  // .nd/ 两条,老项目的文件里没有,不补的话 SDK 转录会被 commit 进项目历史。
249
243
  await ensureGitignore(path.join(root, '.gitignore'));
250
- if (!(await fileExists(path.join(root, '.claude', 'CLAUDE.md')))) {
251
- await fs.writeFile(path.join(root, '.claude', 'CLAUDE.md'), DEFAULT_CLAUDE_MD, 'utf8');
244
+ // 记忆体系改版迁移(2026-08-24,幂等:源不在了就什么都不做;三步全是"搬走后源删除")
245
+ // —— CLAUDE.md 挪工作区根、旧偏好/风格档案并入、auto-memory 存量搬 记忆/。细节见函数文档。
246
+ await migrateMemoryLayout(root, { fileExists });
247
+ if (!(await fileExists(path.join(root, 'CLAUDE.md')))) {
248
+ // rp 项目的档案按"戏"设栏(templates 注释);已有 CLAUDE.md 的一字不动,用户内容优先于模板换代
249
+ const isRp = (getProject(projectId)?.mode || 'design') === 'rp';
250
+ await fs.writeFile(path.join(root, 'CLAUDE.md'), isRp ? DEFAULT_CLAUDE_MD_RP : DEFAULT_CLAUDE_MD, 'utf8');
252
251
  }
253
252
  // settings.json:每次 merge defaults 让代码层 default 升级时现存 project 自动跟上
254
253
  // (用户字段优先,缺失的 NoDesign default 字段补进去)
@@ -17,6 +17,9 @@ import fs from 'node:fs';
17
17
  import os from 'node:os';
18
18
  import path from 'node:path';
19
19
  import { isAvailable as rembgAvailable, REMBG_SETUP_HINT } from '../engine/mcp/tools/helpers/rembg.js';
20
+ // 盒子开没开机只有一个判据 —— h3box-ssh.localBoxEnabled。这里曾经自己写了一遍
21
+ // `=== 'on'`,跟那边 `!== 'off'`(默认开)的默认值正好相反,同一个变量两套读法。
22
+ import { localBoxEnabled } from '../engine/mcp/tools/h3box-ssh.js';
20
23
 
21
24
  const isWin = process.platform === 'win32';
22
25
 
@@ -96,8 +99,9 @@ export const CAPABILITY_DEFS = Object.freeze([
96
99
  } },
97
100
  { id: 'localBox', kind: 'service', level: 'feature', label: '本地 GPU 盒子(paint_still / roll_film)', uses: '自部署生图 / 文生视频',
98
101
  fix: 'NODESIGN_LOCAL_BOX=on + NODESIGN_H3BOX_SSH(站主自己的 5090 盒子;一般用户用不上)',
99
- probe: () => (process.env.NODESIGN_LOCAL_BOX === 'on' && process.env.NODESIGN_H3BOX_SSH
100
- ? { available: true, detail: process.env.NODESIGN_H3BOX_SSH } : { available: false, detail: process.env.NODESIGN_LOCAL_BOX === 'on' ? 'NODESIGN_H3BOX_SSH 为空' : 'NODESIGN_LOCAL_BOX 未开' }) },
102
+ probe: () => (localBoxEnabled() && process.env.NODESIGN_H3BOX_SSH
103
+ ? { available: true, detail: process.env.NODESIGN_H3BOX_SSH }
104
+ : { available: false, detail: !localBoxEnabled() ? '站主标记为关机(NODESIGN_LOCAL_BOX=off)' : 'NODESIGN_H3BOX_SSH 为空' }) },
101
105
  ]);
102
106
 
103
107
  function bin(name, extra = []) {
@@ -32,7 +32,7 @@ import { fileURLToPath } from 'node:url';
32
32
  import path from 'node:path';
33
33
  import http from 'node:http';
34
34
  import { promises as fs, readFileSync, writeFileSync } from 'node:fs';
35
- import { REMBG_VENV_PYTHON, REMBG_SETUP_HINT } from '../engine/mcp/tools/helpers/rembg.js';
35
+ import { REMBG_VENV_PYTHON, REMBG_SETUP_HINT, resolveRembgSocket } from '../engine/mcp/tools/helpers/rembg.js';
36
36
 
37
37
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
38
  // services/ 在 server/services/,server root 上溯 1 层
@@ -46,7 +46,10 @@ const DEFAULT_SERVICE = path.join(__dirname, 'rembg-service.py');
46
46
  // 改回多模型预热:env NODESIGN_REMBG_PRELOAD=isnet-general-use,birefnet-general-lite
47
47
  const DEFAULT_PRELOAD = 'isnet-general-use';
48
48
 
49
- const DEFAULT_SOCKET = '/tmp/nodesign-rembg.sock';
49
+ // 按端口派生的实例私有 socket(真相源在 helpers/rembg.js 的 resolveRembgSocket;
50
+ // 08-24 案:prod/exp 共用一个默认路径时,killStaleServices 按 socket 判"自己人"
51
+ // 形同虚设,exp 一启动就把生产的抠图服务当 stale 杀掉再自己占坑)
52
+ const DEFAULT_SOCKET = resolveRembgSocket();
50
53
  const MAX_RSS_MB = Number(process.env.NODESIGN_REMBG_MAX_RSS_MB ?? 900);
51
54
  const WATCH_MS = Number(process.env.NODESIGN_REMBG_WATCH_MS ?? 60_000);
52
55
 
@@ -0,0 +1,70 @@
1
+ /**
2
+ * server/shared/locales.js — 支持的界面语言(服务端真相源,2026-08-26)
3
+ *
4
+ * 服务端要这份表做三件事:校验 `users.locale` 的写入、按语言挑用户可见的报错、
5
+ * 给 agent 注语言指令。前端另有一份带显示名的 `web/src/lib/i18n.js` —— 那份要
6
+ * 「中文 / English」这种给人看的标签,塞进服务端没意义。
7
+ *
8
+ * 双份表的宿命是漂移,所以配了 `web/src/lib/i18n-locales.parity.test.js` 逐项对账
9
+ * (跟 board-kind-sizes 那对表同款纪律)。加语言要同时改两处,忘了就红。
10
+ */
11
+
12
+ /** 语言 id。第一项是默认值 —— 这个产品是中文优先的,英文是第二语言 */
13
+ export const LOCALES = Object.freeze(['zh-CN', 'en']);
14
+
15
+ export const DEFAULT_LOCALE = LOCALES[0];
16
+
17
+ export function isLocale(id) {
18
+ return LOCALES.includes(id);
19
+ }
20
+
21
+ /**
22
+ * 把语言 tag 归一到支持的语言,认不出返回 null。
23
+ * `zh` / `zh-Hans` / `zh-TW` 全落 zh-CN(繁体没单独做,先给中文别给英文)。
24
+ */
25
+ export function normalizeLocale(tag) {
26
+ if (!tag) return null;
27
+ const s = String(tag).toLowerCase();
28
+ if (s.startsWith('zh')) return 'zh-CN';
29
+ if (s.startsWith('en')) return 'en';
30
+ return null;
31
+ }
32
+
33
+ /**
34
+ * 解析 HTTP `Accept-Language`,按 q 值排序取第一个认识的。
35
+ * 拿不到就返回 null —— 由调用方决定落什么默认,这里不替它做主。
36
+ *
37
+ * 例:`en-US,en;q=0.9,zh-CN;q=0.8` → 'en'
38
+ */
39
+ export function localeFromAcceptLanguage(header) {
40
+ if (!header) return null;
41
+ const ranked = String(header).split(',')
42
+ .map((part) => {
43
+ const [tag, ...params] = part.trim().split(';');
44
+ const q = params.find((p) => p.trim().startsWith('q='));
45
+ return { tag: tag.trim(), q: q ? parseFloat(q.split('=')[1]) || 0 : 1 };
46
+ })
47
+ .filter((x) => x.tag)
48
+ .sort((a, b) => b.q - a.q);
49
+ for (const { tag } of ranked) {
50
+ const hit = normalizeLocale(tag);
51
+ if (hit) return hit;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Accept-Language 里有没有**至少一个像语言 tag 的东西**(2026-08-29)。
58
+ *
59
+ * 用来区分两种"认不出":
60
+ * `ja,ko;q=0.9` —— 人家明确说了自己读日语韩语,只是我们没有这两门 → 给英文
61
+ * `???` / `;;;` / `*` / 空 —— 什么都没说清 → 中文优先照旧
62
+ * 前一种落回中文等于递给他一页看不懂的字;后一种没有任何证据,不该乱猜。
63
+ */
64
+ export function statesAnyLanguage(header) {
65
+ if (!header) return false;
66
+ return String(header).split(',').some((part) => {
67
+ const tag = part.trim().split(';')[0].trim();
68
+ return tag !== '*' && /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i.test(tag);
69
+ });
70
+ }