@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
@@ -18,17 +18,19 @@ import { guardProject } from './_guard.js';
18
18
  import {
19
19
  getSharedDir, ensureProjectWorkspace, removeSessionWorkspace, commitWorkspace,
20
20
  } from '../projects/workspace.js';
21
- import { patchBoard, readBoard, reconcileBoardRenames, forwardId, forwardPath, renameBoardPaths } from '../projects/board-store.js';
21
+ import { patchBoard, readBoard, reconcileBoardRenames, pruneDanglingBindings, forwardId, forwardPath, renameBoardPaths } from '../projects/board-store.js';
22
22
  import { moveEntry, MoveError } from '../projects/move-entry.js';
23
23
  import { reconcileAutoRefsThrottled } from '../lib/auto-relations.js';
24
24
  import { taskManifest, ENTRY_FILE, KIND_SITE, docxClaimedFiles, isDirArtifact } from '../lib/artifact-target.js';
25
- import { RESERVED_DIRS, HARD_IGNORE_DIRS, DRAFTS_DIR, isReservedFile } from '../lib/task-scan.js';
25
+ import { RESERVED_DIRS, HARD_IGNORE_DIRS, DRAFTS_DIR, isReservedFile, loadIgnore } from '../lib/task-scan.js';
26
+ import { OUTPUT_DIRS } from '../lib/kinds/site.js';
26
27
  import { listReferences } from '../lib/reference-assets.js';
27
28
  import { resolveArtifactFile, isServablePath } from '../lib/artifact-file-path.js';
29
+ import { USER_UPLOAD_DIR, ensureUploadDir, uploadRefPath, listUploadedAssets, deleteUploadedAsset } from '../lib/user-uploads.js';
28
30
  import { getProjectCover } from '../lib/cover.js';
29
31
  import { makeDocxPageHandler, makeDocxPdfHandler } from './assets/docx-page.js';
30
32
  import { mountNotesRoutes } from './assets/notes.js';
31
- import { safeSegment, decorateNoteText } from './assets/helpers.js';
33
+ import { safeSegment, decorateNoteText, decorateFilePreview, PREVIEW_EXTS } from './assets/helpers.js';
32
34
  import { CHALK_DIR } from '../lib/chalk.js';
33
35
  import {
34
36
  sendImage, isThumbPath, findOriginalForThumbnail, imageCacheControl,
@@ -37,6 +39,7 @@ import {
37
39
  import { injectSrcset } from '../lib/html-srcset.js';
38
40
  import { sendVideo, isVideo } from '../lib/video-variant.js';
39
41
  import { IMAGE_EXTS, VIDEO_EXTS, decorateCardKind } from '../lib/kinds/file-kinds.js';
42
+ import { noteBoardDirty } from '../lib/board-dirty.js';
40
43
 
41
44
  const router = express.Router();
42
45
 
@@ -95,7 +98,7 @@ router.post('/:pid/assets', upload.single('file'), async (req, res, next) => {
95
98
  if (!req.file) return res.status(400).json({ error: 'no file (field name: file)' });
96
99
 
97
100
  await ensureProjectWorkspace(req.params.pid);
98
- const assetsDir = path.join(getSharedDir(req.params.pid), 'assets');
101
+ const assetsDir = await ensureUploadDir(getSharedDir(req.params.pid));
99
102
 
100
103
  const originalName = decodeUploadName(req.file.originalname);
101
104
  let filename = sanitizeFilename(originalName);
@@ -112,7 +115,7 @@ router.post('/:pid/assets', upload.single('file'), async (req, res, next) => {
112
115
  asset: {
113
116
  // path 给 agent Read 用 — 相对 cwd(sessions/<sid>/)走 ../shared/assets/
114
117
  // 或者用 SDK additionalDirectories 拿到的绝对路径前缀;前端展示用 name 即可。
115
- path: `../../shared/assets/${filename}`,
118
+ path: uploadRefPath(USER_UPLOAD_DIR, filename),
116
119
  name: filename,
117
120
  originalName,
118
121
  size: req.file.size,
@@ -127,24 +130,7 @@ router.get('/:pid/assets', async (req, res, next) => {
127
130
  if (!guardProject(req, res)) return;
128
131
 
129
132
  const assetsDir = path.join(getSharedDir(req.params.pid), 'assets');
130
- let entries;
131
- try {
132
- entries = await fs.readdir(assetsDir, { withFileTypes: true });
133
- } catch (err) {
134
- if (err.code === 'ENOENT') return res.json({ assets: [] });
135
- throw err;
136
- }
137
- const assets = [];
138
- for (const e of entries) {
139
- if (!e.isFile()) continue;
140
- let stat;
141
- try { stat = await fs.stat(path.join(assetsDir, e.name)); } catch { continue; }
142
- assets.push({
143
- path: `../../shared/assets/${e.name}`, name: e.name,
144
- size: stat.size, mtime: stat.mtime.toISOString(),
145
- });
146
- }
147
- assets.sort((a, b) => (a.mtime < b.mtime ? 1 : -1));
133
+ const assets = await listUploadedAssets(getSharedDir(req.params.pid));
148
134
 
149
135
  // 参考素材单独一组,走抽屉不上画布。为什么它们此前完全看不见(以及为什么
150
136
  // 修法是加扫描口不是改形态注册表)见 lib/reference-assets.js 文件头。
@@ -166,17 +152,9 @@ router.delete('/:pid/assets/:filename', async (req, res, next) => {
166
152
  return res.status(400).json({ error: 'invalid filename' });
167
153
  }
168
154
 
169
- const assetsDir = path.join(getSharedDir(req.params.pid), 'assets');
170
- const filePath = path.resolve(assetsDir, filename);
171
- if (!filePath.startsWith(assetsDir + path.sep)) {
172
- return res.status(400).json({ error: 'invalid filename' });
173
- }
174
- try {
175
- await fs.unlink(filePath);
176
- } catch (err) {
177
- if (err.code === 'ENOENT') return res.status(404).json({ error: 'asset not found' });
178
- throw err;
179
- }
155
+ const outcome = await deleteUploadedAsset(getSharedDir(req.params.pid), filename);
156
+ if (outcome === 'invalid') return res.status(400).json({ error: 'invalid filename' });
157
+ if (outcome === 'not_found') return res.status(404).json({ error: 'asset not found' });
180
158
  res.status(204).end();
181
159
  } catch (err) { next(err); }
182
160
  });
@@ -254,10 +232,10 @@ router.get('/:pid/artifacts', async (req, res, next) => {
254
232
  // 那个接口,而迁移一旦晚于第一次渲染,用户会先看到一个叫 `tasks` 的文件夹
255
233
  // 套着他的文件夹。跑过之后是三次 stat 的事(幂等早退),不值得省。
256
234
  await ensureProjectWorkspace(req.params.pid);
257
- // 跟上 agent 在画布背后做的改名(`mv` 之后卡片 id 就变了)。同上:挂在这里
258
- // 是为了让用户看到的第一帧就是对齐过的。没有新 commit 时是一次 rev-parse。
235
+ // 跟上 agent 在画布背后做的改名(挂在这让第一帧就对齐;无新 commit 时只一次 rev-parse)
259
236
  await reconcileBoardRenames(req.params.pid).catch(
260
237
  (err) => console.warn('[board] 改名对账失败:', err.message));
238
+ await pruneDanglingBindings(req.params.pid).catch(() => {}); // 悬空线 30s 节流清扫(理由见 board-store)
261
239
 
262
240
  const assetsDir = path.join(getSharedDir(req.params.pid), 'assets');
263
241
  const artifacts = [];
@@ -349,8 +327,14 @@ router.get('/:pid/artifacts', async (req, res, next) => {
349
327
  item.meta = JSON.parse(metaRaw);
350
328
  } catch { /* 无 sidecar(旧图)→ 无 meta */ }
351
329
  }
352
- // 便签/板书把正文带给前端渲卡片(≤4KB 截断,完整内容走 artifact-file;见 helpers.decorateNoteText
330
+ // 便签/板书带正文、文本文件带 1KB 预览(完整内容都走 artifact-file;见 helpers)
331
+ // ⚠️ upload 也要(2026-08-29):用户上传的角色卡/世界卡就住在 用户内容/,
332
+ // kind='upload' —— 原来只认 task-file,于是上传来的 json/md 卡面永远是空的
333
+ // 细条。json 预览器做完才发现最常见的那一类 json 根本走不到这里。
353
334
  if (kind === 'note' && ext === '.md') await decorateNoteText(item, path.join(dir, e.name));
335
+ else if ((kind === 'task-file' || kind === 'upload') && PREVIEW_EXTS.has(ext)) {
336
+ await decorateFilePreview(item, path.join(dir, e.name));
337
+ }
354
338
  artifacts.push(item);
355
339
  }
356
340
  };
@@ -376,6 +360,7 @@ router.get('/:pid/artifacts', async (req, res, next) => {
376
360
  // 磁盘上的位置是同一个字符串 —— 这样 agent `mv` 一个文件之后,git 的改名
377
361
  // 检测能直接翻译成画布 id 的改名(见 board-store 的 reconcileBoardRenames)。
378
362
  const workspaceRoot = getSharedDir(req.params.pid);
363
+ const rootIgnore = await loadIgnore(workspaceRoot);
379
364
  const tasks = [];
380
365
  let hasRootSite = false;
381
366
  // 根站认领的一级子目录(pages 顶层段)—— 散文件过滤要用同一口径
@@ -474,6 +459,8 @@ router.get('/:pid/artifacts', async (req, res, next) => {
474
459
  if (!e.isDirectory() || e.name.startsWith('.')) continue;
475
460
  if (RESERVED_DIRS.has(e.name) || HARD_IGNORE_DIRS.has(e.name)) continue;
476
461
  if (e.name === DRAFTS_DIR) continue; // 站点试作,由 site 解析器管
462
+ // 构建目录不当独立站/收纳夹(site:dist 案);递归也吃根 .ndignore(与页面清单同规则)
463
+ if (OUTPUT_DIRS.includes(e.name) || rootIgnore(under(rel, e.name), true)) continue;
477
464
  if (claimed.has(e.name)) continue; // 已经是一件产物了
478
465
  folders.push(under(rel, e.name));
479
466
  await collect(path.join(dir, e.name), under(rel, e.name), depth + 1);
@@ -493,7 +480,11 @@ router.get('/:pid/artifacts', async (req, res, next) => {
493
480
  // 板书住 notes/板书/(不进 notes/ 顶层:便利贴注入只列顶层,板书另有自己的注入)
494
481
  await scanDir(path.join(workspaceRoot, CHALK_DIR), 'note', CHALK_DIR);
495
482
  for (const rel of folders) {
496
- await scanDir(path.join(workspaceRoot, rel), 'task-file', rel, 0);
483
+ // 用户上传的落点扫成 upload 而不是 task-file:来源轴(board-kinds.sourceOf)
484
+ // 按 kind 分「我放的 / agent 做的」,落回 task-file 的话用户自己拖进来的图
485
+ // 会被标成「agent 做的」。子目录仍按普通文件夹走。
486
+ const kind = rel === USER_UPLOAD_DIR ? 'upload' : 'task-file';
487
+ await scanDir(path.join(workspaceRoot, rel), kind, rel, 0);
497
488
  }
498
489
  } catch (err) {
499
490
  if (err.code !== 'ENOENT') throw err;
@@ -663,6 +654,8 @@ router.post('/:pid/move', express.json(), async (req, res, next) => {
663
654
  const out = await moveEntry(req.params.pid, req.body?.from, req.body?.to);
664
655
  res.json(out); // ③
665
656
  if (out.moved) {
657
+ // 板上动静(2026-08-29 刀 4):用户搬了文件,agent 下次摸板前该知道
658
+ try { noteBoardDirty(req.params.pid, [{ kind: 'mv', id: out.from, to: out.to }]); } catch { /* 记不上不挡搬家 */ }
666
659
  // ④ 响应之后再 commit:它只服务于稍后的对账,不该让用户多等一次 git add
667
660
  commitWorkspace(req.params.pid, null, `move: ${out.from} → ${out.to}`, { author: 'user' })
668
661
  .catch(err => console.warn('[git] move commit failed:', err.message));
@@ -10,9 +10,12 @@
10
10
  */
11
11
 
12
12
  import express from 'express';
13
+ import { listRoleNames } from '../engine/agent/role-card.js';
14
+ import { getWorkspaceRoot } from '../projects/workspace.js';
13
15
  import { validateProjectId, getProject } from '../projects/store.js';
14
16
  import { guardProject } from './_guard.js';
15
17
  import { readBoard, replaceBoard, patchBoard, commitStaging, removeByTag } from '../projects/board-store.js';
18
+ import { noteBoardDirty } from '../lib/board-dirty.js';
16
19
  import { setViewpoint, getViewpoint } from '../projects/viewpoint-store.js';
17
20
  import { exportGraph, exportGraphZip } from '../lib/board-graph-export.js';
18
21
  import { getSharedDir } from '../projects/workspace.js';
@@ -27,7 +30,12 @@ function guard(req, res) {
27
30
  router.get('/:pid/board', async (req, res, next) => {
28
31
  try {
29
32
  if (!guard(req, res)) return;
30
- res.json({ board: await readBoard(req.params.pid) });
33
+ // roles 是**派生态**:每次从 .claude/agents/ 现读,不落 board.json ——
34
+ // 板上存的署名是 slug(权威),展示名住在角色文件里(模型可改),两者不能混成一份数据。
35
+ // 前端只拿它把 slug 渲染得好看,任何判断仍按 slug 走。
36
+ let roles = {};
37
+ try { roles = Object.fromEntries(await listRoleNames(getWorkspaceRoot(req.params.pid))); } catch { /* 没有角色就是空表 */ }
38
+ res.json({ board: await readBoard(req.params.pid), roles });
31
39
  } catch (err) { next(err); }
32
40
  });
33
41
 
@@ -36,7 +44,26 @@ router.patch('/:pid/board', express.json({ limit: '600kb' }), async (req, res, n
36
44
  if (!guard(req, res)) return;
37
45
  const patch = req.body?.patch;
38
46
  if (!patch || typeof patch !== 'object') return res.status(400).json({ error: 'patch required' });
47
+ // 板上动静(2026-08-29 刀 4):HTTP PATCH 就是用户的手。**位置真变了才算动静**
48
+ // —— 前端的尺寸回写(useMeasuredSize)也走这条路,只有 w/h 变化不该吵 agent。
49
+ let prev = null;
50
+ if (patch.objects && typeof patch.objects === 'object') {
51
+ try { prev = await readBoard(req.params.pid); } catch { /* 记不上不挡写 */ }
52
+ }
39
53
  const board = await patchBoard(req.params.pid, patch);
54
+ if (prev) {
55
+ const events = [];
56
+ for (const [id, o] of Object.entries(patch.objects)) {
57
+ const was = prev.objects?.[id];
58
+ if (o === null) { if (was) events.push({ kind: 'removed', id }); continue; }
59
+ const now = board.objects?.[id];
60
+ if (!was || !now) continue;
61
+ if (Math.round(was.x) !== Math.round(now.x) || Math.round(was.y) !== Math.round(now.y)) {
62
+ events.push({ kind: 'moved', id });
63
+ }
64
+ }
65
+ if (events.length) noteBoardDirty(req.params.pid, events);
66
+ }
40
67
  res.json({ ok: true, board });
41
68
  } catch (err) {
42
69
  if (err?.status === 400) return res.status(400).json({ error: err.message });
@@ -94,6 +121,7 @@ router.post('/:pid/board/erase', express.json({ limit: '16kb' }), async (req, re
94
121
  const tag = typeof req.body?.tag === 'string' ? req.body.tag : '';
95
122
  if (!tag) return res.status(400).json({ error: 'tag required' });
96
123
  const { board, removed } = await removeByTag(req.params.pid, tag);
124
+ if (removed) noteBoardDirty(req.params.pid, [{ kind: 'erased', id: tag }]);
97
125
  res.json({ ok: true, removed, board });
98
126
  } catch (err) { next(err); }
99
127
  });
@@ -46,6 +46,7 @@ import { pendingHelp } from '../engine/browse/handover.js';
46
46
  import { readVisit, readFrame, saveFrame, recordVisit, forgetVisit } from '../engine/browse/state.js';
47
47
  import { browseCard } from '../engine/browse/card.js';
48
48
  import { checkUrl } from '../lib/ssrf-guard.js';
49
+ import { msg } from '../shared/messages.js';
49
50
 
50
51
  const router = express.Router();
51
52
 
@@ -96,11 +97,11 @@ router.post('/:pid/browse/open', async (req, res, next) => {
96
97
  const visit = await readVisit(pid);
97
98
  const want = String(req.body?.url || visit?.url || '');
98
99
  if (!/^https?:\/\//i.test(want)) {
99
- return res.status(400).json({ error: '没有可打开的地址(这个项目还没逛过任何站)' });
100
+ return res.status(400).json({ error: msg(req, '没有可打开的地址(这个项目还没逛过任何站)') });
100
101
  }
101
102
  // 用户点的也过闸 —— 闸是硬边界,不因为"是人点的"就放行
102
103
  const pre = await checkUrl(want);
103
- if (!pre.ok) return res.status(403).json({ error: `网络闸拒了这个地址:${pre.reason}` });
104
+ if (!pre.ok) return res.status(403).json({ error: msg(req, '网络闸拒了这个地址:{reason}', { reason: pre.reason }) });
104
105
 
105
106
  const out = await withBrowser(pid, async ({ page }) => {
106
107
  if (page.url() !== want) {
@@ -25,6 +25,7 @@ import path from 'node:path';
25
25
  import fs from 'node:fs/promises';
26
26
  import express from 'express';
27
27
  import { guardProject } from './_guard.js';
28
+ import { msg } from '../shared/messages.js';
28
29
  import { getWorkspaceRoot } from '../projects/workspace.js';
29
30
  import { performTurn } from '../engine/chatai/perform.js';
30
31
  import {
@@ -57,7 +58,7 @@ const playing = new Set();
57
58
  function gateApproved(req, res) {
58
59
  // 档位 + 逐人批准(auth/tier.js localGenApproved):basic 档不开,pro 档要被站主批过
59
60
  if (localGenApproved(req.user)) return true;
60
- res.status(403).json({ error: '演出通路尚未对这个账号开放' });
61
+ res.status(403).json({ error: msg(req, '演出通路尚未对这个账号开放') });
61
62
  return false;
62
63
  }
63
64
 
@@ -237,30 +238,30 @@ router.post('/:pid/chatai/turn', async (req, res, next) => {
237
238
  if (!gateApproved(req, res)) return;
238
239
 
239
240
  const input = String(req.body?.input ?? '').trim();
240
- if (!input) return res.status(422).json({ error: 'input 是空的' });
241
+ if (!input) return res.status(422).json({ error: msg(req, 'input 是空的') });
241
242
  if (input.length > MAX_INPUT_CHARS) {
242
- return res.status(422).json({ error: `input 超长(上限 ${MAX_INPUT_CHARS} 字符)` });
243
+ return res.status(422).json({ error: msg(req, 'input 超长(上限 {max} 字符)', { max: MAX_INPUT_CHARS }) });
243
244
  }
244
245
 
245
246
  // 08-21 经营态:演出通路烧的是带钥匙的 API 钱,公开注册号(免费档)不开 —— 跟订阅 Claude 同一把资格
246
247
  if (!can(req.user, 'subscription')) {
247
- return res.status(403).json({ error: '演出模式仅限 Pro 档,暂未对外开放;当前档位请用设计会话', code: 'MODEL_LOCKED' });
248
+ return res.status(403).json({ error: msg(req, '演出模式仅限 Pro 档,暂未对外开放;当前档位请用设计会话'), code: 'MODEL_LOCKED' });
248
249
  }
249
250
  const quota = checkQuota(req.user);
250
251
  if (!quota.ok) {
251
- const word = quota.kind === 'lifetime' ? '试用额度' : '今日额度';
252
- return res.status(429).json({ error: `${word}用完了(${fmtUsd(quota.used)} / ${fmtUsd(quota.limit)})` });
252
+ const word = quota.kind === 'lifetime' ? msg(req, '试用额度') : msg(req, '今日额度');
253
+ return res.status(429).json({ error: msg(req, '{word}用完了({used} / {limit})', { word, used: fmtUsd(quota.used), limit: fmtUsd(quota.limit) }) });
253
254
  }
254
255
  const r = rate.take(req.user?.id ?? 'anon');
255
256
  if (!r.ok) {
256
257
  res.setHeader('Retry-After', Math.ceil(r.retryAfterMs / 1000));
257
- return res.status(429).json({ error: '太快了,歇几秒再发' });
258
+ return res.status(429).json({ error: msg(req, '太快了,歇几秒再发') });
258
259
  }
259
260
 
260
261
  const dir = await resolvePerformanceDir(req, res);
261
262
  if (!dir) return;
262
263
  if (playing.has(dir.abs)) {
263
- return res.status(409).json({ error: '这场演出正有一轮在跑,等它回完' });
264
+ return res.status(409).json({ error: msg(req, '这场演出正有一轮在跑,等它回完') });
264
265
  }
265
266
 
266
267
  const stream = req.body?.stream !== false;
@@ -14,6 +14,7 @@
14
14
  import { collectCards } from '../../lib/export-collect.js';
15
15
  import { packageBundles } from '../../lib/export-package.js';
16
16
 
17
+ import { msg } from '../../shared/messages.js';
17
18
  const FORMATS = ['raw', 'zip', 'md', 'handoff'];
18
19
  const MAX_CARDS = 200;
19
20
 
@@ -47,22 +48,22 @@ export function makeCardsExportHandler({ guard, rootOf }) {
47
48
  if (!project) return;
48
49
  const { cardIds, format } = req.body || {};
49
50
  if (!Array.isArray(cardIds) || !cardIds.length) {
50
- return res.status(400).json({ error: '要导出哪几张卡(cardIds 不能为空)' });
51
+ return res.status(400).json({ error: msg(req, '要导出哪几张卡(cardIds 不能为空)') });
51
52
  }
52
53
  if (cardIds.length > MAX_CARDS) {
53
- return res.status(400).json({ error: `一次最多导出 ${MAX_CARDS} 张卡` });
54
+ return res.status(400).json({ error: msg(req, '一次最多导出 {max} 张卡', { max: MAX_CARDS }) });
54
55
  }
55
56
  if (!FORMATS.includes(format)) {
56
- return res.status(400).json({ error: `不认识的导出格式:${format}` });
57
+ return res.status(400).json({ error: msg(req, '不认识的导出格式:{format}', { format }) });
57
58
  }
58
59
 
59
60
  if (inflight >= MAX_INFLIGHT) {
60
61
  res.setHeader('Retry-After', '10');
61
- return res.status(429).json({ error: '导出排队中(这台机器一次只打两个包),过几秒再点' });
62
+ return res.status(429).json({ error: msg(req, '导出排队中(这台机器一次只打两个包),过几秒再点') });
62
63
  }
63
64
  if (inflightByProject.get(project.id)) {
64
65
  res.setHeader('Retry-After', '10');
65
- return res.status(429).json({ error: '这个项目已经有一个导出在跑了,等它完事' });
66
+ return res.status(429).json({ error: msg(req, '这个项目已经有一个导出在跑了,等它完事') });
66
67
  }
67
68
  inflight += 1;
68
69
  inflightByProject.set(project.id, true);
@@ -71,7 +72,7 @@ export function makeCardsExportHandler({ guard, rootOf }) {
71
72
  const { bundles, skipped } = await collectCards({ workspaceRoot, cardIds });
72
73
  if (!bundles.length) {
73
74
  return res.status(skipped[0]?.status === 404 ? 404 : 400)
74
- .json({ error: '一张都没收到', skipped });
75
+ .json({ error: msg(req, '一张都没收到'), skipped });
75
76
  }
76
77
 
77
78
  // 格式合法性由 packageBundles 自己把关(raw 要单文件、md 要有 markdown),
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { renderDocx, cleanupRender } from '../../lib/docx/render.js';
14
14
 
15
+ import { msg } from '../../shared/messages.js';
15
16
  /**
16
17
  * @param {import('express').Response} res
17
18
  * @param {{absPath:string, relPath:string}} target
@@ -22,7 +23,9 @@ export async function docxToPdfResponse(res, target) {
22
23
  out = await renderDocx(target.absPath, {}); // 只要 pdf,不出 png
23
24
  } catch (err) {
24
25
  return res.status(500).json({
25
- error: 'LibreOffice 转换失败',
26
+ // 这个函数只收 res 不收 req(签名是给导出管线用的)。express 把请求挂在
27
+ // res.req 上,用它取语言,不为一句报错改所有调用点的签名。
28
+ error: msg(res.req, 'LibreOffice 转换失败'),
26
29
  details: String(err.message || err).slice(0, 500),
27
30
  });
28
31
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * server/api/instruction.js — 项目 Instruction 文件读写
3
3
  *
4
- * Instruction = workspace/.claude/CLAUDE.mdSDK 启用 settingSources:
4
+ * Instruction = workspace/CLAUDE.md(2026-08-24 挪到工作区根:画布可见 + SDK 根目录也原生注入)。SDK 启用 settingSources:
5
5
  * ['project'] 后 agent 进每次 session 自动读到 system prompt。S1 已经
6
6
  * 在 ensureProjectWorkspace 写入模板 starter,本阶段加 GET/PUT 让前端
7
7
  * "项目背景" tab 直接编辑该文件。
@@ -37,7 +37,7 @@ router.get('/:pid/instruction', async (req, res, next) => {
37
37
  // ensureProjectWorkspace 幂等补齐(老 project 第一次进 instruction tab 也能读到)
38
38
  await ensureProjectWorkspace(req.params.pid);
39
39
 
40
- const filePath = path.join(getSharedDir(req.params.pid), '.claude', INSTRUCTION_FILE);
40
+ const filePath = path.join(getSharedDir(req.params.pid), INSTRUCTION_FILE);
41
41
  try {
42
42
  const content = await fs.readFile(filePath, 'utf8');
43
43
  res.json({ content, exists: true });
@@ -66,7 +66,7 @@ router.put('/:pid/instruction', async (req, res, next) => {
66
66
  }
67
67
 
68
68
  await ensureProjectWorkspace(req.params.pid);
69
- const filePath = path.join(getSharedDir(req.params.pid), '.claude', INSTRUCTION_FILE);
69
+ const filePath = path.join(getSharedDir(req.params.pid), INSTRUCTION_FILE);
70
70
  await fs.writeFile(filePath, content, 'utf8');
71
71
  res.json({ ok: true });
72
72
  } catch (err) { next(err); }
@@ -16,14 +16,15 @@ import { fileURLToPath } from 'node:url';
16
16
  import path from 'node:path';
17
17
  import { platform } from '../runtime/platform.js';
18
18
  import { loadLocalConfig, saveLocalConfig, CONFIG_ENUMS } from '../runtime/local-config.js';
19
- import { MODEL_CONFIG_ERRORS, EXTERNAL_SDK_ALIAS, externalModelIds } from '../engine/agent/model-context.js';
20
- import { UPSTREAMS_BUILTIN } from '../engine/agent/model-table.js';
19
+ import { MODEL_CONFIG_ERRORS, externalModelIds } from '../engine/agent/model-context.js';
20
+ import { UPSTREAMS_BUILTIN, SHARED_SDK_ALIAS } from '../engine/agent/model-table.js';
21
21
  import { capabilitySnapshot } from '../runtime/capabilities.js';
22
22
  import { TOOL_CAPABILITIES } from '../engine/mcp/capability-gate.js';
23
23
  import { probeCapabilities } from '../runtime/capabilities.js';
24
24
  import { envView, setEnvValues, envPath } from '../runtime/local-env.js';
25
25
  import { probeModel } from '../lib/ingress/slot-probe.js';
26
26
  import { selectableModelsFor } from '../engine/agent/model-context.js';
27
+ import { msg } from '../shared/messages.js';
27
28
 
28
29
  export const RESTART_EXIT_CODE = 75;
29
30
 
@@ -42,7 +43,8 @@ router.get('/status', (_req, res) => {
42
43
  modelConfigErrors: MODEL_CONFIG_ERRORS,
43
44
  // 本机能力位 + 每个能力位管着哪些工具(配置页那张表)
44
45
  capabilities: capabilitySnapshot().map((c) => ({ ...c, tools: Object.entries(TOOL_CAPABILITIES).filter(([, v]) => v.cap === c.id).map(([t]) => t) })),
45
- externalSdkAlias: EXTERNAL_SDK_ALIAS,
46
+ // 外部插槽(和一切不写 sdkAlias 的行)借用的共用 spoof 名。响应字段名保持 externalSdkAlias 不动(对外形状)
47
+ externalSdkAlias: SHARED_SDK_ALIAS,
46
48
  // 内置 Claude 行现在能不能选:'api_key' | 'login' | null(设置页「模型」那块的状态行)
47
49
  claudeAuth: platform.claudeAuthPresent(),
48
50
  // 内置上游(只报名字和是否配了钥匙,不报钥匙):配置页提示「这些名字被占了」
@@ -60,13 +62,13 @@ router.get('/config', (_req, res) => {
60
62
  router.put('/config', (req, res) => {
61
63
  const raw = req.body;
62
64
  if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
63
- return res.status(400).json({ error: '配置必须是一个对象 { upstreams, models }' });
65
+ return res.status(400).json({ error: msg(req, '配置必须是一个对象 { upstreams, models }') });
64
66
  }
65
67
  try {
66
68
  const v = saveLocalConfig(raw);
67
69
  res.json({ ok: true, path: v.path, errors: v.errors, needsRestart: true });
68
70
  } catch (err) {
69
- res.status(500).json({ error: `写配置失败:${err.message}` });
71
+ res.status(500).json({ error: msg(req, '写配置失败:{err}', { err: err.message }) });
70
72
  }
71
73
  });
72
74
 
@@ -92,15 +94,15 @@ router.put('/env', async (req, res) => {
92
94
  const probing = new Set();
93
95
  router.post('/models/:id/probe', async (req, res) => {
94
96
  const id = req.params.id;
95
- if (!selectableModelsFor(req.user).some((m) => m.id === id)) return res.status(404).json({ error: `模型 ${id} 不在可选清单里(没配钥匙的行不体检)` });
96
- if (probing.has(id)) return res.status(409).json({ error: '这一行正在体检,等它完' });
97
+ if (!selectableModelsFor(req.user).some((m) => m.id === id)) return res.status(404).json({ error: msg(req, '模型 {id} 不在可选清单里(没配钥匙的行不体检)', { id }) });
98
+ if (probing.has(id)) return res.status(409).json({ error: msg(req, '这一行正在体检,等它完') });
97
99
  probing.add(id);
98
100
  try {
99
101
  const vision = req.query.vision !== '0';
100
102
  const timeoutMs = Math.min(120_000, Math.max(5_000, Number(req.query.timeoutMs) || 45_000));
101
103
  res.json(await probeModel(id, { vision, timeoutMs }));
102
104
  } catch (err) {
103
- res.status(500).json({ error: `体检出错:${err.message}` });
105
+ res.status(500).json({ error: msg(req, '体检出错:{err}', { err: err.message }) });
104
106
  } finally {
105
107
  probing.delete(id);
106
108
  }
@@ -17,6 +17,7 @@ import {
17
17
  listRunsForProject,
18
18
  } from '../projects/store.js';
19
19
  import { guardProject } from './_guard.js';
20
+ import { chalkPreview } from '../lib/board-excerpt.js';
20
21
  import { taskManifest } from '../lib/artifact-target.js';
21
22
  import { countPublishedByUser } from '../lib/publish-store.js';
22
23
  import { checkQuota } from '../lib/quota.js';
@@ -39,6 +40,9 @@ const ownerScope = (req) => req.user?.id ?? null;
39
40
 
40
41
  const KIND_VALUES = new Set(['project', 'quick']);
41
42
  const KIND_QUERY_VALUES = new Set(['project', 'quick', 'all']);
43
+ // 项目模式(2026-08-27):design=设计工作台(现状默认)/ rp=演出(常驻角色演故事)。
44
+ // 切换下个会话生效(会话启动时读一次),真相源与工具对照表见 engine/mcp/mode-profile.js。
45
+ const MODE_VALUES = new Set(['design', 'rp']);
42
46
 
43
47
  // GET /api/projects 默认行为(2026-05-07):不带 ?kind= 时 **只返 kind='project'**,
44
48
  // 把闪聊(kind='quick')从主项目列表里挡掉 —— 避免老 client / 任何漏传 kind 的调用
@@ -84,7 +88,10 @@ router.get('/stats', async (req, res, next) => {
84
88
  } catch { /* 目录没了:算 0 件 */ }
85
89
  const kinds = {};
86
90
  for (const a of artifacts) if (a.kind) kinds[a.kind] = (kinds[a.kind] || 0) + 1;
87
- stats[p.id] = { tasks: artifacts.length, kinds };
91
+ // 板书不进 artifacts(它不是产物),所以卡上"这里躺着什么"必须单独问一句 ——
92
+ // 不问的话整个演出项目会一直写着"还没出东西",而里面躺着一整个故事
93
+ const chalk = await chalkPreview(getSharedDir(p.id)).catch(() => null);
94
+ stats[p.id] = { tasks: artifacts.length, kinds, ...(chalk ? { chalk } : {}) };
88
95
  }));
89
96
  // dev 模式(不要求登录)没有 req.user,这两笔账就没有主语,整块不下发
90
97
  const summary = req.user
@@ -101,7 +108,7 @@ const DESCRIPTION_MAX = 2000;
101
108
 
102
109
  router.post('/', async (req, res, next) => {
103
110
  try {
104
- const { name, skillId, description, kind, autoNamed } = req.body || {};
111
+ const { name, skillId, description, kind, mode, autoNamed } = req.body || {};
105
112
  if (!name || typeof name !== 'string' || !name.trim()) {
106
113
  return res.status(400).json({ error: 'name required' });
107
114
  }
@@ -114,8 +121,11 @@ router.post('/', async (req, res, next) => {
114
121
  if (kind != null && !KIND_VALUES.has(kind)) {
115
122
  return res.status(400).json({ error: `kind must be project|quick (got ${kind})` });
116
123
  }
124
+ if (mode != null && !MODE_VALUES.has(mode)) {
125
+ return res.status(400).json({ error: `mode must be design|rp (got ${mode})` });
126
+ }
117
127
  const project = createProject({
118
- name, skillId, description, kind, autoNamed: !!autoNamed,
128
+ name, skillId, description, kind, mode: mode ?? undefined, autoNamed: !!autoNamed,
119
129
  ownerId: req.user?.id ?? null,
120
130
  });
121
131
  await ensureProjectWorkspace(project.id);
@@ -154,6 +164,12 @@ router.patch('/:pid', (req, res, next) => {
154
164
  }
155
165
  patch.kind = req.body.kind;
156
166
  }
167
+ if ('mode' in (req.body || {})) {
168
+ if (!MODE_VALUES.has(req.body.mode)) {
169
+ return res.status(400).json({ error: `mode must be design|rp (got ${req.body.mode})` });
170
+ }
171
+ patch.mode = req.body.mode;
172
+ }
157
173
  // E1a(2026-08-13):会话真相源收敛到 projects.active_session_id 之后,
158
174
  // 前端切会话得能直接写这个指针(此前唯一的写入方是 turn.js —— 不发消息、
159
175
  // 只是切着看,指针就不动,别的标签页也就永远对不齐)。接受 null(清空)
@@ -13,6 +13,7 @@
13
13
  import express from 'express';
14
14
  import { guardProject } from './_guard.js';
15
15
  import { publishSite, unpublishSite, validTaskName, lookupPublished } from '../lib/site-publish.js';
16
+ import { msg } from '../shared/messages.js';
16
17
 
17
18
  const router = express.Router();
18
19
 
@@ -44,7 +45,7 @@ router.post(['/:pid/publish', '/:pid/publish/:task'], async (req, res) => {
44
45
  } catch (err) {
45
46
  if (err.status) return res.status(err.status).json({ error: err.message });
46
47
  console.error('[publish] deploy failed:', err.stderr || err.message);
47
- res.status(502).json({ error: '发布失败:Cloudflare 部署没成功,稍后再试' });
48
+ res.status(502).json({ error: msg(req, '发布失败:Cloudflare 部署没成功,稍后再试') });
48
49
  }
49
50
  });
50
51
 
@@ -57,7 +58,7 @@ router.delete(['/:pid/publish', '/:pid/publish/:task'], async (req, res) => {
57
58
  } catch (err) {
58
59
  if (err.status) return res.status(err.status).json({ error: err.message });
59
60
  console.error('[publish] delete failed:', err.stderr || err.message);
60
- res.status(502).json({ error: '下线失败,稍后再试' });
61
+ res.status(502).json({ error: msg(req, '下线失败,稍后再试') });
61
62
  }
62
63
  });
63
64