@xiaobuyu/nodesign 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +42 -22
  9. package/server/api/assets/notes.js +194 -144
  10. package/server/api/assets.js +891 -896
  11. package/server/api/board.js +134 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +465 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1035 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +351 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +152 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/edit-sketch.js +167 -0
  120. package/server/engine/mcp/tools/explain-style.js +237 -237
  121. package/server/engine/mcp/tools/export-handoff.js +133 -133
  122. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  123. package/server/engine/mcp/tools/generate-image.js +842 -842
  124. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  125. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  126. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  127. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  128. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  129. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  130. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  131. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  132. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  133. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  134. package/server/engine/mcp/tools/highlight.js +68 -68
  135. package/server/engine/mcp/tools/list-pages.js +222 -222
  136. package/server/engine/mcp/tools/look-at-board.js +124 -0
  137. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  138. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  139. package/server/engine/mcp/tools/organize-board.js +64 -64
  140. package/server/engine/mcp/tools/paint-still.js +434 -434
  141. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  142. package/server/engine/mcp/tools/preview-deck.js +130 -130
  143. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  144. package/server/engine/mcp/tools/publish-site.js +102 -102
  145. package/server/engine/mcp/tools/query-elements.js +195 -195
  146. package/server/engine/mcp/tools/read-board.js +190 -103
  147. package/server/engine/mcp/tools/read-document.js +93 -93
  148. package/server/engine/mcp/tools/read-page.js +264 -264
  149. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  150. package/server/engine/mcp/tools/read-user-view.js +59 -0
  151. package/server/engine/mcp/tools/record-decision.js +140 -140
  152. package/server/engine/mcp/tools/relate-on-board.js +112 -110
  153. package/server/engine/mcp/tools/report-issue.js +115 -115
  154. package/server/engine/mcp/tools/roll-film.js +266 -266
  155. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  156. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  157. package/server/engine/mcp/tools/screenshot.js +583 -583
  158. package/server/engine/mcp/tools/sketch-on-board.js +337 -0
  159. package/server/engine/mcp/tools/tier-gate.js +96 -96
  160. package/server/engine/mcp/tools/trace-motion.js +215 -215
  161. package/server/engine/mcp/tools/web-search.js +548 -548
  162. package/server/engine/mcp/tools/write-on-board.js +130 -0
  163. package/server/engine/motion/inventory.js +349 -349
  164. package/server/engine/perception/session.js +235 -235
  165. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  174. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
  175. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  176. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  177. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
  178. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
  179. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  180. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
  181. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  182. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  183. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  185. package/server/engine/runs/active-runs.js +698 -698
  186. package/server/engine/runs/board-tasklist.js +130 -0
  187. package/server/engine/runs/live-turn.js +252 -252
  188. package/server/engine/runs/store.js +364 -364
  189. package/server/engine/runs/turn-relay.js +217 -217
  190. package/server/engine/runtime/workspace.js +164 -164
  191. package/server/index.js +3 -0
  192. package/server/lib/artifact-file-path.js +80 -80
  193. package/server/lib/artifact-target.js +284 -284
  194. package/server/lib/asset-refs.js +239 -239
  195. package/server/lib/async-queue.js +104 -104
  196. package/server/lib/auto-relations.js +130 -130
  197. package/server/lib/binding-types.js +72 -60
  198. package/server/lib/board-graph-export.js +191 -0
  199. package/server/lib/board-groups.js +167 -0
  200. package/server/lib/board-hero.js +76 -0
  201. package/server/lib/board-kind-sizes.js +85 -62
  202. package/server/lib/board-relations.js +133 -133
  203. package/server/lib/browse-proxy.js +180 -180
  204. package/server/lib/canvas-id.js +36 -36
  205. package/server/lib/chalk.js +121 -0
  206. package/server/lib/cover.js +227 -227
  207. package/server/lib/danbooru-tags.js +291 -291
  208. package/server/lib/doc-extract.js +156 -156
  209. package/server/lib/docx/build-from-source.js +260 -260
  210. package/server/lib/docx/build.js +490 -490
  211. package/server/lib/docx/dump-styles.js +287 -287
  212. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  213. package/server/lib/docx/merge-runs.js +129 -129
  214. package/server/lib/docx/numbering.js +218 -218
  215. package/server/lib/docx/order.js +152 -152
  216. package/server/lib/docx/rawzip.js +172 -172
  217. package/server/lib/docx/render.js +92 -92
  218. package/server/lib/docx/text-lint.js +225 -225
  219. package/server/lib/docx/tokens.js +335 -335
  220. package/server/lib/docx/units.js +29 -29
  221. package/server/lib/docx/xml.js +291 -291
  222. package/server/lib/docx-pages.js +187 -187
  223. package/server/lib/export-collect.js +289 -289
  224. package/server/lib/export-package.js +210 -210
  225. package/server/lib/html-srcset.js +145 -145
  226. package/server/lib/image-variant.js +460 -460
  227. package/server/lib/ingress/forward-openai-chat.js +305 -305
  228. package/server/lib/ingress/openai-chat.js +491 -491
  229. package/server/lib/ingress/session-notice.js +66 -66
  230. package/server/lib/ingress/session-routes.js +68 -68
  231. package/server/lib/ingress/slot-probe.js +130 -130
  232. package/server/lib/ingress/upstream-billing.js +52 -52
  233. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  234. package/server/lib/ingress/upstream-truncation.js +48 -48
  235. package/server/lib/issues-store.js +182 -182
  236. package/server/lib/kinds/deck.js +72 -72
  237. package/server/lib/kinds/docx.js +283 -283
  238. package/server/lib/kinds/file-kinds.js +113 -113
  239. package/server/lib/kinds/index.js +202 -202
  240. package/server/lib/kinds/site.js +161 -161
  241. package/server/lib/model-ingress.js +543 -543
  242. package/server/lib/moderation.js +255 -255
  243. package/server/lib/notice-store.js +103 -103
  244. package/server/lib/plugin-install.js +104 -104
  245. package/server/lib/plugin-validator.js +721 -721
  246. package/server/lib/publish-store.js +115 -115
  247. package/server/lib/quick-summary.js +52 -52
  248. package/server/lib/quota.js +270 -270
  249. package/server/lib/rate-window.js +29 -29
  250. package/server/lib/reference-assets.js +92 -92
  251. package/server/lib/region-shot.js +135 -135
  252. package/server/lib/safe-path.js +73 -73
  253. package/server/lib/sdk-session.js +33 -33
  254. package/server/lib/showcase-store.js +92 -92
  255. package/server/lib/site-publish.js +465 -465
  256. package/server/lib/sketch-layout.js +277 -0
  257. package/server/lib/ssrf-guard.js +432 -432
  258. package/server/lib/task-scan.js +158 -158
  259. package/server/lib/tavern-json.js +154 -154
  260. package/server/lib/video-variant.js +237 -237
  261. package/server/lib/workspace-path.js +33 -33
  262. package/server/ops/fix-sdk-musl.mjs +40 -40
  263. package/server/ops/install-macos-fonts.sh +114 -114
  264. package/server/ops/macos-fonts.conf +336 -336
  265. package/server/ops/sandbox-shim/bwrap +57 -57
  266. package/server/projects/assets-summary.js +119 -119
  267. package/server/projects/auto-name.js +53 -53
  268. package/server/projects/board-limits.js +12 -0
  269. package/server/projects/board-sanitize.js +252 -0
  270. package/server/projects/board-store.js +507 -640
  271. package/server/projects/move-entry.js +103 -103
  272. package/server/projects/store.js +270 -270
  273. package/server/projects/ui-config.js +60 -54
  274. package/server/projects/viewpoint-store.js +78 -0
  275. package/server/projects/workspace-templates.js +53 -53
  276. package/server/projects/workspace.js +1025 -1025
  277. package/server/runtime/local-config.js +177 -177
  278. package/server/runtime/profile.js +72 -72
  279. package/server/services/rembg-service.py +334 -334
  280. package/server/shared/README.md +25 -25
  281. package/server/shared/deck.js +54 -54
  282. package/server/shared/time.js +49 -49
  283. package/server/style-pipeline/README.md +53 -53
  284. package/server/ws/broker.js +39 -36
  285. package/server/ws/browse-channel.js +177 -177
  286. package/server/ws/index.js +386 -386
  287. package/web/README.md +89 -89
  288. package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
  289. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
  290. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
  291. package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
  292. package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
  293. package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
  294. package/web/dist/assets/arc-D_khH_xN.js +1 -0
  295. package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
  296. package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
  297. package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
  298. package/web/dist/assets/channel-DGkccMO1.js +1 -0
  299. package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
  300. package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
  301. package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
  302. package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
  303. package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
  304. package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
  305. package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
  306. package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
  307. package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
  308. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
  309. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
  310. package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
  311. package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
  312. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
  313. package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
  314. package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
  315. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  316. package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
  317. package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
  318. package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
  319. package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
  320. package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
  321. package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
  322. package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
  323. package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
  324. package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
  325. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
  326. package/web/dist/assets/index-GAamh1p4.js +2152 -0
  327. package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
  328. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  329. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
  330. package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
  331. package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
  332. package/web/dist/assets/layout-CnXiF_NB.js +1 -0
  333. package/web/dist/assets/linear-BMW0u6G6.js +1 -0
  334. package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
  335. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  336. package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
  337. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
  338. package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
  339. package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
  340. package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
  341. package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
  342. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
  343. package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
  344. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
  345. package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
  346. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
  347. package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
  348. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
  349. package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
  350. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
  351. package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
  352. package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
  353. package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
  354. package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
  355. package/web/dist/index.html +17 -17
  356. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
  357. package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
  358. package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
