@xiaobuyu/nodesign 0.0.1 → 0.0.2

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 (282) 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 +22 -22
  9. package/server/api/assets/notes.js +144 -144
  10. package/server/api/assets.js +896 -896
  11. package/server/api/board.js +57 -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 +464 -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 +215 -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 +336 -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 +1024 -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 +339 -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 +155 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/explain-style.js +237 -237
  120. package/server/engine/mcp/tools/export-handoff.js +133 -133
  121. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  122. package/server/engine/mcp/tools/generate-image.js +842 -842
  123. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  124. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  125. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  126. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  127. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  128. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  129. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  130. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  131. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  132. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  133. package/server/engine/mcp/tools/highlight.js +68 -68
  134. package/server/engine/mcp/tools/list-pages.js +222 -222
  135. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  136. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  137. package/server/engine/mcp/tools/organize-board.js +64 -64
  138. package/server/engine/mcp/tools/paint-still.js +434 -434
  139. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  140. package/server/engine/mcp/tools/preview-deck.js +130 -130
  141. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  142. package/server/engine/mcp/tools/publish-site.js +102 -102
  143. package/server/engine/mcp/tools/query-elements.js +195 -195
  144. package/server/engine/mcp/tools/read-board.js +103 -103
  145. package/server/engine/mcp/tools/read-document.js +93 -93
  146. package/server/engine/mcp/tools/read-page.js +264 -264
  147. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  148. package/server/engine/mcp/tools/record-decision.js +140 -140
  149. package/server/engine/mcp/tools/relate-on-board.js +110 -110
  150. package/server/engine/mcp/tools/report-issue.js +115 -115
  151. package/server/engine/mcp/tools/roll-film.js +266 -266
  152. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  153. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  154. package/server/engine/mcp/tools/screenshot.js +583 -583
  155. package/server/engine/mcp/tools/tier-gate.js +96 -96
  156. package/server/engine/mcp/tools/trace-motion.js +215 -215
  157. package/server/engine/mcp/tools/web-search.js +548 -548
  158. package/server/engine/motion/inventory.js +349 -349
  159. package/server/engine/perception/session.js +235 -235
  160. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  161. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
  162. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  163. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  164. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  165. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  169. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
  170. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  171. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  172. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
  173. 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
  174. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  175. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
  176. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  177. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  178. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  179. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  180. package/server/engine/runs/active-runs.js +698 -698
  181. package/server/engine/runs/live-turn.js +252 -252
  182. package/server/engine/runs/store.js +364 -364
  183. package/server/engine/runs/turn-relay.js +217 -217
  184. package/server/engine/runtime/workspace.js +164 -164
  185. package/server/lib/artifact-file-path.js +80 -80
  186. package/server/lib/artifact-target.js +284 -284
  187. package/server/lib/asset-refs.js +239 -239
  188. package/server/lib/async-queue.js +104 -104
  189. package/server/lib/auto-relations.js +130 -130
  190. package/server/lib/binding-types.js +60 -60
  191. package/server/lib/board-kind-sizes.js +62 -62
  192. package/server/lib/board-relations.js +133 -133
  193. package/server/lib/browse-proxy.js +180 -180
  194. package/server/lib/canvas-id.js +36 -36
  195. package/server/lib/cover.js +227 -227
  196. package/server/lib/danbooru-tags.js +291 -291
  197. package/server/lib/doc-extract.js +156 -156
  198. package/server/lib/docx/build-from-source.js +260 -260
  199. package/server/lib/docx/build.js +490 -490
  200. package/server/lib/docx/dump-styles.js +287 -287
  201. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  202. package/server/lib/docx/merge-runs.js +129 -129
  203. package/server/lib/docx/numbering.js +218 -218
  204. package/server/lib/docx/order.js +152 -152
  205. package/server/lib/docx/rawzip.js +172 -172
  206. package/server/lib/docx/render.js +92 -92
  207. package/server/lib/docx/text-lint.js +225 -225
  208. package/server/lib/docx/tokens.js +335 -335
  209. package/server/lib/docx/units.js +29 -29
  210. package/server/lib/docx/xml.js +291 -291
  211. package/server/lib/docx-pages.js +187 -187
  212. package/server/lib/export-collect.js +289 -289
  213. package/server/lib/export-package.js +210 -210
  214. package/server/lib/html-srcset.js +145 -145
  215. package/server/lib/image-variant.js +460 -460
  216. package/server/lib/ingress/forward-openai-chat.js +305 -305
  217. package/server/lib/ingress/openai-chat.js +491 -491
  218. package/server/lib/ingress/session-notice.js +66 -66
  219. package/server/lib/ingress/session-routes.js +68 -68
  220. package/server/lib/ingress/slot-probe.js +130 -130
  221. package/server/lib/ingress/upstream-billing.js +52 -52
  222. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  223. package/server/lib/ingress/upstream-truncation.js +48 -48
  224. package/server/lib/issues-store.js +182 -182
  225. package/server/lib/kinds/deck.js +72 -72
  226. package/server/lib/kinds/docx.js +283 -283
  227. package/server/lib/kinds/file-kinds.js +113 -113
  228. package/server/lib/kinds/index.js +202 -202
  229. package/server/lib/kinds/site.js +161 -161
  230. package/server/lib/model-ingress.js +543 -543
  231. package/server/lib/moderation.js +255 -255
  232. package/server/lib/notice-store.js +103 -103
  233. package/server/lib/plugin-install.js +104 -104
  234. package/server/lib/plugin-validator.js +721 -721
  235. package/server/lib/publish-store.js +115 -115
  236. package/server/lib/quick-summary.js +52 -52
  237. package/server/lib/quota.js +270 -270
  238. package/server/lib/rate-window.js +29 -29
  239. package/server/lib/reference-assets.js +92 -92
  240. package/server/lib/region-shot.js +135 -135
  241. package/server/lib/safe-path.js +73 -73
  242. package/server/lib/sdk-session.js +33 -33
  243. package/server/lib/showcase-store.js +92 -92
  244. package/server/lib/site-publish.js +465 -465
  245. package/server/lib/ssrf-guard.js +432 -432
  246. package/server/lib/task-scan.js +158 -158
  247. package/server/lib/tavern-json.js +154 -154
  248. package/server/lib/video-variant.js +237 -237
  249. package/server/lib/workspace-path.js +33 -33
  250. package/server/ops/fix-sdk-musl.mjs +40 -40
  251. package/server/ops/install-macos-fonts.sh +114 -114
  252. package/server/ops/macos-fonts.conf +336 -336
  253. package/server/ops/sandbox-shim/bwrap +57 -57
  254. package/server/projects/assets-summary.js +119 -119
  255. package/server/projects/auto-name.js +53 -53
  256. package/server/projects/board-store.js +640 -640
  257. package/server/projects/move-entry.js +103 -103
  258. package/server/projects/store.js +270 -270
  259. package/server/projects/ui-config.js +54 -54
  260. package/server/projects/workspace-templates.js +53 -53
  261. package/server/projects/workspace.js +1025 -1025
  262. package/server/runtime/local-config.js +177 -177
  263. package/server/runtime/profile.js +72 -72
  264. package/server/services/rembg-service.py +334 -334
  265. package/server/shared/README.md +25 -25
  266. package/server/shared/deck.js +54 -54
  267. package/server/shared/time.js +49 -49
  268. package/server/style-pipeline/README.md +53 -53
  269. package/server/ws/broker.js +36 -36
  270. package/server/ws/browse-channel.js +177 -177
  271. package/server/ws/index.js +386 -386
  272. package/web/README.md +89 -89
  273. package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
  274. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
  275. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
  276. package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
  277. package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
  278. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
  279. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
  280. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
  281. package/web/dist/index.html +17 -17
  282. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
