@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
@@ -1,139 +1,599 @@
1
1
  /**
2
- * mcp/tools/write-on-board.js —— write_on_board2026-08-23 黑板三期)
2
+ * mcp/tools/write-on-board.js —— write_on_board 统一入口(2026-08-25 范式重做②;
3
+ * 2026-08-29 纸范式刀 2:落位从启发式引擎换成纸)
3
4
  *
4
- * agent 在画布上**说话**的主通道:一段 md 板书,贴着它说的那件东西(`near`),或者
5
- * 接在用户/自己的某条板书下面(`reply_to` = 线程)。本体是文件
6
- * (notes/板书/<stamp>-<slug>.md,见 lib/chalk.js),board.json 只存位置和线。
5
+ * 总纲(站主拍板):**一条板书 = 单节点图,是统一模型的退化情形。** 写字入口只有
6
+ * 这一个;本体选什么不由 agent 选、不由入口分,由一条服务端判据自动定 ——
7
+ * **这一次落板的件数(nodes + shapes 合计;text 简写 = 1 件)**:
7
8
  *
8
- * 落位纪律(gutter):
9
- * - reply_to:落在被回应那条的**正下方**(同列、间距 12),自动连 flow 线(读序)
10
- * - near:落在锚点右侧、顶对齐,撞了往下让;自动连 annotates 线(这段话关于它)
11
- * - 都没有:用户视口里的空地 > 桌面内容底下
12
- * 跟 create_on_board 的分工:那是"记号"(手写一句、画布原生、给人看),这是"话"
13
- * (md、文件本体、agent 要能读回来、有线程)。黑板模式下回复的主体走这里。
9
+ * 件数 = 1(一句话) 件数 ≥ 2(一张图)
10
+ * 本体 notes/板书/*.md 真文件 画布原生 text:/scribble: + data.lid
11
+ * tag 不打(可显式传并组) 必有,缺省自动 sk-<stamp>
12
+ * staging false true(finish 或回合末落定)
13
+ *
14
+ * ## 落位 = 纸(2026-08-29)
15
+ *
16
+ * 旧回路「模糊锚点 + 机器启发式找洞」退役(环搜/挑侧/走廊/学走向全删 —— ㉚ 五刀
17
+ * 证明那一层的坑修不完)。新回路:
18
+ * - at:{x,y} = **当前纸的版心局部像素**(agent 精确摆放,钳进版心、钳了如实报)
19
+ * - reply_to/chain = 接楼:正下方(压住跳过),纸写满自动翻下一张
20
+ * - 什么都不给 = 纸内按阅读序往下排;**排不下就拒收**(08-29 刀 F 起不再替它翻页)
21
+ * - near = 画线的语义(annotates/flow),不再驱动落位;near+side 显式给时按
22
+ * 精确贴放(题注在上方这类语义要求)
23
+ * 没铺过纸时第一笔自动铺一张(对准用户视口)。文件夹层没有纸:reply/near 照常,
24
+ * at 礼貌拒收。
14
25
  */
15
26
 
16
27
  import { tool } from '@anthropic-ai/claude-agent-sdk';
17
- import { z } from 'zod';
18
- import { readBoard, patchBoard } from '../../../projects/board-store.js';
28
+ import { byOf } from '../actor.js';
29
+ import { readBoard, patchBoard, TEXT_FONTS } from '../../../projects/board-store.js';
30
+ import { TAG_RE } from '../../../projects/board-sanitize.js';
19
31
  import { estimateSizeOn } from '../../../lib/board-kind-sizes.js';
20
- import { layerOf, normalizeCanvasId, tagEnvelope } from '../../../lib/canvas-id.js';
21
- import { textBox, findSpot, SKETCH_FIT } from '../../../lib/sketch-layout.js';
32
+ import { layerOf, normalizeCanvasId } from '../../../lib/canvas-id.js';
33
+ import { endpointReal } from './edit-board.js';
34
+ import { BINDING_TYPE_IDS } from '../../../lib/binding-types.js';
35
+ import { UNIT, SKETCH_FIT, SKETCH_MAX, textBox, layoutNodes, resolveTemplate, bboxOrZero, fitFor, capacityOf } from '../../../lib/sketch-layout.js';
36
+ import { CARD_MAX_H } from '../../../lib/screen.js';
37
+ import { innerRect } from '../../../lib/board-sheets.js';
38
+ import { obstaclesIn } from '../../../lib/board-obstacles.js';
39
+ import { makeSheetPlacer } from './write-on-board-place.js';
40
+ import { openSheetFor } from './open-sheet.js';
41
+ import { buildSketchShapes, SKETCH_COLORS as COLORS } from '../../../lib/sketch-shapes.js';
42
+ import { makeAnchorResolver } from '../../../lib/board-anchor.js';
22
43
  import { getViewpoint } from '../../../projects/viewpoint-store.js';
23
44
  import { renderChalk, chalkFileName, writeChalkFile, CHALK_DIR } from '../../../lib/chalk.js';
45
+ import { STATE_TABLE_TAG, parseStateTable } from '../../../lib/state-table.js';
46
+ import { maybeFlowWrite } from './write-on-board-flow.js';
47
+ import { ROLE_SLUG_RE } from '../../agent/cast.js';
48
+ import { WRITE_SCHEMA as SCHEMA } from './write-on-board-schema.js';
49
+ import { roleDefaultAnchor } from './write-on-board-role-anchor.js';
50
+ import { seatArtifacts } from '../../runs/board-seater.js';
51
+ import { applyFollows } from '../../../lib/board-follow.js';
24
52
  import { Events } from '../../agent/events.js';
53
+ import { learnedChalkWidth } from '../../../lib/chalk-size-pref.js';
25
54
 
26
55
  let seq = 0;
27
56
  const stamp = () => `${Date.now().toString(36)}${(seq++ % 1000).toString(36)}`;
28
57
 
