dsh-cli-switch
LLM-provider plugin for DeepSeek Harness: use local AI CLIs (claude / opencode / gemini / cursor / codex) as model backends, hot-switch in the model selector. DSH 的 LLM provider 层插件:本地 AI CLI 当模型后端,一键热切换。
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 20, 2026
- Updated
- Aug 20, 2026
Introduction
dsh-cli-switch
⚠️ 开发中(WIP)· Work in Progress
本仓库处于早期开发阶段:M1(opencode-cli 文本对话)+ M2(claude-cli 完全剥壳)冒烟通过,工具执行链路尚未开放(设计见 ROADMAP M4 专题 A)。欢迎围观、试用、提 issue——但不建议生产使用;接口与行为可能随时变更,恕不另行通知。
DeepSeek Harness 的 LLM provider 层插件:把各家本地 AI CLI(claude / opencode / gemini / cursor / codex…)接成 DSH 的"模型后端",模型选择器里一键热切换。
定位一句话:壳是 DSH 的(agent loop、工具、沙箱、日志、UI 全归 DSH),芯是各家 CLI 里的模型——各家 CLI 在本插件中只保留"思考 + 生成 tool_use"的能力,手和脊髓全部截掉换成 DSH 的。
状态:M1 冒烟通过(2026-08-20:opencode-cli 文本对话 + 不落盘 + T5' 剥壳实证);M2 冒烟通过(同日:claude-cli 完全剥壳——SDK 路线工具往返 + 生产环境 24 工具全可见零泄漏 + 不落盘)。计划见 ROADMAP.md,冒烟证据见 docs/smoke/m1.md 与 docs/smoke/m2.md。
1. 背景与生态位
- DSH(deepseek-ai/deepseek-harness)是 DeepSeek 官方 agent harness,2026-08-13 发布,7 天 16.6 万星,MIT / TypeScript / Cordis 插件架构,"Everything is a Plugin"。
- DSH 官方已在 subagent 层做了 claude-code / codex 集成(
dsh-subagent-claude-code用 Agent SDK、dsh-subagent-codex用 codex app-server)——那是"临时工"模式:one-shot 委派任务给产品、拿回最终答案,一个任务一个进程,官方明确不做流式/续传。 - 本插件做的是 provider 层——"模型脑"模式:产品只当模型通道,持续会话、工具执行、沙箱审批、会话日志、UI 渲染全部是 DSH 的。这个生态位还没人做。
- 已存在的
katsos/dsh-claude-cli(4 星)验证了 claude 单家剥壳可行,本插件是其泛化版本。
2. 核心概念:剥壳 = 截肢 + 接管反射弧
任何完整 agent(有脑有手有循环)塞进 LLM provider 插槽时:
| 部件 | SDK 家族(claude) | ACP 家族(opencode/cursor/gemini) | 处置 |
|---|---|---|---|
| 脑(模型思考 + tool_use 决策) | ✔ | ✔ | 保留——这正是 provider 的职责 |
| 脊髓(自己的 agent loop) | ✘ | ✘ | 砍掉——DSH 的 agent loop 接管驱动(ACP 家族:loop 在 agent 侧,客户端只收通知,见"双层现实") |
| 手(内置工具) | ✘ disallowedTools 按名前缀过滤(R3 实证:'*' 会把我们声明的工具也移除) | ✘ 项目级 opencode.jsonc tools:{...:false} 按名禁用(R1 实证 8 原生全移除) | 剁掉——模型看不到任何原生工具 |
| 记忆/设置/MCP/钩子 | ✘ | ✘ | 清除——统一走 DSH 的会话与凭据体系 |
| 假手(MCP bridge) | + tool_use 流式回客户端(input_json_delta,R3)→ DSH 执行 | + 工具由 agent 服务端执行,客户端只收 tool_call 通知(R4) | 接上——DSH 工具伪装成 MCP server 喂给模型 |
双层现实(M1.5 调研实证)——剥壳有两个层面,两个家族的边界不同:
- ACP 家族(opencode / cursor / gemini)= 工具面可控 + 执行在 agent 侧:原生工具可按名禁用(项目级
opencode.jsonc,R1 实证 8/8 从模型可见面移除),但 MCP 工具由 agent 服务端执行,tool_use 从不回客户端——ACP 规范没有"客户端执行工具并回传结果"的消息类型,这是遥控器设计而非 opencode 特例(R4:opencode 源码级证据——event.ts的handleToolPart()把服务端 ToolPart 状态机翻译成tool_call/tool_call_update通知,客户端纯旁观;gemini/cursor 走官方 ACP SDK 同构受限)。客户端能做的:收tool_call/tool_call_update通知做 wire 级剥壳断言、权限请求自动拒、桥接报错回传。完整工具执行链 = M4 专题 A(SSE bridge +ctx.tools.execute,R2 实证 API 存在)。 - SDK 家族(claude)= 完全剥壳:
input_json_delta把 tool_use 参数逐字流式回客户端(R3 实证),DSH 执行工具、结果回灌模型——本 README 此前的"tool_use 转发回 DSH 执行"只在本家族成立。
工具名单源原则:工具名永远由 DSH 定义(模型只会"看到什么 schema 发什么 tool_use"),不需要映射表/正则——但协议层会加命名空间前缀:MCP 挂载后模型看到的工具名 = <挂载名>_<工具名>(实证:dsh-cli-switch-bridge_fs_read,R1),claude SDK 路线 = mcp__<server>__<tool>(实证:mcp__dsh-bridge__fs_read,R3)→ 客户端按单一前缀规则(非映射表)翻译回 DSH 裸名。翻译回裸名后:未注册工具名 = 剥壳不彻底,处理方式是拒绝 + 明确报错,绝不模糊匹配(那是绕过 schema 校验和 approval 的漏洞)。
3. DSH 侧接口(插件必须遵守)
插件 = Cordis 插件,核心就一个注册动作:
class MyAdapter extends LlmAdapter {
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> { … }
}
export function apply(ctx: Context, config: Config) {
ctx.llm.registerAdapter(['my-provider'], new MyAdapter(…))
}
StreamChunk 协议铁律(官方 cookbook:docs/cookbook/adding-an-llm-adapter.md):
- 流类型:
block-start/text-delta/reasoning-delta/tool-call-delta/block-end/usage/finish usage必须在finish之前发;finish之后什么都不发- 工具
arguments是 raw JSON 字符串端到端;流式片段用argumentsDelta - 块
index按首次出现顺序分配,同一块的每个 delta 复用同一 index - 失败归一化为
finish { kind: 'error' | 'aborted', failure }——错误也是终态 - 凭据用 cordis 原生 schemastery(
!!js process.env.XXX),绝不在代码里读 key 文件 - 无 API key 场景官方认可:"profile 命名 no credential 时通过 provider 自己的 ambient discovery 或 OAuth 认证"——这正是各家 CLI 本地登录态的用武之地
参考实现:packages/llm/llm-deepseek(直接 HTTP+SSE)、packages/llm/llm-pi-ai(包装第三方库)。官方 subagent 包 packages/subagent/subagent-claude-code、packages/subagent/subagent-codex 有进程生命周期与认证的现成代码可借。
4. 各家 CLI 可剥壳性矩阵(2026-08-20 调研结论)
| CLI | headless | 工具禁用 | MCP stdio | ACP | 总评 |
|---|---|---|---|---|---|
| claude (2.1.237 / SDK 0.3.220) | -p | disallowedTools 按名前缀过滤(R3:'*' 会误伤自己声明的工具) | SDK mcpServers 挂 bridge(需 NDJSON 帧,R3) | ❌ 无(Agent SDK 私有流) | ✅ M2 冒烟通过(完全剥壳:工具往返 + 24 工具零泄漏 + 不落盘) |
| opencode (1.18.18) | run --format json | opencode.jsonc tools:{...:false} 按名禁用(R1 实证) | opencode.json mcp;session/new 只收 http/sse 挂载 | opencode acp | ✅ M1 冒烟通过(文本 + 剥壳实证) |
| gemini-cli | -p --output-format stream-json | Policy Engine deny | settings.json mcpServers | --acp(实验性) | ✅ 全符合 |
| cursor | agent -p | ❌ 禁不掉(只读模式兜底) | .cursor/mcp.json | agent acp + request_permission | 🟡 可行 |
| codex (0.147.0) | exec --json | ❌ 无通用禁用 | config.toml mcp_servers | ❌ 无 ACP;codex mcp-server(实验性)可拒审批 | 🟡 部分可行 |
| windsurf | ❌ 无程序化 CLI | — | — | — | ❌ 不可行(已并入 Devin Desktop) |
ACP 是事实标准:opencode / gemini / cursor 全有 ACP(session/prompt 流式 + request_permission 可拒 = 天然的"吐 tool_use 不执行"机制);codex 的 mcp-server 是 ACP 风格(thread/turn)。→ 一个 AcpAdapter 基类吃下四家。claude 走 Agent SDK 私有流,单独一个 adapter。
5. 架构设计
浏览器 Web UI(DSH 原装,零改动)
│ session/event + API Gateway
▼
DSH agent loop(turn/step 驱动、prompt 组装、工具调度)
│ ctx.llm.stream()
▼
┌─────────────────────────────────────────────────────────┐
│ dsh-cli-switch(Cordis 插件,注册多个 provider route) │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ AcpAdapter 基类 │ │ ClaudeSdkAdapter │ │
│ │ (ACP client 骨架) │ │ (@anthropic-ai/ │ │
│ │ ├ opencode route │ │ claude-agent-sdk │ │
│ │ ├ gemini route │ │ query() 流式 + │ │
│ │ ├ cursor route │ │ 剥壳按名前缀(R3 实证) │ │
│ │ └ codex route │ │ + mcpServers 挂 bridge)│ │
│ └─────────┬───────────┘ └───────────┬─────────────┘ │
│ │ 进程生命周期/认证/错误分类 │ │
│ ┌─────────▼───────────────────────────▼─────────────┐ │
│ │ 公共框架:spawnCli / stream 协议翻译 / MCP bridge │ │
│ │ bridge.mjs = DSH 工具 → MCP server(工具名单源) │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │
▼ ▼
opencode 子进程 claude 子进程(SDK)
(ACP stdio) (私有消息流)
6. 设计原则(已定案)
快死是默认,降级必须显式:
- provider 不可用(没装/没登录/版本不兼容)→ 明确诊断(稳定 code + cause chain),不自动换芯——用户意图明确,静默换模型违反 DSH "model-visible means logged" 不变式
- 瞬时故障(网络抖、启动闪断)→ 分类重试(复用
dsh-llm-retry的 retryableCodes:RATE_LIMIT/超时可重试,INVALID_CREDENTIAL/QUOTA 不重试) - 能力差异(某家不流 reasoning、不支持多模态)→ 声明式协商:注册时用
resolveModelInfo()声明能力,UI 据此禁用/标记,运行时只走支持的路——这是"优雅降级"唯一合法形态 - 协议/剥壳错误(未注册工具名、帧损坏)→ 终态,绝不模糊。适用范围:SDK 家族(claude)——tool_use 回客户端,收到未注册工具名(
mcp__前缀翻译回裸名后查表)直接拒绝;ACP 家族——tool_use 从不回客户端(R4),剥壳断言以 wire 级tool_call/tool_call_update通知为准(T5' 模式),权限请求自动拒 + 明确诊断;T5' 实证 bridge 报错后模型可能编造内容(幻觉防护 = M4 专题 A) - fallback 链(高级选项):用户配置显式声明(如
codex-cli → claude-cli → deepseek-official)+ 每次降级写进会话日志(UI 可见)——降级本身变成可重放的事实
7. 开发环境
- DSH 要求:Node 22.19+ / 24+,pnpm 11.7(corepack),
pnpm install+pnpm run typecheck - DSH 源码:
D:\github\free_workspace\deepseek-harness\(tarball 解压,git clone 直连不稳) - 插件安装:
dsh plugin --profile web add ../dsh-cli-switch(profile 层栈启动时读)或--patch一次性覆盖 - 验证:
dsh --profile web --dump-config看插件行;模型选择器(ui-model-selection)里按 provider 分组出现 - Windows 下
opencodeBin解析:npm 全局装的opencode是.cmdshim,spawn 无 shell 直接 ENOENT →resolveOpencodeBin()自动解析为真实.exe(%APPDATA%\npm\node_modules\opencode-ai\bin\opencode.exe、%NPM_CONFIG_PREFIX%/%LOCALAPPDATA%全局路径探测;src/opencode/bin-resolve.ts + 6 用例测试) - 本机 claude 2.1.237 已装(
-p/--mcp-config/--output-format stream-json实锤可用;SDK 0.3.220 工具往返 R3 实测);opencode 1.18.18 已装(C:/Users/qwe13/AppData/Roaming/npm/node_modules/opencode-ai/bin/opencode.exe)
8. 风险与开放问题
- ToS:Claude Code 订阅条款对自动化调用的限制;DSH 官方把产品集成标"生产安装排除"——玩玩可以,商用前查条款
- DSH 兼容性破坏期:官方明确 "THERE WILL BE COMPATIBILITY-BREAKING CHANGES",插件要跟随
- 各家 CLI 版本漂移:codex 0.147.0、SDK 0.3.220 等 pin 版本要定期刷新
- ACP client 端:DSH 只有 ACP server(
packages/acp),client 端要自己实现(协议公开,codex app-server / opencode acp 是参考实现) - Windows 优先:本机 Windows,
claude.exe无 batch shim(官方已验证),子进程生命周期(process-tree 终止)要按 Windows 语义写
9. 参考
- DSH 架构:
deepseek-harness/docs/architecture.md、docs/cookbook/adding-an-llm-adapter.md - 官方产品集成(可借代码):
packages/subagent/subagent-claude-code/、packages/subagent/subagent-codex/、Agent Note2026-08-04-claude-code-and-codex-subagent-backends.md - 先例插件:
katsos/dsh-claude-cli(bridge.mjs 思路) - 调研原始数据:
D:\github\free_workspace\.cache\research\cli_backend\