Skip to main content
Glama

dsh-bridge

让 MCP 客户端(Codex、Claude Desktop、Cursor 等任何标准 MCP client)驱动本机 DeepSeek Harness 的 dsh web 会话:创建或接管命名会话、下发任务清单、异步轮询、按轮取回可审计的执行结果。

dsh web 自带一个本机 /api,但没有 MCP 接口;dsh 也不需要在 Codex 里装插件。这个仓库补的就是中间那一层:一个 stdio MCP server,把 /api 翻译成 9 个 agent 好用的工具。

验证环境:@deepseek-ai/dsh@0.2.1-rc.2、Windows 11、Node 24。接口约定与被版本升级打断后的重建步骤见 docs/API-CONTRACT.md。

为什么需要它

把 dsh 当成第二个执行工位而不是自己的子上下文:它不共享你的对话历史,只看到你写给它的那段任务文本;它是异步的,"已接受"不等于"已完成";它改的是它自己工作区里的文件。所以这个桥接把三件事固化成接口:

  1. 书面交接:任务必须以清单形式下发(目标 / 边界 / 验收标准 / 交付物)。

  2. 可复核的完成判据:只有 state:"completed" 算完成,见 docs/ORCHESTRATION.md。

  3. 边界约束:可选模型集合由配置预先限定,越权请求在下发之前就被拒绝。

Related MCP server: dsh-orchestrator

安装

要求 Node >= 18.14(headers.getSetCookie())。

git clone <this-repo>
cd dsh-bridge
npm install
npm test                 # 离线契约测试:无需 dsh、无网络、不消耗任何额度

接到 MCP 客户端

以 Codex 为例,写入 ~/.codex/config.toml:

[mcp_servers.dsh]
command = 'node'
args = ['/absolute/path/to/dsh-bridge/bridge.mjs']
startup_timeout_sec = 60     # 桥接可能会自己拉起 dsh web,冷启动约 10s
tool_timeout_sec = 45        # 与 dsh_wait 的 maxSeconds 对齐

[mcp_servers.dsh.env]
DSH_PORT = '3080'
DSH_BRIDGE_STATE = '/absolute/path/to/dsh-bridge/.state'

Claude Desktop 等设备上同一段配置的 JSON 形式(mcpServers.dsh.command/args/env)即可。

改完 bridge.mjs 要重启客户端:MCP 子进程加载的是启动时的代码,编辑不会热更新。

先让 dsh web 跑起来

桥接需要一次性启动 token,而它只出现在 dsh web 的 stdout,不落进 dsh 自己的任何文件。两种做法:

# A. 人工启动,并把 stdout 落到桥接读得到的位置(生命周期自己掌握)
powershell -File start-dsh-web.ps1

# B. 什么都不做:端口空闲时桥接会自己启动实例,stdout 写到同一个捕获文件

DSH_BRIDGE_STATE 目录下的 dsh-web.out 是唯一的 token 来源,.state/ 已在 .gitignore 里。

拓扑上建议只跑一个 dsh web 实例:两个实例并发写同一个 ~/.dsh/storages/workspace.json 的行为未经验证。

一轮标准动作

dsh_models                       # 需要指定模型时先看它:只列被策略允许的选项
dsh_sessions {cwd}               # 该工作区已有哪些会话
dsh_launch  {alias, cwd, task}   # 新阶段 → 新别名,立即返回句柄
   或 dsh_send {alias, task}     # 同一阶段追加一轮(复用同一会话)
dsh_wait   {alias, maxSeconds: 45}   # 反复调用直到落定
dsh_result {alias}               # 验收载荷:助手文本 + 产生它的工具调用 + 落盘文件
dsh_status {alias}               # 模型、token 用量、上下文压力、待答问题
dsh_cancel {alias}               # 取消在跑的一轮
dsh_adopt  {alias, sessionId, cwd}   # 接管 web UI 里已经开着的会话

state 只有四个取值:

值

含义

该怎么处理

completed

本轮 turn/end.reason.kind == completed

可以验收,仍建议抽查产物

error

本轮以错误结束,真因在 error.{code,message}

原文上报,不要静默换模型重跑

running

还在跑

继续 dsh_wait,不要拉长单次调用

idle

任务已被接受,但本轮没有记录任何 turn

按"未验证"处理,不能报完成

两个反复出现的陷阱已经做进实现里:session/prompt 返回的 accepted:true 只是受理;session/list.running 由 true 变 false 不区分成败。

工具参考

工具

参数

说明

dsh_models

—

实时模型目录 ∩ 允许清单,含 default / routableProviders / failures。不发模型请求

dsh_launch

alias, cwd, task, phase?, group?, dryRun?, model?, reasoningEffort?

建会话(默认落进该路径对应的侧边栏分栏)、pin 模型、下发首轮。dryRun 只建会话不发任务

dsh_send

alias, task, model?, reasoningEffort?

同一会话的下一轮

dsh_adopt

