@xiaobuyu/nodesign 0.0.7 → 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 (353) 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 +8 -0
  7. package/server/api/assets.js +21 -33
  8. package/server/api/board.js +29 -1
  9. package/server/api/browse.js +3 -2
  10. package/server/api/chatai.js +9 -8
  11. package/server/api/exports/cards.js +7 -6
  12. package/server/api/exports/docx-pdf.js +4 -1
  13. package/server/api/local.js +10 -8
  14. package/server/api/projects.js +19 -3
  15. package/server/api/publish.js +3 -2
  16. package/server/api/sessions-rewind.js +220 -0
  17. package/server/api/sessions.js +58 -194
  18. package/server/api/stage.js +35 -0
  19. package/server/api/turn-model-switch.js +109 -0
  20. package/server/api/turn.js +52 -88
  21. package/server/auth/middleware.js +29 -6
  22. package/server/auth/tier.js +27 -7
  23. package/server/auth/users-store.js +14 -1
  24. package/server/engine/agent/actor-trail.js +110 -0
  25. package/server/engine/agent/agent-shared.js +22 -52
  26. package/server/engine/agent/beat-state.js +61 -0
  27. package/server/engine/agent/cast.js +322 -0
  28. package/server/engine/agent/events.js +9 -2
  29. package/server/engine/agent/hooks/beat-gate.js +73 -0
  30. package/server/engine/agent/hooks/lifecycle.js +6 -2
  31. package/server/engine/agent/hooks/post-trim.js +1 -0
  32. package/server/engine/agent/hooks/pre-board-dirty.js +29 -0
  33. package/server/engine/agent/hooks/pre-defaults.js +206 -2
  34. package/server/engine/agent/hooks/pre-injectors.js +19 -2
  35. package/server/engine/agent/hooks/pre-peer-guard.js +88 -0
  36. package/server/engine/agent/hooks/pre-unknown-params.js +82 -0
  37. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +33 -0
  38. package/server/engine/agent/hooks/resident-role-lifecycle.js +100 -0
  39. package/server/engine/agent/hooks/slot-alias.js +30 -0
  40. package/server/engine/agent/hooks/user-prompt-submit.js +197 -7
  41. package/server/engine/agent/hooks.js +76 -6
  42. package/server/engine/agent/isolation.js +25 -2
  43. package/server/engine/agent/memory-config.js +27 -1
  44. package/server/engine/agent/model-context.js +102 -18
  45. package/server/engine/agent/model-table.js +361 -58
  46. package/server/engine/agent/prompts/nodesign-prelude.md +566 -59
  47. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +1 -1
  48. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +2 -2
  49. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +1 -1
  50. package/server/engine/agent/prompts/tools/site-reference.md +2 -1
  51. package/server/engine/agent/role-card.js +175 -0
  52. package/server/engine/agent/rp-mode.js +17 -0
  53. package/server/engine/agent/session-loop.js +26 -14
  54. package/server/engine/agent/stage-status.js +73 -0
  55. package/server/engine/agent/subagent-flight.js +73 -0
  56. package/server/engine/agent/system-prompts.js +41 -0
  57. package/server/engine/agent/tool-input-stream.js +165 -0
  58. package/server/engine/mcp/actor.js +59 -0
  59. package/server/engine/mcp/capability-gate.js +21 -2
  60. package/server/engine/mcp/index.js +116 -24
  61. package/server/engine/mcp/mode-profile.js +139 -0
  62. package/server/engine/mcp/server-name.js +19 -0
  63. package/server/engine/mcp/tools/artifact-session.js +2 -1
  64. package/server/engine/mcp/tools/browse-find-batch.js +76 -14
  65. package/server/engine/mcp/tools/browse.js +1 -1
  66. package/server/engine/mcp/tools/cast-role.js +167 -0
  67. package/server/engine/mcp/tools/draw-trend.js +104 -0
  68. package/server/engine/mcp/tools/edit-board-schema.js +56 -0
  69. package/server/engine/mcp/tools/edit-board.js +569 -0
  70. package/server/engine/mcp/tools/look-at-board.js +4 -3
  71. package/server/engine/mcp/tools/open-sheet.js +242 -0
  72. package/server/engine/mcp/tools/organize-board.js +19 -5
  73. package/server/engine/mcp/tools/paint-still.js +1 -1
  74. package/server/engine/mcp/tools/pin-to-board.js +78 -3
  75. package/server/engine/mcp/tools/read-board.js +109 -16
  76. package/server/engine/mcp/tools/read-tavern-json.js +87 -19
  77. package/server/engine/mcp/tools/report-issue.js +1 -0
  78. package/server/engine/mcp/tools/role-memory.js +83 -0
  79. package/server/engine/mcp/tools/roll-dice.js +82 -0
  80. package/server/engine/mcp/tools/roll-film.js +1 -1
  81. package/server/engine/mcp/tools/screenshot.js +2 -2
  82. package/server/engine/mcp/tools/set-vars.js +150 -0
  83. package/server/engine/mcp/tools/sheet-replan.js +46 -0
  84. package/server/engine/mcp/tools/web-search.js +1 -1
  85. package/server/engine/mcp/tools/write-on-board-flow.js +108 -0
  86. package/server/engine/mcp/tools/write-on-board-place.js +211 -0
  87. package/server/engine/mcp/tools/write-on-board-role-anchor.js +48 -0
  88. package/server/engine/mcp/tools/write-on-board-schema.js +125 -0
  89. package/server/engine/mcp/tools/write-on-board.js +567 -107
  90. package/server/engine/plugins/nodesign/skills/blackboard-rp/SKILL.md +215 -0
  91. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +11 -3
  92. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +34 -4
  93. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +20 -4
  94. package/server/engine/plugins/nodesign/skills/story-craft/SKILL.md +94 -0
  95. package/server/engine/plugins/nodesign/skills/story-import/SKILL.md +112 -0
  96. package/server/engine/plugins/nodesign/skills/story-intimacy/SKILL.md +72 -0
  97. package/server/engine/plugins/nodesign/skills/story-voice/SKILL.md +66 -0
  98. package/server/engine/plugins/nodesign/skills/story-voice/voices//345/206/267/347/241/254.md +33 -0
  99. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/203/205/346/204/237/347/233/264/347/273/231.md +42 -0
  100. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/204/217/350/257/206/346/265/201.md +42 -0
  101. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/255/246/344/276/240/347/231/275/350/257/235.md +34 -0
  102. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/224/237/346/264/273/346/265/201.md +35 -0
  103. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/231/275/346/217/217.md +37 -0
  104. package/server/engine/plugins/nodesign/skills/story-voice/voices//350/210/236/345/217/260/345/211/247.md +32 -0
  105. package/server/engine/plugins/nodesign/skills/story-voice/voices//351/230/266/346/256/265/345/210/207/346/215/242.md +42 -0
  106. package/server/engine/runs/board-seater.js +272 -0
  107. package/server/engine/runs/board-tasklist.js +90 -48
  108. package/server/engine/runs/turn-relay.js +24 -2
  109. package/server/engine/runs/user-chalk-echo.js +44 -0
  110. package/server/index.js +2 -0
  111. package/server/lib/board-anchor.js +49 -0
  112. package/server/lib/board-dirty.js +93 -0
  113. package/server/lib/board-excerpt.js +80 -0
  114. package/server/lib/board-follow.js +111 -0
  115. package/server/lib/board-groups.js +1 -5
  116. package/server/lib/board-hero.js +3 -1
  117. package/server/lib/board-kind-sizes.js +36 -0
  118. package/server/lib/board-lanes.js +58 -0
  119. package/server/lib/board-obstacles.js +80 -0
  120. package/server/lib/board-relations.js +5 -1
  121. package/server/lib/board-sheets.js +467 -0
  122. package/server/lib/board-shelf.js +86 -0
  123. package/server/lib/board-transform.js +62 -0
  124. package/server/lib/canvas-id.js +18 -1
  125. package/server/lib/chalk-flow.js +88 -0
  126. package/server/lib/chalk-rewrite.js +63 -0
  127. package/server/lib/chalk-size-pref.js +48 -0
  128. package/server/lib/chalk.js +42 -3
  129. package/server/lib/ingress/forward-openai-chat.js +16 -4
  130. package/server/lib/ingress/openai-chat.js +23 -12
  131. package/server/lib/ingress/session-routes.js +3 -3
  132. package/server/lib/ingress/stream-watchdog.js +53 -0
  133. package/server/lib/ingress/upstream-billing.js +15 -0
  134. package/server/lib/ingress/upstream-error-hints.js +36 -0
  135. package/server/lib/json-preview.js +58 -0
  136. package/server/lib/model-ingress.js +26 -1
  137. package/server/lib/moderation.js +10 -5
  138. package/server/lib/quota.js +26 -6
  139. package/server/lib/rect.js +50 -0
  140. package/server/lib/screen.js +36 -0
  141. package/server/lib/sheet-state.js +19 -0
  142. package/server/lib/sketch-array.js +118 -0
  143. package/server/lib/sketch-hatch.js +99 -0
  144. package/server/lib/sketch-layout.js +269 -69
  145. package/server/lib/sketch-shapes.js +152 -0
  146. package/server/lib/sketch-stencils.js +170 -0
  147. package/server/lib/state-table.js +237 -0
  148. package/server/lib/state-trend.js +94 -0
  149. package/server/lib/state-triggers.js +199 -0
  150. package/server/lib/tavern-json.js +48 -0
  151. package/server/lib/user-uploads.js +88 -0
  152. package/server/projects/board-limits.js +5 -3
  153. package/server/projects/board-sanitize.js +220 -10
  154. package/server/projects/board-store.js +95 -6
  155. package/server/projects/session-jsonl.js +110 -0
  156. package/server/projects/store.js +56 -3
  157. package/server/projects/viewpoint-store.js +35 -3
  158. package/server/projects/workspace-slots.js +30 -0
  159. package/server/projects/workspace-templates.js +29 -0
  160. package/server/projects/workspace.js +16 -17
  161. package/server/runtime/capabilities.js +6 -2
  162. package/server/shared/locales.js +70 -0
  163. package/server/shared/messages-en.js +69 -0
  164. package/server/shared/messages.js +65 -0
  165. package/server/ws/broker.js +3 -0
  166. package/server/ws/index.js +43 -3
  167. package/web/README.md +1 -1
  168. package/web/dist/assets/{BrowserWindow-BoOBaIyz.js → BrowserWindow-CcaGRWz5.js} +1 -1
  169. package/web/dist/assets/DeckWindow-BIr2KGFT.js +6 -0
  170. package/web/dist/assets/{DocxWindow-B_u6aX9o.js → DocxWindow-B9KZmU5z.js} +2 -2
  171. package/web/dist/assets/{MermaidBlock-BO-46Du5.js → MermaidBlock-DrfRnz8E.js} +43 -43
  172. package/web/dist/assets/{SiteWindow-ConPmzdM.js → SiteWindow-CJOjCF7d.js} +6 -6
  173. package/web/dist/assets/{abnfDiagram-VCTEODGH-FCO9yf-n.js → abnfDiagram-VCTEODGH-CUZbQGIg.js} +1 -1
  174. package/web/dist/assets/{arc-BLcEOSRX.js → arc-C1-HSUny.js} +1 -1
  175. package/web/dist/assets/{architectureDiagram-5GKGNRK7-kxiX4TeN.js → architectureDiagram-5GKGNRK7-B5nwvu60.js} +2 -2
  176. package/web/dist/assets/{blockDiagram-NRAW4CY4-CptFksix.js → blockDiagram-NRAW4CY4-uPPuFLwR.js} +1 -1
  177. package/web/dist/assets/{c4Diagram-UCG6FXSJ-Dxmxj34F.js → c4Diagram-UCG6FXSJ-C0mKBFlz.js} +1 -1
  178. package/web/dist/assets/channel-BxpWNOGU.js +1 -0
  179. package/web/dist/assets/{chunk-2Q5K7J3B-D9inx5oo.js → chunk-2Q5K7J3B-Cs0rlZ4q.js} +1 -1
  180. package/web/dist/assets/{chunk-5VM5RSS4-DydfHlEc.js → chunk-5VM5RSS4-DtR63oYb.js} +1 -1
  181. package/web/dist/assets/{chunk-F27PBJKO-BO7GxhSv.js → chunk-F27PBJKO-B3MRdezJ.js} +1 -1
  182. package/web/dist/assets/{chunk-G27WJ6UU-BzP6YiQ_.js → chunk-G27WJ6UU-BtwntwpS.js} +1 -1
  183. package/web/dist/assets/{chunk-JWPE2WC7-CHcgcd0s.js → chunk-JWPE2WC7-BHsTe5PD.js} +1 -1
  184. package/web/dist/assets/{chunk-LCL6LL3I-D2Wg5JtX.js → chunk-LCL6LL3I-knY15Qdm.js} +1 -1
  185. package/web/dist/assets/{chunk-POPQ4Y6H-B2fCRfpM.js → chunk-POPQ4Y6H-C51tgK_I.js} +1 -1
  186. package/web/dist/assets/{chunk-SVP7TREG-JSLcsRlZ.js → chunk-SVP7TREG-CnwRZmF_.js} +1 -1
  187. package/web/dist/assets/{chunk-XXDRQBXY-BCjvaeHm.js → chunk-XXDRQBXY-BtXzVF-_.js} +1 -1
  188. package/web/dist/assets/classDiagram-DTDB5LWJ-DUEKmDfo.js +1 -0
  189. package/web/dist/assets/classDiagram-v2-JRS7N3AN-DUEKmDfo.js +1 -0
  190. package/web/dist/assets/{cose-bilkent-JH36ORCC-0uKv6oJ7.js → cose-bilkent-JH36ORCC-CzWlGP6i.js} +1 -1
  191. package/web/dist/assets/{cynefin-OW5HDTMX-Bvt-gEmA.js → cynefin-OW5HDTMX-vK64EGSv.js} +1 -1
  192. package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-C9Ht02qC.js → cynefinDiagram-5FMLGOSQ-Bs3g1Gvh.js} +1 -1
  193. package/web/dist/assets/{dagre-3AP2YEHR-BfxZny4s.js → dagre-3AP2YEHR-BhgkYe8V.js} +1 -1
  194. package/web/dist/assets/{diagram-S7CK7UJ4-CrE0ZETE.js → diagram-S7CK7UJ4-C_6mvD2V.js} +1 -1
  195. package/web/dist/assets/{diagram-UQ7AKVKN-BO8VQ4Fx.js → diagram-UQ7AKVKN-qW7tssUW.js} +1 -1
  196. package/web/dist/assets/{diagram-VSXAHHWV-CHUZnR9F.js → diagram-VSXAHHWV-txJPHpvG.js} +1 -1
  197. package/web/dist/assets/{diagram-VX7I27RA-DYQAz1Vu.js → diagram-VX7I27RA-CQTMsCnL.js} +1 -1
  198. package/web/dist/assets/{diagram-Z3DM3KII-BDjivgLr.js → diagram-Z3DM3KII--cb1hoE6.js} +1 -1
  199. package/web/dist/assets/{ebnfDiagram-PWID7BFC-Bpe_QVbH.js → ebnfDiagram-PWID7BFC-Da6lDU77.js} +1 -1
  200. package/web/dist/assets/{erDiagram-SSCWMZ5O-BxhwuAu1.js → erDiagram-SSCWMZ5O-DGCMrsQn.js} +1 -1
  201. package/web/dist/assets/{flowDiagram-A5DVABFB-CCw2H08-.js → flowDiagram-A5DVABFB-GdOL_qno.js} +1 -1
  202. package/web/dist/assets/{ganttDiagram-EL5Y4UJY-DtTc7MyI.js → ganttDiagram-EL5Y4UJY-Cx1F_jBK.js} +4 -4
  203. package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-BvORxMd2.js → gitGraphDiagram-WWUBYQGX-B5_6JhR3.js} +1 -1
  204. package/web/dist/assets/hand-CuvZZQGW.webp +0 -0
  205. package/web/dist/assets/index-BYKfMnmv.js +2759 -0
  206. package/web/dist/assets/index-CW7VsrVg.css +1 -0
  207. package/web/dist/assets/{infoDiagram-RXCK75RN-DMcrkypD.js → infoDiagram-RXCK75RN-CjvKpSPA.js} +1 -1
  208. package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CZkTIbe-.js → ishikawaDiagram-5VMMS53U-CxLoAPhx.js} +1 -1
  209. package/web/dist/assets/{journeyDiagram-EYS64GPL-DR0XazdE.js → journeyDiagram-EYS64GPL-BfvS1upJ.js} +1 -1
  210. package/web/dist/assets/{kanban-definition-3QL26DDD-DN8VbwVu.js → kanban-definition-3QL26DDD-DKPPu0w4.js} +1 -1
  211. package/web/dist/assets/{layout-9_paCbyt.js → layout-Dhgck-Zo.js} +1 -1
  212. package/web/dist/assets/{linear-g6Ct012e.js → linear-DMSQ-aUw.js} +1 -1
  213. package/web/dist/assets/lxgw-nd-app-bold-D1pKA1bR.woff2 +0 -0
  214. package/web/dist/assets/lxgw-nd-app-regular-BDe3d26v.woff2 +0 -0
  215. package/web/dist/assets/lxgw-nd-bold-Bufvlg03.woff2 +0 -0
  216. package/web/dist/assets/lxgw-nd-regular-DQwoI4OP.woff2 +0 -0
  217. package/web/dist/assets/lxgwwenkaiscreen-subset-100-DLPe7tEt.woff2 +0 -0
  218. package/web/dist/assets/lxgwwenkaiscreen-subset-101-d3VsZKWT.woff2 +0 -0
  219. package/web/dist/assets/lxgwwenkaiscreen-subset-102-ClzJuLq6.woff2 +0 -0
  220. package/web/dist/assets/lxgwwenkaiscreen-subset-103-DuDnQ6K-.woff2 +0 -0
  221. package/web/dist/assets/lxgwwenkaiscreen-subset-104-kPGW3Eem.woff2 +0 -0
  222. package/web/dist/assets/lxgwwenkaiscreen-subset-105-BzD54h-0.woff2 +0 -0
  223. package/web/dist/assets/lxgwwenkaiscreen-subset-106-BQ7fIzlx.woff2 +0 -0
  224. package/web/dist/assets/lxgwwenkaiscreen-subset-107-D8tDfFHI.woff2 +0 -0
  225. package/web/dist/assets/lxgwwenkaiscreen-subset-108-tGhkpwep.woff2 +0 -0
  226. package/web/dist/assets/lxgwwenkaiscreen-subset-109-7SMVSrly.woff2 +0 -0
  227. package/web/dist/assets/lxgwwenkaiscreen-subset-110-_uDKrFed.woff2 +0 -0
  228. package/web/dist/assets/lxgwwenkaiscreen-subset-111-Bio825RE.woff2 +0 -0
  229. package/web/dist/assets/lxgwwenkaiscreen-subset-112-D2ee0i-C.woff2 +0 -0
  230. package/web/dist/assets/lxgwwenkaiscreen-subset-113-JPFVDrga.woff2 +0 -0
  231. package/web/dist/assets/lxgwwenkaiscreen-subset-114-BcfxYA4w.woff2 +0 -0
  232. package/web/dist/assets/lxgwwenkaiscreen-subset-115-B1HeeOsd.woff2 +0 -0
  233. package/web/dist/assets/lxgwwenkaiscreen-subset-116-hY12tXpq.woff2 +0 -0
  234. package/web/dist/assets/lxgwwenkaiscreen-subset-117-BvuZwmre.woff2 +0 -0
  235. package/web/dist/assets/lxgwwenkaiscreen-subset-118-tzvs_H7j.woff2 +0 -0
  236. package/web/dist/assets/lxgwwenkaiscreen-subset-119-J2DWuYDN.woff2 +0 -0
  237. package/web/dist/assets/lxgwwenkaiscreen-subset-21-pDubCJV5.woff2 +0 -0
  238. package/web/dist/assets/lxgwwenkaiscreen-subset-22-ChAPMpqp.woff2 +0 -0
  239. package/web/dist/assets/lxgwwenkaiscreen-subset-23-BBx3oNpT.woff2 +0 -0
  240. package/web/dist/assets/lxgwwenkaiscreen-subset-24-CYt-Oagk.woff2 +0 -0
  241. package/web/dist/assets/lxgwwenkaiscreen-subset-25-Cx3j-ZUv.woff2 +0 -0
  242. package/web/dist/assets/lxgwwenkaiscreen-subset-26-B9AfZ3LL.woff2 +0 -0
  243. package/web/dist/assets/lxgwwenkaiscreen-subset-27-Bh2s9MNE.woff2 +0 -0
  244. package/web/dist/assets/lxgwwenkaiscreen-subset-28-C02JPoKP.woff2 +0 -0
  245. package/web/dist/assets/lxgwwenkaiscreen-subset-29-T_tbUGhk.woff2 +0 -0
  246. package/web/dist/assets/lxgwwenkaiscreen-subset-30-DMfyNzhz.woff2 +0 -0
  247. package/web/dist/assets/lxgwwenkaiscreen-subset-31-DFfdzbkg.woff2 +0 -0
  248. package/web/dist/assets/lxgwwenkaiscreen-subset-32-Bs1fz2iW.woff2 +0 -0
  249. package/web/dist/assets/lxgwwenkaiscreen-subset-33-cpB96OkZ.woff2 +0 -0
  250. package/web/dist/assets/lxgwwenkaiscreen-subset-34-BhinPYWX.woff2 +0 -0
  251. package/web/dist/assets/lxgwwenkaiscreen-subset-35-D_njRQqA.woff2 +0 -0
  252. package/web/dist/assets/lxgwwenkaiscreen-subset-36-DqXlLE3t.woff2 +0 -0
  253. package/web/dist/assets/lxgwwenkaiscreen-subset-37-j5LbvMph.woff2 +0 -0
  254. package/web/dist/assets/lxgwwenkaiscreen-subset-38-oO4jWdKm.woff2 +0 -0
  255. package/web/dist/assets/lxgwwenkaiscreen-subset-39-CabsX4rw.woff2 +0 -0
  256. package/web/dist/assets/lxgwwenkaiscreen-subset-4-PatS3YHh.woff2 +0 -0
  257. package/web/dist/assets/lxgwwenkaiscreen-subset-40-yrLg3C3e.woff2 +0 -0
  258. package/web/dist/assets/lxgwwenkaiscreen-subset-41-DBXmCQhc.woff2 +0 -0
  259. package/web/dist/assets/lxgwwenkaiscreen-subset-42-Bqij_UiQ.woff2 +0 -0
  260. package/web/dist/assets/lxgwwenkaiscreen-subset-43-D3kp0Ntc.woff2 +0 -0
  261. package/web/dist/assets/lxgwwenkaiscreen-subset-44-B_BNGvK4.woff2 +0 -0
  262. package/web/dist/assets/lxgwwenkaiscreen-subset-45-CZc0cQWs.woff2 +0 -0
  263. package/web/dist/assets/lxgwwenkaiscreen-subset-46-CHQX5Kd9.woff2 +0 -0
  264. package/web/dist/assets/lxgwwenkaiscreen-subset-47-Ci8gLrHA.woff2 +0 -0
  265. package/web/dist/assets/lxgwwenkaiscreen-subset-48-run3ugD3.woff2 +0 -0
  266. package/web/dist/assets/lxgwwenkaiscreen-subset-49-DgfiUb6u.woff2 +0 -0
  267. package/web/dist/assets/lxgwwenkaiscreen-subset-5-B0CtGbX_.woff2 +0 -0
  268. package/web/dist/assets/lxgwwenkaiscreen-subset-50-CdCG-POr.woff2 +0 -0
  269. package/web/dist/assets/lxgwwenkaiscreen-subset-51-L2okeQZ2.woff2 +0 -0
  270. package/web/dist/assets/lxgwwenkaiscreen-subset-52-BZAqyGJW.woff2 +0 -0
  271. package/web/dist/assets/lxgwwenkaiscreen-subset-53-B0xWqLeU.woff2 +0 -0
  272. package/web/dist/assets/lxgwwenkaiscreen-subset-54-BZVQX-EA.woff2 +0 -0
  273. package/web/dist/assets/lxgwwenkaiscreen-subset-55-093mkIUJ.woff2 +0 -0
  274. package/web/dist/assets/lxgwwenkaiscreen-subset-56-CbhWG8G-.woff2 +0 -0
  275. package/web/dist/assets/lxgwwenkaiscreen-subset-57-pLQuvo6c.woff2 +0 -0
  276. package/web/dist/assets/lxgwwenkaiscreen-subset-58-BHhoY50x.woff2 +0 -0
  277. package/web/dist/assets/lxgwwenkaiscreen-subset-59-D1_6DuIQ.woff2 +0 -0
  278. package/web/dist/assets/lxgwwenkaiscreen-subset-6-BnAelmYU.woff2 +0 -0
  279. package/web/dist/assets/lxgwwenkaiscreen-subset-60-HDBIuLTZ.woff2 +0 -0
  280. package/web/dist/assets/lxgwwenkaiscreen-subset-61-C-c13SeU.woff2 +0 -0
  281. package/web/dist/assets/lxgwwenkaiscreen-subset-62-kHRVOhZ2.woff2 +0 -0
  282. package/web/dist/assets/lxgwwenkaiscreen-subset-63-C0MCsvFv.woff2 +0 -0
  283. package/web/dist/assets/lxgwwenkaiscreen-subset-64-DZekguCj.woff2 +0 -0
  284. package/web/dist/assets/lxgwwenkaiscreen-subset-65-CtP6c3IU.woff2 +0 -0
  285. package/web/dist/assets/lxgwwenkaiscreen-subset-66-DLVWoMSf.woff2 +0 -0
  286. package/web/dist/assets/lxgwwenkaiscreen-subset-67-BVuOnayH.woff2 +0 -0
  287. package/web/dist/assets/lxgwwenkaiscreen-subset-68-DpxQ6Mnx.woff2 +0 -0
  288. package/web/dist/assets/lxgwwenkaiscreen-subset-69-qesBxLmC.woff2 +0 -0
  289. package/web/dist/assets/lxgwwenkaiscreen-subset-70-BBt1xN7h.woff2 +0 -0
  290. package/web/dist/assets/lxgwwenkaiscreen-subset-71-BWqZacwB.woff2 +0 -0
  291. package/web/dist/assets/lxgwwenkaiscreen-subset-72-B1wP_Yst.woff2 +0 -0
  292. package/web/dist/assets/lxgwwenkaiscreen-subset-73-C99T7jNZ.woff2 +0 -0
  293. package/web/dist/assets/lxgwwenkaiscreen-subset-74-DrUTsWBe.woff2 +0 -0
  294. package/web/dist/assets/lxgwwenkaiscreen-subset-75-C4M5XXQe.woff2 +0 -0
  295. package/web/dist/assets/lxgwwenkaiscreen-subset-76-Bq8spVWF.woff2 +0 -0
  296. package/web/dist/assets/lxgwwenkaiscreen-subset-77-D9vNupE2.woff2 +0 -0
  297. package/web/dist/assets/lxgwwenkaiscreen-subset-78-D6th7Gnu.woff2 +0 -0
  298. package/web/dist/assets/lxgwwenkaiscreen-subset-79-bezM85cS.woff2 +0 -0
  299. package/web/dist/assets/lxgwwenkaiscreen-subset-80-DD8pnhG2.woff2 +0 -0
  300. package/web/dist/assets/lxgwwenkaiscreen-subset-81-N6jtSyBs.woff2 +0 -0
  301. package/web/dist/assets/lxgwwenkaiscreen-subset-82-D6c180bZ.woff2 +0 -0
  302. package/web/dist/assets/lxgwwenkaiscreen-subset-83-em17AadM.woff2 +0 -0
  303. package/web/dist/assets/lxgwwenkaiscreen-subset-84-CJ8gdrMK.woff2 +0 -0
  304. package/web/dist/assets/lxgwwenkaiscreen-subset-85-9PPGO1eB.woff2 +0 -0
  305. package/web/dist/assets/lxgwwenkaiscreen-subset-86-CjWPf-u5.woff2 +0 -0
  306. package/web/dist/assets/lxgwwenkaiscreen-subset-87-8D_X_yOC.woff2 +0 -0
  307. package/web/dist/assets/lxgwwenkaiscreen-subset-88-9vtKFPBx.woff2 +0 -0
  308. package/web/dist/assets/lxgwwenkaiscreen-subset-89-BhE5jAKX.woff2 +0 -0
  309. package/web/dist/assets/lxgwwenkaiscreen-subset-90-D2Npfm5E.woff2 +0 -0
  310. package/web/dist/assets/lxgwwenkaiscreen-subset-91-C6Ft1-Eu.woff2 +0 -0
  311. package/web/dist/assets/lxgwwenkaiscreen-subset-97-CaExyQP9.woff2 +0 -0
  312. package/web/dist/assets/lxgwwenkaiscreen-subset-98-CPoVbsUd.woff2 +0 -0
  313. package/web/dist/assets/lxgwwenkaiscreen-subset-99-ZUQZGVgH.woff2 +0 -0
  314. package/web/dist/assets/{mindmap-definition-FBJOCRG2-BGt4omYC.js → mindmap-definition-FBJOCRG2-CalfL_Kn.js} +1 -1
  315. package/web/dist/assets/{pegDiagram-XKGWAZYB-uSfS-PBE.js → pegDiagram-XKGWAZYB-D9mVWz7l.js} +1 -1
  316. package/web/dist/assets/pending-edit-apply-kwMFWsUe.js +31 -0
  317. package/web/dist/assets/{pieDiagram-E7YTZNPT-DpeI935H.js → pieDiagram-E7YTZNPT-DELzbZNS.js} +1 -1
  318. package/web/dist/assets/{quadrantDiagram-AXDQQJYC-B7IuNvWa.js → quadrantDiagram-AXDQQJYC-TZqp4mfA.js} +1 -1
  319. package/web/dist/assets/{railroadDiagram-O6MQD6OU-ARzYpPQI.js → railroadDiagram-O6MQD6OU-DxX59f0i.js} +1 -1
  320. package/web/dist/assets/{requirementDiagram-EFPCY7ZU-Cj4upkAo.js → requirementDiagram-EFPCY7ZU-DFdsJLIe.js} +1 -1
  321. package/web/dist/assets/{rotate-cw-CXJcyHWk.js → rotate-cw-62liz7LX.js} +1 -1
  322. package/web/dist/assets/{sankeyDiagram-P5KCCOFB-OaVwSnLP.js → sankeyDiagram-P5KCCOFB-ByYcd3fG.js} +1 -1
  323. package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-1yrUbq4t.js → sequenceDiagram-WJ2MYXX4-Bj-ml1jS.js} +1 -1
  324. package/web/dist/assets/{sizeCapture-X5ZJPWSS-nYH5p9D6.js → sizeCapture-X5ZJPWSS-KhsXThyD.js} +1 -1
  325. package/web/dist/assets/{square-dashed-mouse-pointer-CTtTVKoH.js → square-dashed-mouse-pointer-D_VrqVpy.js} +1 -1
  326. package/web/dist/assets/{stateDiagram-HBIQ2CUA-Dj5HvyNq.js → stateDiagram-HBIQ2CUA-BJlpHfWU.js} +1 -1
  327. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-CR2kYAH2.js +1 -0
  328. package/web/dist/assets/{swimlanes-XN3QIQJK-umeT0HpB.js → swimlanes-XN3QIQJK-DUB9OJVs.js} +1 -1
  329. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BitOxnG2.js +8 -0
  330. package/web/dist/assets/{timeline-definition-24CTP7MA-MqnQfHKB.js → timeline-definition-24CTP7MA-C03NX82A.js} +1 -1
  331. package/web/dist/assets/{vennDiagram-4TSXK5OY-b2liyZra.js → vennDiagram-4TSXK5OY-B9pjzngp.js} +1 -1
  332. package/web/dist/assets/{wardleyDiagram-VM6X3IG4-QMK-zNh1.js → wardleyDiagram-VM6X3IG4-UqWq_mcT.js} +1 -1
  333. package/web/dist/assets/{xychartDiagram-S5SC5T6Z-D5Mc6tfA.js → xychartDiagram-S5SC5T6Z-D2pDKtOv.js} +1 -1
  334. package/web/dist/index.html +2 -2
  335. package/server/engine/mcp/tools/arrange-on-board.js +0 -126
  336. package/server/engine/mcp/tools/create-on-board.js +0 -152
  337. package/server/engine/mcp/tools/edit-sketch.js +0 -167
  338. package/server/engine/mcp/tools/relate-on-board.js +0 -150
  339. package/server/engine/mcp/tools/sketch-on-board.js +0 -345
  340. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +0 -235
  341. 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
  342. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +0 -268
  343. package/web/dist/assets/DeckWindow-Bg3rgkvi.js +0 -11
  344. package/web/dist/assets/channel-DFyiuGL0.js +0 -1
  345. package/web/dist/assets/classDiagram-DTDB5LWJ-Aw1f8z-b.js +0 -1
  346. package/web/dist/assets/classDiagram-v2-JRS7N3AN-Aw1f8z-b.js +0 -1
  347. package/web/dist/assets/index-C0KUf6Wd.css +0 -1
  348. package/web/dist/assets/index-DGG5wIZs.js +0 -2118
  349. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  350. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  351. package/web/dist/assets/pending-edit-apply-uP4uvrOD.js +0 -31
  352. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-kJ1m_zJT.js +0 -1
  353. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-DPSkMbbl.js +0 -8
