@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,550 +1,550 @@
1
- /**
2
- * engine/agent/agent-shared.js — agent 跑 run 时复用的常量 + 翻译层
3
- *
4
- * 历史:原 `session-loop.js` 含 runAgent (per-turn 一次性 query 模式)。streamInput 重构后
5
- * 生产代码切到 session-loop.js (runSession,long-running query 跨多 turn),
6
- * runAgent 死掉。本文件保留 session-loop.js 仍依赖的部分:
7
- *
8
- * - NODESIGN_PRELUDE / NODESIGN_PLAN_INSTRUCTIONS 系统 prompt 段
9
- * - DEFAULT_TOOL_ALLOWLIST / STREAMING_ENABLED SDK options 默认值
10
- * - handleSDKMessage / detectArtifact SDK 消息 → EventBus 翻译层
11
- *
12
- * 调用方:server/engine/agent/session-loop.js (runSession)
13
- */
14
-
15
- import { Events } from './events.js';
16
- import { handleTaskMessage } from './task-events.js';
17
- import { listWorkspaceArtifacts } from '../../lib/artifact-target.js';
18
- import { toWorkspaceRel } from '../../lib/workspace-path.js';
19
- import { parse as parsePartialJson, Allow as PartialAllow } from 'partial-json';
20
-
21
- // 系统提示词(prelude / plan instructions 的加载与渲染)2026-08-19 迁去
22
- // ./system-prompts.js —— 那一块跟本文件其余部分(SDK 消息翻译层 + options
23
- // 默认值)零耦合,放一起纯属历史。session-loop 直接从新模块拿,这里不转出口:
24
- // 转一手就是给"从哪 import"开第二个答案。
25
-
26
- // SDK base 工具白名单(Options.tools,sdk.d.ts:1216)—— 限定主 agent 可见的
27
- // **内置工具**集合。MCP 工具(mcp__nodesign__*)由 mcpServers 字段独立暴露,
28
- // 不需要列在这里;新加内置工具按需追加。
29
- //
30
- // 设计要点:
31
- // - Bash 是必需(git/playwright/zip 都靠它)。沙盒由 OS 级 sandbox 字段保证
32
- // - AskUserQuestion 是 deferred 工具:bypassPermissions 不影响它;canUseTool
33
- // callback 拦截它注入用户答案(session-loop.js canUseTool 段)
34
- // - WebFetch(SDK 内置)走 binary 自带的 prompt 总结,不灌完整 HTML 给 model;
35
- // WebSearch 走我们自己的 mcp__nodesign__web_search(4 provider,免 server_tool_use)
36
- // - Task 是子代理调用入口;agents 字段注册的子代理通过 Task 暴露给主 agent。
37
- // **Task 漏挂 = 所有子代理形同摆设**(P0+ stage 1 修复过一次的隐性 bug)
38
- // 工具真名在 SDK 0.3 已改叫 `Agent`,'Task' 走 sdk.mjs 的旧名映射表(i6)
39
- // 照样解析成 Agent,故此处不必改名
40
- // - TaskOutput 是**子代理报告的取回口**(2026-08-03 加)。子代理默认后台跑,
41
- // 后台跑时 tool_result 里只有一句 "Async agent launched successfully",
42
- // 完成通知也只给一个 output_file 路径 —— 那个路径是 SDK 转录 jsonl,既在
43
- // cwd + additionalDirectories 之外(Read 够不着),又明令不许读(会撑爆上下文)。
44
- // 漏挂 TaskOutput 的后果是主 agent 眼睁睁看着子代理跑完却拿不到结果:实测它
45
- // 会去 ToolSearch 找 SendMessage,找不到,然后放弃改用自己的记忆硬写。
46
- // hooks.js 已强制前台(正常路径拿得到报告),这里是兜底的第二条路。
47
- //
48
- // 非显式语义:
49
- // - tools 字段是"可见集合"白名单,不在里面的内置工具会被剥离
50
- // - 不是 auto-allow(auto-allow 由 permissionMode='bypassPermissions' 已经全
51
- // 跳)。之前 sdkOptions 同设 allowedTools 是冗余,已删
52
- // - **Skill 必须显式列出**:SDK `skills:` option 只把 `Skill(<name>)` 注入到
53
- // `allowedTools`(权限层),**不动 tools 数组**(可见集层)。漏列 Skill =
54
- // `--tools` flag 显式不含 Skill → CLI binary 把 Skill 工具从 agent 可见集
55
- // 剥离 → 即使 plugins+skills option 都传对了,agent 也看不到 Skill 工具,
56
- // 永远调不到任何 SDK skill。SDK 文档 sdk.d.ts:1651 那句 "do not need to
57
- // add Skill to allowedTools" 只承诺权限层,没承诺可见层。
58
- export const DEFAULT_TOOL_ALLOWLIST = [
59
- 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'TodoWrite', 'Bash',
60
- 'AskUserQuestion',
61
- 'WebFetch',
62
- 'Task',
63
- 'TaskOutput',
64
- 'Skill',
65
- // deferred MCP 工具的取 schema 入口(ENABLE_TOOL_SEARCH=true 时生效)。
66
- // 漏挂 = 延迟加载的工具永远调不到(同 Skill 的可见集陷阱)
67
- 'ToolSearch',
68
- ];
69
-
70
- // 旧式(cwd 根)产物候选 —— canvas.html 列首位,其余兼容 deec72d 之前的
71
- // e2e smoke / 旧 deskskill-engine 输出。任务模型下产物不在 cwd 根,走
72
- // listWorkspaceArtifacts(见 detectArtifact)。
73
- const ARTIFACT_CANDIDATES = ['canvas.html', 'deck.html', 'index.html', 'output.html'];
74
-
75
- // P0+ s1 C24:流式打字效果(text / thinking 逐 token 推送)。
76
- // 跟 sdkOptions.includePartialMessages 同步 —— 我们默认开(前端要打字效果)。
77
- // 启用时 handleAssistantBlocks 跳过 text/thinking blocks(已经从 stream_event 推完,
78
- // 避免双推),但仍推 tool_use 完整 block(run.delta.tool_use 是入参的权威快照;
79
- // Edit/Write 另有节流的 run.delta.tool_input 真流式增量,见 handleStreamEvent)。
80
- export const STREAMING_ENABLED = true;
81
-
82
-
83
- // ── SDK message → EventBus 翻译层 ──
84
-
85
- /**
86
- * 把 SDK 各种 message 类型翻译成 Nodesign 内部事件。
87
- * SDKMessage union 见 sdk.d.ts:2988(28+ 种 type/subtype 组合)。
88
- *
89
- * 翻译策略:
90
- * - 主流程消息(assistant / user / result):走 handleAssistantBlocks / handleUserBlocks
91
- * - SDK system subtype 多达 14 种:分派到对应 Events 构造器
92
- * - 旁路类型(stream_event / keep_alive):noop(前端不需要)
93
- */
94
- export function handleSDKMessage(ctx, msg) {
95
- // 首条 message 含 session_id,记下
96
- if (msg.session_id) ctx.recordSdkSession(msg.session_id);
97
-
98
- // 子代理时间轴(2026-07-28):forwardSubagentText 开启后,子代理的消息带
99
- // parent_tool_use_id 流进主 query。用原型链派生一个 emit 装饰过的 ctx——
100
- // 该 message 产生的所有事件都盖上 parentToolUseId,前端据此拆时间轴。
101
- // Object.create 保 AgentContext 的原型方法与共享状态(counters 等)原样可用。
102
- if (msg.parent_tool_use_id) {
103
- const parent = msg.parent_tool_use_id;
104
- // 先确保流式入参 map 已挂在真 ctx 上 —— 否则首次访问发生在派生对象上,
105
- // map 会写成派生对象的 own property,随派生对象丢弃(原型链只読共享)
106
- toolInputStreams(ctx);
107
- // ⚠️ 必须捕获 base 快照:闭包若直接引用 ctx 变量,下面 ctx = child 之后
108
- // ctx.emit 会解析回 child 自己 → 无限递归(真机爆过 Maximum call stack)
109
- const base = ctx;
110
- const child = Object.create(base);
111
- child.emit = (evt) => base.emit({ parentToolUseId: parent, ...evt });
112
- ctx = child;
113
- }
114
-
115
- switch (msg.type) {
116
- case 'assistant':
117
- // BetaMessage 含 content[] (text / thinking / tool_use blocks)
118
- // STREAMING_ENABLED 时 text/thinking 已从 stream_event 推完,跳过避免双推。
119
- // 例外:子代理消息(forwardSubagentText 转发)没有对应 stream_event,
120
- // 跳过会把子代理正文整段吞掉(时间轴就空了)—— 不跳。
121
- handleAssistantBlocks(ctx, msg.message?.content || [], STREAMING_ENABLED && !msg.parent_tool_use_id);
122
- break;
123
-
124
- case 'user':
125
- // 一般是 tool_result 反馈(agent loop 中 SDK 会回填)
126
- handleUserBlocks(ctx, msg.message?.content || []);
127
- break;
128
-
129
- case 'system':
130
- handleSystemMessage(ctx, msg);
131
- break;
132
-
133
- case 'stream_event':
134
- // SDKPartialAssistantMessage —— 流增量(includePartialMessages: true)
135
- // 推逐 token text_delta / thinking_delta 给前端实现打字效果
136
- if (STREAMING_ENABLED) handleStreamEvent(ctx, msg);
137
- break;
138
-
139
- case 'tool_use_summary':
140
- // SDKToolUseSummaryMessage —— SDK 的 helper model 对刚才那批工具调用写的
141
- // 一句话总结(Claude Code 侧栏折叠标题就是它)。以前当旁路审计丢掉了,
142
- // 于是前端只能自己切 thinking 头 60 字当标题。现在转发给前端当分组标题。
143
- if (msg.summary) {
144
- ctx.emit(Events.toolUseSummary(msg.summary, msg.preceding_tool_use_ids || []));
145
- }
146
- break;
147
-
148
- case 'tool_progress':
149
- // SDKToolProgressMessage —— 工具执行 >1s 时定期推(前端可显示"读取中 12s...")
150
- ctx.emit(Events.toolProgress(msg.tool_use_id, msg.tool_name, msg.elapsed_time_seconds));
151
- break;
152
-
153
- case 'prompt_suggestion':
154
- // SDKPromptSuggestionMessage —— 每轮后 piggyback 预测的下条 prompt
155
- // 前端 SuggestionChip(C19)渲染
156
- ctx.emit(Events.promptSuggestion(msg.suggestion));
157
- break;
158
-
159
- case 'status':
160
- // SDKStatusMessage —— 'compacting' | 'requesting' | null
161
- ctx.emit({ type: 'run.status', status: msg.status });
162
- break;
163
-
164
- case 'rate_limit_event':
165
- ctx.emit({ type: 'run.rate_limit', info: msg.rate_limit_info });
166
- break;
167
-
168
- case 'auth_status':
169
- // 鉴权状态(首次 spawn 时可能出现)
170
- if (msg.error) {
171
- ctx.emit({ type: 'run.auth_error', message: msg.error });
172
- }
173
- break;
174
-
175
- case 'keep_alive':
176
- // SDKKeepAliveMessage —— WS 心跳,不入 EventBus
177
- break;
178
-
179
- case 'result':
180
- // 由外层 for await 捕获处理(finalText / artifactPath / counters)
181
- break;
182
-
183
- default:
184
- // 兜底:未识别的新 type 留个调试痕迹,方便 SDK 升级时发现
185
- console.warn(`[run ${ctx.runId}] unknown SDK message type:`, msg.type);
186
- break;
187
- }
188
- }
189
-
190
- /**
191
- * SDK type:'system' 下的 14 种 subtype 派发。集中放一处便于维护。
192
- */
193
- function handleSystemMessage(ctx, msg) {
194
- switch (msg.subtype) {
195
- case 'init':
196
- // 初始化元信息:agents / tools / mcp_servers / model / permissionMode 等
197
- // model:SDK 看到的是 spoofing alias(如 claude-opus-4-7[1m],让 SDK 信
198
- // rawMaxTokens=1M 解 Kimi 256k 卡顿,详见 model-context.js)。前端 InfoChips
199
- // 显示 appModel(kimi-k2.6 真名)才不让用户困惑。
200
- ctx.emit(Events.systemInit({
201
- agents: msg.agents,
202
- tools: msg.tools,
203
- mcpServers: msg.mcp_servers,
204
- model: ctx.appModel || msg.model,
205
- permissionMode: msg.permissionMode,
206
- skills: msg.skills,
207
- plugins: msg.plugins,
208
- claudeCodeVersion: msg.claude_code_version,
209
- cwd: msg.cwd,
210
- }));
211
- break;
212
-
213
- case 'compact_boundary':
214
- ctx.counters.compactBoundaries += 1;
215
- ctx.emit({ type: 'run.compact_boundary', compactMetadata: msg.compact_metadata });
216
- break;
217
-
218
- case 'api_retry':
219
- // SDKAPIRetryMessage(sdk.d.ts:2322):API 请求失败,可重试,将在 retry_delay_ms 后重试。
220
- // 之前落到 default warn —— 改为 emit 让上层能看到"网络抖了,agent 还在重试",
221
- // 避免用户以为卡死。
222
- ctx.emit(Events.apiRetry(
223
- msg.attempt,
224
- msg.max_retries,
225
- msg.retry_delay_ms,
226
- msg.error_status, // number | null(连接错误时为 null)
227
- msg.error, // SDKAssistantMessageError union
228
- ));
229
- break;
230
-
231
- case 'files_persisted':
232
- // SDKFilesPersistedEvent —— agent 写完 file checkpoint 持久化通知
233
- // FileChanged hook 触发的 file.changed 事件是更直接的;这个仅审计
234
- ctx.emit(Events.filesPersisted(msg.files, msg.failed));
235
- break;
236
-
237
- case 'memory_recall':
238
- // 自动 memory 召回 —— 前端可显示"recalled from memory"
239
- ctx.emit(Events.memoryRecall(msg.mode, msg.memories));
240
- break;
241
-
242
- case 'task_started':
243
- case 'task_progress':
244
- case 'task_updated':
245
- case 'task_notification':
246
- // SDK 统一任务消息族(后台 Bash / Workflow 也发 task_*):收口翻译
247
- // 住 task-events.js —— 只放真子代理进 run.task.*
248
- handleTaskMessage(ctx, msg);
249
- break;
250
-
251
- case 'notification':
252
- // SDKNotificationMessage —— 系统级 toast(priority: low/medium/high/immediate)
253
- ctx.emit(Events.notification(msg.key, msg.text, msg.priority, msg.color, msg.timeout_ms));
254
- break;
255
-
256
- case 'session_state_changed':
257
- ctx.emit(Events.sessionState(msg.state));
258
- break;
259
-
260
- case 'hook_started':
261
- ctx.emit(Events.hookStarted(msg.hook_name, msg.hook_event));
262
- break;
263
-
264
- case 'hook_progress':
265
- // hook 执行中 stdout/stderr 流(仅 includeHookEvents: true 时)
266
- // 前端不需要,旁路日志即可
267
- break;
268
-
269
- case 'hook_response':
270
- ctx.emit(Events.hookResponse(
271
- msg.hook_name, msg.hook_event, msg.outcome, msg.output, msg.exit_code,
272
- ));
273
- break;
274
-
275
- case 'plugin_install':
276
- // 插件安装进度(headless mode),不入 EventBus
277
- break;
278
-
279
- case 'local_command_output':
280
- // 本地 slash command 输出(/voice / /usage 等),不入 EventBus
281
- break;
282
-
283
- case 'elicitation_complete':
284
- // MCP elicitation URL 模式完成确认,旁路
285
- break;
286
-
287
- case 'mirror_error':
288
- // SessionStore mirror 失败,旁路(我们没用 SessionStore)
289
- break;
290
-
291
- case 'status':
292
- // SDK 进度/心跳状态 → 转发(前端 header 存活点 + compacting toast 消费;
293
- // 曾被旁路,是"前端不知道后端死活"的帮凶之一)
294
- ctx.emit({ type: 'run.status', status: msg.status });
295
- break;
296
-
297
- case 'thinking_tokens':
298
- // SDK 0.3+ 思考进度心跳(~1s 一条,estimated_tokens 累计值)。
299
- // 转成 run.status 让前端知道"在思考、没死"并可显示进度。
300
- ctx.emit({
301
- type: 'run.status', status: 'thinking', tokens: msg.estimated_tokens,
302
- });
303
- break;
304
-
305
- default:
306
- console.warn(`[run ${ctx.runId}] unknown system subtype:`, msg.subtype);
307
- break;
308
- }
309
- }
310
-
311
- function handleAssistantBlocks(ctx, content, skipTextThinking = false) {
312
- for (const block of content) {
313
- switch (block.type) {
314
- case 'text':
315
- // 流式开了 → text 已通过 stream_event 推完,跳过避免重复
316
- if (!skipTextThinking && block.text) {
317
- ctx.emit(Events.deltaText(ctx.counters.turns, block.text));
318
- }
319
- break;
320
- case 'thinking':
321
- if (!skipTextThinking && block.thinking) {
322
- ctx.emit(Events.deltaThinking(ctx.counters.turns, block.thinking));
323
- }
324
- break;
325
- case 'tool_use':
326
- // tool_use 不论流式与否都在 assistant 完成时推一次 —— 完整入参的权威
327
- // 快照(真流式 tool_input 只发抽出字段的增量,别的字段靠这条补全)
328
- ctx.emit(Events.deltaToolUse(ctx.counters.turns, block.id, block.name, block.input));
329
- ctx.incrementTool(false);
330
-
331
- // Phase 1:TodoWrite 工具单独再 emit 一条 todoUpdated。
332
- // SDK 不会在 type:'system' 里专门推 TodoWrite 状态 —— agent 用工具
333
- // 写计划时,input.todos 就是完整的 [{ content, status, activeForm }] 列表
334
- // (sdk-tools.d.ts:530 TodoWriteInput)。
335
- // tool_use 只够前端展示"调了 TodoWrite",但拿不到结构化的 todo 列表给
336
- // 计划面板用,所以这里平行 emit 一次 run.todo.updated。
337
- if (block.name === 'TodoWrite' && block.input && Array.isArray(block.input.todos)) {
338
- ctx.emit(Events.todoUpdated(block.input.todos));
339
- }
340
- break;
341
- // 其他 block 类型(redacted_thinking / image / document)忽略
342
- }
343
- }
344
- }
345
-
346
- /**
347
- * C24:处理 SDK stream_event message(含 BetaRawMessageStreamEvent)。
348
- * 推逐 token 增量给前端实现打字效果。
349
- *
350
- * BetaRawMessageStreamEvent.type 值:
351
- * message_start / content_block_start / content_block_delta /
352
- * content_block_stop / message_delta / message_stop
353
- *
354
- * text_delta / thinking_delta 逐 token 转发;input_json_delta 只对 Edit/Write
355
- * 累积 + 节流抽字段(见下方"真流式工具入参"段),其余工具照旧等完整 block。
356
- */
357
-
358
- // ── 真流式工具入参(2026-07-28,工作台舞台层代码直播)──
359
- // input_json_delta 是半截 JSON 碎片,等拼完再发一次的话,模型逐 token 生成
360
- // new_string 的几十秒里前端只能干转圈。这里对写代码的工具累积缓冲区,节流用
361
- // partial-json 容错解析累积串,把目标字段相对上次的纯文本增量推给前端
362
- // (run.delta.tool_input)。转义符跨块断开由解析器兜住;字段单调增长,
363
- // 万一局部解析短暂回缩就跳过本拍(append 只在变长时发)。
364
- const TOOL_INPUT_STREAM_FIELDS = {
365
- Edit: 'new_string',
366
- Write: 'content',
367
- };
368
- const TOOL_INPUT_THROTTLE_MS = 120;
369
-
370
- function toolInputStreams(ctx) {
371
- if (!ctx._toolInputStreams) ctx._toolInputStreams = new Map();
372
- return ctx._toolInputStreams;
373
- }
374
-
375
-
376
- function pumpToolInputStream(ctx, st, flush) {
377
- const now = Date.now();
378
- if (!flush && now - st.lastEmit < TOOL_INPUT_THROTTLE_MS) return;
379
- let obj;
380
- try { obj = parsePartialJson(st.buf, PartialAllow.ALL); } catch { return; }
381
- if (!obj || typeof obj !== 'object') return;
382
- const text = typeof obj[st.field] === 'string' ? obj[st.field] : '';
383
- // file_path 只在确定流完后才取:容错解析会把半截字符串也带出来,第一拍常
384
- // 截在路径中间(e2e 撞过:抽到项目目录名 → 前端物件寻址指错)。目标字段的
385
- // key 出现(键序在 file_path 之后)或对象已有第二个键 = 路径已闭合。
386
- const pathComplete = obj[st.field] !== undefined || Object.keys(obj).length >= 2;
387
- // 发**工作区相对路径**(2026-08-13):前端拿它当画布物件 id 的路径部分,
388
- // 而 id = 工作区相对路径。以前原样转发绝对路径,前端靠 `tasks/<任务>/`
389
- // 这个特征段抠相对部分 —— 那一层拆掉后绝对路径里没有可锚定的标志了。
390
- const rawFilePath = !st.filePathSent && pathComplete && typeof obj.file_path === 'string' ? obj.file_path : null;
391
- const filePath = rawFilePath ? toWorkspaceRel(rawFilePath, ctx.workspace?.root?.()) : null;
392
- const append = text.length > st.sent ? text.slice(st.sent) : '';
393
- if (!append && !filePath && !flush) return;
394
- st.lastEmit = now;
395
- if (append) st.sent = text.length;
396
- if (filePath) st.filePathSent = true;
397
- ctx.emit(Events.deltaToolInput(ctx.counters.turns, st.id, st.name, {
398
- ...(filePath ? { filePath } : {}),
399
- ...(append ? { append } : {}),
400
- ...(flush ? { done: true } : {}),
401
- }));
402
- }
403
-
404
- function handleStreamEvent(ctx, msg) {
405
- const evt = msg.event;
406
- if (!evt) return;
407
- // 主线与子代理的 stream 可能并行交错,block index 各自归零 —— 加 parent 前缀区分
408
- const streamScope = msg.parent_tool_use_id || 'main';
409
- const streamKey = `${streamScope}:${evt.index}`;
410
-
411
- // 新 assistant message 开始 —— 该 scope 的 block index 归零,残留流式入参条目作废
412
- if (evt.type === 'message_start') {
413
- const streams = toolInputStreams(ctx);
414
- for (const key of [...streams.keys()]) {
415
- if (key.startsWith(`${streamScope}:`)) streams.delete(key);
416
- }
417
- return;
418
- }
419
-
420
- // tool_use 起点 —— content_block_start { content_block: { type: 'tool_use', id, name } }
421
- // 推 toolUseStarted 让前端立即显示 icon + tool name(status='running')。
422
- // input 还没流完,等 assistant message 完成后 deltaToolUse 同 blockId update。
423
- // 体感:agent "想完→开干" 之间几乎没延迟,工具图标第一时间出现。
424
- if (evt.type === 'content_block_start') {
425
- const cb = evt.content_block;
426
- if (cb && cb.type === 'tool_use' && cb.id && cb.name) {
427
- ctx.emit(Events.toolUseStarted(ctx.counters.turns, cb.id, cb.name));
428
- const field = TOOL_INPUT_STREAM_FIELDS[cb.name];
429
- if (field && Number.isInteger(evt.index)) {
430
- toolInputStreams(ctx).set(streamKey, {
431
- id: cb.id, name: cb.name, field,
432
- buf: '', sent: 0, lastEmit: 0, filePathSent: false,
433
- });
434
- }
435
- }
436
- return;
437
- }
438
-
439
- // 跟踪中的 tool_use block 收尾:最后一次 flush + 带 done 标记,清条目
440
- if (evt.type === 'content_block_stop') {
441
- const st = toolInputStreams(ctx).get(streamKey);
442
- if (st) {
443
- pumpToolInputStream(ctx, st, true);
444
- toolInputStreams(ctx).delete(streamKey);
445
- }
446
- return;
447
- }
448
-
449
- if (evt.type !== 'content_block_delta') return;
450
-
451
- const delta = evt.delta;
452
- if (!delta) return;
453
-
454
- if (delta.type === 'text_delta' && delta.text) {
455
- ctx.emit(Events.deltaText(ctx.counters.turns, delta.text));
456
- } else if (delta.type === 'thinking_delta' && delta.thinking) {
457
- ctx.emit(Events.deltaThinking(ctx.counters.turns, delta.thinking));
458
- } else if (delta.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
459
- const st = toolInputStreams(ctx).get(streamKey);
460
- if (st) {
461
- st.buf += delta.partial_json;
462
- pumpToolInputStream(ctx, st, false);
463
- }
464
- }
465
- // signature_delta / citations_delta 暂不处理
466
- }
467
-
468
- function handleUserBlocks(ctx, content) {
469
- if (!Array.isArray(content)) return;
470
- for (const block of content) {
471
- if (block.type === 'tool_result') {
472
- const ok = !block.is_error;
473
-
474
- // C24:tool_result 的 content 可能是:
475
- // - string(简单文本输出)
476
- // - block[](含 type:'text' / type:'image' 等多模态 content blocks)
477
- // P0 时把 image block JSON.stringify 序列化丢到文本里 → 前端显示
478
- // 一段难看的 base64 字符串。本提取分离:text 部分合并到 output,
479
- // image 部分单独传 images[] 数组让前端 <img src="data:..."> 渲染。
480
- let output = null;
481
- const images = [];
482
-
483
- if (typeof block.content === 'string') {
484
- output = block.content;
485
- } else if (Array.isArray(block.content)) {
486
- const textParts = [];
487
- for (const b of block.content) {
488
- if (b?.type === 'text' && b.text) {
489
- textParts.push(b.text);
490
- } else if (b?.type === 'image') {
491
- // 双格式兼容:
492
- // - Anthropic content block: { type:'image', source:{ type:'base64', media_type, data } }
493
- // - MCP CallToolResult ImageContent: { type:'image', data, mimeType }
494
- // SDK 透传 MCP CallToolResult 时格式不一定转换;只查 source.data 会漏接
495
- // generate_image 返的图,前端 chat 缩略图就空。
496
- const imgData = b.source?.data || b.data;
497
- const imgMime = b.source?.media_type || b.mimeType || 'image/png';
498
- if (imgData) {
499
- images.push({ mediaType: imgMime, data: imgData });
500
- } else {
501
- textParts.push(JSON.stringify(b)); // 拿不到 data 时留痕不丢数据
502
- }
503
- } else if (b) {
504
- // 未识别 block 类型 → fallback JSON.stringify 留痕(不丢数据)
505
- textParts.push(JSON.stringify(b));
506
- }
507
- }
508
- output = textParts.length > 0 ? textParts.join('\n') : null;
509
- }
510
-
511
- ctx.emit(Events.deltaToolResult(
512
- ctx.counters.turns,
513
- block.tool_use_id,
514
- '<sdk-tool>', // SDK 不在 tool_result 里带 name;前端可以从 tool_use 配对
515
- ok,
516
- ok ? output : undefined,
517
- ok ? undefined : { message: output || 'tool failed' },
518
- images.length > 0 ? images : undefined,
519
- ));
520
- if (!ok) ctx.counters.toolFailures += 1;
521
- }
522
- }
523
- }
524
-
525
- // ── 产物检测 ──
526
-
527
- /**
528
- * 本轮的主产物是哪份(写进 run.metadata.artifactPath)。
529
- *
530
- * 2026-07-28 修:原来只探 cwd 根的四个候选文件名,而任务模型(2026-07-28 上线)
531
- * 把产物搬进了 `tasks/<任务>/`,于是这个函数**自任务模型上线起恒返 null** ——
532
- * artifactPath 一直是空的,没人发现,因为它不报错只是没值。
533
- */
534
- export async function detectArtifact(ctx) {
535
- const root = typeof ctx?.workspace?.root === 'function' ? ctx.workspace.root() : null;
536
- if (root) {
537
- try {
538
- const artifacts = await listWorkspaceArtifacts(root);
539
- if (artifacts.length > 0) {
540
- // 扁平模型:没有"会话名下的任务"了,取第一份(列表序=收集器的
541
- // 稳定序)。老的 startsWith('tasks/') 优先级从扁平化起就是死代码。
542
- return artifacts[0].rel;
543
- }
544
- } catch { /* 扫不动就回落到旧式探测 */ }
545
- }
546
- for (const candidate of ARTIFACT_CANDIDATES) {
547
- if (await ctx.workspace.exists(candidate)) return candidate;
548
- }
549
- return null;
550
- }
1
+ /**
2
+ * engine/agent/agent-shared.js — agent 跑 run 时复用的常量 + 翻译层
3
+ *
4
+ * 历史:原 `session-loop.js` 含 runAgent (per-turn 一次性 query 模式)。streamInput 重构后
5
+ * 生产代码切到 session-loop.js (runSession,long-running query 跨多 turn),
6
+ * runAgent 死掉。本文件保留 session-loop.js 仍依赖的部分:
7
+ *
8
+ * - NODESIGN_PRELUDE / NODESIGN_PLAN_INSTRUCTIONS 系统 prompt 段
9
+ * - DEFAULT_TOOL_ALLOWLIST / STREAMING_ENABLED SDK options 默认值
10
+ * - handleSDKMessage / detectArtifact SDK 消息 → EventBus 翻译层
11
+ *
12
+ * 调用方:server/engine/agent/session-loop.js (runSession)
13
+ */
14
+
15
+ import { Events } from './events.js';
16
+ import { handleTaskMessage } from './task-events.js';
17
+ import { listWorkspaceArtifacts } from '../../lib/artifact-target.js';
18
+ import { toWorkspaceRel } from '../../lib/workspace-path.js';
19
+ import { parse as parsePartialJson, Allow as PartialAllow } from 'partial-json';
20
+
21
+ // 系统提示词(prelude / plan instructions 的加载与渲染)2026-08-19 迁去
22
+ // ./system-prompts.js —— 那一块跟本文件其余部分(SDK 消息翻译层 + options
23
+ // 默认值)零耦合,放一起纯属历史。session-loop 直接从新模块拿,这里不转出口:
24
+ // 转一手就是给"从哪 import"开第二个答案。
25
+
26
+ // SDK base 工具白名单(Options.tools,sdk.d.ts:1216)—— 限定主 agent 可见的
27
+ // **内置工具**集合。MCP 工具(mcp__nodesign__*)由 mcpServers 字段独立暴露,
28
+ // 不需要列在这里;新加内置工具按需追加。
29
+ //
30
+ // 设计要点:
31
+ // - Bash 是必需(git/playwright/zip 都靠它)。沙盒由 OS 级 sandbox 字段保证
32
+ // - AskUserQuestion 是 deferred 工具:bypassPermissions 不影响它;canUseTool
33
+ // callback 拦截它注入用户答案(session-loop.js canUseTool 段)
34
+ // - WebFetch(SDK 内置)走 binary 自带的 prompt 总结,不灌完整 HTML 给 model;
35
+ // WebSearch 走我们自己的 mcp__nodesign__web_search(4 provider,免 server_tool_use)
36
+ // - Task 是子代理调用入口;agents 字段注册的子代理通过 Task 暴露给主 agent。
37
+ // **Task 漏挂 = 所有子代理形同摆设**(P0+ stage 1 修复过一次的隐性 bug)
38
+ // 工具真名在 SDK 0.3 已改叫 `Agent`,'Task' 走 sdk.mjs 的旧名映射表(i6)
39
+ // 照样解析成 Agent,故此处不必改名
40
+ // - TaskOutput 是**子代理报告的取回口**(2026-08-03 加)。子代理默认后台跑,
41
+ // 后台跑时 tool_result 里只有一句 "Async agent launched successfully",
42
+ // 完成通知也只给一个 output_file 路径 —— 那个路径是 SDK 转录 jsonl,既在
43
+ // cwd + additionalDirectories 之外(Read 够不着),又明令不许读(会撑爆上下文)。
44
+ // 漏挂 TaskOutput 的后果是主 agent 眼睁睁看着子代理跑完却拿不到结果:实测它
45
+ // 会去 ToolSearch 找 SendMessage,找不到,然后放弃改用自己的记忆硬写。
46
+ // hooks.js 已强制前台(正常路径拿得到报告),这里是兜底的第二条路。
47
+ //
48
+ // 非显式语义:
49
+ // - tools 字段是"可见集合"白名单,不在里面的内置工具会被剥离
50
+ // - 不是 auto-allow(auto-allow 由 permissionMode='bypassPermissions' 已经全
51
+ // 跳)。之前 sdkOptions 同设 allowedTools 是冗余,已删
52
+ // - **Skill 必须显式列出**:SDK `skills:` option 只把 `Skill(<name>)` 注入到
53
+ // `allowedTools`(权限层),**不动 tools 数组**(可见集层)。漏列 Skill =
54
+ // `--tools` flag 显式不含 Skill → CLI binary 把 Skill 工具从 agent 可见集
55
+ // 剥离 → 即使 plugins+skills option 都传对了,agent 也看不到 Skill 工具,
56
+ // 永远调不到任何 SDK skill。SDK 文档 sdk.d.ts:1651 那句 "do not need to
57
+ // add Skill to allowedTools" 只承诺权限层,没承诺可见层。
58
+ export const DEFAULT_TOOL_ALLOWLIST = [
59
+ 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'TodoWrite', 'Bash',
60
+ 'AskUserQuestion',
61
+ 'WebFetch',
62
+ 'Task',
63
+ 'TaskOutput',
64
+ 'Skill',
65
+ // deferred MCP 工具的取 schema 入口(ENABLE_TOOL_SEARCH=true 时生效)。
66
+ // 漏挂 = 延迟加载的工具永远调不到(同 Skill 的可见集陷阱)
67
+ 'ToolSearch',
68
+ ];
69
+
70
+ // 旧式(cwd 根)产物候选 —— canvas.html 列首位,其余兼容 deec72d 之前的
71
+ // e2e smoke / 旧 deskskill-engine 输出。任务模型下产物不在 cwd 根,走
72
+ // listWorkspaceArtifacts(见 detectArtifact)。
73
+ const ARTIFACT_CANDIDATES = ['canvas.html', 'deck.html', 'index.html', 'output.html'];
74
+
75
+ // P0+ s1 C24:流式打字效果(text / thinking 逐 token 推送)。
76
+ // 跟 sdkOptions.includePartialMessages 同步 —— 我们默认开(前端要打字效果)。
77
+ // 启用时 handleAssistantBlocks 跳过 text/thinking blocks(已经从 stream_event 推完,
78
+ // 避免双推),但仍推 tool_use 完整 block(run.delta.tool_use 是入参的权威快照;
79
+ // Edit/Write 另有节流的 run.delta.tool_input 真流式增量,见 handleStreamEvent)。
80
+ export const STREAMING_ENABLED = true;
81
+
82
+
83
+ // ── SDK message → EventBus 翻译层 ──
84
+
85
+ /**
86
+ * 把 SDK 各种 message 类型翻译成 Nodesign 内部事件。
87
+ * SDKMessage union 见 sdk.d.ts:2988(28+ 种 type/subtype 组合)。
88
+ *
89
+ * 翻译策略:
90
+ * - 主流程消息(assistant / user / result):走 handleAssistantBlocks / handleUserBlocks
91
+ * - SDK system subtype 多达 14 种:分派到对应 Events 构造器
92
+ * - 旁路类型(stream_event / keep_alive):noop(前端不需要)
93
+ */
94
+ export function handleSDKMessage(ctx, msg) {
95
+ // 首条 message 含 session_id,记下
96
+ if (msg.session_id) ctx.recordSdkSession(msg.session_id);
97
+
98
+ // 子代理时间轴(2026-07-28):forwardSubagentText 开启后,子代理的消息带
99
+ // parent_tool_use_id 流进主 query。用原型链派生一个 emit 装饰过的 ctx——
100
+ // 该 message 产生的所有事件都盖上 parentToolUseId,前端据此拆时间轴。
101
+ // Object.create 保 AgentContext 的原型方法与共享状态(counters 等)原样可用。
102
+ if (msg.parent_tool_use_id) {
103
+ const parent = msg.parent_tool_use_id;
104
+ // 先确保流式入参 map 已挂在真 ctx 上 —— 否则首次访问发生在派生对象上,
105
+ // map 会写成派生对象的 own property,随派生对象丢弃(原型链只読共享)
106
+ toolInputStreams(ctx);
107
+ // ⚠️ 必须捕获 base 快照:闭包若直接引用 ctx 变量,下面 ctx = child 之后
108
+ // ctx.emit 会解析回 child 自己 → 无限递归(真机爆过 Maximum call stack)
109
+ const base = ctx;
110
+ const child = Object.create(base);
111
+ child.emit = (evt) => base.emit({ parentToolUseId: parent, ...evt });
112
+ ctx = child;
113
+ }
114
+
115
+ switch (msg.type) {
116
+ case 'assistant':
117
+ // BetaMessage 含 content[] (text / thinking / tool_use blocks)
118
+ // STREAMING_ENABLED 时 text/thinking 已从 stream_event 推完,跳过避免双推。
119
+ // 例外:子代理消息(forwardSubagentText 转发)没有对应 stream_event,
120
+ // 跳过会把子代理正文整段吞掉(时间轴就空了)—— 不跳。
121
+ handleAssistantBlocks(ctx, msg.message?.content || [], STREAMING_ENABLED && !msg.parent_tool_use_id);
122
+ break;
123
+
124
+ case 'user':
125
+ // 一般是 tool_result 反馈(agent loop 中 SDK 会回填)
126
+ handleUserBlocks(ctx, msg.message?.content || []);
127
+ break;
128
+
129
+ case 'system':
130
+ handleSystemMessage(ctx, msg);
131
+ break;
132
+
133
+ case 'stream_event':
134
+ // SDKPartialAssistantMessage —— 流增量(includePartialMessages: true)
135
+ // 推逐 token text_delta / thinking_delta 给前端实现打字效果
136
+ if (STREAMING_ENABLED) handleStreamEvent(ctx, msg);
137
+ break;
138
+
139
+ case 'tool_use_summary':
140
+ // SDKToolUseSummaryMessage —— SDK 的 helper model 对刚才那批工具调用写的
141
+ // 一句话总结(Claude Code 侧栏折叠标题就是它)。以前当旁路审计丢掉了,
142
+ // 于是前端只能自己切 thinking 头 60 字当标题。现在转发给前端当分组标题。
143
+ if (msg.summary) {
144
+ ctx.emit(Events.toolUseSummary(msg.summary, msg.preceding_tool_use_ids || []));
145
+ }
146
+ break;
147
+
148
+ case 'tool_progress':
149
+ // SDKToolProgressMessage —— 工具执行 >1s 时定期推(前端可显示"读取中 12s...")
150
+ ctx.emit(Events.toolProgress(msg.tool_use_id, msg.tool_name, msg.elapsed_time_seconds));
151
+ break;
152
+
153
+ case 'prompt_suggestion':
154
+ // SDKPromptSuggestionMessage —— 每轮后 piggyback 预测的下条 prompt
155
+ // 前端 SuggestionChip(C19)渲染
156
+ ctx.emit(Events.promptSuggestion(msg.suggestion));
157
+ break;
158
+
159
+ case 'status':
160
+ // SDKStatusMessage —— 'compacting' | 'requesting' | null
161
+ ctx.emit({ type: 'run.status', status: msg.status });
162
+ break;
163
+
164
+ case 'rate_limit_event':
165
+ ctx.emit({ type: 'run.rate_limit', info: msg.rate_limit_info });
166
+ break;
167
+
168
+ case 'auth_status':
169
+ // 鉴权状态(首次 spawn 时可能出现)
170
+ if (msg.error) {
171
+ ctx.emit({ type: 'run.auth_error', message: msg.error });
172
+ }
173
+ break;
174
+
175
+ case 'keep_alive':
176
+ // SDKKeepAliveMessage —— WS 心跳,不入 EventBus
177
+ break;
178
+
179
+ case 'result':
180
+ // 由外层 for await 捕获处理(finalText / artifactPath / counters)
181
+ break;
182
+
183
+ default:
184
+ // 兜底:未识别的新 type 留个调试痕迹,方便 SDK 升级时发现
185
+ console.warn(`[run ${ctx.runId}] unknown SDK message type:`, msg.type);
186
+ break;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * SDK type:'system' 下的 14 种 subtype 派发。集中放一处便于维护。
192
+ */
193
+ function handleSystemMessage(ctx, msg) {
194
+ switch (msg.subtype) {
195
+ case 'init':
196
+ // 初始化元信息:agents / tools / mcp_servers / model / permissionMode 等
197
+ // model:SDK 看到的是 spoofing alias(如 claude-opus-4-7[1m],让 SDK 信
198
+ // rawMaxTokens=1M 解 Kimi 256k 卡顿,详见 model-context.js)。前端 InfoChips
199
+ // 显示 appModel(kimi-k2.6 真名)才不让用户困惑。
200
+ ctx.emit(Events.systemInit({
201
+ agents: msg.agents,
202
+ tools: msg.tools,
203
+ mcpServers: msg.mcp_servers,
204
+ model: ctx.appModel || msg.model,
205
+ permissionMode: msg.permissionMode,
206
+ skills: msg.skills,
207
+ plugins: msg.plugins,
208
+ claudeCodeVersion: msg.claude_code_version,
209
+ cwd: msg.cwd,
210
+ }));
211
+ break;
212
+
213
+ case 'compact_boundary':
214
+ ctx.counters.compactBoundaries += 1;
215
+ ctx.emit({ type: 'run.compact_boundary', compactMetadata: msg.compact_metadata });
216
+ break;
217
+
218
+ case 'api_retry':
219
+ // SDKAPIRetryMessage(sdk.d.ts:2322):API 请求失败,可重试,将在 retry_delay_ms 后重试。
220
+ // 之前落到 default warn —— 改为 emit 让上层能看到"网络抖了,agent 还在重试",
221
+ // 避免用户以为卡死。
222
+ ctx.emit(Events.apiRetry(
223
+ msg.attempt,
224
+ msg.max_retries,
225
+ msg.retry_delay_ms,
226
+ msg.error_status, // number | null(连接错误时为 null)
227
+ msg.error, // SDKAssistantMessageError union
228
+ ));
229
+ break;
230
+
231
+ case 'files_persisted':
232
+ // SDKFilesPersistedEvent —— agent 写完 file checkpoint 持久化通知
233
+ // FileChanged hook 触发的 file.changed 事件是更直接的;这个仅审计
234
+ ctx.emit(Events.filesPersisted(msg.files, msg.failed));
235
+ break;
236
+
237
+ case 'memory_recall':
238
+ // 自动 memory 召回 —— 前端可显示"recalled from memory"
239
+ ctx.emit(Events.memoryRecall(msg.mode, msg.memories));
240
+ break;
241
+
242
+ case 'task_started':
243
+ case 'task_progress':
244
+ case 'task_updated':
245
+ case 'task_notification':
246
+ // SDK 统一任务消息族(后台 Bash / Workflow 也发 task_*):收口翻译
247
+ // 住 task-events.js —— 只放真子代理进 run.task.*
248
+ handleTaskMessage(ctx, msg);
249
+ break;
250
+
251
+ case 'notification':
252
+ // SDKNotificationMessage —— 系统级 toast(priority: low/medium/high/immediate)
253
+ ctx.emit(Events.notification(msg.key, msg.text, msg.priority, msg.color, msg.timeout_ms));
254
+ break;
255
+
256
+ case 'session_state_changed':
257
+ ctx.emit(Events.sessionState(msg.state));
258
+ break;
259
+
260
+ case 'hook_started':
261
+ ctx.emit(Events.hookStarted(msg.hook_name, msg.hook_event));
262
+ break;
263
+
264
+ case 'hook_progress':
265
+ // hook 执行中 stdout/stderr 流(仅 includeHookEvents: true 时)
266
+ // 前端不需要,旁路日志即可
267
+ break;
268
+
269
+ case 'hook_response':
270
+ ctx.emit(Events.hookResponse(
271
+ msg.hook_name, msg.hook_event, msg.outcome, msg.output, msg.exit_code,
272
+ ));
273
+ break;
274
+
275
+ case 'plugin_install':
276
+ // 插件安装进度(headless mode),不入 EventBus
277
+ break;
278
+
279
+ case 'local_command_output':
280
+ // 本地 slash command 输出(/voice / /usage 等),不入 EventBus
281
+ break;
282
+
283
+ case 'elicitation_complete':
284
+ // MCP elicitation URL 模式完成确认,旁路
285
+ break;
286
+
287
+ case 'mirror_error':
288
+ // SessionStore mirror 失败,旁路(我们没用 SessionStore)
289
+ break;
290
+
291
+ case 'status':
292
+ // SDK 进度/心跳状态 → 转发(前端 header 存活点 + compacting toast 消费;
293
+ // 曾被旁路,是"前端不知道后端死活"的帮凶之一)
294
+ ctx.emit({ type: 'run.status', status: msg.status });
295
+ break;
296
+
297
+ case 'thinking_tokens':
298
+ // SDK 0.3+ 思考进度心跳(~1s 一条,estimated_tokens 累计值)。
299
+ // 转成 run.status 让前端知道"在思考、没死"并可显示进度。
300
+ ctx.emit({
301
+ type: 'run.status', status: 'thinking', tokens: msg.estimated_tokens,
302
+ });
303
+ break;
304
+
305
+ default:
306
+ console.warn(`[run ${ctx.runId}] unknown system subtype:`, msg.subtype);
307
+ break;
308
+ }
309
+ }
310
+
311
+ function handleAssistantBlocks(ctx, content, skipTextThinking = false) {
312
+ for (const block of content) {
313
+ switch (block.type) {
314
+ case 'text':
315
+ // 流式开了 → text 已通过 stream_event 推完,跳过避免重复
316
+ if (!skipTextThinking && block.text) {
317
+ ctx.emit(Events.deltaText(ctx.counters.turns, block.text));
318
+ }
319
+ break;
320
+ case 'thinking':
321
+ if (!skipTextThinking && block.thinking) {
322
+ ctx.emit(Events.deltaThinking(ctx.counters.turns, block.thinking));
323
+ }
324
+ break;
325
+ case 'tool_use':
326
+ // tool_use 不论流式与否都在 assistant 完成时推一次 —— 完整入参的权威
327
+ // 快照(真流式 tool_input 只发抽出字段的增量,别的字段靠这条补全)
328
+ ctx.emit(Events.deltaToolUse(ctx.counters.turns, block.id, block.name, block.input));
329
+ ctx.incrementTool(false);
330
+
331
+ // Phase 1:TodoWrite 工具单独再 emit 一条 todoUpdated。
332
+ // SDK 不会在 type:'system' 里专门推 TodoWrite 状态 —— agent 用工具
333
+ // 写计划时,input.todos 就是完整的 [{ content, status, activeForm }] 列表
334
+ // (sdk-tools.d.ts:530 TodoWriteInput)。
335
+ // tool_use 只够前端展示"调了 TodoWrite",但拿不到结构化的 todo 列表给
336
+ // 计划面板用,所以这里平行 emit 一次 run.todo.updated。
337
+ if (block.name === 'TodoWrite' && block.input && Array.isArray(block.input.todos)) {
338
+ ctx.emit(Events.todoUpdated(block.input.todos));
339
+ }
340
+ break;
341
+ // 其他 block 类型(redacted_thinking / image / document)忽略
342
+ }
343
+ }
344
+ }
345
+
346
+ /**
347
+ * C24:处理 SDK stream_event message(含 BetaRawMessageStreamEvent)。
348
+ * 推逐 token 增量给前端实现打字效果。
349
+ *
350
+ * BetaRawMessageStreamEvent.type 值:
351
+ * message_start / content_block_start / content_block_delta /
352
+ * content_block_stop / message_delta / message_stop
353
+ *
354
+ * text_delta / thinking_delta 逐 token 转发;input_json_delta 只对 Edit/Write
355
+ * 累积 + 节流抽字段(见下方"真流式工具入参"段),其余工具照旧等完整 block。
356
+ */
357
+
358
+ // ── 真流式工具入参(2026-07-28,工作台舞台层代码直播)──
359
+ // input_json_delta 是半截 JSON 碎片,等拼完再发一次的话,模型逐 token 生成
360
+ // new_string 的几十秒里前端只能干转圈。这里对写代码的工具累积缓冲区,节流用
361
+ // partial-json 容错解析累积串,把目标字段相对上次的纯文本增量推给前端
362
+ // (run.delta.tool_input)。转义符跨块断开由解析器兜住;字段单调增长,
363
+ // 万一局部解析短暂回缩就跳过本拍(append 只在变长时发)。
364
+ const TOOL_INPUT_STREAM_FIELDS = {
365
+ Edit: 'new_string',
366
+ Write: 'content',
367
+ };
368
+ const TOOL_INPUT_THROTTLE_MS = 120;
369
+
370
+ function toolInputStreams(ctx) {
371
+ if (!ctx._toolInputStreams) ctx._toolInputStreams = new Map();
372
+ return ctx._toolInputStreams;
373
+ }
374
+
375
+
376
+ function pumpToolInputStream(ctx, st, flush) {
377
+ const now = Date.now();
378
+ if (!flush && now - st.lastEmit < TOOL_INPUT_THROTTLE_MS) return;
379
+ let obj;
380
+ try { obj = parsePartialJson(st.buf, PartialAllow.ALL); } catch { return; }
381
+ if (!obj || typeof obj !== 'object') return;
382
+ const text = typeof obj[st.field] === 'string' ? obj[st.field] : '';
383
+ // file_path 只在确定流完后才取:容错解析会把半截字符串也带出来,第一拍常
384
+ // 截在路径中间(e2e 撞过:抽到项目目录名 → 前端物件寻址指错)。目标字段的
385
+ // key 出现(键序在 file_path 之后)或对象已有第二个键 = 路径已闭合。
386
+ const pathComplete = obj[st.field] !== undefined || Object.keys(obj).length >= 2;
387
+ // 发**工作区相对路径**(2026-08-13):前端拿它当画布物件 id 的路径部分,
388
+ // 而 id = 工作区相对路径。以前原样转发绝对路径,前端靠 `tasks/<任务>/`
389
+ // 这个特征段抠相对部分 —— 那一层拆掉后绝对路径里没有可锚定的标志了。
390
+ const rawFilePath = !st.filePathSent && pathComplete && typeof obj.file_path === 'string' ? obj.file_path : null;
391
+ const filePath = rawFilePath ? toWorkspaceRel(rawFilePath, ctx.workspace?.root?.()) : null;
392
+ const append = text.length > st.sent ? text.slice(st.sent) : '';
393
+ if (!append && !filePath && !flush) return;
394
+ st.lastEmit = now;
395
+ if (append) st.sent = text.length;
396
+ if (filePath) st.filePathSent = true;
397
+ ctx.emit(Events.deltaToolInput(ctx.counters.turns, st.id, st.name, {
398
+ ...(filePath ? { filePath } : {}),
399
+ ...(append ? { append } : {}),
400
+ ...(flush ? { done: true } : {}),
401
+ }));
402
+ }
403
+
404
+ function handleStreamEvent(ctx, msg) {
405
+ const evt = msg.event;
406
+ if (!evt) return;
407
+ // 主线与子代理的 stream 可能并行交错,block index 各自归零 —— 加 parent 前缀区分
408
+ const streamScope = msg.parent_tool_use_id || 'main';
409
+ const streamKey = `${streamScope}:${evt.index}`;
410
+
411
+ // 新 assistant message 开始 —— 该 scope 的 block index 归零,残留流式入参条目作废
412
+ if (evt.type === 'message_start') {
413
+ const streams = toolInputStreams(ctx);
414
+ for (const key of [...streams.keys()]) {
415
+ if (key.startsWith(`${streamScope}:`)) streams.delete(key);
416
+ }
417
+ return;
418
+ }
419
+
420
+ // tool_use 起点 —— content_block_start { content_block: { type: 'tool_use', id, name } }
421
+ // 推 toolUseStarted 让前端立即显示 icon + tool name(status='running')。
422
+ // input 还没流完,等 assistant message 完成后 deltaToolUse 同 blockId update。
423
+ // 体感:agent "想完→开干" 之间几乎没延迟,工具图标第一时间出现。
424
+ if (evt.type === 'content_block_start') {
425
+ const cb = evt.content_block;
426
+ if (cb && cb.type === 'tool_use' && cb.id && cb.name) {
427
+ ctx.emit(Events.toolUseStarted(ctx.counters.turns, cb.id, cb.name));
428
+ const field = TOOL_INPUT_STREAM_FIELDS[cb.name];
429
+ if (field && Number.isInteger(evt.index)) {
430
+ toolInputStreams(ctx).set(streamKey, {
431
+ id: cb.id, name: cb.name, field,
432
+ buf: '', sent: 0, lastEmit: 0, filePathSent: false,
433
+ });
434
+ }
435
+ }
436
+ return;
437
+ }
438
+
439
+ // 跟踪中的 tool_use block 收尾:最后一次 flush + 带 done 标记,清条目
440
+ if (evt.type === 'content_block_stop') {
441
+ const st = toolInputStreams(ctx).get(streamKey);
442
+ if (st) {
443
+ pumpToolInputStream(ctx, st, true);
444
+ toolInputStreams(ctx).delete(streamKey);
445
+ }
446
+ return;
447
+ }
448
+
449
+ if (evt.type !== 'content_block_delta') return;
450
+
451
+ const delta = evt.delta;
452
+ if (!delta) return;
453
+
454
+ if (delta.type === 'text_delta' && delta.text) {
455
+ ctx.emit(Events.deltaText(ctx.counters.turns, delta.text));
456
+ } else if (delta.type === 'thinking_delta' && delta.thinking) {
457
+ ctx.emit(Events.deltaThinking(ctx.counters.turns, delta.thinking));
458
+ } else if (delta.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
459
+ const st = toolInputStreams(ctx).get(streamKey);
460
+ if (st) {
461
+ st.buf += delta.partial_json;
462
+ pumpToolInputStream(ctx, st, false);
463
+ }
464
+ }
465
+ // signature_delta / citations_delta 暂不处理
466
+ }
467
+
468
+ function handleUserBlocks(ctx, content) {
469
+ if (!Array.isArray(content)) return;
470
+ for (const block of content) {
471
+ if (block.type === 'tool_result') {
472
+ const ok = !block.is_error;
473
+
474
+ // C24:tool_result 的 content 可能是:
475
+ // - string(简单文本输出)
476
+ // - block[](含 type:'text' / type:'image' 等多模态 content blocks)
477
+ // P0 时把 image block JSON.stringify 序列化丢到文本里 → 前端显示
478
+ // 一段难看的 base64 字符串。本提取分离:text 部分合并到 output,
479
+ // image 部分单独传 images[] 数组让前端 <img src="data:..."> 渲染。
480
+ let output = null;
481
+ const images = [];
482
+
483
+ if (typeof block.content === 'string') {
484
+ output = block.content;
485
+ } else if (Array.isArray(block.content)) {
486
+ const textParts = [];
487
+ for (const b of block.content) {
488
+ if (b?.type === 'text' && b.text) {
489
+ textParts.push(b.text);
490
+ } else if (b?.type === 'image') {
491
+ // 双格式兼容:
492
+ // - Anthropic content block: { type:'image', source:{ type:'base64', media_type, data } }
493
+ // - MCP CallToolResult ImageContent: { type:'image', data, mimeType }
494
+ // SDK 透传 MCP CallToolResult 时格式不一定转换;只查 source.data 会漏接
495
+ // generate_image 返的图,前端 chat 缩略图就空。
496
+ const imgData = b.source?.data || b.data;
497
+ const imgMime = b.source?.media_type || b.mimeType || 'image/png';
498
+ if (imgData) {
499
+ images.push({ mediaType: imgMime, data: imgData });
500
+ } else {
501
+ textParts.push(JSON.stringify(b)); // 拿不到 data 时留痕不丢数据
502
+ }
503
+ } else if (b) {
504
+ // 未识别 block 类型 → fallback JSON.stringify 留痕(不丢数据)
505
+ textParts.push(JSON.stringify(b));
506
+ }
507
+ }
508
+ output = textParts.length > 0 ? textParts.join('\n') : null;
509
+ }
510
+
511
+ ctx.emit(Events.deltaToolResult(
512
+ ctx.counters.turns,
513
+ block.tool_use_id,
514
+ '<sdk-tool>', // SDK 不在 tool_result 里带 name;前端可以从 tool_use 配对
515
+ ok,
516
+ ok ? output : undefined,
517
+ ok ? undefined : { message: output || 'tool failed' },
518
+ images.length > 0 ? images : undefined,
519
+ ));
520
+ if (!ok) ctx.counters.toolFailures += 1;
521
+ }
522
+ }
523
+ }
524
+
525
+ // ── 产物检测 ──
526
+
527
+ /**
528
+ * 本轮的主产物是哪份(写进 run.metadata.artifactPath)。
529
+ *
530
+ * 2026-07-28 修:原来只探 cwd 根的四个候选文件名,而任务模型(2026-07-28 上线)
531
+ * 把产物搬进了 `tasks/<任务>/`,于是这个函数**自任务模型上线起恒返 null** ——
532
+ * artifactPath 一直是空的,没人发现,因为它不报错只是没值。
533
+ */
534
+ export async function detectArtifact(ctx) {
535
+ const root = typeof ctx?.workspace?.root === 'function' ? ctx.workspace.root() : null;
536
+ if (root) {
537
+ try {
538
+ const artifacts = await listWorkspaceArtifacts(root);
539
+ if (artifacts.length > 0) {
540
+ // 扁平模型:没有"会话名下的任务"了,取第一份(列表序=收集器的
541
+ // 稳定序)。老的 startsWith('tasks/') 优先级从扁平化起就是死代码。
542
+ return artifacts[0].rel;
543
+ }
544
+ } catch { /* 扫不动就回落到旧式探测 */ }
545
+ }
546
+ for (const candidate of ARTIFACT_CANDIDATES) {
547
+ if (await ctx.workspace.exists(candidate)) return candidate;
548
+ }
549
+ return null;
550
+ }