@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,242 @@
1
+ /**
2
+ * mcp/tools/open-sheet.js —— open_sheet 铺纸(2026-08-29 纸范式刀 2)
3
+ *
4
+ * 开工先铺纸:在画布上登记一块 0.75 倍缩放下等于用户一屏的矩形工作区。
5
+ * 铺在哪由机器定(第一张对准用户此刻的视口 —— 「agent 在用户眼皮底下开工」;
6
+ * 之后缺省铺在当前纸正下方,像往桌上接着铺稿纸)。铺完这张纸就是**当前纸**:
7
+ * write_on_board 的 at 坐标以它的版心左上角为原点,没给 at 的写入在纸内自动
8
+ * 往下排,写满自动翻下一张。
9
+ *
10
+ * 纸是分配纪律不是容器,**也不渲染**(用户拍板:纸只是位置范围的概念)——用户
11
+ * 看到的只有内容本身;纸的存在体现在落位秩序、翻纸导航和 read_board 的账本里。
12
+ * 用户可以把东西拖出纸外,组照旧由 tag/线派生。
13
+ */
14
+
15
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
16
+ import { z } from 'zod';
17
+ import { byOf } from '../actor.js';
18
+ import { readBoard, patchBoard } from '../../../projects/board-store.js';
19
+ import { TAG_RE } from '../../../projects/board-sanitize.js';
20
+ import { obstaclesIn } from '../../../lib/board-obstacles.js';
21
+ import { fitFor, capacityOf, DEFAULT_CHALK_W } from '../../../lib/sketch-layout.js';
22
+ import { CARD_MAX_H } from '../../../lib/screen.js';
23
+ import { getViewpoint } from '../../../projects/viewpoint-store.js';
24
+ import {
25
+ sheetSizeFor, allocateSheetRect, nextSheetName, currentSheet, innerRect, SHEET_MARGIN,
26
+ membersInRect, sheetMembers,
27
+ } from '../../../lib/board-sheets.js';
28
+ import { setCurrentSheetId, currentSheetIdOf } from '../../../lib/sheet-state.js';
29
+ import { Events } from '../../agent/events.js';
30
+
31
+ /** 版位的 at:两轴各自可省 —— 省掉的轴由竖排糖补(below/接上一块)。
32
+ * ⛔ 不复用 write_on_board 的 SHEET_PT:它双轴必填,`at:{x:640}, below:"s1"`
33
+ * 这种半坐标会被 zod 整调用拒掉。 */
34
+ const SLOT_AT = z.object({
35
+ x: z.number().min(0).max(12000).optional(),
36
+ y: z.number().min(0).max(12000).optional(),
37
+ });
38
+
39
+ /** 一块地(版位)。坐标/尺寸全是纸内局部像素 —— 跟 write_on_board 的 at 同一套
40
+ *(export 给 edit_board 的 replan 用 —— 版位的合法性只有这一份定义) */
41
+ export const SLOT = z.object({
42
+ slot: z.string().regex(TAG_RE).describe('Name for this block, ASCII like main/aside/notes'),
43
+ at: SLOT_AT.optional().describe("Top-left of this block, pixels from the sheet's writable corner. OMIT it to stack this block right below the previous one in the list (or below the block named in `below`) — carve a column of slots without doing any y arithmetic. In a replan, omitting it on an EXISTING slot means resize in place (position kept)"),
44
+ below: z.string().regex(TAG_RE).optional().describe('Stack this block right under the named slot (24px gap). x follows that slot unless at.x says otherwise'),
45
+ w: z.number().min(48).max(12000).describe('Width in PIXELS (a default note column is 432)'),
46
+ h: z.number().min(24).max(12000).describe('Height in PIXELS (~26px per line of text)'),
47
+ about: z.string().max(60).optional().describe('What goes here (正文 / 人物小传 / 待办) — for your own map'),
48
+ for: z.literal('artifacts').optional()
49
+ .describe("Set to 'artifacts' on ONE block to make it this page's landing spot for generated files (images, docs, sites). Without it, files you produce have nowhere planned to go and the page fills up with them wherever there is room."),
50
+ });
51
+
52
+ /**
53
+ * 版面规划钳制(2026-08-29 刀 E;2026-08-30 抽出来给 replan 共用):
54
+ * 开工先把这一屏切成几块地。坐标跟 at 同一套(纸内局部像素)。
55
+ * 越出版心的钳回来 —— 钳过如实报(规划错了要当场知道)。
56
+ *
57
+ * 竖排糖(2026-08-30 用户拍板「agent 自己定几个空位分段填」):省掉 at(或点名
58
+ * `below`)=接在上一块正下方 24px。**y 累加这道算术归机器**,几个空位怎么切、
59
+ * 每段写什么归 agent —— 分段的语义权不外包,只有几何外包。
60
+ * `prevSlots` 是 replan 时该纸已有的版位(below 可以引用它们;点名已有版位又
61
+ * 没给坐标=原地改尺寸,不走竖排糖)。
62
+ */
63
+ export function clampPlan(plan, inner0, prevSlots = {}) {
64
+ const slots = {};
65
+ const clampedSlots = [];
66
+ let prev = null; // 数组序上一块(本批的)
67
+ const refOf = (name) => slots[name] || prevSlots[name] || null;
68
+ for (const it of Array.isArray(plan) ? plan : []) {
69
+ const w = Math.min(Math.round(it.w), inner0.w);
70
+ const h = Math.min(Math.round(it.h), inner0.h);
71
+ // 点名**已有**版位又没给坐标 = 原地改尺寸(2026-08-30):replan 想把 state 加高、
72
+ // 位置没提,此前走竖排糖落到 (0,0) 跟别的版位叠上 —— glm 真会话为这一下花了
73
+ // 三分钟理版面。竖排糖只对新版位生效。
74
+ const keep = !it.at && !it.below ? prevSlots[it.slot] : null;
75
+ const base = keep ? null : (it.below ? refOf(it.below) : (it.at ? null : prev));
76
+ const wantX = it.at?.x ?? keep?.x ?? base?.x ?? 0;
77
+ const wantY = it.at?.y ?? keep?.y ?? (base ? base.y + base.h + 24 : 0);
78
+ const x = Math.min(Math.max(0, Math.round(wantX)), Math.max(0, inner0.w - w));
79
+ const y = Math.min(Math.max(0, Math.round(wantY)), Math.max(0, inner0.h - h));
80
+ if (x !== Math.round(wantX) || y !== Math.round(wantY)
81
+ || w !== Math.round(it.w) || h !== Math.round(it.h)) clampedSlots.push(it.slot);
82
+ slots[it.slot] = { x, y, w, h, ...(it.about ? { about: it.about } : {}), ...(it.for === 'artifacts' ? { for: 'artifacts' } : {}) };
83
+ prev = slots[it.slot];
84
+ }
85
+ return { slots, clampedSlots };
86
+ }
87
+
88
+ /**
89
+ * 铺一张纸并登记(write_on_board 自动铺纸也走这一份 —— 分配纪律只有一份)。
90
+ * @returns {{ id, x, y, w, h, innerW, innerH, basis, overlapsLoose }}
91
+ */
92
+ export async function openSheetFor(projectId, {
93
+ sessionId = null, by = 'agent', title = null, name = null, where = null, plan = null,
94
+ } = {}) {
95
+ const board = await readBoard(projectId);
96
+ const vp = getViewpoint(projectId);
97
+ const fit = fitFor(vp);
98
+ const size = sheetSizeFor(fit);
99
+ let cur = currentSheet(board, currentSheetIdOf(sessionId));
100
+ // 缺省:还没有纸(或点名 viewport)→ 对准用户视口;有当前纸 → 铺在它正下方
101
+ const mode = where || (cur ? 'next' : 'viewport');
102
+ // 翻页裁纸(刀② 2026-08-30):纸固定一屏高、下一张贴满高矩形正下方,上一张
103
+ // 只用了一半时内容之间就是几百像素的空白(proj_mtfpehm3 实测 268~557px,
104
+ // 设计间隔明明只有 48)。翻页那一刻把上一张裁到内容底 —— 纸是分配纪律不是
105
+ // 画出来的框,裁掉的只是"再也不会写进去的余白";它的版位一并钳到新底。
106
+ let trimmed = null;
107
+ if (mode === 'next' && cur) {
108
+ const members = sheetMembers(board, cur.id);
109
+ const innerC = innerRect(cur);
110
+ const bottom = members.length ? Math.max(...members.map((m) => m.y + m.h)) : innerC.y;
111
+ const newH = Math.max(240, Math.ceil((bottom + SHEET_MARGIN - cur.y) / 24) * 24);
112
+ if (newH < cur.h - 24) {
113
+ const oldEntry = board.sheets[cur.id];
114
+ const newSlots = {};
115
+ for (const [nm, sl] of Object.entries(oldEntry.slots || {})) {
116
+ newSlots[nm] = { ...sl, h: Math.max(24, Math.min(sl.h, newH - SHEET_MARGIN * 2 - sl.y)) };
117
+ }
118
+ const patched = { ...oldEntry, h: newH, ...(Object.keys(newSlots).length ? { slots: newSlots } : {}) };
119
+ await patchBoard(projectId, { sheets: { [cur.id]: patched } });
120
+ trimmed = { id: cur.id, from: cur.h, to: newH };
121
+ board.sheets = { ...board.sheets, [cur.id]: patched };
122
+ cur = { ...cur, h: newH };
123
+ }
124
+ }
125
+ // 铺纸尽量不压散件。⛔ 不含文件夹/卷卡这类常驻家具:纸不渲染,纸矩形盖住文件夹
126
+ // 用户什么也看不见,而纸内落位照样会避开它 —— 算进来只会把第一张纸推离用户视口。
127
+ const obstacles = obstaclesIn(board, '', { furniture: false });
128
+ const rect = allocateSheetRect({
129
+ board, size,
130
+ viewport: mode === 'viewport' && vp?.camera && !vp.layer ? vp.camera : null,
131
+ nearSheet: mode === 'next' && cur ? cur.id : null,
132
+ obstacles,
133
+ });
134
+ const id = (name && TAG_RE.test(name) && !board.sheets?.[name]) ? name : nextSheetName(board);
135
+ const inner0 = innerRect({ x: rect.x, y: rect.y, w: rect.w, h: rect.h });
136
+ const { slots, clampedSlots } = clampPlan(plan, inner0);
137
+ const entry = {
138
+ x: rect.x, y: rect.y, w: rect.w, h: rect.h,
139
+ by, at: new Date().toISOString(), ...(title ? { title } : {}),
140
+ ...(Object.keys(slots).length ? { slots } : {}),
141
+ };
142
+ await patchBoard(projectId, { sheets: { [id]: entry } });
143
+ setCurrentSheetId(sessionId, id);
144
+ const inner = innerRect({ ...entry });
145
+ // 占地者点名(刀③ 2026-08-30):铺纸不避家具(纸不渲染,压着文件夹用户什么也
146
+ // 看不见),但**必须说清这块地上已经住着谁** —— proj_mtfpehm3 首拍就是纸铺在
147
+ // 文件夹排上、agent 规划的版位下半截被占,连吃四发拒收却没人告诉它为什么。
148
+ const occupants = membersInRect({ ...board, sheets: board.sheets }, { x: rect.x, y: rect.y, w: rect.w, h: rect.h })
149
+ .map((m) => ({
150
+ id: m.id, folder: !!m.folder,
151
+ local: { x: m.x - inner.x, y: m.y - inner.y }, w: m.w, h: m.h,
152
+ slots: Object.entries(slots)
153
+ .filter(([, sl]) => m.x < inner.x + sl.x + sl.w && m.x + m.w > inner.x + sl.x
154
+ && m.y < inner.y + sl.y + sl.h && m.y + m.h > inner.y + sl.y)
155
+ .map(([nm]) => nm),
156
+ }));
157
+ return {
158
+ id, ...entry, innerW: inner.w, innerH: inner.h,
159
+ basis: rect.basis, overlapsLoose: rect.overlapsLoose, clampedSlots, occupants, trimmed,
160
+ };
161
+ }
162
+
163
+ const DESCRIPTION = `Lay a fresh SHEET on the board and make it the current one — do this before you
164
+ start writing (like turning to a clean page). A sheet is one screen of the user's
165
+ device at 75% zoom; the machine picks where it goes (the first sheet opens right
166
+ under the user's current view; later ones stack below the current sheet).
167
+ After this, write_on_board's at:{x,y} means PIXELS from this sheet's top-left
168
+ writable corner, and writes without at flow top-to-bottom on it; when a sheet
169
+ fills up a new one is opened for you automatically — call open_sheet yourself when
170
+ you START A NEW TOPIC/CHAPTER, so each sheet reads as one page about one thing.
171
+ The user can drag things off a sheet freely — the sheet constrains you, not them.`;
172
+
173
+ export function makeOpenSheetTool({ projectId, sessionId, ctx }) {
174
+ return tool('open_sheet', DESCRIPTION, {
175
+ title: z.string().max(60).optional().describe('What this sheet is about (for read_board / your own map, e.g. 第二章 — sheets are invisible to the user)'),
176
+ name: z.string().regex(TAG_RE).optional().describe('Sheet name to refer to it later (ASCII like act2; default auto p1/p2/…)'),
177
+ where: z.enum(['next', 'viewport']).optional()
178
+ .describe("next = below the current sheet (default when sheets exist); viewport = right where the user is looking now (default for the first sheet — also use it to bring work back to the user's eyes)"),
179
+ plan: z.array(SLOT).max(24).optional()
180
+ .describe('PLAN THE WHOLE PAGE HERE, before writing anything: carve the sheet into named blocks (slots) and say what goes in each. Then write_on_board{slot:"main"} drops content into that block. A sheet is WIDE (landscape) — a single column down the left wastes most of it; think in columns and rows.'),
181
+ }, async (args, extra) => {
182
+ if (!projectId) return { content: [{ type: 'text', text: 'No project bound.' }], isError: true };
183
+ const by = byOf(extra);
184
+ const s = await openSheetFor(projectId, {
185
+ sessionId, by, title: args.title || null, name: args.name || null, where: args.where || null,
186
+ plan: args.plan || null,
187
+ });
188
+ try {
189
+ ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: `铺了一张纸 ${s.id}` });
190
+ ctx?.emit?.(Events.boardFocus({ x: s.x, y: s.y, w: s.w, h: s.h }, { layer: '', soft: s.basis !== 'viewport', actor: by !== 'agent' ? by : null }));
191
+ } catch { /* fail-soft */ }
192
+ const colCap = capacityOf(DEFAULT_CHALK_W, s.innerH);
193
+ const cols = Math.max(1, Math.floor(s.innerW / (DEFAULT_CHALK_W + SHEET_MARGIN)));
194
+ const lines = [
195
+ `Sheet ${s.id} laid at world (${s.x},${s.y}) ${s.w}x${s.h}${s.title ? ` — “${s.title}”` : ''}; it is now the current sheet.`,
196
+ `Writable area: ${s.innerW}x${s.innerH}px, margin ${SHEET_MARGIN}. at:{x,y} means pixels from its top-left writable corner (x→right, y→down).`,
197
+ // 量纲(刀 D):agent 手里的东西是字,护栏却全是像素 —— 让它自己换算
198
+ // = 每次落笔前做一道做不准的算术,结果是写完才发现装不下。
199
+ `Scale: ~26px per line, ~${colCap.perLine} CJK chars per ${DEFAULT_CHALK_W}px-wide line. This sheet is ${colCap.lines} lines deep and ${cols} columns wide (≈${colCap.cjk * cols} CJK chars if you use all of it).`,
200
+ ];
201
+ const slotList = Object.entries(s.slots || {});
202
+ if (slotList.length) {
203
+ // 覆盖率(刀 E):一张横着的纸只画一条竖栏,大半是空的 —— 把这个数直接报出来
204
+ const used = slotList.reduce((n, [, v]) => n + v.w * v.h, 0);
205
+ const pct = Math.round((used / (s.innerW * s.innerH)) * 100);
206
+ lines.push(`Planned ${slotList.length} slots covering ${pct}% of the sheet:`);
207
+ for (const [nm, v] of slotList) {
208
+ const c = capacityOf(v.w, v.h);
209
+ lines.push(` ${nm}${v.about ? `(${v.about})` : ''}: at (${v.x},${v.y}) ${v.w}x${v.h} — ~${c.lines} lines, ~${c.cjk} CJK chars`);
210
+ }
211
+ lines.push('write_on_board{slot:"<name>"} drops a note into a block (they stack downward inside it). Content that does not fit is REFUSED, not squeezed — re-plan or split it.');
212
+ if (pct < 45) lines.push(`⚠ ${100 - pct}% of this sheet is unplanned. A landscape sheet holds ${cols} columns side by side — carve more blocks rather than leaving it empty.`);
213
+ if (s.clampedSlots?.length) lines.push(`⚠ Clamped into the sheet (they stuck out): ${s.clampedSlots.join(', ')} — check their at/w/h.`);
214
+ } else {
215
+ lines.push(`No slots planned. Plan the page in one go — open_sheet{plan:[{slot,at,w,h,about}…]} — instead of writing one note at a time and hoping it lands well. This sheet takes ${cols} columns side by side; a single column down the left leaves ${Math.round((1 - 1 / cols) * 100)}% of it empty.`);
216
+ }
217
+ lines.push(s.basis === 'viewport' ? 'Opened under the user’s current view.' : (s.basis === 'below-sheet' ? 'Stacked below the current sheet.' : 'Placed below existing content.'));
218
+ if (s.trimmed) {
219
+ lines.push(`Previous sheet ${s.trimmed.id} was trimmed to its content (${s.trimmed.from}→${s.trimmed.to}px tall) so the pages sit close — its leftover blank is gone, not its content.`);
220
+ // 利用率点名(2026-08-30):裁掉近半张纸 = 翻页翻快了。只点名不拦 ——
221
+ // 换场景翻新纸永远合法,这里治的是「每拍一张纸」的过度避险。
222
+ const waste = s.trimmed.from - s.trimmed.to;
223
+ if (waste >= 400) {
224
+ lines.push(`⚠ You left ~${waste}px of that sheet unused — pages are turning faster than they fill. Short beats can keep landing on the current sheet (they flow down; no slot needed). Turn the page for a scene change or a refusal, not for every beat.`);
225
+ }
226
+ }
227
+ // 占地者点名(刀③):谁在纸上、占了哪块版位、该怎么办 —— 三样一次说清
228
+ if (s.occupants?.length) {
229
+ const named = s.occupants.slice(0, 6).map((o) =>
230
+ `${o.folder ? '📁 ' : ''}${o.id} at local (${Math.round(o.local.x)},${Math.round(o.local.y)}) ${o.w}x${o.h}${o.slots.length ? ` — sits in your slot "${o.slots.join('", "')}"` : ''}`);
231
+ lines.push(`⚠ ${s.occupants.length} item(s) ALREADY LIVE on this sheet's ground (they count against your space):`);
232
+ for (const n of named) lines.push(` ${n}`);
233
+ if (s.occupants.length > 6) lines.push(` …and ${s.occupants.length - 6} more (read_board lists them).`);
234
+ lines.push(' Options: plan/re-plan your blocks AROUND them (edit_board replan), move them somewhere'
235
+ + ' deliberate (edit_board move — they may be the user\'s, so keep them visible), or re-open with'
236
+ + ' where:"next" for clean ground. Do NOT just write over them.');
237
+ } else if (s.overlapsLoose) {
238
+ lines.push('⚠ Some loose items already sit in this area — they now read as “on this sheet”; move them (edit_board) if that is wrong.');
239
+ }
240
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
241
+ });
242
+ }
@@ -9,13 +9,15 @@
9
9
  * 实现 = **和用户拖拽「移动到…」同一份核心**(projects/move-entry.js):
