@xiaobuyu/nodesign 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/README.md +266 -79
  2. package/README.zh-CN.md +254 -67
  3. package/package.json +2 -2
  4. package/server/api/admin.js +7 -0
  5. package/server/api/assets/docx-page.js +5 -4
  6. package/server/api/assets/helpers.js +26 -0
  7. package/server/api/assets/notes.js +50 -0
  8. package/server/api/assets.js +31 -38
  9. package/server/api/board.js +29 -1
  10. package/server/api/browse.js +3 -2
  11. package/server/api/chatai.js +9 -8
  12. package/server/api/exports/cards.js +7 -6
  13. package/server/api/exports/docx-pdf.js +4 -1
  14. package/server/api/instruction.js +3 -3
  15. package/server/api/local.js +10 -8
  16. package/server/api/projects.js +19 -3
  17. package/server/api/publish.js +3 -2
  18. package/server/api/sessions-rewind.js +220 -0
  19. package/server/api/sessions.js +58 -194
  20. package/server/api/stage.js +35 -0
  21. package/server/api/turn-model-switch.js +109 -0
  22. package/server/api/turn.js +52 -88
  23. package/server/auth/middleware.js +29 -6
  24. package/server/auth/tier.js +27 -7
  25. package/server/auth/users-store.js +14 -1
  26. package/server/engine/agent/actor-trail.js +110 -0
  27. package/server/engine/agent/agent-shared.js +22 -52
  28. package/server/engine/agent/beat-state.js +61 -0
  29. package/server/engine/agent/cast.js +322 -0
  30. package/server/engine/agent/events.js +18 -1
  31. package/server/engine/agent/hooks/beat-gate.js +73 -0
  32. package/server/engine/agent/hooks/failure.js +11 -1
  33. package/server/engine/agent/hooks/lifecycle.js +16 -41
  34. package/server/engine/agent/hooks/post-guidance.js +4 -38
  35. package/server/engine/agent/hooks/post-trim.js +1 -0
  36. package/server/engine/agent/hooks/pre-board-dirty.js +29 -0
  37. package/server/engine/agent/hooks/pre-defaults.js +206 -2
  38. package/server/engine/agent/hooks/pre-injectors.js +19 -2
  39. package/server/engine/agent/hooks/pre-peer-guard.js +88 -0
  40. package/server/engine/agent/hooks/pre-unknown-params.js +82 -0
  41. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +33 -0
  42. package/server/engine/agent/hooks/resident-role-lifecycle.js +100 -0
  43. package/server/engine/agent/hooks/slot-alias.js +30 -0
  44. package/server/engine/agent/hooks/user-prompt-submit.js +208 -47
  45. package/server/engine/agent/hooks.js +80 -14
  46. package/server/engine/agent/isolation.js +25 -2
  47. package/server/engine/agent/memory-config.js +87 -0
  48. package/server/engine/agent/model-context.js +102 -18
  49. package/server/engine/agent/model-table.js +361 -58
  50. package/server/engine/agent/prompts/nodesign-prelude.md +579 -70
  51. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +1 -1
  52. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +26 -9
  53. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +1 -1
  54. package/server/engine/agent/prompts/tools/site-reference.md +15 -4
  55. package/server/engine/agent/role-card.js +175 -0
  56. package/server/engine/agent/rp-mode.js +17 -0
  57. package/server/engine/agent/session-loop.js +55 -32
  58. package/server/engine/agent/stage-status.js +73 -0
  59. package/server/engine/agent/subagent-flight.js +73 -0
  60. package/server/engine/agent/system-prompts.js +41 -0
  61. package/server/engine/agent/tool-input-stream.js +165 -0
  62. package/server/engine/agents/explorer.md +1 -1
  63. package/server/engine/agents/index.js +1 -1
  64. package/server/engine/mcp/actor.js +59 -0
  65. package/server/engine/mcp/capability-gate.js +21 -2
  66. package/server/engine/mcp/index.js +118 -33
  67. package/server/engine/mcp/mode-profile.js +139 -0
  68. package/server/engine/mcp/server-name.js +19 -0
  69. package/server/engine/mcp/tools/artifact-session.js +2 -1
  70. package/server/engine/mcp/tools/browse-computer.js +2 -1
  71. package/server/engine/mcp/tools/browse-find-batch.js +95 -16
  72. package/server/engine/mcp/tools/browse.js +25 -3
  73. package/server/engine/mcp/tools/cast-role.js +167 -0
  74. package/server/engine/mcp/tools/crystallize-skill.js +1 -1
  75. package/server/engine/mcp/tools/draw-trend.js +104 -0
  76. package/server/engine/mcp/tools/edit-board-schema.js +56 -0
  77. package/server/engine/mcp/tools/edit-board.js +569 -0
  78. package/server/engine/mcp/tools/generate-image.js +50 -70
  79. package/server/engine/mcp/tools/helpers/codex-imagegen.js +139 -0
  80. package/server/engine/mcp/tools/helpers/rembg.js +7 -2
  81. package/server/engine/mcp/tools/helpers/shot-pipeline.js +109 -0
  82. package/server/engine/mcp/tools/list-pages.js +11 -5
  83. package/server/engine/mcp/tools/look-at-board.js +4 -3
  84. package/server/engine/mcp/tools/open-sheet.js +242 -0
  85. package/server/engine/mcp/tools/organize-board.js +45 -6
  86. package/server/engine/mcp/tools/paint-still.js +1 -1
  87. package/server/engine/mcp/tools/pin-to-board.js +103 -19
  88. package/server/engine/mcp/tools/read-board.js +123 -16
  89. package/server/engine/mcp/tools/read-tavern-json.js +87 -19
  90. package/server/engine/mcp/tools/report-issue.js +15 -7
  91. package/server/engine/mcp/tools/role-memory.js +83 -0
  92. package/server/engine/mcp/tools/roll-dice.js +82 -0
  93. package/server/engine/mcp/tools/roll-film.js +1 -1
  94. package/server/engine/mcp/tools/screenshot.js +19 -9
  95. package/server/engine/mcp/tools/set-vars.js +150 -0
  96. package/server/engine/mcp/tools/sheet-replan.js +46 -0
  97. package/server/engine/mcp/tools/web-search.js +12 -16
  98. package/server/engine/mcp/tools/write-on-board-flow.js +108 -0
  99. package/server/engine/mcp/tools/write-on-board-place.js +211 -0
  100. package/server/engine/mcp/tools/write-on-board-role-anchor.js +48 -0
  101. package/server/engine/mcp/tools/write-on-board-schema.js +125 -0
  102. package/server/engine/mcp/tools/write-on-board.js +568 -99
  103. package/server/engine/plugins/nodesign/skills/blackboard-rp/SKILL.md +215 -0
  104. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +14 -7
  105. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +44 -5
  106. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +16 -0
  107. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +24 -8
  108. package/server/engine/plugins/nodesign/skills/story-craft/SKILL.md +94 -0
  109. package/server/engine/plugins/nodesign/skills/story-import/SKILL.md +112 -0
  110. package/server/engine/plugins/nodesign/skills/story-intimacy/SKILL.md +72 -0
  111. package/server/engine/plugins/nodesign/skills/story-voice/SKILL.md +66 -0
  112. package/server/engine/plugins/nodesign/skills/story-voice/voices//345/206/267/347/241/254.md +33 -0
  113. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/203/205/346/204/237/347/233/264/347/273/231.md +42 -0
  114. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/204/217/350/257/206/346/265/201.md +42 -0
  115. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/255/246/344/276/240/347/231/275/350/257/235.md +34 -0
  116. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/224/237/346/264/273/346/265/201.md +35 -0
  117. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/231/275/346/217/217.md +37 -0
  118. package/server/engine/plugins/nodesign/skills/story-voice/voices//350/210/236/345/217/260/345/211/247.md +32 -0
  119. package/server/engine/plugins/nodesign/skills/story-voice/voices//351/230/266/346/256/265/345/210/207/346/215/242.md +42 -0
  120. package/server/engine/runs/board-seater.js +272 -0
  121. package/server/engine/runs/board-tasklist.js +90 -48
  122. package/server/engine/runs/turn-relay.js +24 -2
  123. package/server/engine/runs/user-chalk-echo.js +44 -0
  124. package/server/index.js +2 -2
  125. package/server/lib/artifact-target.js +31 -14
  126. package/server/lib/board-anchor.js +49 -0
  127. package/server/lib/board-dirty.js +93 -0
  128. package/server/lib/board-excerpt.js +80 -0
  129. package/server/lib/board-follow.js +111 -0
  130. package/server/lib/board-groups.js +1 -5
  131. package/server/lib/board-hero.js +3 -1
  132. package/server/lib/board-kind-sizes.js +38 -3
  133. package/server/lib/board-lanes.js +58 -0
  134. package/server/lib/board-obstacles.js +80 -0
  135. package/server/lib/board-relations.js +5 -1
  136. package/server/lib/board-sheets.js +467 -0
  137. package/server/lib/board-shelf.js +86 -0
  138. package/server/lib/board-transform.js +62 -0
  139. package/server/lib/canvas-id.js +46 -4
  140. package/server/lib/chalk-flow.js +88 -0
  141. package/server/lib/chalk-rewrite.js +63 -0
  142. package/server/lib/chalk-size-pref.js +48 -0
  143. package/server/lib/chalk.js +42 -3
  144. package/server/lib/export-collect.js +3 -1
  145. package/server/lib/ingress/forward-openai-chat.js +16 -4
  146. package/server/lib/ingress/openai-chat.js +23 -12
  147. package/server/lib/ingress/session-routes.js +3 -3
  148. package/server/lib/ingress/stream-watchdog.js +53 -0
  149. package/server/lib/ingress/upstream-billing.js +15 -0
  150. package/server/lib/ingress/upstream-error-hints.js +36 -0
  151. package/server/lib/issues-store.js +3 -1
  152. package/server/lib/json-preview.js +58 -0
  153. package/server/lib/kinds/docx.js +11 -4
  154. package/server/lib/kinds/index.js +6 -16
  155. package/server/lib/kinds/site.js +40 -7
  156. package/server/lib/model-ingress.js +26 -1
  157. package/server/lib/moderation.js +10 -5
  158. package/server/lib/quota.js +26 -6
  159. package/server/lib/rect.js +50 -0
  160. package/server/lib/rewrite-refs.js +110 -0
  161. package/server/lib/screen.js +36 -0
  162. package/server/lib/sheet-state.js +19 -0
  163. package/server/lib/sketch-array.js +118 -0
  164. package/server/lib/sketch-hatch.js +99 -0
  165. package/server/lib/sketch-layout.js +269 -69
  166. package/server/lib/sketch-shapes.js +152 -0
  167. package/server/lib/sketch-stencils.js +170 -0
  168. package/server/lib/ssrf-guard.js +9 -0
  169. package/server/lib/state-table.js +237 -0
  170. package/server/lib/state-trend.js +94 -0
  171. package/server/lib/state-triggers.js +199 -0
  172. package/server/lib/task-scan.js +18 -0
  173. package/server/lib/tavern-json.js +48 -0
  174. package/server/lib/user-uploads.js +88 -0
  175. package/server/projects/board-limits.js +5 -3
  176. package/server/projects/board-sanitize.js +220 -10
  177. package/server/projects/board-store.js +137 -7
  178. package/server/projects/memory-migration.js +85 -0
  179. package/server/projects/move-entry.js +11 -1
  180. package/server/projects/session-jsonl.js +110 -0
  181. package/server/projects/store.js +56 -3
  182. package/server/projects/ui-config.js +2 -1
  183. package/server/projects/viewpoint-store.js +35 -3
  184. package/server/projects/workspace-slots.js +30 -0
  185. package/server/projects/workspace-templates.js +42 -10
  186. package/server/projects/workspace.js +25 -26
  187. package/server/runtime/capabilities.js +6 -2
  188. package/server/services/rembg-launcher.js +5 -2
  189. package/server/shared/locales.js +70 -0
  190. package/server/shared/messages-en.js +69 -0
  191. package/server/shared/messages.js +65 -0
  192. package/server/ws/broker.js +3 -0
  193. package/server/ws/index.js +43 -3
  194. package/web/README.md +1 -1
  195. package/web/dist/assets/{BrowserWindow-P_vIdh-G.js → BrowserWindow-CcaGRWz5.js} +1 -1
  196. package/web/dist/assets/DeckWindow-BIr2KGFT.js +6 -0
  197. package/web/dist/assets/{DocxWindow-DSAU1-tD.js → DocxWindow-B9KZmU5z.js} +2 -2
  198. package/web/dist/assets/{MermaidBlock-DR1GikCv.js → MermaidBlock-DrfRnz8E.js} +43 -43
  199. package/web/dist/assets/{SiteWindow-92uBuRRR.js → SiteWindow-CJOjCF7d.js} +6 -6
  200. package/web/dist/assets/{abnfDiagram-VCTEODGH-uA8Esix4.js → abnfDiagram-VCTEODGH-CUZbQGIg.js} +1 -1
  201. package/web/dist/assets/{arc-D_khH_xN.js → arc-C1-HSUny.js} +1 -1
  202. package/web/dist/assets/{architectureDiagram-5GKGNRK7-DydJHqOt.js → architectureDiagram-5GKGNRK7-B5nwvu60.js} +2 -2
  203. package/web/dist/assets/{blockDiagram-NRAW4CY4-B7wBbKiW.js → blockDiagram-NRAW4CY4-uPPuFLwR.js} +1 -1
  204. package/web/dist/assets/{c4Diagram-UCG6FXSJ-6mrH0x7Y.js → c4Diagram-UCG6FXSJ-C0mKBFlz.js} +1 -1
  205. package/web/dist/assets/channel-BxpWNOGU.js +1 -0
  206. package/web/dist/assets/{chunk-2Q5K7J3B-Cuikho5Q.js → chunk-2Q5K7J3B-Cs0rlZ4q.js} +1 -1
  207. package/web/dist/assets/{chunk-5VM5RSS4-3-KZ5AUz.js → chunk-5VM5RSS4-DtR63oYb.js} +1 -1
  208. package/web/dist/assets/{chunk-F27PBJKO-Bkwq2gGU.js → chunk-F27PBJKO-B3MRdezJ.js} +1 -1
  209. package/web/dist/assets/{chunk-G27WJ6UU-BLyQzdGE.js → chunk-G27WJ6UU-BtwntwpS.js} +1 -1
  210. package/web/dist/assets/{chunk-JWPE2WC7-B7HxxQds.js → chunk-JWPE2WC7-BHsTe5PD.js} +1 -1
  211. package/web/dist/assets/{chunk-LCL6LL3I-CtXrnUwF.js → chunk-LCL6LL3I-knY15Qdm.js} +1 -1
  212. package/web/dist/assets/{chunk-POPQ4Y6H-dxddZlXt.js → chunk-POPQ4Y6H-C51tgK_I.js} +1 -1
  213. package/web/dist/assets/{chunk-SVP7TREG-DpkmrgHn.js → chunk-SVP7TREG-CnwRZmF_.js} +1 -1
  214. package/web/dist/assets/{chunk-XXDRQBXY-BwZ7RbpQ.js → chunk-XXDRQBXY-BtXzVF-_.js} +1 -1
  215. package/web/dist/assets/classDiagram-DTDB5LWJ-DUEKmDfo.js +1 -0
  216. package/web/dist/assets/classDiagram-v2-JRS7N3AN-DUEKmDfo.js +1 -0
  217. package/web/dist/assets/{cose-bilkent-JH36ORCC-DGFOsMUt.js → cose-bilkent-JH36ORCC-CzWlGP6i.js} +1 -1
  218. package/web/dist/assets/{cynefin-OW5HDTMX-DUwn3ehS.js → cynefin-OW5HDTMX-vK64EGSv.js} +1 -1
  219. package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js → cynefinDiagram-5FMLGOSQ-Bs3g1Gvh.js} +1 -1
  220. package/web/dist/assets/{dagre-3AP2YEHR-CyourLfz.js → dagre-3AP2YEHR-BhgkYe8V.js} +1 -1
  221. package/web/dist/assets/{diagram-S7CK7UJ4-BTMbx2np.js → diagram-S7CK7UJ4-C_6mvD2V.js} +1 -1
  222. package/web/dist/assets/{diagram-UQ7AKVKN-UK94_1-P.js → diagram-UQ7AKVKN-qW7tssUW.js} +1 -1
  223. package/web/dist/assets/{diagram-VSXAHHWV-C7zS-v9v.js → diagram-VSXAHHWV-txJPHpvG.js} +1 -1
  224. package/web/dist/assets/{diagram-VX7I27RA-DKJRa6CK.js → diagram-VX7I27RA-CQTMsCnL.js} +1 -1
  225. package/web/dist/assets/{diagram-Z3DM3KII-kUbAfGAp.js → diagram-Z3DM3KII--cb1hoE6.js} +1 -1
  226. package/web/dist/assets/{ebnfDiagram-PWID7BFC-CVW16_vb.js → ebnfDiagram-PWID7BFC-Da6lDU77.js} +1 -1
  227. package/web/dist/assets/{erDiagram-SSCWMZ5O-BRQMFQ5y.js → erDiagram-SSCWMZ5O-DGCMrsQn.js} +1 -1
  228. package/web/dist/assets/{flowDiagram-A5DVABFB-S34LpLtK.js → flowDiagram-A5DVABFB-GdOL_qno.js} +1 -1
  229. package/web/dist/assets/{ganttDiagram-EL5Y4UJY-BFiqk1-G.js → ganttDiagram-EL5Y4UJY-Cx1F_jBK.js} +4 -4
  230. package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-LZsrdyyv.js → gitGraphDiagram-WWUBYQGX-B5_6JhR3.js} +1 -1
  231. package/web/dist/assets/hand-CuvZZQGW.webp +0 -0
  232. package/web/dist/assets/index-BYKfMnmv.js +2759 -0
  233. package/web/dist/assets/index-CW7VsrVg.css +1 -0
  234. package/web/dist/assets/{infoDiagram-RXCK75RN-0DvN6KIp.js → infoDiagram-RXCK75RN-CjvKpSPA.js} +1 -1
  235. package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CaY3A-Bx.js → ishikawaDiagram-5VMMS53U-CxLoAPhx.js} +1 -1
  236. package/web/dist/assets/{journeyDiagram-EYS64GPL-BBeiaCrj.js → journeyDiagram-EYS64GPL-BfvS1upJ.js} +1 -1
  237. package/web/dist/assets/{kanban-definition-3QL26DDD-ux42DRXQ.js → kanban-definition-3QL26DDD-DKPPu0w4.js} +1 -1
  238. package/web/dist/assets/{layout-CnXiF_NB.js → layout-Dhgck-Zo.js} +1 -1
  239. package/web/dist/assets/{linear-BMW0u6G6.js → linear-DMSQ-aUw.js} +1 -1
  240. package/web/dist/assets/lxgw-nd-app-bold-D1pKA1bR.woff2 +0 -0
  241. package/web/dist/assets/lxgw-nd-app-regular-BDe3d26v.woff2 +0 -0
  242. package/web/dist/assets/lxgw-nd-bold-Bufvlg03.woff2 +0 -0
  243. package/web/dist/assets/lxgw-nd-regular-DQwoI4OP.woff2 +0 -0
  244. package/web/dist/assets/lxgwwenkaiscreen-subset-100-DLPe7tEt.woff2 +0 -0
  245. package/web/dist/assets/lxgwwenkaiscreen-subset-101-d3VsZKWT.woff2 +0 -0
  246. package/web/dist/assets/lxgwwenkaiscreen-subset-102-ClzJuLq6.woff2 +0 -0
  247. package/web/dist/assets/lxgwwenkaiscreen-subset-103-DuDnQ6K-.woff2 +0 -0
  248. package/web/dist/assets/lxgwwenkaiscreen-subset-104-kPGW3Eem.woff2 +0 -0
  249. package/web/dist/assets/lxgwwenkaiscreen-subset-105-BzD54h-0.woff2 +0 -0
  250. package/web/dist/assets/lxgwwenkaiscreen-subset-106-BQ7fIzlx.woff2 +0 -0
  251. package/web/dist/assets/lxgwwenkaiscreen-subset-107-D8tDfFHI.woff2 +0 -0
  252. package/web/dist/assets/lxgwwenkaiscreen-subset-108-tGhkpwep.woff2 +0 -0
  253. package/web/dist/assets/lxgwwenkaiscreen-subset-109-7SMVSrly.woff2 +0 -0
  254. package/web/dist/assets/lxgwwenkaiscreen-subset-110-_uDKrFed.woff2 +0 -0
  255. package/web/dist/assets/lxgwwenkaiscreen-subset-111-Bio825RE.woff2 +0 -0
  256. package/web/dist/assets/lxgwwenkaiscreen-subset-112-D2ee0i-C.woff2 +0 -0
  257. package/web/dist/assets/lxgwwenkaiscreen-subset-113-JPFVDrga.woff2 +0 -0
  258. package/web/dist/assets/lxgwwenkaiscreen-subset-114-BcfxYA4w.woff2 +0 -0
  259. package/web/dist/assets/lxgwwenkaiscreen-subset-115-B1HeeOsd.woff2 +0 -0
  260. package/web/dist/assets/lxgwwenkaiscreen-subset-116-hY12tXpq.woff2 +0 -0
  261. package/web/dist/assets/lxgwwenkaiscreen-subset-117-BvuZwmre.woff2 +0 -0
  262. package/web/dist/assets/lxgwwenkaiscreen-subset-118-tzvs_H7j.woff2 +0 -0
  263. package/web/dist/assets/lxgwwenkaiscreen-subset-119-J2DWuYDN.woff2 +0 -0
  264. package/web/dist/assets/lxgwwenkaiscreen-subset-21-pDubCJV5.woff2 +0 -0
  265. package/web/dist/assets/lxgwwenkaiscreen-subset-22-ChAPMpqp.woff2 +0 -0
  266. package/web/dist/assets/lxgwwenkaiscreen-subset-23-BBx3oNpT.woff2 +0 -0
  267. package/web/dist/assets/lxgwwenkaiscreen-subset-24-CYt-Oagk.woff2 +0 -0
  268. package/web/dist/assets/lxgwwenkaiscreen-subset-25-Cx3j-ZUv.woff2 +0 -0
  269. package/web/dist/assets/lxgwwenkaiscreen-subset-26-B9AfZ3LL.woff2 +0 -0
  270. package/web/dist/assets/lxgwwenkaiscreen-subset-27-Bh2s9MNE.woff2 +0 -0
  271. package/web/dist/assets/lxgwwenkaiscreen-subset-28-C02JPoKP.woff2 +0 -0
  272. package/web/dist/assets/lxgwwenkaiscreen-subset-29-T_tbUGhk.woff2 +0 -0
  273. package/web/dist/assets/lxgwwenkaiscreen-subset-30-DMfyNzhz.woff2 +0 -0
  274. package/web/dist/assets/lxgwwenkaiscreen-subset-31-DFfdzbkg.woff2 +0 -0
  275. package/web/dist/assets/lxgwwenkaiscreen-subset-32-Bs1fz2iW.woff2 +0 -0
  276. package/web/dist/assets/lxgwwenkaiscreen-subset-33-cpB96OkZ.woff2 +0 -0
  277. package/web/dist/assets/lxgwwenkaiscreen-subset-34-BhinPYWX.woff2 +0 -0
  278. package/web/dist/assets/lxgwwenkaiscreen-subset-35-D_njRQqA.woff2 +0 -0
  279. package/web/dist/assets/lxgwwenkaiscreen-subset-36-DqXlLE3t.woff2 +0 -0
  280. package/web/dist/assets/lxgwwenkaiscreen-subset-37-j5LbvMph.woff2 +0 -0
  281. package/web/dist/assets/lxgwwenkaiscreen-subset-38-oO4jWdKm.woff2 +0 -0
  282. package/web/dist/assets/lxgwwenkaiscreen-subset-39-CabsX4rw.woff2 +0 -0
  283. package/web/dist/assets/lxgwwenkaiscreen-subset-4-PatS3YHh.woff2 +0 -0
  284. package/web/dist/assets/lxgwwenkaiscreen-subset-40-yrLg3C3e.woff2 +0 -0
  285. package/web/dist/assets/lxgwwenkaiscreen-subset-41-DBXmCQhc.woff2 +0 -0
  286. package/web/dist/assets/lxgwwenkaiscreen-subset-42-Bqij_UiQ.woff2 +0 -0
  287. package/web/dist/assets/lxgwwenkaiscreen-subset-43-D3kp0Ntc.woff2 +0 -0
  288. package/web/dist/assets/lxgwwenkaiscreen-subset-44-B_BNGvK4.woff2 +0 -0
  289. package/web/dist/assets/lxgwwenkaiscreen-subset-45-CZc0cQWs.woff2 +0 -0
  290. package/web/dist/assets/lxgwwenkaiscreen-subset-46-CHQX5Kd9.woff2 +0 -0
  291. package/web/dist/assets/lxgwwenkaiscreen-subset-47-Ci8gLrHA.woff2 +0 -0
  292. package/web/dist/assets/lxgwwenkaiscreen-subset-48-run3ugD3.woff2 +0 -0
  293. package/web/dist/assets/lxgwwenkaiscreen-subset-49-DgfiUb6u.woff2 +0 -0
  294. package/web/dist/assets/lxgwwenkaiscreen-subset-5-B0CtGbX_.woff2 +0 -0
  295. package/web/dist/assets/lxgwwenkaiscreen-subset-50-CdCG-POr.woff2 +0 -0
  296. package/web/dist/assets/lxgwwenkaiscreen-subset-51-L2okeQZ2.woff2 +0 -0
  297. package/web/dist/assets/lxgwwenkaiscreen-subset-52-BZAqyGJW.woff2 +0 -0
  298. package/web/dist/assets/lxgwwenkaiscreen-subset-53-B0xWqLeU.woff2 +0 -0
  299. package/web/dist/assets/lxgwwenkaiscreen-subset-54-BZVQX-EA.woff2 +0 -0
  300. package/web/dist/assets/lxgwwenkaiscreen-subset-55-093mkIUJ.woff2 +0 -0
  301. package/web/dist/assets/lxgwwenkaiscreen-subset-56-CbhWG8G-.woff2 +0 -0
  302. package/web/dist/assets/lxgwwenkaiscreen-subset-57-pLQuvo6c.woff2 +0 -0
  303. package/web/dist/assets/lxgwwenkaiscreen-subset-58-BHhoY50x.woff2 +0 -0
  304. package/web/dist/assets/lxgwwenkaiscreen-subset-59-D1_6DuIQ.woff2 +0 -0
  305. package/web/dist/assets/lxgwwenkaiscreen-subset-6-BnAelmYU.woff2 +0 -0
  306. package/web/dist/assets/lxgwwenkaiscreen-subset-60-HDBIuLTZ.woff2 +0 -0
  307. package/web/dist/assets/lxgwwenkaiscreen-subset-61-C-c13SeU.woff2 +0 -0
  308. package/web/dist/assets/lxgwwenkaiscreen-subset-62-kHRVOhZ2.woff2 +0 -0
  309. package/web/dist/assets/lxgwwenkaiscreen-subset-63-C0MCsvFv.woff2 +0 -0
  310. package/web/dist/assets/lxgwwenkaiscreen-subset-64-DZekguCj.woff2 +0 -0
  311. package/web/dist/assets/lxgwwenkaiscreen-subset-65-CtP6c3IU.woff2 +0 -0
  312. package/web/dist/assets/lxgwwenkaiscreen-subset-66-DLVWoMSf.woff2 +0 -0
  313. package/web/dist/assets/lxgwwenkaiscreen-subset-67-BVuOnayH.woff2 +0 -0
  314. package/web/dist/assets/lxgwwenkaiscreen-subset-68-DpxQ6Mnx.woff2 +0 -0
  315. package/web/dist/assets/lxgwwenkaiscreen-subset-69-qesBxLmC.woff2 +0 -0
  316. package/web/dist/assets/lxgwwenkaiscreen-subset-70-BBt1xN7h.woff2 +0 -0
  317. package/web/dist/assets/lxgwwenkaiscreen-subset-71-BWqZacwB.woff2 +0 -0
  318. package/web/dist/assets/lxgwwenkaiscreen-subset-72-B1wP_Yst.woff2 +0 -0
  319. package/web/dist/assets/lxgwwenkaiscreen-subset-73-C99T7jNZ.woff2 +0 -0
  320. package/web/dist/assets/lxgwwenkaiscreen-subset-74-DrUTsWBe.woff2 +0 -0
  321. package/web/dist/assets/lxgwwenkaiscreen-subset-75-C4M5XXQe.woff2 +0 -0
  322. package/web/dist/assets/lxgwwenkaiscreen-subset-76-Bq8spVWF.woff2 +0 -0
  323. package/web/dist/assets/lxgwwenkaiscreen-subset-77-D9vNupE2.woff2 +0 -0
  324. package/web/dist/assets/lxgwwenkaiscreen-subset-78-D6th7Gnu.woff2 +0 -0
  325. package/web/dist/assets/lxgwwenkaiscreen-subset-79-bezM85cS.woff2 +0 -0
  326. package/web/dist/assets/lxgwwenkaiscreen-subset-80-DD8pnhG2.woff2 +0 -0
  327. package/web/dist/assets/lxgwwenkaiscreen-subset-81-N6jtSyBs.woff2 +0 -0
  328. package/web/dist/assets/lxgwwenkaiscreen-subset-82-D6c180bZ.woff2 +0 -0
  329. package/web/dist/assets/lxgwwenkaiscreen-subset-83-em17AadM.woff2 +0 -0
  330. package/web/dist/assets/lxgwwenkaiscreen-subset-84-CJ8gdrMK.woff2 +0 -0
  331. package/web/dist/assets/lxgwwenkaiscreen-subset-85-9PPGO1eB.woff2 +0 -0
  332. package/web/dist/assets/lxgwwenkaiscreen-subset-86-CjWPf-u5.woff2 +0 -0
  333. package/web/dist/assets/lxgwwenkaiscreen-subset-87-8D_X_yOC.woff2 +0 -0
  334. package/web/dist/assets/lxgwwenkaiscreen-subset-88-9vtKFPBx.woff2 +0 -0
  335. package/web/dist/assets/lxgwwenkaiscreen-subset-89-BhE5jAKX.woff2 +0 -0
  336. package/web/dist/assets/lxgwwenkaiscreen-subset-90-D2Npfm5E.woff2 +0 -0
  337. package/web/dist/assets/lxgwwenkaiscreen-subset-91-C6Ft1-Eu.woff2 +0 -0
  338. package/web/dist/assets/lxgwwenkaiscreen-subset-97-CaExyQP9.woff2 +0 -0
  339. package/web/dist/assets/lxgwwenkaiscreen-subset-98-CPoVbsUd.woff2 +0 -0
  340. package/web/dist/assets/lxgwwenkaiscreen-subset-99-ZUQZGVgH.woff2 +0 -0
  341. package/web/dist/assets/{mindmap-definition-FBJOCRG2-hUxSpdKn.js → mindmap-definition-FBJOCRG2-CalfL_Kn.js} +1 -1
  342. package/web/dist/assets/{pegDiagram-XKGWAZYB-aCaQPH0w.js → pegDiagram-XKGWAZYB-D9mVWz7l.js} +1 -1
  343. package/web/dist/assets/pending-edit-apply-kwMFWsUe.js +31 -0
  344. package/web/dist/assets/{pieDiagram-E7YTZNPT-CmOEZ8zg.js → pieDiagram-E7YTZNPT-DELzbZNS.js} +1 -1
  345. package/web/dist/assets/{quadrantDiagram-AXDQQJYC-VW6fAhsV.js → quadrantDiagram-AXDQQJYC-TZqp4mfA.js} +1 -1
  346. package/web/dist/assets/{railroadDiagram-O6MQD6OU-CKJ7Ba5K.js → railroadDiagram-O6MQD6OU-DxX59f0i.js} +1 -1
  347. package/web/dist/assets/{requirementDiagram-EFPCY7ZU-pNGkq9Vw.js → requirementDiagram-EFPCY7ZU-DFdsJLIe.js} +1 -1
  348. package/web/dist/assets/{rotate-cw-KAfChbGV.js → rotate-cw-62liz7LX.js} +1 -1
  349. package/web/dist/assets/{sankeyDiagram-P5KCCOFB-BIMmDTfZ.js → sankeyDiagram-P5KCCOFB-ByYcd3fG.js} +1 -1
  350. package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-BBW7wzKY.js → sequenceDiagram-WJ2MYXX4-Bj-ml1jS.js} +1 -1
  351. package/web/dist/assets/{sizeCapture-X5ZJPWSS-DdkXmLYR.js → sizeCapture-X5ZJPWSS-KhsXThyD.js} +1 -1
  352. package/web/dist/assets/{square-dashed-mouse-pointer-CDUlhUP7.js → square-dashed-mouse-pointer-D_VrqVpy.js} +1 -1
  353. package/web/dist/assets/{stateDiagram-HBIQ2CUA-W313Uz16.js → stateDiagram-HBIQ2CUA-BJlpHfWU.js} +1 -1
  354. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-CR2kYAH2.js +1 -0
  355. package/web/dist/assets/{swimlanes-XN3QIQJK-C29TL26K.js → swimlanes-XN3QIQJK-DUB9OJVs.js} +1 -1
  356. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BitOxnG2.js +8 -0
  357. package/web/dist/assets/{timeline-definition-24CTP7MA-CF0JMObX.js → timeline-definition-24CTP7MA-C03NX82A.js} +1 -1
  358. package/web/dist/assets/{vennDiagram-4TSXK5OY-DrDFsaH3.js → vennDiagram-4TSXK5OY-B9pjzngp.js} +1 -1
  359. package/web/dist/assets/{wardleyDiagram-VM6X3IG4-CVrnRIS_.js → wardleyDiagram-VM6X3IG4-UqWq_mcT.js} +1 -1
  360. package/web/dist/assets/{xychartDiagram-S5SC5T6Z-BvkN1zsT.js → xychartDiagram-S5SC5T6Z-D2pDKtOv.js} +1 -1
  361. package/web/dist/index.html +2 -2
  362. package/server/api/memory.js +0 -185
  363. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +0 -583
  364. package/server/engine/mcp/tools/arrange-on-board.js +0 -124
  365. package/server/engine/mcp/tools/create-on-board.js +0 -152
  366. package/server/engine/mcp/tools/edit-sketch.js +0 -167
  367. package/server/engine/mcp/tools/record-decision.js +0 -140
  368. package/server/engine/mcp/tools/relate-on-board.js +0 -112
  369. package/server/engine/mcp/tools/sketch-on-board.js +0 -337
  370. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +0 -235
  371. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +0 -78
  372. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +0 -268
  373. package/web/dist/assets/DeckWindow-BpLv5082.js +0 -16
  374. package/web/dist/assets/channel-DGkccMO1.js +0 -1
  375. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +0 -1
  376. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +0 -1
  377. package/web/dist/assets/index-C0KUf6Wd.css +0 -1
  378. package/web/dist/assets/index-GAamh1p4.js +0 -2152
  379. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  380. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  381. package/web/dist/assets/pending-edit-apply-AcdOQ9Ij.js +0 -31
  382. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +0 -1
  383. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +0 -8