alias, sessionId, cwd, phase?, ...

接管已存在的会话;不能改它所在的分栏

dsh_wait

alias, maxSeconds<=45

盯到本轮落定或时间窗结束

dsh_status

alias

立即快照,含 token 用量与上下文压力

dsh_result

alias, messages<=40

本轮验收载荷(默认只看本轮)

dsh_sessions

cwd?

列出桥接拥有的别名;给了 cwd 才返回该工作区的活会话(全量列表有 90+ 条)

dsh_cancel

alias

请求取消,返回 {accepted};结果要用 dsh_wait 复查

别名规则:^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$,会话 id 是 dsh-<alias>。cwd 必须是存在的绝对路径;Windows 上桥接会把正斜杠统一成反斜杠,因为 dsh 逐字登记路径,C:/x 与 C:\x 会被当成两个工作区。

模型策略

模型可选集合由配置预先约束,桥接是唯一同时看得见"调用方要什么"和"dsh 允许什么"的位置。

变量

默认

说明

DSH_ALLOWED_MODELS

空=不限制

文法 provider/model[:effort|effort][, ...]。写了但解析不出条目会在启动时直接失败,不会退化成全部拒绝或全部放行

DSH_DEFAULT_MODEL

空

调用方不传 model 时 pin 的 provider/model

DSH_DEFAULT_EFFORT

空

与上一行配套的推理强度

行为要点:

  • 校验是纯预检:别名、路径、模型、推理强度任何一项不合规,请求在发出任何 /api 调用之前就被拒绝 —— 不冷启动 dsh、不建会话、不写账本。

  • 缺省值与允许清单冲突时,错误信息会标明冲突来自 DSH_DEFAULT_MODEL/DSH_DEFAULT_EFFORT,而不是让人误以为是调用方写错。

  • 模型目录在 dsh 启动后异步加载。桥接在 pin 之前会等目标 provider 出现(这个等待不消耗额度),因此自启动路径不会因为目录尚未加载完成而空耗一轮。

  • dsh 的目录条目不声明各模型支持哪些推理强度,所以强度合法性只能由 dsh 判定(非法值报 session/model-unavailable)。

其余环境变量

变量

默认

说明

DSH_HOST / DSH_PORT

127.0.0.1 / 3080

localhost 与 127.0.0.1 是两个不同的 audience,cookie 不通用,全程只用一种写法

DSH_BRIDGE_STATE

<包目录>/.state

会话账本、token 捕获、审计日志,以及 dsh 子进程的 cwd(run/,第三方插件的缓存会落在那里)

DSH_ATTACH_OUTFILE

空

指向"已由别处捕获的 dsh web stdout",进入 attach 模式,不再自己启动实例

DSH_API_TIMEOUT_MS

15000

单次 /api 调用上限,超时重试一次

DSH_POLL_MS

3000

轮询间隔

DSH_WATCH_MS

40000

dsh_launch/dsh_send 内部盯轮时间,必须小于客户端的 tool_timeout_sec

DSH_CATALOG_WAIT_MS

15000

等待 provider 进入模型目录的上限

DSH_BOOT_TIMEOUT_MS

90000

实例启动等待上限

DSH_LOG_MAX_BYTES

2000000

审计日志滚动阈值(保留一份 .1)

DSH_NO_SPAWN

空

1 时若端口无人监听就直接报错,绝不启动实例

DSH_AUTO_GROUP

空

0 关闭自动分栏(单次调用也可传 group:false)

DSH_NODE / DSH_PKG_DIR

自动探测

覆盖 node.exe 与 @deepseek-ai/dsh 的安装位置;CLI 入口读该包 package.json 的 bin 字段

DSH_TESTED_VERSION

0.2.1-rc.2

版本闸门,不一致时预检报警

会话账本

DSH_BRIDGE_STATE/sessions.json 记录每个别名的会话 id、工作区、分栏、每轮的 requestId 与基线序号。它是"下发过一次"的可审计记录,也是跨进程共享的:一个客户端会话会拉起很多短生命周期的桥接进程,因此写入走 O_EXCL 锁 + 临时文件 rename,持久化失败会抛出错误而不是静默。

  • 账本按 DSH_BRIDGE_STATE 隔离:不同目录的别名互不可见。

  • 只存句柄,不存执行结果 —— 审计载荷一律回 dsh 重新读取。

  • 一个别名一个归属:并发写提供原子性,不提供跨进程的合并语义,同一别名不要并行发两轮。

侧边栏分栏

分栏由 workspace 的 sessionIds 成员关系决定,与 cwd 是两套独立机制:用 cwd 建的会话落在 Ungrouped,用 workspaceId 建的会进对应项目分栏且 cwd 不变。workspace/create {path} 是幂等的,因此兼作 path → workspaceId 的解析器,桥接据此自动分栏。

分栏只能在创建时决定:把已存在的会话移进 workspace 会被 dsh 拒绝(the session is not accounted)。dsh_adopt 因此只能接管,不能改组。

