@whaletalk/dsh-codex-collab 0.1.9 → 0.1.10
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/CHANGELOG.md +9 -0
- package/README.md +105 -25
- package/codex-plugin/plugins/dsh/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/plugins/dsh/skills/dsh-collab/SKILL.md +63 -3
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/review.mjs +91 -6
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/task.mjs +443 -39
- package/docs/CODEX_DSH_APPROVAL_DELEGATION_PLAN.md +130 -0
- package/docs/CODEX_DSH_APPROVAL_M0_EVIDENCE.md +285 -0
- package/docs/CODEX_DSH_DELIVERY_CONTROL_PLAN.md +81 -0
- package/docs/CODEX_DSH_MODEL_ROUTING_PLAN.md +75 -0
- package/docs/DSH_APPROVAL_DELEGATION.md +142 -0
- package/docs/DSH_DELIVERY_M0_EVIDENCE.md +69 -0
- package/docs/DSH_MODEL_ROUTING_M0_EVIDENCE.md +166 -0
- package/harness/audit-tail.mjs +32 -0
- package/harness/codex-approvals.mjs +1078 -0
- package/harness/codex-associations.mjs +39 -7
- package/harness/dsh-bridge.mjs +1950 -191
- package/harness/model-route-host.mjs +530 -0
- package/harness/model-routing.mjs +500 -0
- package/harness/session-observer.mjs +217 -0
- package/harness/session-target.mjs +161 -6
- package/harness/task-table.mjs +6 -4
- package/mcp/dsh-mcp.mjs +474 -12
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.10 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
- 修复大对话每 2.5 秒全量读取:同会话共享 SDK 增量事件/有界 follow,低成本观察只用于初始化或补缺;慢检查互斥、失败退避,空闲 worker 零历史读取。
|
|
6
|
+
- 审批/模型选择复用连续证据索引,关键决定仍做新鲜核验;缺口、卸载、缓冲溢出时安全停止取证,不以过期缓存放行。
|
|
7
|
+
- 模型审计状态读取改为缓存最近 1 MiB,保留完整原始审计文件;debug 新增观察计数与审计尾部统计。
|
|
8
|
+
- 兼容 DSH 0.2.1 的 `startActivation`(同时保留 0.2.0 入口);worker 未确认时保留唯一归属,迟到的官方结束仍结算原任务。
|
|
9
|
+
- 一并发布已验收的审批委托、来源绑定的 queue/steer、模型目录及 DeepSeek 优先自动选择/有界故障切换;MCP 工具共 9 个。
|
|
10
|
+
- 增加共享观察、快照补缺、慢读竞态、失败退避、缓冲上限和审计回归;保留上游署名和 MIT 许可。
|
|
11
|
+
|
|
3
12
|
## 0.1.9 — 2026-10-13
|
|
4
13
|
|
|
5
14
|
> **0.1.8 从未发布到 npm。** `v0.1.8` tag 建立后,CI 的 Linux gate 在 `test/codex-associations.test.mjs` 暴露了一个**仅测试代码**的跨平台断言缺陷:
|
package/README.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
# DSH Collab — Codex × DeepSeek Harness 双向编码协作
|
|
6
6
|
|
|
7
|
+
本项目 fork 自 [FuJackGao/dsh-collab](https://github.com/FuJackGao/dsh-collab),在上游基础上由 [Whale-Talk](https://github.com/Whale-Talk) 继续维护与扩展。感谢原作者及上游贡献者。
|
|
8
|
+
|
|
9
|
+
[安装与下载](#安装) · [升级方法](#升级到新版本) · [使用示例](#使用) · [npm 包](https://www.npmjs.com/package/@whaletalk/dsh-codex-collab) · [GitHub 源码](https://github.com/Whale-Talk/dsh-codex-collab)
|
|
10
|
+
|
|
11
|
+
> **版本说明**:0.1.10 提供审批委托、queue/steer 投递控制、已配置模型自动选择与故障切换,以及大对话增量观察修复。升级时同时更新 DSH 网关和 Codex 侧 MCP/CLI,并重启 DSH 使新模块生效。
|
|
12
|
+
|
|
7
13
|
**Codex 主导,DeepSeek 执行,Codex 验收。** 一个插件体系,让 ChatGPT(Codex)像项目经理一样把编码任务派给本机 DeepSeek Harness 的编码子代理,实时回传结果,读文件验收,并由独立评审员实际跑通构建/测试后出具评审报告。
|
|
8
14
|
|
|
9
15
|
派活有**两种目标**,必须显式选一种——"接进用户正在用的那条对话"和"新建一个干活的子代理"是两件事:
|
|
@@ -55,15 +61,25 @@ flowchart LR
|
|
|
55
61
|
|
|
56
62
|
---
|
|
57
63
|
|
|
64
|
+
## 大对话性能与排障
|
|
65
|
+
|
|
66
|
+
同一会话的任务共享增量观察;健康的同进程事件或 follow 连接不会再每 2.5 秒全量读取历史。启动和断流补缺使用 SDK 的 `observeSession({ projectionMode: "none" })`,及时释放观察租约;旧 SDK 才降级到串行的 `readSession`。后台失败会退避,慢检查不会重叠,空闲 worker 不读取历史。
|
|
67
|
+
|
|
68
|
+
兼容 DSH 0.2.0 与 0.2.1 的子代理入口。worker 长时间安静时只报告未确认,继续保留该任务的执行占用,收到官方结束后再回填结果;不会把旧结果交给后来派发的任务。
|
|
69
|
+
|
|
70
|
+
审批和人工模型选择仍要求新鲜证据。事件缺口、读取失败或缓冲溢出会补缺或返回未确认,不会把缓存当作批准依据。增量回放与慢读缓冲各有 4 MiB 上限,不保留工具结果正文;任务自身的最终回答仍按原有方式返回。
|
|
71
|
+
|
|
72
|
+
只读访问 `GET /api/dsh-bridge/debug` 可查看 `observation` 的完整读取/观察/缺口/失败计数,以及 `routeAuditTail` 的读取字节与截断标志。模型审计的状态回显只解码最近 1 MiB,原始 JSONL 仍完整保留,可用于历史回溯。冷会话第一次载入仍可能较慢;这次修复针对重复读取,不代表 DSH 的其他界面或插件性能问题均已解决。
|
|
73
|
+
|
|
58
74
|
## 组件
|
|
59
75
|
|
|
60
76
|
| 目录 | 内容 | 说明 |
|
|
61
77
|
|---|---|---|
|
|
62
78
|
| `codex-plugin/` | Codex 本地市场插件包 | 标准 marketplace 结构(`.agents/plugins/marketplace.json` + 插件目录),含协作技能 `dsh-collab` |
|
|
63
|
-
| `mcp/dsh-mcp.mjs` | MCP 服务器(零依赖 Node) | stdio 传输(Codex `[mcp_servers]` 直连)+ Streamable HTTP 传输(`--http`,供 Secure MCP Tunnel / cloudflared 桥接 ChatGPT);
|
|
79
|
+
| `mcp/dsh-mcp.mjs` | MCP 服务器(零依赖 Node) | stdio 传输(Codex `[mcp_servers]` 直连)+ Streamable HTTP 传输(`--http`,供 Secure MCP Tunnel / cloudflared 桥接 ChatGPT);9 个工具:`dsh_task` / `dsh_sessions` / `dsh_task_status` / `dsh_task_cancel` / `dsh_review` / `dsh_read_file` / `dsh_approvals` / `dsh_approval_decide` / `dsh_models` |
|
|
64
80
|
| `harness/dsh-bridge.mjs` | DeepSeek Harness 宿主组合网关插件 | 提供 HTTP 网关(`/api/dsh-bridge/*`)、WS 推送、原生工具;管理编码/评审子代理,并把任务投递进已有会话。**必须装进 harness 宿主组合**(见下文) |
|
|
65
|
-
| `harness/session-target.mjs` | 会话目标的纯决策层 | 目标判定、候选挑选(0/1/N 不猜)、RemoteError→稳定 reason、基线切分、汇报选文。无 DSH import,可单测;`dsh-bridge.mjs`
|
|
66
|
-
| `test/` |
|
|
81
|
+
| `harness/session-target.mjs` | 会话目标的纯决策层 | 目标判定、候选挑选(0/1/N 不猜)、RemoteError→稳定 reason、基线切分、汇报选文。无 DSH import,可单测;`dsh-bridge.mjs` 依赖它,**源码手工部署时保留整个 `harness/` 目录** |
|
|
82
|
+
| `test/` | Node 行为回归测试 | `session-target` 纯函数单测 + `dsh-bridge` 路由级回归测试(假 DSH 运行时加载真实 bridge)。CI 每次都跑 |
|
|
67
83
|
| `templates/codex-mcp/` | 反向通道配置模板 | 把 Codex CLI 当 MCP 服务端挂进 DSH(得到 `mcp__codex__*` 工具),让 DSH 侧也能调用 Codex。见下文「反向通道」 |
|
|
68
84
|
| `cordis.patch.yml` | bundle patch | 把网关插件挂进 DSH 宿主组合;`dsh.bundle.patch` 指向它,所以 `dsh plugin add` 能自动 reconcile |
|
|
69
85
|
|
|
@@ -81,7 +97,17 @@ flowchart LR
|
|
|
81
97
|
|
|
82
98
|
## 安装
|
|
83
99
|
|
|
84
|
-
###
|
|
100
|
+
### npm 安装(推荐)
|
|
101
|
+
|
|
102
|
+
前提:Node.js **20.11 或更新版本**、npm,以及已安装的 DeepSeek Harness(desktop 或 web profile)。本包提供插件和协作工具,DSH 本体需另行安装。
|
|
103
|
+
|
|
104
|
+
安装包名为 **`@whaletalk/dsh-codex-collab`**。可先查询 npm 已发布版本:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
npm view @whaletalk/dsh-codex-collab version
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
下面两步分别安装 DSH 网关和 Codex 侧工具,两端都需要:
|
|
85
111
|
|
|
86
112
|
```sh
|
|
87
113
|
# 1) 网关插件装进 harness profile(自动 reconcile bundles,无需手工复制文件)
|
|
@@ -109,12 +135,13 @@ curl http://127.0.0.1:3080/api/dsh-bridge/status
|
|
|
109
135
|
|
|
110
136
|
装完**必须重启 profile 进程**——`dsh plugin add` 改的是 bundles 列表与 node_modules,HMR 不监控这两处;**替换包内容也不会热重载**(ESM 缓存),只有重启进程才加载新版本。
|
|
111
137
|
|
|
112
|
-
###
|
|
138
|
+
### 升级到新版本
|
|
113
139
|
|
|
114
140
|
```sh
|
|
115
|
-
#
|
|
116
|
-
dsh plugin --profile desktop add @whaletalk/dsh-codex-collab
|
|
117
|
-
|
|
141
|
+
# 将 X.Y.Z 替换成 npm 已发布的新版本号;两端使用同一版本
|
|
142
|
+
dsh plugin --profile desktop add @whaletalk/dsh-codex-collab@X.Y.Z
|
|
143
|
+
npm install -g @whaletalk/dsh-codex-collab@X.Y.Z
|
|
144
|
+
# 保存当前工作后,重启 DSH profile,并重新加载 Codex 的 MCP 连接
|
|
118
145
|
```
|
|
119
146
|
|
|
120
147
|
- **不要用"卸载 → 重装"**:卸载会把这个包从 `dsh.profile.bundles` 移除;重装后若没被重新选中,插件根本不会挂载(表现:所有 `/api/dsh-bridge/*` 都落到网关鉴权层返回 **401**)。真遇到了就在客户端插件页把它的开关打开。
|
|
@@ -135,7 +162,7 @@ dsh plugin --profile desktop add @whaletalk/dsh-codex-collab@<新版本>
|
|
|
135
162
|
name: './harness/dsh-bridge.mjs'
|
|
136
163
|
```
|
|
137
164
|
|
|
138
|
-
> `dsh-bridge.mjs`
|
|
165
|
+
> 网关依赖同目录下的会话关联、审批和锁模块。**复制整个 `harness/` 目录并保留结构**,不要只挑 `dsh-bridge.mjs` 和 `session-target.mjs`。
|
|
139
166
|
|
|
140
167
|
重启 harness。默认共享工作目录为 `D:\Harness`,可用环境变量 `DSH_BRIDGE_WORKSPACE` 覆盖。验证:
|
|
141
168
|
|
|
@@ -147,12 +174,12 @@ GET http://127.0.0.1:3080/api/dsh-bridge/status
|
|
|
147
174
|
|
|
148
175
|
### 2. Codex 侧(二选一或都用)
|
|
149
176
|
|
|
150
|
-
**A. MCP
|
|
177
|
+
**A. MCP 工具**:保留完整源码目录(包括 `mcp/` 和 `harness/`),在 `~/.codex/config.toml` 追加;不要把 `dsh-mcp.mjs` 单独拷走,它会导入 `../harness/codex-approvals.mjs`:
|
|
151
178
|
|
|
152
179
|
```toml
|
|
153
180
|
[mcp_servers.dsh]
|
|
154
181
|
command = 'node'
|
|
155
|
-
args = ['
|
|
182
|
+
args = ['<项目绝对路径>/mcp/dsh-mcp.mjs']
|
|
156
183
|
startup_timeout_sec = 120
|
|
157
184
|
```
|
|
158
185
|
|
|
@@ -207,9 +234,12 @@ dsh_sessions({ "query": "按文档启动OKX策略实验计划" })
|
|
|
207
234
|
// → { "items": [{ "sessionId": "session-example-…", "snippet": "按文档启动OKX策略实验计划",
|
|
208
235
|
// "agentAvailable": true }], "matchedBy": "list" }
|
|
209
236
|
|
|
210
|
-
// 2)
|
|
211
|
-
dsh_task({ "instruction": "继续推进第 3 步…", "sessionId": "session-example-…" })
|
|
212
|
-
|
|
237
|
+
// 2) 排队派活,记录返回的 taskId(来源必须显式传当前 Codex 对话 id)
|
|
238
|
+
dsh_task({ "instruction": "继续推进第 3 步…", "sessionId": "session-example-…", "originThreadId": "<当前 Codex 对话 id>", "deliver": "queue", "wait": false })
|
|
239
|
+
|
|
240
|
+
// 3) 当前源码支持:向正在运行的父任务补充(需控制凭据与投递能力)
|
|
241
|
+
dsh_task({ "instruction": "给原任务补充验收要求…", "sessionId": "session-example-…", "originThreadId": "<当前 Codex 对话 id>", "deliver": "steer", "parentTaskId": "<上一步 taskId>", "wait": false })
|
|
242
|
+
// 返回的是补充回执;最终结果仍按原 originThreadId + 父 taskId 查询。父任务尚在排队或已结束时不能 steer。
|
|
213
243
|
```
|
|
214
244
|
|
|
215
245
|
**命令行**(npm 安装后直接用;源码方式用 `node <脚本路径>`):
|
|
@@ -221,8 +251,10 @@ dsh-task --in "<指令>" --cwd "<目录>" [--lane backend] [--model fast|pro] [-
|
|
|
221
251
|
# 接进已有会话:先找(或先空跑验证),再派
|
|
222
252
|
dsh-task --sessions "<标题关键词>" # 只读列候选,拿 sessionId
|
|
223
253
|
dsh-task --dry-run --find "<标题关键词>" # 只解析目标、回显命中,不投递任何消息
|
|
224
|
-
dsh-task --in "<指令>" --session "<sessionId>"
|
|
225
|
-
|
|
254
|
+
dsh-task --in "<指令>" --session "<sessionId>" --origin "<当前 Codex 对话 id>"
|
|
255
|
+
# 当前源码:补充一条正在运行的父任务;--parent 同时选择 steer
|
|
256
|
+
dsh-task --in "<补充>" --session "<sessionId>" --origin "<当前 Codex 对话 id>" --parent "<父 taskId>"
|
|
257
|
+
dsh-task --in "<指令>" --find "<标题关键词>" --origin "<当前 Codex 对话 id>" # 唯一命中才派;否则退出码 4
|
|
226
258
|
|
|
227
259
|
dsh-review --cwd "<目录>" [--diff git|@文件|"diff文本"] [--focus "<重点>"]
|
|
228
260
|
dsh-task --list | --status <taskId> | --cancel <taskId> [--force]
|
|
@@ -243,12 +275,12 @@ dsh-task --dry-run --find "量化框架"
|
|
|
243
275
|
# "candidates":[{"sessionId":"1c0f99f8-…","snippet":"你是一个量化研究项目的技术","score":0.333}, …]}
|
|
244
276
|
|
|
245
277
|
# 2) 接通(投递进那条会话;这一轮的助手回复作为汇报返回,只回报本轮新增文本)
|
|
246
|
-
dsh-task --in "<继续推进的指令>" --session "session-example-…"
|
|
278
|
+
dsh-task --in "<继续推进的指令>" --session "session-example-…" --origin "<当前 Codex 对话 id>"
|
|
247
279
|
```
|
|
248
280
|
|
|
249
281
|
`matchedBy` 说明"找到"走了哪条路:`search` = 会话搜索索引;`list` = 索引被部署禁用时退回遍历列表 + 标题投影(都不激活 Agent)。
|
|
250
282
|
|
|
251
|
-
**`delivered` 与排队语义**(重要):`prompt` 一旦被目标会话接受,就表示消息**已经进了它的队列**,响应里会带 `delivered: true
|
|
283
|
+
**`delivered` 与排队语义**(重要):`prompt` 一旦被目标会话接受,就表示消息**已经进了它的队列**,响应里会带 `delivered: true`;此后即便我们的观察超时(会话可能正跑一个很长的回合),也**不要重发**——重发会让同一份内容在用户会话里出现两次。已接收的 `queue` 任务未在 120s 内开跑只记 note;带来源任务必须观察到自身回合结束,观察窗口到期而证据不足时返回 `unknown` + `resend:false`(不把观察超时当执行失败)。有效审批委托按其授权窗口观察。
|
|
252
284
|
|
|
253
285
|
失败返回稳定 `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`)。
|
|
254
286
|
|
|
@@ -367,7 +399,7 @@ dsh-task --status "<taskId>" --origin "<当前 Codex 对话 id>"
|
|
|
367
399
|
**不是**完成证明。落盘并回显(内存分支与恢复分支都给):`requestId`(=taskId)与匹配到的 `dshTurn`;
|
|
368
400
|
`turnEndKind`/`partialResult` 以及未确认时的 `resend`/`note` **只在活任务的内存响应里**出现(不在持久记录里;
|
|
369
401
|
重启后非终态记录由恢复分支合成 `unknown` + `resend:false`)。
|
|
370
|
-
-
|
|
402
|
+
- **带来源的 `steer` 是父任务补充回执**:必须显式给同来源、同会话且正在运行的父 `taskId`,并有控制凭据。回执只报告自身是否被观察、是否偏离目标回合;不承载父结果、不成为审批 owner。旧网关没有 `capabilities.delivery` 时,客户端在派活前拒绝。
|
|
371
403
|
- **没有证据就是未确认**:无匹配 `rpcId` 的终态、quiet/预算到期 → `status:"unknown"` + `resend:false`,绝不猜 done、绝不重发。
|
|
372
404
|
- **网关必须回显并匹配 `originThreadId` 才算确认绑定/取值**:能力检查通过≠这次绑定成功(网关可能中途重载/旧 handler)。
|
|
373
405
|
未确认时 MCP/CLI 都按"未确认 + 禁止重发"上报(CLI 退出码 8),**不返回**那种响应里的结果。
|
|
@@ -384,11 +416,49 @@ dsh-task --status "<taskId>" --origin "<当前 Codex 对话 id>"
|
|
|
384
416
|
验证:`node test/codex-r1-contracts.test.mjs`(8 项:非法来源零执行、已关联任务缺/错来源拒绝、通道 busy、旧网关零执行拒绝、能力上报、
|
|
385
417
|
能力通过但未确认来源 → 未确认+禁止重发、列表 unknown 语义、CLI 严格参数)、
|
|
386
418
|
`node test/codex-session-correlation.test.mjs`(8 项:真实 SDK 字段的请求↔回合关联:排队请求忽略先前回合、只有自己 turn/end 才完成、
|
|
387
|
-
error/blocked 不得 done、snapshot.records、重复投递不串文本、快照缺前半段回填、无匹配 rpcId →
|
|
419
|
+
error/blocked 不得 done、snapshot.records、重复投递不串文本、快照缺前半段回填、无匹配 rpcId → 未确认、无有效父任务的 steer 拒绝)、
|
|
388
420
|
`node test/codex-association-e2e.test.mjs`(3 项:**真实 HTTP + 独立 MCP 进程 + 独立 CLI 进程** 的跨回合收取与重启恢复)、
|
|
389
421
|
`node test/codex-associations.test.mjs`(13 项:存储/不可改投/来源校验/失败不泄漏 record/跨重启/命名空间隔离/真多进程并发/任务表淘汰)、
|
|
390
422
|
`node test/dsh-bridge.test.mjs`(13 项:非法来源 400、执行前落盘、新实例恢复、200+ 记录、存储故障拒绝派活)。
|
|
391
423
|
|
|
424
|
+
## 审批委托:Codex 接手 DSH 单次审批(needs-approval 闭环)
|
|
425
|
+
|
|
426
|
+
当 DSH 执行需要一次审批(沙箱升级等)而你不在电脑前时,受委托且正在等待/处理任务的 Codex 对话可以**逐次**决定。前提是网关与客户端都已加载支持该功能的版本,控制凭据、委托范围与租约有效;它不会自动唤醒空闲的 Codex 聊天。超出范围、证据不足、断线/过期时交还人工链。
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
# 0) 只做一次:离线初始化控制凭据(仓库/工作区之外的私有目录;raw key 不回显、不上线)
|
|
430
|
+
dsh-task --approval-key-init # 轮换:--approval-key-rotate(= 撤销全部旧 grant)
|
|
431
|
+
|
|
432
|
+
# 1) 派活时显式开启委托(grant 与 task+origin 绑定**先于** prompt;任何失败=零派活)
|
|
433
|
+
dsh-task --in "<指令>" --session <会话id> --origin <当前对话id> --delegate [--authorization "本次授权范围"]
|
|
434
|
+
|
|
435
|
+
# 2) 任务在等审批时以退出码 9 及时返回(非终态)→ 取真实动作 → 单次决定
|
|
436
|
+
dsh-task --approvals --origin <id> --task <taskId> --grant <grantId>
|
|
437
|
+
dsh-task --approve <approvalKey> --origin <id> --task <taskId> --grant <grantId> --fingerprint <fp>
|
|
438
|
+
# 3) 继续收结果(等待期间认证续租;长任务可 --renew-grant 在原窗口内显式续期)
|
|
439
|
+
dsh-task --status <taskId> --origin <id> --grant <grantId> --status-wait
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
- **MCP**:`dsh_task { delegate: true, authorization }`、`dsh_approvals { originThreadId, taskId }`、`dsh_approval_decide { originThreadId, taskId, approvalKey, grantId, fingerprint, decision }`、`dsh_task_status { wait: true, grantId }`、`dsh_models { force? }`(工具集 6 → 9:旧 6 个原样保留 + 审批 2 个 + 模型目录 1 个)。委托派活/状态响应带**非秘密** `grantId`/`grantExpiresAt`/`grantLeaseUntil`,等待循环据此**自适应认证续租**(短租约也不会过期,不扩大永久授权)。
|
|
443
|
+
- **动态模型路由(第五轮新功能,`model:auto` 默认)**:插件管理的任务(编码/评审子代理、有明确来源的普通会话 queue 任务)在**派活前**从本机**已注册** provider 目录里选模型,并在模型失效时有界切换:
|
|
444
|
+
- 输入:`model` 支持 `auto`(默认)/`fast`/`pro`(兼容偏好)/`default`(继承当前选择)/精确模型 id;精确指定可配 `provider`(跨 provider 同名模型**不合并**,缺 provider 时明确拒绝并给候选);`modelFallback:false` 关闭本任务切换(精确指定默认固定)。
|
|
445
|
+
- **四个入口同一契约**:HTTP `/api/dsh-bridge/task`、`/api/dsh-bridge/review`、MCP `dsh_task`/`dsh_review`、CLI `task.mjs`/`review.mjs`(`--session`/`--find`/`--cwd` 三种目标一致;评审与编码同 kind=review/worker 语义,评审也支持 `--origin`、模型预检与逐次 status 来源核对)。原生工具 `dsh_collab_send`/`dsh_collab_review` 走同一任务级机制。`provider+model` 是**一对身份**,任何入口都不会只发一半。
|
|
446
|
+
- 只读目录:`GET /api/dsh-bridge/models`、MCP `dsh_models`、CLI `dsh-task --models`;`capabilities.modelRouting` 供客户端**预检**(旧网关遇到新语义零派活)。
|
|
447
|
+
- 归属与恢复:首请求用官方 `agent/inbox/claimed` 按派活前登记的 requestId 认领(不靠事后日志);失败只用**稳定 failure.code** 分类,同 turn/step 内切换(不重派 prompt、不新建 child、不重跑已执行工具);默认最多 3 个不同候选、每候选与整任务都有时限;用户取消/安全拦截/审批拒绝/工具错误/未知异常**不**靠切模型绕过。
|
|
448
|
+
- 边界:目录可见 ≠ 凭据/额度/远端健康已验证(没有质量/价格评分,`toolUpdate` 不是"支持工具"布尔);不使用 `selectModel`/`saveSelection`,不改 readonly options/全局 preset;任务收口时**条件恢复**用户原选择(人类后选的模型优先)。
|
|
449
|
+
- 证据:[docs/DSH_MODEL_ROUTING_M0_EVIDENCE.md](docs/DSH_MODEL_ROUTING_M0_EVIDENCE.md);审计(稳定 code,无正文/密钥)落 `<assoc>/model-routing.jsonl`,任务状态可查 `route` 与 `routeAudit`。
|
|
450
|
+
- **认证**:`X-Dsh-Control` 传**私有 raw key**(MCP/CLI 内部读取;服务端只存域分离单向摘要,**摘要不能用于登录**);缺 key = 能力关闭;不存在未认证的 enable/bootstrap/领 token 路由。
|
|
451
|
+
- **授权**:`origin/task/session/callId` 只是定位符,**不构成授权**;真正的授权 = 绑定 task/origin/session/版本/期限的不可变 grant + 服务端按**不可变宿主快照**核对的指纹 + 单次状态机 + 审计写成功才放行。审批查询/决定必须显式带 `originThreadId`+`taskId`(服务端硬核对,绝不跨来源泄露)。
|
|
452
|
+
- **投递控制 queue / steer(第四轮新功能)**:DSH 忙时不再只有一个"busy 拒绝"——Codex 可按任务关系**自己选择**:
|
|
453
|
+
- `queue`(默认):排进**官方原生 FIFO**,当前任务完成后执行;忙时也可靠接收(202),每个任务有独立 taskId/requestId/回合/结果,互不覆盖。官方 `claim()` 一次只取一条 next-turn(`dsh-agent-loop:103-109`),所以排队任务各自独占自己的回合。
|
|
454
|
+
- `steer`:作为**补充回执**插进**显式父任务**的当前回合(官方 `next-step`,同 turn 被消费)。必须给 `parentTaskId`(可选 `expectedTurn`),父必须同 origin/同会话**正在执行**;回执有独立 taskId/observedTurn,**不签发也不扩大父 grant、不成为审批 owner、不伪装独立结果**(`resultScope: parent-task`),父任务的最终结果仍归父任务。
|
|
455
|
+
- 由 Codex 在工具参数里显式选 `deliver`(服务器**不按关键词猜**急迫程度);`--steer` 兼容保留但语义改为"父回执"。HTTP/MCP/CLI/DSH 原生工具都支持;steer 复用既有离线控制 key 做内部鉴权(MCP/CLI 自动带头,**缺凭据零插入**),旧网关缺 `capabilities.delivery` 时**零派活**拒绝。旧无来源 queue 路径行为不变(忙锁保持)。
|
|
456
|
+
- 官方契约证据:[docs/DSH_DELIVERY_M0_EVIDENCE.md](docs/DSH_DELIVERY_M0_EVIDENCE.md)(真实 SDK 队列顺序/消费时机/竞态)。
|
|
457
|
+
- **语义边界**:只 `approve-once`(官方唯一许可,且由那一次调用消费);官方 ask 身份必须唯一认领(同 callId 两次 ask = 两条独立 pending、两次独立决定);`next()` 交还人工、绝不吞链;**完整执行身份**(toolName/root/parent/cwd/真实参数 + 日志实时 turn/step)任一变化即拒绝旧审批,身份拿不到时明确 unsupported 而不放行;有效认证委托按 **grant 有效期**观察(不被 quiet/总预算误收口),失联/撤销/到期后有界退出并交还人工;`needs-approval` 保持父任务在途及审批所有权;带来源的新 queue 可排队,旧无来源路径仍保留忙锁;活跃审批绝不重复列进 `unanswerable`(重启恢复语义不变:submitted→decision-unknown、未决定→unanswerable);到期/失联/取消/卸载/内部错误 → 有界收口或 `next()` 且**先关闭可答状态**(迟到批准被拒),**绝不凭超时放行**;自己不写 `approval/asked|decided`;官方 `decided` 的 outcome 决定状态(cancelled/rejected 绝不算批准,reject 的终态是 `rejected` 而非 `confirmed`);不动 `approval/policy`/preset/never。
|
|
458
|
+
- **如实报 false 的能力**:worker 子代理目标(child 身份未证明,委托派活前拒绝)、PTC(callId/root 未证明,嵌套调用一律 `next()`)、空闲自动唤醒(仍 blocked)。
|
|
459
|
+
- **加载前提**:答复者是宿主 bundle 插件——**安装更新包 + 重启 profile** 后才在真实桌面生效。
|
|
460
|
+
- 详见 [docs/DSH_APPROVAL_DELEGATION.md](docs/DSH_APPROVAL_DELEGATION.md);官方接口取证见 [docs/CODEX_DSH_APPROVAL_M0_EVIDENCE.md](docs/CODEX_DSH_APPROVAL_M0_EVIDENCE.md)。
|
|
461
|
+
|
|
392
462
|
## 环境变量
|
|
393
463
|
|
|
394
464
|
| 变量 | 默认 | 说明 |
|
|
@@ -397,10 +467,12 @@ error/blocked 不得 done、snapshot.records、重复投递不串文本、快照
|
|
|
397
467
|
| `DSH_BRIDGE_WORKSPACE` | `D:\Harness` | 网关默认共享工作目录 |
|
|
398
468
|
| `DSH_BRIDGE_ROOT` | `D:\Harness` | `dsh_read_file` 相对路径基准 |
|
|
399
469
|
| `DSH_BRIDGE_HISTORY` | 多级降级 | 协作历史文件位置(默认 D 盘 → 用户目录 → 临时目录逐级降级) |
|
|
400
|
-
| `DSH_BRIDGE_SESSION_DISPATCH_TIMEOUT_MS` | `120000` |
|
|
401
|
-
| `DSH_BRIDGE_SESSION_QUIET_TIMEOUT_MS` | `600000` |
|
|
470
|
+
| `DSH_BRIDGE_SESSION_DISPATCH_TIMEOUT_MS` | `120000` | 未开跑的观察阈值;prompt 已被接受时仅提示排队,不据此判执行失败 |
|
|
471
|
+
| `DSH_BRIDGE_SESSION_QUIET_TIMEOUT_MS` | `600000` | 安静观察阈值;带来源任务缺少自身 turn/end 时只能报 unknown,不能据此判完成;有效委托不因 quiet 提前收口 |
|
|
402
472
|
| `DSH_BRIDGE_SESSION_TOTAL_TIMEOUT_MS` | `3600000` | 单个会话任务的总预算 |
|
|
403
473
|
| `DSH_CODEX_TASK_ASSOC_DIR` | `~/.dsh-codex-task-assoc` | 任务关联记录的存储根(**默认只在用户目录,绝不写仓库**) |
|
|
474
|
+
| `DSH_APPROVAL_CONTROL_KEY_PATH` | `~/.dsh-codex-approval/control.key` | 审批控制凭据文件(**仓库/工作区之外**,64-hex;宿主与 MCP/CLI 内部读取,HTTP 以 `X-Dsh-Control` 传私有 raw key)。缺文件或格式不合规=审批能力关闭 |
|
|
475
|
+
| `DSH_APPROVAL_DIR` | `~/.dsh-codex-approval` | 审批 grant/审计存储根(JSONL;测试用独立临时根) |
|
|
404
476
|
| `DSH_BRIDGE_ASSOC_NAMESPACE` | `default` | 关联记录的命名空间(`[A-Za-z0-9._-]{1,64}`);多 profile 可各自一个来分隔存储 |
|
|
405
477
|
|
|
406
478
|
## 已知限制
|
|
@@ -409,7 +481,7 @@ error/blocked 不得 done、snapshot.records、重复投递不串文本、快照
|
|
|
409
481
|
- **`sessionController.search` 可能被部署禁用**(`session-query` 索引 `openAt: "never"`)。此时按标题查找自动退回 `list`(读持久化 header + 标题投影,同样不激活 Agent),响应里的 `matchedBy` 标明走了哪条路
|
|
410
482
|
- **不能投递进子代理会话**:`resolveAgent` 对 subagent 路由拥有的会话返回 `session-busy: owned by subagent routing`;只能接普通会话
|
|
411
483
|
- `POST /api/dsh-bridge/sessions {"create": true, "cwd": "..."}` 可新建一条空白会话(探针/一次性任务用),它不会碰任何已有会话
|
|
412
|
-
-
|
|
484
|
+
- **带来源的会话任务支持多个 queue**,各自绑定请求和回合;旧无来源任务仍有忙锁,并拒绝与带来源任务在同一会话混用。补充必须指定正在运行的父任务;无法逐项安全撤回队列/回执时拒绝取消,避免清掉别人的队列。
|
|
413
485
|
- **冷会话可能接不上**:DSH 激活 Agent 需要会话投影(`Agent activation requires a projected Session observation`),未激活的会话会返回 `session-not-activatable`;归档会话的回合会以 `blocked` 收口(`session-archived`)
|
|
414
486
|
- **评审不支持会话目标**:评审员必须与编码会话隔离,"独立评审"才成立
|
|
415
487
|
- session 目标不使用 `cwd`/`lane`/`model`/`--commit`(会话自带这些语义),传了会被判 `invalid-target`
|
|
@@ -431,13 +503,21 @@ error/blocked 不得 done、snapshot.records、重复投递不串文本、快照
|
|
|
431
503
|
```sh
|
|
432
504
|
node test/session-target.test.mjs # 纯函数:目标判定 / 候选挑选 / 错误映射 / 基线切分 / 汇报选文
|
|
433
505
|
node test/dsh-bridge.test.mjs # 路由级:假 DSH 运行时 + 桩 peer,加载真实 bridge 跑真实路由
|
|
434
|
-
node --test test/*.test.mjs #
|
|
506
|
+
node --test test/*.test.mjs # 全部行为回归(CI 用的是这条)
|
|
435
507
|
```
|
|
436
508
|
|
|
437
509
|
- 路由级测试会断言 **worker 分支确实调用了 `startContinuable`**、**session 分支只调用 `prompt` 且不建 owner/子代理**——这类"参数遮蔽/串线"缺陷只有在这一层才抓得到(v0.1.2~0.1.4 的 `deliver` 遮蔽 bug 就是它抓回来的)。
|
|
438
|
-
- CI(`.github/workflows/publish.yml`)每次 tag 都跑:语法检查 → 单测 → 包契约 → 版本号三处一致 → tarball 清单 → 干净目录安装 + CLI/MCP 冒烟 →
|
|
510
|
+
- CI(`.github/workflows/publish.yml`)每次 tag 都跑:语法检查 → 单测 → 包契约 → 版本号三处一致 → tarball 清单 → 干净目录安装 + CLI/MCP 冒烟 → 8 工具握手断言 → bridge 导出契约。**任何一步失败都不会发布**。
|
|
439
511
|
- 发布:`package.json` / `.codex-plugin/plugin.json` / `mcp/dsh-mcp.mjs` 的 `SERVER_VERSION` 三处同步改版本 → commit → 打 tag `vX.Y.Z` → 推 tag,CI 自动带 provenance 发布。
|
|
440
512
|
|
|
513
|
+
## 项目来源与鸣谢
|
|
514
|
+
|
|
515
|
+
- **上游原项目**:[FuJackGao/dsh-collab](https://github.com/FuJackGao/dsh-collab)。
|
|
516
|
+
- **原作者**:[FuJackGao](https://github.com/FuJackGao),以及参与上游项目的所有贡献者。感谢他们提供原始协作实现与开源基础。
|
|
517
|
+
- **本 fork**:[Whale-Talk/dsh-codex-collab](https://github.com/Whale-Talk/dsh-codex-collab)。后续工作在上游基础上开展,包括 npm 包分发、任务与来源对话关联、审批委托及 queue/steer 投递控制等扩展;各功能是否已发布,以本文的发布状态和 npm 实际版本为准。
|
|
518
|
+
|
|
519
|
+
本项目保留原有版权与许可声明,许可证见 [LICENSE](LICENSE)。
|
|
520
|
+
|
|
441
521
|
## License
|
|
442
522
|
|
|
443
523
|
MIT
|
|
@@ -58,7 +58,9 @@ dsh-task --in "<完整中文指令>" --find "<标题关键词>" # 唯一命
|
|
|
58
58
|
- `--lane`:并行通道名(默认 main)。同一目录不同 lane 的任务**并行执行**;跨目录天然并行。
|
|
59
59
|
- `--model`:`fast`(deepseek-v4-flash,快而省)或 `pro`(deepseek-v4-pro,质量优先);默认继承当前模型。session 目标不使用此参数(会话自己有模型)。
|
|
60
60
|
- `--commit`:任务完成后在 `--cwd` 自动 `git add -A` 并提交;非 git 仓库自动跳过。session 目标禁止使用(会话自带工作目录,本工具不替它提交)。
|
|
61
|
-
- `--steer
|
|
61
|
+
- `--steer` / `--parent <父taskId>`:仅 session 目标。**默认 `queue`**(排进官方 FIFO,等当前任务忙完再跑);
|
|
62
|
+
`--parent`(等价 `--steer --parent`)表示"**作为补充回执插进那条父任务的当前回合**"。用户已授权你在同一任务范围内**自主选择**:
|
|
63
|
+
需要排队就 `queue`,需要补充/纠正正在执行的父任务就 `steer`;**用户显式指定的模式优先**。steer 必须给正确父任务(同来源/同会话/正在执行/回合可证)并自带控制鉴权,否则执行前零投递拒绝。
|
|
62
64
|
- 指令必须包含:目标与产出物、验收标准、约束。`--wait` 为默认行为(结果直接回本对话),长任务加 `--timeout 1800`。
|
|
63
65
|
|
|
64
66
|
退出码:`0` 成功 / `1` 失败 / `2` 用法(含缺少来源、`--origin` 缺值、网关不支持来源绑定) / `3` 超时 / `4` 会话未命中或不唯一 / `5` session 目标取消被拒 / `6` 按 taskId 查询取不到(未知任务 / 已关联但缺来源 / 来源不匹配) / `7` 派活被拒(通道忙) / `8` 网关未确认来源绑定(可能已执行,**不要重发**)。
|
|
@@ -86,8 +88,13 @@ dsh-task --status "<taskId>" --origin "<当前对话id>"
|
|
|
86
88
|
只收该回合的助手文本;只有该回合并以 `completed` 结束才算成功,`aborted/blocked/error/max-tokens/interrupted/forked`
|
|
87
89
|
一律是**明确失败**(部分文字放在 `partialResult`,仅内存可见;落盘的是 `status`/`error`/`reason`/`requestId`/`dshTurn`)。
|
|
88
90
|
因此"会话里别的回合跑完"不会把你的任务判成完成。
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
+
- **带来源时的 `--steer`(第四轮投递控制)**:来源绑定 steer **是支持的**,但必须是**父任务回执**——
|
|
92
|
+
显式给 `--parent <父taskId>`(可选 `--expected-turn`),父任务须同来源/同会话/**正在执行且回合可证**,
|
|
93
|
+
且本机要有控制 key(steer 改动既有任务,缺 key 零投递)。
|
|
94
|
+
预检不通过(无父 / 父已结束 / 父还在排队 / 来源不符 / 回合不符 / 缺凭据)→ **执行前拒绝、零投递**。
|
|
95
|
+
回执只报**自身**状态(`receipt-observed` / `receipt-deviated` / `receipt-unconfirmed` / `receipt-pending`,`resultScope=parent-task`),
|
|
96
|
+
**不代表父任务结果、也不参与审批归属**;未观察到时 `resend:false`,父任务结果请查父 task。
|
|
97
|
+
非 session 目标(worker)不走 steer。
|
|
91
98
|
- **没有证据就是未确认**:`api-session/status` 只表示 agent 是否在跑,**不是**你的请求完成的证明。
|
|
92
99
|
若一直没观察到匹配回合的结束(例如快照/日志里缺前半段),到时会返回 `status: "unknown"` + `resend: false`——**不要重发**。
|
|
93
100
|
- **网关必须回显来源才算确认**:派活/取值的响应里必须带**匹配的** `originThreadId`;若网关接受了请求却没回显(例如中途重载/旧 handler),
|
|
@@ -123,8 +130,61 @@ dsh-task --cancel <taskId> # 取消 worker 任务
|
|
|
123
130
|
dsh-task --cancel <taskId> --force # 取消 session 目标任务(会打断用户自己的回合,默认被拒)
|
|
124
131
|
```
|
|
125
132
|
|
|
133
|
+
## 审批委托(任务等审批时由你逐次决定,用户不在也能继续)
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# 0) 只做一次:离线初始化控制凭据(仓库/工作区之外的私有目录;raw key 不回显、不上线)
|
|
137
|
+
dsh-task --approval-key-init # 轮换用 --approval-key-rotate(会让全部旧 grant 立即失效)
|
|
138
|
+
|
|
139
|
+
# 1) 派活时显式开启委托(grant 与 task+origin 绑定先于 prompt;失败=零派活)
|
|
140
|
+
dsh-task --in "<指令>" --session <id> --origin <对话id> --delegate [--delegate-ttl 秒] [--delegate-lease 秒] [--authorization "本次授权范围"]
|
|
141
|
+
# 初次默认等待与下面的 --status-wait 是同一条有界等待路径:带 origin 逐次核对回显、按租约自适应认证续租、
|
|
142
|
+
# 遇到 needs-approval 立即 exit 9(不必先手动查 grant,也不必再调一次 --status-wait 才能续租)。
|
|
143
|
+
|
|
144
|
+
# 2) 任务等你审批时会以退出码 9 立即返回(不是失败、不是完成)
|
|
145
|
+
dsh-task --status <taskId> --origin <id> # status=needs-approval 时看 needsApproval/approvalPendingCount
|
|
146
|
+
dsh-task --approvals --origin <id> --task <taskId> [--grant <grantId>] [--wait-approval 秒]
|
|
147
|
+
|
|
148
|
+
# 3) 逐条单次决定(approvalKey/grantId/fingerprint 都来自上一步;多余字段与参数副本一律被拒)
|
|
149
|
+
dsh-task --approve <approvalKey> --origin <id> --task <taskId> --grant <grantId> --fingerprint <fp> [--idem 幂等键]
|
|
150
|
+
dsh-task --reject <approvalKey> --origin <id> --task <taskId> --grant <grantId> --fingerprint <fp>
|
|
151
|
+
|
|
152
|
+
# 4) 决定后继续收结果(等待期间会做认证续租)
|
|
153
|
+
dsh-task --status <taskId> --origin <id> [--grant <grantId>] --status-wait
|
|
154
|
+
|
|
155
|
+
# 撤销 / 在原授权窗口内显式续期
|
|
156
|
+
dsh-task --revoke-grant <grantId> --origin <id> --task <taskId> [--reason "…"]
|
|
157
|
+
dsh-task --renew-grant <grantId> --origin <id> --task <taskId> [--extend 3600]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- **MCP 等价**:`dsh_task { delegate: true, authorization }` / `dsh_approvals { originThreadId, taskId, … }` /
|
|
161
|
+
`dsh_approval_decide { originThreadId, taskId, approvalKey, grantId, fingerprint, decision: "approve-once"|"reject" }`;
|
|
162
|
+
`dsh_task_status { wait: true, grantId }` 可阻塞轮询到终态(遇 `needs-approval` 会立即返回,等待期间认证续租)。
|
|
163
|
+
- **`origin`+`task` 是必填定位**(服务端还会与 pending/grant 硬核对);它们**不是授权**,绝不能只凭它们放行。
|
|
164
|
+
- 控制凭据只在 MCP/CLI 内部读取并作为 `X-Dsh-Control` 头发出;raw key 不出现在参数、结果、错误或审计里。
|
|
165
|
+
- **看到 `needs-approval` 不要重发任务**:它是非终态,只差你对挂起项做一次决定。
|
|
166
|
+
- `submitted ≠ confirmed`:再查一次看到 `state=confirmed`(官方 `approval/decided=allowed-once` 已观测)才算确认;`decision-unknown` 表示确认丢失——**不要重发放行**;若官方 outcome 是 cancelled/rejected,绝不会被记成批准。
|
|
167
|
+
- 只接手能严格证明属于本委托的请求:无 callId / 无宿主参数快照 / 工具名不符 / turn·step 证明不了 / 不是自己那个 turn / PTC 嵌套(`…:ptc:<n>`)/ 父子混淆 / 官方 ask 身份歧义 → 一律交还人工(`next()`);worker 与 PTC 能力如实报 false。
|
|
168
|
+
- 审批类退出码:`9` = 等待审批(非终态);`0-8` 语义不变;缺本机 key / 预检不支持 → `2` 且**零派活**。
|
|
169
|
+
|
|
170
|
+
- **投递模式(queue / steer,第四轮)**:DSH 忙时由**你**按任务关系选,服务器不猜急迫程度:
|
|
171
|
+
- `queue`(默认):排进官方原生 FIFO,当前任务完成后执行;忙时也可靠接收。每个任务独立 taskId/requestId/回合/结果。
|
|
172
|
+
- `steer`:**补充/纠正当前任务**时用,插进父任务的当前回合(同 turn 消费)。**必须显式给父任务**:
|
|
173
|
+
`dsh-task --in "<补充>" --session <id> --origin <对话id> --parent <父taskId> [--expected-turn N]`;
|
|
174
|
+
MCP: `dsh_task { deliver: "steer", parentTaskId, expectedTurn }`。父必须同 origin/同会话**正在执行**;父未开始/已结束/来源不符/回合不符 → 零投递拒绝。
|
|
175
|
+
- 回执语义:有独立 taskId 与 `observedTurn`,`resultScope=parent-task`;**不等于**父任务结果,也**不签发/扩大**父 grant、不自动决定旧 pending。回执 `receipt-unconfirmed` 表示"官方已接收但没观察到落进父回合"——**不要重发**;父任务结果查 `--status <父taskId> --origin <id> --status-wait`。
|
|
176
|
+
- CLI/MCP/**DSH 原生工具**都会校验控制凭据(steer 改动既有任务):CLI/MCP 自动带 `X-Dsh-Control` 头,原生工具只读**本机已有**可信 key(绝不接受调用方传入的 raw key);缺 key/坏 key → **零插入**。普通非委托 queue 不增加凭据前提。旧网关缺 `capabilities.delivery` → 零派活拒绝。
|
|
177
|
+
|
|
126
178
|
## 故障排查
|
|
127
179
|
|
|
180
|
+
- **模型选择(第五轮,`model:auto` 为默认)**:派活时**不用**再猜固定型号——插件会从本机**已注册** provider 目录里选并回显:
|
|
181
|
+
- 缺省/`--model auto`:按真实能力(模态/上下文/effort 兼容)+ DeepSeek 偏好 + 任务类型自动选;`fast`/`pro` 仍是兼容偏好;`--model default` 表示**继承**当前选择(不覆盖)。
|
|
182
|
+
- `--model <精确id> [--provider <p>]`:精确指定;同一模型名在多个 provider 出现时**必须**给 `--provider`,否则拒绝并列出候选。精确指定默认**不**自动降级,要降级须显式 `--model-fallback`;`--no-model-fallback` 可显式关闭。
|
|
183
|
+
- 只读目录:`dsh-task --models`(或 MCP `dsh_models`)列出候选 pair、目录失败隔离、已知临时失效与冷却;**目录可见 ≠ 凭据/额度/远端健康已验证**,也没有质量/价格评分。
|
|
184
|
+
- 失效切换边界:只按**稳定 failure.code** 分类;认证/额度/模型失效可切,限流/网络先有界重试再切;**用户取消、拒答/安全、审批拒绝、工具错误、未知插件异常都不切**。默认最多 3 个不同候选,且同 turn/step 内恢复(不重派提示、不丢已执行工具、不新建子代理)。
|
|
185
|
+
- 任务状态可查 `route`(实际选择/模式/是否允许降级)与 `routeAudit`(切换次数/原因/终态,跨重启保留;只存稳定 code,无正文/密钥)。
|
|
186
|
+
- **评审与编码同一契约**:`dsh_review` / `review.mjs` / 原生 `dsh_collab_review` 默认也是 `auto`(优先 DeepSeek),同样支持 `--model/--provider/--model-fallback`;`review.mjs` 还要求**显式来源**(`--origin` 或本次调用的 `CODEX_THREAD_ID`)并逐次 status 核对来源,旧网关遇到模型新语义**零派活**拒绝。`--session`/`--find`/`--cwd` 三种目标的模型参数都成对发送(`provider+model` 是一对身份,不允许只发一半)。
|
|
187
|
+
|
|
128
188
|
- 连接失败:DeepSeek Harness 未运行(网关应监听本机端口,默认 3080;挂在 web/desktop profile 时也可能就是 GUI 的端口)。
|
|
129
189
|
- 派 session 任务返回 `session-writer-held` / `session-busy`:那条会话被别的写入方或另一个 bridge 任务占用;稍后重试,或改用 worker 目标。
|
|
130
190
|
- 返回 `session-busy: owned by subagent routing`:那是子代理会话,**不能作为投递目标**;只能接普通会话。
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
// Codex → DeepSeek Harness 评审脚本:把代码变更 diff 交给独立评审子代理。
|
|
3
3
|
// 评审员会读 cwd 真实文件、静态审查、并实际运行构建/测试验证可跑通。
|
|
4
4
|
// 用法: node review.mjs --cwd <目录> [--diff <diff 文本|@文件|git|git-staged>] [--focus <审查重点>] [--timeout 秒]
|
|
5
|
+
// [--origin <Codex 对话 id>] [--model auto|fast|pro|default|<精确 model id>] [--provider <provider id>]
|
|
6
|
+
// [--model-fallback | --no-model-fallback]
|
|
7
|
+
//
|
|
8
|
+
// 评审与编码使用**同一套模型契约**(服务端任务 kind=review、执行目标 worker):
|
|
9
|
+
// - 默认 auto(优先兼容 DeepSeek);
|
|
10
|
+
// - 显式 provider+model 是一对身份,必须成对发送;
|
|
11
|
+
// - 旧网关没有 capabilities.modelRouting / originBinding 时 → **零派活**拒绝,绝不忽略新参数照常执行。
|
|
5
12
|
import fs from 'node:fs'
|
|
6
13
|
import http from 'node:http'
|
|
7
14
|
import { execFileSync } from 'node:child_process'
|
|
@@ -9,7 +16,7 @@ import { execFileSync } from 'node:child_process'
|
|
|
9
16
|
const GATEWAY = process.env.DSH_BRIDGE_URL || 'http://127.0.0.1:3080'
|
|
10
17
|
|
|
11
18
|
function parseArgs(argv) {
|
|
12
|
-
const out = { timeoutSec: 900, cwd: '', diff: '', focus: '' }
|
|
19
|
+
const out = { timeoutSec: 900, cwd: '', diff: '', focus: '', origin: '', model: '', provider: '', valueError: undefined }
|
|
13
20
|
const rest = []
|
|
14
21
|
for (let i = 0; i < argv.length; i++) {
|
|
15
22
|
const a = argv[i]
|
|
@@ -17,6 +24,25 @@ function parseArgs(argv) {
|
|
|
17
24
|
else if (a === '--diff') { out.diff = argv[++i] || '' }
|
|
18
25
|
else if (a === '--focus') { out.focus = argv[++i] || '' }
|
|
19
26
|
else if (a === '--timeout') { const v = Number(argv[++i]); if (Number.isFinite(v)) out.timeoutSec = v }
|
|
27
|
+
// ---- 模型路由(与 task.mjs 同一契约)----
|
|
28
|
+
else if (a === '--model') {
|
|
29
|
+
const v = argv[++i]
|
|
30
|
+
if (v === undefined || v.trim() === '' || v.startsWith('--')) { out.valueError = '--model 需要一个值(auto/fast/pro/default 或精确 model id)'; continue }
|
|
31
|
+
out.model = v.trim()
|
|
32
|
+
}
|
|
33
|
+
else if (a === '--provider') {
|
|
34
|
+
const v = argv[++i]
|
|
35
|
+
if (v === undefined || v.trim() === '' || v.startsWith('--')) { out.valueError = '--provider 需要一个值(已注册的 provider id)'; continue }
|
|
36
|
+
out.provider = v.trim()
|
|
37
|
+
}
|
|
38
|
+
else if (a === '--model-fallback') { out.modelFallback = true }
|
|
39
|
+
else if (a === '--no-model-fallback') { out.modelFallback = false }
|
|
40
|
+
// ---- 来源(显式,跨回合精确收取)----
|
|
41
|
+
else if (a === '--origin') {
|
|
42
|
+
const v = argv[++i]
|
|
43
|
+
if (v === undefined || v.trim() === '' || v.startsWith('--')) { out.valueError = '--origin 需要一个值(Codex 对话 id)'; continue }
|
|
44
|
+
out.origin = v.trim()
|
|
45
|
+
}
|
|
20
46
|
else { rest.push(a) }
|
|
21
47
|
}
|
|
22
48
|
return out
|
|
@@ -76,41 +102,100 @@ function resolveDiff(cwd, diffSpec) {
|
|
|
76
102
|
return diffSpec
|
|
77
103
|
}
|
|
78
104
|
|
|
105
|
+
/** 来源解析:显式 --origin 优先,其次本次调用的 CODEX_THREAD_ID(绝不推断"最近对话")。 */
|
|
106
|
+
function resolveOrigin(opts) {
|
|
107
|
+
if (typeof opts.origin === 'string' && opts.origin.trim() !== '') return { originThreadId: opts.origin.trim(), source: 'explicit' }
|
|
108
|
+
const fromEnv = typeof process.env.CODEX_THREAD_ID === 'string' ? process.env.CODEX_THREAD_ID.trim() : ''
|
|
109
|
+
if (fromEnv !== '') return { originThreadId: fromEnv, source: 'env' }
|
|
110
|
+
return { source: 'none' }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 只读能力探测(GET /status,不派活)。 */
|
|
114
|
+
async function capabilityOf() {
|
|
115
|
+
try {
|
|
116
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/status')
|
|
117
|
+
const body = res.json
|
|
118
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: 'status-http-' + res.status }
|
|
119
|
+
if (body === null || typeof body !== 'object' || body.capabilities === null || typeof body.capabilities !== 'object') return { ok: false, reason: 'no-capabilities' }
|
|
120
|
+
return { ok: true, capabilities: body.capabilities }
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return { ok: false, reason: 'status-unreachable' }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
79
126
|
async function main() {
|
|
80
127
|
const opts = parseArgs(process.argv.slice(2))
|
|
128
|
+
if (opts.valueError !== undefined) { console.error('review.mjs: ' + opts.valueError); finish(2) }
|
|
81
129
|
const cwd = (opts.cwd || '').trim()
|
|
82
130
|
if (!cwd) {
|
|
83
|
-
console.error('用法: node review.mjs --cwd <目录> [--diff git|git-staged|@文件|"diff 文本"] [--focus <重点>] [--timeout 秒]')
|
|
131
|
+
console.error('用法: node review.mjs --cwd <目录> [--diff git|git-staged|@文件|"diff 文本"] [--focus <重点>] [--timeout 秒] [--origin <id>] [--model ...] [--provider <id>] [--model-fallback|--no-model-fallback]')
|
|
84
132
|
finish(2)
|
|
85
133
|
}
|
|
86
134
|
if (!fs.existsSync(cwd)) {
|
|
87
135
|
console.error('review.mjs: 目录不存在: ' + cwd)
|
|
88
136
|
finish(2)
|
|
89
137
|
}
|
|
138
|
+
const origin = resolveOrigin(opts)
|
|
139
|
+
const usesModelControl = opts.model !== '' || opts.provider !== '' || opts.modelFallback !== undefined
|
|
140
|
+
const cap = await capabilityOf()
|
|
141
|
+
if (!cap.ok) {
|
|
142
|
+
// 连能力都读不到:派活前拒绝(旧网关/未装载插件),绝不忽略新参数照常执行。
|
|
143
|
+
console.error('review.mjs: 无法确认网关能力(' + cap.reason + ')——评审派活拒绝,未执行任何投递。')
|
|
144
|
+
finish(2)
|
|
145
|
+
}
|
|
146
|
+
if (origin.originThreadId === undefined) {
|
|
147
|
+
console.error('review.mjs: 评审派活需要**显式来源**(--origin 或本次调用的 CODEX_THREAD_ID),否则无法跨回合精确收取。')
|
|
148
|
+
finish(2)
|
|
149
|
+
}
|
|
150
|
+
if (cap.capabilities.originBinding === undefined || cap.capabilities.originBinding.supported !== true) {
|
|
151
|
+
console.error('review.mjs: 该网关不支持持久来源绑定(capabilities.originBinding)——评审派活拒绝,未执行任何投递。')
|
|
152
|
+
finish(2)
|
|
153
|
+
}
|
|
154
|
+
if (usesModelControl && (cap.capabilities.modelRouting === undefined || cap.capabilities.modelRouting.supported !== true)) {
|
|
155
|
+
console.error('review.mjs: 该网关未声明 capabilities.modelRouting(旧网关)——带模型参数的评审派活拒绝,未执行任何投递。')
|
|
156
|
+
finish(2)
|
|
157
|
+
}
|
|
90
158
|
const diff = resolveDiff(cwd, opts.diff)
|
|
91
|
-
console.log('[review] diff 长度: ' + diff.length + '
|
|
159
|
+
console.log('[review] diff 长度: ' + diff.length + ' 字符;来源 ' + origin.originThreadId + '(' + origin.source + ')')
|
|
92
160
|
const posted = await request('POST', GATEWAY + '/api/dsh-bridge/review', {
|
|
93
161
|
cwd, diff,
|
|
94
162
|
...(opts.focus ? { focus: opts.focus } : {}),
|
|
163
|
+
originThreadId: origin.originThreadId,
|
|
164
|
+
...(opts.provider !== '' ? { provider: opts.provider } : {}),
|
|
165
|
+
...(opts.model !== '' ? { model: opts.model } : {}),
|
|
166
|
+
...(opts.modelFallback === undefined ? {} : { modelFallback: opts.modelFallback }),
|
|
95
167
|
})
|
|
96
168
|
const task = posted.json
|
|
97
169
|
console.log(JSON.stringify({ phase: 'submitted', ...task }))
|
|
98
|
-
if (
|
|
170
|
+
if (task !== null && typeof task === 'object' && task.taskId !== undefined && task.originThreadId !== origin.originThreadId) {
|
|
171
|
+
// 网关接受了请求但没回显我们的来源:绑定**未被确认**(可能已执行)→ 不重发。
|
|
172
|
+
console.error('review.mjs: 网关未回显匹配的来源 originThreadId —— 绑定未被确认,**不要重发**。')
|
|
173
|
+
finish(1)
|
|
174
|
+
}
|
|
175
|
+
if (posted.status !== 202 || task === null || typeof task !== 'object' || task.status === 'error') finish(posted.status === 202 ? 0 : 1)
|
|
99
176
|
|
|
100
177
|
const deadline = Date.now() + opts.timeoutSec * 1000
|
|
101
178
|
while (Date.now() < deadline) {
|
|
102
179
|
await sleep(3000)
|
|
103
|
-
const res = await request('GET', GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId))
|
|
180
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId) + '&originThreadId=' + encodeURIComponent(origin.originThreadId))
|
|
104
181
|
const status = res.json
|
|
182
|
+
if (status === null || typeof status !== 'object') continue
|
|
183
|
+
if (status.status === 'unknown' && status.recovered === true) {
|
|
184
|
+
// 执行状态未知:如实报告,绝不重发、不无限等
|
|
185
|
+
console.error('review.mjs: 任务执行状态未确认(网关重启/重载),不重发;原任务: ' + task.taskId)
|
|
186
|
+
console.log(JSON.stringify({ phase: 'unconfirmed', ...status }))
|
|
187
|
+
finish(1)
|
|
188
|
+
}
|
|
105
189
|
if (status.status === 'done' || status.status === 'error' || status.status === 'cancelled') {
|
|
106
190
|
console.log(JSON.stringify({ phase: 'finished', ...status }))
|
|
191
|
+
if (status.route !== undefined) console.log('[review] route: ' + JSON.stringify(status.route))
|
|
107
192
|
console.log('=== 评审报告 ===')
|
|
108
193
|
console.log(status.result || status.error || '(无内容)')
|
|
109
194
|
finish(status.status === 'done' ? 0 : 1)
|
|
110
195
|
}
|
|
111
196
|
}
|
|
112
197
|
console.error('等待超时(' + opts.timeoutSec + 's),评审仍在后台执行:')
|
|
113
|
-
console.error(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId))
|
|
198
|
+
console.error(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId) + '&originThreadId=' + encodeURIComponent(origin.originThreadId))
|
|
114
199
|
finish(3)
|
|
115
200
|
}
|
|
116
201
|
|