@xiaobuyu/nodesign 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/README.md +266 -79
  2. package/README.zh-CN.md +254 -67
  3. package/package.json +2 -2
  4. package/server/api/admin.js +7 -0
  5. package/server/api/assets/docx-page.js +5 -4
  6. package/server/api/assets/helpers.js +26 -0
  7. package/server/api/assets/notes.js +50 -0
  8. package/server/api/assets.js +31 -38
  9. package/server/api/board.js +29 -1
  10. package/server/api/browse.js +3 -2
  11. package/server/api/chatai.js +9 -8
  12. package/server/api/exports/cards.js +7 -6
  13. package/server/api/exports/docx-pdf.js +4 -1
  14. package/server/api/instruction.js +3 -3
  15. package/server/api/local.js +10 -8
  16. package/server/api/projects.js +19 -3
  17. package/server/api/publish.js +3 -2
  18. package/server/api/sessions-rewind.js +220 -0
  19. package/server/api/sessions.js +58 -194
  20. package/server/api/stage.js +35 -0
  21. package/server/api/turn-model-switch.js +109 -0
  22. package/server/api/turn.js +52 -88
  23. package/server/auth/middleware.js +29 -6
  24. package/server/auth/tier.js +27 -7
  25. package/server/auth/users-store.js +14 -1
  26. package/server/engine/agent/actor-trail.js +110 -0
  27. package/server/engine/agent/agent-shared.js +22 -52
  28. package/server/engine/agent/beat-state.js +61 -0
  29. package/server/engine/agent/cast.js +322 -0
  30. package/server/engine/agent/events.js +18 -1
  31. package/server/engine/agent/hooks/beat-gate.js +73 -0
  32. package/server/engine/agent/hooks/failure.js +11 -1
  33. package/server/engine/agent/hooks/lifecycle.js +16 -41
  34. package/server/engine/agent/hooks/post-guidance.js +4 -38
  35. package/server/engine/agent/hooks/post-trim.js +1 -0
  36. package/server/engine/agent/hooks/pre-board-dirty.js +29 -0
  37. package/server/engine/agent/hooks/pre-defaults.js +206 -2
  38. package/server/engine/agent/hooks/pre-injectors.js +19 -2
  39. package/server/engine/agent/hooks/pre-peer-guard.js +88 -0
  40. package/server/engine/agent/hooks/pre-unknown-params.js +82 -0
  41. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +33 -0
  42. package/server/engine/agent/hooks/resident-role-lifecycle.js +100 -0
  43. package/server/engine/agent/hooks/slot-alias.js +30 -0
  44. package/server/engine/agent/hooks/user-prompt-submit.js +208 -47
  45. package/server/engine/agent/hooks.js +80 -14
  46. package/server/engine/agent/isolation.js +25 -2
  47. package/server/engine/agent/memory-config.js +87 -0
  48. package/server/engine/agent/model-context.js +102 -18
  49. package/server/engine/agent/model-table.js +361 -58
  50. package/server/engine/agent/prompts/nodesign-prelude.md +579 -70
  51. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +1 -1
  52. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +26 -9
  53. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +1 -1
  54. package/server/engine/agent/prompts/tools/site-reference.md +15 -4
  55. package/server/engine/agent/role-card.js +175 -0
  56. package/server/engine/agent/rp-mode.js +17 -0
  57. package/server/engine/agent/session-loop.js +55 -32
  58. package/server/engine/agent/stage-status.js +73 -0
  59. package/server/engine/agent/subagent-flight.js +73 -0
  60. package/server/engine/agent/system-prompts.js +41 -0
  61. package/server/engine/agent/tool-input-stream.js +165 -0
  62. package/server/engine/agents/explorer.md +1 -1
  63. package/server/engine/agents/index.js +1 -1
  64. package/server/engine/mcp/actor.js +59 -0
  65. package/server/engine/mcp/capability-gate.js +21 -2
  66. package/server/engine/mcp/index.js +118 -33
  67. package/server/engine/mcp/mode-profile.js +139 -0
  68. package/server/engine/mcp/server-name.js +19 -0
  69. package/server/engine/mcp/tools/artifact-session.js +2 -1
  70. package/server/engine/mcp/tools/browse-computer.js +2 -1
  71. package/server/engine/mcp/tools/browse-find-batch.js +95 -16
  72. package/server/engine/mcp/tools/browse.js +25 -3
  73. package/server/engine/mcp/tools/cast-role.js +167 -0
  74. package/server/engine/mcp/tools/crystallize-skill.js +1 -1
  75. package/server/engine/mcp/tools/draw-trend.js +104 -0
  76. package/server/engine/mcp/tools/edit-board-schema.js +56 -0
  77. package/server/engine/mcp/tools/edit-board.js +569 -0
  78. package/server/engine/mcp/tools/generate-image.js +50 -70
  79. package/server/engine/mcp/tools/helpers/codex-imagegen.js +139 -0
  80. package/server/engine/mcp/tools/helpers/rembg.js +7 -2
  81. package/server/engine/mcp/tools/helpers/shot-pipeline.js +109 -0
  82. package/server/engine/mcp/tools/list-pages.js +11 -5
  83. package/server/engine/mcp/tools/look-at-board.js +4 -3
  84. package/server/engine/mcp/tools/open-sheet.js +242 -0
  85. package/server/engine/mcp/tools/organize-board.js +45 -6
  86. package/server/engine/mcp/tools/paint-still.js +1 -1
  87. package/server/engine/mcp/tools/pin-to-board.js +103 -19
  88. package/server/engine/mcp/tools/read-board.js +123 -16
  89. package/server/engine/mcp/tools/read-tavern-json.js +87 -19
  90. package/server/engine/mcp/tools/report-issue.js +15 -7
  91. package/server/engine/mcp/tools/role-memory.js +83 -0
  92. package/server/engine/mcp/tools/roll-dice.js +82 -0
  93. package/server/engine/mcp/tools/roll-film.js +1 -1
  94. package/server/engine/mcp/tools/screenshot.js +19 -9
  95. package/server/engine/mcp/tools/set-vars.js +150 -0
  96. package/server/engine/mcp/tools/sheet-replan.js +46 -0
  97. package/server/engine/mcp/tools/web-search.js +12 -16
  98. package/server/engine/mcp/tools/write-on-board-flow.js +108 -0
  99. package/server/engine/mcp/tools/write-on-board-place.js +211 -0
  100. package/server/engine/mcp/tools/write-on-board-role-anchor.js +48 -0
  101. package/server/engine/mcp/tools/write-on-board-schema.js +125 -0
  102. package/server/engine/mcp/tools/write-on-board.js +568 -99
  103. package/server/engine/plugins/nodesign/skills/blackboard-rp/SKILL.md +215 -0
  104. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +14 -7
  105. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +44 -5
  106. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +16 -0
  107. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +24 -8
  108. package/server/engine/plugins/nodesign/skills/story-craft/SKILL.md +94 -0
  109. package/server/engine/plugins/nodesign/skills/story-import/SKILL.md +112 -0
  110. package/server/engine/plugins/nodesign/skills/story-intimacy/SKILL.md +72 -0
  111. package/server/engine/plugins/nodesign/skills/story-voice/SKILL.md +66 -0
  112. package/server/engine/plugins/nodesign/skills/story-voice/voices//345/206/267/347/241/254.md +33 -0
  113. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/203/205/346/204/237/347/233/264/347/273/231.md +42 -0
  114. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/204/217/350/257/206/346/265/201.md +42 -0
  115. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/255/246/344/276/240/347/231/275/350/257/235.md +34 -0
  116. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/224/237/346/264/273/346/265/201.md +35 -0
  117. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/231/275/346/217/217.md +37 -0
  118. package/server/engine/plugins/nodesign/skills/story-voice/voices//350/210/236/345/217/260/345/211/247.md +32 -0
  119. package/server/engine/plugins/nodesign/skills/story-voice/voices//351/230/266/346/256/265/345/210/207/346/215/242.md +42 -0
  120. package/server/engine/runs/board-seater.js +272 -0
  121. package/server/engine/runs/board-tasklist.js +90 -48
  122. package/server/engine/runs/turn-relay.js +24 -2
  123. package/server/engine/runs/user-chalk-echo.js +44 -0
  124. package/server/index.js +2 -2
  125. package/server/lib/artifact-target.js +31 -14
  126. package/server/lib/board-anchor.js +49 -0
  127. package/server/lib/board-dirty.js +93 -0
  128. package/server/lib/board-excerpt.js +80 -0
  129. package/server/lib/board-follow.js +111 -0
  130. package/server/lib/board-groups.js +1 -5
  131. package/server/lib/board-hero.js +3 -1
  132. package/server/lib/board-kind-sizes.js +38 -3
  133. package/server/lib/board-lanes.js +58 -0
  134. package/server/lib/board-obstacles.js +80 -0
  135. package/server/lib/board-relations.js +5 -1
  136. package/server/lib/board-sheets.js +467 -0
  137. package/server/lib/board-shelf.js +86 -0
  138. package/server/lib/board-transform.js +62 -0
  139. package/server/lib/canvas-id.js +46 -4
  140. package/server/lib/chalk-flow.js +88 -0
  141. package/server/lib/chalk-rewrite.js +63 -0
  142. package/server/lib/chalk-size-pref.js +48 -0
  143. package/server/lib/chalk.js +42 -3
  144. package/server/lib/export-collect.js +3 -1
  145. package/server/lib/ingress/forward-openai-chat.js +16 -4
  146. package/server/lib/ingress/openai-chat.js +23 -12
  147. package/server/lib/ingress/session-routes.js +3 -3
  148. package/server/lib/ingress/stream-watchdog.js +53 -0
  149. package/server/lib/ingress/upstream-billing.js +15 -0
  150. package/server/lib/ingress/upstream-error-hints.js +36 -0
  151. package/server/lib/issues-store.js +3 -1
  152. package/server/lib/json-preview.js +58 -0
  153. package/server/lib/kinds/docx.js +11 -4
  154. package/server/lib/kinds/index.js +6 -16
  155. package/server/lib/kinds/site.js +40 -7
  156. package/server/lib/model-ingress.js +26 -1
  157. package/server/lib/moderation.js +10 -5
  158. package/server/lib/quota.js +26 -6
  159. package/server/lib/rect.js +50 -0
  160. package/server/lib/rewrite-refs.js +110 -0
  161. package/server/lib/screen.js +36 -0
  162. package/server/lib/sheet-state.js +19 -0
  163. package/server/lib/sketch-array.js +118 -0
  164. package/server/lib/sketch-hatch.js +99 -0
  165. package/server/lib/sketch-layout.js +269 -69
  166. package/server/lib/sketch-shapes.js +152 -0
  167. package/server/lib/sketch-stencils.js +170 -0
  168. package/server/lib/ssrf-guard.js +9 -0
  169. package/server/lib/state-table.js +237 -0
  170. package/server/lib/state-trend.js +94 -0
  171. package/server/lib/state-triggers.js +199 -0
  172. package/server/lib/task-scan.js +18 -0
  173. package/server/lib/tavern-json.js +48 -0
  174. package/server/lib/user-uploads.js +88 -0
  175. package/server/projects/board-limits.js +5 -3
  176. package/server/projects/board-sanitize.js +220 -10
  177. package/server/projects/board-store.js +137 -7
  178. package/server/projects/memory-migration.js +85 -0
  179. package/server/projects/move-entry.js +11 -1
  180. package/server/projects/session-jsonl.js +110 -0
  181. package/server/projects/store.js +56 -3
  182. package/server/projects/ui-config.js +2 -1
  183. package/server/projects/viewpoint-store.js +35 -3
  184. package/server/projects/workspace-slots.js +30 -0
  185. package/server/projects/workspace-templates.js +42 -10
  186. package/server/projects/workspace.js +25 -26
  187. package/server/runtime/capabilities.js +6 -2
  188. package/server/services/rembg-launcher.js +5 -2
  189. package/server/shared/locales.js +70 -0
  190. package/server/shared/messages-en.js +69 -0
  191. package/server/shared/messages.js +65 -0
  192. package/server/ws/broker.js +3 -0
  193. package/server/ws/index.js +43 -3
  194. package/web/README.md +1 -1
  195. package/web/dist/assets/{BrowserWindow-P_vIdh-G.js → BrowserWindow-CcaGRWz5.js} +1 -1
  196. package/web/dist/assets/DeckWindow-BIr2KGFT.js +6 -0
  197. package/web/dist/assets/{DocxWindow-DSAU1-tD.js → DocxWindow-B9KZmU5z.js} +2 -2
  198. package/web/dist/assets/{MermaidBlock-DR1GikCv.js → MermaidBlock-DrfRnz8E.js} +43 -43
  199. package/web/dist/assets/{SiteWindow-92uBuRRR.js → SiteWindow-CJOjCF7d.js} +6 -6
  200. package/web/dist/assets/{abnfDiagram-VCTEODGH-uA8Esix4.js → abnfDiagram-VCTEODGH-CUZbQGIg.js} +1 -1
  201. package/web/dist/assets/{arc-D_khH_xN.js → arc-C1-HSUny.js} +1 -1
  202. package/web/dist/assets/{architectureDiagram-5GKGNRK7-DydJHqOt.js → architectureDiagram-5GKGNRK7-B5nwvu60.js} +2 -2
  203. package/web/dist/assets/{blockDiagram-NRAW4CY4-B7wBbKiW.js → blockDiagram-NRAW4CY4-uPPuFLwR.js} +1 -1
  204. package/web/dist/assets/{c4Diagram-UCG6FXSJ-6mrH0x7Y.js → c4Diagram-UCG6FXSJ-C0mKBFlz.js} +1 -1
  205. package/web/dist/assets/channel-BxpWNOGU.js +1 -0
  206. package/web/dist/assets/{chunk-2Q5K7J3B-Cuikho5Q.js → chunk-2Q5K7J3B-Cs0rlZ4q.js} +1 -1
  207. package/web/dist/assets/{chunk-5VM5RSS4-3-KZ5AUz.js → chunk-5VM5RSS4-DtR63oYb.js} +1 -1
  208. package/web/dist/assets/{chunk-F27PBJKO-Bkwq2gGU.js → chunk-F27PBJKO-B3MRdezJ.js} +1 -1
  209. package/web/dist/assets/{chunk-G27WJ6UU-BLyQzdGE.js → chunk-G27WJ6UU-BtwntwpS.js} +1 -1
  210. package/web/dist/assets/{chunk-JWPE2WC7-B7HxxQds.js → chunk-JWPE2WC7-BHsTe5PD.js} +1 -1
  211. package/web/dist/assets/{chunk-LCL6LL3I-CtXrnUwF.js → chunk-LCL6LL3I-knY15Qdm.js} +1 -1
  212. package/web/dist/assets/{chunk-POPQ4Y6H-dxddZlXt.js → chunk-POPQ4Y6H-C51tgK_I.js} +1 -1
  213. package/web/dist/assets/{chunk-SVP7TREG-DpkmrgHn.js → chunk-SVP7TREG-CnwRZmF_.js} +1 -1
  214. package/web/dist/assets/{chunk-XXDRQBXY-BwZ7RbpQ.js → chunk-XXDRQBXY-BtXzVF-_.js} +1 -1
  215. package/web/dist/assets/classDiagram-DTDB5LWJ-DUEKmDfo.js +1 -0
  216. package/web/dist/assets/classDiagram-v2-JRS7N3AN-DUEKmDfo.js +1 -0
  217. package/web/dist/assets/{cose-bilkent-JH36ORCC-DGFOsMUt.js → cose-bilkent-JH36ORCC-CzWlGP6i.js} +1 -1
  218. package/web/dist/assets/{cynefin-OW5HDTMX-DUwn3ehS.js → cynefin-OW5HDTMX-vK64EGSv.js} +1 -1
  219. package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js → cynefinDiagram-5FMLGOSQ-Bs3g1Gvh.js} +1 -1
  220. package/web/dist/assets/{dagre-3AP2YEHR-CyourLfz.js → dagre-3AP2YEHR-BhgkYe8V.js} +1 -1
  221. package/web/dist/assets/{diagram-S7CK7UJ4-BTMbx2np.js → diagram-S7CK7UJ4-C_6mvD2V.js} +1 -1
  222. package/web/dist/assets/{diagram-UQ7AKVKN-UK94_1-P.js → diagram-UQ7AKVKN-qW7tssUW.js} +1 -1
  223. package/web/dist/assets/{diagram-VSXAHHWV-C7zS-v9v.js → diagram-VSXAHHWV-txJPHpvG.js} +1 -1
  224. package/web/dist/assets/{diagram-VX7I27RA-DKJRa6CK.js → diagram-VX7I27RA-CQTMsCnL.js} +1 -1
  225. package/web/dist/assets/{diagram-Z3DM3KII-kUbAfGAp.js → diagram-Z3DM3KII--cb1hoE6.js} +1 -1
  226. package/web/dist/assets/{ebnfDiagram-PWID7BFC-CVW16_vb.js → ebnfDiagram-PWID7BFC-Da6lDU77.js} +1 -1
  227. package/web/dist/assets/{erDiagram-SSCWMZ5O-BRQMFQ5y.js → erDiagram-SSCWMZ5O-DGCMrsQn.js} +1 -1
  228. package/web/dist/assets/{flowDiagram-A5DVABFB-S34LpLtK.js → flowDiagram-A5DVABFB-GdOL_qno.js} +1 -1
  229. package/web/dist/assets/{ganttDiagram-EL5Y4UJY-BFiqk1-G.js → ganttDiagram-EL5Y4UJY-Cx1F_jBK.js} +4 -4
  230. package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-LZsrdyyv.js → gitGraphDiagram-WWUBYQGX-B5_6JhR3.js} +1 -1
  231. package/web/dist/assets/hand-CuvZZQGW.webp +0 -0
  232. package/web/dist/assets/index-BYKfMnmv.js +2759 -0
  233. package/web/dist/assets/index-CW7VsrVg.css +1 -0
  234. package/web/dist/assets/{infoDiagram-RXCK75RN-0DvN6KIp.js → infoDiagram-RXCK75RN-CjvKpSPA.js} +1 -1
  235. package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CaY3A-Bx.js → ishikawaDiagram-5VMMS53U-CxLoAPhx.js} +1 -1
  236. package/web/dist/assets/{journeyDiagram-EYS64GPL-BBeiaCrj.js → journeyDiagram-EYS64GPL-BfvS1upJ.js} +1 -1
  237. package/web/dist/assets/{kanban-definition-3QL26DDD-ux42DRXQ.js → kanban-definition-3QL26DDD-DKPPu0w4.js} +1 -1
  238. package/web/dist/assets/{layout-CnXiF_NB.js → layout-Dhgck-Zo.js} +1 -1
  239. package/web/dist/assets/{linear-BMW0u6G6.js → linear-DMSQ-aUw.js} +1 -1
  240. package/web/dist/assets/lxgw-nd-app-bold-D1pKA1bR.woff2 +0 -0
  241. package/web/dist/assets/lxgw-nd-app-regular-BDe3d26v.woff2 +0 -0
  242. package/web/dist/assets/lxgw-nd-bold-Bufvlg03.woff2 +0 -0
  243. package/web/dist/assets/lxgw-nd-regular-DQwoI4OP.woff2 +0 -0
  244. package/web/dist/assets/lxgwwenkaiscreen-subset-100-DLPe7tEt.woff2 +0 -0
  245. package/web/dist/assets/lxgwwenkaiscreen-subset-101-d3VsZKWT.woff2 +0 -0
  246. package/web/dist/assets/lxgwwenkaiscreen-subset-102-ClzJuLq6.woff2 +0 -0
  247. package/web/dist/assets/lxgwwenkaiscreen-subset-103-DuDnQ6K-.woff2 +0 -0
  248. package/web/dist/assets/lxgwwenkaiscreen-subset-104-kPGW3Eem.woff2 +0 -0
  249. package/web/dist/assets/lxgwwenkaiscreen-subset-105-BzD54h-0.woff2 +0 -0
  250. package/web/dist/assets/lxgwwenkaiscreen-subset-106-BQ7fIzlx.woff2 +0 -0
  251. package/web/dist/assets/lxgwwenkaiscreen-subset-107-D8tDfFHI.woff2 +0 -0
  252. package/web/dist/assets/lxgwwenkaiscreen-subset-108-tGhkpwep.woff2 +0 -0
  253. package/web/dist/assets/lxgwwenkaiscreen-subset-109-7SMVSrly.woff2 +0 -0
  254. package/web/dist/assets/lxgwwenkaiscreen-subset-110-_uDKrFed.woff2 +0 -0
  255. package/web/dist/assets/lxgwwenkaiscreen-subset-111-Bio825RE.woff2 +0 -0
  256. package/web/dist/assets/lxgwwenkaiscreen-subset-112-D2ee0i-C.woff2 +0 -0
  257. package/web/dist/assets/lxgwwenkaiscreen-subset-113-JPFVDrga.woff2 +0 -0
  258. package/web/dist/assets/lxgwwenkaiscreen-subset-114-BcfxYA4w.woff2 +0 -0
  259. package/web/dist/assets/lxgwwenkaiscreen-subset-115-B1HeeOsd.woff2 +0 -0
  260. package/web/dist/assets/lxgwwenkaiscreen-subset-116-hY12tXpq.woff2 +0 -0
  261. package/web/dist/assets/lxgwwenkaiscreen-subset-117-BvuZwmre.woff2 +0 -0
  262. package/web/dist/assets/lxgwwenkaiscreen-subset-118-tzvs_H7j.woff2 +0 -0
  263. package/web/dist/assets/lxgwwenkaiscreen-subset-119-J2DWuYDN.woff2 +0 -0
  264. package/web/dist/assets/lxgwwenkaiscreen-subset-21-pDubCJV5.woff2 +0 -0
  265. package/web/dist/assets/lxgwwenkaiscreen-subset-22-ChAPMpqp.woff2 +0 -0
  266. package/web/dist/assets/lxgwwenkaiscreen-subset-23-BBx3oNpT.woff2 +0 -0
  267. package/web/dist/assets/lxgwwenkaiscreen-subset-24-CYt-Oagk.woff2 +0 -0
  268. package/web/dist/assets/lxgwwenkaiscreen-subset-25-Cx3j-ZUv.woff2 +0 -0
  269. package/web/dist/assets/lxgwwenkaiscreen-subset-26-B9AfZ3LL.woff2 +0 -0
  270. package/web/dist/assets/lxgwwenkaiscreen-subset-27-Bh2s9MNE.woff2 +0 -0
  271. package/web/dist/assets/lxgwwenkaiscreen-subset-28-C02JPoKP.woff2 +0 -0
  272. package/web/dist/assets/lxgwwenkaiscreen-subset-29-T_tbUGhk.woff2 +0 -0
  273. package/web/dist/assets/lxgwwenkaiscreen-subset-30-DMfyNzhz.woff2 +0 -0
  274. package/web/dist/assets/lxgwwenkaiscreen-subset-31-DFfdzbkg.woff2 +0 -0
  275. package/web/dist/assets/lxgwwenkaiscreen-subset-32-Bs1fz2iW.woff2 +0 -0
  276. package/web/dist/assets/lxgwwenkaiscreen-subset-33-cpB96OkZ.woff2 +0 -0
  277. package/web/dist/assets/lxgwwenkaiscreen-subset-34-BhinPYWX.woff2 +0 -0
  278. package/web/dist/assets/lxgwwenkaiscreen-subset-35-D_njRQqA.woff2 +0 -0
  279. package/web/dist/assets/lxgwwenkaiscreen-subset-36-DqXlLE3t.woff2 +0 -0
  280. package/web/dist/assets/lxgwwenkaiscreen-subset-37-j5LbvMph.woff2 +0 -0
  281. package/web/dist/assets/lxgwwenkaiscreen-subset-38-oO4jWdKm.woff2 +0 -0
  282. package/web/dist/assets/lxgwwenkaiscreen-subset-39-CabsX4rw.woff2 +0 -0
  283. package/web/dist/assets/lxgwwenkaiscreen-subset-4-PatS3YHh.woff2 +0 -0
  284. package/web/dist/assets/lxgwwenkaiscreen-subset-40-yrLg3C3e.woff2 +0 -0
  285. package/web/dist/assets/lxgwwenkaiscreen-subset-41-DBXmCQhc.woff2 +0 -0
  286. package/web/dist/assets/lxgwwenkaiscreen-subset-42-Bqij_UiQ.woff2 +0 -0
  287. package/web/dist/assets/lxgwwenkaiscreen-subset-43-D3kp0Ntc.woff2 +0 -0
  288. package/web/dist/assets/lxgwwenkaiscreen-subset-44-B_BNGvK4.woff2 +0 -0
  289. package/web/dist/assets/lxgwwenkaiscreen-subset-45-CZc0cQWs.woff2 +0 -0
  290. package/web/dist/assets/lxgwwenkaiscreen-subset-46-CHQX5Kd9.woff2 +0 -0
  291. package/web/dist/assets/lxgwwenkaiscreen-subset-47-Ci8gLrHA.woff2 +0 -0
  292. package/web/dist/assets/lxgwwenkaiscreen-subset-48-run3ugD3.woff2 +0 -0
  293. package/web/dist/assets/lxgwwenkaiscreen-subset-49-DgfiUb6u.woff2 +0 -0
  294. package/web/dist/assets/lxgwwenkaiscreen-subset-5-B0CtGbX_.woff2 +0 -0
  295. package/web/dist/assets/lxgwwenkaiscreen-subset-50-CdCG-POr.woff2 +0 -0
  296. package/web/dist/assets/lxgwwenkaiscreen-subset-51-L2okeQZ2.woff2 +0 -0
  297. package/web/dist/assets/lxgwwenkaiscreen-subset-52-BZAqyGJW.woff2 +0 -0
  298. package/web/dist/assets/lxgwwenkaiscreen-subset-53-B0xWqLeU.woff2 +0 -0
  299. package/web/dist/assets/lxgwwenkaiscreen-subset-54-BZVQX-EA.woff2 +0 -0
  300. package/web/dist/assets/lxgwwenkaiscreen-subset-55-093mkIUJ.woff2 +0 -0
  301. package/web/dist/assets/lxgwwenkaiscreen-subset-56-CbhWG8G-.woff2 +0 -0
  302. package/web/dist/assets/lxgwwenkaiscreen-subset-57-pLQuvo6c.woff2 +0 -0
  303. package/web/dist/assets/lxgwwenkaiscreen-subset-58-BHhoY50x.woff2 +0 -0
  304. package/web/dist/assets/lxgwwenkaiscreen-subset-59-D1_6DuIQ.woff2 +0 -0
  305. package/web/dist/assets/lxgwwenkaiscreen-subset-6-BnAelmYU.woff2 +0 -0
  306. package/web/dist/assets/lxgwwenkaiscreen-subset-60-HDBIuLTZ.woff2 +0 -0
  307. package/web/dist/assets/lxgwwenkaiscreen-subset-61-C-c13SeU.woff2 +0 -0
  308. package/web/dist/assets/lxgwwenkaiscreen-subset-62-kHRVOhZ2.woff2 +0 -0
  309. package/web/dist/assets/lxgwwenkaiscreen-subset-63-C0MCsvFv.woff2 +0 -0
  310. package/web/dist/assets/lxgwwenkaiscreen-subset-64-DZekguCj.woff2 +0 -0
  311. package/web/dist/assets/lxgwwenkaiscreen-subset-65-CtP6c3IU.woff2 +0 -0
  312. package/web/dist/assets/lxgwwenkaiscreen-subset-66-DLVWoMSf.woff2 +0 -0
  313. package/web/dist/assets/lxgwwenkaiscreen-subset-67-BVuOnayH.woff2 +0 -0
  314. package/web/dist/assets/lxgwwenkaiscreen-subset-68-DpxQ6Mnx.woff2 +0 -0
  315. package/web/dist/assets/lxgwwenkaiscreen-subset-69-qesBxLmC.woff2 +0 -0
  316. package/web/dist/assets/lxgwwenkaiscreen-subset-70-BBt1xN7h.woff2 +0 -0
  317. package/web/dist/assets/lxgwwenkaiscreen-subset-71-BWqZacwB.woff2 +0 -0
  318. package/web/dist/assets/lxgwwenkaiscreen-subset-72-B1wP_Yst.woff2 +0 -0
  319. package/web/dist/assets/lxgwwenkaiscreen-subset-73-C99T7jNZ.woff2 +0 -0
  320. package/web/dist/assets/lxgwwenkaiscreen-subset-74-DrUTsWBe.woff2 +0 -0
  321. package/web/dist/assets/lxgwwenkaiscreen-subset-75-C4M5XXQe.woff2 +0 -0
  322. package/web/dist/assets/lxgwwenkaiscreen-subset-76-Bq8spVWF.woff2 +0 -0
  323. package/web/dist/assets/lxgwwenkaiscreen-subset-77-D9vNupE2.woff2 +0 -0
  324. package/web/dist/assets/lxgwwenkaiscreen-subset-78-D6th7Gnu.woff2 +0 -0
  325. package/web/dist/assets/lxgwwenkaiscreen-subset-79-bezM85cS.woff2 +0 -0
  326. package/web/dist/assets/lxgwwenkaiscreen-subset-80-DD8pnhG2.woff2 +0 -0
  327. package/web/dist/assets/lxgwwenkaiscreen-subset-81-N6jtSyBs.woff2 +0 -0
  328. package/web/dist/assets/lxgwwenkaiscreen-subset-82-D6c180bZ.woff2 +0 -0
  329. package/web/dist/assets/lxgwwenkaiscreen-subset-83-em17AadM.woff2 +0 -0
  330. package/web/dist/assets/lxgwwenkaiscreen-subset-84-CJ8gdrMK.woff2 +0 -0
  331. package/web/dist/assets/lxgwwenkaiscreen-subset-85-9PPGO1eB.woff2 +0 -0
  332. package/web/dist/assets/lxgwwenkaiscreen-subset-86-CjWPf-u5.woff2 +0 -0
  333. package/web/dist/assets/lxgwwenkaiscreen-subset-87-8D_X_yOC.woff2 +0 -0
  334. package/web/dist/assets/lxgwwenkaiscreen-subset-88-9vtKFPBx.woff2 +0 -0
  335. package/web/dist/assets/lxgwwenkaiscreen-subset-89-BhE5jAKX.woff2 +0 -0
  336. package/web/dist/assets/lxgwwenkaiscreen-subset-90-D2Npfm5E.woff2 +0 -0
  337. package/web/dist/assets/lxgwwenkaiscreen-subset-91-C6Ft1-Eu.woff2 +0 -0
  338. package/web/dist/assets/lxgwwenkaiscreen-subset-97-CaExyQP9.woff2 +0 -0
  339. package/web/dist/assets/lxgwwenkaiscreen-subset-98-CPoVbsUd.woff2 +0 -0
  340. package/web/dist/assets/lxgwwenkaiscreen-subset-99-ZUQZGVgH.woff2 +0 -0
  341. package/web/dist/assets/{mindmap-definition-FBJOCRG2-hUxSpdKn.js → mindmap-definition-FBJOCRG2-CalfL_Kn.js} +1 -1
  342. package/web/dist/assets/{pegDiagram-XKGWAZYB-aCaQPH0w.js → pegDiagram-XKGWAZYB-D9mVWz7l.js} +1 -1
  343. package/web/dist/assets/pending-edit-apply-kwMFWsUe.js +31 -0
  344. package/web/dist/assets/{pieDiagram-E7YTZNPT-CmOEZ8zg.js → pieDiagram-E7YTZNPT-DELzbZNS.js} +1 -1
  345. package/web/dist/assets/{quadrantDiagram-AXDQQJYC-VW6fAhsV.js → quadrantDiagram-AXDQQJYC-TZqp4mfA.js} +1 -1
  346. package/web/dist/assets/{railroadDiagram-O6MQD6OU-CKJ7Ba5K.js → railroadDiagram-O6MQD6OU-DxX59f0i.js} +1 -1
  347. package/web/dist/assets/{requirementDiagram-EFPCY7ZU-pNGkq9Vw.js → requirementDiagram-EFPCY7ZU-DFdsJLIe.js} +1 -1
  348. package/web/dist/assets/{rotate-cw-KAfChbGV.js → rotate-cw-62liz7LX.js} +1 -1
  349. package/web/dist/assets/{sankeyDiagram-P5KCCOFB-BIMmDTfZ.js → sankeyDiagram-P5KCCOFB-ByYcd3fG.js} +1 -1
  350. package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-BBW7wzKY.js → sequenceDiagram-WJ2MYXX4-Bj-ml1jS.js} +1 -1
  351. package/web/dist/assets/{sizeCapture-X5ZJPWSS-DdkXmLYR.js → sizeCapture-X5ZJPWSS-KhsXThyD.js} +1 -1
  352. package/web/dist/assets/{square-dashed-mouse-pointer-CDUlhUP7.js → square-dashed-mouse-pointer-D_VrqVpy.js} +1 -1
  353. package/web/dist/assets/{stateDiagram-HBIQ2CUA-W313Uz16.js → stateDiagram-HBIQ2CUA-BJlpHfWU.js} +1 -1
  354. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-CR2kYAH2.js +1 -0
  355. package/web/dist/assets/{swimlanes-XN3QIQJK-C29TL26K.js → swimlanes-XN3QIQJK-DUB9OJVs.js} +1 -1
  356. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BitOxnG2.js +8 -0
  357. package/web/dist/assets/{timeline-definition-24CTP7MA-CF0JMObX.js → timeline-definition-24CTP7MA-C03NX82A.js} +1 -1
  358. package/web/dist/assets/{vennDiagram-4TSXK5OY-DrDFsaH3.js → vennDiagram-4TSXK5OY-B9pjzngp.js} +1 -1
  359. package/web/dist/assets/{wardleyDiagram-VM6X3IG4-CVrnRIS_.js → wardleyDiagram-VM6X3IG4-UqWq_mcT.js} +1 -1
  360. package/web/dist/assets/{xychartDiagram-S5SC5T6Z-BvkN1zsT.js → xychartDiagram-S5SC5T6Z-D2pDKtOv.js} +1 -1
  361. package/web/dist/index.html +2 -2
  362. package/server/api/memory.js +0 -185
  363. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +0 -583
  364. package/server/engine/mcp/tools/arrange-on-board.js +0 -124
  365. package/server/engine/mcp/tools/create-on-board.js +0 -152
  366. package/server/engine/mcp/tools/edit-sketch.js +0 -167
  367. package/server/engine/mcp/tools/record-decision.js +0 -140
  368. package/server/engine/mcp/tools/relate-on-board.js +0 -112
  369. package/server/engine/mcp/tools/sketch-on-board.js +0 -337
  370. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +0 -235
  371. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +0 -78
  372. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +0 -268
  373. package/web/dist/assets/DeckWindow-BpLv5082.js +0 -16
  374. package/web/dist/assets/channel-DGkccMO1.js +0 -1
  375. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +0 -1
  376. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +0 -1
  377. package/web/dist/assets/index-C0KUf6Wd.css +0 -1
  378. package/web/dist/assets/index-GAamh1p4.js +0 -2152
  379. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  380. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  381. package/web/dist/assets/pending-edit-apply-AcdOQ9Ij.js +0 -31
  382. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +0 -1
  383. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +0 -8