@@ -0,0 +1,19 @@
1
+ /**
2
+ * mcp/server-name.js —— MCP 服务名的唯一真相源(2026-08-25)
3
+ *
4
+ * 这个名字有两个读者,而且**改错了不会报错,只会静默失效**:
5
+ * 1. session-loop.js 的 `mcpServers: { [MCP_SERVER_NAME]: server }` —— 决定
6
+ * 工具在模型眼里叫 `mcp__<名>__<工具>`
7
+ * 2. isolation.js 的 `permissions.allow: ['mcp__<名>']` —— 决定这些工具跳不跳
8
+ * auto 模式分类器
9
+ * 两处对不上时:工具照常能用,只是每一次调用又开始过分类器,没有任何报错。
10
+ * 所以名字收在这里一份,两边都 import,并由 isolation.test.js 钉住。
11
+ *
12
+ * 单独一个文件而不是挂在 mcp/index.js:isolation.js 只要这一个字符串,
13
+ * 不该为此把整棵工具树(连带 DB 模块)拖进来。
14
+ */
15
+
16
+ export const MCP_SERVER_NAME = 'nodesign';
17
+
18
+ /** 给 permissions.allow 用的整服务放行规则 */
19
+ export const MCP_ALLOW_RULE = `mcp__${MCP_SERVER_NAME}`;
@@ -257,11 +257,12 @@ export const ARTIFACT_BATCHABLE = [
257
257
  'screenshot_canvas', 'trace_motion', 'get_computed_styles', 'explain_style', 'query_elements',
258
258
  ];