10
10
  * 磁盘先行、画布身份同步、转发表记账,一个字不重写。
11
11
  *
12
- * 批量制(同 roll_film / paint_still):≤16 件、串行、**中途失败即停**
13
- * (后面的不动,报告哪件停的)。目标夹不存在就建 —— 归纳常配新夹。
12
+ * 批量制(同 roll_film / paint_still):≤16 件、串行。**重名跳过、其余即停**
13
+ * (停了就报哪件停的,后面的不动)。目标夹不存在就建 —— 归纳常配新夹。
14
14
  */
15
15
 
16
16
  import { tool } from '@anthropic-ai/claude-agent-sdk';
17
17
  import { z } from 'zod';
18
18
  import { moveEntry, MoveError } from '../../../projects/move-entry.js';
19
+ import { rewriteWorkspaceRefs } from '../../../lib/rewrite-refs.js';
20
+ import { getSharedDir } from '../../../projects/workspace.js';
19
21
  import { Events } from '../../agent/events.js';
20
22
 
21
23
  export function makeOrganizeBoardTool({ projectId, ctx }) {
@@ -26,20 +28,29 @@ export function makeOrganizeBoardTool({ projectId, ctx }) {
26
28
  Use for: grouping generated images into a folder, collecting a site's materials into <site>/assets/, un-cluttering the desktop root. Sticky notes (notes/*.md) may move too, but they become plain .md file cards outside notes/ — lose the flippable sticky form.
27
29
  Not for: site roots as destination (they are artifacts, not storage — a site takes materials in its assets/ subfolder).
28
30
 
29
- Batch: up to 16 items, moved in order, stops at first failure.`,
31
+ After moving, references across the workspace (src/href in html/css/md pointing
32
+ at the moved items) are rewritten to the new paths automatically — the count of
33
+ rewritten spots is reported per file so you can verify. Pass rewrite_refs:false
34
+ to move only.
35
+
36
+ Batch: up to 16 items, moved in order. A name clash at the destination is skipped (that one is likely already filed) and the rest still move; any other failure stops the batch.`,
30
37
  {
31
38
  items: z.array(z.string().min(1)).min(1).max(16)
32
39
  .describe('Workspace-relative paths to move (files or folders), e.g. ["assets/generated/a.png", "旧稿.html"]'),
33
40
  into: z.string()
34
41
  .describe('Destination folder (workspace-relative), e.g. "素材" or "观察日志/assets". Created if missing. "" = workspace root (un-nest).'),
42
+ rewrite_refs: z.boolean().optional()
43
+ .describe('Also rewrite references to the moved items across workspace text files (default true).'),
35
44
  },
36
- async ({ items, into }) => {
45
+ async ({ items, into, rewrite_refs: rewriteRefs }) => {
37
46
  const lines = [];
38
- let moved = 0;
47
+ const moves = [];
48
+ let moved = 0; let skipped = 0;
39
49
  for (const item of items) {
40
50
  try {
41
51
  const out = await moveEntry(projectId, item, into, { createFolder: true });
42
52
  moved += 1;
53
+ if (out.moved) moves.push({ from: out.from, to: out.to });
43
54
  lines.push(out.moved ? `✓ ${out.from} → ${out.to}` : `· ${out.from}(已在原地)`);
44
55
  if (out.moved) {
45
56
  try {
@@ -50,11 +61,39 @@ Batch: up to 16 items, moved in order, stops at first failure.`,
50
61
  }
51
62
  } catch (err) {
52
63
  const why = err instanceof MoveError ? err.message : (err?.message || String(err));
64
+ /**
65
+ * 重名(MoveError 409)**不中断整批**:目标夹里已经有个同名的,说明这件
66
+ * 大概率已经收过一遍了,它挡不住后面十几件。其余错误照旧即停。
67
+ *
68
+ * 真案 proj_mtg61or1 19:37,用户说"把素材都收到一块,别四处丢",agent
69
+ * 一次收 12 件,第 1 件重名 → 后面 11 件全没动,暂存架自始至终没清过。
70
+ * 收纳这个动词本来就不是事务性的(前面搬成的不会回滚),中途停下只是
71
+ * 让人分不清搬到哪儿了。
72
+ */
73
+ if (err instanceof MoveError && err.status === 409) {
74
+ skipped += 1;
75
+ lines.push(`· ${item}:${why}(跳过,接着搬后面的)`);
76
+ continue;
77
+ }
53
78
  lines.push(`✗ ${item}:${why}`);
54
- lines.push(`(后面 ${items.length - moved - 1} 件没动 —— 修正后重调)`);
79
+ lines.push(`(后面 ${items.length - moved - skipped - 1} 件没动 —— 修正后重调)`);
55
80
  break;
56
81
  }
57
82
  }
83
+ // 「新建文件夹然后改个索引」的后半件(iss_mt38uih6):把全工作区指向
84
+ // 被搬条目的引用改到新路径。改了多少处逐文件上报 —— 自动改写用户内容
85
+ // 必须可核对;报 0 也说一声,agent 不用再自己 grep。
86
+ if (moves.length > 0 && rewriteRefs !== false) {
87
+ try {
88
+ const rw = await rewriteWorkspaceRefs(getSharedDir(projectId), moves);
89
+ lines.push(rw.hits > 0
90
+ ? `引用改写:${rw.hits} 处 / ${rw.files} 个文件`
91
+ : '引用改写:全工作区没有指向这些条目的文本引用(0 处)');
92
+ lines.push(...rw.lines.slice(0, 12));
93
+ } catch (err) {
94
+ lines.push(`⚠️ 引用改写失败(文件已搬成,引用要自己 grep 修):${err?.message || err}`);
95
+ }
96
+ }
58
97
  try {
59
98
  if (moved > 0) ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: `归纳了 ${moved} 件到 ${into || '桌面根'}` });
60
99
  } catch { /* fail-soft */ }
@@ -398,7 +398,7 @@ breakage; do not burn high-detail on every frame.`,
398
398
  negative: z.string().optional().describe('overrides the per-model default; no-op for krea2'),
399
399
  size: z.string().regex(/^\d{3,4}x\d{3,4}$/).default('1344x768'),
400
400
  seed: z.number().int().optional().describe('omit for fresh random per still; batch uses seed, seed+1, ...'),
401
- name: z.string().regex(/^[\w-]{1,40}$/).default('still'),
401
+ name: z.string().regex(/^[\w一-鿿぀-ヿ-]{1,40}$/).default('still'),
402
402
  batch: z.number().int().min(1).max(8).optional()
403
403
  .describe('variations of this same prompt in one pass (default 1). Use 4-8 to roll for a keeper.'),
404
404
  lora: z.string().optional()
@@ -8,7 +8,6 @@
8
8
  * - 普通文件(图片 / 便签 / 数据):路径本身,`assets/generated/a.webp`
9
9
  * - deck:`deck:<路径>`,如 `deck:稿件/主稿.html`
10
10
  * - 站点:`site:<目录>`
11
- * - 项目文档:`doc:_root`(记忆)/ `doc:brand`(品牌档案)
12
11
  *
13
12
  * ## 2026-08-13:这个工具的职权范围缩小了
14
13
  *
@@ -30,16 +29,14 @@ import { tool } from '@anthropic-ai/claude-agent-sdk';
30
29
  import { z } from 'zod';
31
30
  import path from 'path';
32
31
  import { promises as fs } from 'fs';
33
- import { pinToZone } from '../../../projects/board-store.js';
32
+ import { pinToZone, readBoard, patchBoard } from '../../../projects/board-store.js';
33
+ import { layerOf } from '../../../lib/canvas-id.js';
34
+ import { currentSheet, slotRectOf, nextSpotInSlot, placeAtOnSheet } from '../../../lib/board-sheets.js';
35
+ import { estimateSizeOn } from '../../../lib/board-kind-sizes.js';
36
+ import { currentSheetIdOf } from '../../../lib/sheet-state.js';
34
37
 
35
- /** 物件 id 它住在哪个文件夹(与前端 stage.js zoneOfObjectId 同一套规则) */
36
- function folderOfObjectId(objectId) {
37
- if (objectId.startsWith('doc:')) return '';
38
- const c = objectId.indexOf(':');
39
- const p = (c > 0 && /^[a-z]+$/.test(objectId.slice(0, c))) ? objectId.slice(c + 1) : objectId;
40
- const i = p.lastIndexOf('/');
41
- return i > 0 ? p.slice(0, i) : '';
42
- }
38
+ // (folderOfObjectId 08-24 拆除:它按 dirname 硬算,会给 assets/generated 这类
39
+ // 前端不当层渲染的路径发"层"标签 —— 改问 layerOf + board.zones,跟渲染同口径)
43
40
 
44
41
  /**
45
42
  * @param {object} deps
@@ -47,7 +44,7 @@ function folderOfObjectId(objectId) {
47
44
  * @param {string} [deps.projectId]
48
45
  * @param {import('../../agent/context.js').AgentContext} [deps.ctx]
49
46
  */
50
- export function makePinToBoardTool({ sharedRoot, projectId, ctx }) {
47
+ export function makePinToBoardTool({ sharedRoot, projectId, sessionId = null, ctx }) {
51
48
  return tool(
52
49
  'pin_to_board',
53
50
  `Bring an item to the front of the user's canvas, at a free spot in whatever
@@ -55,7 +52,10 @@ folder it lives in. The canvas is their desktop: whatever you write appears
55
52
  there automatically — you do NOT need this tool for your own outputs.
56
53
  Use it only to deliberately surface something:
57
54
 
58
- - Pull a reference (an uploaded asset, the brand doc, an older image) into view
55
+ - Place a staged arrival: new files land on the staging shelf (the vertical
56
+ strip of unplaced cards the status block names) — pin_to_board{path, slot}
57
+ moves one into a block you planned
58
+ - Pull a reference (an uploaded asset, a memory note, an older image) into view
59
59
  - Restore something the user dragged off-screen, when they ask for it back
60
60
 
61
61
  This does NOT change which folder the item belongs to — that is decided by
@@ -64,16 +64,21 @@ where the file is on disk. To move it, \`mv\` the file; the canvas follows.
64
64
  Paths are workspace-relative, exactly as they are on disk. Accepted forms:
65
65
  - any file path: 'assets/generated/hero.webp', 'notes/灵感.md', '稿件/数据.csv'
66
66
  - a deck: 'deck:<path>.html' a site: 'site:<dir>'
67
- (a bare '<path>.html' is read as a deck)
68
- - '.claude/agent-memory/memory.md' (project memory) / '.../brand/memory.md' (brand doc)`,
67
+ (a bare '<path>.html' is read as a deck)`,
69
68
  {
70
69
  path: z
71
70
  .string()
72
71
  .min(1)
73
72
  .max(300)
74
73
  .describe('Item to surface — see accepted forms in the tool description'),
74
+ slot: z.string().max(40).optional()
75
+ .describe('Drop it into a planned block on the current sheet (names from open_sheet / read_board). This is how a produced file lands where you planned it instead of wherever there was room.'),
76
+ at: z.object({ x: z.number(), y: z.number() }).optional()
77
+ .describe('Exact spot on the sheet, pixels from its top-left writable corner. Use slot unless this one file needs a precise place.'),
78
+ sheet: z.string().max(40).optional()
79
+ .describe('Which sheet (default: the one you are working on)'),
75
80
  },
76
- async ({ path: rawPath }) => {
81
+ async ({ path: rawPath, slot, at, sheet }) => {
77
82
  try {
78
83
  if (!projectId) {
79
84
  return { content: [{ type: 'text', text: 'No project bound; cannot pin.' }], isError: true };
@@ -84,9 +89,7 @@ Paths are workspace-relative, exactly as they are on disk. Accepted forms:
84
89
  if (!objectId || objectId.includes('..')) {
85
90
  return { content: [{ type: 'text', text: 'Invalid path.' }], isError: true };
86
91
  }
87
- if (objectId.endsWith('agent-memory/brand/memory.md')) objectId = 'doc:brand';
88
- else if (objectId.endsWith('agent-memory/memory.md')) objectId = 'doc:_root';
89
- else if (!/^(deck|site|doc):/.test(objectId)) {
92
+ if (!/^(deck|site):/.test(objectId)) {
90
93
  // 裸路径:.html 是一份 deck,其余(图片 / 便签 / 数据文件)id 就是路径。
91
94
  // 站点的目录要显式写 `site:` —— 光看路径分不出
92
95
  // "一个收纳文件夹"和"一件目录型产物",猜错了钉上去的是个不存在的 id。
@@ -102,7 +105,88 @@ Paths are workspace-relative, exactly as they are on disk. Accepted forms:
102
105
  if (/\.html?$/i.test(objectId)) objectId = `deck:${objectId}`;
103
106
  }
104
107
 
105
- const zoneId = folderOfObjectId(objectId);
108
+ // 可见性预检(08-21 案:钉 assets/ 深处的图报成功,用户画布上根本没有)。
109
+ // 画布扫描只认 assets 顶层 / assets/generated / assets/notes 这三个口,
110
+ // 更深的(references/web/… 之类)有座位也不渲染 —— 钉了等于没钉。
111
+ if (/^assets\//.test(objectId)
112
+ && !/^assets\/[^/]+$/.test(objectId)
113
+ && !/^assets\/(generated|notes)\/[^/]+$/.test(objectId)) {
114
+ return {
115
+ content: [{
116
+ type: 'text',
117
+ text: `${objectId} 在画布扫描范围之外(assets/ 只有顶层、generated/、notes/ 三个口上墙),pin 了用户也看不到。`
118
+ + '先把文件 cp 到工作区根或某个文件夹里再 pin(用户要看的素材放看得见的地方)。',
119
+ }],
120
+ isError: true,
121
+ };
122
+ }
123
+ // 层归属按**真实存在的层**算('' 或 zones 里的文件夹)——跟前端渲染同口径。
124
+ // dirname 硬算的老写法会把 assets/generated 当层写进 zone,arrange 就此拒摆
125
+ const boardNow = await readBoard(projectId);
126
+ const zoneId = layerOf(objectId, null, new Set(Object.keys(boardNow.zones || {})));
127
+
128
+ /**
129
+ * 点名落位(2026-08-30 刀 G):`slot` / `at` 给了就走纸上落位,跟 write_on_board
130
+ * 同一套几何。这是「产物也由 agent 规划位置」的那只手 —— 暂存架上的东西,
131
+ * agent 规划出地方之后就用它请下来(seat 改写成 agent,自然离架)。
132
+ */
133
+ if (slot || at) {
134
+ const sh = (sheet && boardNow.sheets?.[sheet])
135
+ ? { id: sheet, ...boardNow.sheets[sheet] }
136
+ : currentSheet(boardNow, currentSheetIdOf(sessionId)); // 会话正写的那张优先
137
+ if (!sh) {
138
+ return { content: [{ type: 'text', text: 'No sheet yet — open_sheet first (plan the page, then place things into its blocks).' }], isError: true };
139
+ }
140
+ const box = estimateSizeOn(boardNow, objectId, boardNow.objects?.[objectId] || null);
141
+ let spot = null; let where = '';
142
+ if (slot) {
143
+ const rect = slotRectOf(sh, slot);
144
+ if (!rect) {
145
+ const names = Object.keys(sh.slots || {});
146
+ return { content: [{ type: 'text', text: names.length
147
+ ? `Sheet ${sh.id} has no slot "${slot}". It has: ${names.join(', ')}.`
148
+ : `Sheet ${sh.id} has no slots planned. Plan the page first: open_sheet{plan:[{slot,at,w,h,about}…]}.` }], isError: true };
149
+ }
150
+ const p = nextSpotInSlot(boardNow, rect, box);
151
+ if (p.full) {
152
+ return { content: [{ type: 'text', text: `⛔ Slot "${slot}" on sheet ${sh.id} is full — ${objectId} needs ${box.h}px, ~${p.freeH}px left. `
153
+ + 'Nothing moved. Re-plan the page with a taller block, or point it at another slot.' }], isError: true };
154
+ }
155
+ spot = p; where = ` in slot "${slot}"`;
156
+ } else {
157
+ const p = placeAtOnSheet(sh, at, box);
158
+ spot = p; where = p.clamped ? ' (clamped into the sheet)' : '';
159
+ }
160
+ /**
161
+ * ⛔ 摆到纸上 = 摆到**根层**,zone 必须写 ''(不是文件所在的文件夹层)。
162
+ *
163
+ * spot 是 nextSpotInSlot 在根层纸矩形里算出来的**世界坐标**;zone 写成
164
+ * 文件夹层的话,这两个字段当场互相矛盾:前端 dirOf 按 zone 把它渲染进
165
+ * 那个文件夹里(世界坐标在那儿是没意义的局部坐标),根层画布上根本
166
+ * 看不见它 —— 而工具还报了「Placed on sheet p3 in slot refs at (24,2941)」。
167
+ *
168
+ * 更实的后果:rootObjects 只收根层,membersInRect 跟着看不见它,于是
169
+ * **下一件 pin 进同一个版位的东西算出来还是同一个坐标**。真案
170
+ * proj_mtg61or1 19:18 连 pin 五张官方参考图,后四张全落 (24,3405),
171
+ * 工具四次都报 success。对照组(同样五张放根层)落点正常且装不下时
172
+ * 如实报「版位满」。
173
+ *
174
+ * 把文件夹里的文件 pin 到纸上,语义就是把它拎到桌面上摆着 —— 跟用户
175
+ * 从文件夹里拖一张卡出来是同一件事(前端注释:拖出来的写 '')。
176
+ */
177
+ const prev = boardNow.objects?.[objectId] || {};
178
+ const nextPending = (boardNow.pending || []).filter(r => r !== objectId && `deck:${r}` !== objectId && `site:${r}` !== objectId);
179
+ await patchBoard(projectId, {
180
+ objects: { [objectId]: { ...prev, x: Math.round(spot.x), y: Math.round(spot.y), w: Math.round(box.w), h: Math.round(box.h), zone: '', seat: 'agent' } },
181
+ ...(nextPending.length !== (boardNow.pending || []).length ? { pending: nextPending } : {}),
182
+ });
183
+ try {
184
+ ctx?.emit?.({ type: 'board.updated', sessionId: null, objectId, zoneId: '', summary: `已把 ${objectId} 摆到 ${sh.id}${where}` });
185
+ } catch { /* emit fail-safe */ }
186
+ return { content: [{ type: 'text', text: `Placed ${objectId} on sheet ${sh.id}${where} at (${Math.round(spot.x)}, ${Math.round(spot.y)}).`
187
+ + (nextPending.length !== (boardNow.pending || []).length ? ` It is no longer waiting for a spot (${nextPending.length} still are).` : '') }] };
188
+ }
189
+
106
190
  const { zone: placedZone, placed } = await pinToZone(projectId, { objectId, zoneId });
107
191
 
108
192
  try {