dsh-codex-collab
已验证@whaletalk/dsh-codex-collab · v0.1.7 · MIT
Codex × DeepSeek Harness 双向编码协作:Codex 主导派活与验收,DeepSeek Harness 编码子代理在共享工作目录执行并回传,独立评审子代理实际跑通构建/测试后出具报告。提供 DSH 宿主网关插件、零依赖 MCP 服务器与 Codex 技能三件套。
安装
dsh plugin add @whaletalk/dsh-codex-collab 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
标签
作者
说明文档
DSH Collab — Codex × DeepSeek Harness 双向编码协作
Codex 主导,DeepSeek 执行,Codex 验收。 一个插件体系,让 ChatGPT(Codex)像项目经理一样把编码任务派给本机 DeepSeek Harness 的编码子代理,实时回传结果,读文件验收,并由独立评审员实际跑通构建/测试后出具评审报告。
派活有两种目标,必须显式选一种——"接进用户正在用的那条对话"和"新建一个干活的子代理"是两件事:
| 目标 | 什么时候用 | 效果 |
|---|---|---|
worker(默认) |
全新任务、独立项目目录 | 在 --cwd 里新建持久编码子代理(多工作区 · 多 lane · fast/pro 模型) |
session |
用户已经在某条对话里推进这件事,你只是替他继续 | 指令投递进那条会话,沿用它的上下文,不新建会话/工作区 |
Codex 工作区 (ChatGPT)
│ dsh_task / dsh_sessions / dsh_review / dsh_read_file
▼
DeepSeek Harness 网关 /api/dsh-bridge
├─ worker 目标 ──→ 新建编码子代理(多工作区 · 多 lane · fast/pro)─┐
├─ session 目标 ─→ 已有那条会话(prompt 投递,queue / steer) ──┤
└─ 独立评审子代理(必须跑通构建/测试)───────────────────────────┴→ 共享目录 / 原会话
flowchart LR
subgraph Codex["Codex (ChatGPT) — 项目经理 + 验收员"]
A1[dsh_sessions 找会话]
A2[dsh_task 派活]
A3[dsh_review 评审]
A4[dsh_read_file 验收]
end
subgraph DSH["DeepSeek Harness"]
B1[HTTP 网关 /api/dsh-bridge]
B2[worker 目标<br/>新建编码子代理]
B3[session 目标<br/>投递进已有会话]
B4[独立评审子代理<br/>必须跑通构建/测试]
end
subgraph FS["工作现场"]
C1[共享工作目录]
C2[已有会话与其上下文]
end
A1 -->|只读查找| B1
A2 --> B1
A3 --> B1
A4 --> C1
B1 --> B2 --> C1
B1 --> B3 --> C2
B1 --> B4 --> C1
B2 -.实时回传汇报.-> B1
B3 -.实时回传汇报.-> B1
B1 -.任务结果.-> A2
组件
| 目录 | 内容 | 说明 |
|---|---|---|
codex-plugin/ |
Codex 本地市场插件包 | 标准 marketplace 结构(.agents/plugins/marketplace.json + 插件目录),含协作技能 dsh-collab |
mcp/dsh-mcp.mjs |
MCP 服务器(零依赖 Node) | stdio 传输(Codex [mcp_servers] 直连)+ Streamable HTTP 传输(--http,供 Secure MCP Tunnel / cloudflared 桥接 ChatGPT);6 个工具:dsh_task / dsh_sessions / dsh_task_status / dsh_task_cancel / dsh_review / dsh_read_file |
harness/dsh-bridge.mjs |
DeepSeek Harness 宿主组合网关插件 | 提供 HTTP 网关(/api/dsh-bridge/*)、WS 推送、原生工具;管理编码/评审子代理,并把任务投递进已有会话。必须装进 harness 宿主组合(见下文) |
harness/session-target.mjs |
会话目标的纯决策层 | 目标判定、候选挑选(0/1/N 不猜)、RemoteError→稳定 reason、基线切分、汇报选文。无 DSH import,可单测;dsh-bridge.mjs 依赖它,两个文件必须一起部署 |
test/ |
25 项 Node 测试 | session-target 纯函数单测 + dsh-bridge 路由级回归测试(假 DSH 运行时加载真实 bridge)。CI 每次都跑 |
templates/codex-mcp/ |
反向通道配置模板 | 把 Codex CLI 当 MCP 服务端挂进 DSH(得到 mcp__codex__* 工具),让 DSH 侧也能调用 Codex。见下文「反向通道」 |
cordis.patch.yml |
bundle patch | 把网关插件挂进 DSH 宿主组合;dsh.bundle.patch 指向它,所以 dsh plugin add 能自动 reconcile |
功能
- 派活:
dsh_task/task.mjs,任意工作目录(--cwd,不存在自动创建并注册为工作区) - 接进已有会话:
dsh_sessions只读搜索既有会话 →dsh_task --session <id>把指令投递进去(queue排队 /steer插入当前回合)。不新建会话、不新建工作区;"找不到/接不上"一律返回稳定 reason 而不是静默新建 - 并行分工:同目录多 lane(
--lane)+ 跨目录天然并行 - 模型切换:
fast(deepseek-v4-flash)或pro(deepseek-v4-pro),动态解析不写死 - 双向评审:独立评审子代理与编码员分离,读磁盘真实文件、静态审查、实际运行构建/测试验证可跑通,输出结构化报告
- Git 自动提交:
--commit,任务完成后在 cwd 自动git add -A并提交 - 跨任务记忆:持久子代理会话 + 历史摘要注入(冷恢复失败自动降级为一次性执行,功能不中断)
- 任务管理:
--list/--status <id>/--cancel <id> [--force] - 结果回传:任务汇报直接回 Codex 对话(
--wait默认);每个任务响应都回显target,一眼看出是"新建"还是"接进哪条会话"
安装
0. 一条命令装全部(推荐,npm)
# 1) 网关插件装进 harness profile(自动 reconcile bundles,无需手工复制文件)
dsh plugin --profile desktop add @whaletalk/dsh-codex-collab
# ↑ DSH 客户端用的就是 desktop profile;纯 Web 部署换成 --profile web
# 2) MCP 服务器交给 Codex
npm install -g @whaletalk/dsh-codex-collab
之后 dsh-mcp / dsh-task / dsh-review 三个命令进入 PATH,~/.codex/config.toml 里写:
[mcp_servers.dsh]
command = 'dsh-mcp'
startup_timeout_sec = 120
env = { DSH_BRIDGE_URL = "http://127.0.0.1:3080" } # 见下方"端口";不写则默认 3080
端口:网关挂在 profile 的 webServer 上,地址取决于 profile 怎么起的——独立 harness 通常 3080,而 DSH 客户端里就是 GUI 自己的端口(例如 http://127.0.0.1:19387)。MCP 与 CLI 都用 DSH_BRIDGE_URL 找网关,指错只会看到连接失败或 401。验证:
curl http://127.0.0.1:3080/api/dsh-bridge/status
装完必须重启 profile 进程——dsh plugin add 改的是 bundles 列表与 node_modules,HMR 不监控这两处;替换包内容也不会热重载(ESM 缓存),只有重启进程才加载新版本。
升级到新版本(有坑,照做)
# 1) 带精确版本号安装:不带版本号时 lockfile 会把旧版按回来
dsh plugin --profile desktop add @whaletalk/dsh-codex-collab@<新版本>
# 2) 重启 profile 进程:不重启不会加载新的 JS 模块
- 不要用"卸载 → 重装":卸载会把这个包从
dsh.profile.bundles移除;重装后若没被重新选中,插件根本不会挂载(表现:所有/api/dsh-bridge/*都落到网关鉴权层返回 401)。真遇到了就在客户端插件页把它的开关打开。 - 刚发布的版本约 24 小时内装不上:profile 的 pnpm 带
minimumReleaseAge供应链冷却(ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION)。管理器会把它写进pnpm-workspace.yaml的minimumReleaseAgeExclude才放行;手工安装可临时加--config.minimumReleaseAge=0。
本包只提供 bundle patch,不含 profile 之外的强依赖:
@deepseek-ai/dsh-tools是 optional peer,由 DSH 安装树在运行时提供。因此npm install不会去 registry 拉它,也不会因它缺失而报错。
不想用 npm?仍可手工安装(源码方式)
1. DeepSeek Harness 网关(必需,后端)
把 harness/ 整个目录复制到你的 dsh profile 目录(例如 $DSH_HOME/profiles/web/ 下的 harness/),并在该目录的 cordis.patch.yml 中追加:
- insert:
- id: dsh-bridge
name: './harness/dsh-bridge.mjs'
dsh-bridge.mjs会import './session-target.mjs'——两个文件必须在一起,只复制前者会直接加载失败。
重启 harness。默认共享工作目录为 D:\Harness,可用环境变量 DSH_BRIDGE_WORKSPACE 覆盖。验证:
GET http://127.0.0.1:3080/api/dsh-bridge/status
手工方式必须用相对路径(文件就在 profile 目录里);npm 方式则用包子路径
@whaletalk/dsh-codex-collab/bridge,两者不要混用。
2. Codex 侧(二选一或都用)
A. MCP 工具(推荐,走 Codex 官方通道):把 mcp/dsh-mcp.mjs 放到本机任意位置,在 ~/.codex/config.toml 追加:
[mcp_servers.dsh]
command = 'node'
args = ['<绝对路径>/dsh-mcp.mjs']
startup_timeout_sec = 120
重启 Codex,会话中出现 mcp__dsh__* 工具。
B. 本地市场插件(技能形式):把 codex-plugin/ 放到任意位置,在个人市场清单 ~/.agents/plugins/marketplace.json 中追加插件条目("path": "./plugins/dsh"),并把插件目录放到 ~/plugins/dsh/,然后:
codex plugin add dsh@personal
npm 安装后,插件目录已在包内,用一行拿到绝对路径:
node -p "require.resolve('@whaletalk/dsh-codex-collab/package.json').replace(/package\.json$/,'codex-plugin')"
⚠️ 已知问题:Codex Desktop 26.803 在 Windows 上存在插件技能不注入会话的 bug(openai/codex#26037、#22078)。技能形式可能不生效,以 MCP 方式为准。
3. ChatGPT 普通聊天区(可选,受账户限制)
ChatGPT 不能直连 localhost MCP。官方通道是 Secure MCP Tunnel:
- 在
https://platform.openai.com/settings/organization/tunnels创建隧道(需组织上下文),创建 Runtime API key; - 启动本地 HTTP MCP:
node dsh-mcp.mjs --http 127.0.0.1:4800; - 启动隧道客户端(含
HTTPS_PROXY环境变量若需代理):
tunnel-client init --sample sample_mcp_remote_no_auth --profile dsh --tunnel-id tunnel_xxx --mcp-server-url http://127.0.0.1:4800/mcp
tunnel-client run --profile dsh
- ChatGPT → 设置 → Connectors 中连接。
注意:写入型 MCP 工具对 ChatGPT 个人订阅(Plus)的开放度存在产品级限制,此路径仅供有能力/有组织的账户使用。
使用
Codex 工作区会话(MCP 工具就绪后):
新建子代理:用 dsh_task 让 DeepSeek 生成一个随机密码 CLI + 测试 + README,放在当前项目目录,完成后你用 dsh_read_file 验收,再 dsh_review 评审
接进原对话:先用 dsh_sessions 按标题找到用户正在用的那条会话,再用 dsh_task 带 sessionId 把下一步指令投递进去(不要新建会话)
MCP 工具(Codex 侧首选,工具名带 mcp__dsh__ 前缀):
// 1) 找到会话(只读)
dsh_sessions({ "query": "按文档启动OKX策略实验计划" })
// → { "items": [{ "sessionId": "session-8d481ad9-…", "snippet": "按文档启动OKX策略实验计划",
// "agentAvailable": true }], "matchedBy": "list" }
// 2) 投递进那条会话
dsh_task({ "instruction": "继续推进第 3 步…", "sessionId": "session-8d481ad9-…" })
// deliver 可选 "queue"(默认,排队)或 "steer"(插入当前回合)
命令行(npm 安装后直接用;源码方式用 node <脚本路径>):
# 新建子代理(默认行为)
dsh-task --in "<指令>" --cwd "<目录>" [--lane backend] [--model fast|pro] [--commit]
# 接进已有会话:先找(或先空跑验证),再派
dsh-task --sessions "<标题关键词>" # 只读列候选,拿 sessionId
dsh-task --dry-run --find "<标题关键词>" # 只解析目标、回显命中,不投递任何消息
dsh-task --in "<指令>" --session "<sessionId>" [--steer]
dsh-task --in "<指令>" --find "<标题关键词>" # 唯一命中才派;否则退出码 4
dsh-review --cwd "<目录>" [--diff git|@文件|"diff文本"] [--focus "<重点>"]
dsh-task --list | --status <taskId> | --cancel <taskId> [--force]
已有会话派活(对应"接进我正在用的那条对话"):
# 1) 找到(只读,不创建任何东西)
dsh-task --sessions "按文档启动OKX策略实验计划"
# → {"items":[{"sessionId":"session-8d481ad9-…","snippet":"按文档启动OKX策略实验计划",
# "agentAvailable":true}],"total":1,"matchedBy":"list"}
# 1b) 没命中时会给近似候选(按标题相似度 + 最近更新排序),换词重查或改用 sessionId——
# 绝不要发一条消息去"试探命中"(它会落进对方会话并跑成回合,污染对话)
dsh-task --dry-run --find "量化框架"
# → {"resolved":false,"reason":"session-query-empty",
# "candidates":[{"sessionId":"1c0f99f8-…","snippet":"你是一个量化研究项目的技术","score":0.333}, …]}
# 2) 接通(投递进那条会话;这一轮的助手回复作为汇报返回,只回报本轮新增文本)
dsh-task --in "<继续推进的指令>" --session "session-8d481ad9-…"
matchedBy 说明"找到"走了哪条路:search = 会话搜索索引;list = 索引被部署禁用时退回遍历列表 + 标题投影(都不激活 Agent)。
delivered 与排队语义(重要):prompt 一旦被目标会话接受,就表示消息已经进了它的队列,响应里会带 delivered: true;此后即便我们的观察超时(会话可能正跑一个很长的回合),也不要重发——重发会让同一份内容在用户会话里出现两次。queue 模式不再用 120s 判失败:没在 120s 内开跑只记一条 note 并继续等,最终由总预算(默认 1h)收口为 session-timeout + delivered: true。
失败返回稳定 reason 且不创建任何会话/工作区:session-query-empty(没命中,附 candidates)、session-ambiguous(多条命中,附 candidates)、session-not-found、session-busy、session-writer-held(会话被别的写入方占用)、session-archived、session-not-accepted(仅当 prompt 未被接受)、session-timeout、session-cancel-refused(取消会话目标默认被拒,需 --force)。
反向通道:让 DSH 调用 Codex
上面的链路都是 Codex → DSH。反过来的 DSH → Codex 也能做,但要分清两种含义:
| 含义 | 可行性 |
|---|---|
| 让 DSH 调用 Codex(把 Codex 当执行器/评审员) | ✅ 两端原生支持,见下 |
| 让 DSH 往你正开着的某条 ChatGPT/Codex 对话里插一条消息、就地开一轮 | ❌ 做不到 |
第二种做不到的原因:现有链路是 Codex 主动(它启动 dsh-mcp 子进程调工具),而 MCP 是
客户端发起的协议——服务端无法命令客户端开一轮。服务端→客户端只有 notifications/*
(纯通知,不触发回合)、sampling/createMessage、elicitation/create(取决于客户端是否
实现,且不等于"在对话里跑工具");ChatGPT 侧同样没有公开 API 能向用户会话投消息。
方案 A:反向通道已内置在本包里(推荐)
装本包就同时得到两个方向——cordis.patch.yml 里插了两行:dsh-bridge(Codex → DSH 派活)与 codex-mcp(DSH → Codex 反向调用)。原理是两端各有一个原生件:
- DSH 侧
@deepseek-ai/dsh-mcp-client(harness 自带):连外部 MCP 服务器,并把工具以mcp__<serverName>__<tool>注册进ctx.tools; - Codex 侧
codex mcp-server(stdio):把 Codex 暴露成 MCP 服务端。
内置的那一行等价于:
- insert:
- id: codex-mcp
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: codex
transport: stdio
command: codex # 跨平台:SDK 用 cross-spawn,Windows 的 .cmd 垫片也能解析
args: ['mcp-server']
failOnStartupError: false # 没装/没登录 Codex 只是没这批工具,不会拖垮 harness
toolCallTimeoutMs: 600000 # 默认只有 60s,Codex 跑一轮常常不够
- 前提:本机装了 Codex CLI 并已登录(
codex --version/codex mcp-server --help能跑)。 - 不需要反向通道?在 profile 的
cordis.patch.yml里覆盖一行即可关掉:- id: codex-mcp+disabled: true。 - 想单独用反向通道(不装本插件主体)可以用独立模板
templates/codex-mcp/。 - 装完需重启 profile 进程;连接失败只是这批工具不出现(日志里有原因)。
- 排查:若
codex不在 DSH 进程的 PATH 上,把command换成node+args: ['<...>/@openai/codex/bin/codex.js', 'mcp-server'],或直接指向平台codex.exe。
方案 B:worker 里直接跑 codex exec
codex exec "把刚才的 diff 审一遍,只回结论" --cd E:\proj
一次性、零协议改动;代价是每次都是新会话(无跨轮记忆),且需要执行 shell 的权限。
注意:插件自带的
dsh_collab_send/dsh_collab_review是 DSH 侧入口,只回到同一个网关,不经过 Codex。要"叫 Codex 干活"用上面 A/B;要"让 Codex 那条对话继续",只能等 Codex 自己下一轮(dsh_task --wait或dsh_task_status)。
环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
DSH_BRIDGE_URL |
http://127.0.0.1:3080 |
网关地址。必须指向插件实际挂载的那个 profile 的端口(DSH 客户端里就是 GUI 端口,例如 19387) |
DSH_BRIDGE_WORKSPACE |
D:\Harness |
网关默认共享工作目录 |
DSH_BRIDGE_ROOT |
D:\Harness |
dsh_read_file 相对路径基准 |
DSH_BRIDGE_HISTORY |
多级降级 | 协作历史文件位置(默认 D 盘 → 用户目录 → 临时目录逐级降级) |
DSH_BRIDGE_SESSION_DISPATCH_TIMEOUT_MS |
120000 |
会话任务被接受后、迟迟没开始跑的容忍时间(仍在排队则报 session-not-accepted) |
DSH_BRIDGE_SESSION_QUIET_TIMEOUT_MS |
600000 |
见过 running 之后多久没有新助手文本算跑完 |
DSH_BRIDGE_SESSION_TOTAL_TIMEOUT_MS |
3600000 |
单个会话任务的总预算 |
已知限制
- 网关插件需要
webServer服务(由dsh-web-app提供),因此只能挂 web / desktop profile;headless profile 会一直 pending。session 目标还需要sessionController——它由组合异步注册(webserver → web-runtime → connection → file-upload → session-controller),所以插件是惰性取用,注册前调用会返回session-controller-unavailable(稍后重试即可) sessionController.search可能被部署禁用(session-query索引openAt: "never")。此时按标题查找自动退回list(读持久化 header + 标题投影,同样不激活 Agent),响应里的matchedBy标明走了哪条路- 不能投递进子代理会话:
resolveAgent对 subagent 路由拥有的会话返回session-busy: owned by subagent routing;只能接普通会话 POST /api/dsh-bridge/sessions {"create": true, "cwd": "..."}可新建一条空白会话(探针/一次性任务用),它不会碰任何已有会话- 同一会话同时只允许一个在途 bridge 任务:DSH 自己会排队,但无法可靠地把两次并发派活的回报各自归属,所以第二个会被明确拒绝(
session-busy)而不是猜 - 冷会话可能接不上:DSH 激活 Agent 需要会话投影(
Agent activation requires a projected Session observation),未激活的会话会返回session-not-activatable;归档会话的回合会以blocked收口(session-archived) - 评审不支持会话目标:评审员必须与编码会话隔离,"独立评审"才成立
- session 目标不使用
cwd/lane/model/--commit(会话自带这些语义),传了会被判invalid-target - 网关重启会丢失内存中的任务表(含 session 观察器),这是既有设计
- 替换包内容不会热重载:升级后必须重启 profile 进程,否则跑的还是旧 JS 模块(HMR 只重挂组合,不复用新代码)
- 组合包被取消选中 = 插件完全不挂载:此时所有
/api/dsh-bridge/*都落到网关鉴权层返回 401(不是 404,容易误判成没装) - DSH profile 的 pnpm 供应链冷却:新发布的版本约 24 小时内会被
minimumReleaseAge拒绝安装(ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION);管理器会把它写进pnpm-workspace.yaml的minimumReleaseAgeExclude才放行。刚发版就装不上属正常,不是包的问题 - 升级已装组合包:客户端插件页没有升级入口,官方姿势是卸载 → 重装;但卸载会把该包从
dsh.profile.bundles移除,重装后要确认它被重新选中,否则插件不会挂载(表现为所有/api/dsh-bridge/*都落到网关鉴权层返回 401)。更省事的做法是直接dsh plugin add <包>@<版本>,见上文「升级到新版本」 - Codex Desktop Windows 本地市场技能注入 bug(见上,MCP 通道不受影响)
- 动态插件环境无
AbortSignal,冷恢复失败时自动降级为一次性执行;宿主组合持久化版无此问题 - ChatGPT Plus 写入型 MCP 的开放度取决于 OpenAI 产品策略
开发与测试
node test/session-target.test.mjs # 纯函数:目标判定 / 候选挑选 / 错误映射 / 基线切分 / 汇报选文
node test/dsh-bridge.test.mjs # 路由级:假 DSH 运行时 + 桩 peer,加载真实 bridge 跑真实路由
node --test test/*.test.mjs # 两个一起跑(CI 用的是这条)
- 路由级测试会断言 worker 分支确实调用了
startContinuable、session 分支只调用prompt且不建 owner/子代理——这类"参数遮蔽/串线"缺陷只有在这一层才抓得到(v0.1.2~0.1.4 的deliver遮蔽 bug 就是它抓回来的)。 - CI(
.github/workflows/publish.yml)每次 tag 都跑:语法检查 → 单测 → 包契约 → 版本号三处一致 → tarball 清单 → 干净目录安装 + CLI/MCP 冒烟 → 6 工具握手断言 → bridge 导出契约。任何一步失败都不会发布。 - 发布:
package.json/.codex-plugin/plugin.json/mcp/dsh-mcp.mjs的SERVER_VERSION三处同步改版本 → commit → 打 tagvX.Y.Z→ 推 tag,CI 自动带 provenance 发布。
License
MIT