运维命令

node launch.mjs resolve    # 解析到哪个 node / dsh 入口 / 版本
node launch.mjs check      # 预检:不启动进程,验证版本一致与 profile 能否组合
node launch.mjs doctor     # 体检:解析 → 组合 → 端口 → token → 鉴权 → modelCatalog,并给出 broken 结论
node launch.mjs start      # 已在跑就 attach;没跑就预检后自启并捕获 stdout
node launch.mjs stop       # 只停 instance.json 里记录的 pid,不杀不属于自己的进程

测试与验证

npm test                    # 离线:契约 + MCP 子进程,20 个用例,零额度
node tests/live-check.mjs   # 对着活实例只读复验,不建会话、不消耗额度
node tools/probe-api.mjs    # 重新推导各端点的 args 形状(--mutate 才会建会话)
node tools/probe-acp.mjs    # ACP 面是否仍可用(--prompt 才会下发)
node tools/read-session-log.mjs <session.v4.jsonl.zstd>   # 多帧 zstd 会话日志解码

live-check 有三个显式开启的、有代价的步骤,被跳过的步骤不计入通过:

  • --adopt:接管 DSH_TEST_WS 里一个空闲会话。不创建任何东西。

  • --create:两个并发 dryRun 启动,验证共享账本在并发下的合并。会留下会话,而 API 删不掉它们(session/delete 未转发),只能在 dsh 界面归档。

  • --dispatch:真实一轮任务,消耗模型额度,需要 DSH_TEST_WS 指向一个可随便写的目录。

代码注释里的 MEASURED: 标记表示该行为由实际执行验证过,而不是从文档推断。

.github/workflows/ci.yml 在 Linux 与 Windows、Node 20 与 22 上跑同一套离线用例,所以贡献者本地不需要 dsh 也能验证改动。

安全说明

  • 捕获文件里的启动 token 换来的 cookie 是 owner 权限,dsh 对 owner 不做逐方法鉴权 —— 读到该文件等于读到那台机器上的 dsh。文件权限 600,目录已 gitignore。

  • 桥接只访问 127.0.0.1,不外发数据;审计日志记录端点名与 HTTP 状态,不记录 token。

  • /api 没有向后兼容承诺,因此每次 dsh 升级后先跑 npm test,再看 docs/API-CONTRACT.md 末节的重建步骤。

  • 把外派任务交给另一个 agent 意味着它会在它的工作区里改文件。下发前确认 cwd 是可写的沙箱目录,验收时按 docs/ORCHESTRATION.md 抽查产物。

故障排查

现象

原因

处理

no launch token in …

端口上那个实例的 stdout 没被捕获到桥接能读的位置(不是 token 过期)

用 start-dsh-web.ps1;或设 DSH_ATTACH_OUTFILE;或停掉它让桥接自启

/connect -> HTTP … with no auth cookie

token 属于另一个实例(它被重启过),或端口不对

node launch.mjs doctor,它会标出 STALE: token belongs to a dead process

dsh web 启动即失败,提示某插件 did not activate

profile patch 里有新版本拒绝的配置键

node launch.mjs check 会在不启动进程的情况下复述 dsh 原文;改 profile 前先备份

EADDRINUSE

端口已被占用

查占用进程并停掉旧实例

state:"error" + UNKNOWN_MODEL

该 profile 没有这个 provider,或目录尚未加载完

dsh_models 看 routableProviders 与 failures

会话不在预期的侧边栏分栏

分栏在创建时决定,接管无法更改

用 dsh_launch 新建并让它自动分栏,或在 dsh 界面手工归组

客户端的 MCP 面板看不到工具列表

部分客户端不展示 tools

调一次 dsh_models,看 .state/bridge.log.jsonl 是否出现 ev:"tool"

版本支持策略

launch.mjs(进程与凭据)+ lib/api-contract.mjs(线上格式)是两条唯一路径,bridge.mjs 只做传输与工具。升级 dsh 时最多坏这两处,而 tests/contract.test.mjs 会第一时间变红。

支持的 dsh 版本记录在 launch.mjs 的 DSH_TESTED_VERSION。如果你的版本不同且 /api 应答异常,先跑 docs/API-CONTRACT.md 末节的命令重新推导契约,再决定是否需要改 lib/api-contract.mjs。

贡献方式

  • 改动接口形状时,先让 tests/contract.test.mjs 反映实测结果,再改实现。

  • 任何新增断言必须来自实际执行;注释里用 MEASURED: 标注,并避免写入与某台机器绑定的路径、进程号或账户名。

  • 测试用例若拉起子进程,必须在 finally 里关闭 —— 否则断言失败会变成挂起,而不是失败。

  • 提交前确认 .state/、node_modules/、任何 *.out / *.ndjson 捕获都没进入暂存区(git status 看一眼,git diff --cached 再核一遍)。

许可

MIT,见 LICENSE。

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers