@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
@@ -95,7 +95,10 @@ export function clearActiveArtifact(sessionId) {
95
95
  }
96
96
 
97
97
  function normalizeRel(p) {
98
- return String(p).replace(/\\/g, '/').replace(/^\.\//, '');
98
+ // 尾部 ?query / #hash 一并剥掉:agent 常把预览 URL 原样拿来当 path
99
+ //('dist/index.html?t=18&panel=0' 落 not found,08-24 清账两案)。
100
+ // 磁盘文件名里合法出现 ? # 的场景在这套产物约定里不存在。
101
+ return String(p).replace(/\\/g, '/').replace(/^\.\//, '').replace(/[?#].*$/, '');
99
102
  }
100
103
 
101
104
  async function exists(p) {
@@ -108,13 +111,9 @@ function insideWorkspace(workspaceRoot, absPath) {
108
111
  return absPath === root || absPath.startsWith(root + path.sep);
109
112
  }
110
113
 
111
- /**
112
- * 站点的页面清单(相对产物根)。
113
- */
114
- export async function listSitePages(workspaceRoot) {
115
- const m = await KINDS[KIND_SITE].manifest(workspaceRoot, null);
116
- return m.pages;
117
- }
114
+ // (listSitePages 2026-08-24 拆除:调用 KINDS.site.manifest 这个不存在的方法,
115
+ // 一调必 TypeError,全仓无调用方 —— 死代码假装公开 API。页面清单读
116
+ // taskManifest().artifacts[].pages。)
118
117
 
119
118
  /**
120
119
  * 推断某个 html 路径的产物形态。
@@ -209,11 +208,28 @@ export async function resolveArtifactTarget(workspaceRoot, relPath, sessionId) {
209
208
  const hit = await tryPath(relPath);
210
209
  if (hit) { setActiveArtifact(sessionId, hit.relPath, hit.kind); return hit; }
211
210
  const abs = path.resolve(workspaceRoot, relPath);
211
+ if (!insideWorkspace(workspaceRoot, abs)) return { ok: false, message: 'path escapes workspace' };
212
+ // 回退:agent 常拿**产物根相对**或**源目录相对**路径来喂('index.html'、
213
+ // 'dist/index.html'),而这里收的是工作区相对('jet-engine/dist/index.html')。
214
+ // 没命中时按各站点实例的 root / srcRoot 补前缀再试一轮,省一次注定失败的
215
+ // 往返(08-24 案:三个工具三种写法全 not found)。
216
+ const m = await taskManifest(workspaceRoot);
217
+ const rel = normalizeRel(relPath);
218
+ const prefixes = new Set();
219
+ for (const a of (m?.artifacts || [])) {
220
+ if (a.kind !== KIND_SITE || a.single) continue;
221
+ if (a.root) prefixes.add(a.root);
222
+ if (a.srcRoot && a.srcRoot !== a.root) prefixes.add(a.srcRoot);
223
+ }
224
+ for (const pre of prefixes) {
225
+ const hit2 = await tryPath(`${pre}/${rel}`);
226
+ if (hit2) { setActiveArtifact(sessionId, hit2.relPath, hit2.kind); return hit2; }
227
+ }
228
+ const candidates = (m?.artifacts || []).map(a => a.entryRel).filter(Boolean);
212
229
  return {
213
230
  ok: false,
214
- message: insideWorkspace(workspaceRoot, abs)
215
- ? `${relPath} not found. Write it first, or check the path.`
216
- : 'path escapes workspace',
231
+ message: `${relPath} not found. Write it first, or check the path (workspace-relative).`
232
+ + (candidates.length ? `\nExisting artifact entries:\n${candidates.map(c => `- ${c}`).join('\n')}` : ''),
217
233
  };
218
234
  }
219
235
 
@@ -272,9 +288,10 @@ export function requireBrowsable(target) {
272
288
 
273
289
  /** 给各工具复用的 path 参数描述(保持措辞一致) */
274
290
  export const ARTIFACT_PATH_DESC =
275
- 'Relative path of the artifact entry (deck: "canvas.html", site: "index.html" '
276
- + 'or its built output like "dist/index.html", word: "文档.docx"). '
277
- + 'Omit to use the artifact you are currently working on.';
291
+ 'WORKSPACE-relative path of the artifact entry (deck: "canvas.html", '
292
+ + 'root site: "index.html" or "dist/index.html", site in a folder: full path '
293
+ + 'like "my-site/dist/index.html", word: "文档.docx"). All artifact tools share '
294
+ + 'this one convention. Omit to use the artifact you are currently working on.';
278
295
 
279
296
  // ── 兼容层:老名字继续可用,内部全部走上面的实现 ────────────────────────────
280
297
  export const CANVAS_PATH_DESC = ARTIFACT_PATH_DESC;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * server/lib/board-anchor.js —— 锚点解析(2026-08-27 行数棘轮拆件)
3
+ *
4
+ * 从 mcp/tools/write-on-board.js **原样**搬出:真 id > tag 包络 > **救援入座**
5
+ * (文件真在只是还没座位 —— 当场给它排一个再锚。入座下沉后防抖 1.5s 内的窗口、
6
+ * 以及历史欠座都从这里兜住,「还没有座位」这个失败类只剩"确实不存在"一种真情况)。
7
+ *
8
+ * readBoard / seatArtifacts 走注入,不在这里 import —— lib 不该反向抓 engine/runs
9
+ * 和 projects 层的东西;纯几何依赖(canvas-id / board-kind-sizes)留作直接 import。
10
+ */
11
+
12
+ import { layerOf, normalizeCanvasId, tagEnvelope } from './canvas-id.js';
13
+ import { estimateSizeOn } from './board-kind-sizes.js';
14
+
15
+ /**
16
+ * @param {object} deps
17
+ * projectId 救援入座要按项目发
18
+ * known Set(board.zones 的键) —— layerOf 的分层判据
19
+ * readBoard (pid) => board 救援后重读
20
+ * seatArtifacts (pid, [rel]) => {seated}
21
+ * @returns {(raw: string, b: object) => Promise<{anchorId,zone,rect,board,rescued?}|null>}
22
+ */
23
+ export function makeAnchorResolver({ projectId, known, readBoard, seatArtifacts }) {
24
+ const sizeOf = (b) => (id, e) => estimateSizeOn(b, id, e);
25
+ return async function resolveAnchor(raw, b) {
26
+ const nid = normalizeCanvasId(raw);
27
+ const e = nid ? b.objects?.[nid] : null;
28
+ if (e && Number.isFinite(e.x)) {
29
+ return { anchorId: nid, zone: layerOf(nid, e, known), rect: { x: e.x, y: e.y, ...estimateSizeOn(b, nid, e) }, board: b };
30
+ }
31
+ const env = tagEnvelope(b, raw, sizeOf(b));
32
+ if (env) {
33
+ return { anchorId: env.anchorId, zone: layerOf(env.anchorId, b.objects[env.anchorId], known), rect: { x: env.x, y: env.y, w: env.w, h: env.h }, board: b };
34
+ }
35
+ if (nid) {
36
+ const bare = nid.replace(/^(deck|site|docx|text|scribble):/, '');
37
+ const { seated } = await seatArtifacts(projectId, [bare]).catch(() => ({ seated: 0 }));
38
+ if (seated) {
39
+ const nb = await readBoard(projectId);
40
+ const ne = nb.objects?.[nid] || nb.objects?.[bare];
41
+ const realId = nb.objects?.[nid] ? nid : bare;
42
+ if (ne && Number.isFinite(ne.x)) {
43
+ return { anchorId: realId, zone: layerOf(realId, ne, known), rect: { x: ne.x, y: ne.y, ...estimateSizeOn(nb, realId, ne) }, board: nb, rescued: true };
44
+ }
45
+ }
46
+ }
47
+ return null;
48
+ };
49
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * server/lib/board-dirty.js —— 板上动静(2026-08-29 纸范式刀 4)
3
+ *
4
+ * 病根:用户拖拽落盘(HTTP PATCH /board)与文件搬家(POST /move)此前**完全静默**
5
+ * —— board.updated 七处 emit 全是 agent 侧动作,agent 对「用户刚动过什么」只能等
6
+ * 下一轮注入撞运气。这里立一份进程内的动静台账:
7
+ *
8
+ * 写方 api/board.js PATCH(位置真变了才记 —— 尺寸回写不算动静)、
9
+ * api/board.js erase、api/assets.js /move
10
+ * 读方 UserPromptSubmit「板上动静」节(下轮必达)、
11
+ * PreToolUse 板工具注入器(agent 下一次摸板时插话,按会话恰好一次)
12
+ *
13
+ * HTTP 层就是用户(agent 侧写板走 board-store 直调不过这里),所以不用猜作者。
14
+ * 进程内存不落盘:动静是「此刻」,重启即作废。
15
+ */
16
+
17
+ const CAP = 60;
18
+ const TTL_MS = 30 * 60 * 1000;
19
+ let seq = 0; // 单调序号(同毫秒两笔动静时间戳会撞车,序号不会)
20
+ const store = new Map(); // pid → [{seq, at, kind, id, to?}]
21
+ const markers = new Map(); // `${pid}:${sessionId}` → 上次已告知的序号
22
+
23
+ function prune(list, now) {
24
+ const keep = list.filter(e => now - e.at <= TTL_MS);
25
+ return keep.length > CAP ? keep.slice(keep.length - CAP) : keep;
26
+ }
27
+
28
+ /** 记一批动静。kind ∈ moved | removed | mv(文件搬家,带 to)| erased(整组擦) */
29
+ export function noteBoardDirty(pid, events) {
30
+ if (!pid || !events?.length) return;
31
+ const now = Date.now();
32
+ const list = store.get(pid) || [];
33
+ for (const e of events) {
34
+ if (!e || typeof e.id !== 'string') continue;
35
+ list.push({ seq: ++seq, at: now, kind: e.kind || 'moved', id: e.id.slice(0, 300), ...(e.to ? { to: String(e.to).slice(0, 300) } : {}) });
36
+ }
37
+ store.set(pid, prune(list, now));
38
+ }
39
+
40
+ /** 某序号之后的动静(新在后)。sinceSeq=0 = 全部未过期的 */
41
+ export function dirtyEvents(pid, sinceSeq = 0) {
42
+ const now = Date.now();
43
+ const list = prune(store.get(pid) || [], now);
44
+ store.set(pid, list);
45
+ return list.filter(e => e.seq > sinceSeq);
46
+ }
47
+
48
+ export function lastSeen(pid, sessionId) {
49
+ return markers.get(`${pid}:${sessionId}`) || 0;
50
+ }
51
+
52
+ export function markSeen(pid, sessionId, seenSeq = seq) {
53
+ if (pid && sessionId) markers.set(`${pid}:${sessionId}`, seenSeq);
54
+ }
55
+
56
+ /**
57
+ * 有限负责制(刀⑤ 2026-08-30,站主原话改的):agent 只对**自己专注的工作区**
58
+ * (当前纸的内部)的摆放负责 —— 挪进当前纸的东西点名出来、要它处理;挪出纸面
59
+ * 的是用户自留地,只报不催。板整个是「用户随便动、agent 别拔河」,这条只是把
60
+ * 「动到哪儿了」按纸分拣,agent 才知道哪些动静该接手。
61
+ *
62
+ * @param events dirtyEvents 的产物
63
+ * @param board 现在的板(位置以现状为准,不看动静发生时的旧坐标)
64
+ * @param sheetOf (id) => sheetId|null —— 由调用方绑好 board 的判定函数
65
+ * @param currentSheetId agent 此刻的当前纸
66
+ * @returns {{ inMine: string[], elsewhere: string[] }} 两拨 id
67
+ */
68
+ export function splitDirtyByCharge(events, { sheetOf, currentSheetId } = {}) {
69
+ const inMine = []; const elsewhere = [];
70
+ const seen = new Set();
71
+ for (const e of events) {
72
+ if (e.kind === 'erased' || seen.has(e.id)) continue;
73
+ seen.add(e.id);
74
+ const sid = sheetOf ? sheetOf(e.kind === 'mv' ? (e.to || e.id) : e.id) : null;
75
+ (currentSheetId && sid === currentSheetId ? inMine : elsewhere).push(e.id);
76
+ }
77
+ return { inMine, elsewhere };
78
+ }
79
+
80
+ /** 一批动静的人话(注入两处共用一份措辞) */
81
+ export function describeDirty(events, { limit = 6 } = {}) {
82
+ if (!events?.length) return null;
83
+ const verbs = { moved: '挪了', removed: '从板上移走了', mv: '把文件搬到', erased: '擦掉了整组' };
84
+ const lines = events.slice(-limit).map((e) => {
85
+ if (e.kind === 'mv') return `${verbs.mv} ${e.to}(原 ${e.id})`;
86
+ if (e.kind === 'erased') return `${verbs.erased} #${e.id}`;
87
+ return `${verbs[e.kind] || '动了'} ${e.id}`;
88
+ });
89
+ const more = events.length > limit ? `(还有 ${events.length - limit} 条更早的)` : '';
90
+ return `${lines.join(';')}${more}`;
91
+ }
92
+
93
+ export function _resetBoardDirty() { store.clear(); markers.clear(); }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * lib/board-excerpt.js —— 板书的一句话预览(首页卡片用)
3
+ *
4
+ * 为什么单开一个而不是并进封面管线(lib/cover.js):**板书不是产物**。
5
+ * 封面管线的输入是 taskManifest 里的 artifacts(deck / site / docx …),
6
+ * 拿 chromium 截一张印样贴到卡上。板书是写在画布上的字,没有入口文件、
7
+ * 没有画幅,截图这条路对它没有意义 —— 结果就是整个演出项目的卡常年一片空白
8
+ * 加一行"还没出东西",而里面躺着一整个故事。
9
+ *
10
+ * 它该有的样子不是"贴一张印样",是**这张纸上写着什么**:把最近一条板书的开头
11
+ * 直接写在卡的空白纸上。所以这里只出文本,不出图。
12
+ *
13
+ * 便宜是硬要求:首页 /projects/stats 一次要过一遍所有项目。所以
14
+ * - 按**文件名**倒序(板书文件名以 YYYYMMDD-HHMMSS 打头),不 stat 每个文件
15
+ * - 只读最新那一个文件的前几 KB
16
+ */
17
+
18
+ import path from 'node:path';
19
+ import fs from 'node:fs/promises';
20
+
21
+ /** 板书本体的落点(跟 write-on-board 里那份一致) */
22
+ export const CHALK_DIR = path.join('notes', '板书');
23
+
24
+ /** 读文件只取头一截:一条板书可能很长,预览只要开头几行 */
25
+ const HEAD_BYTES = 4096;
26
+
27
+ /**
28
+ * 去掉 frontmatter 和几个行首记号,压成能直接写在卡上的几行。
29
+ * 不做完整 markdown 渲染 —— 卡上那几行是"笔迹",不是排版好的正文。
30
+ */
31
+ export function excerptOf(raw, max) {
32
+ const body = String(raw || '')
33
+ .replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '')
34
+ // ⛔ 围栏块整段扔掉。板书里最常见的围栏是 ```nd:controls(画布上那排控件的
35
+ // 声明)—— 它在卡上会变成一坨源码,而它根本不是这段故事的字。
36
+ // 后半个 |``` 收尾未闭合的围栏:宁可少几行,也不能把源码倒到卡上。
37
+ .replace(/^[ \t]*```[\s\S]*?^[ \t]*```[ \t]*$|^[ \t]*```[\s\S]*$/gm, '');
38
+ const lines = body.split(/\r?\n/)
39
+ // 行首:标题 / 引用 / 列表点 / 任务框,都是排版记号不是字
40
+ .map((l) => l.replace(/^\s*(?:#{1,6}\s+|>\s+|[-*+]\s+)/, '').replace(/^\s*\[[ xX]\]\s*/, ''))
41
+ // 行内:强调和行内码只留字。卡上那几行是笔迹,星号和反引号原样显示就是噪音
42
+ .map((l) => l.replace(/\*\*([^*]+)\*\*/g, '$1').replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '$1')
43
+ .replace(/`([^`\n]+)`/g, '$1').trim())
44
+ .filter(Boolean);
45
+ if (!lines.length) return '';
46
+ let out = '';
47
+ for (const l of lines) {
48
+ if (out.length + l.length > max && out) break;
49
+ out += (out ? '\n' : '') + l;
50
+ if (out.length >= max) break;
51
+ }
52
+ return out.length > max ? `${out.slice(0, max)}…` : out;
53
+ }
54
+
55
+ /**
56
+ * @returns {Promise<null | { count: number, text: string }>} 没有板书就 null
57
+ */
58
+ export async function chalkPreview(sharedDir, { max = 150 } = {}) {
59
+ const dir = path.join(sharedDir, CHALK_DIR);
60
+ let names;
61
+ try {
62
+ names = (await fs.readdir(dir)).filter((n) => n.endsWith('.md'));
63
+ } catch { return null; } // 没这个目录 = 这个项目没写过板书
64
+ if (!names.length) return null;
65
+ // 文件名带时间戳前缀,按名倒序就是按时间倒序(省掉 N 次 stat)
66
+ names.sort().reverse();
67
+ let raw = '';
68
+ try {
69
+ const fh = await fs.open(path.join(dir, names[0]), 'r');
70
+ try {
71
+ const buf = Buffer.alloc(HEAD_BYTES);
72
+ const { bytesRead } = await fh.read(buf, 0, HEAD_BYTES, 0);
73
+ // ⚠️ 按字节截会把最后一个多字节字符劈成两半,转字符串时变成替换字符。
74
+ // 预览允许丢尾巴,但不允许出现乱码 —— 所以砍掉可能不完整的那一个。
75
+ raw = buf.subarray(0, bytesRead).toString('utf8').replace(/�+$/, '');
76
+ } finally { await fh.close(); }
77
+ } catch { return null; }
78
+ const text = excerptOf(raw, max);
79
+ return text ? { count: names.length, text } : null;
80
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * server/lib/board-follow.js —— 跟随线(2026-08-25 范式重做,第三件)
3
+ *
4
+ * 来历:08-25 黑板 RP 真会话里「状态板每章重新锚定到最新章节」是用户自己提出的
5
+ * 需求,agent 每章手工 move_group + remove/add 线两次、做了十几遍。这里把它做成
6
+ * 板上第一条**约束**:一条带 `follow: <目标tag>` 的线,to 端永远指向该 tag 最新
7
+ * 落板的那件 —— 新件一落,服务端自动重指线、并把 from 端所在的组挪到它旁边。
8
+ *
9
+ * 设计取舍:
10
+ * - 约束的家安在**线上**(不另立 rules 区):线本来就是「A 关于 B」的真相,
11
+ * follow 只是把 B 从一件东西升级成「一族东西的最新一件」。
12
+ * - 挪组连 seat:'user' 一起(08-28 全放开):平移保相对格局本来就是这条线先证明的;
13
+ * 首跟也不再把用户件留在原地(撕组比挪用户件更丑)。
14
+ * - fail-soft:跟随失败绝不连累落板本身。
15
+ */
16
+
17
+ import { readBoard, patchBoard } from '../projects/board-store.js';
18
+ import { estimateSizeOn } from './board-kind-sizes.js';
19
+ import { layerOf } from './canvas-id.js';
20
+ import { placeBeside } from './board-sheets.js';
21
+ import { UNIT } from './rect.js';
22
+
23
+ /**
24
+ * 某 tag 有新件落板:把所有 follow 这个 tag 的线重指到 newId,并挪各自的组。
25
+ * @returns {Promise<{followed: number}>}
26
+ */
27
+ export async function applyFollows(projectId, { tag, newId }) {
28
+ if (!tag || !newId) return { followed: 0 };
29
+ const board = await readBoard(projectId);
30
+ const followers = Object.entries(board.bindings || {})
31
+ .filter(([, b]) => b.follow === tag && b.to !== newId);
32
+ /**
33
+ * 挂账的跟随规则兑现(2026-08-30):`board.follows` 里目标是本 tag、但还没有线的,
34
+ * 现在这一刻目标第一次出现 —— 当场把线补出来,交给下面同一段几何走「首跟」。
35
+ *
36
+ * 规则和线分开存就是为了这一刻:立规则时目标可以是空的(skill 教的顺序如此),
37
+ * 线只在两端都真实存在时才有意义。
38
+ */
39
+ for (const [groupTag, rule] of Object.entries(board.follows || {})) {
40
+ if (rule?.target !== tag) continue;
41
+ const already = Object.entries(board.bindings || {})
42
+ .some(([, b]) => b.follow === tag && board.objects?.[b.from]?.tag === groupTag);
43
+ if (already) continue;
44
+ const member = Object.entries(board.objects || {})
45
+ .filter(([, e]) => e?.tag === groupTag && Number.isFinite(e?.x))
46
+ .sort((a, b) => a[1].y - b[1].y)[0];
47
+ if (!member || member[0] === newId) continue;
48
+ followers.push([`b:f${Date.now().toString(36)}${groupTag.length}`, {
49
+ type: 'annotates', from: member[0], to: '', by: 'agent',
50
+ label: rule.label || '跟随', follow: tag, ...(rule.side ? { followSide: rule.side } : {}),
51
+ }]);
52
+ }
53
+ if (!followers.length) return { followed: 0 };
54
+ const target = board.objects?.[newId];
55
+ if (!target || !Number.isFinite(target.x)) return { followed: 0 };
56
+ // 只认主控的新件(08-28 用户拍板):状态板这类跟随组挂在 GM 的叙事链上,
57
+ // 角色台词/用户落痕就算落进同一个 tag 也不把面板拽走。判据按板上作者,
58
+ // 不靠"角色自觉别打 tag"(08-27 真会话:GM 亲手让角色打 章节,面板跟着台词跑了)。
59
+ if ((target.by || 'agent') !== 'agent') return { followed: 0 };
60
+ const known = new Set(Object.keys(board.zones || {}));
61
+ const targetRect = { x: target.x, y: target.y, ...estimateSizeOn(board, newId, target) };
62
+ const targetZone = layerOf(newId, target, known);
63
+
64
+ const objects = {}; const bindings = {};
65
+ let followed = 0;
66
+ for (const [bid, b] of followers) {
67
+ // 新目标自己在跟随组里(状态板的卡也打了目标 tag 之类的配置错误):跳过防自噬
68
+ const fromEntry = board.objects?.[b.from];
69
+ const groupTag = fromEntry?.tag || null;
70
+ if (groupTag && board.objects?.[newId]?.tag === groupTag) continue;
71
+ const oldTarget = board.objects?.[b.to];
72
+ bindings[bid] = { ...b, to: newId };
73
+ followed += 1;
74
+
75
+ // 挪 from 端的组(同 tag 全员;没 tag 就只挪 from 自己)
76
+ const members = groupTag
77
+ ? Object.entries(board.objects).filter(([, e]) => e.tag === groupTag && Number.isFinite(e?.x))
78
+ : (fromEntry && Number.isFinite(fromEntry.x) ? [[b.from, fromEntry]] : []);
79
+ if (!members.length) continue;
80
+
81
+ // **平移跟随**(08-25 用户报「跟随坏了」后改):旧目标有座位时,整组按
82
+ // 新旧目标的位移平移 —— 全员照挪、seat:user 也挪。用户拖动 = 调整相对
83
+ // 格局(保留),不 = 取消跟随;旧行为(user 座罢工)在他把三张卡都拖过
84
+ // 之后让 follow 永久死掉。要真停用有 unfollow。
85
+ if (oldTarget && Number.isFinite(oldTarget.x)) {
86
+ const dx = Math.round(target.x - oldTarget.x);
87
+ const dy = Math.round(target.y - oldTarget.y);
88
+ if (dx || dy) for (const [id, e] of members) objects[id] = { ...e, x: e.x + dx, y: e.y + dy };
89
+ continue;
90
+ }
91
+
92
+ // 首跟(旧目标没座位可参照):解一个位置整组放过去(08-28 起含 user 座 —— 撕组更丑)
93
+ const movable = members;
94
+ if (!movable.length) continue;
95
+ const rects = members.map(([id, e]) => ({ id, x: e.x, y: e.y, ...estimateSizeOn(board, id, e) }));
96
+ const bb = {
97
+ x: Math.min(...rects.map(r => r.x)), y: Math.min(...rects.map(r => r.y)),
98
+ w: Math.max(...rects.map(r => r.x + r.w)) - Math.min(...rects.map(r => r.x)),
99
+ h: Math.max(...rects.map(r => r.y + r.h)) - Math.min(...rects.map(r => r.y)),
100
+ };
101
+ // 首跟 = 精确贴放(2026-08-29 纸范式:环搜退役)。跟随组的位置本来就该
102
+ // 紧贴目标的固定一侧 —— 压上什么由之后的平移跟随自然化解,不代找洞。
103
+ void targetZone; void known;
104
+ const p = placeBeside(targetRect, { w: bb.w, h: bb.h }, b.followSide || 'right', UNIT);
105
+ const dx = Math.round(p.x - bb.x); const dy = Math.round(p.y - bb.y);
106
+ if (!dx && !dy) continue;
107
+ for (const [id, e] of movable) objects[id] = { ...e, x: e.x + dx, y: e.y + dy };
108
+ }
109
+ if (followed || Object.keys(objects).length) await patchBoard(projectId, { objects, bindings });
110
+ return { followed };
111
+ }
@@ -121,11 +121,7 @@ export function asciiMinimap(rects, { cols = 48, rows = 16, viewport = null } =
121
121
  * 列(x 聚类)给出阅读顺序提示。
122
122
  * ──────────────────────────────────────────────────────────────────────── */
123
123
 
124
- export function bboxOfRects(rects) {
125
- let x0 = Infinity; let y0 = Infinity; let x1 = -Infinity; let y1 = -Infinity;
126
- for (const r of rects) { x0 = Math.min(x0, r.x); y0 = Math.min(y0, r.y); x1 = Math.max(x1, r.x + r.w); y1 = Math.max(y1, r.y + r.h); }
127
- return Number.isFinite(x0) ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : null;
128
- }
124
+ export { bboxOf as bboxOfRects } from './rect.js';
129
125
 
130
126
  /** b 相对 a 在哪(以 a 为参照):左/右/上/下 + 距离;重叠则说重叠 */
131
127
  export function relationOf(a, b) {
@@ -27,7 +27,9 @@ export function pickHero(items, bindings) {
27
27
  for (const b of Object.values(bindings || {})) {
28
28
  if (b.type === 'derives-from') { bump(b.from, 2); bump(b.to, -3); }
29
29
  if (b.type === 'ref' && b.by === 'auto') bump(b.to, -3);
30
- if (b.by === 'user' || b.by === 'agent') {
30
+ // 「手画的线」= 用户 / 主控 / 常驻角色画的(08-26:RP 场里大半线是角色画的,
31
+ // 只认 user|agent 的话主角推断的信号会大量流失)。auto 那支在上面已单独处理。
32
+ if (b.by && b.by !== 'auto') {
31
33
  for (const end of [b.from, b.to]) {
32
34
  if (!score.has(end)) continue;
33
35
  const used = focus.get(end) || 0;
@@ -19,13 +19,48 @@ export const ARTIFACT_PREVIEW_H = { deck: 360, site: 400, docx: 420, browse: 360
19
19
  // file 是 224x32 的细条卡(parity 测试上岗第一天就逮住我猜成 160x120 ——
20
20
  // 那是涂鸦的默认身位。别猜,抄表)
21
21
  export const KIND_SIZES = {
22
- doc: { w: 200, h: 96 },
23
22
  image: { w: 200, h: 176 },
24
23
  video: { w: 240, h: 160 },
25
24
  note: { w: 200, h: 148 },
26
25
  file: { w: 224, h: 32 },
27
26
  };
28
27
 
28
+ /**
29
+ * 文件夹卡的身位(2026-08-29 占位契约刀 A)。真身在前端
30
+ * `web/src/lib/board-geometry.js` 的 FOLDER_CARD —— zones 存档 08-13 瘦身后只剩
31
+ * 坐标,尺寸就是这个常量,两边靠 parity 测试钉着。
32
+ *
33
+ * ⛔ 它以前从不参与落位:三处障碍集合都只遍历 objects,文件夹对落位系统**结构性
34
+ * 隐形**(生产 128 块真板实测:文件夹被压 112 次 —— 被产物 34、被文件 33、
35
+ * 被文档 23、被板书 13)。现在它跟别的物件一样是障碍,见 board-obstacles.js。
36
+ */
37
+ export const FOLDER_CARD = { w: 288, h: 240 };
38
+
39
+ /**
40
+ * 文件夹卡矩形(id = 目录路径,跟 zones 的键一致)。
41
+ *
42
+ * ⚠️ 每张文件夹卡只住在**一层**:`世界书/常驻` 的卡摆在 `世界书` 那一层里,
43
+ * 它存的坐标是那一层的坐标——拿它当根层矩形用就是跨层幻影(2026-08-30
44
+ * proj_mtfpehm3 案:铺在根层的纸被别的层的卡「占满」,一连 6 发容量拒收里
45
+ * 4 发是假的)。所以取矩形必须点名要哪一层的;`layer: null` 是给「所有层
46
+ * 一起数」的调用方(目前只有对账/测试)留的口。
47
+ *
48
+ * @param {string|null} layer ''=根层(缺省);'世界书'=那一层里的子文件夹卡;null=全部
49
+ */
50
+ export function zoneRects(board, { layer = '' } = {}) {
51
+ const out = [];
52
+ for (const [path, z] of Object.entries(board?.zones || {})) {
53
+ if (!Number.isFinite(z?.x) || !Number.isFinite(z?.y)) continue;
54
+ if (layer !== null) {
55
+ const i = path.lastIndexOf('/');
56
+ const parent = i > 0 ? path.slice(0, i) : '';
57
+ if (parent !== layer) continue;
58
+ }
59
+ out.push({ id: path, x: z.x, y: z.y, ...FOLDER_CARD, folder: true });
60
+ }
61
+ return out;
62
+ }
63
+
29
64
  // 前缀表从注册表派生(「写死表家族」第 4 处,2026-08-18 收):手写
30
65
  // `deck|site|docx` 的话加形态必漏,新形态的卡掉到 file 兜底(224×32 细条),
31
66
  // agent 摆位按错矩形算。⚠️ 新形态要同步给 ARTIFACT_PREVIEW_H 加一行
@@ -44,7 +79,6 @@ export function estimateSize(id, entry) {
44
79
  return { w: entry.w, h: entry.h };
45
80
  }
46
81
  const s = String(id || '');
47
- if (s.startsWith('doc:')) return KIND_SIZES.doc;
48
82
  // 浏览器卡是**单例**,id 就是 'browse'(没有路径可挂 —— 它背后不是文件)。
49
83
  // 不给这条分支的话它会掉到最后的 file 兜底(224×32 的细条),agent 摆位时
50
84
  // 就会拿一个错的矩形去算"挨着谁摆、会不会压到谁"。
@@ -58,7 +92,8 @@ export function estimateSize(id, entry) {
58
92
  }
59
93
  if (IMG_EXT.test(s)) return KIND_SIZES.image;
60
94
  if (VIDEO_EXT.test(s)) return KIND_SIZES.video;
61
- if (/\.(md|txt)$/i.test(s)) return KIND_SIZES.note;
95
+ // 文本类文件卡带内容预览(08-24:md/json 等升级出预览体),身位=note
96
+ if (/\.(md|markdown|txt|json|csv|ya?ml)$/i.test(s)) return KIND_SIZES.note;
62
97
  return KIND_SIZES.file;
63
98
  }
64
99
 
@@ -0,0 +1,58 @@
1
+ /**
2
+ * server/lib/board-lanes.js —— 线(lane)的几何与符号地图(2026-08-27 空间规划第一刀)
3
+ *
4
+ * ## 设计总纲:模型声明拓扑,机器做几何
5
+ *
6
+ * 模型对坐标是瞎的(把摆放写成话术必然失效 —— 轮次机同一课),但它对**关系**很强。
7
+ * 所以「线」不是新概念:**线就是 tag** —— chain:true 早就是「接在同 tag 最新一条
8
+ * 下面」(continue 动词现成)。这里补的只有三件真正缺的:
9
+ *
10
+ * 1. **开列** —— 2026-08-29 纸范式后由「铺纸」承担(一条线 = 它自己的一叠纸,
11
+ * 见 board-sheets.js);这里的 allocateLaneColumn 扫空列已退役。
12
+ * 2. **注册表**:board.lanes = { tag: {x,y,w,parent} },持久在 board.json 里
13
+ * (版面地理要跟板走,不跟会话走)。frontier **不存** —— 从成员现算,存了就是
14
+ * 第二份真相源(feedback-single-source-of-truth)。
15
+ * 3. **符号地图**(laneSummaries):read_board 用它报「有哪些线、各几节、接着写
16
+ * 落哪」—— agent 的空间意识是读得起这张地图,不是看得见像素。
17
+ *
18
+ * 头脑风暴回路因此零空间推理:用户在旧节点标注提问 → agent 回答带
19
+ * open_lane:<那条的id> → 分支列长出来;后续 {tag, chain:true} 续。
20
+ * RP 场景位、跑团拍行是同一注册表的后续消费方(先不建,地基兼容)。
21
+ */
22
+
23
+ import { UNIT } from './rect.js';
24
+
25
+ /**
26
+ * 符号地图:板上现在有哪些线。已注册的报起点/节数/frontier/岔自谁;
27
+ * 没注册但成串的 tag(≥2 件)也报 —— 那是登记制之前长出来的「野线」,
28
+ * chain:true 照样能续,别让 agent 以为它们不存在。
29
+ */
30
+ export function laneSummaries(board) {
31
+ const members = new Map(); // tag → [{id, y, bottom}](有座位的才算)
32
+ for (const [id, e] of Object.entries(board?.objects || {})) {
33
+ if (!e?.tag || !Number.isFinite(e.y)) continue;
34
+ if (!members.has(e.tag)) members.set(e.tag, []);
35
+ members.get(e.tag).push({ id, y: e.y, bottom: e.y + (e.h || 5 * UNIT) });
36
+ }
37
+ const out = [];
38
+ const seen = new Set();
39
+ for (const [tag, l] of Object.entries(board?.lanes || {})) {
40
+ seen.add(tag);
41
+ const ms = (members.get(tag) || []).sort((a, b) => a.y - b.y);
42
+ const last = ms[ms.length - 1] || null;
43
+ out.push({
44
+ tag, registered: true, x: l.x, y: l.y, parent: l.parent || null,
45
+ count: ms.length, lastId: last?.id || null,
46
+ frontier: { x: l.x, y: last ? Math.round(last.bottom) + UNIT : l.y },
47
+ });
48
+ }
49
+ for (const [tag, ms0] of members) {
50
+ if (seen.has(tag) || ms0.length < 2) continue;
51
+ const ms = ms0.sort((a, b) => a.y - b.y);
52
+ out.push({
53
+ tag, registered: false, x: null, y: null, parent: null,
54
+ count: ms.length, lastId: ms[ms.length - 1].id, frontier: null,
55
+ });
56
+ }
57
+ return out;
58
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * server/lib/board-obstacles.js —— 一层桌面上「谁占着地方」的唯一真相源
3
+ * (2026-08-29 占位契约刀 A)
4
+ *
5
+ * ## 为什么要有这个文件
6
+ *
7
+ * 落位/压上判定原来在三个地方各写了一遍**一模一样**的障碍集合:
8
+ * write-on-board.js 的 obstaclesOf / edit-board.js 的 obstaclesNear / board-seater.js
9
+ * 三份都只遍历 `board.objects`,于是**不住在 objects 里的东西对落位系统结构性隐形**:
10
+ *
11
+ * - 文件夹卡(`board.zones`,只存 {x,y},尺寸是常量 FOLDER_CARD)
12
+ * - 卷卡(`board.rolls`,前端合成物,矩形由 rollCardRect 现算)
13
+ *
14
+ * 生产 128 块真板实测(根层、剔掉圈注类的合法压盖):**文件夹被压 112 次**
15
+ * (被产物 34 / 被文件 33 / 被文档 23 / 被板书 13 / 被文件夹 7 / 被文字 2)。
16
+ * 站主点名的「文件夹能被板书覆盖」就是这 13 次,而且它是 100% 必现不是概率问题 ——
17
+ * 工具连"压住了谁"都报不出来,因为那块地在它眼里根本是空的。
18
+ *
19
+ * 收成一份的另一半理由:加一类物件要改三处,改漏一处就是新的隐形物件。
20
+ * 现在加在这儿一次,三个调用点自动同步。
21
+ *
22
+ * ## 边界(写清楚,免得被读成"全量占位")
23
+ *
24
+ * 这套东西**只约束 agent 的自动落位**,不约束用户的手 —— 用户把卡拖到哪都合法,
25
+ * 拖出纸、拖成重叠都不会被纠正(纸范式纲领:纸是分配纪律不是本体容器)。
26
+ *
27
+ * 不进障碍集合的东西,都是真的不占板面,不是漏了:舞台贴/精灵(浮层,几秒即散,
28
+ * 且精灵自己会避让真卡)、聊天卡/小地图/工具栏(屏幕固定 UI,不在世界坐标系里)、
29
+ * 包络 chip(纯派生的一圈虚线)、连线(是线不是面)。
30
+ */
31
+
32
+ import { layerOf } from './canvas-id.js';
33
+ import { estimateSizeOn, zoneRects } from './board-kind-sizes.js';
34
+ import { inflateSpriteSeats, rollCardRect } from './board-sheets.js';
35
+
36
+ /**
37
+ * 一层的障碍集。
38
+ *
39
+ * @param {object} board 整块板(要读 zones/rolls/hero)
40
+ * @param {string} zone 层名;'' = 根层桌面(文件夹卡和卷卡只在这一层)
41
+ * @param {object} [opts]
42
+ * @param {object} [opts.objects] 用哪份 objects(缺省 board.objects;edit_board
43
+ * 要用带上本批改动的 live 副本,那份才是"这一刻"的板)
44
+ * @param {Set|string[]} [opts.exclude] 排除的 id(主角自己、同组成员)
45
+ * @param {boolean} [opts.furniture=true] 含不含"常驻家具"(文件夹卡/卷卡)。
46
+ * **落位要(true),铺纸不要(false)**:纸不渲染,一张纸的矩形盖在文件夹上
47
+ * 用户什么也看不见,而纸内落位本来就会避开文件夹;把家具算进铺纸避让,只换来
48
+ * 第一张纸被推离用户视口 —— "在用户眼皮底下开工"当场失效(写这条时刚被自己
49
+ * 的端到端测试逮住:纸从 (0,0) 滑到 y=384 绕开一个文件夹)。
50
+ * @returns {Array<{id,x,y,w,h}>}
51
+ */
52
+ export function obstaclesIn(board, zone = '', { objects = null, exclude = null, furniture = true } = {}) {
53
+ const objs = objects || board?.objects || {};
54
+ const known = new Set(Object.keys(board?.zones || {}));
55
+ const skip = exclude instanceof Set ? exclude : new Set(exclude || []);
56
+ const rects = [];
57
+ for (const [id, e] of Object.entries(objs)) {
58
+ if (skip.has(id) || !Number.isFinite(e?.x)) continue;
59
+ if (layerOf(id, e, known) !== zone) continue;
60
+ rects.push({ id, x: e.x, y: e.y, ...estimateSizeOn(board, id, e) });
61
+ }
62
+ // 每层还住着不在 objects 里的占面积物件:文件夹卡按**所在层**取
63
+ //(子文件夹卡住在父层里,2026-08-30 跨层幻影案之前这儿把它们全当根层矩形,
64
+ // 文件夹层内的落位则完全看不见它们 —— 同一个错的两面);卷卡只在根层。
65
+ if (furniture) {
66
+ for (const z of zoneRects(board, { layer: zone })) {
67
+ if (!skip.has(z.id)) rects.push(z);
68
+ }
69
+ }
70
+ if (!zone && furniture) {
71
+ for (const tag of Object.keys(board?.rolls || {})) {
72
+ if (skip.has(tag)) continue;
73
+ const r = rollCardRect(board, tag);
74
+ // 卷起来的成员座位照旧留在 objects 里当障碍(收纳不等于腾地),这一条
75
+ // 只为让"压住了卷标签"也能被如实报出来。
76
+ if (r) rects.push({ id: `roll:${tag}`, ...r });
77
+ }
78
+ }
79
+ return inflateSpriteSeats(rects, objs);
80
+ }