@xiaobuyu/nodesign 0.0.7 → 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 (353) 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 +8 -0
  7. package/server/api/assets.js +21 -33
  8. package/server/api/board.js +29 -1
  9. package/server/api/browse.js +3 -2
  10. package/server/api/chatai.js +9 -8
  11. package/server/api/exports/cards.js +7 -6
  12. package/server/api/exports/docx-pdf.js +4 -1
  13. package/server/api/local.js +10 -8
  14. package/server/api/projects.js +19 -3
  15. package/server/api/publish.js +3 -2
  16. package/server/api/sessions-rewind.js +220 -0
  17. package/server/api/sessions.js +58 -194
  18. package/server/api/stage.js +35 -0
  19. package/server/api/turn-model-switch.js +109 -0
  20. package/server/api/turn.js +52 -88
  21. package/server/auth/middleware.js +29 -6
  22. package/server/auth/tier.js +27 -7
  23. package/server/auth/users-store.js +14 -1
  24. package/server/engine/agent/actor-trail.js +110 -0
  25. package/server/engine/agent/agent-shared.js +22 -52
  26. package/server/engine/agent/beat-state.js +61 -0
  27. package/server/engine/agent/cast.js +322 -0
  28. package/server/engine/agent/events.js +9 -2
  29. package/server/engine/agent/hooks/beat-gate.js +73 -0
  30. package/server/engine/agent/hooks/lifecycle.js +6 -2
  31. package/server/engine/agent/hooks/post-trim.js +1 -0
  32. package/server/engine/agent/hooks/pre-board-dirty.js +29 -0
  33. package/server/engine/agent/hooks/pre-defaults.js +206 -2
  34. package/server/engine/agent/hooks/pre-injectors.js +19 -2
  35. package/server/engine/agent/hooks/pre-peer-guard.js +88 -0
  36. package/server/engine/agent/hooks/pre-unknown-params.js +82 -0
  37. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +33 -0
  38. package/server/engine/agent/hooks/resident-role-lifecycle.js +100 -0
  39. package/server/engine/agent/hooks/slot-alias.js +30 -0
  40. package/server/engine/agent/hooks/user-prompt-submit.js +197 -7
  41. package/server/engine/agent/hooks.js +76 -6
  42. package/server/engine/agent/isolation.js +25 -2
  43. package/server/engine/agent/memory-config.js +27 -1
  44. package/server/engine/agent/model-context.js +102 -18
  45. package/server/engine/agent/model-table.js +361 -58
  46. package/server/engine/agent/prompts/nodesign-prelude.md +566 -59
  47. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +1 -1
  48. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +2 -2
  49. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +1 -1
  50. package/server/engine/agent/prompts/tools/site-reference.md +2 -1
  51. package/server/engine/agent/role-card.js +175 -0
  52. package/server/engine/agent/rp-mode.js +17 -0
  53. package/server/engine/agent/session-loop.js +26 -14
  54. package/server/engine/agent/stage-status.js +73 -0
  55. package/server/engine/agent/subagent-flight.js +73 -0
  56. package/server/engine/agent/system-prompts.js +41 -0
  57. package/server/engine/agent/tool-input-stream.js +165 -0
  58. package/server/engine/mcp/actor.js +59 -0
  59. package/server/engine/mcp/capability-gate.js +21 -2
  60. package/server/engine/mcp/index.js +116 -24
  61. package/server/engine/mcp/mode-profile.js +139 -0
  62. package/server/engine/mcp/server-name.js +19 -0
  63. package/server/engine/mcp/tools/artifact-session.js +2 -1
  64. package/server/engine/mcp/tools/browse-find-batch.js +76 -14
  65. package/server/engine/mcp/tools/browse.js +1 -1
  66. package/server/engine/mcp/tools/cast-role.js +167 -0
  67. package/server/engine/mcp/tools/draw-trend.js +104 -0
  68. package/server/engine/mcp/tools/edit-board-schema.js +56 -0
  69. package/server/engine/mcp/tools/edit-board.js +569 -0
  70. package/server/engine/mcp/tools/look-at-board.js +4 -3
  71. package/server/engine/mcp/tools/open-sheet.js +242 -0
  72. package/server/engine/mcp/tools/organize-board.js +19 -5
  73. package/server/engine/mcp/tools/paint-still.js +1 -1
  74. package/server/engine/mcp/tools/pin-to-board.js +78 -3
  75. package/server/engine/mcp/tools/read-board.js +109 -16
  76. package/server/engine/mcp/tools/read-tavern-json.js +87 -19
  77. package/server/engine/mcp/tools/report-issue.js +1 -0
  78. package/server/engine/mcp/tools/role-memory.js +83 -0
  79. package/server/engine/mcp/tools/roll-dice.js +82 -0
  80. package/server/engine/mcp/tools/roll-film.js +1 -1
  81. package/server/engine/mcp/tools/screenshot.js +2 -2
  82. package/server/engine/mcp/tools/set-vars.js +150 -0
  83. package/server/engine/mcp/tools/sheet-replan.js +46 -0
  84. package/server/engine/mcp/tools/web-search.js +1 -1
  85. package/server/engine/mcp/tools/write-on-board-flow.js +108 -0
  86. package/server/engine/mcp/tools/write-on-board-place.js +211 -0
  87. package/server/engine/mcp/tools/write-on-board-role-anchor.js +48 -0
  88. package/server/engine/mcp/tools/write-on-board-schema.js +125 -0
  89. package/server/engine/mcp/tools/write-on-board.js +567 -107
  90. package/server/engine/plugins/nodesign/skills/blackboard-rp/SKILL.md +215 -0
  91. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +11 -3
  92. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +34 -4
  93. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +20 -4
  94. package/server/engine/plugins/nodesign/skills/story-craft/SKILL.md +94 -0
  95. package/server/engine/plugins/nodesign/skills/story-import/SKILL.md +112 -0
  96. package/server/engine/plugins/nodesign/skills/story-intimacy/SKILL.md +72 -0
  97. package/server/engine/plugins/nodesign/skills/story-voice/SKILL.md +66 -0
  98. package/server/engine/plugins/nodesign/skills/story-voice/voices//345/206/267/347/241/254.md +33 -0
  99. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/203/205/346/204/237/347/233/264/347/273/231.md +42 -0
  100. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/204/217/350/257/206/346/265/201.md +42 -0
  101. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/255/246/344/276/240/347/231/275/350/257/235.md +34 -0
  102. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/224/237/346/264/273/346/265/201.md +35 -0
  103. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/231/275/346/217/217.md +37 -0
  104. package/server/engine/plugins/nodesign/skills/story-voice/voices//350/210/236/345/217/260/345/211/247.md +32 -0
  105. package/server/engine/plugins/nodesign/skills/story-voice/voices//351/230/266/346/256/265/345/210/207/346/215/242.md +42 -0
  106. package/server/engine/runs/board-seater.js +272 -0
  107. package/server/engine/runs/board-tasklist.js +90 -48
  108. package/server/engine/runs/turn-relay.js +24 -2
  109. package/server/engine/runs/user-chalk-echo.js +44 -0
  110. package/server/index.js +2 -0
  111. package/server/lib/board-anchor.js +49 -0
  112. package/server/lib/board-dirty.js +93 -0
  113. package/server/lib/board-excerpt.js +80 -0
  114. package/server/lib/board-follow.js +111 -0
  115. package/server/lib/board-groups.js +1 -5
  116. package/server/lib/board-hero.js +3 -1
  117. package/server/lib/board-kind-sizes.js +36 -0
  118. package/server/lib/board-lanes.js +58 -0
  119. package/server/lib/board-obstacles.js +80 -0
  120. package/server/lib/board-relations.js +5 -1
  121. package/server/lib/board-sheets.js +467 -0
  122. package/server/lib/board-shelf.js +86 -0
  123. package/server/lib/board-transform.js +62 -0
  124. package/server/lib/canvas-id.js +18 -1
  125. package/server/lib/chalk-flow.js +88 -0
  126. package/server/lib/chalk-rewrite.js +63 -0
  127. package/server/lib/chalk-size-pref.js +48 -0
  128. package/server/lib/chalk.js +42 -3
  129. package/server/lib/ingress/forward-openai-chat.js +16 -4
  130. package/server/lib/ingress/openai-chat.js +23 -12
  131. package/server/lib/ingress/session-routes.js +3 -3
  132. package/server/lib/ingress/stream-watchdog.js +53 -0
  133. package/server/lib/ingress/upstream-billing.js +15 -0
  134. package/server/lib/ingress/upstream-error-hints.js +36 -0
  135. package/server/lib/json-preview.js +58 -0
  136. package/server/lib/model-ingress.js +26 -1
  137. package/server/lib/moderation.js +10 -5
  138. package/server/lib/quota.js +26 -6
  139. package/server/lib/rect.js +50 -0
  140. package/server/lib/screen.js +36 -0
  141. package/server/lib/sheet-state.js +19 -0
  142. package/server/lib/sketch-array.js +118 -0
  143. package/server/lib/sketch-hatch.js +99 -0
  144. package/server/lib/sketch-layout.js +269 -69
  145. package/server/lib/sketch-shapes.js +152 -0
  146. package/server/lib/sketch-stencils.js +170 -0
  147. package/server/lib/state-table.js +237 -0
  148. package/server/lib/state-trend.js +94 -0
  149. package/server/lib/state-triggers.js +199 -0
  150. package/server/lib/tavern-json.js +48 -0
  151. package/server/lib/user-uploads.js +88 -0
  152. package/server/projects/board-limits.js +5 -3
  153. package/server/projects/board-sanitize.js +220 -10
  154. package/server/projects/board-store.js +95 -6
  155. package/server/projects/session-jsonl.js +110 -0
  156. package/server/projects/store.js +56 -3
  157. package/server/projects/viewpoint-store.js +35 -3
  158. package/server/projects/workspace-slots.js +30 -0
  159. package/server/projects/workspace-templates.js +29 -0
  160. package/server/projects/workspace.js +16 -17
  161. package/server/runtime/capabilities.js +6 -2
  162. package/server/shared/locales.js +70 -0
  163. package/server/shared/messages-en.js +69 -0
  164. package/server/shared/messages.js +65 -0
  165. package/server/ws/broker.js +3 -0
  166. package/server/ws/index.js +43 -3
  167. package/web/README.md +1 -1
  168. package/web/dist/assets/{BrowserWindow-BoOBaIyz.js → BrowserWindow-CcaGRWz5.js} +1 -1
  169. package/web/dist/assets/DeckWindow-BIr2KGFT.js +6 -0
  170. package/web/dist/assets/{DocxWindow-B_u6aX9o.js → DocxWindow-B9KZmU5z.js} +2 -2
  171. package/web/dist/assets/{MermaidBlock-BO-46Du5.js → MermaidBlock-DrfRnz8E.js} +43 -43
  172. package/web/dist/assets/{SiteWindow-ConPmzdM.js → SiteWindow-CJOjCF7d.js} +6 -6
  173. package/web/dist/assets/{abnfDiagram-VCTEODGH-FCO9yf-n.js → abnfDiagram-VCTEODGH-CUZbQGIg.js} +1 -1
  174. package/web/dist/assets/{arc-BLcEOSRX.js → arc-C1-HSUny.js} +1 -1
  175. package/web/dist/assets/{architectureDiagram-5GKGNRK7-kxiX4TeN.js → architectureDiagram-5GKGNRK7-B5nwvu60.js} +2 -2
  176. package/web/dist/assets/{blockDiagram-NRAW4CY4-CptFksix.js → blockDiagram-NRAW4CY4-uPPuFLwR.js} +1 -1
  177. package/web/dist/assets/{c4Diagram-UCG6FXSJ-Dxmxj34F.js → c4Diagram-UCG6FXSJ-C0mKBFlz.js} +1 -1
  178. package/web/dist/assets/channel-BxpWNOGU.js +1 -0
  179. package/web/dist/assets/{chunk-2Q5K7J3B-D9inx5oo.js → chunk-2Q5K7J3B-Cs0rlZ4q.js} +1 -1
  180. package/web/dist/assets/{chunk-5VM5RSS4-DydfHlEc.js → chunk-5VM5RSS4-DtR63oYb.js} +1 -1
  181. package/web/dist/assets/{chunk-F27PBJKO-BO7GxhSv.js → chunk-F27PBJKO-B3MRdezJ.js} +1 -1
  182. package/web/dist/assets/{chunk-G27WJ6UU-BzP6YiQ_.js → chunk-G27WJ6UU-BtwntwpS.js} +1 -1
  183. package/web/dist/assets/{chunk-JWPE2WC7-CHcgcd0s.js → chunk-JWPE2WC7-BHsTe5PD.js} +1 -1
  184. package/web/dist/assets/{chunk-LCL6LL3I-D2Wg5JtX.js → chunk-LCL6LL3I-knY15Qdm.js} +1 -1
  185. package/web/dist/assets/{chunk-POPQ4Y6H-B2fCRfpM.js → chunk-POPQ4Y6H-C51tgK_I.js} +1 -1
  186. package/web/dist/assets/{chunk-SVP7TREG-JSLcsRlZ.js → chunk-SVP7TREG-CnwRZmF_.js} +1 -1
  187. package/web/dist/assets/{chunk-XXDRQBXY-BCjvaeHm.js → chunk-XXDRQBXY-BtXzVF-_.js} +1 -1
  188. package/web/dist/assets/classDiagram-DTDB5LWJ-DUEKmDfo.js +1 -0
  189. package/web/dist/assets/classDiagram-v2-JRS7N3AN-DUEKmDfo.js +1 -0
  190. package/web/dist/assets/{cose-bilkent-JH36ORCC-0uKv6oJ7.js → cose-bilkent-JH36ORCC-CzWlGP6i.js} +1 -1
  191. package/web/dist/assets/{cynefin-OW5HDTMX-Bvt-gEmA.js → cynefin-OW5HDTMX-vK64EGSv.js} +1 -1
  192. package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-C9Ht02qC.js → cynefinDiagram-5FMLGOSQ-Bs3g1Gvh.js} +1 -1
  193. package/web/dist/assets/{dagre-3AP2YEHR-BfxZny4s.js → dagre-3AP2YEHR-BhgkYe8V.js} +1 -1
  194. package/web/dist/assets/{diagram-S7CK7UJ4-CrE0ZETE.js → diagram-S7CK7UJ4-C_6mvD2V.js} +1 -1
  195. package/web/dist/assets/{diagram-UQ7AKVKN-BO8VQ4Fx.js → diagram-UQ7AKVKN-qW7tssUW.js} +1 -1
  196. package/web/dist/assets/{diagram-VSXAHHWV-CHUZnR9F.js → diagram-VSXAHHWV-txJPHpvG.js} +1 -1
  197. package/web/dist/assets/{diagram-VX7I27RA-DYQAz1Vu.js → diagram-VX7I27RA-CQTMsCnL.js} +1 -1
  198. package/web/dist/assets/{diagram-Z3DM3KII-BDjivgLr.js → diagram-Z3DM3KII--cb1hoE6.js} +1 -1
  199. package/web/dist/assets/{ebnfDiagram-PWID7BFC-Bpe_QVbH.js → ebnfDiagram-PWID7BFC-Da6lDU77.js} +1 -1
  200. package/web/dist/assets/{erDiagram-SSCWMZ5O-BxhwuAu1.js → erDiagram-SSCWMZ5O-DGCMrsQn.js} +1 -1
  201. package/web/dist/assets/{flowDiagram-A5DVABFB-CCw2H08-.js → flowDiagram-A5DVABFB-GdOL_qno.js} +1 -1
  202. package/web/dist/assets/{ganttDiagram-EL5Y4UJY-DtTc7MyI.js → ganttDiagram-EL5Y4UJY-Cx1F_jBK.js} +4 -4
  203. package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-BvORxMd2.js → gitGraphDiagram-WWUBYQGX-B5_6JhR3.js} +1 -1
  204. package/web/dist/assets/hand-CuvZZQGW.webp +0 -0
  205. package/web/dist/assets/index-BYKfMnmv.js +2759 -0
  206. package/web/dist/assets/index-CW7VsrVg.css +1 -0
  207. package/web/dist/assets/{infoDiagram-RXCK75RN-DMcrkypD.js → infoDiagram-RXCK75RN-CjvKpSPA.js} +1 -1
  208. package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CZkTIbe-.js → ishikawaDiagram-5VMMS53U-CxLoAPhx.js} +1 -1
  209. package/web/dist/assets/{journeyDiagram-EYS64GPL-DR0XazdE.js → journeyDiagram-EYS64GPL-BfvS1upJ.js} +1 -1
  210. package/web/dist/assets/{kanban-definition-3QL26DDD-DN8VbwVu.js → kanban-definition-3QL26DDD-DKPPu0w4.js} +1 -1
  211. package/web/dist/assets/{layout-9_paCbyt.js → layout-Dhgck-Zo.js} +1 -1
  212. package/web/dist/assets/{linear-g6Ct012e.js → linear-DMSQ-aUw.js} +1 -1
  213. package/web/dist/assets/lxgw-nd-app-bold-D1pKA1bR.woff2 +0 -0
  214. package/web/dist/assets/lxgw-nd-app-regular-BDe3d26v.woff2 +0 -0
  215. package/web/dist/assets/lxgw-nd-bold-Bufvlg03.woff2 +0 -0
  216. package/web/dist/assets/lxgw-nd-regular-DQwoI4OP.woff2 +0 -0
  217. package/web/dist/assets/lxgwwenkaiscreen-subset-100-DLPe7tEt.woff2 +0 -0
  218. package/web/dist/assets/lxgwwenkaiscreen-subset-101-d3VsZKWT.woff2 +0 -0
  219. package/web/dist/assets/lxgwwenkaiscreen-subset-102-ClzJuLq6.woff2 +0 -0
  220. package/web/dist/assets/lxgwwenkaiscreen-subset-103-DuDnQ6K-.woff2 +0 -0
  221. package/web/dist/assets/lxgwwenkaiscreen-subset-104-kPGW3Eem.woff2 +0 -0
  222. package/web/dist/assets/lxgwwenkaiscreen-subset-105-BzD54h-0.woff2 +0 -0
  223. package/web/dist/assets/lxgwwenkaiscreen-subset-106-BQ7fIzlx.woff2 +0 -0
  224. package/web/dist/assets/lxgwwenkaiscreen-subset-107-D8tDfFHI.woff2 +0 -0
  225. package/web/dist/assets/lxgwwenkaiscreen-subset-108-tGhkpwep.woff2 +0 -0
  226. package/web/dist/assets/lxgwwenkaiscreen-subset-109-7SMVSrly.woff2 +0 -0
  227. package/web/dist/assets/lxgwwenkaiscreen-subset-110-_uDKrFed.woff2 +0 -0
  228. package/web/dist/assets/lxgwwenkaiscreen-subset-111-Bio825RE.woff2 +0 -0
  229. package/web/dist/assets/lxgwwenkaiscreen-subset-112-D2ee0i-C.woff2 +0 -0
  230. package/web/dist/assets/lxgwwenkaiscreen-subset-113-JPFVDrga.woff2 +0 -0
  231. package/web/dist/assets/lxgwwenkaiscreen-subset-114-BcfxYA4w.woff2 +0 -0
  232. package/web/dist/assets/lxgwwenkaiscreen-subset-115-B1HeeOsd.woff2 +0 -0
  233. package/web/dist/assets/lxgwwenkaiscreen-subset-116-hY12tXpq.woff2 +0 -0
  234. package/web/dist/assets/lxgwwenkaiscreen-subset-117-BvuZwmre.woff2 +0 -0
  235. package/web/dist/assets/lxgwwenkaiscreen-subset-118-tzvs_H7j.woff2 +0 -0
  236. package/web/dist/assets/lxgwwenkaiscreen-subset-119-J2DWuYDN.woff2 +0 -0
  237. package/web/dist/assets/lxgwwenkaiscreen-subset-21-pDubCJV5.woff2 +0 -0
  238. package/web/dist/assets/lxgwwenkaiscreen-subset-22-ChAPMpqp.woff2 +0 -0
  239. package/web/dist/assets/lxgwwenkaiscreen-subset-23-BBx3oNpT.woff2 +0 -0
  240. package/web/dist/assets/lxgwwenkaiscreen-subset-24-CYt-Oagk.woff2 +0 -0
  241. package/web/dist/assets/lxgwwenkaiscreen-subset-25-Cx3j-ZUv.woff2 +0 -0
  242. package/web/dist/assets/lxgwwenkaiscreen-subset-26-B9AfZ3LL.woff2 +0 -0
  243. package/web/dist/assets/lxgwwenkaiscreen-subset-27-Bh2s9MNE.woff2 +0 -0
  244. package/web/dist/assets/lxgwwenkaiscreen-subset-28-C02JPoKP.woff2 +0 -0
  245. package/web/dist/assets/lxgwwenkaiscreen-subset-29-T_tbUGhk.woff2 +0 -0
  246. package/web/dist/assets/lxgwwenkaiscreen-subset-30-DMfyNzhz.woff2 +0 -0
  247. package/web/dist/assets/lxgwwenkaiscreen-subset-31-DFfdzbkg.woff2 +0 -0
  248. package/web/dist/assets/lxgwwenkaiscreen-subset-32-Bs1fz2iW.woff2 +0 -0
  249. package/web/dist/assets/lxgwwenkaiscreen-subset-33-cpB96OkZ.woff2 +0 -0
  250. package/web/dist/assets/lxgwwenkaiscreen-subset-34-BhinPYWX.woff2 +0 -0
  251. package/web/dist/assets/lxgwwenkaiscreen-subset-35-D_njRQqA.woff2 +0 -0
  252. package/web/dist/assets/lxgwwenkaiscreen-subset-36-DqXlLE3t.woff2 +0 -0
  253. package/web/dist/assets/lxgwwenkaiscreen-subset-37-j5LbvMph.woff2 +0 -0
  254. package/web/dist/assets/lxgwwenkaiscreen-subset-38-oO4jWdKm.woff2 +0 -0
  255. package/web/dist/assets/lxgwwenkaiscreen-subset-39-CabsX4rw.woff2 +0 -0
  256. package/web/dist/assets/lxgwwenkaiscreen-subset-4-PatS3YHh.woff2 +0 -0
  257. package/web/dist/assets/lxgwwenkaiscreen-subset-40-yrLg3C3e.woff2 +0 -0
  258. package/web/dist/assets/lxgwwenkaiscreen-subset-41-DBXmCQhc.woff2 +0 -0
  259. package/web/dist/assets/lxgwwenkaiscreen-subset-42-Bqij_UiQ.woff2 +0 -0
  260. package/web/dist/assets/lxgwwenkaiscreen-subset-43-D3kp0Ntc.woff2 +0 -0
  261. package/web/dist/assets/lxgwwenkaiscreen-subset-44-B_BNGvK4.woff2 +0 -0
  262. package/web/dist/assets/lxgwwenkaiscreen-subset-45-CZc0cQWs.woff2 +0 -0
  263. package/web/dist/assets/lxgwwenkaiscreen-subset-46-CHQX5Kd9.woff2 +0 -0
  264. package/web/dist/assets/lxgwwenkaiscreen-subset-47-Ci8gLrHA.woff2 +0 -0
  265. package/web/dist/assets/lxgwwenkaiscreen-subset-48-run3ugD3.woff2 +0 -0
  266. package/web/dist/assets/lxgwwenkaiscreen-subset-49-DgfiUb6u.woff2 +0 -0
  267. package/web/dist/assets/lxgwwenkaiscreen-subset-5-B0CtGbX_.woff2 +0 -0
  268. package/web/dist/assets/lxgwwenkaiscreen-subset-50-CdCG-POr.woff2 +0 -0
  269. package/web/dist/assets/lxgwwenkaiscreen-subset-51-L2okeQZ2.woff2 +0 -0
  270. package/web/dist/assets/lxgwwenkaiscreen-subset-52-BZAqyGJW.woff2 +0 -0
  271. package/web/dist/assets/lxgwwenkaiscreen-subset-53-B0xWqLeU.woff2 +0 -0
  272. package/web/dist/assets/lxgwwenkaiscreen-subset-54-BZVQX-EA.woff2 +0 -0
  273. package/web/dist/assets/lxgwwenkaiscreen-subset-55-093mkIUJ.woff2 +0 -0
  274. package/web/dist/assets/lxgwwenkaiscreen-subset-56-CbhWG8G-.woff2 +0 -0
  275. package/web/dist/assets/lxgwwenkaiscreen-subset-57-pLQuvo6c.woff2 +0 -0
  276. package/web/dist/assets/lxgwwenkaiscreen-subset-58-BHhoY50x.woff2 +0 -0
  277. package/web/dist/assets/lxgwwenkaiscreen-subset-59-D1_6DuIQ.woff2 +0 -0
  278. package/web/dist/assets/lxgwwenkaiscreen-subset-6-BnAelmYU.woff2 +0 -0
  279. package/web/dist/assets/lxgwwenkaiscreen-subset-60-HDBIuLTZ.woff2 +0 -0
  280. package/web/dist/assets/lxgwwenkaiscreen-subset-61-C-c13SeU.woff2 +0 -0
  281. package/web/dist/assets/lxgwwenkaiscreen-subset-62-kHRVOhZ2.woff2 +0 -0
  282. package/web/dist/assets/lxgwwenkaiscreen-subset-63-C0MCsvFv.woff2 +0 -0
  283. package/web/dist/assets/lxgwwenkaiscreen-subset-64-DZekguCj.woff2 +0 -0
  284. package/web/dist/assets/lxgwwenkaiscreen-subset-65-CtP6c3IU.woff2 +0 -0
  285. package/web/dist/assets/lxgwwenkaiscreen-subset-66-DLVWoMSf.woff2 +0 -0
  286. package/web/dist/assets/lxgwwenkaiscreen-subset-67-BVuOnayH.woff2 +0 -0
  287. package/web/dist/assets/lxgwwenkaiscreen-subset-68-DpxQ6Mnx.woff2 +0 -0
  288. package/web/dist/assets/lxgwwenkaiscreen-subset-69-qesBxLmC.woff2 +0 -0
  289. package/web/dist/assets/lxgwwenkaiscreen-subset-70-BBt1xN7h.woff2 +0 -0
  290. package/web/dist/assets/lxgwwenkaiscreen-subset-71-BWqZacwB.woff2 +0 -0
  291. package/web/dist/assets/lxgwwenkaiscreen-subset-72-B1wP_Yst.woff2 +0 -0
  292. package/web/dist/assets/lxgwwenkaiscreen-subset-73-C99T7jNZ.woff2 +0 -0
  293. package/web/dist/assets/lxgwwenkaiscreen-subset-74-DrUTsWBe.woff2 +0 -0
  294. package/web/dist/assets/lxgwwenkaiscreen-subset-75-C4M5XXQe.woff2 +0 -0
  295. package/web/dist/assets/lxgwwenkaiscreen-subset-76-Bq8spVWF.woff2 +0 -0
  296. package/web/dist/assets/lxgwwenkaiscreen-subset-77-D9vNupE2.woff2 +0 -0
  297. package/web/dist/assets/lxgwwenkaiscreen-subset-78-D6th7Gnu.woff2 +0 -0
  298. package/web/dist/assets/lxgwwenkaiscreen-subset-79-bezM85cS.woff2 +0 -0
  299. package/web/dist/assets/lxgwwenkaiscreen-subset-80-DD8pnhG2.woff2 +0 -0
  300. package/web/dist/assets/lxgwwenkaiscreen-subset-81-N6jtSyBs.woff2 +0 -0
  301. package/web/dist/assets/lxgwwenkaiscreen-subset-82-D6c180bZ.woff2 +0 -0
  302. package/web/dist/assets/lxgwwenkaiscreen-subset-83-em17AadM.woff2 +0 -0
  303. package/web/dist/assets/lxgwwenkaiscreen-subset-84-CJ8gdrMK.woff2 +0 -0
  304. package/web/dist/assets/lxgwwenkaiscreen-subset-85-9PPGO1eB.woff2 +0 -0
  305. package/web/dist/assets/lxgwwenkaiscreen-subset-86-CjWPf-u5.woff2 +0 -0
  306. package/web/dist/assets/lxgwwenkaiscreen-subset-87-8D_X_yOC.woff2 +0 -0
  307. package/web/dist/assets/lxgwwenkaiscreen-subset-88-9vtKFPBx.woff2 +0 -0
  308. package/web/dist/assets/lxgwwenkaiscreen-subset-89-BhE5jAKX.woff2 +0 -0
  309. package/web/dist/assets/lxgwwenkaiscreen-subset-90-D2Npfm5E.woff2 +0 -0
  310. package/web/dist/assets/lxgwwenkaiscreen-subset-91-C6Ft1-Eu.woff2 +0 -0
  311. package/web/dist/assets/lxgwwenkaiscreen-subset-97-CaExyQP9.woff2 +0 -0
  312. package/web/dist/assets/lxgwwenkaiscreen-subset-98-CPoVbsUd.woff2 +0 -0
  313. package/web/dist/assets/lxgwwenkaiscreen-subset-99-ZUQZGVgH.woff2 +0 -0
  314. package/web/dist/assets/{mindmap-definition-FBJOCRG2-BGt4omYC.js → mindmap-definition-FBJOCRG2-CalfL_Kn.js} +1 -1
  315. package/web/dist/assets/{pegDiagram-XKGWAZYB-uSfS-PBE.js → pegDiagram-XKGWAZYB-D9mVWz7l.js} +1 -1
  316. package/web/dist/assets/pending-edit-apply-kwMFWsUe.js +31 -0
  317. package/web/dist/assets/{pieDiagram-E7YTZNPT-DpeI935H.js → pieDiagram-E7YTZNPT-DELzbZNS.js} +1 -1
  318. package/web/dist/assets/{quadrantDiagram-AXDQQJYC-B7IuNvWa.js → quadrantDiagram-AXDQQJYC-TZqp4mfA.js} +1 -1
  319. package/web/dist/assets/{railroadDiagram-O6MQD6OU-ARzYpPQI.js → railroadDiagram-O6MQD6OU-DxX59f0i.js} +1 -1
  320. package/web/dist/assets/{requirementDiagram-EFPCY7ZU-Cj4upkAo.js → requirementDiagram-EFPCY7ZU-DFdsJLIe.js} +1 -1
  321. package/web/dist/assets/{rotate-cw-CXJcyHWk.js → rotate-cw-62liz7LX.js} +1 -1
  322. package/web/dist/assets/{sankeyDiagram-P5KCCOFB-OaVwSnLP.js → sankeyDiagram-P5KCCOFB-ByYcd3fG.js} +1 -1
  323. package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-1yrUbq4t.js → sequenceDiagram-WJ2MYXX4-Bj-ml1jS.js} +1 -1
  324. package/web/dist/assets/{sizeCapture-X5ZJPWSS-nYH5p9D6.js → sizeCapture-X5ZJPWSS-KhsXThyD.js} +1 -1
  325. package/web/dist/assets/{square-dashed-mouse-pointer-CTtTVKoH.js → square-dashed-mouse-pointer-D_VrqVpy.js} +1 -1
  326. package/web/dist/assets/{stateDiagram-HBIQ2CUA-Dj5HvyNq.js → stateDiagram-HBIQ2CUA-BJlpHfWU.js} +1 -1
  327. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-CR2kYAH2.js +1 -0
  328. package/web/dist/assets/{swimlanes-XN3QIQJK-umeT0HpB.js → swimlanes-XN3QIQJK-DUB9OJVs.js} +1 -1
  329. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BitOxnG2.js +8 -0
  330. package/web/dist/assets/{timeline-definition-24CTP7MA-MqnQfHKB.js → timeline-definition-24CTP7MA-C03NX82A.js} +1 -1
  331. package/web/dist/assets/{vennDiagram-4TSXK5OY-b2liyZra.js → vennDiagram-4TSXK5OY-B9pjzngp.js} +1 -1
  332. package/web/dist/assets/{wardleyDiagram-VM6X3IG4-QMK-zNh1.js → wardleyDiagram-VM6X3IG4-UqWq_mcT.js} +1 -1
  333. package/web/dist/assets/{xychartDiagram-S5SC5T6Z-D5Mc6tfA.js → xychartDiagram-S5SC5T6Z-D2pDKtOv.js} +1 -1
  334. package/web/dist/index.html +2 -2
  335. package/server/engine/mcp/tools/arrange-on-board.js +0 -126
  336. package/server/engine/mcp/tools/create-on-board.js +0 -152
  337. package/server/engine/mcp/tools/edit-sketch.js +0 -167
  338. package/server/engine/mcp/tools/relate-on-board.js +0 -150
  339. package/server/engine/mcp/tools/sketch-on-board.js +0 -345
  340. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +0 -235
  341. 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
  342. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +0 -268
  343. package/web/dist/assets/DeckWindow-Bg3rgkvi.js +0 -11
  344. package/web/dist/assets/channel-DFyiuGL0.js +0 -1
  345. package/web/dist/assets/classDiagram-DTDB5LWJ-Aw1f8z-b.js +0 -1
  346. package/web/dist/assets/classDiagram-v2-JRS7N3AN-Aw1f8z-b.js +0 -1
  347. package/web/dist/assets/index-C0KUf6Wd.css +0 -1
  348. package/web/dist/assets/index-DGG5wIZs.js +0 -2118
  349. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  350. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  351. package/web/dist/assets/pending-edit-apply-uP4uvrOD.js +0 -31
  352. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-kJ1m_zJT.js +0 -1
  353. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-DPSkMbbl.js +0 -8
