@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,337 @@
1
+ /**
2
+ * mcp/tools/sketch-on-board.js —— sketch_on_board / finish_sketch(2026-08-23 黑板)
3
+ *
4
+ * 让 agent 把画布当黑板:一次调用落一整张示意图(节点 + 形状 + 线),而不是
5
+ * 像 create_on_board 那样一条条写。设计要点(跟用户商定的形状):
6
+ * - **没有几何容器**:一张图 = 一批带同一 #tag 的普通物件和线。read_board 按
7
+ * 连通分量 + tag 分段读,用户可以整组选/整组擦,入座算法照旧绕着走
8
+ * - **两阶段**:落下来的默认是 staging(半透明,"正在画"),finish_sketch 或
9
+ * 回合结束自动落定;落定只清 staging 位,tag 留着当逻辑分组
10
+ * - **结构不是像素**:agent 给模板 + 网格坐标(1 格 = 24px),服务端算像素与落位
11
+ * - **形状是涂鸦**:rect/ellipse/arrow… 在服务端生成带手绘抖动的路径,落成
12
+ * 普通 scribble 对象,前端零改动;自由路径(path)也收,同一白名单
13
+ * - 线的材质缺省 pencil(手绘)—— 草图上的线是人顺手拉的;要丝线/墨线自己说
14
+ *
15
+ * 护栏:每回合 3 张图;每张 ≤ 40 节点 / 30 形状 / 60 线。
16
+ */
17
+
18
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
19
+ import { z } from 'zod';
20
+ import { readBoard, patchBoard, commitStaging, removeByTag, TEXT_FONTS } from '../../../projects/board-store.js';
21
+ import { estimateSizeOn } from '../../../lib/board-kind-sizes.js';
22
+ import { layerOf, normalizeCanvasId } from '../../../lib/canvas-id.js';
23
+ import { BINDING_TYPE_IDS, BINDING_MATERIALS } from '../../../lib/binding-types.js';
24
+ import { UNIT, SKETCH_FIT, SKETCH_MAX, textBox, shapePath, layoutNodes, bboxOf, findSpot, fitFor } from '../../../lib/sketch-layout.js';
25
+ import { getViewpoint } from '../../../projects/viewpoint-store.js';
26
+
27
+ const MAX_NODES = 40;
28
+ const MAX_SHAPES = 30;
29
+ const MAX_EDGES = 60;
30
+
31
+ let seq = 0;
32
+ const stamp = () => `${Date.now().toString(36)}${(seq++ % 1000).toString(36)}`;
33
+ const COLORS = ['ink', 'red', 'pencil', 'brass'];
34
+
35
+ const LOCAL_ID = z.string().regex(/^[A-Za-z0-9_-]{1,24}$/, 'local id: letters/digits/_/-');
36
+ const GRID_PT = z.object({ x: z.number().min(-400).max(400), y: z.number().min(-400).max(400) });
37
+
38
+ export function makeSketchOnBoardTool({ projectId, ctx }) {
39
+ const turnStamp = { turn: -1, count: 0 }; // 只记数(返回里不再报余额)
40
+ return tool(
41
+ 'sketch_on_board',
42
+ `Draw a whole sketch on the canvas in ONE call — notes, shapes and lines, laid out
43
+ together: a comparison table, a flow, a mind map, a detective board linking real
44
+ artifacts. The canvas is the blackboard; use it when thinking out loud with the user
45
+ would be clearer as a picture than as prose.
46
+
47
+ How it works
48
+ - You describe STRUCTURE; the server does pixels. Pick a layout template (column /
49
+ row / grid / mindmap) or place nodes yourself on a grid (1 cell = ${UNIT}px, layout
50
+ 'free', at:{x,y}). Whole sketch lands beside \`near\` (a canvas id from read_board)
51
+ or under the desktop's current content.
52
+ - nodes: plain handwriting or format:'md' (Markdown + KaTeX $…$ + \`\`\`mermaid fences).
53
+ Use md for lists/tables/formulas; mermaid only for dense formal diagrams
54
+ (sequence/state) — anything that should connect to real artifacts must be nodes+edges.
55
+ - shapes: rect / ellipse / circle / line / arrow / underline (hand-drawn look is
56
+ applied for you) or path (raw SVG M/L/Q in local px) — all in grid units.
57
+ - edges: between local node ids and/or real canvas ids. type = the relation
58
+ vocabulary (link/annotates/flow/contrast/derives-from/ref); material = ink (quiet
59
+ archive line) / pencil (hand-drawn, default here) / yarn (detective red string + pins
60
+ for hypotheses and evidence).
61
+ - Everything carries one #tag (a group): read_board {tag} reads just it, the user can
62
+ select/erase it as a whole, finish_sketch commits or erases it.
63
+ - Items land as STAGING (half-transparent = you are still drawing). Call finish_sketch
64
+ when done, or they commit automatically when the turn ends. Look at the result with
65
+ look_at_board before you call it done — agent scribbles only make sense with eyes.
66
+ - Readability rule: the user reads the board at 80–100% zoom. Body text stays md/lg
67
+ (sm only for captions ≤40 chars); one sketch ≤ ${SKETCH_FIT.w}x${SKETCH_FIT.h} world px (one screen at 80%),
68
+ hard limit ${SKETCH_MAX.w}x${SKETCH_MAX.h} — split into two tagged sketches instead of one huge one.
69
+ - To change an existing sketch use edit_sketch (move/retext/add/remove by id) — do NOT
70
+ erase and redraw the whole thing for a small change.
71
+ Per sketch: ≤${MAX_NODES} nodes, ${MAX_SHAPES} shapes, ${MAX_EDGES} edges (split big ones).`,
72
+ {
73
+ title: z.string().max(60).optional().describe('Optional heading written at the top of the sketch'),
74
+ tag: z.string().regex(/^[\w一-鿿぀-ヿ-]{1,40}$/).optional()
75
+ .describe('Group tag (letters/digits/_/-/CJK, ≤40). Default: auto "sk-<stamp>"'),
76
+ near: z.string().max(300).optional().describe('Canvas id to place the sketch beside (right side; falls back to below)'),
77
+ layout: z.enum(['auto', 'free', 'column', 'row', 'grid', 'mindmap']).optional()
78
+ .describe('auto = free if any node has at, else column (≤4) / grid'),
79
+ cols: z.number().int().min(1).max(8).optional().describe('grid columns'),
80
+ staging: z.boolean().optional().describe('Default true. false = land committed (solid) immediately'),
81
+ nodes: z.array(z.object({
82
+ id: LOCAL_ID.describe('Local id to reference from edges/shapes'),
83
+ text: z.string().min(1).max(4000),
84
+ format: z.enum(['plain', 'md']).optional(),
85
+ size: z.enum(['sm', 'md', 'lg', 'xl']).optional(),
86
+ font: z.enum(['pen', 'kai', 'sans', 'serif', 'mono']).optional(),
87
+ color: z.enum(['ink', 'red', 'pencil', 'brass']).optional(),
88
+ at: GRID_PT.optional().describe('Grid position (layout free); top-left of the node'),
89
+ w: z.number().min(3).max(22).optional().describe('Width in grid units (md nodes; default by content; ≤22 = 528px — paragraphs grow down, not wide)'),
90
+ })).max(MAX_NODES).optional(),
91
+ shapes: z.array(z.object({
92
+ id: LOCAL_ID.optional(),
93
+ kind: z.enum(['rect', 'ellipse', 'circle', 'line', 'arrow', 'underline', 'path']),
94
+ at: GRID_PT.optional().describe('Grid position (top-left for rect/ellipse; start point for line/arrow). Or use around:'),
95
+ around: LOCAL_ID.optional().describe('rect/ellipse/circle/underline: wrap this node (pad applied) instead of at/w/h'),
96
+ w: z.number().min(0).max(200).optional().describe('grid units'),
97
+ h: z.number().min(0).max(200).optional().describe('grid units'),
98
+ to: GRID_PT.optional().describe('line/arrow end point (grid)'),
99
+ toNode: LOCAL_ID.optional().describe('line/arrow: aim at this node\'s center instead of to'),
100
+ d: z.string().max(8000).optional().describe('path kind: SVG path (M/L/Q/Z only) in local px'),
101
+ color: z.enum(['ink', 'red', 'pencil', 'brass']).optional(),
102
+ width: z.number().min(1).max(12).optional(),
103
+ })).max(MAX_SHAPES).optional(),
104
+ edges: z.array(z.object({
105
+ from: z.string().min(1).max(300).describe('local node id or canvas id'),
106
+ to: z.string().min(1).max(300).describe('local node id or canvas id'),
107
+ type: z.enum(BINDING_TYPE_IDS).optional().describe('default link'),
108
+ material: z.enum(BINDING_MATERIALS).optional().describe('default pencil'),
109
+ label: z.string().max(60).optional(),
110
+ })).max(MAX_EDGES).optional(),
111
+ },
112
+ async (args) => {
113
+ if (!projectId) return { content: [{ type: 'text', text: 'No project bound.' }], isError: true };
114
+ const err = (t) => ({ content: [{ type: 'text', text: t }], isError: true });
115
+ // 回合闸 08-23 用户拍板先不设(「别设置限制吧先」):只记数不拦
116
+ const turn = ctx?.runId ?? -1;
117
+ if (turnStamp.turn !== turn) { turnStamp.turn = turn; turnStamp.count = 0; }
118
+
119
+ const nodesIn = args.nodes || [];
120
+ const shapesIn = args.shapes || [];
121
+ const edgesIn = args.edges || [];
122
+ if (!nodesIn.length && !shapesIn.length) return err('空图不落板:至少给一个节点或形状。');
123
+ const tag = args.tag || `sk-${stamp()}`;
124
+ const staging = args.staging !== false;
125
+
126
+ // ── 节点身位 ──
127
+ const localIds = new Set();
128
+ const nodes = [];
129
+ if (args.title) {
130
+ nodes.push({ key: '__title', text: `## ${args.title}`, format: 'md', size: 'md', font: 'kai', color: 'ink', at: null, w: null });
131
+ }
132
+ for (const n of nodesIn) {
133
+ if (localIds.has(n.id)) return err(`节点 id 重复:${n.id}`);
134
+ localIds.add(n.id);
135
+ // 可读性规范:正文不低于 md(0.8 倍下 12.8 屏幕像素是底线);sm 只给 ≤40 字的题注
136
+ const size = (n.size === 'sm' && n.text.length > 40) ? 'md' : (n.size || 'md');
137
+ nodes.push({ key: n.id, text: n.text, format: n.format || 'plain', size, font: n.font || 'pen', color: n.color || 'ink', at: n.at || null, w: n.w || null });
138
+ }
139
+ for (const n of nodes) {
140
+ const box = textBox(n.text, n.size, { md: n.format === 'md', wUnits: n.w });
141
+ n.w = box.w; n.h = box.h;
142
+ }
143
+ // 标题永远在最上面:free 模式给它 (0,-2),模板模式它就是第一个
144
+ const titleNode = nodes.find(n => n.key === '__title');
145
+ let tpl = args.layout || 'auto';
146
+ if (tpl === 'auto') tpl = nodes.some(n => n.at) ? 'free' : (nodes.length - (titleNode ? 1 : 0) <= 4 ? 'column' : 'grid');
147
+ const layoutInput = titleNode && tpl === 'mindmap' ? nodes.filter(n => n !== titleNode) : nodes;
148
+ const pos = layoutNodes(layoutInput, { template: tpl, cols: args.cols });
149
+ if (titleNode && !pos.has('__title')) {
150
+ const bb = bboxOf([...pos.entries()].map(([k, p]) => ({ x: p.x, y: p.y, ...nodes.find(n => n.key === k) })));
151
+ pos.set('__title', { x: bb.x, y: bb.y - titleNode.h - 12 });
152
+ } else if (titleNode && tpl === 'free' && !titleNode.at) {
153
+ const bb = bboxOf([...pos.entries()].filter(([k]) => k !== '__title').map(([k, p]) => ({ x: p.x, y: p.y, ...nodes.find(n => n.key === k) })));
154
+ pos.set('__title', { x: bb.x, y: bb.y - titleNode.h - 12 });
155
+ }
156
+ const rectOfNode = (key) => { const n = nodes.find(x => x.key === key); const p = pos.get(key); return n && p ? { x: p.x, y: p.y, w: n.w, h: n.h } : null; };
157
+
158
+ // ── 形状(局部像素)──
159
+ const shapes = [];
160
+ for (let i = 0; i < shapesIn.length; i += 1) {
161
+ const s = shapesIn[i];
162
+ const sid = s.id || `s${i + 1}`;
163
+ if (localIds.has(sid) || shapes.some(x => x.key === sid)) return err(`形状 id「${sid}」跟节点/别的形状重名(形状缺省叫 s1,s2…,节点别用这类名)`);
164
+ const seed = `${tag}:${sid}`;
165
+ const color = COLORS.includes(s.color) ? s.color : (s.kind === 'arrow' || s.kind === 'line' ? 'ink2' : 'ink');
166
+ const width = s.width || 2;
167
+ let rect; let d;
168
+ if (s.kind === 'path') {
169
+ if (!s.d || !/^[\dMLQCZ ,.\-eE]+$/.test(s.d)) return err(`形状 ${sid}:path 只收 M/L/Q/Z 与数字`);
170
+ // 自由路径:算它的包围盒,平移到 (at 或 0,0)
171
+ const nums = s.d.match(/-?\d*\.?\d+(?:[eE][-+]?\d+)?/g)?.map(Number) || [];
172
+ const xs = nums.filter((_, k) => k % 2 === 0); const ys = nums.filter((_, k) => k % 2 === 1);
173
+ const w = Math.max(4, Math.max(...xs) - Math.min(0, Math.min(...xs))); const h = Math.max(4, Math.max(...ys) - Math.min(0, Math.min(...ys)));
174
+ rect = { x: (s.at?.x || 0) * UNIT, y: (s.at?.y || 0) * UNIT, w: w + 6, h: h + 6 };
175
+ d = s.d;
176
+ } else if (s.kind === 'line' || s.kind === 'arrow' || s.kind === 'underline') {
177
+ let a; let b;
178
+ if (s.kind === 'underline' && s.around) {
179
+ const r = rectOfNode(s.around); if (!r) return err(`形状 ${sid}:around 指向不存在的节点 ${s.around}`);
180
+ a = { x: r.x + 2, y: r.y + r.h - 2 }; b = { x: r.x + r.w - 2, y: r.y + r.h - 2 };
181
+ } else {
182
+ if (!s.at) return err(`形状 ${sid}:${s.kind} 要 at 起点`);
183
+ a = { x: s.at.x * UNIT, y: s.at.y * UNIT };
184
+ if (s.toNode) {
185
+ const r = rectOfNode(s.toNode); if (!r) return err(`形状 ${sid}:toNode 指向不存在的节点 ${s.toNode}`);
186
+ b = { x: r.x + r.w / 2, y: r.y + r.h / 2 };
187
+ } else if (s.to) b = { x: s.to.x * UNIT, y: s.to.y * UNIT };
188
+ else if (s.kind === 'underline') b = { x: a.x + (s.w || 4) * UNIT, y: a.y };
189
+ else return err(`形状 ${sid}:${s.kind} 要 to 或 toNode`);
190
+ }
191
+ const sp = shapePath(s.kind, { to: { x: b.x - a.x, y: b.y - a.y } }, seed);
192
+ rect = { x: Math.min(a.x, b.x) - 6, y: Math.min(a.y, b.y) - 6, w: sp.w, h: sp.h };
193
+ d = sp.d;
194
+ } else {
195
+ let box;
196
+ if (s.around) {
197
+ const r = rectOfNode(s.around); if (!r) return err(`形状 ${sid}:around 指向不存在的节点 ${s.around}`);
198
+ const pad = s.kind === 'rect' ? 8 : 14;
199
+ box = { x: r.x - pad, y: r.y - pad, w: r.w + pad * 2, h: r.h + pad * 2 };
200
+ if (s.kind === 'circle') { const dmax = Math.max(box.w, box.h); box = { x: box.x + (box.w - dmax) / 2, y: box.y + (box.h - dmax) / 2, w: dmax, h: dmax }; }
201
+ } else {
202
+ if (!s.at || !s.w) return err(`形状 ${sid}:${s.kind} 要 at + w(+h)或 around`);
203
+ box = { x: s.at.x * UNIT, y: s.at.y * UNIT, w: s.w * UNIT, h: (s.h || s.w) * UNIT };
204
+ }
205
+ const sp = shapePath(s.kind, { w: box.w, h: box.h }, seed);
206
+ rect = { x: box.x - 6, y: box.y - 6, w: sp.w, h: sp.h };
207
+ d = sp.d;
208
+ }
209
+ shapes.push({ key: sid, rect, d, color: COLORS.includes(color) ? color : 'ink', width });
210
+ }
211
+
212
+ // ── 先分配 id、解析线 ──
213
+ // 线要先于落位算:一条连到真实产物卡的线会改变**主角判断**(手画线 +0.5 焦点分),
214
+ // 那张卡可能因此放大 1.5 倍 —— 落位必须按"线画上去之后"的尺寸避让
215
+ // (08-23 首跑真踩:草图连了一条丝线到纸本站,纸本当场成主角,身体盖住半张图)。
216
+ const board = await readBoard(projectId);
217
+ const known = new Set(Object.keys(board.zones || {}));
218
+ const idOf = new Map(); // local → canvas id
219
+ for (const n of nodes) idOf.set(n.key, `text:a${stamp()}`);
220
+ for (const sh of shapes) idOf.set(sh.key, `scribble:a${stamp()}`);
221
+ const resolveEnd = (raw) => {
222
+ if (idOf.has(raw)) return idOf.get(raw);
223
+ const cid = normalizeCanvasId(raw);
224
+ if (cid && board.objects?.[cid]) return cid;
225
+ if (cid && board.zones?.[cid] !== undefined) return cid;
226
+ return null;
227
+ };
228
+ const bindings = {};
229
+ const badEdges = [];
230
+ for (const e of edgesIn) {
231
+ const from = resolveEnd(e.from); const to = resolveEnd(e.to);
232
+ if (!from || !to || from === to) { badEdges.push(`${e.from}→${e.to}`); continue; }
233
+ bindings[`b:a${stamp()}`] = {
234
+ type: e.type || 'link', from, to, by: 'agent', material: e.material || 'pencil', tag,
235
+ ...(staging ? { staging: true } : {}), ...(e.label ? { label: e.label } : {}),
236
+ };
237
+ }
238
+ // 落位用"线画上之后"的板来估尺寸(主角可能换人)
239
+ const after = { ...board, bindings: { ...(board.bindings || {}), ...bindings } };
240
+
241
+ // ── 宏观落位 ──
242
+ const local = bboxOf([
243
+ ...nodes.map(n => ({ ...pos.get(n.key), w: n.w, h: n.h })),
244
+ ...shapes.map(sh => sh.rect),
245
+ ]);
246
+ let zone = '';
247
+ let anchor = null;
248
+ if (args.near) {
249
+ const nid = normalizeCanvasId(args.near);
250
+ const e = nid ? board.objects?.[nid] : null;
251
+ if (!e || !Number.isFinite(e.x)) return err(`锚点 ${args.near} 还没有座位(read_board 里看不到就锚不上)。`);
252
+ zone = layerOf(nid, e, known);
253
+ anchor = { x: e.x, y: e.y, ...estimateSizeOn(after, nid, e) };
254
+ }
255
+ const obstacles = [];
256
+ let contentBottom = 0;
257
+ for (const [id, e] of Object.entries(board.objects || {})) {
258
+ if (!Number.isFinite(e?.x) || layerOf(id, e, known) !== zone) continue;
259
+ const r = { x: e.x, y: e.y, ...estimateSizeOn(after, id, e) };
260
+ obstacles.push(r); contentBottom = Math.max(contentBottom, r.y + r.h);
261
+ }
262
+ if (!zone) for (const zz of Object.values(board.zones || {})) if (Number.isFinite(zz?.y)) contentBottom = Math.max(contentBottom, zz.y + 240);
263
+ // 尺寸规范:硬上限直接拒(一张图说一件事),推荐上限以上给提醒
264
+ if (local.w > SKETCH_MAX.w || local.h > SKETCH_MAX.h) {
265
+ return err(`这张图太大了(${Math.round(local.w)}x${Math.round(local.h)} 世界像素,上限 ${SKETCH_MAX.w}x${SKETCH_MAX.h}):用户在 80%~100% 缩放下看不全。拆成两张(各自一个 tag,之间用线连),或减节点/缩 w。`);
266
+ }
267
+ // 用户视口(同一层才算;黑板是主窗口时画在他眼前)+ 按他的屏幕算"一屏"
268
+ const vp = getViewpoint(projectId);
269
+ const vpRect = (vp && (vp.layer || '') === zone && vp.camera) ? vp.camera : null;
270
+ const fit = fitFor(vp);
271
+ const spot = findSpot({ w: local.w + 24, h: local.h + 24, near: anchor, obstacles, contentBottom, viewport: vpRect });
272
+ const ox = spot.x - local.x + 12; const oy = spot.y - local.y + 12;
273
+
274
+ // ── 落盘 ──
275
+ const objects = {};
276
+ const common = { z: 1, zone, by: 'agent', tag, ...(staging ? { staging: true } : {}) };
277
+ for (const n of nodes) {
278
+ const p = pos.get(n.key);
279
+ objects[idOf.get(n.key)] = {
280
+ x: Math.round(p.x + ox), y: Math.round(p.y + oy), w: n.w, h: n.h, kind: 'text',
281
+ data: { t: n.text, ...(n.format === 'md' ? { format: 'md' } : {}), font: TEXT_FONTS.includes(n.font) ? n.font : 'pen', size: n.size, color: n.color },
282
+ ...common,
283
+ };
284
+ }
285
+ for (const sh of shapes) {
286
+ objects[idOf.get(sh.key)] = { x: Math.round(sh.rect.x + ox), y: Math.round(sh.rect.y + oy), w: Math.round(sh.rect.w), h: Math.round(sh.rect.h), kind: 'scribble', data: { d: sh.d, color: sh.color, width: sh.width }, ...common };
287
+ }
288
+ const saved = await patchBoard(projectId, { objects, bindings });
289
+ const landed = Object.keys(objects).filter(id => saved.objects?.[id]).length;
290
+ if (!landed) return err('草图被 board 拒了(内容或字段不合法)。');
291
+ turnStamp.count += 1;
292
+ const world = { x: Math.round(local.x + ox), y: Math.round(local.y + oy), w: Math.round(local.w), h: Math.round(local.h) };
293
+ try {
294
+ ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: `画了一张草图 #${tag}` });
295
+ // 镜头提示(前端只在黑板模式跟随;不是劫持,是"黑板是主窗口"的约定)
296
+ ctx?.emit?.({ type: 'board.focus', sessionId: null, tag, rect: world, layer: zone });
297
+ } catch { /* fail-soft */ }
298
+ const lines = [
299
+ `Sketch #${tag} landed${staging ? ' as STAGING (半透明)' : ''}: ${Object.keys(objects).length - shapes.length} nodes, ${shapes.length} shapes, ${Object.keys(bindings).length} lines; layout ${tpl}; at world (${world.x},${world.y}) ${world.w}x${world.h}${anchor ? ` (${spot.side} of ${args.near})` : ' (below current content)'}.`,
300
+ `ids: ${[...idOf].map(([k, v]) => `${k}=${v}`).join(', ')}`,
301
+ ];
302
+ if (badEdges.length) lines.push(`Skipped ${badEdges.length} edge(s) with unknown endpoints: ${badEdges.slice(0, 6).join(', ')}`);
303
+ if (world.w > fit.w || world.h > fit.h) lines.push(`⚠ Bigger than one screen at 80% zoom${fit.screen ? ` (user's screen ${fit.screen.w}x${fit.screen.h}px → ${fit.w}x${fit.h} world px fits)` : ` (${fit.w}x${fit.h} fits)`} — the user will have to pan. Split into two tagged sketches (side by side or stacked) next time.`);
304
+ if (vp?.zoom && vp.zoom < 0.8) lines.push(`User's zoom is ${vp.zoom} (<0.8): text this size is small on their screen. Keep nodes md/lg and say in one line that there is a sketch on the board.`);
305
+ lines.push(spot.side === 'viewport' ? 'Placed inside the user\'s current viewport.' : `Placed ${spot.side === 'bottom' ? 'below current content' : spot.side + ' of the anchor'}${vpRect ? ' (no room in the user\'s viewport)' : ''}.`);
306
+ lines.push(staging
307
+ ? 'Next: look_at_board {tag} to check it, then finish_sketch {tag} (or it commits at turn end). Mention the sketch to the user in one line.'
308
+ : 'Next: look_at_board {tag} to check it.');
309
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
310
+ },
311
+ );
312
+ }
313
+
314
+ export function makeFinishSketchTool({ projectId, ctx }) {
315
+ return tool(
316
+ 'finish_sketch',
317
+ `Commit a staging sketch (make it solid) — or erase it. Pass the #tag from
318
+ sketch_on_board; omit tag to commit everything still staging. erase:true deletes the
319
+ whole group (its notes/shapes/lines; real artifact cards only lose the tag).`,
320
+ {
321
+ tag: z.string().max(40).optional(),
322
+ erase: z.boolean().optional(),
323
+ },
324
+ async ({ tag, erase }) => {
325
+ if (!projectId) return { content: [{ type: 'text', text: 'No project bound.' }], isError: true };
326
+ if (erase) {
327
+ if (!tag) return { content: [{ type: 'text', text: 'erase needs a tag.' }], isError: true };
328
+ const { removed } = await removeByTag(projectId, tag);
329
+ try { ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: `擦掉了草图 #${tag}` }); } catch { /* */ }
330
+ return { content: [{ type: 'text', text: `Erased #${tag}: ${removed} item(s)/line(s) removed.` }] };
331
+ }
332
+ const { committed } = await commitStaging(projectId, { tag: tag || null });
333
+ if (committed) { try { ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: `草图落定${tag ? ` #${tag}` : ''}` }); } catch { /* */ } }
334
+ return { content: [{ type: 'text', text: committed ? `Committed ${committed} item(s)${tag ? ` of #${tag}` : ''}.` : 'Nothing was staging.' }] };
335
+ },
336
+ );
337
+ }
@@ -1,96 +1,96 @@
1
- /**
2
- * mcp/tools/tier-gate.js —— 按账号档位(auth/tier.js)给 MCP 工具套闸的包装器(08-21 晚)。
3
- *
4
- * 为什么是包装器而不是改工具本体:generate-image.js / web-search.js 都是行数棘轮
5
- * 冻结的胖户(拆不抬),而且"谁能用这个工具"跟"这个工具怎么干活"本来就是两层。
6
- * 闸按**项目 owner** 算(同 publish_site / paint_still / roll_film 一把尺),不看请求者、
7
- * 不看会话跑在哪个模型上(用户 08-21 拍板:只做档位隔离,不做模型隔离)。
8
- *
9
- * 用法(mcp/index.js 工具列表里):
10
- * withTierGate(makeGenerateImageTool({...}), 'imageGen', projectId)
11
- * withTierGate(makeWebSearchTool({...}), 'webSearch', projectId) // basic 档附带日上限
12
- */
13
- import { getProject } from '../../../projects/store.js';
14
- import { getUserById } from '../../../auth/users-store.js';
15
- import { can, webSearchDailyCap, DENIAL } from '../../../auth/tier.js';
16
- import { makeRateWindow } from '../../../lib/rate-window.js';
17
- import { checkQuota, imageChargeUsd } from '../../../lib/quota.js';
18
-
19
- /** 项目 owner 的用户对象;无主 / 找不到 → null(tier.can(null) 一律 false,fail-closed) */
20
- export function ownerOfProject(projectId) {
21
- const ownerId = projectId ? getProject(projectId)?.ownerId : null;
22
- return ownerId ? getUserById(ownerId) : null;
23
- }
24
-
25
- // basic 档 web_search 日上限:tavily/baidu 是花钱的,300 轮/天的回合闸拦不住一轮里连搜几十次。
26
- // 内存滑动窗口按 owner 计(重启清零 —— 保护性关卡不是记账,同 rate-window.js 的定位);
27
- // 上限数字住 auth/tier.js 能力表(env NODESIGN_BASIC_WEB_SEARCH_PER_DAY,默认 60)。
28
- const DAY_MS = 24 * 60 * 60 * 1000;
29
- const dailyWindows = new Map(); // cap → rateWindow(cap 可被 env 改,按值分桶)
30
- function takeDaily(ownerId, cap) {
31
- if (!dailyWindows.has(cap)) dailyWindows.set(cap, makeRateWindow({ limit: cap, windowMs: DAY_MS }));
32
- return dailyWindows.get(cap).take(ownerId);
33
- }
34
-
35
- const deny = (text) => ({ content: [{ type: 'text', text }], isError: true });
36
-
37
- /**
38
- * 调用前判一次;返回 null = 放行,否则返回要原样交给 agent 的拒绝结果。
39
- * 拆出来是为了能不起 SDK 直接测(tier-gate.test.js)。
40
- */
41
- export function tierDenial(projectId, capability, toolName) {
42
- return tierDenialForOwner(ownerOfProject(projectId), capability, toolName);
43
- }
44
-
45
- /** 同上,但直接给 owner(测试不用造项目)。 */
46
- export function tierDenialForOwner(owner, capability, toolName) {
47
- if (!can(owner, capability)) {
48
- const why = capability === 'imageGen' ? DENIAL.imageGen
49
- : capability === 'webSearch' ? '这个账号不开放联网搜索。原话转告用户,不要重试。'
50
- : `这个账号档位不开放 ${toolName}。原话转告用户,不要重试。`;
51
- return deny(`${toolName} denied: ${why}`);
52
- }
53
- if (capability === 'webSearch') {
54
- const cap = webSearchDailyCap(owner);
55
- if (cap != null) {
56
- const r = takeDaily(owner.id, cap);
57
- if (!r.ok) {
58
- const hrs = Math.ceil(r.retryAfterMs / 3_600_000);
59
- return deny(`${toolName} denied: basic 档每天最多 ${cap} 次联网搜索,今天已用完(约 ${hrs} 小时后恢复)。原话转告用户,这轮别再搜了。`);
60
- }
61
- }
62
- }
63
- return null;
64
- }
65
-
66
- /**
67
- * 给 SdkMcpToolDefinition 套闸:handler 前先问档位,其余字段原样透传。
68
- * imageGen(08-21 深夜 basic 开放生图)还多两步:调用前过 owner 的日限(checkQuota,admin 不限);
69
- * 成功出图后 ctx.addToolCharge('generate_image', $0.20) 记进本回合账(quota.usedCostToday 同一本)。
70
- */
71
- export function withTierGate(toolDef, capability, projectId, ctx = null) {
72
- return {
73
- ...toolDef,
74
- handler: async (args, extra) => {
75
- const denied = tierDenial(projectId, capability, toolDef.name);
76
- if (denied) return denied;
77
- if (capability === 'imageGen') {
78
- const owner = ownerOfProject(projectId);
79
- const q = checkQuota(owner);
80
- if (!q.ok) return deny(`${toolDef.name} denied: ${DENIAL.imageQuota}`);
81
- }
82
- const result = await toolDef.handler(args, extra);
83
- if (capability === 'imageGen') chargeForImage(result, ctx);
84
- return result;
85
- },
86
- };
87
- }
88
-
89
- /** 出了图才记账(result 非 error 且 content 里有 image 块);返回记了多少(0 = 没记) */
90
- export function chargeForImage(result, ctx) {
91
- const ok = result && !result.isError && Array.isArray(result.content) && result.content.some((b) => b?.type === 'image');
92
- if (!ok) return 0;
93
- const usd = imageChargeUsd();
94
- ctx?.addToolCharge?.('generate_image', usd);
95
- return usd;
96
- }
1
+ /**
2
+ * mcp/tools/tier-gate.js —— 按账号档位(auth/tier.js)给 MCP 工具套闸的包装器(08-21 晚)。
3
+ *
4
+ * 为什么是包装器而不是改工具本体:generate-image.js / web-search.js 都是行数棘轮
5
+ * 冻结的胖户(拆不抬),而且"谁能用这个工具"跟"这个工具怎么干活"本来就是两层。
6
+ * 闸按**项目 owner** 算(同 publish_site / paint_still / roll_film 一把尺),不看请求者、
7
+ * 不看会话跑在哪个模型上(用户 08-21 拍板:只做档位隔离,不做模型隔离)。
8
+ *
9
+ * 用法(mcp/index.js 工具列表里):
10
+ * withTierGate(makeGenerateImageTool({...}), 'imageGen', projectId)
11
+ * withTierGate(makeWebSearchTool({...}), 'webSearch', projectId) // basic 档附带日上限
12
+ */
13
+ import { getProject } from '../../../projects/store.js';
14
+ import { getUserById } from '../../../auth/users-store.js';
15
+ import { can, webSearchDailyCap, DENIAL } from '../../../auth/tier.js';
16
+ import { makeRateWindow } from '../../../lib/rate-window.js';
17
+ import { checkQuota, imageChargeUsd } from '../../../lib/quota.js';
18
+
19
+ /** 项目 owner 的用户对象;无主 / 找不到 → null(tier.can(null) 一律 false,fail-closed) */
20
+ export function ownerOfProject(projectId) {
21
+ const ownerId = projectId ? getProject(projectId)?.ownerId : null;
22
+ return ownerId ? getUserById(ownerId) : null;
23
+ }
24
+
25
+ // basic 档 web_search 日上限:tavily/baidu 是花钱的,300 轮/天的回合闸拦不住一轮里连搜几十次。
26
+ // 内存滑动窗口按 owner 计(重启清零 —— 保护性关卡不是记账,同 rate-window.js 的定位);
27
+ // 上限数字住 auth/tier.js 能力表(env NODESIGN_BASIC_WEB_SEARCH_PER_DAY,默认 60)。
28
+ const DAY_MS = 24 * 60 * 60 * 1000;
29
+ const dailyWindows = new Map(); // cap → rateWindow(cap 可被 env 改,按值分桶)
30
+ function takeDaily(ownerId, cap) {
31
+ if (!dailyWindows.has(cap)) dailyWindows.set(cap, makeRateWindow({ limit: cap, windowMs: DAY_MS }));
32
+ return dailyWindows.get(cap).take(ownerId);
33
+ }
34
+
35
+ const deny = (text) => ({ content: [{ type: 'text', text }], isError: true });
36
+
37
+ /**
38
+ * 调用前判一次;返回 null = 放行,否则返回要原样交给 agent 的拒绝结果。
39
+ * 拆出来是为了能不起 SDK 直接测(tier-gate.test.js)。
40
+ */
41
+ export function tierDenial(projectId, capability, toolName) {
42
+ return tierDenialForOwner(ownerOfProject(projectId), capability, toolName);
43
+ }
44
+
45
+ /** 同上,但直接给 owner(测试不用造项目)。 */
46
+ export function tierDenialForOwner(owner, capability, toolName) {
47
+ if (!can(owner, capability)) {
48
+ const why = capability === 'imageGen' ? DENIAL.imageGen
49
+ : capability === 'webSearch' ? '这个账号不开放联网搜索。原话转告用户,不要重试。'
50
+ : `这个账号档位不开放 ${toolName}。原话转告用户,不要重试。`;
51
+ return deny(`${toolName} denied: ${why}`);
52
+ }
53
+ if (capability === 'webSearch') {
54
+ const cap = webSearchDailyCap(owner);
55
+ if (cap != null) {
56
+ const r = takeDaily(owner.id, cap);
57
+ if (!r.ok) {
58
+ const hrs = Math.ceil(r.retryAfterMs / 3_600_000);
59
+ return deny(`${toolName} denied: basic 档每天最多 ${cap} 次联网搜索,今天已用完(约 ${hrs} 小时后恢复)。原话转告用户,这轮别再搜了。`);
60
+ }
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * 给 SdkMcpToolDefinition 套闸:handler 前先问档位,其余字段原样透传。
68
+ * imageGen(08-21 深夜 basic 开放生图)还多两步:调用前过 owner 的日限(checkQuota,admin 不限);
69
+ * 成功出图后 ctx.addToolCharge('generate_image', $0.20) 记进本回合账(quota.usedCostToday 同一本)。
70
+ */
71
+ export function withTierGate(toolDef, capability, projectId, ctx = null) {
72
+ return {
73
+ ...toolDef,
74
+ handler: async (args, extra) => {
75
+ const denied = tierDenial(projectId, capability, toolDef.name);
76
+ if (denied) return denied;
77
+ if (capability === 'imageGen') {
78
+ const owner = ownerOfProject(projectId);
79
+ const q = checkQuota(owner);
80
+ if (!q.ok) return deny(`${toolDef.name} denied: ${DENIAL.imageQuota}`);
81
+ }
82
+ const result = await toolDef.handler(args, extra);
83
+ if (capability === 'imageGen') chargeForImage(result, ctx);
84
+ return result;
85
+ },
86
+ };
87
+ }
88
+
89
+ /** 出了图才记账(result 非 error 且 content 里有 image 块);返回记了多少(0 = 没记) */
90
+ export function chargeForImage(result, ctx) {
91
+ const ok = result && !result.isError && Array.isArray(result.content) && result.content.some((b) => b?.type === 'image');
92
+ if (!ok) return 0;
93
+ const usd = imageChargeUsd();
94
+ ctx?.addToolCharge?.('generate_image', usd);
95
+ return usd;
96
+ }