259
259
 
260
- export function makeArtifactBatchTool({ tools }) {
260
+ export function makeArtifactBatchTool({ tools, resolve = null }) {
261
261
  const names = ARTIFACT_BATCHABLE.filter(n => tools.some(t => t.name === n));
262
262
  return makeBatchTool({
263
263
  name: 'artifact_batch',
264
264
  tools,
265
+ resolve,
265
266
  batchable: ARTIFACT_BATCHABLE,
266
267
  finalShot: { name: 'artifact_computer', input: { action: 'screenshot' } },
267
268
  description: `Run several artifact-session steps in ONE round trip: open → click → type →
@@ -85,20 +85,36 @@ as an error telling you to find again.`,
85
85
  * @param {string[]} o.batchable 允许的名字
86
86
  * @param {{name:string, input:object}} [o.finalShot] 结尾补的那张"当前状态";不传就不补
87
87
  */
88
- export function makeBatchTool({ name, description, tools, batchable, finalShot }) {
88
+ export function makeBatchTool({ name, description, tools = [], batchable, resolve = null, finalShot }) {
89
+ // resolve(2026-08-27 重置,清 08-26 那条「batch 绕闸」挂账):传了它,子工具在
90
+ // **运行时**从装配完成后的注册表取(index.js 在包装管线跑完后回填 wrappedByName),
91
+ // 能力闸 / 参数消毒 / 模式过滤对 batch 内的调用照常生效。batchable 仍是**政策名单**
92
+ // (哪些名字许进 batch);resolve 查不到 = 这台机器/该模式把它下架了,如实报。
93
+ // 不传 resolve 走旧的实例表(单测直连用 —— 但生产装配一律传 resolve)。
89
94
  const byName = new Map(tools.filter(t => batchable.includes(t.name)).map(t => [t.name, t]));
90
- const names = [...byName.keys()];
95
+ const lookup = (n) => (resolve ? (batchable.includes(n) ? resolve(n) : undefined) : byName.get(n));
96
+ const names = resolve ? [...batchable] : [...byName.keys()];
97
+ const shotDefault = finalShot ? finalShot.default !== false : false;
91
98
  return tool(
92
99
  name,
93
100
  description,
94
101
  {
95
- actions: z.array(z.object({
102
+ // looseObject(2026-08-29):**放错层的参数不许静默消失**。模型很自然会写
103
+ // {name, chain:true, input:{…}} —— chain 是 write_on_board 的参数,写在了
104
+ // action 这一层。z.object 默认剥掉未知键,于是那一步照跑、只是丢了半个意图
105
+ // (真会话 proj_mtdr2xpa:两章的 chain:true 都这么没的,章节不接线程、
106
+ // 落到兜底位,正文列当场散架,用户看到的是"摆位乱了")。收下它们,在
107
+ // handler 里按子工具的 schema 归位,并如实报一句。
108
+ actions: z.array(z.looseObject({
96
109
  name: z.string().describe(`Tool name: one of ${names.join(' | ')}.`),
97
- input: z.record(z.string(), z.unknown()).optional().describe("That tool's input, same shape as calling it directly."),
110
+ input: z.record(z.string(), z.unknown()).optional().describe("That tool's input, same shape as calling it directly. Put the tool's OWN parameters in here — a parameter written beside `name` instead of inside `input` is folded in for you and reported."),
98
111
  })).min(1).max(MAX_ITEMS)
99
112
  .describe(`1-${MAX_ITEMS} items, run in order. Each item: {"name": <tool>, "input": {...}}.`),
100
- screenshotAfter: z.boolean().optional()
101
- .describe('Append a screenshot of the resulting state at the end (default true). Set false for text-only sequences to save tokens.'),
113
+ // 没有 finalShot 就不放这个参数 —— 写一个永远无效的旋钮是在骗模型
114
+ ...(finalShot ? { screenshotAfter: z.boolean().optional()
115
+ .describe(shotDefault
116
+ ? 'Append a look at the resulting state at the end (default true). Set false for text-only sequences to save tokens.'
117
+ : 'Pass true to append a look at the resulting state at the end (default false — most board upkeep is text work; ask for eyes when looks matter).') } : {}),
102
118
  },
103
119
  async ({ actions, screenshotAfter }, extra) => {
104
120
  const out = [];
@@ -106,18 +122,60 @@ export function makeBatchTool({ name, description, tools, batchable, finalShot }
106
122
  let failedAt = -1;
107
123
  let failText = '';
108
124
  let lastHadImage = false;
125
+ let shotLifted = false;
109
126
  for (let i = 0; i < n; i += 1) {
110
- const { name: toolName, input } = actions[i];
127
+ const { name: toolName, input, ...stray } = actions[i];
111
128
  const label = `[${i + 1}/${n}] ${toolName}${input?.action ? ` ${input.action}` : ''}`;
112
129
  if (failedAt >= 0) { out.push({ type: 'text', text: `${label}: ${HALT_TEXT}` }); continue; }
113
- const def = byName.get(toolName);
130
+ const def = lookup(toolName);
114
131
  if (!def) {
115
132
  failedAt = i;
116
- failText = `"${toolName}" is not batchable here (use one of ${names.join(', ')}).`;
133
+ failText = batchable.includes(toolName)
134
+ ? `"${toolName}" 在这台机器/该项目模式下没有注册(能力或模式闸下架了它)——这一步换别的路。`
135
+ : `"${toolName}" is not batchable here (use one of ${names.join(', ')}).`;
117
136
  out.push({ type: 'text', text: `${label}: Error: ${failText}` });
118
137
  continue;
119
138
  }
120
- const parsed = z.object(def.inputSchema).safeParse(input || {});
139
+ // 归位(08-29):action 这一层的多余键按子工具的 schema 收进 input;
140
+ // screenshotAfter 是 batch 自己的旋钮,抬到 batch 层。都如实报 —— 静默
141
+ // 修正会让下一次还写错,静默丢弃会让这一次就出错。
142
+ let folded = input || {};
143
+ const notes = [];
144
+ const own = new Set(Object.keys(def.inputSchema || {}));
145
+ const strayKeys = Object.keys(stray);
146
+ if (strayKeys.length) {
147
+ const patch = {};
148
+ for (const k of strayKeys) {
149
+ if (k === 'screenshotAfter') { shotLifted = shotLifted || stray[k] === true; notes.push('screenshotAfter 是整批的旋钮,已抬到 batch 层'); continue; }
150
+ if (own.has(k) && !(k in folded)) { patch[k] = stray[k]; continue; }
151
+ notes.push(`${k} 不是 ${toolName} 的参数,忽略了`);
152
+ }
153
+ if (Object.keys(patch).length) {
154
+ folded = { ...folded, ...patch };
155
+ notes.push(`${Object.keys(patch).join('/')} 本该写在 input 里(已收进去,下次直接写 input 内)`);
156
+ }
157
+ }
158
+ // 双层信封(2026-08-30):{"input":{"input":{…},"op":"x"}} 真实发生过 ——
159
+ // 模型要拼两级嵌套,多包一层不出声。里层不是这个工具的参数、而里面的键是,
160
+ // 就摊平上来。
161
+ if (!own.has('input') && folded.input && typeof folded.input === 'object' && !Array.isArray(folded.input)) {
162
+ const inner = folded.input;
163
+ if (Object.keys(inner).some((k) => own.has(k))) {
164
+ const { input: _drop, ...rest } = folded;
165
+ folded = { ...inner, ...rest };
166
+ notes.push('input 里又套了一层 input,已摊平(下次一层就够)');
167
+ }
168
+ }
169
+ // ⛔ input **内部**的未知键:z.object 静默 strip,不报错不留痕。08-29 那把
170
+ // looseObject 只盖住了写在 name 旁边的(action 这一层),差一层没盖到 ——
171
+ // 真会话里 write_on_board 收到过整份 `facts`(一长串剧情事实),一个字没落、
172
+ // 一句话没报。归不了位就点名,别静默丢。
173
+ const unknownIn = Object.keys(folded).filter((k) => !own.has(k));
174
+ if (unknownIn.length) {
175
+ notes.push(`⛔ ${unknownIn.join('/')} 不是 ${toolName} 的参数,这次**整个丢掉了**(内容没落盘)`
176
+ + `—— ${toolName} 收的是:${[...own].join('/')}`);
177
+ }
178
+ const parsed = z.object(def.inputSchema).safeParse(folded);
121
179
  if (!parsed.success) {
122
180
  failedAt = i;
123
181
  const why = parsed.error.issues.map(is => `${is.path.join('.') || '(root)'}: ${is.message}`).join('; ');
@@ -134,10 +192,12 @@ export function makeBatchTool({ name, description, tools, batchable, finalShot }
134
192
  // 第一块文本前面挂上 [i/n] 标签;没有文本块(纯图)就补一行标签再放图
135
193
  if (!blocks.some(b => b.type === 'text')) out.push({ type: 'text', text: `${label}: done` });
136
194
  let prefixed = false;
195
+ const tail = notes.length ? `\n⚠ ${notes.join(';')}` : '';
137
196
  for (const b of blocks) {
138
- if (b.type === 'text' && !prefixed) { out.push({ type: 'text', text: `${label}: ${b.text}` }); prefixed = true; }
197
+ if (b.type === 'text' && !prefixed) { out.push({ type: 'text', text: `${label}: ${b.text}${tail}` }); prefixed = true; }
139
198
  else out.push(b);
140
199
  }
200
+ if (!prefixed && tail) out.push({ type: 'text', text: `${label}:${tail}` });
141
201
  if (r?.isError) {
142
202
  failedAt = i;
143
203
  failText = blocks.find(b => b.type === 'text')?.text || '(no error text)';
@@ -154,8 +214,9 @@ export function makeBatchTool({ name, description, tools, batchable, finalShot }
154
214
  + `Steps 1-${failedAt} already ran — do NOT re-run the whole batch; continue from the failed step.`,
155
215
  });
156
216
  }
157
- if (finalShot && screenshotAfter !== false && !lastHadImage) {
158
- const shotDef = byName.get(finalShot.name);
217
+ const wantShot = screenshotAfter === undefined ? (shotLifted || shotDefault) : screenshotAfter;
218
+ if (finalShot && wantShot && !lastHadImage) {
219
+ const shotDef = lookup(finalShot.name);
159
220
  if (shotDef) {
160
221
  try {
161
222
  const s = await shotDef.handler(finalShot.input || {}, extra);
@@ -171,11 +232,12 @@ export function makeBatchTool({ name, description, tools, batchable, finalShot }
171
232
  );
172
233
  }
173
234
 
174
- export function makeBrowserBatchTool({ tools }) {
235
+ export function makeBrowserBatchTool({ tools, resolve = null }) {
175
236
  const names = BATCHABLE.filter(n => tools.some(t => t.name === n));
176
237
  return makeBatchTool({
177
238
  name: 'browser_batch',
178
239
  tools,
240
+ resolve,
179
241
  batchable: BATCHABLE,
180
242
  finalShot: { name: 'browser_screenshot', input: {} },
181
243
  description: `Run a sequence of browser tool calls in ONE round trip. Each item is
@@ -458,7 +458,7 @@ about why they were taken is landfill.`,
458
458
  normalize: normalizeShot,
459
459
  });
460
460
  for (const f of r.files) {
461
- try { ctx?.emit?.({ type: 'run.file_changed', path: f.rel, change: 'add' }); } catch { /* */ }
461
+ try { ctx?.emit?.({ type: 'run.file_changed', filePath: f.rel, event: 'add' }); } catch { /* */ }
462
462
  }
463
463
  const lines = [`从 ${r.data.title || r.data.url} 采下来了:`];
464
464
  for (const f of r.files) lines.push(` ${f.rel}(${(f.bytes / 1024).toFixed(0)} KB,${f.kind})`);
@@ -0,0 +1,167 @@
1
+ /**
2
+ * mcp/tools/cast-role.js — cast_role MCP tool(2026-08-26 建;2026-08-28 演员位重构重写)
3
+ *
4
+ * ## 重写前后
5
+ *
6
+ * 旧语义:写 `.claude/agents/rp-<id>.md`(一角色一个子代理定义),派发要等 CLI 重扫。
7
+ * ⛔ 那条路 08-28 勘定为生产不可靠:会话中途写入的定义文件只有**写入时活着的那个
8
+ * CLI 进程**认得(chokidar 事件),resume 换进程后永久 `not found`(三场真会话:
9
+ * 12 连败 / 恰好成功 / 永久失明)。等待、轮询、公告续接五版都救不了,全史见 git
10
+ * 里本文件的旧版头注。
11
+ *
12
+ * 新语义:角色卡是**数据**,子代理是**预注册的演员位**(rp-actor / rp-narrator,
13
+ * 建项目时就落盘,见 cast.js slotAgentFile)。cast_role 只做两件事:
14
+ * 1. 把人设写成 `角色/<名>/角色卡.md`(用户在画布上看得见、随时能改的文件夹范式)
15
+ * 2. 在 `.nd/cast.json` 登记 slug → 展示名/笔权/卡路径(板书署名与名册 API 的展示源)
16
+ * 写完**当回合就能派**:Agent(subagent_type: 演员位, name: "rp-<id>", prompt: 卡)。
17
+ *
18
+ * 登记不是上场的前置(不登记直接派也行),它换来的是:板上署名是「程晚」不是
19
+ * slug、重启后按卡重新开演有据可查、用户能在角色文件夹里改人设。
20
+ */
21
+
22
+ import path from 'node:path';
23
+ import fs from 'node:fs/promises';
24
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
25
+ import { z } from 'zod';
26
+ import { ROLE_PREFIX, ROLE_SLOT, isSlotType, isValidRoleSlug } from '../../agent/cast.js';
27
+ import { readCastRegistry } from '../../agent/role-card.js';
28
+
29
+ /** 角色 id(不含 rp- 前缀):ASCII、能当文件名、能当 SendMessage 收件人名。
30
+ * ⚠️ 这是 cast.js `ROLE_SLUG_RE` 的**收紧子集**(这边只许小写、≥2 字符)——
31
+ * 两个判据的蕴含关系(ID_RE 过的 id 拼上前缀必被 ROLE_SLUG_RE 认)由
32
+ * cast-role.test.js 钉着;改任何一边先看那条测试。导出仅供测试。 */
33
+ export const ID_RE = /^[a-z0-9][a-z0-9_-]{1,40}$/;
34
+
35
+ const ROLES_DIR = '角色';
36
+ const REGISTRY_REL = '.nd/cast.json';
37
+ const PERSONA_MAX = 20000;
38
+
39
+ /** 自由文本压一行:控制字符/行终止符全清(进 JSON 与返回话术都安全) */
40
+ function oneLine(v, max) {
41
+ return String(v ?? '')
42
+ .replace(/[\r\n\u2028\u2029]+/g, ' ')
43
+ .replace(/[\u0000-\u001f\u007f]/g, '')
44
+ .trim().slice(0, max);
45
+ }
46
+
47
+ /** 展示名 → 文件夹名:剥路径分隔与点前缀,空了退回 slug(文件夹名坏不了路径) */
48
+ function folderNameFor(displayName, slug) {
49
+ const cleaned = oneLine(displayName, 40).replace(/[\/\\]/g, '').replace(/^\.+/, '').trim();
50
+ return cleaned || slug;
51
+ }
52
+
53
+ export function makeCastRoleTool({ workspaceRoot, sessionId = null, ctx, roster = null }) {
54
+ void sessionId; void roster; // 旧签名的调用方兼容位(公告续接与回合闸已随重构退役)
55
+ return tool(
56
+ 'cast_role',
57
+ `Register a character card so this person has a fixed identity to act from.
58
+
59
+ Writes the card to ${ROLES_DIR}/<name>/角色卡.md (user-visible, user-editable) and records
60
+ the display name. The card is DATA, and in this build it is a reference for YOU:
61
+ character subagents are disabled, so you write everyone on stage yourself.
62
+
63
+ Use it for anyone who has a name, will show up again, and whom the user may want to talk
64
+ to. Re-read the card right before you write that person's lines — that is the thing that
65
+ keeps several characters from all sounding like you. A walk-on with one line needs no card.
66
+
67
+ ⛔ Do NOT dispatch a role subagent after this: no Agent(subagent_type: "${ROLE_SLOT}"),
68
+ no SendMessage to a character. That path is being debugged; the cards you write now will
69
+ plug straight back in once it returns.
70
+ When the user hands you a ready-made character card, prefer copying it in verbatim over
71
+ rewriting it.`,
72
+ {
73
+ id: z.string().describe('ASCII slug, no prefix: lowercase letters/digits/_/- , 2-41 chars. Becomes the address (rp-<id>).'),
74
+ name: z.string().min(1).max(40).describe('Display name, any language — what this character is actually called. Board signatures use it.'),
75
+ duty: z.string().min(1).max(400).describe('One line: who this is and when you would talk to them.'),
76
+ persona: z.string().min(1).describe("The card itself: who this person is, how they speak, what they would never do, plus two or three short samples of their voice. You read this back before writing their lines, so write it for that use — no meta rules about who writes what."),
77
+ },
78
+ async (args) => {
79
+ const fail = (msg) => ({ content: [{ type: 'text', text: msg }], isError: true });
80
+ if (!workspaceRoot) return fail('没有工作区,cast_role 不可用。');
81
+
82
+ const id = String(args.id || '').trim();
83
+ if (!ID_RE.test(id)) {
84
+ return fail(`角色 id「${id}」不合法:只能用小写字母、数字、下划线、连字符,2-41 个字符,首字符是字母或数字。`
85
+ + `(这个 id 要当 SendMessage 的收件人名,收件人名不收中文和空格 —— 中文名字放 name 参数。)`);
86
+ }
87
+ const slug = `${ROLE_PREFIX}${id}`;
88
+ if (!isValidRoleSlug(slug) || isSlotType(slug)) return fail(`角色名「${slug}」过不了名册校验(演员位的名字不能当角色名)。`);
89
+
90
+ const displayName = oneLine(args.name, 40);
91
+ if (!displayName) return fail('name 清洗后是空的 —— 换个展示名。');
92
+ const persona = String(args.persona || '').trim();
93
+ if (!persona) return fail('persona 是空的 —— 角色卡就是这个角色的全部身份,不能空。');
94
+ if (persona.length > PERSONA_MAX) {
95
+ return fail(`persona 有 ${persona.length} 字符,超过 ${PERSONA_MAX} 上限。`
96
+ + `把大部头设定挪进世界书/设定文件让角色自己 grep,卡上留人设主干。`);
97
+ }
98
+
99
+ // 卡落盘:角色/<名>/角色卡.md(文件夹范式:这个文件夹就是该角色的家,
100
+ // 之后的记忆/日记等件都住这里;用户随时可改,改动对"下次派发/唤醒后重读卡"生效)
101
+ const folder = folderNameFor(displayName, slug);
102
+ const dir = path.join(workspaceRoot, ROLES_DIR, folder);
103
+ const file = path.join(dir, '角色卡.md');
104
+ if (!path.resolve(file).startsWith(path.resolve(workspaceRoot, ROLES_DIR) + path.sep)) {
105
+ return fail('角色卡路径异常,拒绝写入。');
106
+ }
107
+ // 一个家只住一个角色(2026-08-28 对账发现):文件夹按展示名取,两个不同的
108
+ // slug 用同一个展示名就会共用 角色/<名>/ —— 卡被后来者覆盖,**记忆.md 也共用**,
109
+ // 于是 A 的 jot_memory 写进 B 的记忆里,谁都不会发现。堵在写口,不是在
110
+ // roleHomeDir 那头兜(判据别建在模型可写的登记表上)。
111
+ // 同一个 slug 重登(改卡)不受影响 —— 那本来就是同一个人。
112
+ const claimedBy = Object.entries((await readCastRegistry(workspaceRoot)).roles || {})
113
+ .find(([s2, e2]) => s2 !== slug && typeof e2?.card === 'string'
114
+ && e2.card.split('/').slice(0, 2).join('/') === `${ROLES_DIR}/${folder}`);
115
+ if (claimedBy) {
116
+ return fail(`「${displayName}」这个家已经是 ${claimedBy[0]} 的了(角色/${folder}/)。`
117
+ + `同名两个角色会共用角色卡和记忆件 —— 换个展示名,`
118
+ + `或者你要的其实是同一个人的话,用 ${claimedBy[0]} 这个名字重登(改卡就是重登同一个 slug)。`);
119
+ }
120
+ let existed = false;
121
+ try { await fs.access(file); existed = true; } catch { /* 新角色 */ }
122
+ await fs.mkdir(dir, { recursive: true });
123
+ // 记忆件骨架(只在不存在时铺):角色 jot_memory 追加,用户/GM 可整理
124
+ const memFile = path.join(dir, '记忆.md');
125
+ try { await fs.access(memFile); } catch {
126
+ await fs.writeFile(memFile, `# 记忆\n\n<!-- ${slug} 的记忆:角色自己 jot_memory 追加,用户和 GM 可整理改写。还是空的。 -->\n`, 'utf8');
127
+ }
128
+ await fs.writeFile(file, [
129
+ `# ${displayName}`,
130
+ '',
131
+ `<!-- ${slug} · cast_role 登记。`,
132
+ ' 正文就是角色的人设,派发时全文随 prompt 进入角色 —— 想改人设直接改这里,',
133
+ ' 对已在场的角色用 SendMessage 告知,对下次开演自动生效。 -->',
134
+ '',
135
+ persona,
136
+ '',
137
+ ].join('\n'), 'utf8');
138
+
139
+ // 登记表:板书署名与名册 API 的展示名来源(fail-soft:登记坏了不拦上场)
140
+ const cardRel = path.join(ROLES_DIR, folder, '角色卡.md');
141
+ try {
142
+ const reg = await readCastRegistry(workspaceRoot);
143
+ reg.roles[slug] = { name: displayName, duty: oneLine(args.duty, 400), card: cardRel };
144
+ await fs.mkdir(path.join(workspaceRoot, '.nd'), { recursive: true });
145
+ await fs.writeFile(path.join(workspaceRoot, REGISTRY_REL), JSON.stringify(reg, null, 2), 'utf8');
146
+ } catch { /* 展示层,坏了角色照样能上场(署名退回 slug) */ }
147
+
148
+ try { ctx?.emit?.({ type: 'run.role_cast', slug, name: displayName }); } catch { /* 事件失败不挡正事 */ }
149
+
150
+ // ⚠️ 这段话和工具 description 是**同一条教义的两个读者**,改一头必须改另一头 ——
151
+ // 08-30 停用子代理那一刀只改了 description,这里还留着「现在就可以派它上场」,
152
+ // 而返回文案离模型的下一个动作更近,等于白改(fable 评审当场抓到)。
153
+ const lines = [
154
+ `${existed ? '改写' : '写好'}了角色卡「${displayName}」→ ${cardRel}(登记为 ${slug})`,
155
+ '',
156
+ `⛔ **不要派子代理**:这一版角色由你自己演,`
157
+ + `Agent(subagent_type: "${ROLE_SLOT}") 和 SendMessage 给角色这两条路正在调试。`,
158
+ `轮到「${displayName}」说话时,**先 Read 一遍 ${cardRel}** 把腔调找回来,再由你写他这一段。`,
159
+ `几个人同场就一个个来,每人开口前各读各的卡 —— 这是防止全场一个腔的正事。`,
160
+ ];
161
+ if (existed) {
162
+ lines.push('', '⚠️ 卡改了,下次写他的话之前重新 Read 一遍 —— 你上下文里那份是旧的。');
163
+ }
164
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
165
+ },
166
+ );
167
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * mcp/tools/draw-trend.js —— `draw_trend`:状态表历史 → 手绘趋势线
3
+ * (2026-08-30 画图能力线·「活图」第一块)
4
+ *
5
+ * 数据不用记:状态表住在板书文件里、文件进 git、**每回合落一个 commit** ——
6
+ * 「好感度随拍数怎么走」的序列是现成的,git 就是时间轴(lib/state-trend.js)。
7
+ *
8
+ * 重画语义:同 key 再调一次 = 原地重画(旧的擦掉、**位置保留** —— 用户拖过它
9
+ * 摆在哪就还在哪)。所以每拍 set_vars 之后想让曲线长一格,再调一次就行。
10
+ */
11
+
12
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
13
+ import { z } from 'zod';
14
+ import { readBoard, patchBoard, removeByTag } from '../../../projects/board-store.js';
15
+ import { trendSeries, trendGeometry } from '../../../lib/state-trend.js';
16
+ import { roughFreePath, textBox } from '../../../lib/sketch-layout.js';
17
+ import { KEY_MAX } from '../../../lib/state-table.js';
18
+ import { currentSheet, toWorld } from '../../../lib/board-sheets.js';
19
+ import { currentSheetIdOf } from '../../../lib/sheet-state.js';
20
+ import { estimateSizeOn } from '../../../lib/board-kind-sizes.js';
21
+ import { Events } from '../../agent/events.js';
22
+
23
+ let seq = 0;
24
+ const stamp = () => `${Date.now().toString(36)}${(seq++ % 1000).toString(36)}`;
25
+
26
+ export function makeDrawTrendTool({ projectId, sharedRoot, sessionId, ctx }) {
27
+ return tool(
28
+ 'draw_trend',
29
+ `Draw a hand-inked trend line of one state-table key over the story so far — 好感度
30
+ climbing across eight beats, HP bleeding down. The series comes from the workspace's
31
+ git history (one commit per turn), so there is nothing to record: if the number lived
32
+ in the state table, its past is already there. Call it again after set_vars and the
33
+ chart redraws IN PLACE (position kept, even if the user dragged it). Needs ≥2 numeric
34
+ points — early beats will refuse loudly, that is normal.`,
35
+ {
36
+ key: z.string().min(1).max(KEY_MAX).describe('Which state-table key to chart (must hold numbers — 8/10 counts as 8)'),
37
+ at: z.object({ x: z.number().min(0).max(12000), y: z.number().min(0).max(12000) }).optional()
38
+ .describe('Sheet-local pixels for first placement. Omit it: lands under the state-table card (or where the previous chart of this key sits)'),
39
+ },
40
+ async (args) => {
41
+ const err = (t) => ({ content: [{ type: 'text', text: t }], isError: true });
42
+ if (!projectId) return err('No project bound.');
43
+ const s = await trendSeries(sharedRoot, args.key);
44
+ if (!s.ok) return err(`⛔ ${s.why}`);
45
+
46
+ const g = trendGeometry(s.points);
47
+ const tag = `trend-${args.key}`.slice(0, 40);
48
+ const board = await readBoard(projectId);
49
+
50
+ // 落点:旧图原位 > at(纸内像素) > 状态表卡正下方
51
+ let origin = null;
52
+ const prev = Object.entries(board.objects || {}).filter(([, e]) => e?.tag === tag && Number.isFinite(e?.x));
53
+ if (prev.length) {
54
+ origin = { x: Math.min(...prev.map(([, e]) => e.x)), y: Math.min(...prev.map(([, e]) => e.y)) };
55
+ await removeByTag(projectId, tag);
56
+ } else if (args.at) {
57
+ const sheet = currentSheet(board, currentSheetIdOf(sessionId));
58
+ if (!sheet) return err('还没有纸 —— 先 open_sheet,或者省掉 at 让它落在状态表卡下面。');
59
+ origin = toWorld(sheet, args.at);
60
+ } else {
61
+ const e = board.objects?.[s.rel];
62
+ if (!e || !Number.isFinite(e.x)) {
63
+ return err(`状态表那条板书(${s.rel})不在板上(没有座位)—— 传 at:{x,y}(纸内像素)指个位置。`);
64
+ }
65
+ const sz = estimateSizeOn(board, s.rel, e);
66
+ origin = { x: e.x, y: e.y + sz.h + 16 };
67
+ }
68
+
69
+ // 标签在图上方一行;曲线 ink、基线 pencil、现值小圈 red —— 全过同一支抖动笔
70
+ const label = `${args.key} ${s.points[0]} → ${s.points[s.points.length - 1]}(${s.points.length} 拍,${g.min}~${g.max})`;
71
+ const lb = textBox(label, 'sm');
72
+ const seed = `trend:${args.key}`;
73
+ const objects = {
74
+ [`text:a${stamp()}`]: {
75
+ x: Math.round(origin.x), y: Math.round(origin.y), w: lb.w, h: lb.h, z: 1, kind: 'text',
76
+ data: { t: label, font: 'kai', size: 'sm' }, zone: '', by: 'agent', seat: 'agent', tag,
77
+ },
78
+ [`scribble:a${stamp()}`]: {
79
+ x: Math.round(origin.x), y: Math.round(origin.y + lb.h + 4), w: g.w, h: g.h, z: 1, kind: 'scribble',
80
+ data: { d: roughFreePath(g.baselineD, `${seed}:base`), color: 'pencil', width: 1 }, zone: '', by: 'agent', seat: 'agent', tag,
81
+ },
82
+ [`scribble:b${stamp()}`]: {
83
+ x: Math.round(origin.x), y: Math.round(origin.y + lb.h + 4), w: g.w, h: g.h, z: 1, kind: 'scribble',
84
+ data: { d: roughFreePath(g.lineD, `${seed}:line`), color: 'ink', width: 2 }, zone: '', by: 'agent', seat: 'agent', tag,
85
+ },
86
+ [`scribble:c${stamp()}`]: {
87
+ x: Math.round(origin.x), y: Math.round(origin.y + lb.h + 4), w: g.w, h: g.h, z: 1, kind: 'scribble',
88
+ data: { d: g.dotD, color: 'red', width: 2 }, zone: '', by: 'agent', seat: 'agent', tag,
89
+ },
90
+ };
91
+ await patchBoard(projectId, { objects });
92
+ const rect = { x: Math.round(origin.x), y: Math.round(origin.y), w: g.w, h: g.h + lb.h + 4 };
93
+ try {
94
+ ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: `画了 ${args.key} 的趋势线` });
95
+ ctx?.emit?.(Events.boardFocus(rect, { tag, layer: '', soft: true }));
96
+ } catch { /* fail-soft */ }
97
+ return { content: [{ type: 'text', text:
98
+ `Trend #${tag}: ${s.points.length} points (${s.points.join(' → ')}), range ${g.min}~${g.max}, `
99
+ + `at (${rect.x},${rect.y}) ${rect.w}x${rect.h}${prev.length ? ' — redrawn in place' : ''}.\n`
100
+ + `Call draw_trend again after set_vars to grow it; move/group it like any sketch (tag #${tag} — `
101
+ + `edit_board follow can pin it to the 状态板 group).` }] };
102
+ },
103
+ );
104
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * edit_board 的 ops schema(2026-08-30 从 edit-board.js 拆出 —— 行数棘轮)。
3
+ * 纯数据;弱模型方言垫片($text 剥壳)与坐标钳制都住在这里。
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import { BINDING_TYPE_IDS, BINDING_MATERIALS } from '../../../lib/binding-types.js';
8
+ import { SLOT } from './open-sheet.js';
9
+
10
+ const clampTo = (lo, hi) => (v) => (typeof v === 'number' && Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : v);
11
+ const GAP = z.preprocess(clampTo(0, 400), z.number().min(0).max(400));
12
+ const PX_DELTA = z.preprocess(clampTo(-20000, 20000), z.number().min(-20000).max(20000));
13
+ /** 弱模型方言垫片:免费档模型给字符串字段裹 {$text:"…"} 壳(27/61 条真实错误,
14
+ * 且读不懂 zod 报文会原样重试到死)。单键 $text 自动剥壳,合法对象碰不到。 */
15
+ const unwrapText = (v) => (v && typeof v === 'object' && !Array.isArray(v)
16
+ && typeof v.$text === 'string' && Object.keys(v).length === 1) ? v.$text : v;
17
+ const ENDPOINT = z.preprocess(unwrapText, z.string().min(1).max(300));
18
+
19
+ const REL = z.object({
20
+ ref: z.string().min(1).max(300).describe('canvas id to place relative to'),
21
+ side: z.enum(['right', 'left', 'above', 'below']),
22
+ gap: GAP.optional().describe('PIXELS of breathing room between the two (default 24, max 400) — same unit as every position you read back'),
23
+ });
24
+ const DELTA = z.object({ dx: PX_DELTA, dy: PX_DELTA }).describe('PIXELS to shift by (+x right, +y down) — the same pixels read_board reports');
25
+ // 纸内绝对坐标(2026-08-29 纸范式):以某张纸版心左上为原点的像素。sheet 缺省 = 当前纸
26
+ const ABS = z.object({
27
+ x: z.preprocess((v) => (typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(12000, v)) : v), z.number().min(0).max(12000)),
28
+ y: z.preprocess((v) => (typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(12000, v)) : v), z.number().min(0).max(12000)),
29
+ sheet: z.string().max(40).optional().describe('which sheet these pixels are on (default the current one)'),
30
+ }).describe("PIXELS from a sheet's top-left writable corner — exact placement, you own the layout");
31
+ const TO = z.union([REL, ABS, DELTA]);
32
+
33
+ export const OP = z.discriminatedUnion('op', [
34
+ z.object({ op: z.literal('set_text'), id: z.string().min(1).max(300), text: z.string().min(1).max(8000).optional(), format: z.enum(['plain', 'md']).optional(), size: z.enum(['sm', 'md', 'lg', 'xl']).optional(), color: z.enum(['ink', 'red', 'pencil', 'brass']).optional(), font: z.enum(['pen', 'kai', 'sans', 'serif', 'mono']).optional() }),
35
+ z.object({ op: z.literal('move'), id: z.string().min(1).max(300), to: TO }),
36
+ z.object({ op: z.literal('move_group'), tag: z.string().min(1).max(40), to: TO }),
37
+ z.object({ op: z.literal('remove'), id: z.string().min(1).max(300) }),
38
+ z.object({ op: z.literal('add_node'), id: z.string().regex(/^[A-Za-z0-9_-]{1,24}$/).optional().describe('local handle for later ops of this call'), text: z.string().min(1).max(8000), format: z.enum(['plain', 'md']).optional(), size: z.enum(['sm', 'md', 'lg', 'xl']).optional(), font: z.enum(['pen', 'kai', 'sans', 'serif', 'mono']).optional(), color: z.enum(['ink', 'red', 'pencil', 'brass']).optional(), at: REL, tag: z.string().max(40).optional() }),
39
+ z.object({ op: z.literal('add_shape'), kind: z.enum(['rect', 'ellipse', 'circle', 'underline']), around: z.string().min(1).max(300).describe('canvas id to wrap/underline — the mark HUGS it and follows when it moves'), color: z.enum(['ink', 'red', 'pencil', 'brass']).optional(), width: z.number().min(1).max(12).optional(), tag: z.string().max(40).optional() }),
40
+ z.object({ op: z.literal('set_shape'), id: z.string().min(1).max(300), color: z.enum(['ink', 'red', 'pencil', 'brass']).optional(), width: z.number().min(1).max(12).optional() }),
41
+ z.object({ op: z.literal('add_edge'), from: ENDPOINT, to: ENDPOINT, type: z.enum(BINDING_TYPE_IDS).optional(), material: z.enum(BINDING_MATERIALS).optional(), label: z.string().max(60).optional(), tag: z.string().max(40).optional() }),
42
+ z.object({ op: z.literal('set_edge'), id: z.string().min(1).max(300), label: z.string().max(60).optional(), type: z.enum(BINDING_TYPE_IDS).optional(), material: z.enum(BINDING_MATERIALS).optional(), from: ENDPOINT.optional().describe('re-point the line: new source end'), to: ENDPOINT.optional().describe('re-point the line: new target end') }),
43
+ z.object({ op: z.literal('remove_edge'), id: z.string().min(1).max(300) }),
44
+ z.object({ op: z.literal('reflow'), tag: z.string().min(1).max(40), layout: z.enum(['column', 'row']).optional().describe('default column: restack the group in reading order with real sizes (use after set_text changes heights)') }),
45
+ z.object({ op: z.literal('follow'), group_tag: z.string().min(1).max(40).describe('the group that should follow (e.g. a status panel)'), target_tag: z.string().min(1).max(40).describe('whenever a new item with this tag lands, the group auto-moves beside it and the anchor line re-points'), side: z.enum(['right', 'left', 'above', 'below']).optional(), label: z.string().max(60).optional() }),
46
+ z.object({ op: z.literal('unfollow'), group_tag: z.string().min(1).max(40) }),
47
+ z.object({ op: z.literal('commit'), tag: z.string().max(40).optional().describe('make staging solid; omit tag = everything staging') }),
48
+ z.object({ op: z.literal('erase_group'), tag: z.string().min(1).max(40).describe('delete the whole tagged group (notes/shapes/lines; artifact cards only lose the tag)') }),
49
+ z.object({ op: z.literal('roll'), tag: z.string().min(1).max(40).describe('STOW a finished group: hides it behind a compact scroll card, everything kept in place (seats reserved, files intact, one click to unroll). For a scene/act/chapter that is DONE — use erase_group only to destroy'), label: z.string().max(60).optional().describe('scroll card title (default: the tag)') }),
50
+ z.object({ op: z.literal('unroll'), tag: z.string().min(1).max(40).describe('expand a rolled group back — everything returns to its original seat') }),
51
+ z.object({ op: z.literal('feature'), id: z.string().min(1).max(300).describe('make this the hero of the desktop') }),
52
+ z.object({ op: z.literal('unfeature') }),
53
+ z.object({ op: z.literal('transform_group'), tag: z.string().min(1).max(40), scale: z.number().min(0.3).max(3).optional(), rotate: z.number().min(-180).max(180).optional().describe('degrees clockwise') }),
54
+ z.object({ op: z.literal('replan'), sheet: z.string().max(40).optional().describe('which sheet (default: the current one)'), plan: z.array(SLOT).min(1).max(24).describe('slots to ADD or RESIZE on an existing sheet (merged by name; slots you do not mention stay). Same local-pixel coordinates as open_sheet{plan}') }),
55
+ z.object({ op: z.literal('chalk_edit'), on: z.boolean().describe('true = turn ON the user-side 改板书 toggle (notes become freely draggable/editable for the user); false = back to guarded mode') }),
56
+ ]);