@xiaobuyu/nodesign 0.0.1 → 0.0.3

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 (358) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +42 -22
  9. package/server/api/assets/notes.js +194 -144
  10. package/server/api/assets.js +891 -896
  11. package/server/api/board.js +134 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +465 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1035 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +351 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +152 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/edit-sketch.js +167 -0
  120. package/server/engine/mcp/tools/explain-style.js +237 -237
  121. package/server/engine/mcp/tools/export-handoff.js +133 -133
  122. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  123. package/server/engine/mcp/tools/generate-image.js +842 -842
  124. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  125. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  126. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  127. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  128. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  129. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  130. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  131. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  132. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  133. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  134. package/server/engine/mcp/tools/highlight.js +68 -68
  135. package/server/engine/mcp/tools/list-pages.js +222 -222
  136. package/server/engine/mcp/tools/look-at-board.js +124 -0
  137. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  138. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  139. package/server/engine/mcp/tools/organize-board.js +64 -64
  140. package/server/engine/mcp/tools/paint-still.js +434 -434
  141. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  142. package/server/engine/mcp/tools/preview-deck.js +130 -130
  143. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  144. package/server/engine/mcp/tools/publish-site.js +102 -102
  145. package/server/engine/mcp/tools/query-elements.js +195 -195
  146. package/server/engine/mcp/tools/read-board.js +190 -103
  147. package/server/engine/mcp/tools/read-document.js +93 -93
  148. package/server/engine/mcp/tools/read-page.js +264 -264
  149. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  150. package/server/engine/mcp/tools/read-user-view.js +59 -0
  151. package/server/engine/mcp/tools/record-decision.js +140 -140
  152. package/server/engine/mcp/tools/relate-on-board.js +112 -110
  153. package/server/engine/mcp/tools/report-issue.js +115 -115
  154. package/server/engine/mcp/tools/roll-film.js +266 -266
  155. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  156. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  157. package/server/engine/mcp/tools/screenshot.js +583 -583
  158. package/server/engine/mcp/tools/sketch-on-board.js +337 -0
  159. package/server/engine/mcp/tools/tier-gate.js +96 -96
  160. package/server/engine/mcp/tools/trace-motion.js +215 -215
  161. package/server/engine/mcp/tools/web-search.js +548 -548
  162. package/server/engine/mcp/tools/write-on-board.js +130 -0
  163. package/server/engine/motion/inventory.js +349 -349
  164. package/server/engine/perception/session.js +235 -235
  165. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  174. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
  175. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  176. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  177. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
  178. 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 +78 -78
  179. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  180. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
  181. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  182. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  183. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  185. package/server/engine/runs/active-runs.js +698 -698
  186. package/server/engine/runs/board-tasklist.js +130 -0
  187. package/server/engine/runs/live-turn.js +252 -252
  188. package/server/engine/runs/store.js +364 -364
  189. package/server/engine/runs/turn-relay.js +217 -217
  190. package/server/engine/runtime/workspace.js +164 -164
  191. package/server/index.js +3 -0
  192. package/server/lib/artifact-file-path.js +80 -80
  193. package/server/lib/artifact-target.js +284 -284
  194. package/server/lib/asset-refs.js +239 -239
  195. package/server/lib/async-queue.js +104 -104
  196. package/server/lib/auto-relations.js +130 -130
  197. package/server/lib/binding-types.js +72 -60
  198. package/server/lib/board-graph-export.js +191 -0
  199. package/server/lib/board-groups.js +167 -0
  200. package/server/lib/board-hero.js +76 -0
  201. package/server/lib/board-kind-sizes.js +85 -62
  202. package/server/lib/board-relations.js +133 -133
  203. package/server/lib/browse-proxy.js +180 -180
  204. package/server/lib/canvas-id.js +36 -36
  205. package/server/lib/chalk.js +121 -0
  206. package/server/lib/cover.js +227 -227
  207. package/server/lib/danbooru-tags.js +291 -291
  208. package/server/lib/doc-extract.js +156 -156
  209. package/server/lib/docx/build-from-source.js +260 -260
  210. package/server/lib/docx/build.js +490 -490
  211. package/server/lib/docx/dump-styles.js +287 -287
  212. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  213. package/server/lib/docx/merge-runs.js +129 -129
  214. package/server/lib/docx/numbering.js +218 -218
  215. package/server/lib/docx/order.js +152 -152
  216. package/server/lib/docx/rawzip.js +172 -172
  217. package/server/lib/docx/render.js +92 -92
  218. package/server/lib/docx/text-lint.js +225 -225
  219. package/server/lib/docx/tokens.js +335 -335
  220. package/server/lib/docx/units.js +29 -29
  221. package/server/lib/docx/xml.js +291 -291
  222. package/server/lib/docx-pages.js +187 -187
  223. package/server/lib/export-collect.js +289 -289
  224. package/server/lib/export-package.js +210 -210
  225. package/server/lib/html-srcset.js +145 -145
  226. package/server/lib/image-variant.js +460 -460
  227. package/server/lib/ingress/forward-openai-chat.js +305 -305
  228. package/server/lib/ingress/openai-chat.js +491 -491
  229. package/server/lib/ingress/session-notice.js +66 -66
  230. package/server/lib/ingress/session-routes.js +68 -68
  231. package/server/lib/ingress/slot-probe.js +130 -130
  232. package/server/lib/ingress/upstream-billing.js +52 -52
  233. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  234. package/server/lib/ingress/upstream-truncation.js +48 -48
  235. package/server/lib/issues-store.js +182 -182
  236. package/server/lib/kinds/deck.js +72 -72
  237. package/server/lib/kinds/docx.js +283 -283
  238. package/server/lib/kinds/file-kinds.js +113 -113
  239. package/server/lib/kinds/index.js +202 -202
  240. package/server/lib/kinds/site.js +161 -161
  241. package/server/lib/model-ingress.js +543 -543
  242. package/server/lib/moderation.js +255 -255
  243. package/server/lib/notice-store.js +103 -103
  244. package/server/lib/plugin-install.js +104 -104
  245. package/server/lib/plugin-validator.js +721 -721
  246. package/server/lib/publish-store.js +115 -115
  247. package/server/lib/quick-summary.js +52 -52
  248. package/server/lib/quota.js +270 -270
  249. package/server/lib/rate-window.js +29 -29
  250. package/server/lib/reference-assets.js +92 -92
  251. package/server/lib/region-shot.js +135 -135
  252. package/server/lib/safe-path.js +73 -73
  253. package/server/lib/sdk-session.js +33 -33
  254. package/server/lib/showcase-store.js +92 -92
  255. package/server/lib/site-publish.js +465 -465
  256. package/server/lib/sketch-layout.js +277 -0
  257. package/server/lib/ssrf-guard.js +432 -432
  258. package/server/lib/task-scan.js +158 -158
  259. package/server/lib/tavern-json.js +154 -154
  260. package/server/lib/video-variant.js +237 -237
  261. package/server/lib/workspace-path.js +33 -33
  262. package/server/ops/fix-sdk-musl.mjs +40 -40
  263. package/server/ops/install-macos-fonts.sh +114 -114
  264. package/server/ops/macos-fonts.conf +336 -336
  265. package/server/ops/sandbox-shim/bwrap +57 -57
  266. package/server/projects/assets-summary.js +119 -119
  267. package/server/projects/auto-name.js +53 -53
  268. package/server/projects/board-limits.js +12 -0
  269. package/server/projects/board-sanitize.js +252 -0
  270. package/server/projects/board-store.js +507 -640
  271. package/server/projects/move-entry.js +103 -103
  272. package/server/projects/store.js +270 -270
  273. package/server/projects/ui-config.js +60 -54
  274. package/server/projects/viewpoint-store.js +78 -0
  275. package/server/projects/workspace-templates.js +53 -53
  276. package/server/projects/workspace.js +1025 -1025
  277. package/server/runtime/local-config.js +177 -177
  278. package/server/runtime/profile.js +72 -72
  279. package/server/services/rembg-service.py +334 -334
  280. package/server/shared/README.md +25 -25
  281. package/server/shared/deck.js +54 -54
  282. package/server/shared/time.js +49 -49
  283. package/server/style-pipeline/README.md +53 -53
  284. package/server/ws/broker.js +39 -36
  285. package/server/ws/browse-channel.js +177 -177
  286. package/server/ws/index.js +386 -386
  287. package/web/README.md +89 -89
  288. package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
  289. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
  290. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
  291. package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
  292. package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
  293. package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
  294. package/web/dist/assets/arc-D_khH_xN.js +1 -0
  295. package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
  296. package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
  297. package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
  298. package/web/dist/assets/channel-DGkccMO1.js +1 -0
  299. package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
  300. package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
  301. package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
  302. package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
  303. package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
  304. package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
  305. package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
  306. package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
  307. package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
  308. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
  309. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
  310. package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
  311. package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
  312. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
  313. package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
  314. package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
  315. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  316. package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
  317. package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
  318. package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
  319. package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
  320. package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
  321. package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
  322. package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
  323. package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
  324. package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
  325. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
  326. package/web/dist/assets/index-GAamh1p4.js +2152 -0
  327. package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
  328. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  329. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
  330. package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
  331. package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
  332. package/web/dist/assets/layout-CnXiF_NB.js +1 -0
  333. package/web/dist/assets/linear-BMW0u6G6.js +1 -0
  334. package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
  335. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  336. package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
  337. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
  338. package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
  339. package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
  340. package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
  341. package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
  342. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
  343. package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
  344. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
  345. package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
  346. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
  347. package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
  348. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
  349. package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
  350. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
  351. package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
  352. package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
  353. package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
  354. package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
  355. package/web/dist/index.html +17 -17
  356. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
  357. package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
  358. package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
