@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.
- package/README.md +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +22 -22
- package/server/api/assets/notes.js +144 -144
- package/server/api/assets.js +896 -896
- package/server/api/board.js +57 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +464 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1024 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +339 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +155 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +103 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +110 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
- 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
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +60 -60
- package/server/lib/board-kind-sizes.js +62 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-store.js +640 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +54 -54
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +36 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
- package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
- package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
- package/web/dist/index.html +17 -17
- 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
|
+
}
|