@xiaobuyu/nodesign 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +42 -22
  9. package/server/api/assets/notes.js +194 -144
  10. package/server/api/assets.js +891 -896
  11. package/server/api/board.js +134 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +465 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1035 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +351 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +152 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/edit-sketch.js +167 -0
  120. package/server/engine/mcp/tools/explain-style.js +237 -237
  121. package/server/engine/mcp/tools/export-handoff.js +133 -133
  122. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  123. package/server/engine/mcp/tools/generate-image.js +842 -842
  124. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  125. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  126. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  127. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  128. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  129. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  130. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  131. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  132. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  133. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  134. package/server/engine/mcp/tools/highlight.js +68 -68
  135. package/server/engine/mcp/tools/list-pages.js +222 -222
  136. package/server/engine/mcp/tools/look-at-board.js +124 -0
  137. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  138. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  139. package/server/engine/mcp/tools/organize-board.js +64 -64
  140. package/server/engine/mcp/tools/paint-still.js +434 -434
  141. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  142. package/server/engine/mcp/tools/preview-deck.js +130 -130
  143. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  144. package/server/engine/mcp/tools/publish-site.js +102 -102
  145. package/server/engine/mcp/tools/query-elements.js +195 -195
  146. package/server/engine/mcp/tools/read-board.js +190 -103
  147. package/server/engine/mcp/tools/read-document.js +93 -93
  148. package/server/engine/mcp/tools/read-page.js +264 -264
  149. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  150. package/server/engine/mcp/tools/read-user-view.js +59 -0
  151. package/server/engine/mcp/tools/record-decision.js +140 -140
  152. package/server/engine/mcp/tools/relate-on-board.js +112 -110
  153. package/server/engine/mcp/tools/report-issue.js +115 -115
  154. package/server/engine/mcp/tools/roll-film.js +266 -266
  155. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  156. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  157. package/server/engine/mcp/tools/screenshot.js +583 -583
  158. package/server/engine/mcp/tools/sketch-on-board.js +337 -0
  159. package/server/engine/mcp/tools/tier-gate.js +96 -96
  160. package/server/engine/mcp/tools/trace-motion.js +215 -215
  161. package/server/engine/mcp/tools/web-search.js +548 -548
  162. package/server/engine/mcp/tools/write-on-board.js +130 -0
  163. package/server/engine/motion/inventory.js +349 -349
  164. package/server/engine/perception/session.js +235 -235
  165. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  174. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
  175. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  176. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  177. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
  178. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
  179. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  180. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
  181. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  182. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  183. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  185. package/server/engine/runs/active-runs.js +698 -698
  186. package/server/engine/runs/board-tasklist.js +130 -0
  187. package/server/engine/runs/live-turn.js +252 -252
  188. package/server/engine/runs/store.js +364 -364
  189. package/server/engine/runs/turn-relay.js +217 -217
  190. package/server/engine/runtime/workspace.js +164 -164
  191. package/server/index.js +3 -0
  192. package/server/lib/artifact-file-path.js +80 -80
  193. package/server/lib/artifact-target.js +284 -284
  194. package/server/lib/asset-refs.js +239 -239
  195. package/server/lib/async-queue.js +104 -104
  196. package/server/lib/auto-relations.js +130 -130
  197. package/server/lib/binding-types.js +72 -60
  198. package/server/lib/board-graph-export.js +191 -0
  199. package/server/lib/board-groups.js +167 -0
  200. package/server/lib/board-hero.js +76 -0
  201. package/server/lib/board-kind-sizes.js +85 -62
  202. package/server/lib/board-relations.js +133 -133
  203. package/server/lib/browse-proxy.js +180 -180
  204. package/server/lib/canvas-id.js +36 -36
  205. package/server/lib/chalk.js +121 -0
  206. package/server/lib/cover.js +227 -227
  207. package/server/lib/danbooru-tags.js +291 -291
  208. package/server/lib/doc-extract.js +156 -156
  209. package/server/lib/docx/build-from-source.js +260 -260
  210. package/server/lib/docx/build.js +490 -490
  211. package/server/lib/docx/dump-styles.js +287 -287
  212. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  213. package/server/lib/docx/merge-runs.js +129 -129
  214. package/server/lib/docx/numbering.js +218 -218
  215. package/server/lib/docx/order.js +152 -152
  216. package/server/lib/docx/rawzip.js +172 -172
  217. package/server/lib/docx/render.js +92 -92
  218. package/server/lib/docx/text-lint.js +225 -225
  219. package/server/lib/docx/tokens.js +335 -335
  220. package/server/lib/docx/units.js +29 -29
  221. package/server/lib/docx/xml.js +291 -291
  222. package/server/lib/docx-pages.js +187 -187
  223. package/server/lib/export-collect.js +289 -289
  224. package/server/lib/export-package.js +210 -210
  225. package/server/lib/html-srcset.js +145 -145
  226. package/server/lib/image-variant.js +460 -460
  227. package/server/lib/ingress/forward-openai-chat.js +305 -305
  228. package/server/lib/ingress/openai-chat.js +491 -491
  229. package/server/lib/ingress/session-notice.js +66 -66
  230. package/server/lib/ingress/session-routes.js +68 -68
  231. package/server/lib/ingress/slot-probe.js +130 -130
  232. package/server/lib/ingress/upstream-billing.js +52 -52
  233. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  234. package/server/lib/ingress/upstream-truncation.js +48 -48
  235. package/server/lib/issues-store.js +182 -182
  236. package/server/lib/kinds/deck.js +72 -72
  237. package/server/lib/kinds/docx.js +283 -283
  238. package/server/lib/kinds/file-kinds.js +113 -113
  239. package/server/lib/kinds/index.js +202 -202
  240. package/server/lib/kinds/site.js +161 -161
  241. package/server/lib/model-ingress.js +543 -543
  242. package/server/lib/moderation.js +255 -255
  243. package/server/lib/notice-store.js +103 -103
  244. package/server/lib/plugin-install.js +104 -104
  245. package/server/lib/plugin-validator.js +721 -721
  246. package/server/lib/publish-store.js +115 -115
  247. package/server/lib/quick-summary.js +52 -52
  248. package/server/lib/quota.js +270 -270
  249. package/server/lib/rate-window.js +29 -29
  250. package/server/lib/reference-assets.js +92 -92
  251. package/server/lib/region-shot.js +135 -135
  252. package/server/lib/safe-path.js +73 -73
  253. package/server/lib/sdk-session.js +33 -33
  254. package/server/lib/showcase-store.js +92 -92
  255. package/server/lib/site-publish.js +465 -465
  256. package/server/lib/sketch-layout.js +277 -0
  257. package/server/lib/ssrf-guard.js +432 -432
  258. package/server/lib/task-scan.js +158 -158
  259. package/server/lib/tavern-json.js +154 -154
  260. package/server/lib/video-variant.js +237 -237
  261. package/server/lib/workspace-path.js +33 -33
  262. package/server/ops/fix-sdk-musl.mjs +40 -40
  263. package/server/ops/install-macos-fonts.sh +114 -114
  264. package/server/ops/macos-fonts.conf +336 -336
  265. package/server/ops/sandbox-shim/bwrap +57 -57
  266. package/server/projects/assets-summary.js +119 -119
  267. package/server/projects/auto-name.js +53 -53
  268. package/server/projects/board-limits.js +12 -0
  269. package/server/projects/board-sanitize.js +252 -0
  270. package/server/projects/board-store.js +507 -640
  271. package/server/projects/move-entry.js +103 -103
  272. package/server/projects/store.js +270 -270
  273. package/server/projects/ui-config.js +60 -54
  274. package/server/projects/viewpoint-store.js +78 -0
  275. package/server/projects/workspace-templates.js +53 -53
  276. package/server/projects/workspace.js +1025 -1025
  277. package/server/runtime/local-config.js +177 -177
  278. package/server/runtime/profile.js +72 -72
  279. package/server/services/rembg-service.py +334 -334
  280. package/server/shared/README.md +25 -25
  281. package/server/shared/deck.js +54 -54
  282. package/server/shared/time.js +49 -49
  283. package/server/style-pipeline/README.md +53 -53
  284. package/server/ws/broker.js +39 -36
  285. package/server/ws/browse-channel.js +177 -177
  286. package/server/ws/index.js +386 -386
  287. package/web/README.md +89 -89
  288. package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
  289. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
  290. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
  291. package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
  292. package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
  293. package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
  294. package/web/dist/assets/arc-D_khH_xN.js +1 -0
  295. package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
  296. package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
  297. package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
  298. package/web/dist/assets/channel-DGkccMO1.js +1 -0
  299. package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
  300. package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
  301. package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
  302. package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
  303. package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
  304. package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
  305. package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
  306. package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
  307. package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
  308. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
  309. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
  310. package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
  311. package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
  312. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
  313. package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
  314. package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
  315. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  316. package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
  317. package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
  318. package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
  319. package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
  320. package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
  321. package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
  322. package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
  323. package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
  324. package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
  325. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
  326. package/web/dist/assets/index-GAamh1p4.js +2152 -0
  327. package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
  328. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  329. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
  330. package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
  331. package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
  332. package/web/dist/assets/layout-CnXiF_NB.js +1 -0
  333. package/web/dist/assets/linear-BMW0u6G6.js +1 -0
  334. package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
  335. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  336. package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
  337. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
  338. package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
  339. package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
  340. package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
  341. package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
  342. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
  343. package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
  344. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
  345. package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
  346. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
  347. package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
  348. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
  349. package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
  350. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
  351. package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
  352. package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
  353. package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
  354. package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
  355. package/web/dist/index.html +17 -17
  356. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
  357. package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
  358. package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