@@ -1,418 +1,418 @@
1
- /**
2
- * AgentContext — 一次 run 的运行时上下文(SDK 包装版)
3
- *
4
- * 形态变更(2026-04-29 战略转向):
5
- * - 之前:自写 agent-loop + tool-registry,ctx 含 todoState / forTool() 给 tool execute 用
6
- * - 现在:包 Claude Agent SDK 的 query(),工具由 SDK 内置 + 子进程跑,ctx 不再管 tool 执行
7
- *
8
- * 职责:
9
- * - 串起 runId / skillId / EventBus / AbortController
10
- * - 提供 workspace.* 包装供 skill loader / 业务逻辑读取沙盒
11
- * - 暴露 emit() 把业务事件推到 EventBus(给 WS 桥接)
12
- * - 跟踪 SDK 返回的 sessionId(从首条 message 提取,存 metadata)
13
- * - 维护 counters(rounds / tokens / cost / errors)落 run.metadata
14
- *
15
- * 不再管:
16
- * - todoState(SDK TodoWrite 工具自管)
17
- * - forTool()(SDK 自己管 tool execute 上下文)
18
- */
19
-
20
- import { promises as fs } from 'fs';
21
- import path from 'path';
22
- import { ensureWorkspace, getWorkspaceRoot, readFile, writeFile, listDir, exists, safeResolve } from '../runtime/workspace.js';
23
- import { EventBus } from './events.js';
24
- import { repriceUsageDeltas } from './model-context.js';
25
-
26
- /**
27
- * 一个回合的计数器初值。**唯一形状真相**:AgentContext 构造函数与 session-loop 的
28
- * startTurn(每个回合重置一次)都用它。
29
- *
30
- * ⛔ 08-21 夜血案:startTurn 里原来抄了一份字面量,08-21 深夜给构造函数加 `toolCharges`
31
- * 时没同步过去 → startTurn 必定跑在任何工具之前 → `addToolCharge` 读 undefined 直接
32
- * TypeError:generate_image 的 $0.20 一分没记,而且**成功出的图还会变成一次工具报错**。
33
- * 收成一份就没有下一次。
34
- */
35
- export function freshTurnCounters() {
36
- return {
37
- turns: 0, // SDK num_turns
38
- toolCalls: 0,
39
- toolFailures: 0,
40
- compactBoundaries: 0,
41
- apiRetries: 0,
42
- durationMs: 0,
43
- durationApiMs: 0,
44
- totalCostUsd: 0,
45
- inputTokens: 0,
46
- outputTokens: 0,
47
- cacheReadTokens: 0,
48
- cacheCreateTokens: 0,
49
- modelUsage: null, // 分模型本 turn 增量(absorbResult 差分产物)
50
- toolCharges: {}, // 按件计价的工具花费(generate_image $0.20/张),absorbResult 末尾并进 modelUsage
51
- };
52
- }
53
-
54
- export class AgentContext {
55
- /**
56
- * @param {object} opts
57
- * @param {string} opts.runId
58
- * @param {string} opts.skillId
59
- * @param {EventBus} [opts.eventBus]
60
- * @param {AbortController} [opts.abortController]
61
- * @param {object} [opts.metadata={}]
62
- * @param {string} [opts.workspaceRoot] - 外部 workspace(如 per-project 目录);
63
- * 传了就走它,否则用 runId 推路径(旧 smoke 兼容)
64
- * @param {string} [opts.sessionId] - 当前 SDK session id;emit 时自动 enrich 到事件,
65
- * 让 WS 端按 sid 过滤防跨 session 串扰
66
- * @param {string} [opts.appModel] - NoDesign 上层真实 model(如 kimi-k2.6)。
67
- * 区别于 sdkOptions.model(spoofing alias,让 SDK
68
- * 内部 rawMaxTokens 算对)。hooks/events 算真实
69
- * context window 时按 appModel 查映射表。
70
- */
71
- constructor({ runId, skillId, eventBus, abortController, metadata = {}, workspaceRoot = null, sessionId = null, appModel = null }) {
72
- if (!runId) throw new Error('AgentContext: runId required');
73
- if (!skillId) throw new Error('AgentContext: skillId required');
74
-
75
- this.runId = runId;
76
- this.skillId = skillId;
77
- this.sessionId = sessionId;
78
- this.appModel = appModel;
79
- this.eventBus = eventBus || new EventBus();
80
- this.abortController = abortController || new AbortController();
81
- this.metadata = metadata;
82
- this._externalWorkspaceRoot = workspaceRoot;
83
-
84
- // SDK 在 message 流里返回 session_id,首次见到时记下
85
- this.sdkSessionId = null;
86
-
87
- // result.modelUsage 的差分基准(2026-07-31)。modelUsage 是**会话累计值**
88
- // (探针实测:turn2 的值 = turn1 + turn2),要拿到本 turn 增量必须对上一条
89
- // result 做差。基准跟 ctx 同生命周期 = 跟 SDK query stream 同生命周期
90
- // (runSession 每次 new AgentContext + new stream),stream 重启累计值归零、
91
- // 基准也归零,差分天然对齐。**不随 turn 边界重置**。
92
- this._modelUsageBase = null;
93
-
94
- // 可观测计数器(落 run.metadata 用)—— 形状只有 freshTurnCounters 一份
95
- this.counters = freshTurnCounters();
96
-
97
- this.startedAt = Date.now();
98
- }
99
-
100
- // ── 取消 ──
101
-
102
- get signal() {
103
- return this.abortController.signal;
104
- }
105
-
106
- /**
107
- * 取消 run。Phase 3c 加幂等保证 —— cancel 多次只触发一次 abort + 一次
108
- * run.cancelled emit。两条调用路径都依赖此幂等:
109
- * 1. cancelRun(active-runs.js)race window / interrupt 兜底时直接调
110
- * 2. session-loop.js result 处理识别 terminal_reason: 'aborted_*' 时调(interrupt 路径)
111
- *
112
- * 没有这个 flag,interrupt 路径 + abort 兜底会双 emit run.cancelled,
113
- * 前端 toast '已取消' 弹两次。
114
- */
115
- cancel(reason = 'user_cancel') {
116
- if (this._cancelled) return;
117
- this._cancelled = true;
118
- this.abortController.abort(reason);
119
- this.emit({ type: 'run.cancelled', reason });
120
- }
121
-
122
- ensureNotAborted() {
123
- if (this.signal.aborted) {
124
- const err = new Error(`Run ${this.runId} aborted: ${this.signal.reason || 'unknown'}`);
125
- err.code = 'AGENT_ABORTED';
126
- throw err;
127
- }
128
- }
129
-
130
- // ── 事件 ──
131
-
132
- emit(event) {
133
- if (!event || !event.type) throw new Error('emit: event.type required');
134
- // sessionId 自动 enrich 让 WS 端按 sid 过滤事件,防多 session / 多 tab 跨 session
135
- // 串扰(同 project bus 共享)。`...event` 在后保证调用方显式传的字段不被覆盖。
136
- const enriched = {
137
- runId: this.runId,
138
- sessionId: this.sessionId,
139
- ts: new Date().toISOString(),
140
- ...event,
141
- };
142
- this.eventBus.publish(enriched);
143
- return enriched;
144
- }
145
-
146
- // ── workspace 包装 ──
147
- // 外部 workspaceRoot 模式(P0 per-project 目录):直接走绝对路径,
148
- // 由调用方负责 mkdir + git init(见 projects/workspace.js)。
149
- // 旧 runId 模式(保留给 smoke 测试 / 单 run 沙盒):走 runtime/workspace.js 那套。
150
-
151
- workspace = {
152
- ensure: async () => {
153
- if (this._externalWorkspaceRoot) {
154
- await fs.mkdir(this._externalWorkspaceRoot, { recursive: true });
155
- return this._externalWorkspaceRoot;
156
- }
157
- return ensureWorkspace(this.runId);
158
- },
159
- root: () => this._externalWorkspaceRoot || getWorkspaceRoot(this.runId),
160
- exists: async (rel) => {
161
- if (this._externalWorkspaceRoot) {
162
- try {
163
- await fs.access(path.resolve(this._externalWorkspaceRoot, rel));
164
- return true;
165
- } catch {
166
- return false;
167
- }
168
- }
169
- return exists(this.runId, rel);
170
- },
171
- read: (rel) => readFile(this.runId, rel),
172
- write: (rel, content) => writeFile(this.runId, rel, content),
173
- list: (rel) => listDir(this.runId, rel),
174
- resolve: (rel) => safeResolve(this.runId, rel),
175
- };
176
-
177
- // ── 跟踪 SDK 数据 ──
178
-
179
- /** SDK 第一条 message 带 session_id,记下供 metadata / debug 用 */
180
- recordSdkSession(sessionId) {
181
- if (!this.sdkSessionId && sessionId) {
182
- this.sdkSessionId = sessionId;
183
- this.emit({ type: 'run.sdk.session', sessionId });
184
- }
185
- }
186
-
187
- /**
188
- * SDK SDKResultMessage 含全套统计;一次性吸收。
189
- *
190
- * 计量口径(2026-07-31 起):权威数据源是 result.modelUsage —— SDK 的分模型
191
- * 总账,含子代理消耗,中断的 turn 也不丢(累计值持续增长,下一条 result 的
192
- * 差分会捞回来)。差分出的本 turn 增量:
193
- * - 按模型汇总进 counters.inputTokens 等主字段(配额真列的来源)
194
- * - 分模型明细留在 counters.modelUsage,finishTurn 落 run_model_usage 表
195
- * - totalCostUsd 同样取差分(result.total_cost_usd 是会话累计值,以前
196
- * 原样落库导致 cost 列不能 sum —— 那是虚价,但至少语义要对)
197
- * result.usage 只做 modelUsage 缺失时的兜底(usage 是本轮真增量,探针实测)。
198
- */
199
- absorbResult(result) {
200
- if (!result) return;
201
- this.counters.turns = result.num_turns ?? this.counters.turns;
202
- this.counters.durationMs = result.duration_ms ?? this.counters.durationMs;
203
- this.counters.durationApiMs = result.duration_api_ms ?? this.counters.durationApiMs;
204
-
205
- // reprice:仅 API 会话 —— key 从 SDK spoof alias 还原成 appModel、按表价
206
- // 重算 costUsd(SDK 按 alias 的 Claude 价目表算,Kimi 时代虚高 30×)。
207
- // 订阅会话原样过(alias 与真订阅模型名同形,必须以会话通路为准,见
208
- // model-context.js repriceUsageDeltas 注释)。
209
- const deltas = repriceUsageDeltas(this._diffModelUsage(result.modelUsage), this.appModel);
210
- if (deltas) {
211
- let inp = 0; let out = 0; let cr = 0; let cc = 0; let cost = 0;
212
- for (const d of Object.values(deltas)) {
213
- inp += d.inputTokens; out += d.outputTokens;
214
- cr += d.cacheReadTokens; cc += d.cacheCreateTokens;
215
- cost += d.costUsd;
216
- }
217
- this.counters.inputTokens = inp;
218
- this.counters.outputTokens = out;
219
- this.counters.cacheReadTokens = cr;
220
- this.counters.cacheCreateTokens = cc;
221
- this.counters.totalCostUsd = cost;
222
- this.counters.modelUsage = deltas;
223
- } else if (result.usage) {
224
- this.counters.inputTokens = result.usage.input_tokens || 0;
225
- this.counters.outputTokens = result.usage.output_tokens || 0;
226
- this.counters.cacheReadTokens = result.usage.cache_read_input_tokens || 0;
227
- this.counters.cacheCreateTokens = result.usage.cache_creation_input_tokens || 0;
228
- this.counters.totalCostUsd = result.total_cost_usd ?? this.counters.totalCostUsd;
229
- }
230
- this._foldToolCharges();
231
- }
232
-
233
- /**
234
- * 半截续接的账要**结转**(08-21 夜,fable 评审 P0)。
235
- *
236
- * 病:absorbResult 对 counters 是**赋值不是累加**(差分基准 _modelUsageBase 在第一个
237
- * result 就前移了),而半截续接会让同一个 run 出现第二个 result → 第一轮的整份账
238
- * (token、表价 cost、已经折进去的 generate_image 按件费、上游自报的真 cost)被第二轮
239
- * 的增量整个覆盖 → 那一轮等于白跑,日限闸门(quota.usedCostToday 读 run_model_usage)少收。
240
- * 真路径实测:续接的回合上游真打 2 发、落库只有 1 发的量。
241
- *
242
- * 用法:推续接消息**之前** takeCarry() 存一份,下一个 result 的 absorbResult /
243
- * applyUpstreamBilling 跑完后 addCarry(carry) 加回去。多次续接会一路滚下去。
244
- *
245
- * ⚠️ 只加"可累加"的量:token / cost / 时长 / 工具计数。`turns` 不加 —— 它是 SDK 的
246
- * num_turns,本身带累计语义,加了会双数。
247
- */
248
- takeCarry() {
249
- const c = this.counters;
250
- return {
251
- inputTokens: c.inputTokens || 0,
252
- outputTokens: c.outputTokens || 0,
253
- cacheReadTokens: c.cacheReadTokens || 0,
254
- cacheCreateTokens: c.cacheCreateTokens || 0,
255
- totalCostUsd: c.totalCostUsd || 0,
256
- durationMs: c.durationMs || 0,
257
- durationApiMs: c.durationApiMs || 0,
258
- toolCalls: c.toolCalls || 0,
259
- toolFailures: c.toolFailures || 0,
260
- compactBoundaries: c.compactBoundaries || 0,
261
- apiRetries: c.apiRetries || 0,
262
- modelUsage: c.modelUsage ? JSON.parse(JSON.stringify(c.modelUsage)) : null,
263
- };
264
- }
265
-
266
- addCarry(carry) {
267
- if (!carry) return;
268
- const c = this.counters;
269
- for (const k of ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheCreateTokens', 'totalCostUsd', 'durationMs', 'durationApiMs']) {
270
- c[k] = (c[k] || 0) + (carry[k] || 0);
271
- }
272
- // toolCalls 等计数器**不是**每回合重置后单调累加的差分量,而是 handleSDKMessage 一路 ++ 的,
273
- // 续接期间它们没被重置(同一个 run 同一个 ctx)→ 已经含了第一轮,别再加一遍。
274
- if (carry.modelUsage) {
275
- const usage = c.modelUsage && typeof c.modelUsage === 'object' ? c.modelUsage : {};
276
- for (const [name, d] of Object.entries(carry.modelUsage)) {
277
- const prev = usage[name] || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, costUsd: 0 };
278
- usage[name] = {
279
- ...prev,
280
- inputTokens: (prev.inputTokens || 0) + (d.inputTokens || 0),
281
- outputTokens: (prev.outputTokens || 0) + (d.outputTokens || 0),
282
- cacheReadTokens: (prev.cacheReadTokens || 0) + (d.cacheReadTokens || 0),
283
- cacheCreateTokens: (prev.cacheCreateTokens || 0) + (d.cacheCreateTokens || 0),
284
- costUsd: (prev.costUsd || 0) + (d.costUsd || 0),
285
- };
286
- }
287
- c.modelUsage = usage;
288
- }
289
- }
290
-
291
- /** 按件计价的工具花费(generate_image 每张 $0.20):工具成功后记一笔,结账时并进 modelUsage 同一本账 */
292
- addToolCharge(name, usd) {
293
- const v = Number(usd);
294
- if (!name || !Number.isFinite(v) || v <= 0) return;
295
- this.counters.toolCharges[name] = (this.counters.toolCharges[name] || 0) + v;
296
- }
297
- _foldToolCharges() {
298
- const charges = this.counters.toolCharges || {};
299
- const names = Object.keys(charges);
300
- if (!names.length) return;
301
- const usage = this.counters.modelUsage && typeof this.counters.modelUsage === 'object' ? this.counters.modelUsage : {};
302
- for (const name of names) {
303
- const prev = usage[name] || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, costUsd: 0 };
304
- usage[name] = { ...prev, costUsd: (prev.costUsd || 0) + charges[name] };
305
- }
306
- this.counters.modelUsage = usage;
307
- this.counters.totalCostUsd = Object.values(usage).reduce((s, d) => s + (d.costUsd || 0), 0);
308
- this.counters.toolCharges = {};
309
- }
310
-
311
- /**
312
- * 上游自报的费用覆盖(08-21 晚,lib/ingress/upstream-billing.js)。
313
- * billing = { appModel → { costUsd, responses, promptTokens, completionTokens, cachedTokens } },
314
- * 是 ingress 在本轮累加、session-loop 结账前取走的。规则:
315
- * - 上游报的 cost **> 0**(真扣了余额)→ 覆盖 modelUsage[appModel].costUsd;token 数仍信 SDK 差分(口径不同)
316
- * - 上游报 0 / 没报 → 保留表价算出来的数。OpenCode Go 订阅行(08-21 深夜实测)对订阅额度内的请求报 cost=0,
317
- * 但它消耗的是全站共享的 Go 池子($12/5h),按表价记才能让每用户日限跟着受控;免费行表价本就是 0
318
- * - SDK 没有该模型条目(CLI 失败 / 没覆盖到的 helper)→ 用上游 token 数补一条,按表价算(repriceUsageDeltas)
319
- * 覆盖后重算 totalCostUsd。counters.costSource:'upstream'(上游真扣费覆盖过)| 'table'(只按表价)
320
- */
321
- applyUpstreamBilling(billing) {
322
- if (!billing || typeof billing !== 'object') return;
323
- let touched = false; let overrode = false;
324
- const usage = this.counters.modelUsage && typeof this.counters.modelUsage === 'object' ? this.counters.modelUsage : {};
325
- for (const [appModel, acc] of Object.entries(billing)) {
326
- if (!acc || !acc.responses) continue;
327
- const reported = acc.costUsd != null && Number(acc.costUsd) > 0 ? Number(acc.costUsd) : null;
328
- const prev = usage[appModel];
329
- if (prev) {
330
- if (reported != null) { usage[appModel] = { ...prev, costUsd: reported }; overrode = true; touched = true; }
331
- continue;
332
- }
333
- // SDK 没这条(CLI 失败那类):用上游 token 数补一条,先按表价,再看上游有没有真扣费
334
- const cached = acc.cachedTokens || 0;
335
- const raw = { [appModel]: { inputTokens: Math.max(0, (acc.promptTokens || 0) - cached), outputTokens: acc.completionTokens || 0, cacheReadTokens: cached, cacheCreateTokens: 0, costUsd: 0 } };
336
- const priced = repriceUsageDeltas(raw, this.appModel)?.[appModel] || raw[appModel];
337
- usage[appModel] = reported != null ? { ...priced, costUsd: reported } : priced;
338
- if (reported != null) overrode = true;
339
- touched = true;
340
- }
341
- if (!touched) return;
342
- this.counters.modelUsage = usage;
343
- this.counters.totalCostUsd = Object.values(usage).reduce((s, d) => s + (d.costUsd || 0), 0);
344
- this.counters.costSource = overrode ? 'upstream' : 'table';
345
- }
346
-
347
- /**
348
- * 会话累计的 modelUsage → 本 turn 增量。返回 null 表示"没有可用的 modelUsage"
349
- * (调用方退回 usage 兜底);返回 {}(空对象)表示"有 modelUsage 但本 turn
350
- * 没有任何新消耗",此时主字段照常清零是正确语义。
351
- * Math.max(0, ...) 防浮点 / 上游异常出负数;基准无论如何都更新成最新快照。
352
- */
353
- _diffModelUsage(modelUsage) {
354
- if (!modelUsage || typeof modelUsage !== 'object') return null;
355
- const models = Object.keys(modelUsage);
356
- if (models.length === 0) return null;
357
- const base = this._modelUsageBase || {};
358
- const deltas = {};
359
- const snapshot = {};
360
- for (const model of models) {
361
- const u = modelUsage[model] || {};
362
- const b = base[model] || {};
363
- snapshot[model] = {
364
- inputTokens: u.inputTokens || 0,
365
- outputTokens: u.outputTokens || 0,
366
- cacheReadInputTokens: u.cacheReadInputTokens || 0,
367
- cacheCreationInputTokens: u.cacheCreationInputTokens || 0,
368
- costUSD: u.costUSD || 0,
369
- };
370
- const d = {
371
- inputTokens: Math.max(0, snapshot[model].inputTokens - (b.inputTokens || 0)),
372
- outputTokens: Math.max(0, snapshot[model].outputTokens - (b.outputTokens || 0)),
373
- cacheReadTokens: Math.max(0, snapshot[model].cacheReadInputTokens - (b.cacheReadInputTokens || 0)),
374
- cacheCreateTokens: Math.max(0, snapshot[model].cacheCreationInputTokens - (b.cacheCreationInputTokens || 0)),
375
- costUsd: Math.max(0, snapshot[model].costUSD - (b.costUSD || 0)),
376
- };
377
- if (d.inputTokens || d.outputTokens || d.cacheReadTokens || d.cacheCreateTokens || d.costUsd) {
378
- deltas[model] = d;
379
- }
380
- }
381
- // 基准里已有但本次 modelUsage 没出现的模型:保留旧值(SDK 不应该丢 key,
382
- // 防御性合并,免得它真丢时差分把历史消耗又算一遍)
383
- this._modelUsageBase = { ...base, ...snapshot };
384
- return deltas;
385
- }
386
-
387
- incrementTool(failed = false) {
388
- this.counters.toolCalls += 1;
389
- if (failed) this.counters.toolFailures += 1;
390
- }
391
-
392
- /**
393
- * 子代理收尾用量(SDK task_notification.usage)累积(2026-07-30)。
394
- * **单独记账,不加进主 inputTokens/outputTokens** —— 主字段现在来自
395
- * modelUsage 差分(SDK 的分模型总账,已含 sidechain),相加必然双重计数。
396
- * 这两个字段只进 metadata 做可观测("这轮子代理烧了多少"),也是交叉验证
397
- * 钩子:某轮 subagent 用量明显大于 modelUsage 差分总和 = 总账没含 sidechain
398
- * 的信号(目前判断是含的,出现反例再查)。
399
- */
400
- absorbSubagentUsage(usage) {
401
- if (!usage || typeof usage !== 'object') return;
402
- this.counters.subagentInputTokens = (this.counters.subagentInputTokens || 0) + (usage.input_tokens || 0);
403
- this.counters.subagentOutputTokens = (this.counters.subagentOutputTokens || 0) + (usage.output_tokens || 0);
404
- }
405
-
406
- // ── observability ──
407
-
408
- snapshot() {
409
- return {
410
- runId: this.runId,
411
- skillId: this.skillId,
412
- sdkSessionId: this.sdkSessionId,
413
- counters: { ...this.counters },
414
- elapsedMs: Date.now() - this.startedAt,
415
- aborted: this.signal.aborted,
416
- };
417
- }
418
- }
1
+ /**
2
+ * AgentContext — 一次 run 的运行时上下文(SDK 包装版)
3
+ *
4
+ * 形态变更(2026-04-29 战略转向):
5
+ * - 之前:自写 agent-loop + tool-registry,ctx 含 todoState / forTool() 给 tool execute 用
6
+ * - 现在:包 Claude Agent SDK 的 query(),工具由 SDK 内置 + 子进程跑,ctx 不再管 tool 执行
7
+ *
8
+ * 职责:
9
+ * - 串起 runId / skillId / EventBus / AbortController
10
+ * - 提供 workspace.* 包装供 skill loader / 业务逻辑读取沙盒
11
+ * - 暴露 emit() 把业务事件推到 EventBus(给 WS 桥接)
12
+ * - 跟踪 SDK 返回的 sessionId(从首条 message 提取,存 metadata)
13
+ * - 维护 counters(rounds / tokens / cost / errors)落 run.metadata
14
+ *
15
+ * 不再管:
16
+ * - todoState(SDK TodoWrite 工具自管)
17
+ * - forTool()(SDK 自己管 tool execute 上下文)
18
+ */
19
+
20
+ import { promises as fs } from 'fs';
21
+ import path from 'path';
22
+ import { ensureWorkspace, getWorkspaceRoot, readFile, writeFile, listDir, exists, safeResolve } from '../runtime/workspace.js';
23
+ import { EventBus } from './events.js';
24
+ import { repriceUsageDeltas } from './model-context.js';
25
+
26
+ /**
27
+ * 一个回合的计数器初值。**唯一形状真相**:AgentContext 构造函数与 session-loop 的
28
+ * startTurn(每个回合重置一次)都用它。
29
+ *
30
+ * ⛔ 08-21 夜血案:startTurn 里原来抄了一份字面量,08-21 深夜给构造函数加 `toolCharges`
31
+ * 时没同步过去 → startTurn 必定跑在任何工具之前 → `addToolCharge` 读 undefined 直接
32
+ * TypeError:generate_image 的 $0.20 一分没记,而且**成功出的图还会变成一次工具报错**。
33
+ * 收成一份就没有下一次。
34
+ */
35
+ export function freshTurnCounters() {
36
+ return {
37
+ turns: 0, // SDK num_turns
38
+ toolCalls: 0,
39
+ toolFailures: 0,
40
+ compactBoundaries: 0,
41
+ apiRetries: 0,
42
+ durationMs: 0,
43
+ durationApiMs: 0,
44
+ totalCostUsd: 0,
45
+ inputTokens: 0,
46
+ outputTokens: 0,
47
+ cacheReadTokens: 0,
48
+ cacheCreateTokens: 0,
49
+ modelUsage: null, // 分模型本 turn 增量(absorbResult 差分产物)
50
+ toolCharges: {}, // 按件计价的工具花费(generate_image $0.20/张),absorbResult 末尾并进 modelUsage
51
+ };
52
+ }
53
+
54
+ export class AgentContext {
55
+ /**
56
+ * @param {object} opts
57
+ * @param {string} opts.runId
58
+ * @param {string} opts.skillId
59
+ * @param {EventBus} [opts.eventBus]
60
+ * @param {AbortController} [opts.abortController]
61
+ * @param {object} [opts.metadata={}]
62
+ * @param {string} [opts.workspaceRoot] - 外部 workspace(如 per-project 目录);
63
+ * 传了就走它,否则用 runId 推路径(旧 smoke 兼容)
64
+ * @param {string} [opts.sessionId] - 当前 SDK session id;emit 时自动 enrich 到事件,
65
+ * 让 WS 端按 sid 过滤防跨 session 串扰
66
+ * @param {string} [opts.appModel] - NoDesign 上层真实 model(如 kimi-k2.6)。
67
+ * 区别于 sdkOptions.model(spoofing alias,让 SDK
68
+ * 内部 rawMaxTokens 算对)。hooks/events 算真实
69
+ * context window 时按 appModel 查映射表。
70
+ */
71
+ constructor({ runId, skillId, eventBus, abortController, metadata = {}, workspaceRoot = null, sessionId = null, appModel = null }) {
72
+ if (!runId) throw new Error('AgentContext: runId required');
73
+ if (!skillId) throw new Error('AgentContext: skillId required');
74
+
75
+ this.runId = runId;
76
+ this.skillId = skillId;
77
+ this.sessionId = sessionId;
78
+ this.appModel = appModel;
79
+ this.eventBus = eventBus || new EventBus();
80
+ this.abortController = abortController || new AbortController();
81
+ this.metadata = metadata;
82
+ this._externalWorkspaceRoot = workspaceRoot;
83
+
84
+ // SDK 在 message 流里返回 session_id,首次见到时记下
85
+ this.sdkSessionId = null;
86
+
87
+ // result.modelUsage 的差分基准(2026-07-31)。modelUsage 是**会话累计值**
88
+ // (探针实测:turn2 的值 = turn1 + turn2),要拿到本 turn 增量必须对上一条
89
+ // result 做差。基准跟 ctx 同生命周期 = 跟 SDK query stream 同生命周期
90
+ // (runSession 每次 new AgentContext + new stream),stream 重启累计值归零、
91
+ // 基准也归零,差分天然对齐。**不随 turn 边界重置**。
92
+ this._modelUsageBase = null;
93
+
94
+ // 可观测计数器(落 run.metadata 用)—— 形状只有 freshTurnCounters 一份
95
+ this.counters = freshTurnCounters();
96
+
97
+ this.startedAt = Date.now();
98
+ }
99
+
100
+ // ── 取消 ──
101
+
102
+ get signal() {
103
+ return this.abortController.signal;
104
+ }
105
+
106
+ /**
107
+ * 取消 run。Phase 3c 加幂等保证 —— cancel 多次只触发一次 abort + 一次
108
+ * run.cancelled emit。两条调用路径都依赖此幂等:
109
+ * 1. cancelRun(active-runs.js)race window / interrupt 兜底时直接调
110
+ * 2. session-loop.js result 处理识别 terminal_reason: 'aborted_*' 时调(interrupt 路径)
111
+ *
112
+ * 没有这个 flag,interrupt 路径 + abort 兜底会双 emit run.cancelled,
113
+ * 前端 toast '已取消' 弹两次。
114
+ */
115
+ cancel(reason = 'user_cancel') {
116
+ if (this._cancelled) return;
117
+ this._cancelled = true;
118
+ this.abortController.abort(reason);
119
+ this.emit({ type: 'run.cancelled', reason });
120
+ }
121
+
122
+ ensureNotAborted() {
123
+ if (this.signal.aborted) {
124
+ const err = new Error(`Run ${this.runId} aborted: ${this.signal.reason || 'unknown'}`);
125
+ err.code = 'AGENT_ABORTED';
126
+ throw err;
127
+ }
128
+ }
129
+
130
+ // ── 事件 ──
131
+
132
+ emit(event) {
133
+ if (!event || !event.type) throw new Error('emit: event.type required');
134
+ // sessionId 自动 enrich 让 WS 端按 sid 过滤事件,防多 session / 多 tab 跨 session
135
+ // 串扰(同 project bus 共享)。`...event` 在后保证调用方显式传的字段不被覆盖。
136
+ const enriched = {
137
+ runId: this.runId,
138
+ sessionId: this.sessionId,
139
+ ts: new Date().toISOString(),
140
+ ...event,
141
+ };
142
+ this.eventBus.publish(enriched);
143
+ return enriched;
144
+ }
145
+
146
+ // ── workspace 包装 ──
147
+ // 外部 workspaceRoot 模式(P0 per-project 目录):直接走绝对路径,
148
+ // 由调用方负责 mkdir + git init(见 projects/workspace.js)。
149
+ // 旧 runId 模式(保留给 smoke 测试 / 单 run 沙盒):走 runtime/workspace.js 那套。
150
+
151
+ workspace = {
152
+ ensure: async () => {
153
+ if (this._externalWorkspaceRoot) {
154
+ await fs.mkdir(this._externalWorkspaceRoot, { recursive: true });
155
+ return this._externalWorkspaceRoot;
156
+ }
157
+ return ensureWorkspace(this.runId);
158
+ },
159
+ root: () => this._externalWorkspaceRoot || getWorkspaceRoot(this.runId),
160
+ exists: async (rel) => {
161
+ if (this._externalWorkspaceRoot) {
162
+ try {
163
+ await fs.access(path.resolve(this._externalWorkspaceRoot, rel));
164
+ return true;
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+ return exists(this.runId, rel);
170
+ },
171
+ read: (rel) => readFile(this.runId, rel),
172
+ write: (rel, content) => writeFile(this.runId, rel, content),
173
+ list: (rel) => listDir(this.runId, rel),
174
+ resolve: (rel) => safeResolve(this.runId, rel),
175
+ };
176
+
177
+ // ── 跟踪 SDK 数据 ──
178
+
179
+ /** SDK 第一条 message 带 session_id,记下供 metadata / debug 用 */
180
+ recordSdkSession(sessionId) {
181
+ if (!this.sdkSessionId && sessionId) {
182
+ this.sdkSessionId = sessionId;
183
+ this.emit({ type: 'run.sdk.session', sessionId });
184
+ }
185
+ }
186
+
187
+ /**
188
+ * SDK SDKResultMessage 含全套统计;一次性吸收。
189
+ *
190
+ * 计量口径(2026-07-31 起):权威数据源是 result.modelUsage —— SDK 的分模型
191
+ * 总账,含子代理消耗,中断的 turn 也不丢(累计值持续增长,下一条 result 的
192
+ * 差分会捞回来)。差分出的本 turn 增量:
193
+ * - 按模型汇总进 counters.inputTokens 等主字段(配额真列的来源)
194
+ * - 分模型明细留在 counters.modelUsage,finishTurn 落 run_model_usage 表
195
+ * - totalCostUsd 同样取差分(result.total_cost_usd 是会话累计值,以前
196
+ * 原样落库导致 cost 列不能 sum —— 那是虚价,但至少语义要对)
197
+ * result.usage 只做 modelUsage 缺失时的兜底(usage 是本轮真增量,探针实测)。
198
+ */
199
+ absorbResult(result) {
200
+ if (!result) return;
201
+ this.counters.turns = result.num_turns ?? this.counters.turns;
202
+ this.counters.durationMs = result.duration_ms ?? this.counters.durationMs;
203
+ this.counters.durationApiMs = result.duration_api_ms ?? this.counters.durationApiMs;
204
+
205
+ // reprice:仅 API 会话 —— key 从 SDK spoof alias 还原成 appModel、按表价
206
+ // 重算 costUsd(SDK 按 alias 的 Claude 价目表算,Kimi 时代虚高 30×)。
207
+ // 订阅会话原样过(alias 与真订阅模型名同形,必须以会话通路为准,见
208
+ // model-context.js repriceUsageDeltas 注释)。
209
+ const deltas = repriceUsageDeltas(this._diffModelUsage(result.modelUsage), this.appModel);
210
+ if (deltas) {
211
+ let inp = 0; let out = 0; let cr = 0; let cc = 0; let cost = 0;
212
+ for (const d of Object.values(deltas)) {
213
+ inp += d.inputTokens; out += d.outputTokens;
214
+ cr += d.cacheReadTokens; cc += d.cacheCreateTokens;
215
+ cost += d.costUsd;
216
+ }
217
+ this.counters.inputTokens = inp;
218
+ this.counters.outputTokens = out;
219
+ this.counters.cacheReadTokens = cr;
220
+ this.counters.cacheCreateTokens = cc;
221
+ this.counters.totalCostUsd = cost;
222
+ this.counters.modelUsage = deltas;
223
+ } else if (result.usage) {
224
+ this.counters.inputTokens = result.usage.input_tokens || 0;
225
+ this.counters.outputTokens = result.usage.output_tokens || 0;
226
+ this.counters.cacheReadTokens = result.usage.cache_read_input_tokens || 0;
227
+ this.counters.cacheCreateTokens = result.usage.cache_creation_input_tokens || 0;
228
+ this.counters.totalCostUsd = result.total_cost_usd ?? this.counters.totalCostUsd;
229
+ }
230
+ this._foldToolCharges();
231
+ }
232
+
233
+ /**
234
+ * 半截续接的账要**结转**(08-21 夜,fable 评审 P0)。
235
+ *
236
+ * 病:absorbResult 对 counters 是**赋值不是累加**(差分基准 _modelUsageBase 在第一个
237
+ * result 就前移了),而半截续接会让同一个 run 出现第二个 result → 第一轮的整份账
238
+ * (token、表价 cost、已经折进去的 generate_image 按件费、上游自报的真 cost)被第二轮
239
+ * 的增量整个覆盖 → 那一轮等于白跑,日限闸门(quota.usedCostToday 读 run_model_usage)少收。
240
+ * 真路径实测:续接的回合上游真打 2 发、落库只有 1 发的量。
241
+ *
242
+ * 用法:推续接消息**之前** takeCarry() 存一份,下一个 result 的 absorbResult /
243
+ * applyUpstreamBilling 跑完后 addCarry(carry) 加回去。多次续接会一路滚下去。
244
+ *
245
+ * ⚠️ 只加"可累加"的量:token / cost / 时长 / 工具计数。`turns` 不加 —— 它是 SDK 的
246
+ * num_turns,本身带累计语义,加了会双数。
247
+ */
248
+ takeCarry() {
249
+ const c = this.counters;
250
+ return {
251
+ inputTokens: c.inputTokens || 0,
252
+ outputTokens: c.outputTokens || 0,
253
+ cacheReadTokens: c.cacheReadTokens || 0,
254
+ cacheCreateTokens: c.cacheCreateTokens || 0,
255
+ totalCostUsd: c.totalCostUsd || 0,
256
+ durationMs: c.durationMs || 0,
257
+ durationApiMs: c.durationApiMs || 0,
258
+ toolCalls: c.toolCalls || 0,
259
+ toolFailures: c.toolFailures || 0,
260
+ compactBoundaries: c.compactBoundaries || 0,
261
+ apiRetries: c.apiRetries || 0,
262
+ modelUsage: c.modelUsage ? JSON.parse(JSON.stringify(c.modelUsage)) : null,
263
+ };
264
+ }
265
+
266
+ addCarry(carry) {
267
+ if (!carry) return;
268
+ const c = this.counters;
269
+ for (const k of ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheCreateTokens', 'totalCostUsd', 'durationMs', 'durationApiMs']) {
270
+ c[k] = (c[k] || 0) + (carry[k] || 0);
271
+ }
272
+ // toolCalls 等计数器**不是**每回合重置后单调累加的差分量,而是 handleSDKMessage 一路 ++ 的,
273
+ // 续接期间它们没被重置(同一个 run 同一个 ctx)→ 已经含了第一轮,别再加一遍。
274
+ if (carry.modelUsage) {
275
+ const usage = c.modelUsage && typeof c.modelUsage === 'object' ? c.modelUsage : {};
276
+ for (const [name, d] of Object.entries(carry.modelUsage)) {
277
+ const prev = usage[name] || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, costUsd: 0 };
278
+ usage[name] = {
279
+ ...prev,
280
+ inputTokens: (prev.inputTokens || 0) + (d.inputTokens || 0),
281
+ outputTokens: (prev.outputTokens || 0) + (d.outputTokens || 0),
282
+ cacheReadTokens: (prev.cacheReadTokens || 0) + (d.cacheReadTokens || 0),
283
+ cacheCreateTokens: (prev.cacheCreateTokens || 0) + (d.cacheCreateTokens || 0),
284
+ costUsd: (prev.costUsd || 0) + (d.costUsd || 0),
285
+ };
286
+ }
287
+ c.modelUsage = usage;
288
+ }
289
+ }
290
+
291
+ /** 按件计价的工具花费(generate_image 每张 $0.20):工具成功后记一笔,结账时并进 modelUsage 同一本账 */
292
+ addToolCharge(name, usd) {
293
+ const v = Number(usd);
294
+ if (!name || !Number.isFinite(v) || v <= 0) return;
295
+ this.counters.toolCharges[name] = (this.counters.toolCharges[name] || 0) + v;
296
+ }
297
+ _foldToolCharges() {
298
+ const charges = this.counters.toolCharges || {};
299
+ const names = Object.keys(charges);
300
+ if (!names.length) return;
301
+ const usage = this.counters.modelUsage && typeof this.counters.modelUsage === 'object' ? this.counters.modelUsage : {};
302
+ for (const name of names) {
303
+ const prev = usage[name] || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, costUsd: 0 };
304
+ usage[name] = { ...prev, costUsd: (prev.costUsd || 0) + charges[name] };
305
+ }
306
+ this.counters.modelUsage = usage;
307
+ this.counters.totalCostUsd = Object.values(usage).reduce((s, d) => s + (d.costUsd || 0), 0);
308
+ this.counters.toolCharges = {};
309
+ }
310
+
311
+ /**
312
+ * 上游自报的费用覆盖(08-21 晚,lib/ingress/upstream-billing.js)。
313
+ * billing = { appModel → { costUsd, responses, promptTokens, completionTokens, cachedTokens } },
314
+ * 是 ingress 在本轮累加、session-loop 结账前取走的。规则:
315
+ * - 上游报的 cost **> 0**(真扣了余额)→ 覆盖 modelUsage[appModel].costUsd;token 数仍信 SDK 差分(口径不同)
316
+ * - 上游报 0 / 没报 → 保留表价算出来的数。OpenCode Go 订阅行(08-21 深夜实测)对订阅额度内的请求报 cost=0,
317
+ * 但它消耗的是全站共享的 Go 池子($12/5h),按表价记才能让每用户日限跟着受控;免费行表价本就是 0
318
+ * - SDK 没有该模型条目(CLI 失败 / 没覆盖到的 helper)→ 用上游 token 数补一条,按表价算(repriceUsageDeltas)
319
+ * 覆盖后重算 totalCostUsd。counters.costSource:'upstream'(上游真扣费覆盖过)| 'table'(只按表价)
320
+ */
321
+ applyUpstreamBilling(billing) {
322
+ if (!billing || typeof billing !== 'object') return;
323
+ let touched = false; let overrode = false;
324
+ const usage = this.counters.modelUsage && typeof this.counters.modelUsage === 'object' ? this.counters.modelUsage : {};
325
+ for (const [appModel, acc] of Object.entries(billing)) {
326
+ if (!acc || !acc.responses) continue;
327
+ const reported = acc.costUsd != null && Number(acc.costUsd) > 0 ? Number(acc.costUsd) : null;
328
+ const prev = usage[appModel];
329
+ if (prev) {
330
+ if (reported != null) { usage[appModel] = { ...prev, costUsd: reported }; overrode = true; touched = true; }
331
+ continue;
332
+ }
333
+ // SDK 没这条(CLI 失败那类):用上游 token 数补一条,先按表价,再看上游有没有真扣费
334
+ const cached = acc.cachedTokens || 0;
335
+ const raw = { [appModel]: { inputTokens: Math.max(0, (acc.promptTokens || 0) - cached), outputTokens: acc.completionTokens || 0, cacheReadTokens: cached, cacheCreateTokens: 0, costUsd: 0 } };
336
+ const priced = repriceUsageDeltas(raw, this.appModel)?.[appModel] || raw[appModel];
337
+ usage[appModel] = reported != null ? { ...priced, costUsd: reported } : priced;
338
+ if (reported != null) overrode = true;
339
+ touched = true;
340
+ }
341
+ if (!touched) return;
342
+ this.counters.modelUsage = usage;
343
+ this.counters.totalCostUsd = Object.values(usage).reduce((s, d) => s + (d.costUsd || 0), 0);
344
+ this.counters.costSource = overrode ? 'upstream' : 'table';
345
+ }
346
+
347
+ /**
348
+ * 会话累计的 modelUsage → 本 turn 增量。返回 null 表示"没有可用的 modelUsage"
349
+ * (调用方退回 usage 兜底);返回 {}(空对象)表示"有 modelUsage 但本 turn
350
+ * 没有任何新消耗",此时主字段照常清零是正确语义。
351
+ * Math.max(0, ...) 防浮点 / 上游异常出负数;基准无论如何都更新成最新快照。
352
+ */
353
+ _diffModelUsage(modelUsage) {
354
+ if (!modelUsage || typeof modelUsage !== 'object') return null;
355
+ const models = Object.keys(modelUsage);
356
+ if (models.length === 0) return null;
357
+ const base = this._modelUsageBase || {};
358
+ const deltas = {};
359
+ const snapshot = {};
360
+ for (const model of models) {
361
+ const u = modelUsage[model] || {};
362
+ const b = base[model] || {};
363
+ snapshot[model] = {
364
+ inputTokens: u.inputTokens || 0,
365
+ outputTokens: u.outputTokens || 0,
366
+ cacheReadInputTokens: u.cacheReadInputTokens || 0,
367
+ cacheCreationInputTokens: u.cacheCreationInputTokens || 0,
368
+ costUSD: u.costUSD || 0,
369
+ };
370
+ const d = {
371
+ inputTokens: Math.max(0, snapshot[model].inputTokens - (b.inputTokens || 0)),
372
+ outputTokens: Math.max(0, snapshot[model].outputTokens - (b.outputTokens || 0)),
373
+ cacheReadTokens: Math.max(0, snapshot[model].cacheReadInputTokens - (b.cacheReadInputTokens || 0)),
374
+ cacheCreateTokens: Math.max(0, snapshot[model].cacheCreationInputTokens - (b.cacheCreationInputTokens || 0)),
375
+ costUsd: Math.max(0, snapshot[model].costUSD - (b.costUSD || 0)),
376
+ };
377
+ if (d.inputTokens || d.outputTokens || d.cacheReadTokens || d.cacheCreateTokens || d.costUsd) {
378
+ deltas[model] = d;
379
+ }
380
+ }
381
+ // 基准里已有但本次 modelUsage 没出现的模型:保留旧值(SDK 不应该丢 key,
382
+ // 防御性合并,免得它真丢时差分把历史消耗又算一遍)
383
+ this._modelUsageBase = { ...base, ...snapshot };
384
+ return deltas;
385
+ }
386
+
387
+ incrementTool(failed = false) {
388
+ this.counters.toolCalls += 1;
389
+ if (failed) this.counters.toolFailures += 1;
390
+ }
391
+
392
+ /**
393
+ * 子代理收尾用量(SDK task_notification.usage)累积(2026-07-30)。
394
+ * **单独记账,不加进主 inputTokens/outputTokens** —— 主字段现在来自
395
+ * modelUsage 差分(SDK 的分模型总账,已含 sidechain),相加必然双重计数。
396
+ * 这两个字段只进 metadata 做可观测("这轮子代理烧了多少"),也是交叉验证
397
+ * 钩子:某轮 subagent 用量明显大于 modelUsage 差分总和 = 总账没含 sidechain
398
+ * 的信号(目前判断是含的,出现反例再查)。
399
+ */
400
+ absorbSubagentUsage(usage) {
401
+ if (!usage || typeof usage !== 'object') return;
402
+ this.counters.subagentInputTokens = (this.counters.subagentInputTokens || 0) + (usage.input_tokens || 0);
403
+ this.counters.subagentOutputTokens = (this.counters.subagentOutputTokens || 0) + (usage.output_tokens || 0);
404
+ }
405
+
406
+ // ── observability ──
407
+
408
+ snapshot() {
409
+ return {
410
+ runId: this.runId,
411
+ skillId: this.skillId,
412
+ sdkSessionId: this.sdkSessionId,
413
+ counters: { ...this.counters },
414
+ elapsedMs: Date.now() - this.startedAt,
415
+ aborted: this.signal.aborted,
416
+ };
417
+ }
418
+ }