@@ -1,252 +1,252 @@
1
- /**
2
- * server/engine/runs/live-turn.js — 进行中 turn 的物化快照(内存态)
3
- *
4
- * 解决的问题:EventBus 是 fire-and-forget + ring buffer(2000 条),token 级
5
- * streaming 一个 turn 就上万条事件 —— 刷新 / 断线重连后"进行中的 turn"在任何
6
- * 可恢复的地方都不存在(jsonl 只在 turn 边界 flush)。老架构靠 ?since= 游标 +
7
- * ring 回放,重连几秒就 gap,gap 补 hydrate 又把已渲染内容反向覆盖。
8
- *
9
- * 新范式(快照 + 尾随,2026-07-27 重构):
10
- * - 本模块订阅每个 project bus,把进行中 turn 的事件**同步折叠**成前端
11
- * display-message 形态的物化状态(跟 ProjectWorkspace handleEvent 的累加
12
- * 逻辑同构)
13
- * - WS 连接时:jsonl hydrate(已完成 turn)→ ws.live_turn 快照(进行中 turn)
14
- * → 从快照 seq 起订阅实时流。ring buffer 大小从此只影响极小的同步窗口,
15
- * 不再影响正确性
16
- * - turn 结束(done / error / cancelled)或 session 结束即清,内存占用 =
17
- * 活跃 turn 数 × 单 turn 消息体量(tool output 截断到 16k)
18
- *
19
- * 折叠规则跟前端 handleEvent 对齐(改前端渲染逻辑时两边同步改):
20
- * run.delta.text / thinking → 同 role 连续累加一条消息
21
- * run.tool_use.started → push tool 消息 status='running'
22
- * run.delta.tool_use → 补 toolInput
23
- * run.delta.tool_result → 补 status / output / error(images 不进快照,太大)
24
- * run.todo.updated → todos 覆盖
25
- * run.context_usage → contextUsage 覆盖
26
- * run.done / error / cancelled → 标记 ended(保留 GRACE 毫秒,见下),不再折叠
27
- * run.query.end → 清
28
- *
29
- * turn 结束后为什么还留一小会(2026-07-28):jsonl 是 SDK 子进程按 block 逐条写的,
30
- * 我们读 jsonl 与 turn 收尾之间有几百毫秒的错位窗口。这段时间内重连,若快照已经
31
- * 没了,尾随订阅会把已经落进 jsonl 的那几条 delta 再放一遍 → 正文重复。留 GRACE
32
- * 毫秒让"本轮由快照权威、hydrate 裁掉本轮"这条规则在收尾瞬间同样成立。
33
- */
34
-
35
- const MAX_TOOL_TEXT = 16_000;
36
- // 3s:够盖住"读 jsonl 的几百毫秒"这个错位窗口,又短到收尾后的重连基本不会
37
- // 落在里面(快照不带 tool_result 的图片,长时间用快照顶替 hydrate 会丢缩略图)。
38
- const ENDED_GRACE_MS = 3_000;
39
-
40
- /** @type {Map<string, object>} sessionId → live turn state */
41
- const liveTurns = new Map();
42
-
43
- /**
44
- * sessionId → 最近一次 run.context_usage 事件(2026-07-30)
45
- *
46
- * 跟 liveTurns 分开活着:turn 一结束 liveTurns 就清,但"这个会话现在装了多少上下文"
47
- * 在两轮之间照样成立 —— 而那恰恰是用户最想看它的时刻(要不要先压缩再开新活)。
48
- *
49
- * query 结束也**不清**:关掉标签页就会触发 grace 超时关 session,而下次打开这个会话
50
- * 时 SDK 从同一份 jsonl resume,上下文体量基本还是那么大。这时给个带"当前没有活跃
51
- * 会话"标注的旧数字(live:false),比一句"还没开始对话"有用得多。只有 query 活着时才向 SDK 现问
52
- * (见 GET /sessions/:sid/context-usage),所以准确的那条路一直优先。
53
- *
54
- * 只活在内存里:进程重启就没了,下一轮 turn 自然补上。
55
- */
56
- const lastUsage = new Map();
57
- const LAST_USAGE_CAP = 200;
58
-
59
- export function getLastContextUsage(sessionId) {
60
- return lastUsage.get(sessionId) || null;
61
- }
62
-
63
- /**
64
- * 不变量守卫:只记 Events.contextUsage 构造出来的那种形状。
65
- *
66
- * `run.context_usage` 曾经有两个生产者,字段名对不上:session-loop 每条 assistant
67
- * message 后发 { totalTokens, maxTokens, percentage }(前端读这套),hooks.js 的 Stop
68
- * handler 每轮收尾发 { used, max, percent, categories }。前端靠 store 的"不覆盖已有值"
69
- * 侥幸没显形,但每轮的**最后**一条恰好总是后者,凡是"取最新一条"的下游拿到的都是
70
- * 一片 undefined —— 这个记忆层就是这么栽的。两个生产者已经合并成一个构造函数
71
- * (2026-07-30),这里留一道守卫:以后再冒出第三种形状,是它被丢掉,不是这里出错数。
72
- */
73
- function rememberUsage(sid, evt) {
74
- if (typeof evt.totalTokens !== 'number') return;
75
- if (lastUsage.size >= LAST_USAGE_CAP && !lastUsage.has(sid)) {
76
- // 先进先出丢一条:进程活得久了不至于攒着所有历史 session
77
- const oldest = lastUsage.keys().next().value;
78
- if (oldest) lastUsage.delete(oldest);
79
- }
80
- lastUsage.set(sid, evt);
81
- }
82
-
83
- function truncate(s) {
84
- if (typeof s !== 'string') return s;
85
- if (s.length <= MAX_TOOL_TEXT) return s;
86
- return s.slice(0, MAX_TOOL_TEXT) + `\n…[截断,共 ${s.length} 字符]`;
87
- }
88
-
89
- /**
90
- * 给 project bus 挂折叠订阅(幂等 —— broker.getProjectBus 每次创建 bus 时调)。
91
- */
92
- export function attachLiveTurnTracker(bus) {
93
- if (bus.__liveTurnTracked) return;
94
- bus.__liveTurnTracked = true;
95
- bus.subscribe('*', fold);
96
- }
97
-
98
- /**
99
- * 取 session 当前 turn 的快照;没有在跑、且上一轮已过 grace 期 → null。
100
- * 返回值直接可作 ws.live_turn 帧 payload(messages 是前端 display 形态)。
101
- *
102
- * running=false 表示这是刚结束那一轮的尾巴(grace 期内),前端据此**只合并消息、
103
- * 不把自己切回 streaming 态**。
104
- */
105
- export function getLiveTurnSnapshot(sessionId) {
106
- const st = liveTurns.get(sessionId);
107
- if (!st) return null;
108
- if (st.endedAt && Date.now() - st.endedAt > ENDED_GRACE_MS) {
109
- liveTurns.delete(sessionId);
110
- return null;
111
- }
112
- return {
113
- runId: st.runId,
114
- seq: st.seq,
115
- startedAt: st.startedAt,
116
- running: !st.endedAt,
117
- messages: st.messages,
118
- todos: st.todos,
119
- contextUsage: st.contextUsage,
120
- };
121
- }
122
-
123
- function fold(evt) {
124
- const sid = evt.sessionId;
125
- if (!sid) return;
126
-
127
- // 记在 liveTurns 之外:turn 结束、快照删掉之后,这个数字还要继续给 composer
128
- // 的 [+] 菜单用。放在 st 判空之前,快照已经清了也照记。
129
- if (evt.type === 'run.context_usage') rememberUsage(sid, evt);
130
-
131
- if (evt.type === 'run.start') {
132
- liveTurns.set(sid, {
133
- runId: evt.runId || null,
134
- startedAt: evt.ts || new Date().toISOString(),
135
- seq: evt.seq || 0,
136
- messages: [],
137
- todos: [],
138
- contextUsage: null,
139
- endedAt: null,
140
- _msgCounter: 0,
141
- });
142
- return;
143
- }
144
-
145
- const st = liveTurns.get(sid);
146
- if (!st) return;
147
-
148
- // 已收尾的那一轮只留着给 grace 期内的重连当权威快照,不再吸新事件
149
- // (新一轮由上面的 run.start 整块换掉)。seq 照常推进,免得尾随订阅从旧游标
150
- // 起把这轮已经放过的事件再放一遍。
151
- if (st.endedAt) {
152
- if (evt.type === 'run.query.end' || Date.now() - st.endedAt > ENDED_GRACE_MS) {
153
- liveTurns.delete(sid);
154
- return;
155
- }
156
- if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
157
- return;
158
- }
159
-
160
- // stale 事件(老 turn 的尾巴)不折叠也不清新 turn 的状态
161
- const runMatches = !evt.runId || !st.runId || evt.runId === st.runId;
162
-
163
- switch (evt.type) {
164
- case 'run.delta.text':
165
- if (runMatches) appendText(st, 'assistant', evt.text, evt.parentToolUseId);
166
- break;
167
- case 'run.delta.thinking':
168
- if (runMatches) appendText(st, 'thinking', evt.text, evt.parentToolUseId);
169
- break;
170
- case 'run.tool_use.started':
171
- if (runMatches && evt.blockId && !st.messages.some(m => m.id === evt.blockId)) {
172
- st.messages.push({
173
- id: evt.blockId, role: 'tool', toolName: evt.name,
174
- toolInput: undefined, status: 'running', runId: st.runId,
175
- ...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
176
- });
177
- }
178
- break;
179
- case 'run.delta.tool_use': {
180
- if (!runMatches) break;
181
- const existing = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
182
- if (existing) existing.toolInput = evt.input;
183
- else if (evt.blockId) {
184
- st.messages.push({
185
- id: evt.blockId, role: 'tool', toolName: evt.name,
186
- toolInput: evt.input, status: 'running', runId: st.runId,
187
- ...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
188
- });
189
- }
190
- break;
191
- }
192
- case 'run.delta.tool_result': {
193
- if (!runMatches) break;
194
- const tool = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
195
- if (tool) {
196
- tool.status = evt.ok ? 'success' : 'error';
197
- if (evt.output !== undefined) tool.toolOutput = truncate(typeof evt.output === 'string' ? evt.output : JSON.stringify(evt.output));
198
- if (evt.error) tool.toolError = truncate(typeof evt.error === 'string' ? evt.error : JSON.stringify(evt.error));
199
- // images 不进快照:base64 体积大,turn 结束后 jsonl hydrate 会带回来
200
- }
201
- break;
202
- }
203
- case 'run.tool_use_summary': {
204
- if (!runMatches || !evt.summary) break;
205
- const ids = Array.isArray(evt.blockIds) ? evt.blockIds : [];
206
- const hit = st.messages.find(m => m.role === 'tool' && ids.includes(m.id));
207
- if (hit) hit.groupSummary = evt.summary;
208
- break;
209
- }
210
- case 'run.todo.updated':
211
- if (runMatches && Array.isArray(evt.todos)) st.todos = evt.todos;
212
- break;
213
- case 'run.context_usage':
214
- if (runMatches) st.contextUsage = evt;
215
- break;
216
- case 'run.done':
217
- case 'run.error':
218
- case 'run.cancelled':
219
- // 不立刻删:留 grace 期给"收尾瞬间重连"当权威快照(见文件头说明)
220
- if (runMatches) {
221
- st.endedAt = Date.now();
222
- if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
223
- }
224
- return;
225
- case 'run.query.end':
226
- liveTurns.delete(sid);
227
- return;
228
- default:
229
- break;
230
- }
231
-
232
- // 所有本 session 事件都推进快照游标(包括未折叠类型)——
233
- // 重连订阅从 seq 起,不折叠的事件类型(queue depth 等)丢了也无碍(幂等 UI 状态)
234
- if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
235
- }
236
-
237
- function appendText(st, role, text, parentToolUseId = null) {
238
- if (!text) return;
239
- const last = st.messages[st.messages.length - 1];
240
- // 子代理时间轴:不同 parent 的流不互吸(跟前端 lib/chat-stream.js 同构)
241
- if (last && last.role === role && (last.parentToolUseId || null) === (parentToolUseId || null)) {
242
- last.content = (last.content || '') + text;
243
- return;
244
- }
245
- st.messages.push({
246
- id: `${st.runId || 'live'}:m${st._msgCounter++}`,
247
- ...(parentToolUseId ? { parentToolUseId } : {}),
248
- role,
249
- content: text,
250
- runId: st.runId,
251
- });
252
- }
1
+ /**
2
+ * server/engine/runs/live-turn.js — 进行中 turn 的物化快照(内存态)
3
+ *
4
+ * 解决的问题:EventBus 是 fire-and-forget + ring buffer(2000 条),token 级
5
+ * streaming 一个 turn 就上万条事件 —— 刷新 / 断线重连后"进行中的 turn"在任何
6
+ * 可恢复的地方都不存在(jsonl 只在 turn 边界 flush)。老架构靠 ?since= 游标 +
7
+ * ring 回放,重连几秒就 gap,gap 补 hydrate 又把已渲染内容反向覆盖。
8
+ *
9
+ * 新范式(快照 + 尾随,2026-07-27 重构):
10
+ * - 本模块订阅每个 project bus,把进行中 turn 的事件**同步折叠**成前端
11
+ * display-message 形态的物化状态(跟 ProjectWorkspace handleEvent 的累加
12
+ * 逻辑同构)
13
+ * - WS 连接时:jsonl hydrate(已完成 turn)→ ws.live_turn 快照(进行中 turn)
14
+ * → 从快照 seq 起订阅实时流。ring buffer 大小从此只影响极小的同步窗口,
15
+ * 不再影响正确性
16
+ * - turn 结束(done / error / cancelled)或 session 结束即清,内存占用 =
17
+ * 活跃 turn 数 × 单 turn 消息体量(tool output 截断到 16k)
18
+ *
19
+ * 折叠规则跟前端 handleEvent 对齐(改前端渲染逻辑时两边同步改):
20
+ * run.delta.text / thinking → 同 role 连续累加一条消息
21
+ * run.tool_use.started → push tool 消息 status='running'
22
+ * run.delta.tool_use → 补 toolInput
23
+ * run.delta.tool_result → 补 status / output / error(images 不进快照,太大)
24
+ * run.todo.updated → todos 覆盖
25
+ * run.context_usage → contextUsage 覆盖
26
+ * run.done / error / cancelled → 标记 ended(保留 GRACE 毫秒,见下),不再折叠
27
+ * run.query.end → 清
28
+ *
29
+ * turn 结束后为什么还留一小会(2026-07-28):jsonl 是 SDK 子进程按 block 逐条写的,
30
+ * 我们读 jsonl 与 turn 收尾之间有几百毫秒的错位窗口。这段时间内重连,若快照已经
31
+ * 没了,尾随订阅会把已经落进 jsonl 的那几条 delta 再放一遍 → 正文重复。留 GRACE
32
+ * 毫秒让"本轮由快照权威、hydrate 裁掉本轮"这条规则在收尾瞬间同样成立。
33
+ */
34
+
35
+ const MAX_TOOL_TEXT = 16_000;
36
+ // 3s:够盖住"读 jsonl 的几百毫秒"这个错位窗口,又短到收尾后的重连基本不会
37
+ // 落在里面(快照不带 tool_result 的图片,长时间用快照顶替 hydrate 会丢缩略图)。
38
+ const ENDED_GRACE_MS = 3_000;
39
+
40
+ /** @type {Map<string, object>} sessionId → live turn state */
41
+ const liveTurns = new Map();
42
+
43
+ /**
44
+ * sessionId → 最近一次 run.context_usage 事件(2026-07-30)
45
+ *
46
+ * 跟 liveTurns 分开活着:turn 一结束 liveTurns 就清,但"这个会话现在装了多少上下文"
47
+ * 在两轮之间照样成立 —— 而那恰恰是用户最想看它的时刻(要不要先压缩再开新活)。
48
+ *
49
+ * query 结束也**不清**:关掉标签页就会触发 grace 超时关 session,而下次打开这个会话
50
+ * 时 SDK 从同一份 jsonl resume,上下文体量基本还是那么大。这时给个带"当前没有活跃
51
+ * 会话"标注的旧数字(live:false),比一句"还没开始对话"有用得多。只有 query 活着时才向 SDK 现问
52
+ * (见 GET /sessions/:sid/context-usage),所以准确的那条路一直优先。
53
+ *
54
+ * 只活在内存里:进程重启就没了,下一轮 turn 自然补上。
55
+ */
56
+ const lastUsage = new Map();
57
+ const LAST_USAGE_CAP = 200;
58
+
59
+ export function getLastContextUsage(sessionId) {
60
+ return lastUsage.get(sessionId) || null;
61
+ }
62
+
63
+ /**
64
+ * 不变量守卫:只记 Events.contextUsage 构造出来的那种形状。
65
+ *
66
+ * `run.context_usage` 曾经有两个生产者,字段名对不上:session-loop 每条 assistant
67
+ * message 后发 { totalTokens, maxTokens, percentage }(前端读这套),hooks.js 的 Stop
68
+ * handler 每轮收尾发 { used, max, percent, categories }。前端靠 store 的"不覆盖已有值"
69
+ * 侥幸没显形,但每轮的**最后**一条恰好总是后者,凡是"取最新一条"的下游拿到的都是
70
+ * 一片 undefined —— 这个记忆层就是这么栽的。两个生产者已经合并成一个构造函数
71
+ * (2026-07-30),这里留一道守卫:以后再冒出第三种形状,是它被丢掉,不是这里出错数。
72
+ */
73
+ function rememberUsage(sid, evt) {
74
+ if (typeof evt.totalTokens !== 'number') return;
75
+ if (lastUsage.size >= LAST_USAGE_CAP && !lastUsage.has(sid)) {
76
+ // 先进先出丢一条:进程活得久了不至于攒着所有历史 session
77
+ const oldest = lastUsage.keys().next().value;
78
+ if (oldest) lastUsage.delete(oldest);
79
+ }
80
+ lastUsage.set(sid, evt);
81
+ }
82
+
83
+ function truncate(s) {
84
+ if (typeof s !== 'string') return s;
85
+ if (s.length <= MAX_TOOL_TEXT) return s;
86
+ return s.slice(0, MAX_TOOL_TEXT) + `\n…[截断,共 ${s.length} 字符]`;
87
+ }
88
+
89
+ /**
90
+ * 给 project bus 挂折叠订阅(幂等 —— broker.getProjectBus 每次创建 bus 时调)。
91
+ */
92
+ export function attachLiveTurnTracker(bus) {
93
+ if (bus.__liveTurnTracked) return;
94
+ bus.__liveTurnTracked = true;
95
+ bus.subscribe('*', fold);
96
+ }
97
+
98
+ /**
99
+ * 取 session 当前 turn 的快照;没有在跑、且上一轮已过 grace 期 → null。
100
+ * 返回值直接可作 ws.live_turn 帧 payload(messages 是前端 display 形态)。
101
+ *
102
+ * running=false 表示这是刚结束那一轮的尾巴(grace 期内),前端据此**只合并消息、
103
+ * 不把自己切回 streaming 态**。
104
+ */
105
+ export function getLiveTurnSnapshot(sessionId) {
106
+ const st = liveTurns.get(sessionId);
107
+ if (!st) return null;
108
+ if (st.endedAt && Date.now() - st.endedAt > ENDED_GRACE_MS) {
109
+ liveTurns.delete(sessionId);
110
+ return null;
111
+ }
112
+ return {
113
+ runId: st.runId,
114
+ seq: st.seq,
115
+ startedAt: st.startedAt,
116
+ running: !st.endedAt,
117
+ messages: st.messages,
118
+ todos: st.todos,
119
+ contextUsage: st.contextUsage,
120
+ };
121
+ }
122
+
123
+ function fold(evt) {
124
+ const sid = evt.sessionId;
125
+ if (!sid) return;
126
+
127
+ // 记在 liveTurns 之外:turn 结束、快照删掉之后,这个数字还要继续给 composer
128
+ // 的 [+] 菜单用。放在 st 判空之前,快照已经清了也照记。
129
+ if (evt.type === 'run.context_usage') rememberUsage(sid, evt);
130
+
131
+ if (evt.type === 'run.start') {
132
+ liveTurns.set(sid, {
133
+ runId: evt.runId || null,
134
+ startedAt: evt.ts || new Date().toISOString(),
135
+ seq: evt.seq || 0,
136
+ messages: [],
137
+ todos: [],
138
+ contextUsage: null,
139
+ endedAt: null,
140
+ _msgCounter: 0,
141
+ });
142
+ return;
143
+ }
144
+
145
+ const st = liveTurns.get(sid);
146
+ if (!st) return;
147
+
148
+ // 已收尾的那一轮只留着给 grace 期内的重连当权威快照,不再吸新事件
149
+ // (新一轮由上面的 run.start 整块换掉)。seq 照常推进,免得尾随订阅从旧游标
150
+ // 起把这轮已经放过的事件再放一遍。
151
+ if (st.endedAt) {
152
+ if (evt.type === 'run.query.end' || Date.now() - st.endedAt > ENDED_GRACE_MS) {
153
+ liveTurns.delete(sid);
154
+ return;
155
+ }
156
+ if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
157
+ return;
158
+ }
159
+
160
+ // stale 事件(老 turn 的尾巴)不折叠也不清新 turn 的状态
161
+ const runMatches = !evt.runId || !st.runId || evt.runId === st.runId;
162
+
163
+ switch (evt.type) {
164
+ case 'run.delta.text':
165
+ if (runMatches) appendText(st, 'assistant', evt.text, evt.parentToolUseId);
166
+ break;
167
+ case 'run.delta.thinking':
168
+ if (runMatches) appendText(st, 'thinking', evt.text, evt.parentToolUseId);
169
+ break;
170
+ case 'run.tool_use.started':
171
+ if (runMatches && evt.blockId && !st.messages.some(m => m.id === evt.blockId)) {
172
+ st.messages.push({
173
+ id: evt.blockId, role: 'tool', toolName: evt.name,
174
+ toolInput: undefined, status: 'running', runId: st.runId,
175
+ ...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
176
+ });
177
+ }
178
+ break;
179
+ case 'run.delta.tool_use': {
180
+ if (!runMatches) break;
181
+ const existing = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
182
+ if (existing) existing.toolInput = evt.input;
183
+ else if (evt.blockId) {
184
+ st.messages.push({
185
+ id: evt.blockId, role: 'tool', toolName: evt.name,
186
+ toolInput: evt.input, status: 'running', runId: st.runId,
187
+ ...(evt.parentToolUseId ? { parentToolUseId: evt.parentToolUseId } : {}),
188
+ });
189
+ }
190
+ break;
191
+ }
192
+ case 'run.delta.tool_result': {
193
+ if (!runMatches) break;
194
+ const tool = st.messages.find(m => m.role === 'tool' && m.id === evt.blockId);
195
+ if (tool) {
196
+ tool.status = evt.ok ? 'success' : 'error';
197
+ if (evt.output !== undefined) tool.toolOutput = truncate(typeof evt.output === 'string' ? evt.output : JSON.stringify(evt.output));
198
+ if (evt.error) tool.toolError = truncate(typeof evt.error === 'string' ? evt.error : JSON.stringify(evt.error));
199
+ // images 不进快照:base64 体积大,turn 结束后 jsonl hydrate 会带回来
200
+ }
201
+ break;
202
+ }
203
+ case 'run.tool_use_summary': {
204
+ if (!runMatches || !evt.summary) break;
205
+ const ids = Array.isArray(evt.blockIds) ? evt.blockIds : [];
206
+ const hit = st.messages.find(m => m.role === 'tool' && ids.includes(m.id));
207
+ if (hit) hit.groupSummary = evt.summary;
208
+ break;
209
+ }
210
+ case 'run.todo.updated':
211
+ if (runMatches && Array.isArray(evt.todos)) st.todos = evt.todos;
212
+ break;
213
+ case 'run.context_usage':
214
+ if (runMatches) st.contextUsage = evt;
215
+ break;
216
+ case 'run.done':
217
+ case 'run.error':
218
+ case 'run.cancelled':
219
+ // 不立刻删:留 grace 期给"收尾瞬间重连"当权威快照(见文件头说明)
220
+ if (runMatches) {
221
+ st.endedAt = Date.now();
222
+ if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
223
+ }
224
+ return;
225
+ case 'run.query.end':
226
+ liveTurns.delete(sid);
227
+ return;
228
+ default:
229
+ break;
230
+ }
231
+
232
+ // 所有本 session 事件都推进快照游标(包括未折叠类型)——
233
+ // 重连订阅从 seq 起,不折叠的事件类型(queue depth 等)丢了也无碍(幂等 UI 状态)
234
+ if (typeof evt.seq === 'number' && evt.seq > st.seq) st.seq = evt.seq;
235
+ }
236
+
237
+ function appendText(st, role, text, parentToolUseId = null) {
238
+ if (!text) return;
239
+ const last = st.messages[st.messages.length - 1];
240
+ // 子代理时间轴:不同 parent 的流不互吸(跟前端 lib/chat-stream.js 同构)
241
+ if (last && last.role === role && (last.parentToolUseId || null) === (parentToolUseId || null)) {
242
+ last.content = (last.content || '') + text;
243
+ return;
244
+ }
245
+ st.messages.push({
246
+ id: `${st.runId || 'live'}:m${st._msgCounter++}`,
247
+ ...(parentToolUseId ? { parentToolUseId } : {}),
248
+ role,
249
+ content: text,
250
+ runId: st.runId,
251
+ });
252
+ }