@@ -0,0 +1,569 @@
1
+ /**
2
+ * mcp/tools/edit-board.js —— edit_board(2026-08-25 范式重做③)
3
+ *
4
+ * 改板的唯一入口。前身 edit_sketch,08-25 这一刀吞进四件;旧名薄别名 08-28 全部
5
+ * 收摊(exp 不为过去的会话背兼容,用户拍板):
6
+ * arrange_on_board → feature / unfeature(beside/below = move 的 to:{ref,side})
7
+ * finish_sketch → commit / erase_group
8
+ * relate_on_board → add_edge(它独有的"端点必须真实存在"校验下沉进共享 add_edge
9
+ * —— 原来一个查一个不查是口径病,悬空线全从不查的那个进来)
10
+ * edit_sketch → 本体改名
11
+ *
12
+ * 新能力(08-25 RP 真会话那批 friction 的正面回答):
13
+ * set_edge 支持改端点 from/to(「状态锚在这一章」重指一条命令,不再 remove+add)
14
+ * reflow{tag} set_text 改高后整组按 column/row 重堆(agent 不该自带补丁模板)
15
+ * remove 对 agent 自己写的板书放行(连文件带座位带线一起清)
16
+ * move 有避让了(原 placeRel 裸落点,是六套引擎里唯一不避让的)
17
+ *
18
+ * 位置永远是相对表达({ref,side,gap} 或位移 {dx,dy}),落到哪一格由服务端解析。
19
+ * 距离单位一律像素(08-29;此前是格,见下面 GAP/PX_DELTA 那段的账)。
20
+ */
21
+
22
+ import path from 'node:path';
23
+ import { promises as fs } from 'node:fs';
24
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
25
+ import { byOf } from '../actor.js';
26
+ import { z } from 'zod';
27
+ import { readBoard, patchBoard, commitStaging, removeByTag, chalkAbsPath, TEXT_FONTS } from '../../../projects/board-store.js';
28
+ import { estimateSizeOn, FOLDER_CARD } from '../../../lib/board-kind-sizes.js';
29
+ import { layerOf, normalizeCanvasId, tagEnvelope, bareTag } from '../../../lib/canvas-id.js';
30
+ import { UNIT, textBox, shapePath } from '../../../lib/sketch-layout.js';
31
+ import { placeBeside, placeAtOnSheet, overlapIds, currentSheet } from '../../../lib/board-sheets.js';
32
+ import { applyReplan } from './sheet-replan.js';
33
+ import { transformGroup } from '../../../lib/board-transform.js';
34
+ import { OP } from './edit-board-schema.js';
35
+ import { obstaclesIn } from '../../../lib/board-obstacles.js';
36
+ import { currentSheetIdOf } from '../../../lib/sheet-state.js';
37
+ import { rewriteChalkBody } from '../../../lib/chalk-rewrite.js';
38
+ import { CHALK_DIR, trashChalkFile, parseChalk, renderChalk } from '../../../lib/chalk.js';
39
+ import { readUiConfigFile, writeUiConfig } from '../../../projects/ui-config.js';
40
+
41
+ const MAX_OPS = 120;
42
+ let seq = 0;
43
+ const stamp = () => `${Date.now().toString(36)}${(seq++ % 1000).toString(36)}`;
44
+
45
+ /**
46
+ * 距离一律**像素**(2026-08-29 改口径)。原来 gap/dx/dy 是格(1 格 24px),配了一层
47
+ * 「大于上限的按像素收编」的垫片 —— 于是同一个字段小数当格、大数当像素,中间那段
48
+ * 静默错 24 倍:真会话里 agent 想左移 120px 写了 dx:-120,实际挪了 2880px,再写
49
+ * dx:7000(这次按像素算)往回捞,四发才收敛(proj_mtdr2xpa 03:09)。
50
+ *
51
+ * agent 读到的每一个位置都是像素(read_board / look_at_board / 落位回执全是),
52
+ * 让它写位移时换算成格,是给一件没有收益的事发一张必错的许可证。格只留在起草图
53
+ * (write_on_board 的 nodes[].at)那种「从零排版面」的场合。
54
+ *
55
+ * 超范围的值**钳住不拒收**(08-27 那条教训留着):schema 校验是整单拒,一个越界的
56
+ * gap 会陪葬同批全部合法 op —— 代价远大于把它按到上限。
57
+ */
58
+
59
+ export function makeEditBoardTool({ projectId, sharedRoot, sessionId = null, ctx }) {
60
+ const handler = makeHandler({ projectId, sharedRoot, sessionId, ctx });
61
+ return tool(
62
+ 'edit_board',
63
+ `Edit what is already on the board — by id, without redrawing. Positions: sheet-absolute
64
+ {x,y,sheet?} (pixels from a sheet's top-left writable corner — you own the layout),
65
+ relative {ref, side, gap}, or a shift {dx,dy}. Every distance is in PIXELS — the same
66
+ pixels read_board and look_at_board report back, no grid conversion. Placement is EXACT
67
+ (no auto-nudging); if you cover something the return says so. ids come from read_board
68
+ (nodes text:…/scribble:…, cards deck:…/site:…/paths, lines b:…); local names from the
69
+ sketch that drew them work too.
70
+ ops (run in order; a failing op is reported, the rest still apply):
71
+ set_text{id,text?,…} (canvas text nodes AND your own board-note files — a note's body is
72
+ rewritten in place, threads/lines/annotations survive; never redraw to change words) ·
73
+ move{id,to} · move_group{tag,to} (a tagged panel is ONE thing — move the whole status
74
+ board with its tag, never its members one by one; pair with reflow to re-stack it) ·
75
+ remove{id} (agent-written board
76
+ notes included: file + seat + lines go together) · add_node{id?,text,at:{ref,side,gap?},…} ·
77
+ add_shape{kind,around,…} (circle/box/underline an EXISTING thing after the fact — the mark
78
+ hugs it and follows when it moves) · set_shape{id,color?,width?} ·
79
+ add_edge{from,to,type?,material?,label?} · set_edge{id,from?,to?,label?,type?,material?}
80
+ (re-point a line in one op) · remove_edge{id} · reflow{tag,layout?} (restack a group after
81
+ text edits changed heights) · replan{sheet?,plan:[{slot,at,w,h,about}…]} (ADD or RESIZE planned
82
+ blocks on an existing sheet — fix a bad layout instead of abandoning the page; unnamed slots stay) ·
83
+ transform_group{tag,scale?,rotate?} (scale/rotate a whole tagged drawing about its center —
84
+ scribbles truly transform; text/cards just re-seat, reported honestly) · follow{group_tag,target_tag,side?} (standing rule: whenever a
85
+ new item with target_tag lands, the group auto-moves beside it — a status panel that tracks
86
+ the latest chapter needs this ONCE, not per turn) · unfollow{group_tag} ·
87
+ commit{tag?} (staging → solid) · erase_group{tag} ·
88
+ roll{tag,label?} (STOW a finished scene/act/chapter behind one compact scroll card —
89
+ seats/files/lines all kept, user or you can unroll anytime; the tidy way to end an act) ·
90
+ unroll{tag} · feature{id} / unfeature (hero) ·
91
+ chalk_edit{on} (flip the user's 改板书 toggle — turn it ON when the session leans on
92
+ board notes, e.g. blackboard RP, so the user can drag/edit notes without double-click arming).
93
+ Moves avoid collisions (nearest free cell). User-dragged items CAN be moved (the result
94
+ says so when you do) — move them for a reason, and never tug-of-war: if the user drags
95
+ it back, that placement is final.
96
+ For brand-new content use write_on_board.`,
97
+ {
98
+ tag: z.string().max(40).optional().describe('Default tag for add_node/add_edge (the group you are editing)'),
99
+ ops: z.array(OP).min(1).max(MAX_OPS),
100
+ },
101
+ handler,
102
+ );
103
+ }
104
+
105
+ /** 端点存在性(add_edge/set_edge 共用;08-28 起 write_on_board 的图内边也用它 ——
106
+ * 两个入口对「悬空边」的容忍度曾不对称):板上有座位 / zones 命中 / 磁盘上真有这个路径。 */
107
+ export async function endpointReal(id, live, zones, sharedRoot) {
108
+ if (live[id]) return true;
109
+ if (zones && zones[id] !== undefined) return true;
110
+ // (doc: 无条件放行分支 08-27 审计拆除:doc:brand/_root 已于 08-24 退役,全仓
111
+ // 无写方;留着它,手滑把 docx: 打成 doc: 就能绕过存在性闸产出悬空线)
112
+ if (!sharedRoot) return false;
113
+ const bare = id.replace(/^(deck|site|docx|text|scribble):/, '');
114
+ if (!bare || bare.includes('..')) return false;
115
+ try { await fs.access(path.join(sharedRoot, bare)); return true; } catch { return false; }
116
+ }
117
+
118
+ function makeHandler({ projectId, sharedRoot, sessionId = null, ctx }) {
119
+ return async ({ tag: defaultTag, ops }, extra) => {
120
+ // 署名按调用者(常驻角色改板时署它的名)——见 mcp/actor.js
121
+ const by = byOf(extra);
122
+ const err = (t) => ({ content: [{ type: 'text', text: t }], isError: true });
123
+ if (!projectId) return err('No project bound.');
124
+ const board = await readBoard(projectId);
125
+ const known = new Set(Object.keys(board.zones || {}));
126
+ const objects = {}; const bindings = {}; // 增量 patch
127
+ const rolls = {}; // 卷(收纳器):tag → {at,by,label}|null
128
+ const follows = {}; // 跟随规则增量:组tag → {target,side?,label?}|null
129
+ const live = { ...board.objects }; // 调用内"当前态"
130
+ const liveBindings = { ...board.bindings };
131
+ const local = new Map(); // add_node 本地句柄 → canvas id
132
+ let heroPatch; // undefined = 不动;null = 撤;string = 立
133
+ const chalkUnlinks = []; // remove 板书:patch 后再删文件
134
+ let committed = 0; let erased = 0;
135
+
136
+ const byLid = (raw) => {
137
+ const hits = Object.entries(live).filter(([, e]) => e?.data?.lid === raw && Number.isFinite(e?.x));
138
+ if (!hits.length) return null;
139
+ const scoped = defaultTag ? hits.filter(([, e]) => e.tag === defaultTag) : [];
140
+ const pick = (scoped.length ? scoped : hits);
141
+ return pick[pick.length - 1][0];
142
+ };
143
+ const rid = (raw) => {
144
+ if (local.has(raw)) return local.get(raw);
145
+ const c = normalizeCanvasId(raw);
146
+ if (c && live[c]) return c;
147
+ if (c && isZone(c)) return c; // 文件夹卡(刀 G)
148
+ const l = byLid(raw);
149
+ if (l) return l;
150
+ // tag 兜底(2026-08-30):`ref:"状态板"` / `ref:"#状态板"` 是很自然的写法,
151
+ // 而 tag 本来就是"一片东西的名字"。取包络里最右那件当代表,跟 near 同一条规则。
152
+ const env = tagEnvelope({ objects: live }, raw, (id, e) => estimateSizeOn(board, id, e));
153
+ return env ? env.anchorId : null;
154
+ };
155
+ const rectOf = (id) => {
156
+ if (isZone(id)) { const z = liveZones[id]; return Number.isFinite(z?.x) ? { x: z.x, y: z.y, ...FOLDER_CARD } : null; }
157
+ const e = live[id]; return e ? { x: e.x, y: e.y, ...estimateSizeOn(board, id, e) } : null;
158
+ };
159
+ /** 压上判定的障碍集(同层,subject/组员除外;含文件夹卡/卷卡/精灵身位) */
160
+ const obstaclesNear = (zone, exclude = new Set()) => obstaclesIn(board, zone, { objects: live, exclude });
161
+ /** 相对落位 = 精确贴放(2026-08-29:环搜退役 —— 压上如实报,不代找洞) */
162
+ const placeRel = (subjectId, box, rel) => {
163
+ const refId = rid(rel.ref);
164
+ const r = rectOf(refId);
165
+ if (!r) return null;
166
+ const zone = layerOf(refId, live[refId], known);
167
+ const p = placeBeside(r, box, rel.side, rel.gap ?? UNIT);
168
+ const pressed = overlapIds({ x: p.x, y: p.y, w: box.w, h: box.h }, obstaclesNear(zone, new Set([subjectId])));
169
+ return { ...p, pressed };
170
+ };
171
+ /** 纸内绝对坐标 → 世界(sheet 缺省当前纸;钳进版心,钳了如实报) */
172
+ const placeAbs = (to, box) => {
173
+ const s = to.sheet && board.sheets?.[to.sheet]
174
+ ? { id: to.sheet, ...board.sheets[to.sheet] }
175
+ : currentSheet(board, currentSheetIdOf(sessionId));
176
+ if (!s) return null;
177
+ const p = placeAtOnSheet(s, { x: to.x, y: to.y }, box);
178
+ return { ...p, sheetId: s.id };
179
+ };
180
+ const report = []; let ok = 0;
181
+ const setObj = (id, e) => { live[id] = e; objects[id] = e; };
182
+ /**
183
+ * 文件夹(board.zones)也进摆位系统(2026-08-30 刀 G)。
184
+ *
185
+ * 在此之前 agent **完全没有摆文件夹的手**:move 走 `live`(= board.objects),
186
+ * 文件夹住 board.zones,rid() 永远查不到它,报错还是「不在板上」。位置全由前端
187
+ * newStackedZoneRect 一行行码在桌面上,跟纸毫无关系 —— 站主原话「甚至包括
188
+ * 文件夹..都需要 agent 规划之后放置在对应位置上」。
189
+ * 尺寸取 FOLDER_CARD(前端同一个常量,board-kind-sizes 里有 parity 测试钉着)。
190
+ */
191
+ const liveZones = { ...(board.zones || {}) };
192
+ const zonesPatch = {};
193
+ const sheetsPatch = {}; // replan:纸的版位增改
194
+ const isZone = (id) => Object.prototype.hasOwnProperty.call(liveZones, id);
195
+ const setZone = (id, z) => { liveZones[id] = z; zonesPatch[id] = z; };
196
+ /** 贴身记号跟随(08-27 shapes 编辑面):挪一件东西时,圈着它的涂鸦一起走。
197
+ * except = 这次已经被挪过的 id 集(整组拖时组员别被挪两次)。 */
198
+ const moveHuggers = (nodeId, dx, dy, except = null) => {
199
+ if (!dx && !dy) return;
200
+ for (const [hid, he] of Object.entries(live)) {
201
+ if (he?.hug !== nodeId || he.kind !== 'scribble') continue;
202
+ if (except?.has(hid)) continue;
203
+ setObj(hid, { ...he, x: he.x + dx, y: he.y + dy });
204
+ }
205
+ };
206
+
207
+ for (let i = 0; i < ops.length; i += 1) {
208
+ const o = ops[i];
209
+ const fail = (why) => report.push(`✗ #${i + 1} ${o.op}: ${why}`);
210
+ try {
211
+ if (o.op === 'set_text') {
212
+ const id = rid(o.id); const e = id && live[id];
213
+ // 板书正门(08-27):set_text 也认板书**文件** —— 改字不再要求重画/绕道
214
+ // Edit。笔权按作者判:只有作者本人能改自己的话(接续权闸的同一条纪律,
215
+ // 角色因此第一次拥有了改自己板书的手)。
216
+ if (e && !e.kind && id.startsWith(`${CHALK_DIR}/`)) {
217
+ if ((e.by || 'agent') !== by) { fail(`这条板书是「${e.by || 'agent'}」写的,笔权在它 —— 想让它改,寄 cue(SendMessage)或让用户直接说。`); continue; }
218
+ if (!o.text) { fail('改板书给 text(字号/字体/颜色是画布原生节点的旋钮,板书没有)'); continue; }
219
+ const abs = chalkAbsPath(projectId, id);
220
+ if (!abs) { fail(`${id} 的文件路径解析不了`); continue; }
221
+ // 落盘 + 量高收成一份(lib/chalk-rewrite.js,2026-08-30):set_vars 做的是
222
+ // 同一件事,抄第二份必然漏掉「用户拖出来的留白留得住」那条 —— 三条语义里
223
+ // 只有它漏了不报错,只是用户的排版悄悄没了。
224
+ let box2;
225
+ try { box2 = await rewriteChalkBody(abs, o.text, e); } catch (ex) {
226
+ fail(ex?.code === 'STATE_TABLE' ? `⛔ ${ex.message}` : `${id} 文件读不到(磁盘上已无此路径?)`); continue;
227
+ }
228
+ setObj(id, { ...e, w: box2.w, h: box2.h }); ok += 1;
229
+ report.push(`· #${i + 1} set_text 重写了板书 ${id} 的正文(线/标注/座位全保留)`);
230
+ continue;
231
+ }
232
+ if (!e || e.kind !== 'text') { fail(`${o.id} 不是文字节点,也不是你的板书文件`); continue; }
233
+ const data = { ...e.data };
234
+ if (o.text) data.t = o.text;
235
+ if (o.format) { if (o.format === 'md') data.format = 'md'; else delete data.format; }
236
+ if (o.size) data.size = o.size; if (o.color) data.color = o.color; if (o.font && TEXT_FONTS.includes(o.font)) data.font = o.font;
237
+ const box = textBox(data.t, data.size || 'md', { md: data.format === 'md' });
238
+ setObj(id, { ...e, data, w: box.w, h: box.h }); ok += 1;
239
+ } else if (o.op === 'move') {
240
+ const id = rid(o.id); const e = id && live[id];
241
+ // 文件夹卡走自己的分支:它只有坐标,没有 by/seat/tag 那一套(刀 G)
242
+ if (!e && id && isZone(id)) {
243
+ const box = rectOf(id);
244
+ let p = null;
245
+ if ('ref' in o.to) { p = placeRel(id, box, o.to); if (!p) { fail(`参照 ${o.to.ref} 不在板上`); continue; } }
246
+ else if ('x' in o.to) { p = placeAbs(o.to, box); if (!p) { fail(o.to.sheet ? `纸 ${o.to.sheet} 不存在(read_board 看纸的清单)` : '还没有铺过纸 —— 先 open_sheet,或用 {dx,dy}/{ref,side}'); continue; } }
247
+ else { p = { x: box.x + (o.to.dx || 0), y: box.y + (o.to.dy || 0) }; }
248
+ setZone(id, { x: Math.round(p.x), y: Math.round(p.y) });
249
+ ok += 1;
250
+ report.push(`· #${i + 1} move 文件夹「${id}」→ (${Math.round(p.x)},${Math.round(p.y)})${p.pressed?.length ? `(⚠ 压住了 ${p.pressed.slice(0, 3).join('、')})` : ''}`);
251
+ continue;
252
+ }
253
+ if (!e) { fail(`${o.id} 不在板上`); continue; }
254
+ // seat:'user' 08-28 从「冻结」放开(用户拍板"全部放开试试"):排位引擎
255
+ // 已经能按用户手感排(inferFlowDir 学票、自动挑侧),硬拒的最大受害者
256
+ // 是用户自己("帮我挪一下"被 agent 顶回"你自己拖")。放开但**如实报**:
257
+ // 挪的是他亲手摆的东西,agent 得心里有数、他不认可拖回去就是。
258
+ const wasUser = e.seat === 'user' ? '(原是用户亲手摆的,已挪 —— 他不认可会拖回去)' : '';
259
+ const box = rectOf(id);
260
+ if ('ref' in o.to) {
261
+ const p = placeRel(id, box, o.to);
262
+ if (!p) { fail(`参照 ${o.to.ref} 不在板上`); continue; }
263
+ setObj(id, { ...e, x: Math.round(p.x), y: Math.round(p.y), seat: 'agent' });
264
+ moveHuggers(id, Math.round(p.x) - e.x, Math.round(p.y) - e.y);
265
+ report.push(`· #${i + 1} move → (${Math.round(p.x)},${Math.round(p.y)})${p.pressed?.length ? `(⚠ 压住了 ${p.pressed.slice(0, 3).join('、')})` : ''}${wasUser}`);
266
+ } else if ('x' in o.to) {
267
+ const p = placeAbs(o.to, box);
268
+ if (!p) { fail(o.to.sheet ? `纸 ${o.to.sheet} 不存在(read_board 看纸的清单)` : '还没有铺过纸 —— 先 open_sheet,或用 {dx,dy}/{ref,side}'); continue; }
269
+ setObj(id, { ...e, x: p.x, y: p.y, seat: 'agent' });
270
+ moveHuggers(id, p.x - e.x, p.y - e.y);
271
+ report.push(`· #${i + 1} move → 纸 ${p.sheetId} (${p.x},${p.y})${p.clamped ? '(越界,钳进了版心)' : ''}${wasUser}`);
272
+ } else {
273
+ const nx = Math.round(e.x + o.to.dx); const ny = Math.round(e.y + o.to.dy);
274
+ setObj(id, { ...e, x: nx, y: ny, seat: 'agent' });
275
+ moveHuggers(id, nx - e.x, ny - e.y);
276
+ report.push(`· #${i + 1} move → (${nx},${ny})${wasUser}`);
277
+ }
278
+ ok += 1;
279
+ } else if (o.op === 'move_group') {
280
+ const members = Object.entries(live).filter(([, e]) => e.tag === bareTag(o.tag) && Number.isFinite(e?.x));
281
+ if (!members.length) { fail(`没有 #${o.tag} 的东西`); continue; }
282
+ const bb = { x: Math.min(...members.map(([, e]) => e.x)), y: Math.min(...members.map(([, e]) => e.y)) };
283
+ const rects = members.map(([id]) => rectOf(id));
284
+ const w = Math.max(...rects.map(r => r.x + r.w)) - bb.x; const h = Math.max(...rects.map(r => r.y + r.h)) - bb.y;
285
+ let p; let pressed = [];
286
+ if ('ref' in o.to) {
287
+ const refId = rid(o.to.ref);
288
+ const r = rectOf(refId);
289
+ if (!r) { fail(`参照 ${o.to.ref} 不在板上`); continue; }
290
+ const zone = layerOf(refId, live[refId], known);
291
+ const memberIds = new Set(members.map(([id]) => id));
292
+ p = placeBeside(r, { w, h }, o.to.side, o.to.gap ?? UNIT);
293
+ pressed = overlapIds({ x: p.x, y: p.y, w, h }, obstaclesNear(zone, memberIds));
294
+ } else if ('x' in o.to) {
295
+ const pa = placeAbs(o.to, { w, h });
296
+ if (!pa) { fail(o.to.sheet ? `纸 ${o.to.sheet} 不存在` : '还没有铺过纸 —— 先 open_sheet,或用 {dx,dy}/{ref,side}'); continue; }
297
+ p = pa;
298
+ } else {
299
+ p = { x: bb.x + o.to.dx, y: bb.y + o.to.dy };
300
+ }
301
+ const dx = Math.round(p.x - bb.x); const dy = Math.round(p.y - bb.y);
302
+ // 08-28 放开:user 座随组平移(相对格局原样保留,学 follow 平移跟随的先例)
303
+ // —— 旧行为跳过用户件会把组撕开留一半在原地,那才是最丑的结果。如实报件数。
304
+ const userSeated = members.filter(([, e]) => e.seat === 'user').map(([id]) => id);
305
+ const movedSet = new Set(members.map(([id]) => id));
306
+ for (const [id, e] of members) setObj(id, { ...e, x: e.x + dx, y: e.y + dy, ...(e.seat === 'user' ? {} : { seat: 'agent' }) });
307
+ for (const [id] of members) moveHuggers(id, dx, dy, movedSet);
308
+ if (userSeated.length) report.push(`· #${i + 1} move_group: 含用户亲手摆的 ${userSeated.length} 件(随组平移,相对格局保留)`);
309
+ report.push(`· #${i + 1} move_group #${o.tag} → 组左上 (${Math.round(p.x)},${Math.round(p.y)})${pressed.length ? `(⚠ 压住了 ${pressed.slice(0, 3).join('、')})` : ''}${p.clamped ? '(越界,钳进了版心)' : ''}`);
310
+ ok += 1;
311
+ } else if (o.op === 'remove') {
312
+ const id = rid(o.id); const e = id && live[id];
313
+ if (!e) { fail(`${o.id} 不在板上`); continue; }
314
+ if (!e.kind) {
315
+ // 板书文件卡:**agent 侧写的**放行(连文件),用户写的和普通产物卡拒。
316
+ // 08-26:agent 侧现在有三类署名(主控 'agent' + 常驻角色 rp-*)。原来死比
317
+ // 'agent',于是角色写的板书谁都删不掉(连角色自己),报错还说那是「用户的板书」。
318
+ if (id.startsWith(`${CHALK_DIR}/`) && e.by && e.by !== 'user') {
319
+ const abs = chalkAbsPath(projectId, id);
320
+ if (abs) chalkUnlinks.push(abs);
321
+ delete live[id]; objects[id] = null; ok += 1; continue;
322
+ }
323
+ fail(`${id} 是${id.startsWith(`${CHALK_DIR}/`) ? '用户的板书' : '产物卡'},不能从黑板删;整组擦用 erase_group`); continue;
324
+ }
325
+ delete live[id]; objects[id] = null; ok += 1;
326
+ } else if (o.op === 'add_node') {
327
+ const size = (o.size === 'sm' && o.text.length > 40) ? 'md' : (o.size || 'md');
328
+ const box = textBox(o.text, size, { md: o.format === 'md' });
329
+ const p = placeRel(null, box, o.at);
330
+ if (!p) { fail(`参照 ${o.at.ref} 不在板上`); continue; }
331
+ const refId = rid(o.at.ref);
332
+ const zone = layerOf(refId, live[refId], known);
333
+ const id = `text:a${stamp()}`;
334
+ const tag = o.tag || defaultTag || live[refId]?.tag || null;
335
+ setObj(id, {
336
+ x: Math.round(p.x), y: Math.round(p.y), z: 1, w: box.w, h: box.h, kind: 'text',
337
+ data: { t: o.text, ...(o.format === 'md' ? { format: 'md' } : {}), font: TEXT_FONTS.includes(o.font) ? o.font : 'pen', size, color: o.color || 'ink', ...(o.id ? { lid: o.id } : {}) },
338
+ zone, by, seat: 'agent', ...(tag ? { tag } : {}),
339
+ });
340
+ if (o.id) local.set(o.id, id);
341
+ report.push(`+ node ${o.id ? `${o.id}=` : ''}${id}`); ok += 1;
342
+ } else if (o.op === 'add_shape') {
343
+ // 事后圈重点(08-27 shapes 编辑面):给**已在板上**的东西补一个手画记号。
344
+ // hug 让它跟着目标走 —— 之前画完的圈是死的,目标一挪就散架。
345
+ const refId = rid(o.around); const r = refId && rectOf(refId);
346
+ if (!r) { fail(`around ${o.around} 不在板上`); continue; }
347
+ const seed = `${refId}:m${stamp()}`;
348
+ let sp; let ent;
349
+ if (o.kind === 'underline') {
350
+ sp = shapePath('underline', { to: { x: Math.max(8, r.w - 4), y: 0 } }, seed);
351
+ ent = { x: Math.round(r.x + 2 - 6), y: Math.round(r.y + r.h - 2 - 6) };
352
+ } else {
353
+ const padPx = o.kind === 'rect' ? 8 : 14;
354
+ let bx = { x: r.x - padPx, y: r.y - padPx, w: r.w + padPx * 2, h: r.h + padPx * 2 };
355
+ if (o.kind === 'circle') { const dmax = Math.max(bx.w, bx.h); bx = { x: bx.x + (bx.w - dmax) / 2, y: bx.y + (bx.h - dmax) / 2, w: dmax, h: dmax }; }
356
+ sp = shapePath(o.kind, { w: bx.w, h: bx.h }, seed);
357
+ ent = { x: Math.round(bx.x - 6), y: Math.round(bx.y - 6) };
358
+ }
359
+ const sid = `scribble:a${stamp()}`;
360
+ const tag2 = o.tag || defaultTag || live[refId]?.tag || null;
361
+ setObj(sid, {
362
+ ...ent, z: 1, w: Math.round(sp.w), h: Math.round(sp.h), kind: 'scribble',
363
+ data: { d: sp.d, color: o.color || 'ink', width: o.width || 2 },
364
+ by, seat: 'agent', hug: refId,
365
+ ...(tag2 ? { tag: tag2 } : {}),
366
+ zone: layerOf(refId, live[refId], known) || '',
367
+ });
368
+ report.push(`· #${i + 1} add_shape ${o.kind} 圈住 ${refId}(id ${sid},会跟着它走)`);
369
+ ok += 1;
370
+ } else if (o.op === 'set_shape') {
371
+ const id = rid(o.id); const e = id && live[id];
372
+ if (!e || e.kind !== 'scribble') { fail(`${o.id} 不是手画记号(scribble)`); continue; }
373
+ const data = { ...e.data };
374
+ if (o.color) data.color = o.color;
375
+ if (o.width) data.width = o.width;
376
+ setObj(id, { ...e, data }); ok += 1;
377
+ } else if (o.op === 'add_edge') {
378
+ const from = rid(o.from) || normalizeCanvasId(o.from);
379
+ const to = rid(o.to) || normalizeCanvasId(o.to);
380
+ if (!from || !to || from === to) { fail(`端点不合法:${o.from} → ${o.to}`); continue; }
381
+ // relate 下沉来的闸:两端都必须真实存在(板上有座 / doc: / 磁盘真身)
382
+ const missing = [];
383
+ if (!(await endpointReal(from, live, board.zones, sharedRoot))) missing.push(o.from);
384
+ if (!(await endpointReal(to, live, board.zones, sharedRoot))) missing.push(o.to);
385
+ if (missing.length) {
386
+ fail(`端点不在板上也不是存在的工作区路径:${missing.join(' / ')}。read_board 看一眼现在都有谁。`);
387
+ continue;
388
+ }
389
+ const id = `b:a${stamp()}`;
390
+ const tag = o.tag || defaultTag || live[from]?.tag || live[to]?.tag || null;
391
+ const binding = { type: o.type || 'link', from, to, by, ...(o.material && o.material !== 'ink' ? { material: o.material } : {}), ...(o.label ? { label: o.label } : {}), ...(tag ? { tag } : {}) };
392
+ bindings[id] = binding; liveBindings[id] = binding;
393
+ report.push(`+ edge ${id}`); ok += 1;
394
+ } else if (o.op === 'set_edge') {
395
+ const b = liveBindings[o.id];
396
+ if (!b) { fail(`线 ${o.id} 不存在`); continue; }
397
+ const nb = { ...b };
398
+ if (o.label !== undefined) { if (o.label) nb.label = o.label; else delete nb.label; }
399
+ if (o.type) nb.type = o.type;
400
+ if (o.material) { if (o.material === 'ink') delete nb.material; else nb.material = o.material; }
401
+ // 改端点(08-25 RP 案:「状态锚在这一章」每章 remove+add 两次 → 一条命令重指)
402
+ let bad = null;
403
+ for (const end of ['from', 'to']) {
404
+ if (o[end] === undefined) continue;
405
+ const nid = rid(o[end]) || normalizeCanvasId(o[end]);
406
+ if (!nid || !(await endpointReal(nid, live, board.zones, sharedRoot))) { bad = o[end]; break; }
407
+ nb[end] = nid;
408
+ }
409
+ if (bad) { fail(`新端点不在板上也不是存在的路径:${bad}`); continue; }
410
+ if (nb.from === nb.to) { fail('改完两端相同(自环)'); continue; }
411
+ bindings[o.id] = nb; liveBindings[o.id] = nb; ok += 1;
412
+ } else if (o.op === 'remove_edge') {
413
+ if (!liveBindings[o.id]) { fail(`线 ${o.id} 不存在`); continue; }
414
+ bindings[o.id] = null; delete liveBindings[o.id]; ok += 1;
415
+ } else if (o.op === 'reflow') {
416
+ const members = Object.entries(live).filter(([, e]) => e.tag === bareTag(o.tag) && Number.isFinite(e?.x) && e.kind !== 'scribble');
417
+ if (!members.length) { fail(`没有 #${o.tag} 的东西`); continue; }
418
+ const horizontal = o.layout === 'row';
419
+ const sorted = members.map(([id, e]) => ({ id, e, r: rectOf(id) }))
420
+ .sort((a, b) => (horizontal ? a.r.x - b.r.x || a.r.y - b.r.y : a.r.y - b.r.y || a.r.x - b.r.x));
421
+ // 08-28 放开:user 座也进重排 —— 调 reflow 本来就是明确的"求结构",
422
+ // 排序按现位置来,用户挑的**顺序**天然保留(他拖到中间的还在中间)。如实报件数。
423
+ const userSeated = sorted.filter(m => m.e.seat === 'user').map(m => m.id);
424
+ const left = Math.min(...sorted.map(m => m.r.x));
425
+ const top = Math.min(...sorted.map(m => m.r.y));
426
+ let cur = horizontal ? left : top;
427
+ for (const m of sorted) {
428
+ const nx = horizontal ? cur : left;
429
+ const ny = horizontal ? top : cur;
430
+ if (nx !== m.e.x || ny !== m.e.y) {
431
+ setObj(m.id, { ...m.e, x: Math.round(nx), y: Math.round(ny) });
432
+ // 圈着这个节点的记号跟着走 —— reflow 之前的病:文字重排、圈留在原地
433
+ moveHuggers(m.id, Math.round(nx) - m.e.x, Math.round(ny) - m.e.y);
434
+ }
435
+ cur += (horizontal ? m.r.w : m.r.h) + 16;
436
+ }
437
+ if (userSeated.length) report.push(`· #${i + 1} reflow: 含用户拖过的 ${userSeated.length} 件(顺序按他摆的保留)`);
438
+ ok += 1;
439
+ } else if (o.op === 'follow') {
440
+ const gTag = bareTag(o.group_tag); const tTag = bareTag(o.target_tag);
441
+ const members = Object.entries(live).filter(([, e]) => e.tag === gTag && Number.isFinite(e?.x));
442
+ if (!members.length) { fail(`没有 #${o.group_tag} 的东西`); continue; }
443
+ // 规则先立下(2026-08-30):follow 是**一条规则**,不是一条线。目标 tag 现在
444
+ // 空着不是错 —— skill 教的顺序就是「开场画状态板 → 立跟随 → 写第一章」,
445
+ // 而线要求两端此刻都在板上,于是这条最自然的写法过去必炸(全库 5 次 / 4 个
446
+ // 项目)。规则落进 board.follows,第一件带该 tag 的东西一落,applyFollows
447
+ // 顺手把线接上。
448
+ follows[gTag] = { target: tTag, ...(o.side ? { side: o.side } : {}), ...(o.label ? { label: o.label } : {}) };
449
+ const targets = Object.entries(live).filter(([, e]) => e.tag === tTag && Number.isFinite(e?.x));
450
+ if (!targets.length) {
451
+ ok += 1;
452
+ report.push(`· follow:规则立好了 —— #${gTag} 从此跟着 #${tTag} 的最新一件。`
453
+ + `#${tTag} 板上还没有东西,等第一件落下来自动接线并就位(不用再调一次)`);
454
+ continue;
455
+ }
456
+ const from = members.sort((a, b) => a[1].y - b[1].y)[0][0]; // 组里最上面那件当代表
457
+ const to = targets.sort((a, b) => (a[1].y + (a[1].h || 0)) - (b[1].y + (b[1].h || 0))).pop()[0]; // 最下面 = 最新
458
+ if (from === to) { fail('组代表和目标是同一件(group_tag/target_tag 传反了?)'); continue; }
459
+ const existing = Object.entries(liveBindings).find(([, b]) => b.follow === bareTag(o.target_tag) && live[b.from]?.tag === bareTag(o.group_tag));
460
+ const id = existing ? existing[0] : `b:a${stamp()}`;
461
+ const binding = { type: 'annotates', from, to, by, label: o.label || '跟随', follow: bareTag(o.target_tag), ...(o.side ? { followSide: o.side } : {}) };
462
+ bindings[id] = binding; liveBindings[id] = binding; ok += 1;
463
+ // 立规则的同时把组摆到目标旁边:之后的跟随是**平移**(保留相对格局),
464
+ // 基线偏移必须在此刻就有意义 —— 不摆的话组停在原地,平移只是搬运一个
465
+ // 无意义的初始偏移(08-25 平移跟随改造时补)
466
+ {
467
+ const r = rectOf(to);
468
+ const memberIds = new Set(members.map(([mid]) => mid));
469
+ const zone = layerOf(to, live[to], known);
470
+ const rects = members.map(([mid]) => rectOf(mid));
471
+ const bb = {
472
+ x: Math.min(...rects.map(x => x.x)), y: Math.min(...rects.map(x => x.y)),
473
+ w: Math.max(...rects.map(x => x.x + x.w)) - Math.min(...rects.map(x => x.x)),
474
+ h: Math.max(...rects.map(x => x.y + x.h)) - Math.min(...rects.map(x => x.y)),
475
+ };
476
+ void zone;
477
+ const pp = placeBeside(r, { w: bb.w, h: bb.h }, o.side || 'right', UNIT);
478
+ const mdx = Math.round(pp.x - bb.x); const mdy = Math.round(pp.y - bb.y);
479
+ if (mdx || mdy) for (const [mid, me] of members) setObj(mid, { ...me, x: me.x + mdx, y: me.y + mdy, seat: 'agent' });
480
+ }
481
+ report.push(`· follow:#${o.group_tag} 已摆到目标旁并从此跟着 #${o.target_tag} 的最新一件(整组平移,用户摆的相对位置保留)`);
482
+ } else if (o.op === 'unfollow') {
483
+ const gTag = bareTag(o.group_tag);
484
+ const hits = Object.entries(liveBindings).filter(([, b]) => b.follow && live[b.from]?.tag === gTag);
485
+ const hadRule = !!board.follows?.[gTag];
486
+ if (!hits.length && !hadRule) { fail(`#${o.group_tag} 没有跟随线`); continue; }
487
+ for (const [id] of hits) { bindings[id] = null; delete liveBindings[id]; }
488
+ follows[gTag] = null; // 规则也撤 —— 只删线的话下一件落下来它又长回来
489
+ ok += 1;
490
+ } else if (o.op === 'commit') {
491
+ const { committed: n } = await commitStaging(projectId, { tag: o.tag || null });
492
+ committed += n; ok += 1;
493
+ report.push(`· commit:落定 ${n} 件`);
494
+ } else if (o.op === 'erase_group') {
495
+ const { removed } = await removeByTag(projectId, o.tag);
496
+ erased += removed; ok += 1;
497
+ for (const id of Object.keys(live)) if (live[id]?.tag === o.tag && live[id]?.kind) delete live[id];
498
+ report.push(`· erase_group #${o.tag}:擦掉 ${removed} 件`);
499
+ } else if (o.op === 'roll') {
500
+ // 收卷(2026-08-27 收纳器):只立状态位,成员座位一件不动 —— 前端把这组
501
+ // 藏进一张卷卡,展开即归位。视觉/渲染/read_board 三头减负,地皮照旧占着
502
+ //(落位引擎仍把它们当障碍,所以永远不会有新东西压进卷里)。
503
+ const members = Object.entries(live).filter(([, e]) => e?.tag === o.tag && Number.isFinite(e?.x));
504
+ if (!members.length) { fail(`没有 #${o.tag} 的东西,没得收`); continue; }
505
+ if (board.rolls?.[o.tag] && !rolls[o.tag]) {
506
+ report.push(`· #${i + 1} roll:#${o.tag} 本来就收着(${members.length} 件)`); ok += 1; continue;
507
+ }
508
+ rolls[o.tag] = { at: new Date().toISOString(), by, ...(o.label ? { label: o.label } : {}) };
509
+ report.push(`· #${i + 1} roll:#${o.tag} 收进卷里(${members.length} 件,座位和文件都在,卷卡单击可展开)`);
510
+ ok += 1;
511
+ } else if (o.op === 'unroll') {
512
+ if (!board.rolls?.[o.tag] && rolls[o.tag] === undefined) { fail(`#${o.tag} 没收着`); continue; }
513
+ rolls[o.tag] = null;
514
+ report.push(`· #${i + 1} unroll:#${o.tag} 展开,全部归位`);
515
+ ok += 1;
516
+ } else if (o.op === 'feature') {
517
+ const id = rid(o.id) || normalizeCanvasId(o.id);
518
+ if (!id) { fail(`${o.id} 不合法`); continue; }
519
+ heroPatch = id; ok += 1;
520
+ } else if (o.op === 'unfeature') {
521
+ heroPatch = null; ok += 1;
522
+ } else if (o.op === 'transform_group') {
523
+ // 整组缩放/旋转(2026-08-30,拆件见 lib/board-transform.js):涂鸦真变形,
524
+ // 文字/卡只换座(字号没有"转 30°"的渲染语义,硬转是藏问题)
525
+ if (!o.scale && !o.rotate) { fail('transform_group 要 scale 或 rotate(都不给等于没变)'); continue; }
526
+ const gTag2 = bareTag(o.tag);
527
+ const members = Object.entries(live)
528
+ .filter(([, e]) => e?.tag === gTag2 && Number.isFinite(e?.x))
529
+ .map(([id, e]) => ({ id, entry: e, ...estimateSizeOn(board, id, e) }));
530
+ if (!members.length) { fail(`没有 #${o.tag} 的东西`); continue; }
531
+ const r = transformGroup(members, { scale: o.scale || 1, rotate: o.rotate || 0 });
532
+ for (const [id, e] of Object.entries(r.patch)) setObj(id, e);
533
+ report.push(`· transform_group #${o.tag}:绕组心 (${r.center.x},${r.center.y}) ${o.scale ? `缩放 ${o.scale}×` : ''}${o.rotate ? ` 旋转 ${o.rotate}°` : ''} —— ${r.inked} 件涂鸦真变形${r.seated ? `,${r.seated} 件(文字/卡)只挪了位没变形` : ''}`);
534
+ ok += 1;
535
+ } else if (o.op === 'replan') {
536
+ // 补版位/调版位(刀⑧ 2026-08-30,拆件见 sheet-replan.js)
537
+ const r = applyReplan({ board, sheetsPatch, sessionId, op: o });
538
+ if (r.error) { fail(r.error); continue; }
539
+ sheetsPatch[r.sheetId] = r.entry;
540
+ report.push(r.report);
541
+ ok += 1;
542
+ } else if (o.op === 'chalk_edit') {
543
+ // 改板书开关(08-25 用户提:黑板 RP 这类板书密集会话该由 agent 帮忙打开)。
544
+ // 存 ui-config(重开页面还在),并广播给开着的前端当场生效。
545
+ const cfg = (await readUiConfigFile(sharedRoot)) || {};
546
+ await writeUiConfig(sharedRoot, { ...cfg, chalk_edit: !!o.on });
547
+ try { ctx?.emit?.({ type: 'ui.chalk_edit', sessionId: null, on: !!o.on }); } catch { /* */ }
548
+ report.push(`· 改板书开关 → ${o.on ? '开(用户现在可直接拖动/编辑板书)' : '关'}`);
549
+ ok += 1;
550
+ }
551
+ } catch (e) { fail(String(e?.message || e).slice(0, 120)); }
552
+ }
553
+ if (!ok) return err(`没有一条操作成功:\n${report.join('\n')}`);
554
+ if (Object.keys(objects).length || Object.keys(bindings).length || Object.keys(rolls).length || Object.keys(follows).length || Object.keys(zonesPatch).length || Object.keys(sheetsPatch).length || heroPatch !== undefined) {
555
+ await patchBoard(projectId, {
556
+ objects, bindings,
557
+ ...(Object.keys(zonesPatch).length ? { zones: zonesPatch } : {}),
558
+ ...(Object.keys(sheetsPatch).length ? { sheets: sheetsPatch } : {}),
559
+ ...(Object.keys(rolls).length ? { rolls } : {}),
560
+ ...(Object.keys(follows).length ? { follows } : {}),
561
+ ...(heroPatch !== undefined ? { hero: heroPatch } : {}),
562
+ });
563
+ }
564
+ // 软删进 .nd/trash/(08-25:删掉的板书要捞得回来,别裸 unlink)
565
+ for (const abs of chalkUnlinks) await trashChalkFile(sharedRoot, abs);
566
+ try { ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: `改了黑板(${ok} 处)` }); } catch { /* */ }
567
+ return { content: [{ type: 'text', text: `Applied ${ok}/${ops.length} op(s).${report.length ? `\n${report.join('\n')}` : ''}` }] };
568
+ };
569
+ }