@@ -0,0 +1,152 @@
1
+ /**
2
+ * server/lib/sketch-shapes.js —— sketch 形状构建(2026-08-27 行数棘轮拆件)
3
+ *
4
+ * 从 mcp/tools/write-on-board.js **原样**搬出:入参形状声明 → 局部像素矩形 +
5
+ * 手绘 path + hug 标记。纯计算、不碰磁盘;语义一字未动,搬家原因只有一个 ——
6
+ * write-on-board 顶到 600 行棘轮(642),拆最独立的这一块。
7
+ *
8
+ * 返回 { shapes } 或 { error }(原来的 err() 出口改成 error 字符串,调用方
9
+ * 自己包成工具错误 —— 这层不该知道 MCP 的返回形状)。
10
+ */
11
+
12
+ import { UNIT, shapePath, roughFreePath } from './sketch-layout.js';
13
+ import { stencilStrokes, STENCIL_NAMES } from './sketch-stencils.js';
14
+ import { expandModifiers } from './sketch-array.js';
15
+ import { flattenClosed, hatchD } from './sketch-hatch.js';
16
+
17
+ /** 板书墨色表(zod 不硬拒,认不出落 ink —— 风格不用 -32602 管) */
18
+ export const SKETCH_COLORS = ['ink', 'red', 'pencil', 'brass'];
19
+
20
+ /**
21
+ * @param {Array} shapesIn 工具入参的 shapes
22
+ * @param {object} deps
23
+ * rectOfNode(key) → {x,y,w,h}|null 已落位节点的局部矩形
24
+ * isTaken(id) → boolean id 是否已被节点/形状占用
25
+ * tag 本张图的 tag(手绘抖动的种子)
26
+ * @returns {{shapes: Array<{key,rect,d,color,width,hug}>}|{error: string}}
27
+ */
28
+ export function buildSketchShapes(shapesIn, { rectOfNode, isTaken, tag }) {
29
+ const shapes = [];
30
+ for (let i = 0; i < shapesIn.length; i += 1) {
31
+ const s = shapesIn[i];
32
+ const sid = s.id || `s${i + 1}`;
33
+ if (isTaken(sid) || shapes.some(x => x.key === sid)) return { error: `形状 id「${sid}」跟节点/别的形状重名(形状缺省叫 s1,s2…,节点别用这类名)` };
34
+ const seed = `${tag || 'solo'}:${sid}`;
35
+ // 缺省一律 ink。曾有个 'ink2'(箭头用更深墨色)的死分支:色板里没这个色,
36
+ // 落盘前的白名单每次都把它打回 'ink',意图从未生效 —— 08-28 勘查后铲平。
37
+ const color = SKETCH_COLORS.includes(s.color) ? s.color : 'ink';
38
+ const width = s.width || 2;
39
+ // makeOne:按种子重新生成一份本尊 —— 算子展开的每一份各自的抖(看起来是
40
+ // 手画了 n 遍,不是图章盖了 n 下)。返回 {rect, d},d 局部于 rect。
41
+ let makeOne;
42
+ if (s.kind === 'path') {
43
+ // 单位单义(2026-08-30 画图探针案):d 的坐标与 at/w/h **同一套格**(1 格 =
44
+ // 24px,小数随意)。此前 schema 写着"d 是像素"而 at 是格 —— 同一个形状两套
45
+ // 单位,真会话里整幅画的 path 墨迹只有别的图元 1/24 大,agent 误诊成
46
+ // 「path 不渲染」,从此只敢用 rect/circle 拼图,曲线全灭。
47
+ if (!s.d || !/^[\dMLQCZ ,.\-eE]+$/.test(s.d)) return { error: `形状 ${sid}:path 只收大写 M/L/Q/C/Z 与数字(绝对坐标,单位=格)` };
48
+ const nums = s.d.match(/-?\d*\.?\d+(?:[eE][-+]?\d+)?/g)?.map(Number) || [];
49
+ if (!nums.length) return { error: `形状 ${sid}:path 里没有坐标` };
50
+ const xs = nums.filter((_, k) => k % 2 === 0); const ys = nums.filter((_, k) => k % 2 === 1);
51
+ const P = 6;
52
+ const minX = Math.min(...xs) * UNIT; const minY = Math.min(...ys) * UNIT;
53
+ const w = Math.max(4, (Math.max(...xs) - Math.min(...xs)) * UNIT);
54
+ const h = Math.max(4, (Math.max(...ys) - Math.min(...ys)) * UNIT);
55
+ // ×UNIT 并平移到墨迹包围盒(pad 6)—— rect 跟墨走,选中框不再套住一片空气
56
+ let k = 0;
57
+ const local = s.d.replace(/-?\d*\.?\d+(?:[eE][-+]?\d+)?/g, (n) => {
58
+ const v = Number(n) * UNIT - ((k++ % 2 === 0) ? minX : minY) + P;
59
+ return String(Math.round(v * 10) / 10);
60
+ });
61
+ const rect0 = { x: (s.at?.x || 0) * UNIT + minX - P, y: (s.at?.y || 0) * UNIT + minY - P, w: w + P * 2, h: h + P * 2 };
62
+ let fillPolys = null;
63
+ if (s.fill) {
64
+ const fc = flattenClosed(local);
65
+ if (!fc.closedAny) return { error: `形状 ${sid}:fill 只对闭合轮廓生效(Z 收尾)—— 这条 path 没有闭合的子路径` };
66
+ fillPolys = fc.polys;
67
+ }
68
+ makeOne = (sx) => ({ rect: { ...rect0 }, d: roughFreePath(local, seed + sx) + (fillPolys ? ' ' + hatchD(fillPolys, seed + sx) : '') });
69
+ } else if (s.kind === 'stencil') {
70
+ // 词汇表(刀①):参数化简笔画 —— 名字 + 位置 + 尺寸,一笔一画归机器
71
+ if (!STENCIL_NAMES.includes(s.name)) return { error: `形状 ${sid}:stencil 认识这些名字 —— ${STENCIL_NAMES.join(', ')}` };
72
+ if (!s.at || !s.w) return { error: `形状 ${sid}:stencil 要 at + w(格;h 可省 = 按图形比例)` };
73
+ const st = stencilStrokes(s.name, s.w * UNIT, s.h ? s.h * UNIT : null);
74
+ const flipped = s.flip
75
+ ? st.strokes.map((d) => { let k2 = 0; return d.replace(/-?\d*\.?\d+/g, (n) => ((k2++ % 2 === 0) ? String(Math.round((st.w - Number(n)) * 10) / 10) : n)); })
76
+ : st.strokes;
77
+ const rect0 = { x: s.at.x * UNIT - 6, y: s.at.y * UNIT - 6, w: st.w + 12, h: st.h + 12 };
78
+ let stFill = null;
79
+ if (s.fill) {
80
+ const fc = flattenClosed(flipped.join(' '));
81
+ if (!fc.closedAny) return { error: `形状 ${sid}:这个 stencil 没有闭合轮廓,fill 排不了线` };
82
+ stFill = fc.polys;
83
+ }
84
+ makeOne = (sx) => ({
85
+ rect: { ...rect0 },
86
+ d: flipped.map((d2, j) => roughFreePath(d2, `${seed}${sx}:${j}`)).join(' ')
87
+ + (stFill ? ' ' + hatchD(stFill, seed + sx) : ''),
88
+ });
89
+ } else if (s.kind === 'line' || s.kind === 'arrow' || s.kind === 'underline') {
90
+ if (s.fill) return { error: `形状 ${sid}:${s.kind} 是一笔线,没有内部可填 —— fill 只给闭合形状` };
91
+ let a; let b;
92
+ if (s.kind === 'underline' && s.around) {
93
+ const r = rectOfNode(s.around); if (!r) return { error: `形状 ${sid}:around 指向不存在的节点 ${s.around}` };
94
+ a = { x: r.x + 2, y: r.y + r.h - 2 }; b = { x: r.x + r.w - 2, y: r.y + r.h - 2 };
95
+ } else {
96
+ if (!s.at) return { error: `形状 ${sid}:${s.kind} 要 at 起点` };
97
+ a = { x: s.at.x * UNIT, y: s.at.y * UNIT };
98
+ if (s.toNode) {
99
+ const r = rectOfNode(s.toNode); if (!r) return { error: `形状 ${sid}:toNode 指向不存在的节点 ${s.toNode}` };
100
+ b = { x: r.x + r.w / 2, y: r.y + r.h / 2 };
101
+ } else if (s.to) b = { x: s.to.x * UNIT, y: s.to.y * UNIT };
102
+ else if (s.kind === 'underline') b = { x: a.x + (s.w || 4) * UNIT, y: a.y };
103
+ else return { error: `形状 ${sid}:${s.kind} 要 to 或 toNode` };
104
+ }
105
+ makeOne = (sx) => {
106
+ const sp = shapePath(s.kind, { to: { x: b.x - a.x, y: b.y - a.y } }, seed + sx);
107
+ return { rect: { x: Math.min(a.x, b.x) - 6, y: Math.min(a.y, b.y) - 6, w: sp.w, h: sp.h }, d: sp.d };
108
+ };
109
+ } else {
110
+ let box;
111
+ if (s.around) {
112
+ const r = rectOfNode(s.around); if (!r) return { error: `形状 ${sid}:around 指向不存在的节点 ${s.around}` };
113
+ const pad = s.kind === 'rect' ? 8 : 14;
114
+ box = { x: r.x - pad, y: r.y - pad, w: r.w + pad * 2, h: r.h + pad * 2 };
115
+ 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 }; }
116
+ } else {
117
+ if (!s.at || !s.w) return { error: `形状 ${sid}:${s.kind} 要 at + w(+h)或 around` };
118
+ box = { x: s.at.x * UNIT, y: s.at.y * UNIT, w: s.w * UNIT, h: (s.h || s.w) * UNIT };
119
+ }
120
+ let boxFill = null;
121
+ if (s.fill) {
122
+ const P2 = 6;
123
+ if (s.kind === 'rect') {
124
+ boxFill = [[{ x: P2, y: P2 }, { x: P2 + box.w, y: P2 }, { x: P2 + box.w, y: P2 + box.h }, { x: P2, y: P2 + box.h }]];
125
+ } else {
126
+ const rx = box.w / 2; const ry = (s.kind === 'circle' ? box.w : box.h) / 2;
127
+ const pts = [];
128
+ for (let a2 = 0; a2 < 24; a2 += 1) {
129
+ const t2 = (a2 / 24) * Math.PI * 2;
130
+ pts.push({ x: P2 + rx + Math.cos(t2) * rx, y: P2 + ry + Math.sin(t2) * ry });
131
+ }
132
+ boxFill = [pts];
133
+ }
134
+ }
135
+ makeOne = (sx) => {
136
+ const sp = shapePath(s.kind, { w: box.w, h: box.h }, seed + sx);
137
+ return { rect: { x: box.x - 6, y: box.y - 6, w: sp.w, h: sp.h }, d: sp.d + (boxFill ? ' ' + hatchD(boxFill, seed + sx) : '') };
138
+ };
139
+ }
140
+ // 算子展开(刀④):repeat/ring/mirror/scatter —— 等距和对称归机器
141
+ const pieces = expandModifiers(s, makeOne, sid);
142
+ if (pieces.error) return { error: pieces.error };
143
+ // 包着节点的记号记 hug:落盘后编辑侧挪节点它跟着走(散架病的另一半在 edit-board)
144
+ const hugKey = (s.around && ['rect', 'ellipse', 'circle', 'underline'].includes(s.kind)) ? s.around : null;
145
+ for (const pc of pieces) {
146
+ if (isTaken(pc.key) || shapes.some((x) => x.key === pc.key)) return { error: `形状 id「${pc.key}」撞名(算子副本叫 <id>-2, <id>-3…,别的形状别占这些名)` };
147
+ shapes.push({ key: pc.key, rect: pc.rect, d: pc.d, color, width, hug: hugKey });
148
+ }
149
+ if (shapes.length > 120) return { error: `形状展开后超过 120 件(第 ${i + 1} 个展开完是 ${shapes.length})—— 阵列收敛一点,或拆成几张图` };
150
+ }
151
+ return { shapes };
152
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * server/lib/sketch-stencils.js —— 图形词汇表(2026-08-30 画图能力线刀①)
3
+ *
4
+ * 参数化简笔画:agent 说「这里一座房子、两格宽」,机器展开成手工调好的 path。
5
+ * 跟落位那条线同一个哲学 —— **语义归 agent,几何外包**:画什么、放哪、多大
6
+ * 它定,一笔一画的坐标不用它算。
7
+ *
8
+ * ## 词表纪律
9
+ *
10
+ * 词表就是 agent 的词汇量(风格命名那条经验):几十个精选 > 无限自由。入选判据
11
+ * 是「侦探板 / 跑团 / 示意图里真会反复用到」。每个 stencil 是 0..100 规范框里的
12
+ * 一组笔画(多条 d),使用时缩放到目标尺寸、逐笔过手绘抖动 —— 跟整个画图族
13
+ * 同一支笔。新增一个词 = 加一条数据,代码零改动。
14
+ *
15
+ * ⚠️ 笔画坐标手工调过、**逐个看过渲染**(scripts 探针)。改的时候别信脑内想象,
16
+ * 渲染出来看 —— 画错的 path 比没有更坏。
17
+ */
18
+
19
+ /** 每个词:strokes = 0..100 框内的笔画(大写绝对 M/L/Q/C/Z);aspect = 高/宽 */
20
+ export const STENCILS = {
21
+ person: {
22
+ aspect: 1.6,
23
+ strokes: [
24
+ 'M50,4 C60,4 66,11 66,20 C66,29 60,36 50,36 C40,36 34,29 34,20 C34,11 40,4 50,4 Z',
25
+ 'M50,36 L50,64',
26
+ 'M22,52 L50,42 L78,52',
27
+ 'M50,64 L32,98',
28
+ 'M50,64 L68,98',
29
+ ],
30
+ },
31
+ house: {
32
+ aspect: 1,
33
+ strokes: [
34
+ 'M14,48 L50,14 L86,48',
35
+ 'M22,48 L22,92 L78,92 L78,48',
36
+ 'M44,92 L44,66 L58,66 L58,92',
37
+ ],
38
+ },
39
+ tree: {
40
+ aspect: 1.3,
41
+ strokes: [
42
+ 'M50,96 L50,58',
43
+ 'M50,58 C28,62 16,46 28,34 C26,16 52,8 60,22 C78,20 84,42 68,48 C66,58 56,62 50,58 Z',
44
+ ],
45
+ },
46
+ pine: {
47
+ aspect: 1.5,
48
+ strokes: [
49
+ 'M50,4 L30,36 L42,36 L24,64 L38,64 L18,92 L82,92 L62,64 L76,64 L58,36 L70,36 Z',
50
+ 'M50,92 L50,100',
51
+ ],
52
+ },
53
+ mountain: {
54
+ aspect: 0.6,
55
+ strokes: [
56
+ 'M2,96 L34,28 L50,58 L66,10 L98,96',
57
+ 'M58,34 L66,22 L74,36',
58
+ ],
59
+ },
60
+ boat: {
61
+ aspect: 0.9,
62
+ strokes: [
63
+ 'M18,74 L82,74 L70,92 L30,92 Z',
64
+ 'M52,74 L52,12',
65
+ 'M52,16 L84,68 L52,68 Z',
66
+ 'M48,24 L24,68 L48,68 Z',
67
+ ],
68
+ },
69
+ flag: {
70
+ aspect: 1.4,
71
+ strokes: [
72
+ 'M32,4 L32,98',
73
+ 'M32,8 Q54,16 74,10 L74,34 Q54,40 32,32 Z',
74
+ ],
75
+ },
76
+ star: {
77
+ aspect: 1,
78
+ strokes: [
79
+ 'M50,6 L60,36 L92,37 L66,56 L76,88 L50,69 L24,88 L34,56 L8,37 L40,36 Z',
80
+ ],
81
+ },
82
+ moon: {
83
+ aspect: 1.2,
84
+ strokes: [
85
+ 'M62,6 C36,14 24,42 34,66 C41,83 55,93 70,92 C50,84 40,62 46,42 C50,28 55,14 62,6 Z',
86
+ ],
87
+ },
88
+ sun: {
89
+ aspect: 1,
90
+ strokes: [
91
+ 'M50,28 C63,28 72,37 72,50 C72,63 63,72 50,72 C37,72 28,63 28,50 C28,37 37,28 50,28 Z',
92
+ 'M50,2 L50,16 M50,84 L50,98 M2,50 L16,50 M84,50 L98,50',
93
+ 'M17,17 L27,27 M73,73 L83,83 M83,17 L73,27 M27,73 L17,83',
94
+ ],
95
+ },
96
+ cloud: {
97
+ aspect: 0.55,
98
+ strokes: [
99
+ 'M24,78 C6,78 6,54 24,54 C24,32 52,26 60,42 C70,26 94,36 90,54 C104,58 100,78 84,78 Z',
100
+ ],
101
+ },
102
+ scroll: {
103
+ aspect: 1.2,
104
+ strokes: [
105
+ 'M18,18 C18,10 82,10 82,18 C82,26 18,26 18,18 Z',
106
+ 'M24,26 L24,80 M76,26 L76,80',
107
+ 'M18,86 C18,78 82,78 82,86 C82,94 18,94 18,86 Z',
108
+ 'M36,42 L64,42',
109
+ 'M36,54 L64,54',
110
+ 'M36,66 L58,66',
111
+ ],
112
+ },
113
+ sword: {
114
+ aspect: 1.6,
115
+ strokes: [
116
+ 'M50,2 L43,60 L50,70 L57,60 Z',
117
+ 'M34,62 L66,62',
118
+ 'M50,70 L50,92',
119
+ 'M44,95 L56,95',
120
+ ],
121
+ },
122
+ dice: {
123
+ aspect: 1,
124
+ strokes: [
125
+ 'M20,20 L80,20 L80,80 L20,80 Z',
126
+ 'M35,33 C38,33 38,37 35,37 C32,37 32,33 35,33 Z',
127
+ 'M50,48 C53,48 53,52 50,52 C47,52 47,48 50,48 Z',
128
+ 'M65,63 C68,63 68,67 65,67 C62,67 62,63 65,63 Z',
129
+ ],
130
+ },
131
+ heart: {
132
+ aspect: 0.95,
133
+ strokes: [
134
+ 'M50,88 C16,58 22,20 50,36 C78,20 84,58 50,88 Z',
135
+ ],
136
+ },
137
+ door: {
138
+ aspect: 1.6,
139
+ strokes: [
140
+ 'M26,96 L26,26 C26,10 74,10 74,26 L74,96',
141
+ 'M26,96 L74,96',
142
+ 'M62,56 C65,56 65,60 62,60 C59,60 59,56 62,56 Z',
143
+ ],
144
+ },
145
+ };
146
+
147
+ export const STENCIL_NAMES = Object.keys(STENCILS);
148
+
149
+ /**
150
+ * 展开一个词:缩放到 w×h(像素),返回未抖动的笔画(抖动由调用方按 seed 加 ——
151
+ * 阵列的每份要各自的抖)。
152
+ * @returns {{ strokes: string[], w:number, h:number }}
153
+ */
154
+ export function stencilStrokes(name, wPx, hPx = null) {
155
+ const st = STENCILS[name];
156
+ if (!st) return null;
157
+ const w = Math.max(8, wPx);
158
+ const h = Math.max(8, hPx || w * st.aspect);
159
+ // ⚠️ 奇偶计数器**每条笔画各自数**:跨笔画累计的话,一条 d 的数字个数为奇数
160
+ // 就把后面所有笔画的 x/y 错位成 y/x
161
+ return { strokes: st.strokes.map((d) => scaleD(d, w / 100, h / 100)), w, h };
162
+ }
163
+
164
+ function scaleD(d, sx, sy) {
165
+ let k = 0;
166
+ return d.replace(/-?\d*\.?\d+/g, (n) => {
167
+ const v = Number(n) * ((k++ % 2 === 0) ? sx : sy);
168
+ return String(Math.round(v * 10) / 10);
169
+ }).replace(/,/g, ' ');
170
+ }
@@ -267,6 +267,14 @@ export function blockReason(rawIp) {
267
267
  /** 只允许这两个 scheme 发起网络请求;data:/blob: 不出网所以另算 */
268
268
  const NET_SCHEMES = new Set(['http:', 'https:']);
269
269
  const OFFLINE_SCHEMES = new Set(['data:', 'blob:', 'about:']);
270
+ /**
271
+ * Chromium 内置 PDF viewer(component extension,id 硬编码在 Chromium 源里)。
272
+ * 它渲染 PDF 时会加载自己的静态资源(pdf_embedder.css 等)——不出网、出不了扩展
273
+ * 包目录,跟 file:// 不是一个量级。不放行的话浏览器里所有 PDF 都渲染不出来
274
+ * (08-24 案)。⚠️ 只放这个 id:别的 chrome-extension:// 照旧拒 + 记账,
275
+ * 将来谁 --load-extension 也不会自动继承放行。
276
+ */
277
+ const PDF_VIEWER_ORIGIN = 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/';
270
278
 
271
279
  /**
272
280
  * 一个 URL 能不能让浏览器去。**解析 DNS 后逐个 IP 判**。
@@ -297,6 +305,7 @@ export async function checkUrl(raw, { timeoutMs = 4000 } = {}) {
297
305
  try { u = new URL(String(raw)); } catch { return { ok: false, reason: `not a valid URL: ${raw}` }; }
298
306
 
299
307
  if (OFFLINE_SCHEMES.has(u.protocol)) return { ok: true, ips: [] }; // 不出网
308
+ if (String(raw).startsWith(PDF_VIEWER_ORIGIN)) return { ok: true, ips: [] }; // 内置 PDF viewer 自身资源,不出网
300
309
  if (!NET_SCHEMES.has(u.protocol)) {
301
310
  return { ok: false, reason: `scheme ${u.protocol} is not allowed (only http/https; file:// would read local files)` };
302
311
  }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * server/lib/state-table.js —— 状态表:板上那张 `| 键 | 值 |` 的解析与改写
3
+ * (2026-08-30)
4
+ *
5
+ * ## 为什么真相是一张 markdown 表,不是一份 JSON
6
+ *
7
+ * 演出里"会变、要记得"的东西(好感度 / 时间 / 线索 / 体力)此前住在状态板的正文里,
8
+ * 是一段自由文本,每拍靠 agent 整卡重写。想让程序参与(下一轮提醒它、将来按条件
9
+ * 触发)就得有结构。
10
+ *
11
+ * 结构可以另起一份存储(`.nd/vars.json` 之类),但那条路被两件事否掉:
12
+ * - `.nd/` 在 gitignore 里(workspace-templates.js)。存那儿 = 用户在画布上看不见、
13
+ * git 里没历史、`revertWorkspace` 回滚正文却不回滚它 →「旧故事配新数值」,
14
+ * 而且没有任何东西会报这个错。
15
+ * - 另起一份 = 同一件事两份真相。状态板的字**已经**在 `notes/板书/` 里、已经进 git、
16
+ * 已经每轮 commit。再存一份就是这条线上翻过最多次车的那种账。
17
+ *
18
+ * 所以:**真相仍是那条板书文件,只是约定它正文里有一张固定格式的表。**
19
+ * 服务端只读解析出派生值喂每轮状态块;`set_vars` 做的是"精确改表里那一格",
20
+ * 不是重写整张卡。文件内容 = 显示内容 = agent Read 到的内容,三者逐字相同。
21
+ *
22
+ * ## 定位判据:tag,不是文件名、不是隐形标记
23
+ *
24
+ * ⛔ **不能用藏在 frontmatter 里的自造键**:`renderChalk` 只回写白名单键,
25
+ * 一次 `edit_board set_text` 就把它整行丢掉,而且不报错。
26
+ * ⛔ **不能用固定文件名**(`状态表.md` 那种):chalk 目录的文件名是时间戳,
27
+ * `recentChalk` 和首页摘句都拿"文件名倒序"当"最近"判据 —— 一个 CJK 开头的
28
+ * 固定名在字典序里排在所有数字时间戳之后,倒序后永远第一,
29
+ * 结果是**首页每个演出项目的卡常年显示「| 键 | 值 |」**。
30
+ * ✅ 用 `tag: 状态表`(专用 tag,跟状态板那个**组** tag 分开)。tag 是少数能双向
31
+ * 往返的字段(parseChalk 读、renderChalk 写、set_text 保留),改名搬位都不影响。
32
+ */
33
+
34
+ import path from 'node:path';
35
+ import { promises as fs } from 'node:fs';
36
+ import { CHALK_DIR, parseChalk } from './chalk.js';
37
+
38
+ /** 状态表专用 tag。⚠️ 跟 blackboard-rp 的「状态板」组 tag 是两个东西:
39
+ * 那个是一**组**卡(PC卡/场景/明骰表)的组名,这个专指承载键值表的那一条。 */
40
+ export const STATE_TABLE_TAG = '状态表';
41
+
42
+ /** 值的上限:一格里塞长文是把表当笔记本用,那是板书正文的活 */
43
+ export const VALUE_MAX = 120;
44
+ /** 键的上限与字符集:键要能当标识符用(将来触发器按它寻址),别收任意文本 */
45
+ export const KEY_MAX = 40;
46
+ const KEY_RE = /^[\p{L}\p{N}_][\p{L}\p{N}_·-]{0,39}$/u;
47
+
48
+ /**
49
+ * 剥掉围栏块再找表。
50
+ * 板书里最常见的围栏是 ```nd:controls(控件声明),里面完全可能出现竖线。
51
+ * 判据抄 board-excerpt.js 的 excerptOf —— 那儿已经为同一个理由剥过一次。
52
+ */
53
+ export function stripFences(body) {
54
+ return String(body || '')
55
+ .replace(/^[ \t]*```[\s\S]*?^[ \t]*```[ \t]*$|^[ \t]*```[\s\S]*$/gm, '');
56
+ }
57
+
58
+ const SEP_RE = /^\s*\|?[\s:-]*-[-\s:|]*\|?\s*$/; // | --- | --- |
59
+ const HEAD_RE = /^\s*\|\s*键\s*\|\s*值\s*\|\s*$/;
60
+ const FENCE_RE = /^[ \t]*```/;
61
+
62
+ /**
63
+ * 逐行标出「这一行在围栏里吗」。
64
+ *
65
+ * ⚠️ 不能用 stripFences 之后的**文本**去比对(第一版就是这么写的,当场翻车):
66
+ * 围栏里那行 `| 键 | 值 |` 和真表头**逐字相同**,按内容判membership 两行都算"活着",
67
+ * 于是一张表被数成两张。判据必须落在**行号**上,不是行的内容。
68
+ * 未闭合的围栏一路吃到末尾(跟 board-excerpt 的取舍一致:宁可少认,不能把源码当数据)。
69
+ */
70
+ function fenceMask(lines) {
71
+ const mask = new Array(lines.length).fill(false);
72
+ let open = false;
73
+ for (let i = 0; i < lines.length; i += 1) {
74
+ if (FENCE_RE.test(lines[i])) { mask[i] = true; open = !open; continue; }
75
+ mask[i] = open;
76
+ }
77
+ return mask;
78
+ }
79
+
80
+ function cells(line) {
81
+ return line.replace(/^\s*\|/, '').replace(/\|\s*$/, '').split('|').map((c) => c.trim());
82
+ }
83
+
84
+ /**
85
+ * 从正文里解析状态表。
86
+ *
87
+ * @returns {{ ok:true, start:number, end:number, rows:Array<{key,value}> }
88
+ * | { ok:false, error:string }}
89
+ * start/end 是表在**原始行数组**里的下标区间(含表头与分隔行),改写时按它回填。
90
+ * ok:false 的两种:没有表、或者有歧义(多张表 / 重复键)—— 一律**大声**,
91
+ * 不 fail-soft 成空表。空表和"读不懂"是两件事,混在一起就是那条最贵的教训。
92
+ */
93
+ export function parseStateTable(body) {
94
+ const raw = String(body || '').split(/\r?\n/);
95
+ const inFence = fenceMask(raw);
96
+ const heads = [];
97
+ for (let i = 0; i < raw.length; i += 1) {
98
+ if (!HEAD_RE.test(raw[i])) continue;
99
+ if (inFence[i]) continue; // 表头在围栏里 → 不算表
100
+ heads.push(i);
101
+ }
102
+ if (!heads.length) return { ok: false, error: 'no-table' };
103
+ if (heads.length > 1) {
104
+ return { ok: false, error: `这条板书里有 ${heads.length} 张「| 键 | 值 |」表 —— 状态表只能有一张,另一张挪到别的板书上去(明骰表用别的表头)。` };
105
+ }
106
+ const start = heads[0];
107
+ if (!SEP_RE.test(raw[start + 1] || '')) {
108
+ return { ok: false, error: '「| 键 | 值 |」下面缺分隔行(`|---|---|`),这不是一张合法的 markdown 表。' };
109
+ }
110
+ const rows = []; const seen = new Map();
111
+ let end = start + 1;
112
+ for (let i = start + 2; i < raw.length; i += 1) {
113
+ const line = raw[i];
114
+ if (!/^\s*\|/.test(line)) break; // 表到此为止
115
+ const c = cells(line);
116
+ if (c.length < 2) break;
117
+ const key = c[0]; const value = c[1];
118
+ if (!key) { end = i; continue; } // 空键行:留着但不算数据
119
+ if (seen.has(key)) {
120
+ return { ok: false, error: `表里「${key}」出现了两次(第 ${seen.get(key) + 1} 行和第 ${i + 1} 行)—— 键必须唯一,先把重复的那行删掉。` };
121
+ }
122
+ seen.set(key, i);
123
+ rows.push({ key, value });
124
+ end = i;
125
+ }
126
+ return { ok: true, start, end, rows };
127
+ }
128
+
129
+ /** 值清洗:压一行、去掉会把表撑破的字符。钳了要说(返回 clamped 让调用方如实报) */
130
+ export function sanitizeValue(v) {
131
+ const src = String(v ?? '');
132
+ const s = src
133
+ // 控制字符(含换行、制表、行分隔符)先抹平:一格里塞多行会把表拆散,
134
+ // 而且拆散之后解析器只会少读几行,不会报错 —— 这类静默降级要堵在写口。
135
+ .replace(/[\u0000-\u001f\u007f\u2028\u2029]/g, ' ')
136
+ .replace(/\|/g, '/') // 竖线会当场炸表
137
+ .replace(/\s+/g, ' ')
138
+ .trim();
139
+ return { value: s.slice(0, VALUE_MAX), clamped: s.length > VALUE_MAX || src.includes('|') };
140
+ }
141
+
142
+ export function validateKey(k) {
143
+ const s = String(k ?? '').trim();
144
+ if (!s) return '键是空的';
145
+ if (s.length > KEY_MAX) return `键「${s.slice(0, 12)}…」超过 ${KEY_MAX} 字`;
146
+ if (!KEY_RE.test(s)) return `键「${s}」含不能用的字符(只收字母数字、中文、下划线、连字符、间隔号,且不能以连字符开头)`;
147
+ return null;
148
+ }
149
+
150
+ /** 把一张表渲染回 markdown 行(列宽不对齐 —— 对齐了 diff 会整片变) */
151
+ export function renderRows(rows) {
152
+ return ['| 键 | 值 |', '| --- | --- |', ...rows.map((r) => `| ${r.key} | ${r.value} |`)];
153
+ }
154
+
155
+ /**
156
+ * 在正文里改/加若干个键,返回新正文与实际发生的变化。
157
+ * 表不存在或看不懂时**抛** —— 调用方负责把话说清楚。
158
+ */
159
+ export function applyVars(body, vars) {
160
+ const parsed = parseStateTable(body);
161
+ if (!parsed.ok) {
162
+ const err = new Error(parsed.error === 'no-table'
163
+ ? '这条板书里没有「| 键 | 值 |」表。'
164
+ : parsed.error);
165
+ err.code = parsed.error === 'no-table' ? 'NO_TABLE' : 'BAD_TABLE';
166
+ throw err;
167
+ }
168
+ const rows = parsed.rows.map((r) => ({ ...r }));
169
+ const changed = []; const added = []; const clamped = [];
170
+ for (const [rawKey, rawVal] of Object.entries(vars)) {
171
+ const key = String(rawKey).trim();
172
+ const bad = validateKey(key);
173
+ if (bad) { const e = new Error(bad); e.code = 'BAD_KEY'; throw e; }
174
+ const { value, clamped: cl } = sanitizeValue(rawVal);
175
+ if (cl) clamped.push(key);
176
+ const hit = rows.find((r) => r.key === key);
177
+ if (hit) { if (hit.value !== value) changed.push({ key, from: hit.value, to: value }); hit.value = value; }
178
+ else { rows.push({ key, value }); added.push({ key, value }); }
179
+ }
180
+ const lines = String(body).split(/\r?\n/);
181
+ const next = [...lines.slice(0, parsed.start), ...renderRows(rows), ...lines.slice(parsed.end + 1)];
182
+ return { body: next.join('\n'), rows, changed, added, clamped };
183
+ }
184
+
185
+ /**
186
+ * 在工作区里找那条状态表板书。
187
+ * @returns {{ found:true, rel, abs, raw, body, chalk }
188
+ * | { found:false, reason:'none' }
189
+ * | { found:false, reason:'multiple', rels:string[] }}
190
+ */
191
+ export async function findStateTable(sharedRoot) {
192
+ const dir = path.join(sharedRoot, CHALK_DIR);
193
+ let names;
194
+ try {
195
+ names = (await fs.readdir(dir)).filter((n) => n.endsWith('.md') && !n.startsWith('.'));
196
+ } catch { return { found: false, reason: 'none' }; }
197
+ const hits = [];
198
+ for (const n of names) {
199
+ let raw;
200
+ try { raw = await fs.readFile(path.join(dir, n), 'utf8'); } catch { continue; }
201
+ const { body, chalk } = parseChalk(raw);
202
+ if (chalk?.tag === STATE_TABLE_TAG) {
203
+ hits.push({ rel: `${CHALK_DIR}/${n}`, abs: path.join(dir, n), raw, body, chalk });
204
+ }
205
+ }
206
+ if (!hits.length) return { found: false, reason: 'none' };
207
+ if (hits.length > 1) return { found: false, reason: 'multiple', rels: hits.map((h) => h.rel) };
208
+ return { found: true, ...hits[0] };
209
+ }
210
+
211
+ /**
212
+ * 给每轮状态块用的读侧。**永远返回一个可渲染的结果**,但「读不懂」和「没有」
213
+ * 是两种不同的返回 —— 上一轮还解析得出、这一轮解析不出,调用方要能报警。
214
+ * @returns {{ state:'none' } | { state:'ok', rel, rows, body } | { state:'broken', rel?, why }}
215
+ */
216
+ export async function readStateVars(sharedRoot) {
217
+ const f = await findStateTable(sharedRoot);
218
+ if (!f.found) {
219
+ if (f.reason === 'multiple') {
220
+ return { state: 'broken', why: `板上有 ${f.rels.length} 条 tag 是「${STATE_TABLE_TAG}」的板书(${f.rels.join('、')})—— 只能有一条。` };
221
+ }
222
+ return { state: 'none' };
223
+ }
224
+ const parsed = parseStateTable(f.body);
225
+ if (!parsed.ok) {
226
+ return {
227
+ state: 'broken',
228
+ rel: f.rel,
229
+ why: parsed.error === 'no-table'
230
+ ? `${f.rel} 挂着「${STATE_TABLE_TAG}」的 tag,但正文里找不到「| 键 | 值 |」表。`
231
+ : parsed.error,
232
+ };
233
+ }
234
+ // body 一起带回:触发器的声明(```nd:triggers 围栏)就住在这条板书的正文里,
235
+ // 调用方拿它去解析,省一次读盘也省一次"两边读的是不是同一份"的疑虑。
236
+ return { state: 'ok', rel: f.rel, rows: parsed.rows, body: f.body };
237
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * server/lib/state-trend.js —— 状态表的历史序列(2026-08-30 画图能力线·活图第一块)
3
+ *
4
+ * 状态表住在一条板书文件里、板书文件进 git、工作区**每回合落一个 commit**
5
+ * (session-loop turn commit)—— 所以「好感度随拍数怎么走」的序列是**现成的**,
6
+ * 不用第二份存储不用记账,git 就是时间轴。这是别家没有的弹药。
7
+ *
8
+ * 读侧纪律与状态表同款:拿不到就大声说拿不到(没有表 / 历史不足 / 值不是数字),
9
+ * 不画一根瞎编的线。git 操作全只读(log/show),不进写 mutex。
10
+ */
11
+
12
+ import { execFile } from 'node:child_process';
13
+ import { promisify } from 'node:util';
14
+ import { findStateTable, parseStateTable } from './state-table.js';
15
+ import { parseChalk } from './chalk.js';
16
+
17
+ const run = promisify(execFile);
18
+ const MAX_POINTS = 80;
19
+
20
+ /** 值里抠数字:`5`→5、`8/10`→8、`≈3.5`→3.5;抠不出返回 null */
21
+ export function numOf(value) {
22
+ const m = /-?\d+(?:\.\d+)?/.exec(String(value ?? ''));
23
+ return m ? Number(m[0]) : null;
24
+ }
25
+
26
+ /**
27
+ * 某个键的历史序列(旧→新,末尾是**盘上现值**——本回合改了还没 commit 的也算数)。
28
+ * @returns {{ ok:true, points:number[], rel:string } |
29
+ * { ok:false, why:string, rel?:string, points?:number[] }}
30
+ */
31
+ export async function trendSeries(sharedRoot, key) {
32
+ const f = await findStateTable(sharedRoot);
33
+ if (!f.found) return { ok: false, why: f.reason === 'multiple' ? '有两条挂着状态表 tag 的板书,先合成一条' : '板上还没有状态表(tag: 状态表 的那条板书)' };
34
+ const rel = f.rel;
35
+
36
+ let hashes = [];
37
+ try {
38
+ const { stdout } = await run('git', ['log', '--format=%H', '--reverse', '--follow', '--', rel], { cwd: sharedRoot });
39
+ hashes = stdout.trim().split('\n').filter(Boolean);
40
+ } catch { /* 没有 git 历史就只有现值 */ }
41
+ if (hashes.length > MAX_POINTS) hashes = hashes.slice(hashes.length - MAX_POINTS);
42
+
43
+ const points = [];
44
+ for (const h of hashes) {
45
+ try {
46
+ const { stdout } = await run('git', ['show', `${h}:${rel}`], { cwd: sharedRoot });
47
+ const rows = parseStateTable(parseChalk(stdout).body || '');
48
+ if (!rows.ok) continue;
49
+ const row = rows.rows.find((r) => r.key === key);
50
+ if (!row) continue;
51
+ const v = numOf(row.value);
52
+ if (v === null) continue;
53
+ // 同值连点不压缩:横盘也是信息(十拍没动 = 一条平线)
54
+ points.push(v);
55
+ } catch { /* 某个版本读不出就跳过 */ }
56
+ }
57
+
58
+ // 盘上现值收尾(本回合 set_vars 改过、还没 commit)
59
+ const { readStateVars } = await import('./state-table.js');
60
+ const live = await readStateVars(sharedRoot);
61
+ if (live.state === 'ok') {
62
+ const row = live.rows.find((r) => r.key === key);
63
+ const v = row ? numOf(row.value) : null;
64
+ if (v !== null && (points.length === 0 || v !== points[points.length - 1])) points.push(v);
65
+ if (row && v === null) return { ok: false, rel, why: `「${key}」的现值是「${row.value}」,抠不出数字 —— 趋势线只画得了数` };
66
+ if (!row) return { ok: false, rel, why: `状态表里没有「${key}」这个键` };
67
+ }
68
+ if (points.length < 2) {
69
+ return { ok: false, rel, points, why: `「${key}」目前只有 ${points.length} 个数据点(历史随每回合的 git commit 长出来)—— 至少两点才有线,过几拍再画` };
70
+ }
71
+ return { ok: true, points, rel };
72
+ }
73
+
74
+ /**
75
+ * 序列 → 手绘折线的**几何**(局部像素;抖动由调用方加,跟全族同一支笔)。
76
+ * @returns {{ w,h, lineD:string, baselineD:string, dotD:string, min:number, max:number }}
77
+ */
78
+ export function trendGeometry(points, { w = 336, h = 120 } = {}) {
79
+ const P = 8;
80
+ const min = Math.min(...points); const max = Math.max(...points);
81
+ const span = (max - min) || 1;
82
+ const iw = w - P * 2; const ih = h - P * 2;
83
+ const xy = points.map((v, i) => ({
84
+ x: P + (points.length === 1 ? iw / 2 : (i / (points.length - 1)) * iw),
85
+ y: P + ih - ((v - min) / span) * ih,
86
+ }));
87
+ const f = (n) => Math.round(n * 10) / 10;
88
+ const lineD = xy.map((p, i) => `${i ? 'L' : 'M'} ${f(p.x)} ${f(p.y)}`).join(' ');
89
+ const last = xy[xy.length - 1];
90
+ // 端点小圈(现值在哪)
91
+ const r = 3;
92
+ const dotD = `M ${f(last.x - r)} ${f(last.y)} C ${f(last.x - r)} ${f(last.y - r * 1.4)} ${f(last.x + r)} ${f(last.y - r * 1.4)} ${f(last.x + r)} ${f(last.y)} C ${f(last.x + r)} ${f(last.y + r * 1.4)} ${f(last.x - r)} ${f(last.y + r * 1.4)} ${f(last.x - r)} ${f(last.y)}`;
93
+ return { w, h, lineD, baselineD: `M ${P} ${h - P} L ${w - P} ${h - P}`, dotD, min, max };
94
+ }