**[English](README.md) | 中文** # dsh-acp-enhanced 面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)的增强版 [Agent Client Protocol](https://agentclientprotocol.com)(ACP)服务器,为 **Zed** 等 ACP 编辑器设计。它是官方 `@deepseek-ai/dsh-acp` 桥接器的即插即用替代品:官方桥只做纯文本 输出,本桥把 Web GUI 的能力(流式、遥测、模型/权限控制、会话管理、MCP)全部暴露到 ACP 线上。 ## 特性 ### 输出与遥测 - **块级流式 + 推理流式**:文本块与思考过程实时到达(`agent_message_chunk` / `agent_thought_chunk`),取消/重试不留半截输出。在 acp-enhanced 行设置 `streamDeltas: true` 可切换为**逐 token 流式**——回复边生成边渲染(75ms 合并一次 上线),代价是中途重试无法收回已发出的半截文本,会以可见的 `_[stream interrupted — retrying]_` 标记隔开(默认关闭) - **完整遥测**:上下文用量环 + 缓存命中率 / TPS / 输入-输出-推理 token / 工具耗时 / 轮次计数(`usage_update._meta` 携带全量明细) - **图片支持(多模态)**:当 dsh 组合挂载了附件存储(`dsh-base` 默认装配 `dsh-attachment-local`)时,会声明 `promptCapabilities.image` 并把粘贴/ 上传的图片持久化进 harness 附件存储——支持视觉的模型(如 `deepseek-v4-flash-vision-exp`) 可按线序原生读取,图文交替不乱序。旧版栈(无附件存储)自动降级:不声明 image、 收到图片 prompt 明确报错。 ### 模型与权限 - **模型切换**:实时 `provider/model` 目录下拉(按 ACP 规范分组线格式) - **推理强度**:`reasoning_effort` 下拉——仅当当前路由暴露可选 efforts 时出现; 每个模型都会记住它上次使用的强度(按 profile 持久化),切回时自动恢复, 首次切换的模型则回退到它自己的默认值——没有默认值时取第一个可选值, 绝不出现空的 "unknown" 选择 - **权限预设**:read-only / workspace-write / full-access 三种会话模式 - **审批**:工具调用弹出原生 allow-once / reject-once 审批 - **Agent 预设**:每个会话的模型侧组合(工具 + 提示词段)来自 dsh agent-presets 名册。`standard` 为完整编码 agent(默认),`minimal`(极简模式)只有裸 shell + 文件编辑器,**不含** subagent/web/todo/plan 等工具——极简 agent 不会泄漏任何 host 层工具;`code` 与 `cordis` 随 dsh CLI 附带,`~/.dsh/.agent-presets` 下你 自己的预设也会自动出现。通过 `agent_preset` 配置项、`/preset` 命令或 `DSH_ACP_PRESET` 环境变量(会话默认)选择;**仅空会话可切换**(还没跑过对话), 历史记录永远不会横跨两套工具面 ### Zed 深度集成 - **工具卡片**:折叠态即显示一行摘要——`Read <路径>`、shell 命令显示模型自己给出的意图描述 (`description`,Codex 风格,展开可见完整命令)、`Search: <模式>`、 `Fetch: ` 等。卡片正文遵循 ACP 最佳实践:文件编辑渲染为真实 **diff 视图**、 **bash/pwsh 命令渲染为真实终端卡片**(codex-acp 线格式:命令 + 输出 + 退出码 pill 都在终端面板里,告别 raw-JSON 卡片)、其他执行器渲染为高亮代码块并在下方 附输出、涉及文件以**可点击路径**呈现(点击直达); `rawInput` / `rawOutput` 保留在展开区备查,按工具类型渲染图标, 状态机为进行中 → 完成/失败 - **Zed 文件与终端**:`zed_read_text_file` / `zed_write_text_file` / `zed_terminal` 把 文件编辑放进 Zed 的"编辑文件"区(diff + 接受/拒绝)、命令跑在 Zed 真实终端 - **原生表单提问**:`ask_user_question` → `elicitation/create` 表单,选项即点即答; 选项带描述展示,每个带选项的问题附一个"自定义答案"输入框——选项都不合适时可自由输入, 单选时自定义答案覆盖所选、多选时与所选并存(与 dsh 原生提问卡片语义一致) - **Plan 面板**:plan mode 开关 → Zed 底部"规划中"状态条 ### 会话 - **恢复与归档**:`session/load` 恢复历史线程(完整回放);`session/list` 列出线程 归档(带标题、按更新时间排序);`session/close` 释放内存中的会话记录,之后的 `session/load` 会从持久化日志完整恢复;标题实时推送。`session/delete` **有意不广播**—— harness 未声明公开的持久化删除接口(见「兼容性」) - **多根工作区**:`sessionCapabilities.additionalDirectories` 已声明,Zed 不再提示 "This agent doesn't currently support multi-root workspaces",而是把所有工作区根 通过 `session/new` / `session/load` 传入。所有根都会写进系统提示词并在 `session/list` 上回报;沙箱仍以主 `cwd` 为唯一可写根(见已知限制) ### 命令 - **Slash 命令**:输入 `/` 即可见命令列表(`available_commands_update`):`/status` 查看路由与遥测、`/model` 列出或切换模型、`/preset` 列出或切换 agent 预设 (列表以等宽代码块排版,一眼全见),其余(`/compact` `/goal` `/permission` `/plan`…)直通 harness 命令注册表,全部**不经过模型 turn** 即时执行。所有 userInvocable 技能也会作为命令广播,`/ask-matt`、`/code-review`、`/tdd` 等能被 编辑器放行到达桥,技能正文按 dsh-tool-skill 的用户调用方式注入消息。斜杠命令 旁粘贴的图片会作为命令附件随行(例如 `/goal` 目标的参考截图),与 Web 端 composer 的提交方式一致 ### MCP - **MCP servers**:`session/new` 的 `mcpServers` 挂载任意 MCP server(stdio + streamable HTTP),工具以 `mcp____` 注入;失败的 server 不会拖垮会话 ## 效果预览 在 Zed 的 AI Agent 面板中选择 **dsh-acp-enhanced** 后: ## 快速开始 **需要 `dsh ≥ 0.1.5-rc.2`**(`npm install -g @deepseek-ai/dsh@0.1.5-rc.2`):本桥只对应 一条已声明的 harness API 线,不在运行期探测更老的代际。 本包遵循 dsh 官方插件规范(声明了 `dsh.bundle`),安装与官方组合包一致:**一条命令** 完成,自动初始化 profile、安装包、追加 bundle 层,全程无需手写 profile YAML。 ### 安装(2 步) **第 1 步:安装**(从 npm registry,无需下载源码) ```sh dsh plugin --profile acp-enhanced add dsh-acp-enhanced ``` > 开发/改源码时用 `link:` 指向本地 checkout(改动实时生效): > `dsh plugin --profile acp-enhanced add "link:/absolute/path/to/dsh-acp-enhanced"` **第 2 步:注册进 Zed**(在 `~/.config/zed/settings.json` 的 `agent_servers` 里注册; Zed 会用极简 PATH 拉起 agent,因此用随附启动器 `scripts/dsh-acp-zed.sh` 定位 `node`/`dsh`) > **启动器随包发布**,绝对路径取决于第 1 步的安装方式: > - **npm 安装(默认)**:`$HOME/.dsh/profiles/acp-enhanced/node_modules/dsh-acp-enhanced/scripts/dsh-acp-zed.sh`。Zed 不会展开 `~` 或环境变量,请把 `$HOME` 换成你的用户目录(如 `/Users/you`)后写全绝对路径。 > - **`link:` 开发安装**:`<你的 checkout 路径>/scripts/dsh-acp-zed.sh`。 #### 最常见:DeepSeek 官方 API(默认路由) ```jsonc { // ...你已有的设置... "agent_servers": { "dsh-acp-enhanced": { "type": "custom", "command": "/bin/bash", "args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"], "env": { "DSH_ACP_PROVIDER": "deepseek-official", // 官方 provider id "DSH_ACP_MODEL": "deepseek-v4-flash", // 官方模型 id "DSH_ACP_PRESET": "standard" // 可选:agent 预设 id(minimal / standard / code / cordis / 自定义) } } } } ``` > 这两项 env 与包自带 patch 的缺省值一致,**省略也能工作**——显式写上只是让路由意图 > 一目了然。`DSH_ACP_PRESET` 在名册侧默认 `standard`;想让每个新会话从一开始就是 > 某个特定模式就设置它。API key 不必写进 Zed:存入 `~/.dsh/.credentials.yaml` > (`DEEPSEEK_API_KEY`)由 dsh 凭据服务解析即可;启动脚本还会兜底继承正在运行的 > `dsh web` 进程的 key。 可选:固定面板默认项(都可随时在面板里改): ```jsonc "dsh-acp-enhanced": { // ...上面的 type/command/args/env... "default_config_options": { "model": "deepseek-official/deepseek-v4-flash", "agent_preset": "standard", "plan_mode": false, "reasoning_effort": "high" }, "favorite_config_option_values": { "model": ["deepseek-official/deepseek-v4-flash", "deepseek-official/deepseek-v4-pro"] } } ``` #### 扩展:走 OpenAI-Responses 网关(如公司内部模型网关) 同一安装路径,只是 env 换成网关暴露的 provider/model 与它要求的 key 环境变量名: ```jsonc "dsh-acp-enhanced": { "type": "custom", "command": "/bin/bash", "args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"], "env": { "DSH_ACP_PROVIDER": "", // 网关暴露的 provider id "DSH_ACP_MODEL": "", // 网关暴露的 model id "": "" // 网关声明读取的 key 环境变量名 } } ``` > `` 也可以省掉,把 key 存进 `~/.dsh/.credentials.yaml` 统一管理。 Zed 会热重载设置。打开 **AI Agent 面板**(`Cmd+Shift+A`)→ agent 选择器选 **dsh-acp-enhanced** → 输入第一条消息即可:回复实时流式返回,状态栏显示上下文用量, 面板顶部有 Model / Permission preset / Plan mode 配置项与三种模式,线程归档可恢复 历史会话。 本地验证(无需 Zed): ```sh node /scripts/acp-doctor.mjs # bundle + 版本、peer 范围,并真实启动一次 node scripts/acp-client.mjs # 仅限仓库检出:完整 ACP 端到端,期望 ALL CHECKS PASSED DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs # 自定义路由时再传 ``` ### Web 搜索 bridge 自身不携带、也不推荐任何搜索 provider:模型侧 `web_search` 工具走 `web` seam 的 `searchProvider`,往 profile 里挂任意 `ctx.web` provider 即可——带 `dsh.bundle` 的包用 `dsh plugin --profile acp-enhanced add ` 安装,普通包 走用户层 `insert` 挂载(见下节)。你的 dsh 部署里有哪些 provider 是 profile 层的 事,与 bridge 无关。 代价要说清楚:provider bundle 位于**每个 ACP 线程的启动路径**上,一旦加载失败整个 profile 都会挂掉,Zed 侧表现为无输出的卡死。若插件只是新增模型侧工具,优先放进 preset composition(见[保持 profile 最小化](#保持-profile-最小化));而必须配置宿主 `web` 行的 provider 只能待在宿主组合(即 profile)里——那就明确接受这一风险,并在每次改动后重跑 doctor。 ### 管理 profile 的插件 dsh-acp-enhanced 跑在**独立的 profile** 里——`acp-enhanced`,位于 `~/.dsh/profiles/acp-enhanced/`,与 `dsh web` 同处一个 dsh home。被隔离的是**组合** 本身,所以在 这里增删改插件不会影响 web 侧的配置,而凭据、设置、会话与 preset 仍是共享的。 profile 的插件树由三层组合而成,后层修补前层: 1. **bundle 层**:profile `package.json` 的 `dsh.profile.bundles`——模板自带的 `@deepseek-ai/dsh-base` 在前,随后是每个声明了 `dsh.bundle` 的已安装包(如 `dsh-acp-enhanced`),按数组顺序排列。 2. **用户层**:`~/.dsh/profiles/acp-enhanced/cordis.patch.yml`——按 id 定位的行配置 覆写、`disabled: true` 行禁用,以及 `insert` 挂载(无 `dsh.bundle` 的包——如手工 挂载的自写 provider——就靠它装配)。 3. **临时覆盖**:`dsh --profile acp-enhanced --patch extra.yml`。 调整插件集: ```sh dsh plugin --profile acp-enhanced add # 安装;声明 dsh.bundle 的包自动加入层栈 dsh plugin --profile acp-enhanced remove # 卸载;自动退出层栈 dsh plugin --profile acp-enhanced update [package] # 更新一个/全部并 reconcile dsh --profile acp-enhanced --dump-config # 查看组合后的完整树(标注每行来自哪一层) ``` `dsh plugin` 本质是在 profile 目录里转发 pnpm,并在每次运行后按安装状态 reconcile `dsh.profile.bundles`。两个值得知道的推论: - **靠从 `bundles` 里删条目来禁用 bundle 是禁不住的**——包仍是已安装依赖,下一次 `dsh plugin` 运行会原样加回来。想不禁载地禁用某一行,请在用户层按**行 id**(不是 包名,id 可在 `--dump-config` 输出里查)定位: ```yaml - id: mnemon disabled: true ``` - **无 `dsh.bundle` 的包自身不会装配**——它只作为普通依赖安装(带一次性警告),需要 自己在用户层 `insert` 挂载;要改已有行的配置,用 `- id: <行>` + `config:` 覆写—— patch 条目是整行替换、不做合并。 改动在**下一个**进程生效:Zed 为每个 agent 线程拉起一个全新的 `dsh --profile acp-enhanced`,编辑 profile 后新开 agent 线程(或重启 Zed)即可。 #### 保持 profile 最小化 profile 是一个**单一故障域**:`cordis-plugin-loader` 会等待每个条目,并把第一个 reject 原样抛出,因此只要有一行加载失败,整棵插件树就会中止——进程甚至可能先正常应答 ACP `initialize` 再立刻退出,客户端只会表现为无输出的卡死,而不是报错。 把 `dsh.profile.bundles` 控制在这两行以内,它们的版本不可能与启动它的 CLI 不匹配: ```json "bundles": ["@deepseek-ai/dsh-base", "dsh-acp-enhanced"] ``` `@deepseek-ai/dsh-base` 随 CLI 一起发布,版本天然等同于启动它的 CLI;其他任何 bundle 都是 第三方,其依赖闭包可能漂移。额外插件请挂到「坏了只废掉一个 preset」的位置: - **只新增模型侧工具/命令的插件** → 把行写进某个 preset composition。用户 preset 放在 `$DSH_HOME/.agent-presets//`(组合写 `agent.cordis.yml`,选择器里的名称写 `preset.yml`);roster 会自动发现,ACP 的 `agent_preset` 下拉也会列出。组合加载失败的 preset 只会被标记为 broken 并从列表里剔除,不会拖垮进程。 - **需要配置宿主服务的插件**(例如要覆写宿主 `web` 行 `searchProvider` 的搜索 provider) → 它属于宿主组合,也就是 profile。这是有意的取舍:接受启动路径上的风险,并在每次改动 后重跑 doctor。 改动后先验证再信任: ```sh node /scripts/acp-doctor.mjs # bundle 与版本、peer 范围,并真实启动一次 dsh --profile acp-enhanced --dump-config # 每一行来自哪一层 ``` ## 兼容性 同一个桥只对应**一条** harness API 线:**dsh ≥ 0.1.5-rc.2**(peer 范围 `^0.1.5-rc.2 || ^0.1.6-alpha.1`)。该范围内的**两条线**每次 CI 都会做真实启动验证——握手、 profile settle 与真实 `session/new`——另有跨代链接检查。桥只消费 harness **已声明**的表面: `docs/capability-seams.md` 里的服务、`docs/event-producer-consumer.md` 里的事件、以及已发布包的导出。 `scripts/api-surface-check.mjs` 会对其他一切报错(CI 的阻塞步骤), 运行期也不再有任何代际探测——没有版本开关,没有鸭子类型探测服务形状。 ### 支持策略 | 桥版本 | 支持的 dsh 线 | 变化 | |---|---|---| | **0.9.x** | `^0.1.5-rc.2 \|\| ^0.1.6-alpha.1` | 只消费已声明表面;下限 0.1.5-rc.2;移除 `session/delete` | | 0.8.x | `^0.1.0-rc.6 … ^0.1.6-alpha.1`(未发布) | 0.1.3+ 实时 seam;0.1.5 持久化 handle API | | 0.7.x 及更早 | ≤ 0.1.2-rc.1 | 运行期同时探测两代 | 这张表背后的规则: - **新的 dsh API 线对应一次新的桥发布,而不是把运行期探测写得更宽。** 0.7.x 正是靠探测吞下 0.1.1 → 0.1.5,也正是它悄悄腐烂的原因。 - **下限只随桥的 minor 移动,且绝不静默**:CLI 低于范围时启动器会在启动前告警,doctor 会以 `RESULT FAIL — CLI too old` 停下。 - **放弃某条线的方式是发布一个明确这么说的桥**;旧线留在 `feat/dsh-0.1.3-plus-support` 分支上, 供无法迁移的用户使用。 - **在下一条线发布之前就盯住它**:定时 `canary` workflow 会安装 `alpha` dist-tag 并跑表面守卫、 链接检查与启动冒烟,因此破坏性变更表现为 canary 变红,而不是用户侧故障。 ### 0.9.0 的破坏性变更 | 变更 | 影响 | 中招了怎么办 | |---|---|---| | 下限提升到 dsh **≥ 0.1.5-rc.2** | 更老的宿主在挂载期就以具名错误失败,而不是静默降级 | 升级 CLI(`npm install -g @deepseek-ai/dsh@0.1.5-rc.2`),或留在 `feat/dsh-0.1.3-plus-support` 分支(≤ 0.1.2-rc.1) | | **移除 `session/delete`** | 不再广播该能力,也永不删除已持久化的会话——harness 未声明公开的持久化删除接口 | 文件仍在 `$DSH_HOME/sessions///`,确有需要请手工删除。上游已有 issue 追踪公开删除 API | | 启动器**不再改写 `DSH_HOME`** | ACP profile 在启动器所处的 home 中启动(`${DSH_HOME:-$HOME/.dsh}`),与 `dsh web` 共享凭据、设置、会话与 preset | 之前用的是隐式隔离的 `~/.dsh-acp`?在 Zed 的 `agent_servers.env` 里显式指回它(`"DSH_HOME": "/.dsh-acp"`),或迁回共享 home | | `assistant/chunk` seam 移除 | 实时流只剩 `agent/assistant-stream`(下限已覆盖该代) | 升级 CLI;完全不发流的宿主仍由已提交的 `assistant/message` 兜底 | ### 从已发布的 ≤ 0.7.0 升级 npm 上的 `latest` 是 **0.7.0**,属于 0.1.3 之前的 API 线,因此桥和 CLI **必须一起动**——只升一半, 两种顺序都会坏: | 顺序 | 结果 | |---|---| | 先升 CLI,桥留在 0.7.0 | profile 能启动、`initialize` 也成功,但**每个 `session/new` 都失败**(Internal error:`tool-subagent: modelSelectionSettings requires … in the Host scope`)。我们无法给出任何提示——那份桥代码已经装好了;而且 0.7.0 既没有 `agent/assistant-stream` seam 也没有 `assistant/message` 兜底,回复同样渲染不出来 | | 先升桥,CLI 留在旧版 | profile 在加载期就死(`… subpath './model-selection-settings' is not defined by "exports"`)。启动器会在它**之前**向 stderr 告警,`scripts/acp-doctor.mjs` 则以 `RESULT FAIL — CLI too old` 直接停下 | | 两者一起升 | 受支持的状态 | 升级清单: 1. `npm install -g @deepseek-ai/dsh@0.1.5-rc.2`(或上面 peer 范围内的任意版本)。 2. `dsh plugin --profile acp-enhanced add dsh-acp-enhanced@0.9.0`。升级桥是显式动作:profile 里的依赖 是对 0.x 的 caret,所以 `dsh plugin update` **不会**自行把你带到新的 minor。 3. 以前是从检出目录启动、或设过 `DSH_PATH`?旧启动器会自行切到 `~/.dsh-acp`,现在不会了。请在 Zed 的 `agent_servers.env` 里设 `DSH_HOME=<那个 home>`,或在默认 home 里重建 profile。启动器若在那里 发现 profile,会主动提示。 4. 以前照旧 README 在 profile 用户层里塞过 `subagent-model-selection-settings`?把它删掉:现在由桥的 patch 提供该行,重复 id 会让启动中止。`scripts/init-acp-home.sh` 会自动清理;启动器会告警,doctor 会点名该 id。 5. 确认 profile 里的第三方 bundle 支持 0.1.5(`dsh-free-search` ≥ 0.4.24 已验证)——profile 是单一故障域。 6. 重启 Zed(或新开一个 agent 线程);先用 `node /scripts/acp-doctor.mjs` 验证整条链路(它现在连 开线程都会实测)。 ### 一个 home 只跑一个 CLI 代际 `$DSH_HOME/profiles/node_modules` 是同 home 下所有 profile 共享的**同一个**依赖闭包, dsh 每次启动都会把它 heal 成最后启动的那个 CLI。因此: > 这是 **0.1.5 线**的行为。到 0.1.6-alpha.2,这个共享闭包已完全不存在(harness 从 CLI 自身 > 的安装位置解析;profile 的 `node_modules` 只放外部插件),所以启动器的漂移检查是「按线」的, > 路径消失时会静默跳过。 - **不要让两个 CLI 代际同时跑在一个 home 下。** 第二次启动会在第一个进程运行期间翻转闭包, 那个进程随后会惰性地解析到不匹配的模块。启动器会把闭包里的 `dsh-agent` 版本与即将启动的 CLI 对比,不一致时向 **stderr** 告警——遇到这种启动后,请重启该 home 下其他 dsh 进程 (`dsh web` 等)。 - **逃生阀是 CLI,不是 home。** 用 `DSH_PATH=`(或下面的仓库锁定)指定启动哪个 dsh: 启动器只决定*用哪个 dsh*,绝不决定*用哪个 home*。 当前解析结果随时可查: ```sh node scripts/compat-check.mjs # 仅限仓库检出:分别安装 0.1.5-rc.2 与 0.1.6-alpha.2 两套,逐一导入本桥 node /scripts/acp-doctor.mjs # CLI 与闭包版本、bundle 列表,并真实启动一次(随包发布) ``` ### 开发检出:仓库锁定 CLI + 共享 home 启动器**从检出目录**(`link:` 安装)运行时,按以下顺序解析 dsh CLI: 1. `$DSH_PATH` —— 显式指定的 dsh 二进制,或其 `node_modules/.bin/dsh` 内含 dsh 的目录 2. 仓库锁定的 CLI —— `/node_modules/.bin/dsh`(本包的 `@deepseek-ai/dsh` devDependency,当前 0.1.5-rc.2) 3. 全局兜底 —— PATH / npx 缓存 / npm 前缀 里的 `dsh`(未 `pnpm install` 的全新检出退化为它) 命中任何一个,profile `acp-enhanced` 都在**启动器所处的 home**(`${DSH_HOME:-$HOME/.dsh}`)中启动。 home 永不被改写。若想让桥跑在自己的依赖闭包上,请另建一个 home 并显式指过去: ```sh DSH_ACP_HOME=~/.dsh-acp scripts/init-acp-home.sh # 可选、幂等:创建并填充一个独立 home # 然后在 Zed 的 agent_servers env 里: "DSH_HOME": "/Users/you/.dsh-acp" ``` 独立 home 是明确的可选项,不是默认值:profile 必须存在于启动器实际使用的 home 里,否则启动器 会以 127 退出,并打印出创建它的那条 `dsh plugin … add link:` 命令。`init-acp-home.sh` 会逐字移植旧 profile 的用户层行、复制凭据/设置、关闭 DeepSeek 插件清单上报,并从用户层清掉遗留的 `subagent-model-selection-settings` 行——该宿主行现在由 bridge 的 bundle patch 插入,再留一份会以 `duplicate loader entry id` 中止启动。 两种 home 都把会话持久化在 `$DSH_HOME/sessions///session.jsonl.zstd`;默认不在 home 之间 拷贝任何东西,因为默认 home 的目录里还有全部 web profile 会话。迁移既有环境时请加 `--copy-sessions`(或直接执行脚本打印的 `rsync`)。 ## 故障排查 先跑 doctor:它会完全按 Zed 的方式启动一次 profile,并指出失败层、出问题的 bundle 与修法。 ```sh node /scripts/acp-doctor.mjs # 已安装副本 node scripts/acp-doctor.mjs # 仓库检出(npm run doctor) node /scripts/acp-doctor.mjs --profile --home --timeout 60000 ``` 它会打印 CLI 与版本、home、profile、每个 bundle 及其版本、支持的 peer 范围与共享闭包版本, 然后把启动失败归入三层之一: | 层 | `dsh` stderr 里的特征 | 含义 | 修法 | |---|---|---|---| | **link-time** | `does not provide an export named …`、`SyntaxError: The requested module …` | 启动的 CLI 闭包无法满足本桥的某个 import | 见 doctor 的 `LAYER link-time`:对齐代次——重启该 home 下其他 dsh 进程(共享闭包会愈合到最后启动的那个 CLI),或用 `DSH_PATH=<匹配的 dsh>` 锁定本启动器 | | **mount-time** | `failed to apply loader entry …`、`… requires … in the Host scope`、`duplicate loader entry id: …` | loader 拒绝了某一个条目并向上抛出,整棵插件树因此中止 | doctor 会打印 `SUBJECT <条目> (<模块>)`——补装缺失模块、在用户层禁用该行(`- id: <条目>` + `disabled: true`),或把 `dsh.profile.bundles` 收敛为 `@deepseek-ai/dsh-base` + `dsh-acp-enhanced`;若为重复 id,请从用户层删除该行(它归 bundle patch 所有) | | **run-time** | 握手成功后出现 `… is not a function` | 桥调用到了该 CLI 代次不提供的 harness 服务方法 | `npm install -g @deepseek-ai/dsh@<支持范围内的版本>`(见[兼容性](#兼容性)) | 启动器在 Zed 启动过程中会把同样三类特征翻译到 **stderr**(stdout 是 ACP 协议线), 所以 agent 日志里已经带有失败层与修法。 | 症状 | 定位 | 处理 | |---|---|---| | Zed 卡死无输出、线程始终不应答 | `node /scripts/acp-doctor.mjs` | 会打印 `BOOT FAILED` 与 `LAYER`/`SUBJECT`/`FIX`,照 `FIX` 做即可。先应答 `initialize` 再立刻退出的 profile 也会被如实报出 | | `exec: dsh: not found`(status 127) | `which dsh` | 用随附 `dsh-acp-zed.sh` 启动器(自定位 node/dsh),或安装 CLI | | `no API key for provider route "xxx"` | `ls -l $DSH_HOME/.credentials.yaml` | 写入 `~/.dsh/.credentials.yaml`,或在 agent_servers 里设 `env.DEEPSEEK_API_KEY` | | `SyntaxError: … 'PresetMountError'` | agent 日志里的桥版本 | 你在 0.1.5 宿主上跑 0.9.0 之前的桥副本——升级本包 | | `modelSelectionSettings requires … in the Host scope` | `dsh --profile acp-enhanced --dump-config \| grep subagent-model-selection` | `standard` preset 需要的宿主行缺失——该行由 bridge 的 bundle patch 提供,请重装/升级 bridge(`dsh plugin --profile acp-enhanced add dsh-acp-enhanced`),并检查用户层没有把它 `disabled: true` | | `duplicate loader entry id: <行>` | doctor 会打印 `LAYER mount-time` 与该 id | 两层都插了同一行。请从 profile 用户层(`$DSH_HOME/profiles/acp-enhanced/cordis.patch.yml`)删掉它——这类宿主行归 bundle patch 所有;`scripts/init-acp-home.sh` 会自动清掉遗留的 `subagent-model-selection-settings` 副本 | | 宿主升级后旧线程变空白 | `ls $DSH_HOME/sessions` | 会话存放在 `$DSH_HOME/sessions//`;把旧 home 的历史拷进来(`scripts/init-acp-home.sh --copy-sessions`)即可继续 | | 无法切换模型 | `ACP_DEBUG=1 dsh --profile acp-enhanced`,然后尝试切换 | 携带的 `reasoning_effort` 在目标模型上不受支持:本桥按模型记住上次使用的强度(随 profile 持久化),会回退到该模型默认值而不是让切换失败。另检查路由是否真实——幽灵 provider 会被过滤,只广播 `config.provider` 的模型 | | 上下文用量不显示 | 线程里执行 `/status` | 选到了不可路由的"幽灵 provider";确认 profile 的 provider 指向真实路由 | | 轮次以 usage 结束但**面板没有回复文本**(空白) | `ACP_DEBUG=1`,看是否有 `agent/assistant-stream frame=chunk` | 0.9.0 起唯一的实时 seam 是 `agent/assistant-stream` 帧,某个 step 完全没有上线文本时由已提交的 `assistant/message` 兜底。有帧却无文本 = 客户端渲染问题;完全没有帧 = 正在走兜底路径(桥太旧就升级) | | 改了插件却不生效 | profile `cordis.patch.yml` 的 mtime | 改动只在**下一个**进程生效:新开 agent 线程(或重启 Zed) | | 需要详细诊断 | — | `ACP_DEBUG=1`(stderr 生命周期 trace)与 `ACP_LOG=/tmp/acp.jsonl`(逐事件 JSONL,带耗时) | ## 开发 ```sh pnpm install # 安装开发依赖(仓库锁定 CLI 与测试脚本) node scripts/compat-check.mjs # 支持线上的链接检查(0.1.5-rc.2 / 0.1.6-alpha.2 临时安装) node scripts/api-surface-check.mjs # 公开表面守卫:不得使用未声明的 harness API(CI 阻塞步骤) node scripts/pack-check.mjs # 包完整性:入口文件、权限位、所引用文件是否都随包发布(CI 阻塞步骤) node scripts/acp-client.mjs # 端到端冒烟(需要 API key) node scripts/acp-client-tools.mjs # 客户端工具测试(模拟 Zed 的 fs/terminal/elicitation/plan) node scripts/acp-mcp-test.mjs # MCP 挂载测试(无模型调用) node scripts/acp-smoke-keyless.mjs # keyless 冒烟(CI 用) node scripts/acp-resume-test.mjs # 会话恢复测试 node scripts/codec-image-test.mjs # 图片编解码单元测试(无网络,假 store) node scripts/terminal-codec-test.mjs # 终端卡片编解码单元测试(无网络) node scripts/replay-order-test.mjs # 重放/回退的分块顺序:思考块先于它产出的回复(无网络) node scripts/acp-image-e2e.mjs # 图片能力端到端(vision 模型段需 API key) node scripts/acp-message-fallback-test.mjs # 实时 seam + assistant/message 回退:seam 确实触发且回复恰好到达一次 node scripts/acp-launcher-test.mjs # 启动器契约:home 不被改写、代次漂移告警、启动失败翻译 node scripts/acp-doctor.mjs # 真实启动一次 profile,指出失败层与出问题的 bundle scripts/init-acp-home.sh # 可选:引导**独立** home(启动器不会自行切过去) ``` harness 包的 devDependency 与锁定的 `@deepseek-ai/dsh` CLI 声明相同的 range(如 `^0.1.5-rc.2`),让仓库依赖树与全新 CLI 安装解析出同一个连贯家族——在此用精确 patch 锁定、与 CLI 的 range 闭包混存会得到分裂闭包(同名包两个版本),profile 启动时报 export-not-found。改这些锁定后务必整体重建 lockfile(`rm -rf node_modules pnpm-lock.yaml && pnpm install`):原地增量安装既会留下污染 profile heal 的残留 store 条目,还会保留 lockfile 里的陈旧 peer 解析——从 0.1.2-alpha.2 原地升到 0.1.2-rc.1 时,rc.1 各包的 snapshot 里仍挂着 `dsh-session-persistence@0.1.2-alpha.3`(旧代 peer),boot 与 session/new 全部通过,直到第一个 turn 才以 `TypeError: Cannot read properties of undefined (reading 'length')`(PersistenceCoordinator)崩掉。`pnpm-workspace.yaml` 放行 了 CLI 闭包的构建脚本(node-pty prebuild、koffi)——仓库 CLI 启动 profile 时它们就是 运行时依赖。 ## 已知限制 不支持音频附件(不声明 audio 能力)、文本默认按块粒度流式(`streamDeltas: true` 可切换为逐 token 流式,见「特性」)、每会话同时一个 in-flight prompt。MCP 支持 stdio 与 streamable HTTP(不声明 legacy SSE / `acp` 传输)。 `session/fork` / `session/resume` 未实现(不声明能力,合规客户端 不会调用)。`session/delete` 同样不广播:harness 未声明公开的持久化删除接口,因此本桥 永不删除已持久化的会话(见「兼容性」)。 多根工作区已声明、模型可见所有根,但 dsh 沙箱策略每会话只解析**一个可写根**(主 `cwd`,即 `session.header.cwd`),本地沙箱也只为该根开放写权限。读操作在所有根均可 用;`workspace-write` 下对附加根的写入会先被拒绝、需升级/批准,`danger-full-access` 下所有根均可写。真正的多根写支持需改 dsh 核心(`dsh-sandbox-policy` / `dsh-sandbox-local` 需要根列表而非单根)。 Agent 预设接管了模型侧相关行:自带 `cordis.patch.yml` 会禁用 preset 拥有的 dsh-base 行(tool-bash/fs/subagent/todo/web/…——与官方 dsh-web-app/tui 清单逐行一致,仅少 `hmr`;清单保持跨代通用:某一代没有的行会被 patch applier 告警并跳过),并挂载 `agent-presets` 名册(默认 `standard`;`code`/`minimal`/`cordis` 随 dsh CLI 附带,`~/.dsh/.agent-presets` 下的自定义预设目录自动收录)。bundle 自带 patch 会自动装配(package.json `dsh.bundle.patch`)——**不要**把它复制进 profile 的用户层 `cordis.patch.yml`,否则 loader 在启动时因重复 entry id 拒绝装配。**升级** 一个已有自定义用户层 patch 的 profile 时,用户层只保留你自己的定制行(例如 acp-enhanced 行的 `includeAllProviders: true`,同时 restate provider/model/preset—— patch 条目是整体替换、不做合并)。升级前创建的会话恢复时会落到名册默认预设上。