@@ -1,386 +1,386 @@
1
- /**
2
- * server/ws/index.js — WebSocket 升级入口
3
- *
4
- * URL 模式:/ws/projects/:pid
5
- *
6
- * 流程:
7
- * 1. http server 'upgrade' 事件触发
8
- * 2. parse URL 取 pid
9
- * 3. validateProjectId + getProject 校验存在
10
- * 4. wss.handleUpgrade → 拿到 ws 对象
11
- * 5. 订阅该 project 的 EventBus,事件 JSON.stringify 推 ws.send
12
- * 6. ping/pong 30s 心跳
13
- * 7. ws close → unsubscribe + clearInterval
14
- */
15
-
16
- import { WebSocketServer } from 'ws';
17
- import { createBrowseWS } from './browse-channel.js';
18
- import { URL } from 'url';
19
- import { getSessionMessages } from '@anthropic-ai/claude-agent-sdk';
20
- import { validateProjectId, getProject } from '../projects/store.js';
21
- import { getSessionWorkspace, validateSessionId } from '../projects/workspace.js';
22
- import { withConfigDir } from '../lib/sdk-session.js';
23
- import { platform } from '../runtime/platform.js';
24
- import { getProjectBus } from './broker.js';
25
- import { requestUser } from '../auth/session.js';
26
- import { originAllowed } from '../auth/origin-guard.js';
27
- import { userOwnsProject } from '../api/_guard.js';
28
- import {
29
- getCurrentTurnRunId,
30
- hasActiveQuerySession,
31
- closeQuerySession,
32
- markSessionActivity,
33
- } from '../engine/runs/active-runs.js';
34
- import { getLiveTurnSnapshot } from '../engine/runs/live-turn.js';
35
-
36
- const HEARTBEAT_INTERVAL_MS = 30_000;
37
- const HYDRATE_CHUNK_SIZE = 50;
38
- const GLOBAL_CLAUDE_CONFIG_DIR = platform.claudeConfigDir;
39
-
40
- /**
41
- * WS 全部断开后再 N ms 仍无重连 → closeQuerySession 让 SDK subprocess 退出,
42
- * 防止用户关 tab / 刷新后服务器孤儿 SDK process 永久占内存(每个 ~250MB RSS)。
43
- *
44
- * 默认 60s 给前端 ws-client.js exponential backoff(1s→2s→...→30s)足够重连窗口。
45
- * env NODESIGN_WS_GRACE_MS 可调(生产可拉到 5min 给移动端弱网)。
46
- */
47
- const WS_GRACE_MS = Number(process.env.NODESIGN_WS_GRACE_MS) || 60_000;
48
-
49
- /**
50
- * sid → { count: 当前活跃 WS 连接数, graceTimer: 0 引用时启动的关闭定时器 }
51
- *
52
- * 每条带 sid 的 WS 连接 ref++,close 时 ref--;归零启 grace timer,N ms 内有新 WS
53
- * 进同 sid 立即清 timer 续命;timer 到期再确认 0 ref 后 closeQuerySession。
54
- *
55
- * /work 路径(无 sid)的 WS 不进 sessionRefs(无 session 可绑)。
56
- */
57
- const sessionRefs = new Map();
58
-
59
- function refSession(sid) {
60
- if (!sid) return;
61
- let entry = sessionRefs.get(sid);
62
- if (!entry) {
63
- entry = { count: 0, graceTimer: null };
64
- sessionRefs.set(sid, entry);
65
- }
66
- entry.count += 1;
67
- if (entry.graceTimer) {
68
- clearTimeout(entry.graceTimer);
69
- entry.graceTimer = null;
70
- }
71
- // WS 连上算一次活跃信号(idle scan 不会立即把刚连的 session 当僵尸关掉)
72
- markSessionActivity(sid);
73
- }
74
-
75
- function unrefSession(sid) {
76
- if (!sid) return;
77
- const entry = sessionRefs.get(sid);
78
- if (!entry) return;
79
- entry.count = Math.max(0, entry.count - 1);
80
- if (entry.count > 0) return;
81
- // 0 ref → 启 grace timer。若 grace 期内有新 WS 进同 sid,refSession 会清掉 timer
82
- if (entry.graceTimer) clearTimeout(entry.graceTimer);
83
- const onGraceExpired = () => {
84
- const cur = sessionRefs.get(sid);
85
- if (!cur || cur.count > 0) return; // 期间有新连上,不关
86
- // 进行中的 turn 不杀(2026-07-27):用户关 tab / 切走时后端应该把活干完,
87
- // 前端重连后靠 hydrate + live_turn 快照接回状态。这正是"前端丢失后端工作
88
- // 状态"要保住的那半 —— 老逻辑 60s 就 cancel 在跑的 turn,是真丢工作。
89
- // 续一个 grace 周期再查;turn 结束后仍无订阅者才真正关(idle SDK 进程回收)。
90
- if (getCurrentTurnRunId(sid)) {
91
- console.info(`[ws] grace expired for sid=${sid.slice(0, 8)} but turn in flight — deferring close`);
92
- cur.graceTimer = setTimeout(onGraceExpired, WS_GRACE_MS);
93
- cur.graceTimer.unref?.();
94
- return;
95
- }
96
- sessionRefs.delete(sid);
97
- if (hasActiveQuerySession(sid)) {
98
- console.info(`[ws] grace expired for sid=${sid.slice(0, 8)}, closing session (no_active_subscriber)`);
99
- closeQuerySession(sid, 'no_active_subscriber');
100
- }
101
- };
102
- entry.graceTimer = setTimeout(onGraceExpired, WS_GRACE_MS);
103
- entry.graceTimer.unref?.();
104
- }
105
-
106
- export function setupWS(httpServer) {
107
- // 浏览器画面/输入的专用通道(2026-08-18),独立 WebSocketServer
108
- const browseWS = createBrowseWS();
109
- // perMessageDeflate:弱网下 hydrate chunk + ring-buffer replay 帧成本是 raw size
110
- // 量级(>2MB session 一次首屏 hydrate 估 250KB+ raw)。permessage-deflate 让
111
- // JSON 文本压到 5-10x。threshold 1KB 跳过小帧(控制帧无收益反增 CPU)。
112
- // env NODESIGN_WS_DISABLE_DEFLATE=1 应急关闭(CPU 紧的部署环境)。
113
- const wss = new WebSocketServer({
114
- noServer: true,
115
- perMessageDeflate: process.env.NODESIGN_WS_DISABLE_DEFLATE === '1'
116
- ? false
117
- : { threshold: 1024 },
118
- });
119
-
120
- httpServer.on('upgrade', (req, socket, head) => {
121
- // ⛔ CSRF 闸(2026-08-18):cookie 是 SameSite=Lax,而已发布站点与应用主机
122
- // 同 eTLD+1 = 同站,Lax 照发(真跑验过,见 auth/origin-guard.js 文件头)。
123
- // 这里**不学下面那个 4401 的握手后再关**:那套是为了让我们自己的前端看得见
124
- // 原因好停止重连;外站页面不需要体面的错误,403 直接掐在升级前最省。
125
- if (!originAllowed(req)) {
126
- socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
127
- return socket.destroy();
128
- }
129
-
130
- // 登录墙(2026-07-30 多用户):解析身份而非布尔。cookie 无效不再写 HTTP 401
131
- // 拒 upgrade —— 浏览器拿不到 upgrade 阶段的状态码,只见 close 1006,前端
132
- // ws-client 会无限指数退避重连。改为完成握手后 close(4401),前端把它列为
133
- // fatal code → 停止重连并回登录页。
134
- const user = requestUser(req);
135
-
136
- let url;
137
- try {
138
- url = new URL(req.url || '/', 'http://x');
139
- } catch {
140
- return socket.destroy();
141
- }
142
-
143
- // ⚠️ 浏览器画面通道的分支**必须排在下面那个 404 之前** —— 那句 404 是兜底,
144
- // 不匹配 `/ws/projects/:pid` 的路径会被它直接毙掉。
145
- // 为什么另开一条而不是复用这条:这条是纯下行 + EventBus 广播 + 2000 条重放
146
- // 缓冲,高频画面帧灌进去会冲爆重放缓冲、混进 hydrate 回放,而且 JSON+base64
147
- // 帧会被 perMessageDeflate 白压一遍。详见 ws/browse-channel.js 文件头。
148
- {
149
- const bm = browseWS.matches(url.pathname);
150
- if (bm) return browseWS.accept(req, socket, head, bm);
151
- }
152
-
153
- const m = url.pathname.match(/^\/ws\/projects\/([^/]+)$/);
154
- if (!m) {
155
- socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
156
- return socket.destroy();
157
- }
158
-
159
- const pid = decodeURIComponent(m[1]);
160
- try {
161
- validateProjectId(pid);
162
- } catch {
163
- socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
164
- return socket.destroy();
165
- }
166
-
167
- // 身份或归属不对 → 握手后 4401 关闭(同一个 code:外人不该分得清
168
- // "没登录"和"不是你的项目")
169
- const project = getProject(pid);
170
- if (!user || !userOwnsProject(user, project)) {
171
- wss.handleUpgrade(req, socket, head, (ws) => {
172
- ws.close(4401, 'unauthorized');
173
- });
174
- return;
175
- }
176
-
177
- // ?since=N — 客户端最后看到的 EventBus seq;server 通过 buffer 回放 (since, _seq] 段
178
- // 第一次连不带 since → since=0 → 不 replay 直接 live。
179
- const sinceRaw = url.searchParams.get('since');
180
- const since = sinceRaw != null ? Math.max(0, parseInt(sinceRaw, 10) || 0) : 0;
181
-
182
- // Phase A.4:?sid=<sessionId> — 客户端当前在哪个 session 上,server 在 first connect /
183
- // gap 时推 ws.hydrate.* 帧补完整 messages。无 sid 时(/work 路径)跳过 hydrate
184
- // 直接 replay+live。
185
- let sid = url.searchParams.get('sid');
186
- if (sid) {
187
- try { validateSessionId(sid); }
188
- catch { sid = null; }
189
- }
190
-
191
- wss.handleUpgrade(req, socket, head, (ws) => {
192
- handleProjectWS(ws, pid, since, sid);
193
- });
194
- });
195
-
196
- return wss;
197
- }
198
-
199
- /**
200
- * 读 jsonl 历史(异步,几百 ms)。发帧是另一个纯同步函数 —— 读完到发出之间不能
201
- * 再有 await,否则"读到的历史"与"当时的 live turn 快照"会错位(见 handleProjectWS)。
202
- */
203
- async function loadHydrate(pid, sid) {
204
- const sessionRoot = getSessionWorkspace(pid, sid);
205
- return withConfigDir(GLOBAL_CLAUDE_CONFIG_DIR, () =>
206
- getSessionMessages(sid, { dir: sessionRoot, includeSystemMessages: false }),
207
- );
208
- }
209
-
210
- /**
211
- * 本轮由快照权威:jsonl 里属于当前 turn 的 assistant 条目全部裁掉。
212
- *
213
- * SDK 按 block 逐条写 jsonl(thinking / text / tool_use 各一行),一个 turn 跑到
214
- * 一半时前面几条已经落盘 —— 那些内容同时也在 live_turn 快照里(快照从 run.start
215
- * 折叠整轮)。两边 id 体系不同(jsonl 是 `<uuid>:text:<n>`,快照是 `<runId>:mN`),
216
- * 前端去不了重,正文就渲染两遍。这是用户报的"重复传"的根因,100% 复现。
217
- *
218
- * 只裁 assistant:user 条目(用户自己那句话)快照里没有,裁了就真丢了。掉队的
219
- * tool_result 会因为找不到对应 tool_use 被 sessionMessagesToDisplay 自然跳过。
220
- */
221
- function dropInFlightTurn(messages, startedAt) {
222
- if (!startedAt) return messages;
223
- const cut = Date.parse(startedAt);
224
- if (Number.isNaN(cut)) return messages;
225
- return messages.filter(m => !(
226
- m?.type === 'assistant' && m.timestamp && Date.parse(m.timestamp) >= cut
227
- ));
228
- }
229
-
230
- /**
231
- * Phase A.4:推 ws.hydrate.start/chunk/end 帧把 jsonl 历史 messages 同步给前端。
232
- * 必须在 subscribeFromSeq 之前发,确保前端先 hydrate 后 apply replay/live events。
233
- * 纯同步。
234
- */
235
- function sendHydrateFrames(ws, sid, messages, asOfSeq) {
236
- if (ws.readyState !== ws.OPEN) return;
237
- const total = messages.length;
238
- ws.send(JSON.stringify({
239
- type: 'ws.hydrate.start',
240
- sessionId: sid,
241
- total,
242
- asOfSeq,
243
- ts: new Date().toISOString(),
244
- }));
245
- for (let i = 0; i < total; i += HYDRATE_CHUNK_SIZE) {
246
- if (ws.readyState !== ws.OPEN) return;
247
- ws.send(JSON.stringify({
248
- type: 'ws.hydrate.chunk',
249
- sessionId: sid,
250
- chunkIdx: Math.floor(i / HYDRATE_CHUNK_SIZE),
251
- messages: messages.slice(i, i + HYDRATE_CHUNK_SIZE),
252
- }));
253
- }
254
- if (ws.readyState !== ws.OPEN) return;
255
- ws.send(JSON.stringify({ type: 'ws.hydrate.end', sessionId: sid, total }));
256
- }
257
-
258
- /** hydrate 读失败 fail-soft:前端收到后兜底走 HTTP Sessions.read */
259
- function sendHydrateError(ws, sid, message) {
260
- if (ws.readyState !== ws.OPEN) return;
261
- try {
262
- ws.send(JSON.stringify({
263
- type: 'ws.hydrate.start', sessionId: sid, kind: 'error', error: message,
264
- }));
265
- } catch { /* ignore */ }
266
- }
267
-
268
- async function handleProjectWS(ws, pid, since = 0, sid = null) {
269
- const bus = getProjectBus(pid);
270
-
271
- // 诊断 log:用户重启服务器后报"前端收不到事件"——这条 log 让 pm2 logs 一眼看出
272
- // WS 是不是真连上 + 客户端传的 sid 跟 server 端 active session 状态对不对。
273
- console.info(
274
- `[ws] connect pid=${pid} sid=${sid ? sid.slice(0, 8) : 'none'} `
275
- + `hasActiveSession=${sid ? hasActiveQuerySession(sid) : 'n/a'} since=${since}`,
276
- );
277
-
278
- // sid lifecycle ref:带 sid 的 WS 连上即 ref++,close/error 时 unref。0 ref 触发
279
- // grace timer N ms 后 closeQuerySession 让 SDK subprocess 自然退出。
280
- refSession(sid);
281
-
282
- // ── 连接协议(2026-07-27 重构:快照 + 尾随,每次连接都全量重建)──
283
- //
284
- // ① jsonl hydrate — 已完成 turn 的历史(turn 边界 flush 的持久层)
285
- // ② ws.live_turn 快照 — 进行中 turn 的物化重建(live-turn.js 同步折叠,
286
- // 覆盖"刷新 / 断线期间错过的全部流式内容")
287
- // ③ 尾随订阅 — 从快照 seq 起 replay ring buffer + 切 live
288
- //
289
- // 老协议的 ?since= 游标 + gap 补 hydrate 废弃:ring buffer(2000) 对 token 级
290
- // streaming 几秒断线就 gap,gap 后 hydrate 帧晚于 replay/live 到达,前端
291
- // hydrate.end 整体替换 messages 把刚渲染的内容洗掉。新协议 hydrate 永远先发,
292
- // 快照覆盖 ring 之外的一切,ring 只需覆盖"读 jsonl 的几百 ms"同步窗口。
293
- // `since` 参数保留解析但忽略(旧前端兼容)。
294
- //
295
- // 两份历史的边界(2026-07-28):jsonl 与快照对"进行中的 turn"是重叠的 ——
296
- // 读 jsonl 与取快照必须在**同一个同步区**里完成并一起发出,中间不能 await,
297
- // 否则 turn 在缝里结束会让两边错位。规则:本轮由快照权威,hydrate 裁掉本轮。
298
- const seqAtHydrateStart = bus._seq || 0;
299
- let loaded = null;
300
- let loadErr = null;
301
- const t0 = Date.now();
302
- if (sid) {
303
- try { loaded = await loadHydrate(pid, sid); }
304
- catch (err) { loadErr = err; }
305
- }
306
-
307
- // ↓↓↓ 同步区开始(不要在这里插 await)↓↓↓
308
- const snapshot = sid ? getLiveTurnSnapshot(sid) : null;
309
- if (sid) {
310
- if (loadErr) {
311
- console.warn(`[ws] hydrate failed for ${pid}/${sid}:`, loadErr.message);
312
- sendHydrateError(ws, sid, loadErr.message);
313
- } else {
314
- const trimmed = snapshot ? dropInFlightTurn(loaded, snapshot.startedAt) : loaded;
315
- console.info(
316
- `[ws] hydrate sid=${sid.slice(0, 8)} loaded ${loaded.length} messages in ${Date.now() - t0}ms`
317
- + (snapshot ? ` (本轮 ${loaded.length - trimmed.length} 条交给快照)` : ''),
318
- );
319
- sendHydrateFrames(ws, sid, trimmed, seqAtHydrateStart);
320
- }
321
- }
322
- if (snapshot && ws.readyState === ws.OPEN) {
323
- try {
324
- ws.send(JSON.stringify({ type: 'ws.live_turn', sessionId: sid, ...snapshot }));
325
- } catch (err) {
326
- console.warn(`[ws] live_turn send failed for ${pid}:`, err.message);
327
- }
328
- }
329
- // ↑↑↑ 同步区结束 ↑↑↑
330
-
331
- // sid 过滤:projectBuses per-project 共享,只推本 session 事件 + 无 sessionId
332
- // 的全局事件。sid=null(/work 路径)时 session-scoped 事件全部不推 ——
333
- // 老行为"收全部"会把别的 session 的 delta 渲进空 chat(串台);/work 没有
334
- // session,本来就不该收 run 流,新建 session 后前端会带 sid 重连。
335
- const subscribeSince = snapshot ? snapshot.seq : seqAtHydrateStart;
336
- const { unsubscribe, replayed, gap } = bus.subscribeFromSeq(subscribeSince, (event) => {
337
- if (event.sessionId && event.sessionId !== sid) return;
338
- if (ws.readyState === ws.OPEN) {
339
- try {
340
- ws.send(JSON.stringify(event));
341
- } catch (err) {
342
- console.warn(`[ws] send failed for ${pid}:`, err.message);
343
- }
344
- }
345
- });
346
-
347
- const heartbeat = setInterval(() => {
348
- if (ws.readyState === ws.OPEN) {
349
- try { ws.ping(); } catch { /* will close */ }
350
- }
351
- }, HEARTBEAT_INTERVAL_MS);
352
-
353
- let cleanedUp = false;
354
- const cleanup = () => {
355
- if (cleanedUp) return;
356
- cleanedUp = true;
357
- clearInterval(heartbeat);
358
- unsubscribe();
359
- unrefSession(sid);
360
- };
361
-
362
- ws.on('close', cleanup);
363
- ws.on('error', (err) => {
364
- console.warn(`[ws] error for ${pid}:`, err.message);
365
- cleanup();
366
- });
367
-
368
- // 确认 + replay 元信息(gap=true 客户端可决定全量 hydrate)。
369
- // 故意放在 replay 之后发:客户端看到 ws.connected 就知道 backlog 已 drain,可切回正常 live 状态。
370
- //
371
- // activeRunId:sid 上当前在跑的 turn runId(无则 null)。前端用它重连后恢复
372
- // isStreaming/currentRunId —— 否则 WS 抖动期间 run.start 已发完且 buffer 没新事件
373
- // 时,前端永远不知道 run 还活着(stop 按钮消失,UX 表现"流没了")。
374
- const activeRunId = sid ? getCurrentTurnRunId(sid) : null;
375
- try {
376
- ws.send(JSON.stringify({
377
- type: 'ws.connected',
378
- projectId: pid,
379
- ts: new Date().toISOString(),
380
- since,
381
- replayed,
382
- gap,
383
- activeRunId,
384
- }));
385
- } catch { /* immediate close edge */ }
386
- }
1
+ /**
2
+ * server/ws/index.js — WebSocket 升级入口
3
+ *
4
+ * URL 模式:/ws/projects/:pid
5
+ *
6
+ * 流程:
7
+ * 1. http server 'upgrade' 事件触发
8
+ * 2. parse URL 取 pid
9
+ * 3. validateProjectId + getProject 校验存在
10
+ * 4. wss.handleUpgrade → 拿到 ws 对象
11
+ * 5. 订阅该 project 的 EventBus,事件 JSON.stringify 推 ws.send
12
+ * 6. ping/pong 30s 心跳
13
+ * 7. ws close → unsubscribe + clearInterval
14
+ */
15
+
16
+ import { WebSocketServer } from 'ws';
17
+ import { createBrowseWS } from './browse-channel.js';
18
+ import { URL } from 'url';
19
+ import { getSessionMessages } from '@anthropic-ai/claude-agent-sdk';
20
+ import { validateProjectId, getProject } from '../projects/store.js';
21
+ import { getSessionWorkspace, validateSessionId } from '../projects/workspace.js';
22
+ import { withConfigDir } from '../lib/sdk-session.js';
23
+ import { platform } from '../runtime/platform.js';
24
+ import { getProjectBus } from './broker.js';
25
+ import { requestUser } from '../auth/session.js';
26
+ import { originAllowed } from '../auth/origin-guard.js';
27
+ import { userOwnsProject } from '../api/_guard.js';
28
+ import {
29
+ getCurrentTurnRunId,
30
+ hasActiveQuerySession,
31
+ closeQuerySession,
32
+ markSessionActivity,
33
+ } from '../engine/runs/active-runs.js';
34
+ import { getLiveTurnSnapshot } from '../engine/runs/live-turn.js';
35
+
36
+ const HEARTBEAT_INTERVAL_MS = 30_000;
37
+ const HYDRATE_CHUNK_SIZE = 50;
38
+ const GLOBAL_CLAUDE_CONFIG_DIR = platform.claudeConfigDir;
39
+
40
+ /**
41
+ * WS 全部断开后再 N ms 仍无重连 → closeQuerySession 让 SDK subprocess 退出,
42
+ * 防止用户关 tab / 刷新后服务器孤儿 SDK process 永久占内存(每个 ~250MB RSS)。
43
+ *
44
+ * 默认 60s 给前端 ws-client.js exponential backoff(1s→2s→...→30s)足够重连窗口。
45
+ * env NODESIGN_WS_GRACE_MS 可调(生产可拉到 5min 给移动端弱网)。
46
+ */
47
+ const WS_GRACE_MS = Number(process.env.NODESIGN_WS_GRACE_MS) || 60_000;
48
+
49
+ /**
50
+ * sid → { count: 当前活跃 WS 连接数, graceTimer: 0 引用时启动的关闭定时器 }
51
+ *
52
+ * 每条带 sid 的 WS 连接 ref++,close 时 ref--;归零启 grace timer,N ms 内有新 WS
53
+ * 进同 sid 立即清 timer 续命;timer 到期再确认 0 ref 后 closeQuerySession。
54
+ *
55
+ * /work 路径(无 sid)的 WS 不进 sessionRefs(无 session 可绑)。
56
+ */
57
+ const sessionRefs = new Map();
58
+
59
+ function refSession(sid) {
60
+ if (!sid) return;
61
+ let entry = sessionRefs.get(sid);
62
+ if (!entry) {
63
+ entry = { count: 0, graceTimer: null };
64
+ sessionRefs.set(sid, entry);
65
+ }
66
+ entry.count += 1;
67
+ if (entry.graceTimer) {
68
+ clearTimeout(entry.graceTimer);
69
+ entry.graceTimer = null;
70
+ }
71
+ // WS 连上算一次活跃信号(idle scan 不会立即把刚连的 session 当僵尸关掉)
72
+ markSessionActivity(sid);
73
+ }
74
+
75
+ function unrefSession(sid) {
76
+ if (!sid) return;
77
+ const entry = sessionRefs.get(sid);
78
+ if (!entry) return;
79
+ entry.count = Math.max(0, entry.count - 1);
80
+ if (entry.count > 0) return;
81
+ // 0 ref → 启 grace timer。若 grace 期内有新 WS 进同 sid,refSession 会清掉 timer
82
+ if (entry.graceTimer) clearTimeout(entry.graceTimer);
83
+ const onGraceExpired = () => {
84
+ const cur = sessionRefs.get(sid);
85
+ if (!cur || cur.count > 0) return; // 期间有新连上,不关
86
+ // 进行中的 turn 不杀(2026-07-27):用户关 tab / 切走时后端应该把活干完,
87
+ // 前端重连后靠 hydrate + live_turn 快照接回状态。这正是"前端丢失后端工作
88
+ // 状态"要保住的那半 —— 老逻辑 60s 就 cancel 在跑的 turn,是真丢工作。
89
+ // 续一个 grace 周期再查;turn 结束后仍无订阅者才真正关(idle SDK 进程回收)。
90
+ if (getCurrentTurnRunId(sid)) {
91
+ console.info(`[ws] grace expired for sid=${sid.slice(0, 8)} but turn in flight — deferring close`);
92
+ cur.graceTimer = setTimeout(onGraceExpired, WS_GRACE_MS);
93
+ cur.graceTimer.unref?.();
94
+ return;
95
+ }
96
+ sessionRefs.delete(sid);
97
+ if (hasActiveQuerySession(sid)) {
98
+ console.info(`[ws] grace expired for sid=${sid.slice(0, 8)}, closing session (no_active_subscriber)`);
99
+ closeQuerySession(sid, 'no_active_subscriber');
100
+ }
101
+ };
102
+ entry.graceTimer = setTimeout(onGraceExpired, WS_GRACE_MS);
103
+ entry.graceTimer.unref?.();
104
+ }
105
+
106
+ export function setupWS(httpServer) {
107
+ // 浏览器画面/输入的专用通道(2026-08-18),独立 WebSocketServer
108
+ const browseWS = createBrowseWS();
109
+ // perMessageDeflate:弱网下 hydrate chunk + ring-buffer replay 帧成本是 raw size
110
+ // 量级(>2MB session 一次首屏 hydrate 估 250KB+ raw)。permessage-deflate 让
111
+ // JSON 文本压到 5-10x。threshold 1KB 跳过小帧(控制帧无收益反增 CPU)。
112
+ // env NODESIGN_WS_DISABLE_DEFLATE=1 应急关闭(CPU 紧的部署环境)。
113
+ const wss = new WebSocketServer({
114
+ noServer: true,
115
+ perMessageDeflate: process.env.NODESIGN_WS_DISABLE_DEFLATE === '1'
116
+ ? false
117
+ : { threshold: 1024 },
118
+ });
119
+
120
+ httpServer.on('upgrade', (req, socket, head) => {
121
+ // ⛔ CSRF 闸(2026-08-18):cookie 是 SameSite=Lax,而已发布站点与应用主机
122
+ // 同 eTLD+1 = 同站,Lax 照发(真跑验过,见 auth/origin-guard.js 文件头)。
123
+ // 这里**不学下面那个 4401 的握手后再关**:那套是为了让我们自己的前端看得见
124
+ // 原因好停止重连;外站页面不需要体面的错误,403 直接掐在升级前最省。
125
+ if (!originAllowed(req)) {
126
+ socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
127
+ return socket.destroy();
128
+ }
129
+
130
+ // 登录墙(2026-07-30 多用户):解析身份而非布尔。cookie 无效不再写 HTTP 401
131
+ // 拒 upgrade —— 浏览器拿不到 upgrade 阶段的状态码,只见 close 1006,前端
132
+ // ws-client 会无限指数退避重连。改为完成握手后 close(4401),前端把它列为
133
+ // fatal code → 停止重连并回登录页。
134
+ const user = requestUser(req);
135
+
136
+ let url;
137
+ try {
138
+ url = new URL(req.url || '/', 'http://x');
139
+ } catch {
140
+ return socket.destroy();
141
+ }
142
+
143
+ // ⚠️ 浏览器画面通道的分支**必须排在下面那个 404 之前** —— 那句 404 是兜底,
144
+ // 不匹配 `/ws/projects/:pid` 的路径会被它直接毙掉。
145
+ // 为什么另开一条而不是复用这条:这条是纯下行 + EventBus 广播 + 2000 条重放
146
+ // 缓冲,高频画面帧灌进去会冲爆重放缓冲、混进 hydrate 回放,而且 JSON+base64
147
+ // 帧会被 perMessageDeflate 白压一遍。详见 ws/browse-channel.js 文件头。
148
+ {
149
+ const bm = browseWS.matches(url.pathname);
150
+ if (bm) return browseWS.accept(req, socket, head, bm);
151
+ }
152
+
153
+ const m = url.pathname.match(/^\/ws\/projects\/([^/]+)$/);
154
+ if (!m) {
155
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
156
+ return socket.destroy();
157
+ }
158
+
159
+ const pid = decodeURIComponent(m[1]);
160
+ try {
161
+ validateProjectId(pid);
162
+ } catch {
163
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
164
+ return socket.destroy();
165
+ }
166
+
167
+ // 身份或归属不对 → 握手后 4401 关闭(同一个 code:外人不该分得清
168
+ // "没登录"和"不是你的项目")
169
+ const project = getProject(pid);
170
+ if (!user || !userOwnsProject(user, project)) {
171
+ wss.handleUpgrade(req, socket, head, (ws) => {
172
+ ws.close(4401, 'unauthorized');
173
+ });
174
+ return;
175
+ }
176
+
177
+ // ?since=N — 客户端最后看到的 EventBus seq;server 通过 buffer 回放 (since, _seq] 段
178
+ // 第一次连不带 since → since=0 → 不 replay 直接 live。
179
+ const sinceRaw = url.searchParams.get('since');
180
+ const since = sinceRaw != null ? Math.max(0, parseInt(sinceRaw, 10) || 0) : 0;
181
+
182
+ // Phase A.4:?sid=<sessionId> — 客户端当前在哪个 session 上,server 在 first connect /
183
+ // gap 时推 ws.hydrate.* 帧补完整 messages。无 sid 时(/work 路径)跳过 hydrate
184
+ // 直接 replay+live。
185
+ let sid = url.searchParams.get('sid');
186
+ if (sid) {
187
+ try { validateSessionId(sid); }
188
+ catch { sid = null; }
189
+ }
190
+
191
+ wss.handleUpgrade(req, socket, head, (ws) => {
192
+ handleProjectWS(ws, pid, since, sid);
193
+ });
194
+ });
195
+
196
+ return wss;
197
+ }
198
+
199
+ /**
200
+ * 读 jsonl 历史(异步,几百 ms)。发帧是另一个纯同步函数 —— 读完到发出之间不能
201
+ * 再有 await,否则"读到的历史"与"当时的 live turn 快照"会错位(见 handleProjectWS)。
202
+ */
203
+ async function loadHydrate(pid, sid) {
204
+ const sessionRoot = getSessionWorkspace(pid, sid);
205
+ return withConfigDir(GLOBAL_CLAUDE_CONFIG_DIR, () =>
206
+ getSessionMessages(sid, { dir: sessionRoot, includeSystemMessages: false }),
207
+ );
208
+ }
209
+
210
+ /**
211
+ * 本轮由快照权威:jsonl 里属于当前 turn 的 assistant 条目全部裁掉。
212
+ *
213
+ * SDK 按 block 逐条写 jsonl(thinking / text / tool_use 各一行),一个 turn 跑到
214
+ * 一半时前面几条已经落盘 —— 那些内容同时也在 live_turn 快照里(快照从 run.start
215
+ * 折叠整轮)。两边 id 体系不同(jsonl 是 `<uuid>:text:<n>`,快照是 `<runId>:mN`),
216
+ * 前端去不了重,正文就渲染两遍。这是用户报的"重复传"的根因,100% 复现。
217
+ *
218
+ * 只裁 assistant:user 条目(用户自己那句话)快照里没有,裁了就真丢了。掉队的
219
+ * tool_result 会因为找不到对应 tool_use 被 sessionMessagesToDisplay 自然跳过。
220
+ */
221
+ function dropInFlightTurn(messages, startedAt) {
222
+ if (!startedAt) return messages;
223
+ const cut = Date.parse(startedAt);
224
+ if (Number.isNaN(cut)) return messages;
225
+ return messages.filter(m => !(
226
+ m?.type === 'assistant' && m.timestamp && Date.parse(m.timestamp) >= cut
227
+ ));
228
+ }
229
+
230
+ /**
231
+ * Phase A.4:推 ws.hydrate.start/chunk/end 帧把 jsonl 历史 messages 同步给前端。
232
+ * 必须在 subscribeFromSeq 之前发,确保前端先 hydrate 后 apply replay/live events。
233
+ * 纯同步。
234
+ */
235
+ function sendHydrateFrames(ws, sid, messages, asOfSeq) {
236
+ if (ws.readyState !== ws.OPEN) return;
237
+ const total = messages.length;
238
+ ws.send(JSON.stringify({
239
+ type: 'ws.hydrate.start',
240
+ sessionId: sid,
241
+ total,
242
+ asOfSeq,
243
+ ts: new Date().toISOString(),
244
+ }));
245
+ for (let i = 0; i < total; i += HYDRATE_CHUNK_SIZE) {
246
+ if (ws.readyState !== ws.OPEN) return;
247
+ ws.send(JSON.stringify({
248
+ type: 'ws.hydrate.chunk',
249
+ sessionId: sid,
250
+ chunkIdx: Math.floor(i / HYDRATE_CHUNK_SIZE),
251
+ messages: messages.slice(i, i + HYDRATE_CHUNK_SIZE),
252
+ }));
253
+ }
254
+ if (ws.readyState !== ws.OPEN) return;
255
+ ws.send(JSON.stringify({ type: 'ws.hydrate.end', sessionId: sid, total }));
256
+ }
257
+
258
+ /** hydrate 读失败 fail-soft:前端收到后兜底走 HTTP Sessions.read */
259
+ function sendHydrateError(ws, sid, message) {
260
+ if (ws.readyState !== ws.OPEN) return;
261
+ try {
262
+ ws.send(JSON.stringify({
263
+ type: 'ws.hydrate.start', sessionId: sid, kind: 'error', error: message,
264
+ }));
265
+ } catch { /* ignore */ }
266
+ }
267
+
268
+ async function handleProjectWS(ws, pid, since = 0, sid = null) {
269
+ const bus = getProjectBus(pid);
270
+
271
+ // 诊断 log:用户重启服务器后报"前端收不到事件"——这条 log 让 pm2 logs 一眼看出
272
+ // WS 是不是真连上 + 客户端传的 sid 跟 server 端 active session 状态对不对。
273
+ console.info(
274
+ `[ws] connect pid=${pid} sid=${sid ? sid.slice(0, 8) : 'none'} `
275
+ + `hasActiveSession=${sid ? hasActiveQuerySession(sid) : 'n/a'} since=${since}`,
276
+ );
277
+
278
+ // sid lifecycle ref:带 sid 的 WS 连上即 ref++,close/error 时 unref。0 ref 触发
279
+ // grace timer N ms 后 closeQuerySession 让 SDK subprocess 自然退出。
280
+ refSession(sid);
281
+
282
+ // ── 连接协议(2026-07-27 重构:快照 + 尾随,每次连接都全量重建)──
283
+ //
284
+ // ① jsonl hydrate — 已完成 turn 的历史(turn 边界 flush 的持久层)
285
+ // ② ws.live_turn 快照 — 进行中 turn 的物化重建(live-turn.js 同步折叠,
286
+ // 覆盖"刷新 / 断线期间错过的全部流式内容")
287
+ // ③ 尾随订阅 — 从快照 seq 起 replay ring buffer + 切 live
288
+ //
289
+ // 老协议的 ?since= 游标 + gap 补 hydrate 废弃:ring buffer(2000) 对 token 级
290
+ // streaming 几秒断线就 gap,gap 后 hydrate 帧晚于 replay/live 到达,前端
291
+ // hydrate.end 整体替换 messages 把刚渲染的内容洗掉。新协议 hydrate 永远先发,
292
+ // 快照覆盖 ring 之外的一切,ring 只需覆盖"读 jsonl 的几百 ms"同步窗口。
293
+ // `since` 参数保留解析但忽略(旧前端兼容)。
294
+ //
295
+ // 两份历史的边界(2026-07-28):jsonl 与快照对"进行中的 turn"是重叠的 ——
296
+ // 读 jsonl 与取快照必须在**同一个同步区**里完成并一起发出,中间不能 await,
297
+ // 否则 turn 在缝里结束会让两边错位。规则:本轮由快照权威,hydrate 裁掉本轮。
298
+ const seqAtHydrateStart = bus._seq || 0;
299
+ let loaded = null;
300
+ let loadErr = null;
301
+ const t0 = Date.now();
302
+ if (sid) {
303
+ try { loaded = await loadHydrate(pid, sid); }
304
+ catch (err) { loadErr = err; }
305
+ }
306
+
307
+ // ↓↓↓ 同步区开始(不要在这里插 await)↓↓↓
308
+ const snapshot = sid ? getLiveTurnSnapshot(sid) : null;
309
+ if (sid) {
310
+ if (loadErr) {
311
+ console.warn(`[ws] hydrate failed for ${pid}/${sid}:`, loadErr.message);
312
+ sendHydrateError(ws, sid, loadErr.message);
313
+ } else {
314
+ const trimmed = snapshot ? dropInFlightTurn(loaded, snapshot.startedAt) : loaded;
315
+ console.info(
316
+ `[ws] hydrate sid=${sid.slice(0, 8)} loaded ${loaded.length} messages in ${Date.now() - t0}ms`
317
+ + (snapshot ? ` (本轮 ${loaded.length - trimmed.length} 条交给快照)` : ''),
318
+ );
319
+ sendHydrateFrames(ws, sid, trimmed, seqAtHydrateStart);
320
+ }
321
+ }
322
+ if (snapshot && ws.readyState === ws.OPEN) {
323
+ try {
324
+ ws.send(JSON.stringify({ type: 'ws.live_turn', sessionId: sid, ...snapshot }));
325
+ } catch (err) {
326
+ console.warn(`[ws] live_turn send failed for ${pid}:`, err.message);
327
+ }
328
+ }
329
+ // ↑↑↑ 同步区结束 ↑↑↑
330
+
331
+ // sid 过滤:projectBuses per-project 共享,只推本 session 事件 + 无 sessionId
332
+ // 的全局事件。sid=null(/work 路径)时 session-scoped 事件全部不推 ——
333
+ // 老行为"收全部"会把别的 session 的 delta 渲进空 chat(串台);/work 没有
334
+ // session,本来就不该收 run 流,新建 session 后前端会带 sid 重连。
335
+ const subscribeSince = snapshot ? snapshot.seq : seqAtHydrateStart;
336
+ const { unsubscribe, replayed, gap } = bus.subscribeFromSeq(subscribeSince, (event) => {
337
+ if (event.sessionId && event.sessionId !== sid) return;
338
+ if (ws.readyState === ws.OPEN) {
339
+ try {
340
+ ws.send(JSON.stringify(event));
341
+ } catch (err) {
342
+ console.warn(`[ws] send failed for ${pid}:`, err.message);
343
+ }
344
+ }
345
+ });
346
+
347
+ const heartbeat = setInterval(() => {
348
+ if (ws.readyState === ws.OPEN) {
349
+ try { ws.ping(); } catch { /* will close */ }
350
+ }
351
+ }, HEARTBEAT_INTERVAL_MS);
352
+
353
+ let cleanedUp = false;
354
+ const cleanup = () => {
355
+ if (cleanedUp) return;
356
+ cleanedUp = true;
357
+ clearInterval(heartbeat);
358
+ unsubscribe();
359
+ unrefSession(sid);
360
+ };
361
+
362
+ ws.on('close', cleanup);
363
+ ws.on('error', (err) => {
364
+ console.warn(`[ws] error for ${pid}:`, err.message);
365
+ cleanup();
366
+ });
367
+
368
+ // 确认 + replay 元信息(gap=true 客户端可决定全量 hydrate)。
369
+ // 故意放在 replay 之后发:客户端看到 ws.connected 就知道 backlog 已 drain,可切回正常 live 状态。
370
+ //
371
+ // activeRunId:sid 上当前在跑的 turn runId(无则 null)。前端用它重连后恢复
372
+ // isStreaming/currentRunId —— 否则 WS 抖动期间 run.start 已发完且 buffer 没新事件
373
+ // 时,前端永远不知道 run 还活着(stop 按钮消失,UX 表现"流没了")。
374
+ const activeRunId = sid ? getCurrentTurnRunId(sid) : null;
375
+ try {
376
+ ws.send(JSON.stringify({
377
+ type: 'ws.connected',
378
+ projectId: pid,
379
+ ts: new Date().toISOString(),
380
+ since,
381
+ replayed,
382
+ gap,
383
+ activeRunId,
384
+ }));
385
+ } catch { /* immediate close edge */ }
386
+ }