58
+ /** md 侦测:正文带 markdown 记号却标 plain 会把 **加粗** 原样吐出来(ldx 案) */
59
+ const looksLikeMd = (t) => /(\*\*|__|^#{1,4}\s|^\s*[-*]\s|\|.+\||```|\$[^$]+\$|\[.+\]\(.+\))/m.test(t);
60
+
61
+ const DESCRIPTION = `Write on the board — the ONE way to put words and pictures on the canvas.
62
+ The board is the conversation; the sidebar is the log.
63
+
64
+ Work happens on SHEETS (open_sheet lays one — a screenful of paper). One thought =
65
+ one call. What you pass decides what lands:
66
+ - {text} → a single Markdown note. It is a real file (${CHALK_DIR}/…md) you can
67
+ Read/Grep/Edit later. near = what it is about (annotates line). reply_to = thread
68
+ under another note; chain:true = auto-thread onto your latest note of the same tag.
69
+ - {nodes, shapes, edges, …} → a whole sketch in one call (comparison table, flow,
70
+ mind map, detective board linking real artifacts). You describe STRUCTURE on a grid
71
+ (1 cell = ${UNIT}px); the server does pixels and hand-drawn shapes. The sketch gets a
72
+ #tag (read/select/erase as a group) and lands as STAGING until commit or turn end.
73
+ Placement — sheet coordinates, no guessing:
74
+ - at:{x,y} = pixels from the current sheet's top-left writable corner. You OWN the
75
+ layout inside a sheet — place precisely, side by side, wherever reads best.
76
+ - no at = flows top-to-bottom on the current sheet; a full sheet turns the page
77
+ automatically. reply_to lands right below the note it answers.
78
+ - New topic/chapter → open_sheet first, so each sheet reads as one page.
79
+ Threads are tags: {tag, chain:true} continues a line of thought; fork with
80
+ {tag:"新名", open_lane:"<id>"} (a fresh sheet is laid for it).
81
+ Node text carrying markdown marks defaults to format md (KaTeX $…$ and \`\`\`mermaid fences work).
82
+ Readability: user reads at 75–100% zoom — body text md/lg; one sketch fits one sheet.
83
+ To change what is already on the board use edit_board — do not redraw.
84
+ Keep the chat reply to one line pointing here.`;
85
+
29
86
  export function makeWriteOnBoardTool({ projectId, sharedRoot, sessionId, ctx }) {
30
- const turnStamp = { turn: -1, count: 0 };
31
- return tool(
32
- 'write_on_board',
33
- `Say it ON THE BOARD: write a short Markdown note on the canvas, beside the thing it is
34
- about (near) or right under a note you are replying to (reply_to). This is your main way
35
- to talk to the user when they work on the canvas — the sidebar is the log, the board is
36
- the conversation. The note is a real file (${CHALK_DIR}/…md) you can Read/Grep/Edit later.
37
-
38
- - near: canvas id the note is about (an artifact card, an image, another note) → lands to
39
- its right, top-aligned, with an annotates line. Use it right after you finish something:
40
- "what this is / why this way / what to look at".
41
- - reply_to: path of a board note (yours or the user's) → lands right under it with a
42
- flow line (a thread). When the user annotates one of your notes, answer with reply_to.
43
- - neither lands in the user's viewport (or below current content).
44
- - text: Markdown (+KaTeX, lists, tables; ≤1500 chars, keep it one thought — long
45
- analysis belongs in a .md the user opens). size 'md' default, 'lg' for headlines.
46
- Keep the chat reply short — a line pointing at the board is enough.`,
47
- {
48
- text: z.string().min(1).max(1500),
49
- near: z.string().max(300).optional(),
50
- reply_to: z.string().max(300).optional().describe(`path like ${CHALK_DIR}/20260823-070809-xxx.md`),
51
- tag: z.string().regex(/^[\w一-鿿぀-ヿ-]{1,40}$/).optional(),
52
- size: z.enum(['md', 'lg']).optional(),
53
- width: z.number().min(8).max(30).optional().describe('grid units (24px); default by content, max 30'),
54
- },
55
- async ({ text, near, reply_to: replyTo, tag, size, width }) => {
56
- const err = (t) => ({ content: [{ type: 'text', text: t }], isError: true });
57
- if (!projectId || !sharedRoot) return err('No project bound.');
58
- // 回合闸 08-23 用户拍板先不设:只记数不拦
59
- const turn = ctx?.runId ?? -1;
60
- if (turnStamp.turn !== turn) { turnStamp.turn = turn; turnStamp.count = 0; }
61
- const body = String(text).trim();
87
+ const handler = makeHandler({ projectId, sharedRoot, sessionId, ctx });
88
+ return tool('write_on_board', DESCRIPTION, SCHEMA, handler);
89
+ }
90
+
91
+ function makeHandler({ projectId, sharedRoot, sessionId, ctx }) {
92
+ return async function handler(args, extra) {
93
+ // 署名:主 agent 'agent',常驻角色 它的 slug。权威是 harness 在派发时盖的章
94
+ // (agent/actor-trail.js),不是角色文件里的自称 —— 那份文件模型能改。
95
+ const by = byOf(extra);
96
+ const err = (t) => ({ content: [{ type: 'text', text: t }], isError: true });
97
+ if (!projectId || !sharedRoot) return err('No project bound.');
98
+
99
+ const nodesIn = args.nodes || [];
100
+ const shapesIn = args.shapes || [];
101
+ const edgesIn = args.edges || [];
102
+ const hasSketch = nodesIn.length || shapesIn.length;
103
+ if (args.text && hasSketch) {
104
+ return err('text 是"单节点图"的简写,跟 nodes/shapes 二选一:一句话给 text,一张图把它写成一个 node。');
105
+ }
106
+ if (!args.text && !hasSketch) {
107
+ return err('空手不上板:给 text(一句话)或 nodes/shapes(一张图)。只想画线用 edit_board 的 add_edge。');
108
+ }
109
+ // 单节点图 = 一句话(统一模型的退化情形):转文件本体那条路,语义字段全保
110
+ if (!args.text && !args.title && nodesIn.length === 1 && !shapesIn.length && !edgesIn.length) {
111
+ const n = nodesIn[0];
112
+ // ⚠️ extra 必须往下传:署名是从 extra 里的 toolUseId 查回来的,
113
+ // 这条自递归漏了它的话,角色用 `nodes:[一件]` 写的板会静默署成 'agent'
114
+ return handler({
115
+ text: n.text, near: args.near, reply_to: args.reply_to, at: args.at, sheet: args.sheet,
116
+ side: args.side, relation: args.relation, chain: args.chain, tag: args.tag, size: n.size, width: n.w,
117
+ }, extra);
118
+ }
119
+
120
+ let board = await readBoard(projectId);
121
+ const known = new Set(Object.keys(board.zones || {}));
122
+ // 这一层上谁占着地方(含文件夹卡/卷卡/精灵身位,见 lib/board-obstacles.js)
123
+ const obstaclesOf = (b, zone) => obstaclesIn(b, zone);
124
+ const vp = getViewpoint(projectId);
125
+ const fit = fitFor(vp);
126
+ // 车道封顶(08-28):触屏档一件不许超过一屏宽。**板书和草图两条路都要过它** ——
127
+ // 只封一条的下场是真会话里量到的:草图乖乖 336,板书照旧 432(判据在 device-lane)。
128
+ // ⛔ 要传 wUnits 不能事后夹 w:textBox 按宽度回推行数算高度,只夹宽=文字溢出框外且不报错。
129
+ const capUnits = fit.column ? Math.max(4, Math.floor(fit.w / UNIT)) : null;
130
+ const capW = (u) => (capUnits ? Math.min(u || capUnits, capUnits) : u);
131
+ const vpRectFor = (zone) => (vp && (vp.layer || '') === (zone || '') && vp.camera) ? vp.camera : null;
132
+ const visibleIn = (rect, vpRect) => !!vpRect && !(rect.x + rect.w < vpRect.x || vpRect.x + vpRect.w < rect.x
133
+ || rect.y + rect.h < vpRect.y || vpRect.y + vpRect.h < rect.y);
134
+
135
+ // 锚点解析(真 id > tag 包络 > 救援入座)本体在 lib/board-anchor.js(棘轮拆件)
136
+ const resolveAnchor = makeAnchorResolver({ projectId, known, readBoard, seatArtifacts });
137
+ // 纸上落位三分支(棘轮拆件,见 write-on-board-place.js)
138
+ const { placeOnSheets, placeInZone, describeSpot, resolveSlot, placeInSlot, describeSheetFull } = makeSheetPlacer({ projectId, sessionId, by });
139
+
140
+ // ───────────────────────── 件数 = 1:板书(文件本体) ─────────────────────────
141
+ if (args.text) {
142
+ let body = String(args.text).trim();
62
143
  if (!body) return err('空话不上板。');
144
+ // 控件围栏自愈(08-28 泉此方案):角色把 nd:controls 写成裸文本开头 —— 语义无歧义
145
+ // (正文以 nd:controls 起头且全文无围栏),替它补上,渲染层只认 ```nd:controls
146
+ if (/^nd:controls\s*\n/.test(body) && !body.includes('```')) {
147
+ body = '```nd:controls\n' + body.replace(/^nd:controls\s*\n/, '') + '\n```';
148
+ }
149
+ // 手写字(ink:'hand',08-27 收编 create_on_board):线程语义长在板书文件上
150
+ if (args.ink === 'hand' && (args.chain || args.open_lane || args.reply_to)) {
151
+ return err("ink:'hand' 是画布手写字(无文件本体),接不进线程 —— 要 chain/open_lane/reply_to 就用默认的 chalk。");
152
+ }
153
+ // ── 开新线(open_lane):模型声明拓扑,机器给这条线铺自己的纸 ──
154
+ if (args.open_lane) {
155
+ if (!args.tag) return err('open_lane 要配 tag:tag 就是这条线的名字,后续用 {tag, chain:true} 续。');
156
+ if (args.reply_to || args.chain || args.at || args.near) {
157
+ return err('open_lane 是开新线,跟 reply_to/chain/at/near 互斥 —— 岔出点直接写在 open_lane 里。');
158
+ }
159
+ if (board.lanes?.[args.tag]) {
160
+ return err(`线 #${args.tag} 已经开过了(read_board 的「线的清单」那一节看得到)。接着写用 {tag:"${args.tag}", chain:true};真要另起炉灶,换个名字。`);
161
+ }
162
+ }
163
+ // chain:接在同 tag 最新一条**自己写的**板书后面(chapter 线程不再手抄路径)。
164
+ // 接续权(2026-08-27 编排):chain 是「续写我的线程」,永远不跨作者 ——
165
+ // GM 的章节线和每个角色的叙事线各自延各自的,中间插了别人的话也不串线。
166
+ let replyToRaw = args.reply_to || null;
167
+ if (!replyToRaw && args.chain) {
168
+ const chalks = Object.entries(board.objects)
169
+ .filter(([id, e]) => id.startsWith(`${CHALK_DIR}/`) && Number.isFinite(e?.x)
170
+ && (!args.tag || e.tag === args.tag) && (e.by || 'agent') === by)
171
+ .map(([id]) => id).sort();
172
+ if (chalks.length) replyToRaw = chalks[chalks.length - 1];
173
+ }
174
+ // 角色缺省锚(08-28;专线优先、「这一拍」其次,拆件见 write-on-board-role-anchor.js)
175
+ let nearRaw = args.near || null;
176
+ if (args.ink !== 'hand' && !replyToRaw && !nearRaw && !args.at && !args.open_lane
177
+ && ROLE_SLUG_RE.test(by)) {
178
+ const d = await roleDefaultAnchor({ board, by, sharedRoot });
179
+ if (d.replyTo) {
180
+ replyToRaw = d.replyTo;
181
+ if (d.tag && !args.tag) args.tag = d.tag; // 进线就着线的 tag,下一条才续得上
182
+ }
183
+ }
63
184
 
64
- const board = await readBoard(projectId);
65
- const known = new Set(Object.keys(board.zones || {}));
66
- // 默认宽度:散文按 14 格(336px)排,短句按内容;太窄会把一句话折成三行
67
- const em = (l) => [...l].reduce((n, c) => n + (/[\u3000-\u9fff\uff00-\uffef]/.test(c) ? 1 : 0.62), 0);
185
+ // 版位(08-29 E):解析要在算宽度之前 —— 一块地多宽,写进去的就多宽
186
+ let slotInfo = null;
187
+ if (args.slot) {
188
+ slotInfo = resolveSlot(board, { slotName: args.slot, sheetName: args.sheet || null });
189
+ if (slotInfo.error) return err(slotInfo.message);
190
+ }
191
+ const em = (l) => [...l].reduce((n, c) => n + (/[ -鿿＀-￯]/.test(c) ? 1 : 0.62), 0);
68
192
  const longest = Math.max(...body.split('\n').map(em));
69
- const wUnits = width || (longest <= 12 ? null : Math.max(12, Math.min(18, Math.ceil(longest * 16 / 24) + 1)));
70
- const box = textBox(body, size || 'md', { md: true, wUnits });
193
+ // 宽度三档回落(2026-08-28):模型点名 > 用户调出来的偏好 > 按正文估。
194
+ // 中间那档是「模仿用户」:他拖宽过板书就说明这个版心读着舒服,下一拍照做,
195
+ // 别让他反复调同一件事。判据是前端拖手柄盖的 sized:'user' 章,模型盖不出。
196
+ const wUnits = slotInfo
197
+ ? Math.max(4, Math.floor(slotInfo.rect.w / UNIT))
198
+ : capW(args.width || learnedChalkWidth(board)
199
+ || (longest <= 12 ? null : Math.max(12, Math.min(18, Math.ceil(longest * 16 / 24) + 1))));
200
+ let box = textBox(body, args.size === 'sm' ? 'md' : (args.size || 'md'), { md: true, wUnits });
201
+ // 占位(刀⑧ 2026-08-30):agent 先声明框的高度、再往里流内容 —— 容量检查、
202
+ // 落位、流式预览全按预约框算。内容更高时按真身来(框不许对内容撒谎)。
203
+ if (!args.flow && Number.isFinite(args.h) && args.h > box.h) {
204
+ box = { ...box, h: Math.round(args.h), reserved: true };
205
+ }
206
+
71
207
  let zone = '';
72
- let pos = null;
73
208
  let anchorId = null; let parentId = null;
74
- const obstaclesOf = (z) => Object.entries(board.objects || {})
75
- .filter(([id, e]) => Number.isFinite(e?.x) && layerOf(id, e, known) === z)
76
- .map(([id, e]) => ({ id, x: e.x, y: e.y, ...estimateSizeOn(board, id, e) }));
77
- const avoid = (p, obstacles, w, h) => {
78
- for (let g = 0; g < 40; g += 1) {
79
- const b = obstacles.find(o => !(p.x + w <= o.x || o.x + o.w <= p.x || p.y + h <= o.y || o.y + o.h <= p.y));
80
- if (!b) break;
81
- p.y = b.y + b.h + 12;
82
- }
83
- return p;
84
- };
85
- if (replyTo) {
86
- const pid = normalizeCanvasId(replyTo);
87
- const e = pid ? board.objects?.[pid] : null;
88
- if (!e || !Number.isFinite(e.x)) return err(`reply_to ${replyTo} 不在板上(read_board 里看不到就接不上)。`);
89
- parentId = pid; zone = layerOf(pid, e, known);
90
- const ps = estimateSizeOn(board, pid, e);
91
- pos = avoid({ x: e.x, y: e.y + ps.h + 12 }, obstaclesOf(zone), box.w, box.h);
92
- } else if (near) {
93
- const nid = normalizeCanvasId(near);
94
- const e = nid ? board.objects?.[nid] : null;
95
- if (e && Number.isFinite(e.x)) {
96
- anchorId = nid; zone = layerOf(nid, e, known);
97
- const as = estimateSizeOn(board, nid, e);
98
- pos = avoid({ x: e.x + as.w + 24, y: e.y }, obstaclesOf(zone), box.w, box.h);
209
+ let replyRect = null; let anchorRect = null;
210
+ let b2 = board; // 救援入座后换新板(新座要进压上判定)
211
+ // 开新线:岔出点解析(fresh = 无岔出点)
212
+ let laneFrom = null; // {id, rect} | 'fresh'
213
+ if (args.open_lane) {
214
+ if (args.open_lane === 'fresh') {
215
+ laneFrom = 'fresh';
99
216
  } else {
100
- // near 也认 tag(08-23 案):落到那片东西的包络右侧,锚线连最右那个
101
- const env = tagEnvelope(board, near, (id2, e2) => estimateSizeOn(board, id2, e2));
102
- if (!env) return err(`锚点 ${near} 还没有座位,也不是板上任何 tag(read_board 里看不到就锚不上)。`);
103
- anchorId = env.anchorId; zone = layerOf(env.anchorId, board.objects[env.anchorId], known);
104
- pos = avoid({ x: env.x + env.w + 24, y: env.y }, obstaclesOf(zone), box.w, box.h);
217
+ const a = await resolveAnchor(args.open_lane, board);
218
+ if (!a) {
219
+ return err(`open_lane 的岔出点 ${args.open_lane} 不在板上(read_board 看一眼现在都有谁)。全新话题用 open_lane:'fresh'。`);
220
+ }
221
+ laneFrom = { id: a.anchorId, rect: a.rect };
222
+ zone = a.zone; if (a.board) b2 = a.board;
223
+ // 分支线:岔出点 → 新线头(flow 读序),跟 near 的画线机制共用
224
+ anchorId = a.anchorId; anchorRect = a.rect;
225
+ }
226
+ }
227
+ if (replyToRaw) {
228
+ const pid2 = normalizeCanvasId(replyToRaw);
229
+ const e = pid2 ? board.objects?.[pid2] : null;
230
+ if (!e || !Number.isFinite(e.x)) return err(`reply_to ${replyToRaw} 不在板上(read_board 里看不到就接不上)。`);
231
+ // 接续权(2026-08-27 编排):角色的话头只有它自己和用户能接。主控接上去
232
+ // 就是代笔/插嘴的物理形态 —— 这条按板上对象的**作者**判,不看内容不看场。
233
+ // 角色之间可以互接(那就是对话),角色接主控的旁白也行。
234
+ if (by === 'agent' && typeof e.by === 'string' && ROLE_SLUG_RE.test(e.by)) {
235
+ return err(`这条是「${e.by}」的话,你不接在它下面。想让它接着说:把 cue 寄给它`
236
+ + `(SendMessage)或让用户直接跟它说;你自己的旁白/场记另起一条(near 指过去就行)。`);
105
237
  }
238
+ parentId = pid2; zone = layerOf(pid2, e, known);
239
+ replyRect = { x: e.x, y: e.y, ...estimateSizeOn(board, pid2, e) };
240
+ }
241
+ if (nearRaw) {
242
+ const a = await resolveAnchor(nearRaw, board);
243
+ if (!a && !parentId && !args.at) {
244
+ return err(`锚点 ${nearRaw} 不在板上:既没有座位、不是任何 tag,磁盘上也没有这个文件(read_board 看一眼现在都有谁)。`);
245
+ }
246
+ if (a) { anchorId = a.anchorId; anchorRect = a.rect; if (!parentId) zone = a.zone; if (a.board) b2 = a.board; }
247
+ }
248
+
249
+ const obstacles = obstaclesOf(b2, zone);
250
+ const vpRect = vpRectFor(zone);
251
+
252
+ // ── flow(刀⑦ 2026-08-30):长文由机器按段拆成一串卡大小的板书(拆件见
253
+ // write-on-board-flow.js)。返回 null = 用不上(守卫不过/一块就装下),走正常路。
254
+ if (args.flow && args.text) {
255
+ const fr = await maybeFlowWrite({
256
+ projectId, sharedRoot, sessionId, by, ctx, args, body, wUnits, zone,
257
+ slotInfo, parentId, replyRect, anchorId, b2, obstaclesOf,
258
+ placeInSlot, placeOnSheets, describeSheetFull, stamp,
259
+ });
260
+ if (fr) return fr;
261
+ }
262
+
263
+
264
+ // 卡高上限(08-29 刀 E):没规划版面时也不许写出一根柱子。执行点在工具层
265
+ // 不在渲染层 —— 折叠/裁切都是替它把问题藏起来,它下一条还会照写。
266
+ if (!slotInfo && box.h > CARD_MAX_H) {
267
+ const c = capacityOf(box.w, CARD_MAX_H);
268
+ return err([
269
+ `⛔ Too long for one card: this needs ${box.h}px, a card holds ${CARD_MAX_H}px (~${c.lines} lines / ~${c.cjk} CJK chars at ${box.w}px wide).`,
270
+ ' Nothing was written. Split it YOUR way: carve a few blocks (open_sheet{plan} / edit_board',
271
+ ' replan — omit at to stack them) and fill one note each, or chain:true a few short notes.',
272
+ ' Lazy fallback: flow:true lets the machine split at paragraph breaks.',
273
+ ].join('\n'));
274
+ }
275
+ let placed;
276
+ if (slotInfo) {
277
+ // 装不下就拒收:什么都不写,把还剩多少报回去让它分块重排(站主拍板)
278
+ const p = placeInSlot(b2, { rect: slotInfo.rect, sheet: slotInfo.sheet, slotName: args.slot, box, obstacles });
279
+ if (p.full) return err(p.message);
280
+ placed = p;
281
+ } else if (zone) {
282
+ if (args.at) return err('at 是纸内坐标,文件夹层没有纸 —— 在文件夹里用 reply_to/near 落位。');
283
+ placed = placeInZone({ box, replyRect, anchorRect, side: args.side || null, obstacles });
284
+ } else if (laneFrom) {
285
+ // 一条线 = 它自己的一叠纸:铺一张以 tag 命名的纸,这条落在纸头
286
+ const opened = await openSheetFor(projectId, {
287
+ sessionId, by, title: args.tag,
288
+ name: TAG_RE.test(args.tag) && !board.sheets?.[args.tag] ? args.tag : null,
289
+ });
290
+ const inner = innerRect(opened);
291
+ placed = { x: inner.x, y: inner.y, resolution: 'lane-open', sheetId: opened.id, opened, pressed: [] };
292
+ b2 = { ...b2, sheets: { ...(b2.sheets || {}), [opened.id]: { x: opened.x, y: opened.y, w: opened.w, h: opened.h, at: opened.at, by } } };
106
293
  } else {
107
- const obstacles = obstaclesOf('');
108
- let bottom = 0; for (const o of obstacles) bottom = Math.max(bottom, o.y + o.h);
109
- for (const zz of Object.values(board.zones || {})) if (Number.isFinite(zz?.y)) bottom = Math.max(bottom, zz.y + 240);
110
- const vp = getViewpoint(projectId);
111
- const vpRect = (vp && !(vp.layer) && vp.camera) ? vp.camera : null;
112
- const spot = findSpot({ w: box.w + 24, h: box.h + 24, obstacles, contentBottom: bottom, viewport: vpRect });
113
- pos = { x: spot.x + 12, y: spot.y + 12 };
294
+ placed = await placeOnSheets(b2, {
295
+ box, at: args.at || null, sheetName: args.sheet || null,
296
+ replyRect, anchorRect, side: args.side || null, obstacles,
297
+ });
298
+ // 纸排满了:不替它翻页,让它自己规划下一页(刀 F,站主"每张纸规划一次")
299
+ if (placed.sheetFull) return err(describeSheetFull(b2, placed.sheetFull));
300
+ if (placed.opened) b2 = { ...b2, sheets: { ...(b2.sheets || {}), [placed.opened.id]: { x: placed.opened.x, y: placed.opened.y, w: placed.opened.w, h: placed.opened.h, at: placed.opened.at, by } } };
114
301
  }
115
302
 
303
+ // ── 手写字本体(ink:'hand'):画布原生 text 节点,不落文件 ──
304
+ if (args.ink === 'hand') {
305
+ const hid = `text:a${stamp()}`;
306
+ const data = {
307
+ t: body, ...(looksLikeMd(body) ? { format: 'md' } : {}),
308
+ font: TEXT_FONTS.includes(args.font) ? args.font : 'kai',
309
+ size: args.size || 'md', ...(COLORS.includes(args.color) ? { color: args.color } : {}),
310
+ };
311
+ const hObjects = { [hid]: {
312
+ x: Math.round(placed.x), y: Math.round(placed.y), z: 1, w: box.w, h: box.h,
313
+ kind: 'text', data, zone, by, seat: 'agent', ...(args.tag ? { tag: args.tag } : {}),
314
+ } };
315
+ const hBindings = {};
316
+ if (anchorId) {
317
+ const type = args.relation || 'annotates';
318
+ const [from, to] = type === 'flow' ? [anchorId, hid] : [hid, anchorId];
319
+ hBindings[`b:a${stamp()}`] = { type, from, to, by, ...(args.tag ? { tag: args.tag } : {}) };
320
+ }
321
+ await patchBoard(projectId, { objects: hObjects, bindings: hBindings });
322
+ const hRect = { x: Math.round(placed.x), y: Math.round(placed.y), w: box.w, h: box.h };
323
+ try {
324
+ ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: '写了一段手写字' });
325
+ ctx?.emit?.(Events.boardFocus(hRect, { tag: args.tag || null, layer: zone, soft: true, actor: by !== 'agent' ? by : null }));
326
+ } catch { /* fail-soft */ }
327
+ return { content: [{ type: 'text', text:
328
+ `Wrote handwritten note ${hid} at (${hRect.x},${hRect.y}) ${hRect.w}x${hRect.h} — ${describeSpot(b2, placed)}.` }] };
329
+ }
330
+
331
+ // 状态表堵写口(2026-08-30):这 tag 载重(set_vars/触发器/趋势线都读它),
332
+ // 第一版就得是能解析的表 —— 坏表落盘比不落更坏。
333
+ if (args.tag === STATE_TABLE_TAG) {
334
+ const t = parseStateTable(String(body).replace(/\r\n?/g, '\n'));
335
+ if (!t.ok) {
336
+ return err(`⛔ tag「${STATE_TABLE_TAG}」是载重的,但这份正文里解析不出状态表(${t.error === 'no-table' ? '找不到「| 键 | 值 |」表' : t.error})—— 什么都没写。正文放一张两列表(| 键 | 值 | / | --- | --- | / 一行一键)再来。`);
337
+ }
338
+ }
116
339
  const fileName = chalkFileName(body);
117
- const content = renderChalk({ body, by: 'agent', anchor: anchorId, replyTo: parentId, tag: tag || null, sessionId: sessionId || null });
340
+ const content = renderChalk({ body, by, anchor: anchorId, replyTo: parentId, tag: args.tag || null, sessionId: sessionId || null });
118
341
  const rel = await writeChalkFile(sharedRoot, fileName, content);
119
342
 
120
- const objects = { [rel]: { x: Math.round(pos.x), y: Math.round(pos.y), z: 1, w: box.w, h: box.h, zone, by: 'agent', ...(tag ? { tag } : {}) } };
343
+ const objects = { [rel]: {
344
+ x: Math.round(placed.x), y: Math.round(placed.y), z: 1, w: box.w, h: box.h,
345
+ zone, by, seat: 'agent', ...(args.tag ? { tag: args.tag } : {}),
346
+ } };
121
347
  const bindings = {};
122
- if (anchorId) bindings[`b:a${stamp()}`] = { type: 'annotates', from: rel, to: anchorId, by: 'agent', ...(tag ? { tag } : {}) };
123
- if (parentId) bindings[`b:a${stamp()}`] = { type: 'flow', from: parentId, to: rel, by: 'agent', material: 'pencil', ...(tag ? { tag } : {}) };
124
- await patchBoard(projectId, { objects, bindings });
125
- turnStamp.count += 1;
126
- const rect = { x: Math.round(pos.x), y: Math.round(pos.y), w: box.w, h: box.h };
348
+ if (anchorId) {
349
+ const type = args.relation || (laneFrom ? 'flow' : 'annotates');
350
+ // flow 是读序(旧 新):锚在前板书在后;其余语义都是"这条说的是它"
351
+ const [from, to] = type === 'flow' ? [anchorId, rel] : [rel, anchorId];
352
+ bindings[`b:a${stamp()}`] = { type, from, to, by, ...(args.tag ? { tag: args.tag } : {}) };
353
+ }
354
+ if (parentId) bindings[`b:a${stamp()}`] = { type: 'flow', from: parentId, to: rel, by, material: 'pencil', ...(args.tag ? { tag: args.tag } : {}) };
355
+ await patchBoard(projectId, {
356
+ objects, bindings,
357
+ // 线注册表照旧登记(read_board 的线清单/角色专线都靠它):登记点 = 这条线的纸
358
+ ...(laneFrom ? { lanes: { [args.tag]: {
359
+ x: Math.round(placed.x), y: Math.round(placed.y), w: box.w,
360
+ ...(laneFrom !== 'fresh' && laneFrom?.id ? { parent: laneFrom.id } : {}),
361
+ } } } : {}),
362
+ });
363
+ // 跟随线:这个 tag 有人跟着(状态板之类)就自动重锚挪组(fail-soft)
364
+ if (args.tag) { try { await applyFollows(projectId, { tag: args.tag, newId: rel }); } catch { /* */ } }
365
+
366
+ const rect = { x: Math.round(placed.x), y: Math.round(placed.y), w: box.w, h: box.h };
127
367
  try {
128
368
  ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: parentId ? '回了一条板书' : '写了一条板书' });
129
- // chalk 字段进精灵追踪链(08-24):形状钉在 Events.boardFocus,别再内联对象
130
- ctx?.emit?.(Events.boardFocus(rect, { tag: tag || null, layer: zone, soft: true, chalk: rel }));
131
- } catch { /* */ }
132
- const big = box.h > SKETCH_FIT.h * 0.6;
133
- return { content: [{ type: 'text', text:
134
- `Wrote board note ${rel} at (${rect.x},${rect.y}) ${rect.w}x${rect.h}`
135
- + (anchorId ? ` beside ${anchorId} (annotates line)` : parentId ? ` under ${parentId} (thread)` : ' in the user\'s view')
136
- + `.${big ? ' ⚠ It is tall — next time split or shorten.' : ''} Mention it in one line in chat; the user can annotate it to reply.` }] };
137
- },
138
- );
369
+ ctx?.emit?.(Events.boardFocus(rect, { tag: args.tag || null, layer: zone, soft: true, chalk: rel, actor: by !== 'agent' ? by : null }));
370
+ } catch { /* fail-soft */ }
371
+ const lines = [
372
+ `Wrote board note ${rel} at (${rect.x},${rect.y}) ${rect.w}x${rect.h} ${describeSpot(b2, placed)}.`,
373
+ `Visible in the user's viewport: ${visibleIn(rect, vpRect) ? 'yes' : (vpRect ? 'no (outside their view — mention where it is)' : 'unknown (no viewpoint yet)')}.`,
374
+ ];
375
+ // 余量随手报(2026-08-30 容量线):下一条要不要拆、还塞不塞得下,
376
+ // 最有用的时机就是刚写完这一刻 —— 别让它再去翻状态块或者赌一发。
377
+ if (slotInfo) {
378
+ const freeH = Math.max(0, Math.round(slotInfo.rect.y + slotInfo.rect.h - (rect.y + rect.h) - UNIT));
379
+ const c = capacityOf(slotInfo.rect.w, freeH);
380
+ // 版位满 ≠ 纸满(2026-08-30 利用率线):旧措辞 "goes elsewhere" 把模型往门外
381
+ // 推(sonnet 真会话走成每拍一张新纸)。版位用完先报纸还剩多少,复用是主路径。
382
+ let tail = '';
383
+ if (freeH < 60) {
384
+ const inn = innerRect(slotInfo.sheet);
385
+ const sheetFree = Math.round(inn.y + inn.h - (rect.y + rect.h) - UNIT);
386
+ tail = sheetFree >= 120 ? ` — this slot is full, but the sheet still has ~${sheetFree}px below: keep writing on it (no slot needed — notes flow down; or replan more blocks). A fresh sheet is for a scene change or a refusal, not for every note` : ' — slot and sheet are both nearly full; plan the next page (open_sheet) before the next note';
387
+ }
388
+ lines.push(`Slot "${args.slot}" now has ~${c.lines} lines (~${c.cjk} CJK chars, ${freeH}px) left${tail}.`);
389
+ }
390
+ if (box.reserved) lines.push(`Box height reserved at ${box.h}px (content measured shorter — the box keeps your planned size).`);
391
+ // 折叠如实报(08-29 占位契约刀 B):卡高封顶到 CARD_MAX_H,超出的折在卡里。
392
+ // ⚠ 旧判据 `box.h > SKETCH_FIT.h*0.6`(720px)封顶之后永远不成立 —— 换成真话。
393
+ if (box.capped) {
394
+ lines.push(`⚠ Too long for one card: it shows ${box.h}px of ~${box.fullH}px — the rest is folded (the reader must click to unfold). Split it into several notes (chain:true keeps them threaded), or start a fresh sheet.`);
395
+ }
396
+ // 收卷提醒(2026-08-27 收纳器):落进收着的组 = 用户看不见这条新话
397
+ {
398
+ const rolledInto = [args.tag, board.objects?.[parentId]?.tag, board.objects?.[anchorId]?.tag]
399
+ .find(t => t && b2.rolls?.[t]);
400
+ if (rolledInto) lines.push(`⚠ #${rolledInto} 这条线收着卷(用户看不见里面)——这条也进了卷。要让用户看见,先 edit_board unroll{tag:"${rolledInto}"}。`);
401
+ }
402
+ if (laneFrom) {
403
+ lines.push(`Opened lane #${args.tag}${laneFrom !== 'fresh' ? ` branching from ${laneFrom.id}` : ''} on its own sheet`
404
+ + ` — continue it with {tag:"${args.tag}", chain:true}; read_board lists lanes and sheets.`);
405
+ }
406
+ lines.push('The user can annotate it to reply; answer with reply_to (or chain:true on the same tag).');
407
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
408
+ }
409
+
410
+ // ───────────────────────── 件数 ≥ 2:一张图(画布原生) ─────────────────────────
411
+ // 单个形状(比如一个圈、一条下划线)是"记号":画布原生,但不强打 tag、不进草稿态
412
+ const soloMark = !args.title && !nodesIn.length && shapesIn.length === 1;
413
+ const tag = args.tag || (soloMark ? null : `sk-${stamp()}`);
414
+ const staging = args.staging !== false && !soloMark;
415
+
416
+ const localIds = new Set();
417
+ const nodes = [];
418
+ if (args.title) {
419
+ nodes.push({ key: '__title', text: `## ${args.title}`, format: 'md', size: 'md', font: 'kai', color: 'ink', at: null, w: null });
420
+ }
421
+ for (const n of nodesIn) {
422
+ if (localIds.has(n.id)) return err(`节点 id 重复:${n.id}`);
423
+ localIds.add(n.id);
424
+ const size = (n.size === 'sm' && n.text.length > 40) ? 'md' : (n.size || 'md');
425
+ // 缺省 format 按内容侦测:带 markdown 记号标 plain 会把 **加粗** 吐成星号(ldx 案)
426
+ const format = n.format || (looksLikeMd(n.text) ? 'md' : 'plain');
427
+ nodes.push({ key: n.id, text: n.text, format, size, font: n.font || 'pen', color: n.color || 'ink', at: n.at || null, w: n.w || null });
428
+ }
429
+ for (const n of nodes) {
430
+ const box = textBox(n.text, n.size, { md: n.format === 'md', wUnits: capW(n.w) });
431
+ n.w = box.w; n.h = box.h;
432
+ }
433
+ const titleNode = nodes.find(n => n.key === '__title');
434
+ // 图内边(两端都是本图节点):布局的结构输入 + 零线大图的提醒判据
435
+ const nodeKeys = new Set(nodes.map(n => n.key));
436
+ const innerEdges = edgesIn.filter(e => nodeKeys.has(e.from) && nodeKeys.has(e.to) && e.from !== e.to);
437
+ const tpl = resolveTemplate(nodes.filter(n => n !== titleNode), { template: args.layout || 'auto', edges: innerEdges, column: fit.column });
438
+ // 节点级拉力(08-27 产物锚 v2):节点 ↔ 板上已有产物的边,给布局一个方向 ——
439
+ // 连着谁就排向谁那一侧(flow 层内排序 / mindmap 环位都吃它)
440
+ const pull = new Map();
441
+ for (const e of edgesIn) {
442
+ for (const [self, other] of [[e.from, e.to], [e.to, e.from]]) {
443
+ if (!nodeKeys.has(self) || nodeKeys.has(other)) continue;
444
+ const cid = normalizeCanvasId(other);
445
+ const ext = cid ? board.objects?.[cid] : null;
446
+ if (!ext || !Number.isFinite(ext.x)) continue;
447
+ const s = estimateSizeOn(board, cid, ext);
448
+ const c = { x: ext.x + s.w / 2, y: ext.y + s.h / 2 };
449
+ const cur = pull.get(self);
450
+ pull.set(self, cur ? { x: (cur.x + c.x) / 2, y: (cur.y + c.y) / 2 } : c);
451
+ }
452
+ }
453
+ if (tpl === 'free') {
454
+ // free 的合同:每个节点都要 at。缺 at 静默排成一列是 ldx 那晚两次重画的病根 —— 明拒,报名单。
455
+ const missing = nodes.filter(n => n !== titleNode && !n.at).map(n => n.key);
456
+ if (missing.length) {
457
+ return err(`layout free 要求每个节点都带 at(网格坐标),缺:${missing.join(', ')}。给它们补 at,或者去掉 layout:'free' 用模板排。`);
458
+ }
459
+ }
460
+ const layoutInput = titleNode && tpl === 'mindmap' ? nodes.filter(n => n !== titleNode) : nodes;
461
+ let pos = layoutNodes(layoutInput, { template: tpl, cols: args.cols, edges: innerEdges, pull });
462
+ const seatTitle = () => {
463
+ if (titleNode && !pos.has('__title')) {
464
+ const bb = bboxOrZero([...pos.entries()].map(([k, p]) => ({ x: p.x, y: p.y, ...nodes.find(n => n.key === k) })));
465
+ pos.set('__title', { x: bb.x, y: bb.y - titleNode.h - 12 });
466
+ } else if (titleNode && tpl === 'free' && !titleNode.at) {
467
+ const bb = bboxOrZero([...pos.entries()].filter(([k]) => k !== '__title').map(([k, p]) => ({ x: p.x, y: p.y, ...nodes.find(n => n.key === k) })));
468
+ pos.set('__title', { x: bb.x, y: bb.y - titleNode.h - 12 });
469
+ }
470
+ };
471
+ seatTitle();
472
+ const rectOfNode = (key) => { const n = nodes.find(x => x.key === key); const p = pos.get(key); return n && p ? { x: p.x, y: p.y, w: n.w, h: n.h } : null; };
473
+
474
+ // ── 形状(局部像素):构建本体在 lib/sketch-shapes.js(08-27 棘轮拆件) ──
475
+ const built = buildSketchShapes(shapesIn, { rectOfNode, isTaken: (id) => localIds.has(id), tag });
476
+ if (built.error) return err(built.error);
477
+ const shapes = built.shapes;
478
+ // ── 线:端点校验跟 edit_board add_edge 同一道闸(endpointReal) ──
479
+ const idOf = new Map();
480
+ for (const n of nodes) idOf.set(n.key, `text:a${stamp()}`);
481
+ for (const sh of shapes) idOf.set(sh.key, `scribble:a${stamp()}`);
482
+ const resolveEnd = async (raw) => {
483
+ if (idOf.has(raw)) return idOf.get(raw);
484
+ const cid = normalizeCanvasId(raw);
485
+ if (!cid) return null;
486
+ return (await endpointReal(cid, board.objects || {}, board.zones, sharedRoot)) ? cid : null;
487
+ };
488
+ const bindings = {};
489
+ const badEdges = [];
490
+ for (const e of edgesIn) {
491
+ const from = await resolveEnd(e.from); const to = await resolveEnd(e.to);
492
+ if (!from || !to || from === to) { badEdges.push(`${e.from}→${e.to}`); continue; }
493
+ bindings[`b:a${stamp()}`] = {
494
+ type: e.type || 'link', from, to, by, material: e.material || 'pencil',
495
+ ...(tag ? { tag } : {}), ...(staging ? { staging: true } : {}), ...(e.label ? { label: e.label } : {}),
496
+ };
497
+ }
498
+
499
+ // ── 宏观落位:纸(sheet 局部 at / 顺排 / near+side 贴放) ──
500
+ const local = bboxOrZero([
501
+ ...nodes.map(n => ({ ...pos.get(n.key), w: n.w, h: n.h })),
502
+ ...shapes.map(sh => sh.rect),
503
+ ]);
504
+ // 巨图不再硬拒(08-25 用户拍板移除上限):照落,返回里强提醒拆分
505
+ const oversized = local.w > SKETCH_MAX.w || local.h > SKETCH_MAX.h;
506
+ let zone = '';
507
+ let anchorRect = null;
508
+ let sketchBase = board;
509
+ if (args.near) {
510
+ const a = await resolveAnchor(args.near, board);
511
+ if (!a && !args.at) return err(`锚点 ${args.near} 不在板上:既没有座位、不是任何 tag,磁盘上也没有这个文件(read_board 看一眼现在都有谁)。`);
512
+ if (a) {
513
+ zone = a.zone;
514
+ if (a.board) sketchBase = a.board;
515
+ const e = sketchBase.objects[a.anchorId];
516
+ anchorRect = { x: a.rect.x, y: a.rect.y, ...estimateSizeOn(sketchBase, a.anchorId, e) };
517
+ if (a.rect.w > anchorRect.w) anchorRect = a.rect; // tag 包络比单卡大就用包络
518
+ }
519
+ }
520
+ const obstacles = obstaclesOf(sketchBase, zone);
521
+ const vpRect = vpRectFor(zone);
522
+ const sketchBox = { w: local.w + 24, h: local.h + 24 };
523
+ let placed;
524
+ if (zone) {
525
+ if (args.at) return err('at 是纸内坐标,文件夹层没有纸 —— 在文件夹里用 near 落位。');
526
+ placed = placeInZone({ box: sketchBox, replyRect: null, anchorRect, side: args.side || null, obstacles });
527
+ } else if (args.slot) {
528
+ // 草图也能进规划好的块(2026-08-29 刀 F 补)。⛔ 之前 slot 只有板书那条路认,
529
+ // 而状态板/对照表这类东西正是走草图写的 —— 真会话 proj_mtfhey1x 里 agent
530
+ // 规划了 aside 却一个字没进去,它想用也用不了,只能拿 at 手摆到别处。
531
+ const si = resolveSlot(sketchBase, { slotName: args.slot, sheetName: args.sheet || null });
532
+ if (si.error) return err(si.message);
533
+ const p = placeInSlot(sketchBase, { rect: si.rect, sheet: si.sheet, slotName: args.slot, box: sketchBox, obstacles });
534
+ if (p.full) return err(p.message);
535
+ placed = p;
536
+ } else {
537
+ placed = await placeOnSheets(sketchBase, {
538
+ box: sketchBox, at: args.at || null, sheetName: args.sheet || null,
539
+ replyRect: null, anchorRect, side: args.side || null, obstacles,
540
+ });
541
+ if (placed.sheetFull) return err(describeSheetFull(sketchBase, placed.sheetFull));
542
+ if (placed.opened) sketchBase = { ...sketchBase, sheets: { ...(sketchBase.sheets || {}), [placed.opened.id]: { x: placed.opened.x, y: placed.opened.y, w: placed.opened.w, h: placed.opened.h, at: placed.opened.at, by } } };
543
+ }
544
+ const ox = placed.x - local.x + 12; const oy = placed.y - local.y + 12;
545
+ // mindmap 的方位重排要**真实图心**(落位前算不出,单锚时质心还会退化)——
546
+ // 环形 bbox 不随槽位变,落位定了再按世界方位二次布局,落位本身不漂
547
+ if (tpl === 'mindmap' && pull.size) {
548
+ pos = layoutNodes(layoutInput, { template: 'mindmap', cols: args.cols, edges: innerEdges, pull, pullOrigin: { x: ox, y: oy } });
549
+ seatTitle();
550
+ }
551
+
552
+ // ── 落盘 ──
553
+ const objects = {};
554
+ const common = { z: 1, zone, by, seat: 'agent', ...(tag ? { tag } : {}), ...(staging ? { staging: true } : {}) };
555
+ for (const n of nodes) {
556
+ const p = pos.get(n.key);
557
+ objects[idOf.get(n.key)] = {
558
+ x: Math.round(p.x + ox), y: Math.round(p.y + oy), w: n.w, h: n.h, kind: 'text',
559
+ data: { t: n.text, ...(n.format === 'md' ? { format: 'md' } : {}), font: TEXT_FONTS.includes(n.font) ? n.font : 'pen', size: n.size, color: n.color, lid: n.key },
560
+ ...common,
561
+ };
562
+ }
563
+ for (const sh of shapes) {
564
+ objects[idOf.get(sh.key)] = { x: Math.round(sh.rect.x + ox), y: Math.round(sh.rect.y + oy), w: Math.round(sh.rect.w), h: Math.round(sh.rect.h), kind: 'scribble', data: { d: sh.d, color: sh.color, width: sh.width }, ...(sh.hug && idOf.get(sh.hug) ? { hug: idOf.get(sh.hug) } : {}), ...common };
565
+ }
566
+ const saved = await patchBoard(projectId, { objects, bindings });
567
+ const landed = Object.keys(objects).filter(id => saved.objects?.[id]).length;
568
+ if (!landed) return err('草图被 board 拒了(内容或字段不合法)。');
569
+ if (tag && nodes.length) { try { await applyFollows(projectId, { tag, newId: idOf.get(nodes[0].key) }); } catch { /* */ } }
570
+ const world = { x: Math.round(local.x + ox), y: Math.round(local.y + oy), w: Math.round(local.w), h: Math.round(local.h) };
571
+ try {
572
+ ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: tag ? `画了一张草图 #${tag}` : '画了一个记号' });
573
+ ctx?.emit?.(Events.boardFocus(world, { tag: tag || null, layer: zone, actor: by !== 'agent' ? by : null }));
574
+ } catch { /* fail-soft */ }
575
+ const lines = [
576
+ `Sketch${tag ? ` #${tag}` : ''} landed${staging ? ' as STAGING (半透明)' : ''}: ${nodes.length} nodes, ${shapes.length} shapes, ${Object.keys(bindings).length} lines; layout ${tpl}; at world (${world.x},${world.y}) ${world.w}x${world.h} — ${describeSpot(sketchBase, placed)}.`,
577
+ `ids: ${[...idOf].map(([k, v]) => `${k}=${v}`).join(', ')}`,
578
+ `Visible in the user's viewport: ${visibleIn(world, vpRect) ? 'yes' : (vpRect ? 'no (outside their view — mention where it is)' : 'unknown (no viewpoint yet)')}.`,
579
+ ];
580
+ if (oversized) lines.push(`⚠ 这张图 ${Math.round(local.w)}x${Math.round(local.h)} 世界像素,远超一张纸(建议 ≤${SKETCH_MAX.w}x${SKETCH_MAX.h})——用户要拖着镜头看。如果你是按**像素**想的坐标:nodes/shapes(含 path 的 d)全族单位是 24px 的格,数值除以 24 重画一版会正好;确实要这么大就拆成几张 tag 图用线连。`);
581
+ // 零线大图提醒(08-27 用户报「草草一堆文字摊在那儿」):软提醒不硬拒 ——
582
+ // 但要说清楚这不是风格问题,是版面语言缺了一半
583
+ if (nodesIn.length >= 3 && !innerEdges.length) {
584
+ lines.push(`⚠ ${nodesIn.length} 件 0 线 —— 这是摊了一堆字,不是一张图。线是版面的语言:`
585
+ + `补 edges(谁连谁、什么关系,布局会按结构分层摆);这些如果本是一条思路,`
586
+ + `改走 {tag, chain:true} 让它长成线。`);
587
+ }
588
+ if (badEdges.length) lines.push(`Skipped ${badEdges.length} edge(s) with unknown endpoints: ${badEdges.slice(0, 6).join(', ')}`);
589
+ // 触屏档宽是硬约束(横向滑动没人受得了),所以话要说在宽上
590
+ if (world.w > fit.w || world.h > fit.h) lines.push(fit.column
591
+ ? `⚠ Too big for a ${fit.lane} screen (${fit.screen.w}x${fit.screen.h}px). Keep each sketch ≤${fit.w} wide — anything wider means sideways scrolling. Stack the next one below, don't put it to the side.`
592
+ : `⚠ Bigger than one sheet${fit.screen ? ` (user's screen ${fit.screen.w}x${fit.screen.h}px → ${fit.w}x${fit.h} world px fits)` : ` (${fit.w}x${fit.h} fits)`} — split into two tagged sketches next time.`);
593
+ if (vp?.zoom && vp.zoom < 0.75) lines.push(`User's zoom is ${vp.zoom} (<0.75): keep nodes md/lg and say in one line that there is a sketch on the board.`);
594
+ lines.push(staging
595
+ ? `Next: look_at_board {tag:"${tag}"} to check it, then edit_board {ops:[{op:"commit",tag:"${tag}"}]} (or it commits at turn end).`
596
+ : `Next: look_at_board ${tag ? `{tag:"${tag}"}` : '{around: one of the ids}'} to check it.`);
597
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
598
+ };
139
599
  }