@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,464 +1,464 @@
1
- /**
2
- * EventBus — agent 事件总线(标准化事件 schema)
3
- *
4
- * 设计:
5
- * - 极简 EventEmitter(基于 Set<listener>),不依赖 node:events 避免 max listener warn
6
- * - 每条事件至少含 { type, runId, ts }
7
- * - 监听器订阅时可选择全订('*')或按前缀('run.delta')或按 type 精确匹配
8
- *
9
- * 标准事件 schema(type):
10
- * ── 主流程 ──
11
- * run.start 开始
12
- * run.round.start { round }
13
- * run.round.end { round, stopReason, usage? }
14
- * run.delta.text { round, text }
15
- * run.tool_use_summary { summary, blockIds } SDK helper 对一批工具调用的一句话总结
16
- * run.delta.thinking { round, text }
17
- * run.delta.tool_use { round, blockId, name, input }
18
- * run.delta.tool_input { round, blockId, name, filePath?, append?, done? } Edit/Write 入参真流式(节流后的字段尾巴增量,工作台舞台层直播代码用)
19
- * run.delta.tool_result { round, blockId, name, ok, output?, error? }
20
- * run.todo.updated { todos }
21
- * run.cancelled { reason }
22
- * run.done { finalText, artifactPath?, snapshot }
23
- * run.error { message, code?, stack? }
24
- *
25
- * ── P0+ stage 1 新增(SDK 28+ 种 message 类型映射)──
26
- * run.tool_progress { blockId, toolName, elapsedSeconds } 工具执行中(>1s 才发)
27
- * run.prompt_suggestion { suggestion } 每轮后预测下条 prompt
28
- * run.task.started { taskId, description, subagentType, taskType } subagent 启动(只发真子代理,bash/workflow 任务被收口)
29
- * run.task.progress { taskId, description, summary?, lastToolName? } subagent 30s 摘要(agentProgressSummaries)
30
- * run.task.updated { taskId, patch } subagent 状态 patch
31
- * run.task.notification { taskId, status, summary } subagent 完成/失败/停止
32
- * run.files_persisted { files: [{ filename, file_id }], failed } agent 写完文件
33
- * run.memory_recall { mode, memories } 自动 memory 召回
34
- * run.notification { key, text, priority } 系统通知(弹 toast)
35
- * run.session_state { state: 'idle' | 'running' | 'requires_action' }
36
- * run.system_init { agents, tools, mcp_servers, model, ... } init 元信息
37
- * run.hook.started { hookName, hookEvent } hook 生命周期(仅 includeHookEvents)
38
- * run.hook.response { hookName, hookEvent, outcome }
39
- * run.compact_boundary { compactMetadata } compact 边界
40
- * run.status { status } compacting / requesting / null
41
- * run.rate_limit { info } rate limit 变化
42
- * run.image_generated { path, sizeBytes, prompt, assetRole, ... } generate_image 完成(Phase A)
43
- * board.updated { objectId, zoneId, summary } pin_to_board 改画布布局(sessionId:null 广播,前端整份重拉 board.json)
44
- * project.active_session { activeSessionId } 项目级会话指针变更(故意不带 sessionId —— 见构造器注释)
45
- *
46
- * 外层把 EventBus 桥接到:
47
- * - WebSocket:subscribe('*') → ws.send(JSON.stringify(evt))
48
- * - 文件审计:subscribe('*') → append events.jsonl
49
- * - 内存测试:buffer.push(evt) 然后断言
50
- */
51
-
52
- import { resolveModelContextWindow, brandOfModel } from './model-context.js';
53
-
54
- /**
55
- * Replay buffer 容量:单 project bus 保留最近 N 条事件供 WS 重连回放。
56
- * 一次 turn 可能 emit 上千条 delta(thinking / text / tool);2000 够覆盖典型
57
- * 几秒到几十秒的网络抖动重连窗口。FAR 旧的事件会被 client 通过 Sessions hydrate
58
- * 重新拉,不依赖 buffer。
59
- */
60
- const REPLAY_BUFFER_SIZE = 2000;
61
-
62
- export class EventBus {
63
- constructor() {
64
- /** @type {Set<{ pattern: string, fn: (evt: object) => void }>} */
65
- this._listeners = new Set();
66
- /** 单调递增 seq,每个 publish 自增一;event.seq 已存在则不覆盖 */
67
- this._seq = 0;
68
- /** Ring buffer:保留最近 REPLAY_BUFFER_SIZE 条事件;FIFO 满了就 shift 最旧 */
69
- this._buffer = [];
70
- }
71
-
72
- /**
73
- * 订阅事件。
74
- * @param {string} pattern - '*' 全订 / 'run.delta' 前缀 / 'run.done' 精确
75
- * @param {(evt: object) => void} fn
76
- * @returns {() => void} 取消订阅
77
- */
78
- subscribe(pattern, fn) {
79
- if (typeof pattern !== 'string') throw new Error('EventBus.subscribe: pattern must be string');
80
- if (typeof fn !== 'function') throw new Error('EventBus.subscribe: fn must be function');
81
- const entry = { pattern, fn };
82
- this._listeners.add(entry);
83
- return () => this._listeners.delete(entry);
84
- }
85
-
86
- /**
87
- * 发布事件。listener 抛错会被吞 + 控制台 warn,不影响其他订阅者。
88
- * 副作用:分配 event.seq(若调用方未显式带 seq)+ 进入 ring buffer。
89
- */
90
- publish(event) {
91
- if (event.seq == null) event.seq = ++this._seq;
92
- this._buffer.push(event);
93
- if (this._buffer.length > REPLAY_BUFFER_SIZE) this._buffer.shift();
94
-
95
- for (const { pattern, fn } of this._listeners) {
96
- if (matches(pattern, event.type)) {
97
- try {
98
- fn(event);
99
- } catch (err) {
100
- console.warn(`[EventBus] listener for "${pattern}" threw:`, err.message);
101
- }
102
- }
103
- }
104
- }
105
-
106
- /**
107
- * Replay-then-live 订阅 — WS 重连用。
108
- *
109
- * 流程(单线程 Node 保证原子,subscribe 与 buffer 快照间无 publish 触发):
110
- * 1. subscribe('*') —— listener 把进来的事件先 push 到 queue(live=false)
111
- * 2. 同步遍历 buffer,filter seq > since,对每条调 fn(dedupe 用 set)
112
- * 3. drain queue —— 跳过 seq 已 replay 过的(buffer 和 queue 边界重叠)
113
- * 4. 切 live,listener 直接 fn(evt)
114
- *
115
- * gap 判断:
116
- * - 客户端 since=0 → 首次连,无 replay 需求 → gap=false
117
- * - since > _seq → server 重启过,client 看过的 seq 现在不存在 → gap=true
118
- * - buffer 最旧 seq > since+1 → 中间一段被 ring 挤掉了 → gap=true
119
- * - 否则 buffer 完整覆盖 (since, _seq] → gap=false
120
- *
121
- * @param {number} since - 客户端最后看到的 seq(0 = 没看过任何事件)
122
- * @param {(evt: object) => void} fn
123
- * @returns {{ unsubscribe: () => void, replayed: number, gap: boolean }}
124
- */
125
- subscribeFromSeq(since, fn) {
126
- let live = false;
127
- const queue = [];
128
- const off = this.subscribe('*', (evt) => {
129
- if (!live) queue.push(evt);
130
- else fn(evt);
131
- });
132
-
133
- const needsReplay = since > 0 && since < this._seq;
134
- const buffered = this._buffer.length > 0;
135
- const restarted = since > this._seq;
136
- const gap = restarted || (needsReplay && (!buffered || this._buffer[0].seq > since + 1));
137
-
138
- // since=0 也 replay buffer 里 seq>0 的事件 —— ws/index.js 调用前先 await sendHydrate
139
- // 是异步阻塞(拉 jsonl 100~数百 ms),期间若 agent emit 事件进 buffer 但 listener
140
- // 还没 attach,老逻辑 `since > 0 ? ... : []` 让 since=0 时 replay=[] → 这些事件全丢,
141
- // 用户报"发 chat 后 agent 不回应"。改成无条件 filter:since=0 + buffer 空(首连)→
142
- // replay=[] 不变;since=0 + buffer 含 hydrate 期间事件 → 全 replay 推前端。
143
- const replay = this._buffer.filter(e => e.seq > since);
144
- const replayedSeqs = new Set();
145
- for (const evt of replay) {
146
- replayedSeqs.add(evt.seq);
147
- try { fn(evt); }
148
- catch (err) { console.warn(`[EventBus] replay listener threw:`, err.message); }
149
- }
150
- for (const evt of queue) {
151
- if (replayedSeqs.has(evt.seq)) continue;
152
- try { fn(evt); }
153
- catch (err) { console.warn(`[EventBus] queue drain threw:`, err.message); }
154
- }
155
- live = true;
156
- return { unsubscribe: off, replayed: replay.length, gap };
157
- }
158
-
159
- /**
160
- * 同步 collect 所有事件到一个数组(测试用)。返回数组 + 取消函数。
161
- */
162
- collect(pattern = '*') {
163
- const buffer = [];
164
- const off = this.subscribe(pattern, (evt) => buffer.push(evt));
165
- return { buffer, stop: off };
166
- }
167
- }
168
-
169
- function matches(pattern, type) {
170
- if (pattern === '*') return true;
171
- if (pattern === type) return true;
172
- // 前缀匹配:'run.delta' 匹配 'run.delta.text'
173
- return type.startsWith(pattern + '.');
174
- }
175
-
176
- // ── 事件构造器(type-safe helpers,调用方少写字面量)──
177
-
178
- export const Events = {
179
- start: () => ({ type: 'run.start' }),
180
- roundStart: (round) => ({ type: 'run.round.start', round }),
181
- roundEnd: (round, stopReason, usage) => ({ type: 'run.round.end', round, stopReason, usage }),
182
- deltaText: (round, text) => ({ type: 'run.delta.text', round, text }),
183
- // 铅笔精灵的手写短句(2026-08-14 日记本批):回合内回复的压缩旁白,
184
- // 本地整形、一条 assistant 消息一发。⚠️ 2026-08-19 前这里同一 round 会来
185
- // 两发(首句底稿 refined:false + haiku 精修 refined:true,前端后到覆盖)——
186
- // 那发 haiku 写死走订阅、不跟随会话模型,整条线路已拆,`refined` 随之取消。
187
- // 收场 recap(run.recap)是同一条 haiku 通道的另一个出口,同批退役。
188
- spriteSummary: (round, text) => ({ type: 'run.sprite_summary', round, text }),
189
- // SDK helper 生成的一句话工具批摘要(Claude Code 侧栏那种折叠标题)
190
- toolUseSummary: (summary, blockIds) => ({ type: 'run.tool_use_summary', summary, blockIds }),
191
- deltaThinking: (round, text) => ({ type: 'run.delta.thinking', round, text }),
192
- deltaToolUse: (round, blockId, name, input) => ({ type: 'run.delta.tool_use', round, blockId, name, input }),
193
- // 真流式工具入参(2026-07-28):patch = { filePath?, append?, done? }。
194
- // append 是抽出字段(Edit.new_string / Write.content)相对上次的纯文本增量
195
- deltaToolInput: (round, blockId, name, patch) => ({ type: 'run.delta.tool_input', round, blockId, name, ...patch }),
196
-
197
- // 工具 streaming 起点 —— SDK content_block_start (type: tool_use) 触发,
198
- // 只携 blockId + name,input 此时还没流完。让前端立刻显示 icon + 名字
199
- // status='running',等后续 deltaToolUse 来时同 blockId update 完整 input。
200
- toolUseStarted: (round, blockId, name) => ({ type: 'run.tool_use.started', round, blockId, name }),
201
- deltaToolResult: (round, blockId, name, ok, output, error, images) => ({
202
- type: 'run.delta.tool_result', round, blockId, name, ok,
203
- ...(output !== undefined ? { output } : {}),
204
- ...(error ? { error } : {}),
205
- // C24:image content blocks(base64)单独传,让前端 <img src="data:..."> 渲染
206
- ...(images && images.length > 0 ? { images } : {}),
207
- }),
208
- todoUpdated: (todos) => ({ type: 'run.todo.updated', todos }),
209
- cancelled: (reason) => ({ type: 'run.cancelled', reason }),
210
- done: (finalText, artifactPath, snapshot) => ({
211
- type: 'run.done', finalText, artifactPath, snapshot,
212
- }),
213
- error: (message, code, stack) => ({ type: 'run.error', message, code, stack }),
214
-
215
- // streamInput 重构(Phase 2):session-level query 起停事件,跟 per-turn
216
- // run.start/done 区分 —— 前端用它判断"会话还活着,能继续追加" vs "会话死了"
217
- querySessionStart: (sessionId) => ({ type: 'run.query.start', sessionId }),
218
- querySessionEnd: (sessionId, reason) => ({ type: 'run.query.end', sessionId, reason }),
219
- // 排队提示:用户在 agent 跑时追加消息,UI 显示"已排队 N 条"
220
- queueDepth: (sessionId, depth) => ({ type: 'run.queue.depth', sessionId, depth }),
221
-
222
- // ── 会话收敛 E1a(2026-08-13):项目级 active session 指针变更广播 ──
223
- //
224
- // 会话真相源收敛到 projects.active_session_id(服务端指针)之后,指针一动
225
- // 就广播这条,同项目的其他标签页靠它把自己对齐到服务端真相。
226
- //
227
- // ⚠️ 这条事件**绝对不能带 sessionId 字段**:ws/index.js 的下发过滤器是
228
- // `event.sessionId && event.sessionId !== sid` —— 带上的话,正开着**别的**
229
- // 会话(或还没有会话)的标签页会把它过滤掉,而那些标签页恰恰是最需要
230
- // 知道"指针已经指向别处"的人。载荷字段叫 activeSessionId,绕开过滤器的
231
- // 同时语义也更准:它是指针的新值,不是"这条事件属于哪个会话"。
232
- // activeSessionId 可为 null(指针被清空,比如指向的会话被删)。
233
- projectActiveSession: (activeSessionId) => ({ type: 'project.active_session', activeSessionId }),
234
-
235
- // ── P0+ stage 1 新增(28+ 种 SDK message 翻译)──
236
- toolProgress: (blockId, toolName, elapsedSeconds) => ({
237
- type: 'run.tool_progress', blockId, toolName, elapsedSeconds,
238
- }),
239
- promptSuggestion: (suggestion) => ({ type: 'run.prompt_suggestion', suggestion }),
240
- // toolUseId 关键 —— main agent 用 Task 工具调子代理时,SDK 推 task_*
241
- // events 都带 tool_use_id,前端用它把 task 状态绑到对应的 Task tool message
242
- // ⚠️ 只发真子代理(agent-shared.js isSubagentTask 收口):SDK 的统一任务
243
- // 系统里后台 Bash / Workflow 也发 task_*,那些不进这条事件。
244
- // subagentType = SDK task_started.subagent_type(explorer / vision-checker
245
- // 这种真名);taskType 是 'local_agent' 这类泛名,只作兜底展示。
246
- taskStarted: (taskId, description, subagentType, taskType, prompt, toolUseId) => ({
247
- type: 'run.task.started', taskId, description, subagentType, taskType, prompt, toolUseId,
248
- }),
249
- taskProgress: (taskId, description, summary, lastToolName, usage, toolUseId) => ({
250
- type: 'run.task.progress', taskId, description, summary, lastToolName, usage, toolUseId,
251
- }),
252
- taskUpdated: (taskId, patch, toolUseId) => ({
253
- type: 'run.task.updated', taskId, patch, toolUseId,
254
- }),
255
- taskNotification: (taskId, status, summary, usage, toolUseId) => ({
256
- type: 'run.task.notification', taskId, status, summary, usage, toolUseId,
257
- }),
258
- filesPersisted: (files, failed) => ({ type: 'run.files_persisted', files, failed }),
259
- memoryRecall: (mode, memories) => ({ type: 'run.memory_recall', mode, memories }),
260
- notification: (key, text, priority, color, timeoutMs) => ({
261
- type: 'run.notification', key, text, priority, color, timeoutMs,
262
- }),
263
- sessionState: (state) => ({ type: 'run.session_state', state }),
264
- systemInit: (info) => ({ type: 'run.system_init', info }),
265
- hookStarted: (hookName, hookEvent) => ({ type: 'run.hook.started', hookName, hookEvent }),
266
- hookResponse: (hookName, hookEvent, outcome, output, exitCode) => ({
267
- type: 'run.hook.response', hookName, hookEvent, outcome, output, exitCode,
268
- }),
269
-
270
- // C4 FileChanged hook → 前端 reload iframe
271
- fileChanged: (filePath, event) => ({ type: 'run.file_changed', filePath, event }),
272
-
273
- // ── Phase 1 翻译补全 ──
274
-
275
- // SDKAPIRetryMessage(sdk.d.ts:2322):API 请求失败可重试。
276
- // 当前 handleSystemMessage 把 'api_retry' subtype 落进 default warn —— 改为 emit
277
- // 让上层(前端 / 监控)能看到"还在重试"。
278
- // error_status: HTTP 状态码,连接错误/超时时为 null(无 HTTP 响应)。
279
- apiRetry: (attempt, maxRetries, retryDelayMs, errorStatus, errorKind) => ({
280
- type: 'run.api_retry',
281
- attempt,
282
- maxRetries,
283
- retryDelayMs,
284
- errorStatus, // number | null
285
- errorKind, // SDKAssistantMessageError union: rate_limit / server_error / ...
286
- }),
287
-
288
- // ── Phase 2 hooks 主动捕事件 ──
289
-
290
- // SessionStart hook:source 'startup' | 'resume' | 'clear' | 'compact'
291
- // 上层据此可决定要不要展示"接续上次会话"提示
292
- sessionStart: (source, agentType, model) => ({
293
- type: 'run.session_start', source, agentType, model,
294
- }),
295
-
296
- // SubagentStart hook(sdk.d.ts:5258):主动捕子代理启动。
297
- // SDK system 'task_started' message 是间接路径(依赖 agentProgressSummaries),
298
- // 这条 hook 是更可靠的主入口。
299
- subagentStart: (agentId, agentType) => ({
300
- type: 'run.subagent.start', agentId, agentType,
301
- }),
302
-
303
- // SubagentStop hook(sdk.d.ts:5269):子代理结束。带 last_assistant_message 字段
304
- // 不用读 transcript 也能拿到收尾文本。
305
- // toolUseId:SDK callback 第 2 参,是触发该 subagent 启动的 main agent
306
- // Task tool_use_id —— 前端能用它把 critique 挂回对应的 Task message。
307
- subagentStop: (agentId, agentType, lastAssistantMessage, transcriptPath, toolUseId) => ({
308
- type: 'run.subagent.stop',
309
- agentId, agentType,
310
- lastAssistantMessage, // 可选;agent 收尾的最后一句
311
- transcriptPath, // 可选;子代理转录文件路径,前端展开"完整对话"用
312
- ...(toolUseId ? { toolUseId } : {}),
313
- }),
314
-
315
- // PostToolUseFailure hook:工具失败,hook 已注入了恢复建议给 agent,
316
- // 上层只需可见"哪个工具失败了"做监控/告警。
317
- toolFailure: (toolName, error) => ({
318
- type: 'run.tool_failure', toolName, error,
319
- }),
320
-
321
- // ── Canvas 焕新升级 S1(2026-05-02)──
322
-
323
- // S1d PostToolUse(Edit|Write canvas.html) hook 自动检测改动的 page 号 emit。
324
- // 前端 SlideNavigator 收到后自动 scrollIntoView + 1.5s pulse 高亮该 section。
325
- // pages: number[] —— 一次 Edit 可能跨多页(罕见),通常单元素。
326
- // anchor: 可选,data-anchor 值(如 'cover-title'),前端能精确高亮元素而非整页。
327
- canvasFocusPage: (pages, anchor) => ({
328
- type: 'run.canvas_focus_page',
329
- pages,
330
- ...(anchor ? { anchor } : {}),
331
- }),
332
-
333
- // ── Canvas 焕新 C1(2026-05-02):MCP 工具反向通道 + tweaks/buffer 通知 ──
334
-
335
- // navigate_to_page 工具触发,前端 ProjectWorkspace 收到后切到第 N 页
336
- // (SlideNavigator setActivePage / iframe 内 scrollIntoView section[data-page="N"])
337
- canvasNavigate: (page) => ({ type: 'run.canvas_navigate', page }),
338
-
339
- // preview_deck 工具触发:前端把这张 deck 卡"替用户双击一下"
340
- //(收起态→内嵌渲染;已展开→画布内最大化窗)。path 为 null = 当前会话的 deck
341
- deckPreview: (filePath) => ({ type: 'run.deck_preview', path: filePath || null }),
342
-
343
- // highlight 工具触发,前端在 InspectFloatingCard 同层挂 pulse overlay
344
- canvasHighlight: (selector, durationMs = 1500) => ({
345
- type: 'run.canvas_highlight', selector, durationMs,
346
- }),
347
-
348
- // expose_tweaks 工具完成,前端 TweaksPanel reload spec.tweaks
349
- tweaksExposed: (count, added, replaced) => ({
350
- type: 'run.tweaks_exposed', count, added, replaced,
351
- }),
352
-
353
- // clear_pending_changes 工具完成,前端可清相应本地 pending hint。
354
- // clearedIds 让前端 comments state 精确 filter 出橙色 overlay(前后端 id 已统一)
355
- pendingChangesCleared: (clearedIds, removed, remaining) => ({
356
- type: 'run.pending_changes_cleared', clearedIds, removed, remaining,
357
- }),
358
-
359
- // ── A4.1(2026-05-02):AskUserQuestion 走 SDK canUseTool 路径 ──
360
- //
361
- // SDK binary 把 AskUserQuestion 设为 `shouldDefer: true` + `requiresUserInteraction: true`,
362
- // 工具的 checkPermissions 总返 `behavior: 'ask', message: 'Answer questions?'`,
363
- // 等 host 程序通过 canUseTool callback 返 `behavior: 'allow', updatedInput: {
364
- // ...input, answers: {...} }`,binary 拿到带 answers 的 input → 调 tool.call →
365
- // 工具直接返回 answers 作 result(cli.js:GR6.call 实现)。
366
- //
367
- // 我们的 canUseTool(session-loop.js)拦到此事件 → emit run.ask_user_question →
368
- // 前端 AskUserQuestionView 卡片 → 用户点选项 → POST /answer → resolve
369
- // canUseTool 的 await,返回 updatedInput。
370
- //
371
- // questions 是从工具 input 摘出的 `{ question, header, options, multiSelect }[]`
372
- // 数组(详见 cli.js:zHK schema),前端按现有卡片字段直接消费。
373
- askUserQuestion: (toolUseId, questions) => ({
374
- type: 'run.ask_user_question',
375
- toolUseId,
376
- questions,
377
- }),
378
-
379
- // ── A1.2(2026-05-02):实时上下文用量 ──
380
- // SDKControlGetContextUsageResponse(sdk.d.ts:2451-2541)由 query.getContextUsage()
381
- // 返回。session-loop.js 在每个 assistant message 后 await 一次 emit,前端 ContextUsageBar
382
- // 接事件渲进度条 + breakdown + autoCompact 阈值预警。
383
- //
384
- // 关键字段(为前端做轻量化封装,不全量透传以节省 ws 带宽):
385
- // totalTokens / maxTokens / percentage — 主进度条数据
386
- // autoCompactThreshold — 触发 autoCompact 的阈值(绝对 token)
387
- // isAutoCompactEnabled — 当前开关状态(前端预警条件之一)
388
- // model — 当前模型 id
389
- // messageBreakdown.toolCallsByType — 工具消耗 token 排名(展开看)
390
- // memoryFiles / mcpTools / agents / skills — 各类目消耗(展开看)
391
- //
392
- // categories / gridRows 不传 —— 前端不渲网格,只用 percentage + breakdown。
393
- // ── 图片生成(Phase A,2026-05-06 后段)──
394
- // generate_image MCP 工具调完后 emit。前端可在 chat / timeline 实时插
395
- // thumbnail,并把 path 关联回 spec.json 的 record_decision 节点。
396
- // 字段:
397
- // path agent cwd 相对路径(assets/generated/<file>.png)
398
- // absPath 绝对落地路径(前端不一定用,便于审计)
399
- // sizeBytes png 字节数
400
- // prompt 生成 prompt 全文
401
- // assetRole 语义类(hero/cover/bg/.../pattern),可空
402
- // aspectRatio / imageSize 生成参数
403
- // referenceImageCount 喂进去的参考图数量
404
- // accompanyText 模型可选的文字 commentary(responseModalities 含 TEXT 时才有)
405
- imageGenerated: (info) => ({
406
- type: 'run.image_generated',
407
- path: info.path,
408
- absPath: info.absPath,
409
- sizeBytes: info.sizeBytes,
410
- prompt: info.prompt,
411
- assetRole: info.assetRole || null,
412
- aspectRatio: info.aspectRatio,
413
- imageSize: info.imageSize,
414
- referenceImageCount: info.referenceImageCount || 0,
415
- ...(info.accompanyText ? { accompanyText: info.accompanyText } : {}),
416
- }),
417
-
418
- // appModel: NoDesign 上层真实 model(如 kimi-k2.6)。spoofing 后 SDK 的
419
- // usage.model 是 alias(如 claude-opus-4-7[1m])不可信;调用方需把真 appModel
420
- // 传进来。给定 appModel 时按真实容量算 maxTokens / percentage,前端 ContextUsageBar
421
- // 显的就是 gateway 真正能扛的容量(不是 SDK 内部 compact 触发线)。
422
- contextUsage: (usage, appModel = null) => {
423
- // 这里 import 在 file 顶(已加),避免循环依赖问题
424
- //
425
- // 分母的兜底链只能有一条(2026-07-30):以前 hooks.js 那个生产者自己写了
426
- // 一条更长的(?? rawMaxTokens ?? 256000),两条链在 appModel 缺失时给出不同
427
- // 的分母,同一个会话前后两次百分比对不上。合并到这里,256000 那个 kimi 时代
428
- // 的常量去掉 —— 拿它当 Claude 会话的分母只会算出个假的高百分比。
429
- const realMax = (appModel ? resolveModelContextWindow(appModel) : null)
430
- ?? usage.maxTokens
431
- ?? usage.rawMaxTokens
432
- ?? null;
433
- const totalTokens = usage.totalTokens;
434
- const percentage = realMax > 0 ? Math.round((totalTokens / realMax) * 100) : (usage.percentage || 0);
435
- return {
436
- type: 'run.context_usage',
437
- totalTokens,
438
- maxTokens: realMax, // 真实容量(kimi=256k)—— 前端进度条分母
439
- sdkMaxTokens: usage.maxTokens, // SDK 给的(compact 触发线 230k)—— debug
440
- percentage,
441
- autoCompactThreshold: usage.autoCompactThreshold,
442
- isAutoCompactEnabled: usage.isAutoCompactEnabled,
443
- model: appModel || usage.model,
444
- // 出自谁家(BRANDS 之一)——画布精灵据此换身份标。⚠️ 不能让前端读 SDK 的 usage.model 去认牌子:
445
- // spoofing 之后那是 alias(DeepSeek 行报 claude-opus-4-7[1m]),照它认牌子会把鲸画成星芒
446
- brand: appModel ? brandOfModel(appModel) : null,
447
- // 轻量化 breakdown — 前端 hover/expand 能看到细节,不渲细节时不占空间
448
- messageBreakdown: usage.messageBreakdown ? {
449
- toolCallTokens: usage.messageBreakdown.toolCallTokens,
450
- toolResultTokens: usage.messageBreakdown.toolResultTokens,
451
- attachmentTokens: usage.messageBreakdown.attachmentTokens,
452
- assistantMessageTokens: usage.messageBreakdown.assistantMessageTokens,
453
- userMessageTokens: usage.messageBreakdown.userMessageTokens,
454
- // toolCallsByType 是排行(用于"哪个工具吃 token 最多"),保留 top 10
455
- toolCallsByType: (usage.messageBreakdown.toolCallsByType || [])
456
- .slice(0, 10),
457
- } : null,
458
- // 类目级别 token 数(聚合,不传完整数组)
459
- memoryFilesTokens: (usage.memoryFiles || []).reduce((s, m) => s + (m.tokens || 0), 0),
460
- mcpToolsTokens: (usage.mcpTools || []).reduce((s, t) => s + (t.tokens || 0), 0),
461
- agentsTokens: (usage.agents || []).reduce((s, a) => s + (a.tokens || 0), 0),
462
- };
463
- },
464
- };
1
+ /**
2
+ * EventBus — agent 事件总线(标准化事件 schema)
3
+ *
4
+ * 设计:
5
+ * - 极简 EventEmitter(基于 Set<listener>),不依赖 node:events 避免 max listener warn
6
+ * - 每条事件至少含 { type, runId, ts }
7
+ * - 监听器订阅时可选择全订('*')或按前缀('run.delta')或按 type 精确匹配
8
+ *
9
+ * 标准事件 schema(type):
10
+ * ── 主流程 ──
11
+ * run.start 开始
12
+ * run.round.start { round }
13
+ * run.round.end { round, stopReason, usage? }
14
+ * run.delta.text { round, text }
15
+ * run.tool_use_summary { summary, blockIds } SDK helper 对一批工具调用的一句话总结
16
+ * run.delta.thinking { round, text }
17
+ * run.delta.tool_use { round, blockId, name, input }
18
+ * run.delta.tool_input { round, blockId, name, filePath?, append?, done? } Edit/Write 入参真流式(节流后的字段尾巴增量,工作台舞台层直播代码用)
19
+ * run.delta.tool_result { round, blockId, name, ok, output?, error? }
20
+ * run.todo.updated { todos }
21
+ * run.cancelled { reason }
22
+ * run.done { finalText, artifactPath?, snapshot }
23
+ * run.error { message, code?, stack? }
24
+ *
25
+ * ── P0+ stage 1 新增(SDK 28+ 种 message 类型映射)──
26
+ * run.tool_progress { blockId, toolName, elapsedSeconds } 工具执行中(>1s 才发)
27
+ * run.prompt_suggestion { suggestion } 每轮后预测下条 prompt
28
+ * run.task.started { taskId, description, subagentType, taskType } subagent 启动(只发真子代理,bash/workflow 任务被收口)
29
+ * run.task.progress { taskId, description, summary?, lastToolName? } subagent 30s 摘要(agentProgressSummaries)
30
+ * run.task.updated { taskId, patch } subagent 状态 patch
31
+ * run.task.notification { taskId, status, summary } subagent 完成/失败/停止
32
+ * run.files_persisted { files: [{ filename, file_id }], failed } agent 写完文件
33
+ * run.memory_recall { mode, memories } 自动 memory 召回
34
+ * run.notification { key, text, priority } 系统通知(弹 toast)
35
+ * run.session_state { state: 'idle' | 'running' | 'requires_action' }
36
+ * run.system_init { agents, tools, mcp_servers, model, ... } init 元信息
37
+ * run.hook.started { hookName, hookEvent } hook 生命周期(仅 includeHookEvents)
38
+ * run.hook.response { hookName, hookEvent, outcome }
39
+ * run.compact_boundary { compactMetadata } compact 边界
40
+ * run.status { status } compacting / requesting / null
41
+ * run.rate_limit { info } rate limit 变化
42
+ * run.image_generated { path, sizeBytes, prompt, assetRole, ... } generate_image 完成(Phase A)
43
+ * board.updated { objectId, zoneId, summary } pin_to_board 改画布布局(sessionId:null 广播,前端整份重拉 board.json)
44
+ * project.active_session { activeSessionId } 项目级会话指针变更(故意不带 sessionId —— 见构造器注释)
45
+ *
46
+ * 外层把 EventBus 桥接到:
47
+ * - WebSocket:subscribe('*') → ws.send(JSON.stringify(evt))
48
+ * - 文件审计:subscribe('*') → append events.jsonl
49
+ * - 内存测试:buffer.push(evt) 然后断言
50
+ */
51
+
52
+ import { resolveModelContextWindow, brandOfModel } from './model-context.js';
53
+
54
+ /**
55
+ * Replay buffer 容量:单 project bus 保留最近 N 条事件供 WS 重连回放。
56
+ * 一次 turn 可能 emit 上千条 delta(thinking / text / tool);2000 够覆盖典型
57
+ * 几秒到几十秒的网络抖动重连窗口。FAR 旧的事件会被 client 通过 Sessions hydrate
58
+ * 重新拉,不依赖 buffer。
59
+ */
60
+ const REPLAY_BUFFER_SIZE = 2000;
61
+
62
+ export class EventBus {
63
+ constructor() {
64
+ /** @type {Set<{ pattern: string, fn: (evt: object) => void }>} */
65
+ this._listeners = new Set();
66
+ /** 单调递增 seq,每个 publish 自增一;event.seq 已存在则不覆盖 */
67
+ this._seq = 0;
68
+ /** Ring buffer:保留最近 REPLAY_BUFFER_SIZE 条事件;FIFO 满了就 shift 最旧 */
69
+ this._buffer = [];
70
+ }
71
+
72
+ /**
73
+ * 订阅事件。
74
+ * @param {string} pattern - '*' 全订 / 'run.delta' 前缀 / 'run.done' 精确
75
+ * @param {(evt: object) => void} fn
76
+ * @returns {() => void} 取消订阅
77
+ */
78
+ subscribe(pattern, fn) {
79
+ if (typeof pattern !== 'string') throw new Error('EventBus.subscribe: pattern must be string');
80
+ if (typeof fn !== 'function') throw new Error('EventBus.subscribe: fn must be function');
81
+ const entry = { pattern, fn };
82
+ this._listeners.add(entry);
83
+ return () => this._listeners.delete(entry);
84
+ }
85
+
86
+ /**
87
+ * 发布事件。listener 抛错会被吞 + 控制台 warn,不影响其他订阅者。
88
+ * 副作用:分配 event.seq(若调用方未显式带 seq)+ 进入 ring buffer。
89
+ */
90
+ publish(event) {
91
+ if (event.seq == null) event.seq = ++this._seq;
92
+ this._buffer.push(event);
93
+ if (this._buffer.length > REPLAY_BUFFER_SIZE) this._buffer.shift();
94
+
95
+ for (const { pattern, fn } of this._listeners) {
96
+ if (matches(pattern, event.type)) {
97
+ try {
98
+ fn(event);
99
+ } catch (err) {
100
+ console.warn(`[EventBus] listener for "${pattern}" threw:`, err.message);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Replay-then-live 订阅 — WS 重连用。
108
+ *
109
+ * 流程(单线程 Node 保证原子,subscribe 与 buffer 快照间无 publish 触发):
110
+ * 1. subscribe('*') —— listener 把进来的事件先 push 到 queue(live=false)
111
+ * 2. 同步遍历 buffer,filter seq > since,对每条调 fn(dedupe 用 set)
112
+ * 3. drain queue —— 跳过 seq 已 replay 过的(buffer 和 queue 边界重叠)
113
+ * 4. 切 live,listener 直接 fn(evt)
114
+ *
115
+ * gap 判断:
116
+ * - 客户端 since=0 → 首次连,无 replay 需求 → gap=false
117
+ * - since > _seq → server 重启过,client 看过的 seq 现在不存在 → gap=true
118
+ * - buffer 最旧 seq > since+1 → 中间一段被 ring 挤掉了 → gap=true
119
+ * - 否则 buffer 完整覆盖 (since, _seq] → gap=false
120
+ *
121
+ * @param {number} since - 客户端最后看到的 seq(0 = 没看过任何事件)
122
+ * @param {(evt: object) => void} fn
123
+ * @returns {{ unsubscribe: () => void, replayed: number, gap: boolean }}
124
+ */
125
+ subscribeFromSeq(since, fn) {
126
+ let live = false;
127
+ const queue = [];
128
+ const off = this.subscribe('*', (evt) => {
129
+ if (!live) queue.push(evt);
130
+ else fn(evt);
131
+ });
132
+
133
+ const needsReplay = since > 0 && since < this._seq;
134
+ const buffered = this._buffer.length > 0;
135
+ const restarted = since > this._seq;
136
+ const gap = restarted || (needsReplay && (!buffered || this._buffer[0].seq > since + 1));
137
+
138
+ // since=0 也 replay buffer 里 seq>0 的事件 —— ws/index.js 调用前先 await sendHydrate
139
+ // 是异步阻塞(拉 jsonl 100~数百 ms),期间若 agent emit 事件进 buffer 但 listener
140
+ // 还没 attach,老逻辑 `since > 0 ? ... : []` 让 since=0 时 replay=[] → 这些事件全丢,
141
+ // 用户报"发 chat 后 agent 不回应"。改成无条件 filter:since=0 + buffer 空(首连)→
142
+ // replay=[] 不变;since=0 + buffer 含 hydrate 期间事件 → 全 replay 推前端。
143
+ const replay = this._buffer.filter(e => e.seq > since);
144
+ const replayedSeqs = new Set();
145
+ for (const evt of replay) {
146
+ replayedSeqs.add(evt.seq);
147
+ try { fn(evt); }
148
+ catch (err) { console.warn(`[EventBus] replay listener threw:`, err.message); }
149
+ }
150
+ for (const evt of queue) {
151
+ if (replayedSeqs.has(evt.seq)) continue;
152
+ try { fn(evt); }
153
+ catch (err) { console.warn(`[EventBus] queue drain threw:`, err.message); }
154
+ }
155
+ live = true;
156
+ return { unsubscribe: off, replayed: replay.length, gap };
157
+ }
158
+
159
+ /**
160
+ * 同步 collect 所有事件到一个数组(测试用)。返回数组 + 取消函数。
161
+ */
162
+ collect(pattern = '*') {
163
+ const buffer = [];
164
+ const off = this.subscribe(pattern, (evt) => buffer.push(evt));
165
+ return { buffer, stop: off };
166
+ }
167
+ }
168
+
169
+ function matches(pattern, type) {
170
+ if (pattern === '*') return true;
171
+ if (pattern === type) return true;
172
+ // 前缀匹配:'run.delta' 匹配 'run.delta.text'
173
+ return type.startsWith(pattern + '.');
174
+ }
175
+
176
+ // ── 事件构造器(type-safe helpers,调用方少写字面量)──
177
+
178
+ export const Events = {
179
+ start: () => ({ type: 'run.start' }),
180
+ roundStart: (round) => ({ type: 'run.round.start', round }),
181
+ roundEnd: (round, stopReason, usage) => ({ type: 'run.round.end', round, stopReason, usage }),
182
+ deltaText: (round, text) => ({ type: 'run.delta.text', round, text }),
183
+ // 铅笔精灵的手写短句(2026-08-14 日记本批):回合内回复的压缩旁白,
184
+ // 本地整形、一条 assistant 消息一发。⚠️ 2026-08-19 前这里同一 round 会来
185
+ // 两发(首句底稿 refined:false + haiku 精修 refined:true,前端后到覆盖)——
186
+ // 那发 haiku 写死走订阅、不跟随会话模型,整条线路已拆,`refined` 随之取消。
187
+ // 收场 recap(run.recap)是同一条 haiku 通道的另一个出口,同批退役。
188
+ spriteSummary: (round, text) => ({ type: 'run.sprite_summary', round, text }),
189
+ // SDK helper 生成的一句话工具批摘要(Claude Code 侧栏那种折叠标题)
190
+ toolUseSummary: (summary, blockIds) => ({ type: 'run.tool_use_summary', summary, blockIds }),
191
+ deltaThinking: (round, text) => ({ type: 'run.delta.thinking', round, text }),
192
+ deltaToolUse: (round, blockId, name, input) => ({ type: 'run.delta.tool_use', round, blockId, name, input }),
193
+ // 真流式工具入参(2026-07-28):patch = { filePath?, append?, done? }。
194
+ // append 是抽出字段(Edit.new_string / Write.content)相对上次的纯文本增量
195
+ deltaToolInput: (round, blockId, name, patch) => ({ type: 'run.delta.tool_input', round, blockId, name, ...patch }),
196
+
197
+ // 工具 streaming 起点 —— SDK content_block_start (type: tool_use) 触发,
198
+ // 只携 blockId + name,input 此时还没流完。让前端立刻显示 icon + 名字
199
+ // status='running',等后续 deltaToolUse 来时同 blockId update 完整 input。
200
+ toolUseStarted: (round, blockId, name) => ({ type: 'run.tool_use.started', round, blockId, name }),
201
+ deltaToolResult: (round, blockId, name, ok, output, error, images) => ({
202
+ type: 'run.delta.tool_result', round, blockId, name, ok,
203
+ ...(output !== undefined ? { output } : {}),
204
+ ...(error ? { error } : {}),
205
+ // C24:image content blocks(base64)单独传,让前端 <img src="data:..."> 渲染
206
+ ...(images && images.length > 0 ? { images } : {}),
207
+ }),
208
+ todoUpdated: (todos) => ({ type: 'run.todo.updated', todos }),
209
+ cancelled: (reason) => ({ type: 'run.cancelled', reason }),
210
+ done: (finalText, artifactPath, snapshot) => ({
211
+ type: 'run.done', finalText, artifactPath, snapshot,
212
+ }),
213
+ error: (message, code, stack) => ({ type: 'run.error', message, code, stack }),
214
+
215
+ // streamInput 重构(Phase 2):session-level query 起停事件,跟 per-turn
216
+ // run.start/done 区分 —— 前端用它判断"会话还活着,能继续追加" vs "会话死了"
217
+ querySessionStart: (sessionId) => ({ type: 'run.query.start', sessionId }),
218
+ querySessionEnd: (sessionId, reason) => ({ type: 'run.query.end', sessionId, reason }),
219
+ // 排队提示:用户在 agent 跑时追加消息,UI 显示"已排队 N 条"
220
+ queueDepth: (sessionId, depth) => ({ type: 'run.queue.depth', sessionId, depth }),
221
+
222
+ // ── 会话收敛 E1a(2026-08-13):项目级 active session 指针变更广播 ──
223
+ //
224
+ // 会话真相源收敛到 projects.active_session_id(服务端指针)之后,指针一动
225
+ // 就广播这条,同项目的其他标签页靠它把自己对齐到服务端真相。
226
+ //
227
+ // ⚠️ 这条事件**绝对不能带 sessionId 字段**:ws/index.js 的下发过滤器是
228
+ // `event.sessionId && event.sessionId !== sid` —— 带上的话,正开着**别的**
229
+ // 会话(或还没有会话)的标签页会把它过滤掉,而那些标签页恰恰是最需要
230
+ // 知道"指针已经指向别处"的人。载荷字段叫 activeSessionId,绕开过滤器的
231
+ // 同时语义也更准:它是指针的新值,不是"这条事件属于哪个会话"。
232
+ // activeSessionId 可为 null(指针被清空,比如指向的会话被删)。
233
+ projectActiveSession: (activeSessionId) => ({ type: 'project.active_session', activeSessionId }),
234
+
235
+ // ── P0+ stage 1 新增(28+ 种 SDK message 翻译)──
236
+ toolProgress: (blockId, toolName, elapsedSeconds) => ({
237
+ type: 'run.tool_progress', blockId, toolName, elapsedSeconds,
238
+ }),
239
+ promptSuggestion: (suggestion) => ({ type: 'run.prompt_suggestion', suggestion }),
240
+ // toolUseId 关键 —— main agent 用 Task 工具调子代理时,SDK 推 task_*
241
+ // events 都带 tool_use_id,前端用它把 task 状态绑到对应的 Task tool message
242
+ // ⚠️ 只发真子代理(agent-shared.js isSubagentTask 收口):SDK 的统一任务
243
+ // 系统里后台 Bash / Workflow 也发 task_*,那些不进这条事件。
244
+ // subagentType = SDK task_started.subagent_type(explorer / vision-checker
245
+ // 这种真名);taskType 是 'local_agent' 这类泛名,只作兜底展示。
246
+ taskStarted: (taskId, description, subagentType, taskType, prompt, toolUseId) => ({
247
+ type: 'run.task.started', taskId, description, subagentType, taskType, prompt, toolUseId,
248
+ }),
249
+ taskProgress: (taskId, description, summary, lastToolName, usage, toolUseId) => ({
250
+ type: 'run.task.progress', taskId, description, summary, lastToolName, usage, toolUseId,
251
+ }),
252
+ taskUpdated: (taskId, patch, toolUseId) => ({
253
+ type: 'run.task.updated', taskId, patch, toolUseId,
254
+ }),
255
+ taskNotification: (taskId, status, summary, usage, toolUseId) => ({
256
+ type: 'run.task.notification', taskId, status, summary, usage, toolUseId,
257
+ }),
258
+ filesPersisted: (files, failed) => ({ type: 'run.files_persisted', files, failed }),
259
+ memoryRecall: (mode, memories) => ({ type: 'run.memory_recall', mode, memories }),
260
+ notification: (key, text, priority, color, timeoutMs) => ({
261
+ type: 'run.notification', key, text, priority, color, timeoutMs,
262
+ }),
263
+ sessionState: (state) => ({ type: 'run.session_state', state }),
264
+ systemInit: (info) => ({ type: 'run.system_init', info }),
265
+ hookStarted: (hookName, hookEvent) => ({ type: 'run.hook.started', hookName, hookEvent }),
266
+ hookResponse: (hookName, hookEvent, outcome, output, exitCode) => ({
267
+ type: 'run.hook.response', hookName, hookEvent, outcome, output, exitCode,
268
+ }),
269
+
270
+ // C4 FileChanged hook → 前端 reload iframe
271
+ fileChanged: (filePath, event) => ({ type: 'run.file_changed', filePath, event }),
272
+
273
+ // ── Phase 1 翻译补全 ──
274
+
275
+ // SDKAPIRetryMessage(sdk.d.ts:2322):API 请求失败可重试。
276
+ // 当前 handleSystemMessage 把 'api_retry' subtype 落进 default warn —— 改为 emit
277
+ // 让上层(前端 / 监控)能看到"还在重试"。
278
+ // error_status: HTTP 状态码,连接错误/超时时为 null(无 HTTP 响应)。
279
+ apiRetry: (attempt, maxRetries, retryDelayMs, errorStatus, errorKind) => ({
280
+ type: 'run.api_retry',
281
+ attempt,
282
+ maxRetries,
283
+ retryDelayMs,
284
+ errorStatus, // number | null
285
+ errorKind, // SDKAssistantMessageError union: rate_limit / server_error / ...
286
+ }),
287
+
288
+ // ── Phase 2 hooks 主动捕事件 ──
289
+
290
+ // SessionStart hook:source 'startup' | 'resume' | 'clear' | 'compact'
291
+ // 上层据此可决定要不要展示"接续上次会话"提示
292
+ sessionStart: (source, agentType, model) => ({
293
+ type: 'run.session_start', source, agentType, model,
294
+ }),
295
+
296
+ // SubagentStart hook(sdk.d.ts:5258):主动捕子代理启动。
297
+ // SDK system 'task_started' message 是间接路径(依赖 agentProgressSummaries),
298
+ // 这条 hook 是更可靠的主入口。
299
+ subagentStart: (agentId, agentType) => ({
300
+ type: 'run.subagent.start', agentId, agentType,
301
+ }),
302
+
303
+ // SubagentStop hook(sdk.d.ts:5269):子代理结束。带 last_assistant_message 字段
304
+ // 不用读 transcript 也能拿到收尾文本。
305
+ // toolUseId:SDK callback 第 2 参,是触发该 subagent 启动的 main agent
306
+ // Task tool_use_id —— 前端能用它把 critique 挂回对应的 Task message。
307
+ subagentStop: (agentId, agentType, lastAssistantMessage, transcriptPath, toolUseId) => ({
308
+ type: 'run.subagent.stop',
309
+ agentId, agentType,
310
+ lastAssistantMessage, // 可选;agent 收尾的最后一句
311
+ transcriptPath, // 可选;子代理转录文件路径,前端展开"完整对话"用
312
+ ...(toolUseId ? { toolUseId } : {}),
313
+ }),
314
+
315
+ // PostToolUseFailure hook:工具失败,hook 已注入了恢复建议给 agent,
316
+ // 上层只需可见"哪个工具失败了"做监控/告警。
317
+ toolFailure: (toolName, error) => ({
318
+ type: 'run.tool_failure', toolName, error,
319
+ }),
320
+
321
+ // ── Canvas 焕新升级 S1(2026-05-02)──
322
+
323
+ // S1d PostToolUse(Edit|Write canvas.html) hook 自动检测改动的 page 号 emit。
324
+ // 前端 SlideNavigator 收到后自动 scrollIntoView + 1.5s pulse 高亮该 section。
325
+ // pages: number[] —— 一次 Edit 可能跨多页(罕见),通常单元素。
326
+ // anchor: 可选,data-anchor 值(如 'cover-title'),前端能精确高亮元素而非整页。
327
+ canvasFocusPage: (pages, anchor) => ({
328
+ type: 'run.canvas_focus_page',
329
+ pages,
330
+ ...(anchor ? { anchor } : {}),
331
+ }),
332
+
333
+ // ── Canvas 焕新 C1(2026-05-02):MCP 工具反向通道 + tweaks/buffer 通知 ──
334
+
335
+ // navigate_to_page 工具触发,前端 ProjectWorkspace 收到后切到第 N 页
336
+ // (SlideNavigator setActivePage / iframe 内 scrollIntoView section[data-page="N"])
337
+ canvasNavigate: (page) => ({ type: 'run.canvas_navigate', page }),
338
+
339
+ // preview_deck 工具触发:前端把这张 deck 卡"替用户双击一下"
340
+ //(收起态→内嵌渲染;已展开→画布内最大化窗)。path 为 null = 当前会话的 deck
341
+ deckPreview: (filePath) => ({ type: 'run.deck_preview', path: filePath || null }),
342
+
343
+ // highlight 工具触发,前端在 InspectFloatingCard 同层挂 pulse overlay
344
+ canvasHighlight: (selector, durationMs = 1500) => ({
345
+ type: 'run.canvas_highlight', selector, durationMs,
346
+ }),
347
+
348
+ // expose_tweaks 工具完成,前端 TweaksPanel reload spec.tweaks
349
+ tweaksExposed: (count, added, replaced) => ({
350
+ type: 'run.tweaks_exposed', count, added, replaced,
351
+ }),
352
+
353
+ // clear_pending_changes 工具完成,前端可清相应本地 pending hint。
354
+ // clearedIds 让前端 comments state 精确 filter 出橙色 overlay(前后端 id 已统一)
355
+ pendingChangesCleared: (clearedIds, removed, remaining) => ({
356
+ type: 'run.pending_changes_cleared', clearedIds, removed, remaining,
357
+ }),
358
+
359
+ // ── A4.1(2026-05-02):AskUserQuestion 走 SDK canUseTool 路径 ──
360
+ //
361
+ // SDK binary 把 AskUserQuestion 设为 `shouldDefer: true` + `requiresUserInteraction: true`,
362
+ // 工具的 checkPermissions 总返 `behavior: 'ask', message: 'Answer questions?'`,
363
+ // 等 host 程序通过 canUseTool callback 返 `behavior: 'allow', updatedInput: {
364
+ // ...input, answers: {...} }`,binary 拿到带 answers 的 input → 调 tool.call →
365
+ // 工具直接返回 answers 作 result(cli.js:GR6.call 实现)。
366
+ //
367
+ // 我们的 canUseTool(session-loop.js)拦到此事件 → emit run.ask_user_question →
368
+ // 前端 AskUserQuestionView 卡片 → 用户点选项 → POST /answer → resolve
369
+ // canUseTool 的 await,返回 updatedInput。
370
+ //
371
+ // questions 是从工具 input 摘出的 `{ question, header, options, multiSelect }[]`
372
+ // 数组(详见 cli.js:zHK schema),前端按现有卡片字段直接消费。
373
+ askUserQuestion: (toolUseId, questions) => ({
374
+ type: 'run.ask_user_question',
375
+ toolUseId,
376
+ questions,
377
+ }),
378
+
379
+ // ── A1.2(2026-05-02):实时上下文用量 ──
380
+ // SDKControlGetContextUsageResponse(sdk.d.ts:2451-2541)由 query.getContextUsage()
381
+ // 返回。session-loop.js 在每个 assistant message 后 await 一次 emit,前端 ContextUsageBar
382
+ // 接事件渲进度条 + breakdown + autoCompact 阈值预警。
383
+ //
384
+ // 关键字段(为前端做轻量化封装,不全量透传以节省 ws 带宽):
385
+ // totalTokens / maxTokens / percentage — 主进度条数据
386
+ // autoCompactThreshold — 触发 autoCompact 的阈值(绝对 token)
387
+ // isAutoCompactEnabled — 当前开关状态(前端预警条件之一)
388
+ // model — 当前模型 id
389
+ // messageBreakdown.toolCallsByType — 工具消耗 token 排名(展开看)
390
+ // memoryFiles / mcpTools / agents / skills — 各类目消耗(展开看)
391
+ //
392
+ // categories / gridRows 不传 —— 前端不渲网格,只用 percentage + breakdown。
393
+ // ── 图片生成(Phase A,2026-05-06 后段)──
394
+ // generate_image MCP 工具调完后 emit。前端可在 chat / timeline 实时插
395
+ // thumbnail,并把 path 关联回 spec.json 的 record_decision 节点。
396
+ // 字段:
397
+ // path agent cwd 相对路径(assets/generated/<file>.png)
398
+ // absPath 绝对落地路径(前端不一定用,便于审计)
399
+ // sizeBytes png 字节数
400
+ // prompt 生成 prompt 全文
401
+ // assetRole 语义类(hero/cover/bg/.../pattern),可空
402
+ // aspectRatio / imageSize 生成参数
403
+ // referenceImageCount 喂进去的参考图数量
404
+ // accompanyText 模型可选的文字 commentary(responseModalities 含 TEXT 时才有)
405
+ imageGenerated: (info) => ({
406
+ type: 'run.image_generated',
407
+ path: info.path,
408
+ absPath: info.absPath,
409
+ sizeBytes: info.sizeBytes,
410
+ prompt: info.prompt,
411
+ assetRole: info.assetRole || null,
412
+ aspectRatio: info.aspectRatio,
413
+ imageSize: info.imageSize,
414
+ referenceImageCount: info.referenceImageCount || 0,
415
+ ...(info.accompanyText ? { accompanyText: info.accompanyText } : {}),
416
+ }),
417
+
418
+ // appModel: NoDesign 上层真实 model(如 kimi-k2.6)。spoofing 后 SDK 的
419
+ // usage.model 是 alias(如 claude-opus-4-7[1m])不可信;调用方需把真 appModel
420
+ // 传进来。给定 appModel 时按真实容量算 maxTokens / percentage,前端 ContextUsageBar
421
+ // 显的就是 gateway 真正能扛的容量(不是 SDK 内部 compact 触发线)。
422
+ contextUsage: (usage, appModel = null) => {
423
+ // 这里 import 在 file 顶(已加),避免循环依赖问题
424
+ //
425
+ // 分母的兜底链只能有一条(2026-07-30):以前 hooks.js 那个生产者自己写了
426
+ // 一条更长的(?? rawMaxTokens ?? 256000),两条链在 appModel 缺失时给出不同
427
+ // 的分母,同一个会话前后两次百分比对不上。合并到这里,256000 那个 kimi 时代
428
+ // 的常量去掉 —— 拿它当 Claude 会话的分母只会算出个假的高百分比。
429
+ const realMax = (appModel ? resolveModelContextWindow(appModel) : null)
430
+ ?? usage.maxTokens
431
+ ?? usage.rawMaxTokens
432
+ ?? null;
433
+ const totalTokens = usage.totalTokens;
434
+ const percentage = realMax > 0 ? Math.round((totalTokens / realMax) * 100) : (usage.percentage || 0);
435
+ return {
436
+ type: 'run.context_usage',
437
+ totalTokens,
438
+ maxTokens: realMax, // 真实容量(kimi=256k)—— 前端进度条分母
439
+ sdkMaxTokens: usage.maxTokens, // SDK 给的(compact 触发线 230k)—— debug
440
+ percentage,
441
+ autoCompactThreshold: usage.autoCompactThreshold,
442
+ isAutoCompactEnabled: usage.isAutoCompactEnabled,
443
+ model: appModel || usage.model,
444
+ // 出自谁家(BRANDS 之一)——画布精灵据此换身份标。⚠️ 不能让前端读 SDK 的 usage.model 去认牌子:
445
+ // spoofing 之后那是 alias(DeepSeek 行报 claude-opus-4-7[1m]),照它认牌子会把鲸画成星芒
446
+ brand: appModel ? brandOfModel(appModel) : null,
447
+ // 轻量化 breakdown — 前端 hover/expand 能看到细节,不渲细节时不占空间
448
+ messageBreakdown: usage.messageBreakdown ? {
449
+ toolCallTokens: usage.messageBreakdown.toolCallTokens,
450
+ toolResultTokens: usage.messageBreakdown.toolResultTokens,
451
+ attachmentTokens: usage.messageBreakdown.attachmentTokens,
452
+ assistantMessageTokens: usage.messageBreakdown.assistantMessageTokens,
453
+ userMessageTokens: usage.messageBreakdown.userMessageTokens,
454
+ // toolCallsByType 是排行(用于"哪个工具吃 token 最多"),保留 top 10
455
+ toolCallsByType: (usage.messageBreakdown.toolCallsByType || [])
456
+ .slice(0, 10),
457
+ } : null,
458
+ // 类目级别 token 数(聚合,不传完整数组)
459
+ memoryFilesTokens: (usage.memoryFiles || []).reduce((s, m) => s + (m.tokens || 0), 0),
460
+ mcpToolsTokens: (usage.mcpTools || []).reduce((s, t) => s + (t.tokens || 0), 0),
461
+ agentsTokens: (usage.agents || []).reduce((s, a) => s + (a.tokens || 0), 0),
462
+ };
463
+ },
464
+ };