@@ -0,0 +1,191 @@
1
+ /**
2
+ * server/lib/board-graph-export.js —— 连接图导出(2026-08-23 黑板)
3
+ *
4
+ * 真相是 board.json(节点 + 线 + 位置 + 标签),由它派生三种形态:
5
+ * json 结构原样(给程序 / 备份)
6
+ * mermaid flowchart 文本(给别的工具吃;位置丢掉,语义留着)
7
+ * svg 一张能看的图:卡片画成带名字的方框、手写字照排、涂鸦照路径、线照语义
8
+ * —— 这是**导出保真度**的渲染,不是画布本体(画布本体只有前端那一份)
9
+ * 可按 #tag 只导一组。带产物的 zip 另议(zip 要搬文件,是交付层的事)。
10
+ */
11
+ import { BINDING_TYPES } from './binding-types.js';
12
+ import { estimateSizeOn } from './board-kind-sizes.js';
13
+ import { layerOf } from './canvas-id.js';
14
+ import { describeEndpoint } from './board-relations.js';
15
+
16
+ function pick(board, { tag = null, layer = '' } = {}) {
17
+ const known = new Set(Object.keys(board.zones || {}));
18
+ const nodes = [];
19
+ for (const [id, e] of Object.entries(board.objects || {})) {
20
+ if (!Number.isFinite(e?.x)) continue;
21
+ if (layerOf(id, e, known) !== layer) continue;
22
+ if (tag && e.tag !== tag) continue;
23
+ const sz = estimateSizeOn(board, id, e);
24
+ nodes.push({
25
+ id, kind: e.kind || kindOfId(id), x: e.x, y: e.y, w: sz.w, h: sz.h,
26
+ ...(e.kind === 'text' ? { text: e.data?.t || '', format: e.data?.format || 'plain', font: e.data?.font, size: e.data?.size, color: e.data?.color } : {}),
27
+ ...(e.kind === 'scribble' ? { d: e.data?.d || '', color: e.data?.color, width: e.data?.width } : {}),
28
+ ...(e.tag ? { tag: e.tag } : {}), ...(e.staging ? { staging: true } : {}), ...(e.by ? { by: e.by } : {}),
29
+ });
30
+ }
31
+ const ids = new Set(nodes.map(n => n.id));
32
+ const edges = [];
33
+ for (const [id, b] of Object.entries(board.bindings || {})) {
34
+ if (!ids.has(b.from) || !ids.has(b.to)) continue;
35
+ if (tag && b.tag !== tag && !(ids.has(b.from) && ids.has(b.to))) continue;
36
+ edges.push({ id, type: b.type, from: b.from, to: b.to, ...(b.label ? { label: b.label } : {}), material: b.material || 'ink', ...(b.by ? { by: b.by } : {}), ...(b.tag ? { tag: b.tag } : {}) });
37
+ }
38
+ return { nodes, edges };
39
+ }
40
+
41
+ function kindOfId(id) {
42
+ const m = /^(deck|site|docx|doc):/.exec(id);
43
+ if (m) return m[1];
44
+ if (/\.(png|jpe?g|webp|gif|svg|avif)$/i.test(id)) return 'image';
45
+ if (/\.(mp4|webm|mov)$/i.test(id)) return 'video';
46
+ if (/\.(md|txt)$/i.test(id)) return 'note';
47
+ return 'file';
48
+ }
49
+
50
+ export function exportGraph(board, { format = 'json', tag = null, layer = '' } = {}) {
51
+ const g = pick(board, { tag, layer });
52
+ if (format === 'json') return { mime: 'application/json', body: JSON.stringify({ version: 1, tag, layer, ...g }, null, 2) };
53
+ if (format === 'mermaid') return { mime: 'text/plain; charset=utf-8', body: toMermaid(g, board) };
54
+ if (format === 'svg') return { mime: 'image/svg+xml; charset=utf-8', body: toSvg(g, board) };
55
+ throw Object.assign(new Error('unknown format'), { status: 400 });
56
+ }
57
+
58
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
59
+
60
+ function nodeTitle(n, board) {
61
+ if (n.kind === 'text') return String(n.text || '').replace(/^#+\s*/gm, '').replace(/\s+/g, ' ').trim().slice(0, 60);
62
+ if (n.kind === 'scribble') return '(涂鸦)';
63
+ return describeEndpoint(n.id, board).replace(/([a-z]+)$/, '');
64
+ }
65
+
66
+ export function toMermaid(g, board) {
67
+ const key = new Map(g.nodes.map((n, i) => [n.id, `n${i}`]));
68
+ const lines = ['flowchart LR'];
69
+ for (const n of g.nodes) {
70
+ if (n.kind === 'scribble') continue;
71
+ const t = nodeTitle(n, board).replace(/["[\]{}()|]/g, ' ').trim() || n.id;
72
+ const shape = n.kind === 'text' ? `("${t}")` : `["${t}"]`;
73
+ lines.push(` ${key.get(n.id)}${shape}`);
74
+ }
75
+ for (const e of g.edges) {
76
+ const t = BINDING_TYPES[e.type];
77
+ const word = (e.label || t?.label || e.type).replace(/["|]/g, ' ');
78
+ const arrow = t?.directed ? `-- ${word} -->` : `--- ${word} ---`;
79
+ if (!key.has(e.from) || !key.has(e.to)) continue;
80
+ lines.push(` ${key.get(e.from)} ${arrow} ${key.get(e.to)}`);
81
+ }
82
+ return lines.join('\n') + '\n';
83
+ }
84
+
85
+ const PAPER = { paper: '#F0EADB', ink: '#2B2117', ink2: '#5F5142', pencil: '#A39882', red: '#A8362B', brass: '#b08c4f', card: '#FFFEF6' };
86
+ const INK = { ink: PAPER.ink, red: PAPER.red, pencil: PAPER.pencil, brass: PAPER.brass };
87
+ const SIZE_PX = { sm: 13, md: 16, lg: 22, xl: 30 };
88
+
89
+ function edgePoint(a, b) {
90
+ const ac = { x: a.x + a.w / 2, y: a.y + a.h / 2 }; const bc = { x: b.x + b.w / 2, y: b.y + b.h / 2 };
91
+ const dx = bc.x - ac.x; const dy = bc.y - ac.y;
92
+ const pt = (r, c, ddx, ddy) => {
93
+ const tx = ddx === 0 ? Infinity : (r.w / 2) / Math.abs(ddx); const ty = ddy === 0 ? Infinity : (r.h / 2) / Math.abs(ddy);
94
+ const t = Math.min(tx, ty); return { x: c.x + ddx * t, y: c.y + ddy * t };
95
+ };
96
+ return { from: pt(a, ac, dx, dy), to: pt(b, bc, -dx, -dy) };
97
+ }
98
+
99
+ export function toSvg(g, board, { hrefOf = null } = {}) {
100
+ if (!g.nodes.length) return '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60"><text x="10" y="35" font-size="14">(空)</text></svg>';
101
+ const PAD = 40;
102
+ const x0 = Math.min(...g.nodes.map(n => n.x)) - PAD; const y0 = Math.min(...g.nodes.map(n => n.y)) - PAD;
103
+ const x1 = Math.max(...g.nodes.map(n => n.x + n.w)) + PAD; const y1 = Math.max(...g.nodes.map(n => n.y + n.h)) + PAD;
104
+ const W = Math.round(x1 - x0); const H = Math.round(y1 - y0);
105
+ const byId = new Map(g.nodes.map(n => [n.id, n]));
106
+ const out = [];
107
+ out.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="${x0} ${y0} ${W} ${H}" font-family="'Kaiti SC','KaiTi','STKaiti',serif">`);
108
+ out.push(`<rect x="${x0}" y="${y0}" width="${W}" height="${H}" fill="${PAPER.paper}"/>`);
109
+ out.push('<defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto" markerUnits="userSpaceOnUse"><path d="M 1 1 L 9 5 L 1 9 z" fill="context-stroke"/></marker><marker id="dot" markerWidth="8" markerHeight="8" refX="4" refY="4" markerUnits="userSpaceOnUse"><circle cx="4" cy="4" r="3" fill="context-stroke"/></marker></defs>');
110
+ // 线在物件下面
111
+ for (const e of g.edges) {
112
+ const a = byId.get(e.from); const b = byId.get(e.to); if (!a || !b) continue;
113
+ const p = edgePoint(a, b);
114
+ const dx = p.to.x - p.from.x; const dy = p.to.y - p.from.y; const dist = Math.hypot(dx, dy) || 1;
115
+ const yarn = e.material === 'yarn';
116
+ const lift = yarn ? 0 : Math.min(dist * 0.14, 46);
117
+ const cx = (p.from.x + p.to.x) / 2 + (yarn ? 0 : (-dy / dist) * lift);
118
+ const cy = (p.from.y + p.to.y) / 2 + (yarn ? Math.min(dist * 0.11, 56) : (dx / dist) * lift);
119
+ const t = BINDING_TYPES[e.type];
120
+ const stroke = yarn ? PAPER.red : (e.type === 'contrast' ? PAPER.brass : e.type === 'ref' ? PAPER.pencil : e.type === 'derives-from' ? PAPER.ink : PAPER.ink2);
121
+ const width = yarn ? 2.6 : e.type === 'derives-from' ? 2.4 : e.type === 'annotates' ? 1.4 : 2;
122
+ const dash = !yarn && e.type === 'annotates' ? ' stroke-dasharray="3 4"' : !yarn && e.type === 'ref' ? ' stroke-dasharray="1 5"' : '';
123
+ const marker = yarn ? '' : (t?.directed ? ' marker-end="url(#arrow)"' : (e.type === 'link' ? ' marker-end="url(#dot)" marker-start="url(#dot)"' : ''));
124
+ out.push(`<path d="M ${p.from.x} ${p.from.y} Q ${cx} ${cy} ${p.to.x} ${p.to.y}" fill="none" stroke="${stroke}" stroke-width="${width}" stroke-linecap="round"${dash}${marker}/>`);
125
+ if (yarn) for (const q of [p.from, p.to]) out.push(`<circle cx="${q.x}" cy="${q.y}" r="4.6" fill="${PAPER.red}" stroke="${PAPER.ink}" stroke-width="0.8"/>`);
126
+ const word = e.label || (e.type !== 'annotates' ? t?.label : null);
127
+ if (word) {
128
+ const mx = 0.25 * p.from.x + 0.5 * cx + 0.25 * p.to.x; const my = 0.25 * p.from.y + 0.5 * cy + 0.25 * p.to.y;
129
+ out.push(`<text x="${mx}" y="${my}" font-size="11" text-anchor="middle" dominant-baseline="middle" fill="${PAPER.ink2}" stroke="${PAPER.paper}" stroke-width="4" paint-order="stroke">${esc(word)}</text>`);
130
+ }
131
+ }
132
+ for (const n of g.nodes) {
133
+ const op = n.staging ? ' opacity="0.55"' : '';
134
+ if (n.kind === 'scribble') {
135
+ out.push(`<g transform="translate(${n.x} ${n.y})"${op}><path d="${esc(n.d)}" fill="none" stroke="${INK[n.color] || PAPER.ink}" stroke-width="${n.width || 2}" stroke-linecap="round" stroke-linejoin="round"/></g>`);
136
+ continue;
137
+ }
138
+ if (n.kind === 'text') {
139
+ const px = SIZE_PX[n.size] || 16; const fill = INK[n.color] || PAPER.ink;
140
+ const lines = String(n.text || '').split('\n').slice(0, 24);
141
+ out.push(`<g transform="translate(${n.x + 6} ${n.y + 4})"${op}>` + lines.map((l, i) => `<text x="0" y="${Math.round((i + 0.85) * px * 1.6)}" font-size="${px}" fill="${fill}">${esc(l.replace(/^#+\s*/, '').replace(/\*\*/g, ''))}</text>`).join('') + '</g>');
142
+ continue;
143
+ }
144
+ const title = nodeTitle(n, board);
145
+ const href = hrefOf ? hrefOf(n) : null;
146
+ out.push(`${href ? `<a href="${esc(href)}">` : ''}<g${op}><rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="10" fill="${PAPER.card}" stroke="rgba(43,33,23,0.18)"/>`
147
+ + `<text x="${n.x + 12}" y="${n.y + 20}" font-size="13" fill="${PAPER.ink}">${esc(title)}</text>`
148
+ + `<text x="${n.x + 12}" y="${n.y + n.h - 10}" font-size="10" fill="${PAPER.pencil}">${esc(n.kind)}</text></g>${href ? '</a>' : ''}`);
149
+ }
150
+ out.push('</svg>');
151
+ return out.join('\n');
152
+ }
153
+
154
+ /* ── 带产物的 zip(2026-08-23)────────────────────────────────────────────
155
+ * 真相仍是 board.json;zip 里:board/graph.json + graph.svg(卡片可点,链到 zip 里的文件)+
156
+ * graph.mmd + index.html(内嵌 svg 的静态重放页)+ 节点指向的产物文件(走 export-collect
157
+ * 同一套寻址:站点整目录、deck 的 html、图片、板书 .md…)。画布原生的手写字/涂鸦没有文件,
158
+ * 它们的内容都在 graph.json/svg 里。
159
+ */
160
+ export async function exportGraphZip(board, { workspaceRoot, tag = null, layer = '', projectName = '画布' } = {}) {
161
+ const [{ default: JSZip }, { collectCards }, fsm] = await Promise.all([
162
+ import('jszip'), import('./export-collect.js'), import('node:fs/promises'),
163
+ ]);
164
+ const g = pick(board, { tag, layer });
165
+ const fileIds = g.nodes.filter(n => n.kind !== 'text' && n.kind !== 'scribble').map(n => n.id);
166
+ const { bundles, skipped } = await collectCards({ workspaceRoot, cardIds: fileIds });
167
+ const zip = new JSZip();
168
+ const relOfNode = new Map();
169
+ for (const b of bundles) {
170
+ for (const f of [...(b.files || []), ...(b.assets || [])]) {
171
+ try { zip.file(f.rel, await fsm.readFile(f.abs)); } catch { /* 读不到就跳,写进 manifest */ }
172
+ }
173
+ }
174
+ // collectCard 不回 cardId:按顺序对齐(collectCards 按 cardIds 顺序 push,跳过的在 skipped)
175
+ const skippedIds = new Set(skipped.map(s => s.cardId));
176
+ let bi = 0;
177
+ for (const id of fileIds) {
178
+ if (skippedIds.has(id)) continue;
179
+ const b = bundles[bi++]; if (!b) break;
180
+ relOfNode.set(id, b.files?.[0]?.rel || b.assets?.[0]?.rel || null);
181
+ }
182
+ const hrefOf = (n) => { const r = relOfNode.get(n.id); return r ? `../${r.split('/').map(encodeURIComponent).join('/')}` : null; };
183
+ const svg = toSvg(g, board, { hrefOf });
184
+ zip.file('board/graph.json', JSON.stringify({ version: 1, tag, layer, ...g, files: Object.fromEntries(relOfNode), skipped }, null, 2));
185
+ zip.file('board/graph.svg', svg);
186
+ zip.file('board/graph.mmd', toMermaid(g, board));
187
+ zip.file('index.html', `<!doctype html><html lang="zh"><head><meta charset="utf-8"><title>${esc(projectName)} · 画布</title>
188
+ <style>html,body{margin:0;background:#F0EADB;font-family:'Kaiti SC','KaiTi',serif}header{padding:12px 20px;color:#2B2117;font-size:14px}main{overflow:auto;padding:0 20px 20px}svg{max-width:100%;height:auto;display:block}a:hover rect{stroke:#b08c4f;stroke-width:2}</style></head>
189
+ <body><header>${esc(projectName)} · 画布导出${tag ? ` · #${esc(tag)}` : ''} · ${g.nodes.length} 件 ${g.edges.length} 线 · 点卡片打开文件</header><main>${svg.replace(/\.\.\//g, '')}</main></body></html>`);
190
+ return { zip, skipped, count: { nodes: g.nodes.length, edges: g.edges.length, files: relOfNode.size } };
191
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * server/lib/board-groups.js —— 画布的「组」与「小地图」(2026-08-23 黑板)
3
+ *
4
+ * 黑板不设几何容器(用户拍板:frame 给黑板开小灶,终局产物卡也要走同一套)。
5
+ * 「组」是派生的:**被线连在一起的一群东西算一组**(连通分量),再加上显式
6
+ * `tag` 字段把没连线的也归到一起。read_board 按组分段输出、用户按组整选、渲染
7
+ * 按组画包络,三处读同一个判据 —— 入座算法里「关系组独占成行」用的也是连通
8
+ * 分量,没有第二份真相。
9
+ *
10
+ * 小地图:把一层的物件投到一张 ≤ 48×16 的字符网格上。模型读网格的整体感远比
11
+ * 读三十行坐标强;从 board.json 直接算,零成本。
12
+ */
13
+
14
+ /**
15
+ * 连通分量 + tag 合并。
16
+ * @param {string[]} ids 这一层的物件 id
17
+ * @param {object} bindings board.bindings
18
+ * @param {(id:string)=>string|null} tagOf
19
+ * @returns {Array<{ members: string[], tags: Set<string>, edges: string[] }>} 大组在前
20
+ */
21
+ export function groupObjects(ids, bindings, tagOf = () => null) {
22
+ const parent = new Map(ids.map(id => [id, id]));
23
+ const find = (x) => {
24
+ while (parent.get(x) !== x) { parent.set(x, parent.get(parent.get(x))); x = parent.get(x); }
25
+ return x;
26
+ };
27
+ const union = (a, b) => {
28
+ if (!parent.has(a) || !parent.has(b)) return;
29
+ const ra = find(a); const rb = find(b);
30
+ if (ra !== rb) parent.set(ra, rb);
31
+ };
32
+ const edgesIn = [];
33
+ const cross = [];
34
+ for (const [bid, b] of Object.entries(bindings || {})) {
35
+ if (b.by === 'auto' && b.type === 'ref') continue; // 自动取材边不成组(蜘蛛网)
36
+ if (!parent.has(b.from) || !parent.has(b.to)) continue;
37
+ // tag 优先:两端 tag 不同(或一端有一端没有)的线是"组间线",不把两张图粘成一组
38
+ // (08-23 真踩:两张草图之间一条 ref 线让 read_board 把它们报成一组 6 列)
39
+ const ta = tagOf(b.from); const tb = tagOf(b.to);
40
+ if (ta !== tb) { cross.push([bid, b]); continue; }
41
+ union(b.from, b.to); edgesIn.push([bid, b]);
42
+ }
43
+ // 同 tag 归一组
44
+ const byTag = new Map();
45
+ for (const id of ids) {
46
+ const t = tagOf(id);
47
+ if (!t) continue;
48
+ if (!byTag.has(t)) byTag.set(t, id); else union(byTag.get(t), id);
49
+ }
50
+ const groups = new Map();
51
+ for (const id of ids) {
52
+ const r = find(id);
53
+ if (!groups.has(r)) groups.set(r, { members: [], tags: new Set(), edges: [] });
54
+ const g = groups.get(r);
55
+ g.members.push(id);
56
+ const t = tagOf(id); if (t) g.tags.add(t);
57
+ }
58
+ for (const [bid, b] of edgesIn) {
59
+ const g = groups.get(find(b.from));
60
+ if (g) g.edges.push(bid);
61
+ }
62
+ const out = [...groups.values()].sort((a, b) => b.members.length - a.members.length);
63
+ out.cross = cross.map(([bid]) => bid); // 组间线 id(调用方愿意列就列)
64
+ return out;
65
+ }
66
+
67
+ const GLYPHS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
68
+
69
+ /**
70
+ * ASCII 小地图。
71
+ * @param {Array<{id:string,x:number,y:number,w:number,h:number}>} rects
72
+ * @param {{ cols?: number, rows?: number, viewport?: {x,y,w,h}|null }} opts
73
+ * @returns {{ grid: string, legend: Array<[string,string]>, bbox: {x,y,w,h} }|null}
74
+ */
75
+ export function asciiMinimap(rects, { cols = 48, rows = 16, viewport = null } = {}) {
76
+ if (!rects.length) return null;
77
+ let x0 = Infinity; let y0 = Infinity; let x1 = -Infinity; let y1 = -Infinity;
78
+ for (const r of rects) {
79
+ x0 = Math.min(x0, r.x); y0 = Math.min(y0, r.y);
80
+ x1 = Math.max(x1, r.x + r.w); y1 = Math.max(y1, r.y + r.h);
81
+ }
82
+ if (viewport) {
83
+ x0 = Math.min(x0, viewport.x); y0 = Math.min(y0, viewport.y);
84
+ x1 = Math.max(x1, viewport.x + viewport.w); y1 = Math.max(y1, viewport.y + viewport.h);
85
+ }
86
+ const W = Math.max(1, x1 - x0); const H = Math.max(1, y1 - y0);
87
+ // 等比:一个字符格近似正方(字符高宽比约 2:1,所以横向给两倍格数)
88
+ const scale = Math.max(W / cols, H / (rows * 2));
89
+ const gw = Math.max(1, Math.ceil(W / scale));
90
+ const gh = Math.max(1, Math.ceil(H / (scale * 2)));
91
+ const grid = Array.from({ length: gh }, () => Array(gw).fill('·'));
92
+ const legend = [];
93
+ if (viewport) {
94
+ // 视口边框用 +─│ 画在底下,物件盖在上面
95
+ const cx0 = Math.floor((viewport.x - x0) / scale); const cx1 = Math.min(gw - 1, Math.floor((viewport.x + viewport.w - x0) / scale));
96
+ const cy0 = Math.floor((viewport.y - y0) / (scale * 2)); const cy1 = Math.min(gh - 1, Math.floor((viewport.y + viewport.h - y0) / (scale * 2)));
97
+ for (let x = cx0; x <= cx1; x += 1) { if (grid[cy0]) grid[cy0][x] = '─'; if (grid[cy1]) grid[cy1][x] = '─'; }
98
+ for (let y = cy0; y <= cy1; y += 1) { if (grid[y]) { grid[y][cx0] = '│'; grid[y][cx1] = '│'; } }
99
+ if (grid[cy0]) { grid[cy0][cx0] = '┌'; grid[cy0][cx1] = '┐'; }
100
+ if (grid[cy1]) { grid[cy1][cx0] = '└'; grid[cy1][cx1] = '┘'; }
101
+ }
102
+ rects.forEach((r, i) => {
103
+ const g = i < GLYPHS.length ? GLYPHS[i] : '#';
104
+ legend.push([g, r.id]);
105
+ const cx0 = Math.floor((r.x - x0) / scale); const cx1 = Math.min(gw - 1, Math.max(cx0, Math.ceil((r.x + r.w - x0) / scale) - 1));
106
+ const cy0 = Math.floor((r.y - y0) / (scale * 2)); const cy1 = Math.min(gh - 1, Math.max(cy0, Math.ceil((r.y + r.h - y0) / (scale * 2)) - 1));
107
+ for (let y = cy0; y <= cy1; y += 1) for (let x = cx0; x <= cx1; x += 1) if (grid[y]) grid[y][x] = g;
108
+ });
109
+ return {
110
+ grid: grid.map(row => row.join('')).join('\n'),
111
+ legend,
112
+ bbox: { x: Math.round(x0), y: Math.round(y0), w: Math.round(W), h: Math.round(H) },
113
+ cell: Math.round(scale),
114
+ };
115
+ }
116
+
117
+ /* ────────────────────────────────────────────────────────────────────────
118
+ * 相对位置(2026-08-23 晚,用户问「我们有没有告诉 agent 各模块的相对位置」——没有,
119
+ * 之前只给绝对坐标和一张小地图;模型对"组 2 在组 1 左侧、同高"这种话的理解远好于
120
+ * 读坐标)。下面把组的包围盒之间、组与用户视口之间的关系说成人话;组内再认一下
121
+ * 列(x 聚类)给出阅读顺序提示。
122
+ * ──────────────────────────────────────────────────────────────────────── */
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
+ }
129
+
130
+ /** b 相对 a 在哪(以 a 为参照):左/右/上/下 + 距离;重叠则说重叠 */
131
+ export function relationOf(a, b) {
132
+ if (!a || !b) return null;
133
+ const gapX = b.x >= a.x + a.w ? b.x - (a.x + a.w) : (b.x + b.w <= a.x ? a.x - (b.x + b.w) : -1);
134
+ const gapY = b.y >= a.y + a.h ? b.y - (a.y + a.h) : (b.y + b.h <= a.y ? a.y - (b.y + b.h) : -1);
135
+ if (gapX < 0 && gapY < 0) return '重叠';
136
+ const horiz = gapX >= 0 && (gapY < 0 || gapX >= gapY);
137
+ if (horiz) {
138
+ const side = b.x >= a.x + a.w ? '右侧' : '左侧';
139
+ const vAlign = Math.abs(b.y - a.y) < 60 ? '顶齐' : (b.y > a.y ? '偏下' : '偏上');
140
+ return `${side} ${Math.round(gapX)}px(${vAlign})`;
141
+ }
142
+ const side = b.y >= a.y + a.h ? '下方' : '上方';
143
+ const hAlign = Math.abs(b.x - a.x) < 60 ? '左齐' : (b.x > a.x ? '偏右' : '偏左');
144
+ return `${side} ${Math.round(gapY)}px(${hAlign})`;
145
+ }
146
+
147
+ /** 组内的列:按左边缘聚类(容差 60px),返回每列的成员数(自左向右) */
148
+ export function columnsOf(rects) {
149
+ const xs = [...rects].sort((a, b) => a.x - b.x);
150
+ const cols = [];
151
+ for (const r of xs) {
152
+ const c = cols.find(k => Math.abs(k.x - r.x) < 60);
153
+ if (c) { c.n += 1; c.x = (c.x * (c.n - 1) + r.x) / c.n; } else cols.push({ x: r.x, n: 1 });
154
+ }
155
+ return cols;
156
+ }
157
+
158
+ /** 视口与一块区域的关系 */
159
+ export function viewportRelation(vp, box) {
160
+ if (!vp || !box) return null;
161
+ const ix = Math.max(0, Math.min(vp.x + vp.w, box.x + box.w) - Math.max(vp.x, box.x));
162
+ const iy = Math.max(0, Math.min(vp.y + vp.h, box.y + box.h) - Math.max(vp.y, box.y));
163
+ const cover = (ix * iy) / Math.max(1, box.w * box.h);
164
+ if (cover >= 0.95) return '整块在用户视口里';
165
+ if (cover > 0) return `用户视口盖住它约 ${Math.round(cover * 100)}%`;
166
+ return `在用户视口之外(视口${relationOf(box, vp) || '别处'})`;
167
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * server/lib/board-hero.js —— 主角判断的服务端镜像(2026-08-23 黑板)
3
+ *
4
+ * 前端 `web/src/lib/hero.js` 的 pickHero 决定哪张产物卡放大 1.5 倍。服务端摆东西
5
+ * (sketch_on_board / create_on_board 的避让、read_board 的尺寸、look_at_board 的
6
+ * 取景)在这之前只认基础尺寸,于是"锚在主角卡右边"会落进主角卡的身体里
7
+ * (08-23 首跑真踩:纸本站是主角 960 宽,草图从 640 处起算,整张压进去)。
8
+ *
9
+ * **镜像不是重写**:下面 pickHero 的函数体必须与 web/src/lib/hero.js 逐字相同,
10
+ * board-hero.test.js 读两份源码比对。npm 包不带 web/src,所以不能 import 过来。
11
+ */
12
+ import { DECK_EMBED_W, ARTIFACT_HEADER_H, ARTIFACT_PREVIEW_H } from './board-kind-sizes.js';
13
+ import { layerOf } from './canvas-id.js';
14
+
15
+ // ── 以下到 END-MIRROR 与 web/src/lib/hero.js 逐字一致 ──
16
+ const ELIGIBLE = new Set(['deck', 'site', 'docx']);
17
+ const FOCUS_PER_EDGE = 0.5;
18
+ const FOCUS_CAP = 1.5;
19
+ export function pickHero(items, bindings) {
20
+ const score = new Map();
21
+ for (const it of items || []) {
22
+ if (ELIGIBLE.has(it.type)) score.set(String(it.id), 3);
23
+ }
24
+ if (!score.size) return null;
25
+ const bump = (id, d) => { if (score.has(id)) score.set(id, score.get(id) + d); };
26
+ const focus = new Map();
27
+ for (const b of Object.values(bindings || {})) {
28
+ if (b.type === 'derives-from') { bump(b.from, 2); bump(b.to, -3); }
29
+ if (b.type === 'ref' && b.by === 'auto') bump(b.to, -3);
30
+ if (b.by === 'user' || b.by === 'agent') {
31
+ for (const end of [b.from, b.to]) {
32
+ if (!score.has(end)) continue;
33
+ const used = focus.get(end) || 0;
34
+ if (used >= FOCUS_CAP) continue;
35
+ focus.set(end, used + FOCUS_PER_EDGE);
36
+ bump(end, FOCUS_PER_EDGE);
37
+ }
38
+ }
39
+ }
40
+ const ranked = [...score.entries()].sort((a, z) => z[1] - a[1] || a[0].localeCompare(z[0]));
41
+ if (ranked.length === 1) return ranked[0][0];
42
+ if (ranked[0][1] === ranked[1][1]) return null; // 并列 = 没证据
43
+ return ranked[0][0];
44
+ }
45
+ // ── END-MIRROR ──
46
+
47
+ export const HERO_SCALE = 1.5; // = web board-kinds.js HERO_SCALE
48
+
49
+ const TYPE_RE = /^(deck|site|docx):/;
50
+
51
+ /**
52
+ * 这块板此刻的主角 id(只看桌面根层,与前端入座同口径):board.hero 显式覆盖,
53
+ * 否则按关系线推。前端还会把谱系收叠藏起来的旧版排除在外 —— 那些卡本来就被
54
+ * 改自边重罚,结果一致,这里不重复实现收叠。
55
+ */
56
+ export function boardHeroId(board) {
57
+ const known = new Set(Object.keys(board?.zones || {}));
58
+ const items = [];
59
+ for (const [id, e] of Object.entries(board?.objects || {})) {
60
+ if (!Number.isFinite(e?.x) || e.kind) continue;
61
+ if (layerOf(id, e, known) !== '') continue;
62
+ const m = TYPE_RE.exec(id);
63
+ if (m) items.push({ id, type: m[1] });
64
+ }
65
+ if (board?.hero && items.some(it => it.id === board.hero)) return board.hero;
66
+ return pickHero(items, board?.bindings || {});
67
+ }
68
+
69
+ export function heroSize(id) {
70
+ const m = TYPE_RE.exec(String(id || ''));
71
+ if (!m) return null;
72
+ return {
73
+ w: Math.round(DECK_EMBED_W * HERO_SCALE),
74
+ h: ARTIFACT_HEADER_H + Math.round(ARTIFACT_PREVIEW_H[m[1]] * HERO_SCALE),
75
+ };
76
+ }
@@ -1,62 +1,85 @@
1
- /**
2
- * board-kind-sizes —— 画布物件身位的服务端镜像(2026-08-14,agent 摆位批)
3
- *
4
- * read_board / arrange_on_board / create_on_board 要在服务端估算卡片矩形
5
- * (挨着谁摆、会不会压到谁),而身位真相住在前端 `web/src/lib/board-kinds.js`
6
- * (渲染方)。这里抄一份**常量**,web 侧有 parity 测试钉着两边一致 ——
7
- * 改那边忘了改这边,测试直接红(跟 binding-types 的双份表同一套纪律)。
8
- *
9
- * 文字/涂鸦不在表里:它们的 w/h 是逐条存在 board.json 里的(本体即数据)。
10
- */
11
-
12
- import { KINDS } from './kinds/index.js';
13
-
14
- export const DECK_EMBED_W = 640;
15
- export const ARTIFACT_HEADER_H = 28;
16
- export const ARTIFACT_PREVIEW_H = { deck: 360, site: 400, docx: 420, browse: 360 };
17
-
18
- // file 是 224x32 的细条卡(parity 测试上岗第一天就逮住我猜成 160x120 ——
19
- // 那是涂鸦的默认身位。别猜,抄表)
20
- export const KIND_SIZES = {
21
- doc: { w: 200, h: 96 },
22
- image: { w: 200, h: 176 },
23
- video: { w: 240, h: 160 },
24
- note: { w: 200, h: 148 },
25
- file: { w: 224, h: 32 },
26
- };
27
-
28
- // 前缀表从注册表派生(「写死表家族」第 4 处,2026-08-18 收):手写
29
- // `deck|site|docx` 的话加形态必漏,新形态的卡掉到 file 兜底(224×32 细条),
30
- // agent 摆位按错矩形算。⚠️ 新形态要同步给 ARTIFACT_PREVIEW_H 加一行
31
- //(两边都有 parity 测试钉着前端那份)。
32
- const KIND_PREFIX_RE = new RegExp(`^(${Object.keys(KINDS).join('|')}):`);
33
-
34
- const IMG_EXT = /\.(png|jpe?g|webp|gif|svg|avif)$/i;
35
- const VIDEO_EXT = /\.(mp4|webm|mov)$/i;
36
-
37
- /**
38
- * 按 id + board.json 条目估身位。优先条目里存的 w/h(文字/涂鸦,或将来任何
39
- * 显式尺寸),其次按 id 形态推:kind 前缀 产物卡,扩展名 → 图/视频/便签/文件。
40
- */
41
- export function estimateSize(id, entry) {
42
- if (entry && Number.isFinite(entry.w) && Number.isFinite(entry.h)) {
43
- return { w: entry.w, h: entry.h };
44
- }
45
- const s = String(id || '');
46
- if (s.startsWith('doc:')) return KIND_SIZES.doc;
47
- // 浏览器卡是**单例**,id 就是 'browse'(没有路径可挂 —— 它背后不是文件)。
48
- // 不给这条分支的话它会掉到最后的 file 兜底(224×32 的细条),agent 摆位时
49
- // 就会拿一个错的矩形去算"挨着谁摆、会不会压到谁"。
50
- if (s === 'browse') {
51
- return { w: DECK_EMBED_W, h: ARTIFACT_HEADER_H + ARTIFACT_PREVIEW_H.browse };
52
- }
53
- const m = KIND_PREFIX_RE.exec(s);
54
- if (m) {
55
- const t = m[1];
56
- return { w: DECK_EMBED_W, h: ARTIFACT_HEADER_H + ARTIFACT_PREVIEW_H[t] };
57
- }
58
- if (IMG_EXT.test(s)) return KIND_SIZES.image;
59
- if (VIDEO_EXT.test(s)) return KIND_SIZES.video;
60
- if (/\.(md|txt)$/i.test(s)) return KIND_SIZES.note;
61
- return KIND_SIZES.file;
62
- }
1
+ /**
2
+ * board-kind-sizes —— 画布物件身位的服务端镜像(2026-08-14,agent 摆位批)
3
+ *
4
+ * read_board / arrange_on_board / create_on_board 要在服务端估算卡片矩形
5
+ * (挨着谁摆、会不会压到谁),而身位真相住在前端 `web/src/lib/board-kinds.js`
6
+ * (渲染方)。这里抄一份**常量**,web 侧有 parity 测试钉着两边一致 ——
7
+ * 改那边忘了改这边,测试直接红(跟 binding-types 的双份表同一套纪律)。
8
+ *
9
+ * 文字/涂鸦不在表里:它们的 w/h 是逐条存在 board.json 里的(本体即数据)。
10
+ */
11
+
12
+ import { KINDS } from './kinds/index.js';
13
+ import { boardHeroId, heroSize } from './board-hero.js';
14
+
15
+ export const DECK_EMBED_W = 640;
16
+ export const ARTIFACT_HEADER_H = 28;
17
+ export const ARTIFACT_PREVIEW_H = { deck: 360, site: 400, docx: 420, browse: 360 };
18
+
19
+ // file 是 224x32 的细条卡(parity 测试上岗第一天就逮住我猜成 160x120 ——
20
+ // 那是涂鸦的默认身位。别猜,抄表)
21
+ export const KIND_SIZES = {
22
+ doc: { w: 200, h: 96 },
23
+ image: { w: 200, h: 176 },
24
+ video: { w: 240, h: 160 },
25
+ note: { w: 200, h: 148 },
26
+ file: { w: 224, h: 32 },
27
+ };
28
+
29
+ // 前缀表从注册表派生(「写死表家族」第 4 处,2026-08-18 收):手写
30
+ // `deck|site|docx` 的话加形态必漏,新形态的卡掉到 file 兜底(224×32 细条),
31
+ // agent 摆位按错矩形算。⚠️ 新形态要同步给 ARTIFACT_PREVIEW_H 加一行
32
+ //(两边都有 parity 测试钉着前端那份)。
33
+ const KIND_PREFIX_RE = new RegExp(`^(${Object.keys(KINDS).join('|')}):`);
34
+
35
+ const IMG_EXT = /\.(png|jpe?g|webp|gif|svg|avif)$/i;
36
+ const VIDEO_EXT = /\.(mp4|webm|mov)$/i;
37
+
38
+ /**
39
+ * id + board.json 条目估身位。优先条目里存的 w/h(文字/涂鸦,或将来任何
40
+ * 显式尺寸),其次按 id 形态推:kind 前缀 → 产物卡,扩展名 → 图/视频/便签/文件。
41
+ */
42
+ export function estimateSize(id, entry) {
43
+ if (entry && Number.isFinite(entry.w) && Number.isFinite(entry.h)) {
44
+ return { w: entry.w, h: entry.h };
45
+ }
46
+ const s = String(id || '');
47
+ if (s.startsWith('doc:')) return KIND_SIZES.doc;
48
+ // 浏览器卡是**单例**,id 就是 'browse'(没有路径可挂 —— 它背后不是文件)。
49
+ // 不给这条分支的话它会掉到最后的 file 兜底(224×32 的细条),agent 摆位时
50
+ // 就会拿一个错的矩形去算"挨着谁摆、会不会压到谁"。
51
+ if (s === 'browse') {
52
+ return { w: DECK_EMBED_W, h: ARTIFACT_HEADER_H + ARTIFACT_PREVIEW_H.browse };
53
+ }
54
+ const m = KIND_PREFIX_RE.exec(s);
55
+ if (m) {
56
+ const t = m[1];
57
+ return { w: DECK_EMBED_W, h: ARTIFACT_HEADER_H + ARTIFACT_PREVIEW_H[t] };
58
+ }
59
+ if (IMG_EXT.test(s)) return KIND_SIZES.image;
60
+ if (VIDEO_EXT.test(s)) return KIND_SIZES.video;
61
+ if (/\.(md|txt)$/i.test(s)) return KIND_SIZES.note;
62
+ return KIND_SIZES.file;
63
+ }
64
+
65
+ /**
66
+ * 按**这块板**估尺寸:主角卡(board-hero 的判断)放大 1.5 倍,其余同 estimateSize。
67
+ * 凡是拿尺寸去避让/取景/列座次的地方都该用它 —— 主角不是特例,是版面常态。
68
+ * heroId 按 board 对象缓存一次(同一次工具调用里反复问不重算)。
69
+ */
70
+ const heroCache = new WeakMap();
71
+ export function estimateSizeOn(board, id, entry) {
72
+ if (entry && Number.isFinite(entry.w) && Number.isFinite(entry.h)) return { w: entry.w, h: entry.h };
73
+ if (board && typeof board === 'object') {
74
+ let hero = heroCache.get(board);
75
+ if (hero === undefined) {
76
+ hero = boardHeroId(board);
77
+ heroCache.set(board, hero);
78
+ }
79
+ if (hero && hero === id) {
80
+ const hs = heroSize(id);
81
+ if (hs) return hs;
82
+ }
83
+ }
84
+ return estimateSize(id, entry);
85
+ }