@@ -0,0 +1,88 @@
1
+ /**
2
+ * server/lib/chalk-flow.js —— 长文自动分条(write_on_board 的 flow,2026-08-30)
3
+ *
4
+ * ## 为什么要有这个文件
5
+ *
6
+ * 「剩 15 行、我这段是 14 行还是 16 行」这道算术模型做不准,也不该由它做 ——
7
+ * proj_mtfpehm3 首场真会话 14 发 board_batch 挂了 6 发,全是容量拒收,本质全是
8
+ * 这道算术。flow 把它拿走:agent 交整段内容,机器按**段落边界**拆成若干条
9
+ * ≤ 一张卡高的小板书,链着往版位/纸里排,装到哪儿是哪儿,装不下的**原样退回**
10
+ * (绝不静默丢、绝不硬挤、绝不替它翻页 —— 刀 F 的教义一条不破)。
11
+ *
12
+ * ## 拆分纪律
13
+ *
14
+ * 1. 只在**空行**处拆,且空行必须在围栏(``` fence)外 —— 代码块/nd:controls/
15
+ * 表格永远整块走,切开它们比写不下更坏。
16
+ * 2. 单块超高时降级到**句子边界**(。!?.!?; 换行也算),仍不切词。
17
+ * 3. 一句话就超高的(极少):整句放行,靠既有的折叠 + 如实报兜底。
18
+ * 4. 贪心装填:往当前条里续块,直到再续就超 maxH。
19
+ */
20
+
21
+ import { textBox } from './sketch-layout.js';
22
+
23
+ const FENCE_LINE = /^[ \t]*```/;
24
+
25
+ /**
26
+ * 按空行拆块(围栏内的空行不算边界)。
27
+ * @returns {string[]} 原文顺序的块,各自 trim 过、非空
28
+ */
29
+ export function splitBlocks(body) {
30
+ const lines = String(body || '').split(/\r?\n/);
31
+ const blocks = [];
32
+ let cur = [];
33
+ let inFence = false;
34
+ const push = () => {
35
+ const t = cur.join('\n').trim();
36
+ if (t) blocks.push(t);
37
+ cur = [];
38
+ };
39
+ for (const l of lines) {
40
+ if (FENCE_LINE.test(l)) inFence = !inFence;
41
+ if (!inFence && !l.trim()) { push(); continue; }
42
+ cur.push(l);
43
+ }
44
+ push();
45
+ return blocks;
46
+ }
47
+
48
+ /** 单块超高时的降级:按句子边界拆(不切词;拆不动就原样一块) */
49
+ function splitSentences(block) {
50
+ const parts = String(block).split(/(?<=[。!?!?;;])\s*/).filter((s) => s.trim());
51
+ return parts.length > 1 ? parts : [block];
52
+ }
53
+
54
+ /**
55
+ * 把长文拆成若干条小板书的正文。
56
+ *
57
+ * @param {string} body
58
+ * @param {object} opts
59
+ * @param {number} opts.wUnits 目标宽(格数,24px 一格)—— 跟真落板同一把尺
60
+ * @param {string} [opts.size] 字号档(同 write_on_board 的 size)
61
+ * @param {number} opts.maxH 单条上限(px;通常 CARD_MAX_H)
62
+ * @returns {string[]} 每条的正文。measure 用的是 textBox —— 跟落位那一刻同一个
63
+ * 函数量出来的数,不存在"估的和真的不一样"这层误差。
64
+ */
65
+ export function flowChunks(body, { wUnits, size = 'md', maxH }) {
66
+ const measure = (t) => textBox(t, size === 'sm' ? 'md' : size, { md: true, wUnits }).h;
67
+ const fits = (t) => measure(t) <= maxH;
68
+
69
+ // 先把超高的单块降级拆开,得到一串"每个都尽量 ≤ maxH"的原子
70
+ const atoms = [];
71
+ for (const b of splitBlocks(body)) {
72
+ if (fits(b)) { atoms.push(b); continue; }
73
+ for (const s of splitSentences(b)) atoms.push(s);
74
+ }
75
+ if (!atoms.length) return [];
76
+
77
+ // 贪心装填:能续就续。用 \n\n 拼回 —— 空行本来就是它们之间的边界
78
+ const chunks = [];
79
+ let cur = atoms[0];
80
+ for (const a of atoms.slice(1)) {
81
+ const joined = `${cur}\n\n${a}`;
82
+ if (fits(joined)) { cur = joined; continue; }
83
+ chunks.push(cur);
84
+ cur = a;
85
+ }
86
+ chunks.push(cur);
87
+ return chunks;
88
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * server/lib/chalk-rewrite.js —— 「重写一条板书的正文」这一件事,收成一份
3
+ * (2026-08-30)
4
+ *
5
+ * 在这之前只有 `edit_board` 的 set_text 会做这件事(edit-board.js 的板书文件分支),
6
+ * 而它做对了三件容易漏的:
7
+ * 1. 读原文 → 只换 body,frontmatter 逐字保留(by/at/anchor/reply_to/tag/session)
8
+ * —— `renderChalk` 只回写白名单键,拼错一个键就是整行静默丢失
9
+ * 2. 宽度沿用现有的(改正文不该改版心),高度按新正文重算
10
+ * 3. **用户亲手拖出来的留白留得住**(`sized:'user'` 时取重算高与现高的较大者)
11
+ * —— 他调的是「这一块留多少空」,重写一次正文就抹掉,是把他的排版意图当缓存
12
+ *
13
+ * `set_vars` 要做的是同一件事(改表里一格 = 重写这条板书的正文),所以这里抽出来
14
+ * 两处共用,而不是抄第二份。抄的话第 3 条几乎必然被漏掉 —— 它是三条里唯一
15
+ * 「漏了也不报错、只是用户的排版悄悄没了」的那条。
16
+ *
17
+ * ⚠️ 笔权(只有作者本人能改自己的话)**不在这里判** —— 那是调用方的事:
18
+ * edit_board 按 `by` 判,set_vars 有自己的规矩(状态表是场务件不是谁的台词)。
19
+ * 放进来会让两个调用方共享一条它们其实不共享的语义。
20
+ */
21
+
22
+ import { promises as fs } from 'node:fs';
23
+ import { parseChalk, renderChalk } from './chalk.js';
24
+ import { STATE_TABLE_TAG, parseStateTable } from './state-table.js';
25
+ import { UNIT, textBox } from './sketch-layout.js';
26
+
27
+ /**
28
+ * 重写一条板书文件的正文,并算出它在板上的新尺寸。
29
+ *
30
+ * @param {string} abs 板书文件的绝对路径
31
+ * @param {string} body 新正文
32
+ * @param {object} entry 这条板书在 board.json 里的条目(要 w / h / sized / by)
33
+ * @returns {Promise<{ w:number, h:number }>} 新的盒子(宽沿用、高按正文重算)
34
+ * @throws 文件读不到 / 写不进时抛,调用方自己决定怎么说
35
+ */
36
+ export async function rewriteChalkBody(abs, body, entry = {}) {
37
+ const parsed = parseChalk(await fs.readFile(abs, 'utf8'));
38
+ const c = parsed.chalk || {};
39
+ // 状态表堵写口(2026-08-30):这条板书是载重的(set_vars/nd:triggers/draw_trend
40
+ // 全读它),一次"成功"的重写就能把它们全弄死 —— glm 真会话用 \r 系字面量重写,
41
+ // set_text 报 Applied 2/2、表却永久 broken。重写后表必须还在,不在就大声拒。
42
+ if (c.tag === STATE_TABLE_TAG) {
43
+ const t = parseStateTable(String(body).replace(/\r\n?/g, '\n'));
44
+ if (!t.ok) {
45
+ const ex = new Error(`这一改会把状态表写坏(${t.error === 'no-table' ? '「| 键 | 值 |」表没了' : t.error})—— 什么都没写。改值用 set_vars;重写正文要保证那张两列表还在。`);
46
+ ex.code = 'STATE_TABLE';
47
+ throw ex;
48
+ }
49
+ }
50
+ await fs.writeFile(abs, renderChalk({
51
+ body,
52
+ by: c.by || entry.by || 'agent',
53
+ ...(c.at ? { at: c.at } : {}),
54
+ anchor: c.anchor,
55
+ replyTo: c.replyTo,
56
+ tag: c.tag,
57
+ sessionId: parsed.sessionId,
58
+ }), 'utf8');
59
+
60
+ const box = textBox(body, 'md', { md: true, wUnits: Math.max(8, Math.round((entry.w || 432) / UNIT)) });
61
+ const h = entry.sized === 'user' ? Math.max(box.h, Number(entry.h) || 0) : box.h;
62
+ return { w: box.w, h };
63
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * lib/chalk-size-pref.js —— 「用户喜欢多宽的板书」(2026-08-28)
3
+ *
4
+ * ## 为什么学得到
5
+ *
6
+ * 板书的**宽是真排版**(前端 BoardObject 的 `width: sz.w` 决定折行),高只是脚印
7
+ * (盒子不设 height,由正文撑)。所以用户拖宽一块板书是一个有信息量的动作:他在说
8
+ * "这个宽度读着舒服"。以前这个动作留不下任何痕迹,下一拍 agent 又按正文估一遍宽,
9
+ * 用户就得反复调同一件事。
10
+ *
11
+ * 学习票源是 `sized: 'user'` —— 前端拖完手柄盖的章,**不是模型能写的东西**
12
+ * (sanitizeObject 白名单收,模型走 patchBoard 也盖不出别人的章)。
13
+ *
14
+ * ## 为什么只学宽不学高
15
+ *
16
+ * 高度跟着正文长短走,一段三行的话和一段三十行的话不该一样高。用户拖高度调的是
17
+ * "这一块留多少空",那是**这一块**的属性,不是可以套到下一块上的偏好。宽度不同:
18
+ * 它是版心,跨块稳定。硬把高度也套过去,结果是每块板书都拖着一截空白。
19
+ *
20
+ * ## 取最近三块的中位数
21
+ *
22
+ * 板书 id 是 `notes/板书/<时间戳>-<slug>.md`,时间戳可直接字典序排 —— 不用另存时间。
23
+ * 取最近三块:一块就听它的(他刚调完,下一拍就该照做),三块取中位数(一次手滑
24
+ * 拖出个极端值不至于带偏后面所有的)。
25
+ */
26
+
27
+ /** 网格单位(px)。跟 write_on_board 的 width 参数同一把尺 */
28
+ const GRID = 24;
29
+ /** 学习窗口:最近几块用户亲手调过的板书 */
30
+ const WINDOW = 3;
31
+
32
+ /**
33
+ * @param {object} board board.json(sanitize 过的)
34
+ * @returns {number|null} 网格单位的宽;没有票就 null(调用方回落按正文估)
35
+ */
36
+ export function learnedChalkWidth(board) {
37
+ const votes = Object.entries(board?.objects || {})
38
+ .filter(([id, e]) => e?.sized === 'user' && Number(e.w) > 0 && id.includes('/板书/'))
39
+ .sort(([a], [b]) => (a < b ? 1 : -1)) // id 里的时间戳倒序 = 最近的在前
40
+ .slice(0, WINDOW)
41
+ .map(([, e]) => Math.round(Number(e.w) / GRID));
42
+ if (!votes.length) return null;
43
+ const sorted = [...votes].sort((a, b) => a - b);
44
+ const mid = sorted[Math.floor(sorted.length / 2)];
45
+ // 回到 write_on_board 的合法区间(schema 是 8..60);超出就当没学到,别拿一个
46
+ // 会被 schema 打回来的值去顶替一个能用的估算
47
+ return mid >= 8 && mid <= 60 ? mid : null;
48
+ }
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * frontmatter(只认这几个键,其余原样当正文):
13
13
  * nd: chalk 标记(没有它就是普通便利贴)
14
- * by: agent|user
14
+ * by: agent | user | rp-<slug>(常驻角色,2026-08-26)
15
15
  * at: ISO 时间
16
16
  * anchor: <canvas id> 这段话关于谁(落盘时顺手画 annotates 线)
17
17
  * reply_to: <path> 回应哪一条板书(线程)
@@ -19,11 +19,26 @@
19
19
  * session: <sid> (沿用便利贴的归属字段)
20
20
  */
21
21
  import path from 'node:path';
22
+ import { ROLE_SLUG_RE } from '../engine/agent/cast.js';
22
23
  import { promises as fs } from 'node:fs';
23
24
 
24
25
  export const CHALK_DIR = 'notes/板书';
25
26
  const FM_RE = /^---\n([\s\S]{0,800}?)\n---\n?/;
26
27
 
28
+ /**
29
+ * 板书的署名收成三类:`user` / `agent` / 常驻角色的 slug(`rp-*`)。
30
+ *
31
+ * 为什么不原样全收:`by` 会被前端拿去显示、被 read_board 拿去分段、将来还要被
32
+ * 收件箱拿去路由。收任意字符串等于给这几个读者一起开口子(而写这个值的是模型)。
33
+ * 认不出的一律落回 'agent' —— 老板书没有这个字段,落回去正好是它本来的语义。
34
+ */
35
+ export function normalizeBy(raw) {
36
+ const v = String(raw || '').trim();
37
+ if (v === 'user') return 'user';
38
+ if (ROLE_SLUG_RE.test(v)) return v;
39
+ return 'agent';
40
+ }
41
+
27
42
  export function parseChalk(raw) {
28
43
  const m = FM_RE.exec(raw);
29
44
  if (!m) return { body: raw, chalk: null, sessionId: null };
@@ -35,7 +50,7 @@ export function parseChalk(raw) {
35
50
  body,
36
51
  sessionId: get('session'),
37
52
  chalk: {
38
- by: get('by') === 'user' ? 'user' : 'agent',
53
+ by: normalizeBy(get('by')),
39
54
  at: get('at'),
40
55
  anchor: get('anchor'),
41
56
  replyTo: get('reply_to'),
@@ -50,7 +65,9 @@ export function renderChalk({ body, by = 'agent', at = new Date().toISOString(),
50
65
  if (replyTo) lines.push(`reply_to: ${replyTo}`);
51
66
  if (tag) lines.push(`tag: ${tag}`);
52
67
  if (sessionId) lines.push(`session: ${sessionId}`);
53
- lines.push('---', '', String(body).trim(), '');
68
+ // CR 系换行在唯一写口归一(2026-08-30):glm 真会话把换行写成 \r 系字面量,
69
+ // 整张表被压成一行还落了盘 —— CR 在板书正文里没有合法用途,进门就换成 \n。
70
+ lines.push('---', '', String(body).replace(/\r\n?/g, '\n').trim(), '');
54
71
  return lines.join('\n');
55
72
  }
56
73
 
@@ -79,6 +96,28 @@ export async function writeChalkFile(sharedRoot, fileName, content, { overwrite
79
96
  return `${CHALK_DIR}/${name}`;
80
97
  }
81
98
 
99
+ /**
100
+ * 软删(2026-08-25,信箱 iss_mt8cvn16:板书 rm 后无法恢复):删除入口一律把文件
101
+ * 挪进 `.nd/trash/<YYYYMMDD>/` 而不是 unlink —— `.nd/` 不上画布不进 git,用户体感
102
+ * 就是删了,但捞得回来。挪不动(跨盘等)才退回真删。
103
+ */
104
+ export async function trashChalkFile(sharedRoot, absPath) {
105
+ try {
106
+ const day = new Date().toISOString().slice(0, 10).replace(/-/g, '');
107
+ const dir = path.join(sharedRoot, '.nd', 'trash', day);
108
+ await fs.mkdir(dir, { recursive: true });
109
+ let dest = path.join(dir, path.basename(absPath));
110
+ for (let i = 2; i < 100; i += 1) {
111
+ try { await fs.access(dest); dest = path.join(dir, path.basename(absPath).replace(/\.md$/, `-${i}.md`)); } catch { break; }
112
+ }
113
+ await fs.rename(absPath, dest);
114
+ return dest;
115
+ } catch {
116
+ try { await fs.unlink(absPath); } catch { /* 已经没了 */ }
117
+ return null;
118
+ }
119
+ }
120
+
82
121
  /** 最近的板书(给注入用):按文件名(=时间戳)倒序,读前几条的首行 */
83
122
  export async function recentChalk(sharedRoot, { limit = 8 } = {}) {
84
123
  const dir = path.join(sharedRoot, CHALK_DIR);
@@ -21,6 +21,9 @@
21
21
  import http from 'node:http';
22
22
  import https from 'node:https';
23
23
  import { toOpenAIChatRequest, fromOpenAIChatResponse, toAnthropicError, OpenAIToAnthropicSSE, truncationOfChatResponse } from './openai-chat.js';
24
+ import { upstreamCostOf } from './upstream-billing.js';
25
+ import { upstreamErrorHint } from './upstream-error-hints.js';
26
+ import { armIdleWatchdog } from './stream-watchdog.js';
24
27
 
25
28
  export const DEFAULT_EMPTY_RETRIES = 2;
26
29
  export const DEFAULT_RETRY_BUDGET_MS = 120_000;
@@ -53,7 +56,7 @@ export function retryBudgetMs(env = process.env) {
53
56
  export function forwardOpenAIChat({ parsed, wire, key, res, sidShort, target, path, agent, onOutcome = () => {}, onBilling = () => {}, onTruncated = () => {}, onNotice = () => {} }) {
54
57
  const wantStream = !!parsed.stream;
55
58
  const label = wire.upstream?.label || wire.upstreamId;
56
- const body = toOpenAIChatRequest(parsed, { reasoningEffort: wire.reasoningEffort, maxOutput: wire.maxOutput });
59
+ const body = toOpenAIChatRequest(parsed, { reasoningEffort: wire.reasoningEffort, maxOutput: wire.maxOutput, bodyExtra: wire.bodyExtra });
57
60
  const outBody = Buffer.from(JSON.stringify(body), 'utf8');
58
61
  const useHttps = target.protocol === 'https:';
59
62
  const headers = {
@@ -214,7 +217,8 @@ export function forwardOpenAIChat({ parsed, wire, key, res, sidShort, target, pa
214
217
  if (settled || dead) return;
215
218
  settled = true;
216
219
  currentReq = null;
217
- const msg = text?.trim() ? text : `${label} 上游返回 ${status}(模型暂时不可用,稍后再发一次)`;
220
+ // 上游 4xx 的英文原文会原样落到用户的聊天框里 —— 能给出路的那几条翻成人话(原文保留在括号里)
221
+ const msg = upstreamErrorHint(text, wire) || (text?.trim() ? text : `${label} 上游返回 ${status}(模型暂时不可用,稍后再发一次)`);
218
222
  console.warn(`[model-ingress] sid=${sidShort} upstream=${wire.upstreamId} ${status} model=${wire.wireModel} body=${String(text || '').slice(0, 200).replace(/\s+/g, ' ')}`);
219
223
  failHard(status, msg, `HTTP ${status}${streaming ? ' on retry' : ''}`);
220
224
  };
@@ -234,6 +238,12 @@ export function forwardOpenAIChat({ parsed, wire, key, res, sidShort, target, pa
234
238
  proxyRes.on('aborted', () => attemptOver('aborted'));
235
239
  proxyRes.on('error', (err) => attemptOver(err.code || err.message));
236
240
  proxyRes.on('end', () => attemptOver(null));
241
+ // 断流看门狗(2026-08-29):destroy 触发上面的 'error' → attemptOver 走
242
+ // 现成的判决/重发机 —— 不新开收尾路,只补「静默也算死」这一种死法。
243
+ armIdleWatchdog(proxyRes, { onIdle: (silentMs) => {
244
+ console.warn(`[model-ingress] sid=${sidShort} upstream=${wire.upstreamId} 流静默 ${Math.round(silentMs / 1000)}s —— 看门狗掐断死流`);
245
+ try { proxyRes.destroy(new Error('stream idle watchdog')); } catch { /* 已经死了就算了 */ }
246
+ } });
237
247
  proxyRes.pipe(xf, { end: false }); // end:false —— 这条 SSE 还要接着用(可能再打一发)
238
248
  }, (detail) => attemptOver(detail, false)); // 连不上 / RST:同一条判决路,绝不自己碰 res
239
249
  };
@@ -265,7 +275,8 @@ export function forwardOpenAIChat({ parsed, wire, key, res, sidShort, target, pa
265
275
  const text = Buffer.concat(chunks).toString('utf8');
266
276
  if (status >= 400) {
267
277
  console.warn(`[model-ingress] sid=${sidShort} upstream=${wire.upstreamId} ${status} model=${wire.wireModel} body=${text.slice(0, 200).replace(/\s+/g, ' ')}`);
268
- const msg = text.trim() ? text : `${label} 上游返回 ${status}(模型暂时不可用,稍后再发一次)`;
278
+ // ⚠️ 跟上面流式那条是**孪生的两条路**,翻译要两边都挂(生产走流式,探针走非流式,只补一边会以为修好了)
279
+ const msg = upstreamErrorHint(text, wire) || (text.trim() ? text : `${label} 上游返回 ${status}(模型暂时不可用,稍后再发一次)`);
269
280
  onOutcome(false, `HTTP ${status}`);
270
281
  const errBody = JSON.stringify(toAnthropicError(status, msg));
271
282
  res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(errBody)) });
@@ -292,7 +303,8 @@ export function forwardOpenAIChat({ parsed, wire, key, res, sidShort, target, pa
292
303
  }
293
304
  onOutcome(true);
294
305
  onTruncated(truncationOfChatResponse(upstreamJson));
295
- if (upstreamJson.cost != null || upstreamJson.usage) onBilling({ costUsd: upstreamJson.cost ?? null, usage: upstreamJson.usage || null });
306
+ const costUsd = upstreamCostOf(upstreamJson); // Zen 放顶层、Merge 网关放 usage.cost
307
+ if (costUsd != null || upstreamJson.usage) onBilling({ costUsd, usage: upstreamJson.usage || null });
296
308
  const respBody = JSON.stringify(out);
297
309
  res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(respBody)) });
298
310
  res.end(respBody);
@@ -4,10 +4,12 @@
4
4
  * ## 为什么有这层
5
5
  *
6
6
  * SDK binary 永远说 Anthropic Messages;model-ingress 以前只会**转发**(上游也说 Anthropic)。
7
- * OpenCode Zen 的免费模型 Ox Alpha(x-preview-f-free)只有 OpenAI chat 格式能用工具
8
- * (Zen 给它架的 /v1/messages 桥一带 tools 就 [1210],08-21 四种写法探死),newapi 中转站
9
- * 同病。所以协议映射得自己做 —— 不上 gproxy(外部守护进程 + 四个已知洞 + 第二个 quirk
10
- * 真相源),在 ingress 里按上游 `protocol: 'openai-chat'` 分岔,其余上游一字不动。
7
+ * OpenCode Zen 整家**只有 OpenAI chat 格式能用工具**(它给模型架的 /v1/messages 桥一带 tools
8
+ * 就 [1210],08-21 拿当时的 Ox Alpha 四种写法探死),newapi 中转站、NVIDIA build 同病。所以协议
9
+ * 映射得自己做 —— 不上 gproxy(外部守护进程 + 四个已知洞 + 第二个 quirk 真相源),在 ingress 里按
10
+ * 上游 `protocol: 'openai-chat'` 分岔,其余上游一字不动。
11
+ * ⭐ 逼出这层的 Ox 08-26 就下架了,这层照常服务着 zen/zenGo/nvidia 三家 —— **闸和转换层才是耐久
12
+ * 资产,模型行是插件**(08-21 加 Ox 时写下的判断,一周后应验)。
11
13
  *
12
14
  * ## 映射要点(都是探针实测逼出来的,不是抄规范)
13
15
  *
@@ -16,7 +18,7 @@
16
18
  * 文本混排 → 先吐 role:tool 条,再吐 role:user 条(文本 + 提出来的图)
17
19
  * - thinking 块不回传(没有 signature 机制);assistant 历史里的 thinking 合成 reasoning_content
18
20
  * (models.dev 标 interleaved.field=reasoning_content,回传给模型接着想)
19
- * - Anthropic thinking 参数 → reasoning_effort(行内 reasoningEffort,Ox 三档 low|high|max
21
+ * - Anthropic thinking 参数 → reasoning_effort(行内 reasoningEffort;zen 系普遍是 low|high|max 三档,**没有 medium**)
20
22
  * - 流式:OpenAI chunk → 合成 message_start / content_block_* / message_delta / message_stop;
21
23
  * usage 在最后一个 chunk(stream_options.include_usage),Anthropic 口径 input 不含 cache 命中
22
24
  * - stop_reason:tool_calls→tool_use · stop→end_turn · length→max_tokens;有 tool_calls 但
@@ -34,6 +36,7 @@
34
36
  * 会话直接判死 —— 现有的 content_filter→end_turn 在 Claude Code 语境下才是对的
35
37
  */
36
38
  import { Transform } from 'node:stream';
39
+ import { upstreamCostOf } from './upstream-billing.js';
37
40
 
38
41
  const STOP_MAP = { tool_calls: 'tool_use', stop: 'end_turn', length: 'max_tokens', content_filter: 'end_turn', function_call: 'tool_use' };
39
42
 
@@ -97,7 +100,7 @@ function splitToolResult(block) {
97
100
 
98
101
  /**
99
102
  * @param {object} parsed Anthropic Messages body(已过 transformForUpstream:model 已是 wireModel)
100
- * @param {{ reasoningEffort?: string, maxOutput?: number }} opts
103
+ * @param {{ reasoningEffort?: string, maxOutput?: number, bodyExtra?: object|null }} opts
101
104
  * @returns {object} OpenAI chat.completions body
102
105
  */
103
106
  export function toOpenAIChatRequest(parsed, opts = {}) {
@@ -169,6 +172,9 @@ export function toOpenAIChatRequest(parsed, opts = {}) {
169
172
  // 档位只看行内 reasoningEffort:Anthropic 的 thinking 字段在进到这里之前已被 transformForUpstream
170
173
  // 按行内 thinking:'strip' 删掉(fable 评审抓的:以前以它存在为前提,档位从没发出去过)
171
174
  if (opts.reasoningEffort && parsed.thinking?.type !== 'disabled') out.reasoning_effort = opts.reasoningEffort;
175
+ // 上游要的额外顶层字段(merge 的 `vendor` 点名)。最后合、且允许盖上面任何一个键:
176
+ // 它是行里明写的上游特配,比这里推导出来的默认值更该赢
177
+ if (opts.bodyExtra) Object.assign(out, opts.bodyExtra);
172
178
  return out;
173
179
  }
174
180
 
@@ -200,7 +206,10 @@ export function fromOpenAIChatResponse(json) {
200
206
  const choice = json.choices[0] || {};
201
207
  const m = choice.message || {};
202
208
  const content = [];
203
- if (m.reasoning_content) content.push({ type: 'thinking', thinking: String(m.reasoning_content), signature: '' });
209
+ // 思考文本的字段名各家不一样:zen 系是 reasoning_content(models.dev interleaved.field),
210
+ // Merge 网关是 `thinking`(另带 thinking_signature,我们不回传签名所以不取)。先认前者,回退后者。
211
+ const reasoning = m.reasoning_content || m.thinking;
212
+ if (reasoning) content.push({ type: 'thinking', thinking: String(reasoning), signature: '' });
204
213
  if (m.content) content.push({ type: 'text', text: String(m.content) });
205
214
  if (m.refusal) content.push({ type: 'text', text: String(m.refusal) });
206
215
  for (const c of m.tool_calls || []) {
@@ -270,7 +279,7 @@ export class OpenAIToAnthropicSSE extends Transform {
270
279
  this.sawText = false; // 有过可见正文(区分"只想没说"的早断流)
271
280
  this.failReason = null; // 本次以 error 事件收场的原因(forward 层据此记会话失败计数;null = 正常收尾)
272
281
  this.truncated = null; // 本次「半截」收场的原因(见 truncationReason;forward 层据此报给 session-loop 续接)
273
- this.cost = null; // 上游报的本次费用(美元,/zen/go 在 [DONE] 后补的 cost 字段;null = 上游没报)
282
+ this.cost = null; // 上游报的本次费用(美元,/zen/go 在 [DONE] 后补顶层 cost、Merge 网关放 usage.cost;null = 上游没报)
274
283
  this.doneSeen = false; // 见过 [DONE]:之后只收 cost,收尾等 _flush
275
284
  this.attempts = 1; // 这条 SSE 一共打了几发上游(就地重发会 ++)
276
285
  this.attemptUsage = null; // 这一发的 usage
@@ -305,13 +314,15 @@ export class OpenAIToAnthropicSSE extends Transform {
305
314
  _handleChunk(chunk) {
306
315
  this._ensureStart(chunk);
307
316
  if (chunk.usage) this.attemptUsage = chunk.usage;
308
- if (chunk.cost != null) this.attemptCost = Number(chunk.cost);
317
+ const chunkCost = upstreamCostOf(chunk); // 顶层 cost(Zen)或 usage.cost(Merge 网关)
318
+ if (chunkCost != null) this.attemptCost = chunkCost;
309
319
  const choice = chunk.choices?.[0];
310
320
  if (!choice) return;
311
321
  const d = choice.delta || {};
312
- if (d.reasoning_content) {
322
+ const reasoningDelta = d.reasoning_content || d.thinking; // 同上:Merge 网关的增量字段叫 thinking
323
+ if (reasoningDelta) {
313
324
  if (this.open?.kind !== 'thinking') this._openBlock('thinking', { type: 'thinking', thinking: '', signature: '' });
314
- this._emit('content_block_delta', { type: 'content_block_delta', index: this.open.index, delta: { type: 'thinking_delta', thinking: String(d.reasoning_content) } });
325
+ this._emit('content_block_delta', { type: 'content_block_delta', index: this.open.index, delta: { type: 'thinking_delta', thinking: String(reasoningDelta) } });
315
326
  }
316
327
  // content 与 refusal 都是"可见正文",进同一个 text 块(拒答也是模型说的话,原样吐给用户)
317
328
  for (const piece of [d.content, d.refusal]) {
@@ -471,7 +482,7 @@ export class OpenAIToAnthropicSSE extends Transform {
471
482
  if (payload === '[DONE]') { this.doneSeen = true; continue; }
472
483
  let j;
473
484
  try { j = JSON.parse(payload); } catch { continue; }
474
- if (this.doneSeen || this.done) { if (j && j.cost != null) this.attemptCost = Number(j.cost); continue; }
485
+ if (this.doneSeen || this.done) { const c = upstreamCostOf(j); if (c != null) this.attemptCost = c; continue; }
475
486
  if (j?.error) { // 流中途的错误体:转成 Anthropic error 事件
476
487
  this._ensureStart(j);
477
488
  this._closeOpen(); // 先把开着的块闭合再发 error(顺序反了会出现 error 后面还跟 content_block_stop)
@@ -11,9 +11,9 @@
11
11
  * 成功转发不留日志。表级断言封不住(SDK 内部会用哪些名字没法枚举),所以是**会话级路由**:一个会话只认
12
12
  * 自己那行和自己的 fast 行,其它一概改道 fast 兜底,绝不跨行。
13
13
  *
14
- * 08-22 起**会话优先**而不是全表优先:本地分发版的外部插槽共用同一个 sdkAlias(表内只剩一个空着的 1M 订阅名,
15
- * model-context.js EXTERNAL_SDK_ALIAS),全表反查分不出它们,只有会话知道自己是谁。先问会话、再查全表,
16
- * 对内置行结果不变(自己的 alias 反查到的就是自己)。
14
+ * 08-22 起**会话优先**而不是全表优先:不写 sdkAlias 的行(外部插槽全部 + 08-25 起的内置新行)共用同一个
15
+ * spoof 名(model-table.js SHARED_SDK_ALIAS),全表反查分不出它们,只有会话知道自己是谁。先问会话、再查全表,
16
+ * 对独占别名的行结果不变(自己的 alias 反查到的就是自己)。
17
17
  */
18
18
 
19
19
  import { resolveWireModel, resolveModelRoute, wireNamesOf } from '../../engine/agent/model-context.js';
@@ -0,0 +1,53 @@
1
+ /**
2
+ * server/lib/ingress/stream-watchdog.js —— 上游流断死看门狗(2026-08-29)
3
+ *
4
+ * 现场(proj_mtexu1kp 纸范式验收当晚):glm-5.3-flash-zai 在开始流 tool_use 入参时
5
+ * 断粮 —— 没有错误、没有 EOF、没有日志,TCP 就那么开着不再来字节。run 无限挂起,
6
+ * 前端还留着一张流到一半的工具卡,直到用户手动中断。**用户不该替我们当看门狗。**
7
+ *
8
+ * 判据:SSE 的正常节奏是亚秒级 chunk(thinking 长停顿也有事件/ping 垫着),静默
9
+ * 超过 IDLE_MS = 死流。掐掉(destroy)让下游走各自现成的错误路:
10
+ * - Anthropic 直连透传路:CLI 收到流错误 → 自己重试
11
+ * - openai-chat 转换路:proxyRes 'error' → attemptOver → 就地重发/判决机
12
+ *
13
+ * 只对流式响应上岗(非流式的整包响应可以长时间零字节,是合法的慢)。
14
+ */
15
+
16
+ const DEFAULT_IDLE_MS = 180_000;
17
+ const CHECK_MS = 15_000;
18
+
19
+ export function idleMsFromEnv() {
20
+ const n = Number(process.env.NODESIGN_INGRESS_IDLE_MS);
21
+ return Number.isFinite(n) && n >= 1_000 ? n : DEFAULT_IDLE_MS;
22
+ }
23
+
24
+ /**
25
+ * 给一条上游响应流装看门狗。返回解除函数(幂等)。
26
+ * @param {import('http').IncomingMessage} stream 上游响应
27
+ * @param {{ idleMs?: number, checkMs?: number, onIdle: (silentMs: number) => void }} opts
28
+ * onIdle 只会被调用一次;由调用方决定怎么掐(destroy)和记账(noteOutcome/日志)。
29
+ */
30
+ export function armIdleWatchdog(stream, { idleMs = idleMsFromEnv(), checkMs = CHECK_MS, onIdle }) {
31
+ let last = Date.now();
32
+ let fired = false;
33
+ const bump = () => { last = Date.now(); };
34
+ stream.on('data', bump);
35
+ const timer = setInterval(() => {
36
+ if (fired) return;
37
+ const silent = Date.now() - last;
38
+ if (silent > idleMs) {
39
+ fired = true;
40
+ stop();
41
+ try { onIdle(silent); } catch { /* 看门狗自己绝不抛 */ }
42
+ }
43
+ }, checkMs);
44
+ timer.unref?.();
45
+ function stop() {
46
+ clearInterval(timer);
47
+ stream.removeListener('data', bump);
48
+ }
49
+ stream.on('end', stop);
50
+ stream.on('close', stop);
51
+ stream.on('error', stop);
52
+ return stop;
53
+ }
@@ -12,6 +12,21 @@
12
12
  * 上游没报 cost(cost 为 null)就不动任何东西 —— 假数据比没有更坏。
13
13
  */
14
14
 
15
+ /**
16
+ * 上游把「这一发花了多少钱」放在哪儿,各家不一样:Zen(/zen/go)放在**顶层** `cost`
17
+ * (流式在 [DONE] 之后补一条 {"choices":[],"cost":"0.00123"}),Merge 网关放在 **usage.cost** 里
18
+ * (非流式在响应体的 usage 上,流式在 include_usage 那个末块的 usage 上)。两处都认,顶层优先。
19
+ * ⚠️ 缺席或不是数一律 null —— 记账那侧看见 null 就不动任何东西(假数据比没有更坏)。
20
+ */
21
+ export function upstreamCostOf(obj) {
22
+ for (const v of [obj?.cost, obj?.usage?.cost]) {
23
+ if (v == null) continue;
24
+ const n = Number(v);
25
+ if (Number.isFinite(n)) return n;
26
+ }
27
+ return null;
28
+ }
29
+
15
30
  export class UpstreamBilling {
16
31
  constructor() { this.map = new Map(); } // sid → Map<appModel, acc>
17
32
  /**
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 上游 4xx 的人话翻译(08-30 建)。
3
+ *
4
+ * 为什么需要:上游的错是**给开发者看的英文**,可它会原样穿过 SDK 落到用户的聊天框里。
5
+ * 用户看到的是「API Error: 400 GLM requests accept at most 8 inline PNG, JPEG, WEBP…」——
6
+ * 这句话没有一个字告诉他该怎么办,而这里恰好有个一句话就能给出的出路(换一条线)。
7
+ *
8
+ * ⭐ 只翻译**用户自己能处理**的那几条。上游挂了、限流、鉴权坏了这些他做不了什么,
9
+ * 翻译过去只是把英文换成中文,没有增益,反而把原文这个排查线索藏了 —— 所以不收。
10
+ * ⚠️ 原文一律保留在括号里:翻译是加一句,不是换一句。看日志和看聊天框的是同一个人。
11
+ */
12
+
13
+ /** 一条上游原文里的图片张数上限(particle 是 8)。这条闸只有 particle 有,zai 到 20 张都收 */
14
+ const INLINE_IMAGE_CAP = /accept at most (\d+) inline/i;
15
+
16
+ /**
17
+ * @param {string} raw 上游回的错误正文
18
+ * @param {object} [wire] 这一发用的行(要 bodyExtra.vendors 判是不是"只走 particle"那条)
19
+ * @returns {string|null} 加了人话的完整文案;没有可翻译的就回 null(调用方原样透传)
20
+ */
21
+ export function upstreamErrorHint(raw, wire) {
22
+ const text = String(raw || '');
23
+ const cap = text.match(INLINE_IMAGE_CAP);
24
+ if (cap) {
25
+ const n = cap[1];
26
+ // 只有"演出"那条线(点死 particle)才该建议换线;默认那条线不该撞到这条闸,
27
+ // 真撞到了说明是别的原因,别把人往一条同样会挂的线上引。
28
+ const onlyParticle = Array.isArray(wire?.bodyExtra?.vendors)
29
+ && wire.bodyExtra.vendors.length === 1 && wire.bodyExtra.vendors[0] === 'particle';
30
+ const hint = onlyParticle
31
+ ? `「演出」这条线整场最多带 ${n} 张图,这一轮超了。在模型菜单里换成「GLM-5.3-Flash · 设计」就没有这个限制 —— 同一个模型、同样的价钱,只是每一步稍慢一点。`
32
+ : `这一轮带的图超过了上游的 ${n} 张上限。少发几张、或者开一个新会话再继续。`;
33
+ return `${hint}(上游原文:${text.slice(0, 200).replace(/\s+/g, ' ')})`;
34
+ }
35
+ return null;
36
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * server/lib/json-preview.js —— json 文件卡的结构预览(2026-08-29 占位契约刀 B)
3
+ *
4
+ * 站主点名「给 json 一个预览器和显示器」。原来 json 跟 txt/csv 走同一条路:
5
+ * 截前 1KB 原样塞进等宽 <pre>,于是卡面上是半行 `{"name":"…` 就没了 —— 既看不出
6
+ * 结构,也**没法在前端画折叠树**(截断的 json parse 不动)。
7
+ *
8
+ * 这里做的是"裁剪"不是"截断":按深度/数组长度/字符串长度各自设限,产出的仍然是
9
+ * **合法 json**,前端 JSON.parse 得动就能画可折叠的键值树。被裁掉的地方留一句人话
10
+ * (`… +42 more`),不假装内容到此为止 —— 跟卡的折叠角标同一条纪律:
11
+ * 省略要看得见。
12
+ */
13
+
14
+ const MAX_DEPTH = 4;
15
+ const MAX_ITEMS = 12; // 数组/对象每层最多留几项
16
+ const MAX_STR = 120; // 单个字符串值
17
+ const MAX_OUT = 4096; // 产出上限,超了降一档深度重来
18
+
19
+ /** 折叠标记:值是以这个开头的字符串 = 这里被裁过(前端据此画成灰色省略号) */
20
+ export const ELLIPSIS = '…';
21
+
22
+ function shrink(v, depth, maxDepth) {
23
+ if (v === null || typeof v === 'number' || typeof v === 'boolean') return v;
24
+ if (typeof v === 'string') return v.length > MAX_STR ? `${v.slice(0, MAX_STR)}${ELLIPSIS}` : v;
25
+ if (Array.isArray(v)) {
26
+ if (depth >= maxDepth) return [`${ELLIPSIS} ${v.length} items`];
27
+ const out = v.slice(0, MAX_ITEMS).map(x => shrink(x, depth + 1, maxDepth));
28
+ if (v.length > MAX_ITEMS) out.push(`${ELLIPSIS} +${v.length - MAX_ITEMS} more`);
29
+ return out;
30
+ }
31
+ if (typeof v === 'object') {
32
+ const keys = Object.keys(v);
33
+ if (depth >= maxDepth) return { [ELLIPSIS]: `${keys.length} keys` };
34
+ const out = {};
35
+ for (const k of keys.slice(0, MAX_ITEMS)) out[k] = shrink(v[k], depth + 1, maxDepth);
36
+ if (keys.length > MAX_ITEMS) out[ELLIPSIS] = `+${keys.length - MAX_ITEMS} more keys`;
37
+ return out;
38
+ }
39
+ return String(v);
40
+ }
41
+
42
+ /**
43
+ * @param {string} raw 文件原文
44
+ * @returns {string|null} 裁剪后的合法 json 文本;不是合法 json 时返回 null(调用方退回原样预览)
45
+ */
46
+ export function jsonPreview(raw) {
47
+ let data;
48
+ try {
49
+ data = JSON.parse(raw);
50
+ } catch {
51
+ return null; // 不是合法 json(或本来就是片段):退回等宽原样,别假装看得懂
52
+ }
53
+ for (let d = MAX_DEPTH; d >= 1; d -= 1) {
54
+ const text = JSON.stringify(shrink(data, 0, d), null, 2);
55
+ if (text.length <= MAX_OUT || d === 1) return text.slice(0, MAX_OUT);
56
+ }
57
+ return null;
58
+ }