dsh-plugin-subagent-bridge 0.15.0 → 0.15.1
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/README.md +200 -276
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,289 +5,220 @@
|
|
|
5
5
|
<!-- M8ven ownership proof — keep this line in place; it verifies the publisher. -->
|
|
6
6
|
[](https://m8ven.ai/mcp/dogtiti-dsh-plugin-subagent-bridge-8a3t8k?s=readme)
|
|
7
7
|
|
|
8
|
-
在 DeepSeek Harness 宿主内部提供**本机回环 MCP
|
|
8
|
+
在 DeepSeek Harness 宿主内部提供**本机回环 MCP 端点**,让另一个智能体把任务派给 DSH 会话并取回结果;反过来,DSH 里的 agent 也能把任务派给 Codex 或 Claude Code。
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
两个方向共用同一个插件,各自遵守对方产品的 `maxDepth: 1`:**能派活,不能再派回来**。
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## 能力一览
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
|
23
|
-
| --- | --- |
|
|
24
|
-
| `prompt` | 必填,任务正文 |
|
|
25
|
-
| `cwd` | 工作目录;默认用当前会话目录 |
|
|
26
|
-
| `resume` | Claude Code 的 session id,续接同一段对话 |
|
|
27
|
-
| `model` | 模型别名或 id;不传则用配置,再退回 Claude 自己的设置 |
|
|
28
|
-
|
|
29
|
-
返回 `{ ok, text, sessionId, durationMs }`,`sessionId` 就是下次 `resume` 用的那个。
|
|
30
|
-
|
|
31
|
-
和 `codex_dispatch` 的差别只在底层:Claude Code 的 `-p` 模式**一次输出一个 JSON 对象**(`{type:"result", result, session_id, is_error}`),不像 Codex 是事件流。所以解析器只看 stdout 的 JSON,**stderr 的警告不算失败**——比如模型名带 `[1M]` 后缀时 CLI 会警告,但仍然正常返回。
|
|
32
|
-
|
|
33
|
-
配置项:`claudeBin`(默认 `claude`)、`claudeModel`(默认空,即用 CLI 自己的设置)、`claudeTimeoutMs`(默认 300 秒,上限 240 秒夹取)。
|
|
34
|
-
|
|
35
|
-
## 创建项目(workspace)
|
|
36
|
-
|
|
37
|
-
DSH 的侧边栏分组是"项目(workspace)",和会话的工作目录是两回事:新建会话默认落在「未分组」,除非它所在的目录已经是一个项目。
|
|
38
|
-
|
|
39
|
-
`dsh_task` / `dsh_spawn` 的 `workspace` 参数把这一步也接管了:
|
|
40
|
-
|
|
41
|
-
```js
|
|
42
|
-
// 目录不是项目 → 用它创建项目(标题取目录名),会话直接归到项目下
|
|
43
|
-
dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true })
|
|
44
|
-
|
|
45
|
-
// 用指定标题创建
|
|
46
|
-
dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
|
|
47
|
-
|
|
48
|
-
// 不传 → 只复用已有项目;目录没有项目就保持未分组(和以前一样)
|
|
49
|
-
dsh_task({ prompt: "…", cwd: "/repo/clawer" })
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
行为细节:
|
|
53
|
-
|
|
54
|
-
- 目录不存在时先递归创建,再做项目登记(登记要求目录已存在);
|
|
55
|
-
- 目录已经是项目 → **直接复用,不改标题**(底层 `create` 对同一路径返回已有记录);
|
|
56
|
-
- 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
|
|
57
|
-
- 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
|
|
16
|
+
| 方向 | 入口 | 一次调用做什么 |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Codex / 任意 MCP 客户端 → DSH | `dsh_task` | 开会话、投递、阻塞到这一轮结束,只回最终答案 |
|
|
19
|
+
| 同上,并行 | `dsh_spawn` + `dsh_wait` | 立刻返回句柄,多个 worker 同时跑,再批量等 |
|
|
20
|
+
| 同上,续派 | `dsh_followup` | 复用 worker 的上下文再派一轮 |
|
|
21
|
+
| DSH → Codex | `codex_dispatch` | 跑一次 `codex exec`,回最终回答与可续接的线程 id |
|
|
22
|
+
| DSH → Claude Code | `claude_dispatch` | 跑一次 `claude -p`,回最终回答与可续接的 session id |
|
|
58
23
|
|
|
59
|
-
|
|
24
|
+
插件运行在宿主进程内,直接用公开服务(`sessionController`、`sessions`、`workspaceRegistry`、`userQuestions`),不需要读凭据文件,也不需要伪造 cookie。
|
|
60
25
|
|
|
61
|
-
|
|
26
|
+
---
|
|
62
27
|
|
|
63
|
-
|
|
28
|
+
## 安装
|
|
64
29
|
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/Dogtiti/dsh-plugin-subagent-bridge
|
|
65
32
|
```
|
|
66
|
-
(如果缺少必要信息或需要在多个方案里选择,直接采用最合理的默认做法继续做完,
|
|
67
|
-
不要把选择权抛回给用户、不要调用 ask_user_question 等回答;
|
|
68
|
-
在结果里用一行说明你假设了什么。)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
- 默认开启,配置项 `assumeDefaults: false` 可关掉;
|
|
72
|
-
- `dsh_task`、`dsh_spawn`、`dsh_followup` 三条路径都会加;
|
|
73
|
-
- prompt 里本来就提到 `ask_user_question` 时不追加,避免和调用方的明确意图打架。
|
|
74
33
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
## 回答 worker 的问题(`dsh_answer`)
|
|
78
|
-
|
|
79
|
-
worker 需要用户拍板时会调用 `ask_user_question`,然后**卡在那里等**。如果人不在,任务就停住了。
|
|
34
|
+
Plugins → 添加 → 选该目录。
|
|
80
35
|
|
|
81
|
-
|
|
36
|
+
启动后在 `127.0.0.1:47821` 监听,并把端点写到 `~/.dsh/bridge/endpoint.json`(目录 0700、文件 0600):
|
|
82
37
|
|
|
83
|
-
```
|
|
84
|
-
// dsh_wait 的返回
|
|
38
|
+
```json
|
|
85
39
|
{
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
"id": "mode",
|
|
93
|
-
"question": "用哪种模式?",
|
|
94
|
-
"header": "Choose Mode",
|
|
95
|
-
"options": [{ "label": "快速", "description": "只跑单测" }, { "label": "完整" }]
|
|
96
|
-
}
|
|
97
|
-
]
|
|
98
|
-
},
|
|
99
|
-
"next": "blocked on a question: answer it with dsh_answer, or call dsh_wait again"
|
|
40
|
+
"url": "http://127.0.0.1:47821/mcp/<token>",
|
|
41
|
+
"port": 47821,
|
|
42
|
+
"token": "<token>",
|
|
43
|
+
"pid": 12345,
|
|
44
|
+
"startedAt": "2026-10-22T12:30:00.000Z"
|
|
100
45
|
}
|
|
101
46
|
```
|
|
102
47
|
|
|
103
|
-
|
|
48
|
+
## 接到 Codex
|
|
104
49
|
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
})
|
|
50
|
+
```bash
|
|
51
|
+
codex mcp add dsh-bridge --url "$(node -p "require(process.env.HOME+'/.dsh/bridge/endpoint.json').url")"
|
|
52
|
+
codex mcp list # 验证
|
|
53
|
+
codex mcp remove dsh-bridge # 移除
|
|
110
54
|
```
|
|
111
55
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
- 一个问题的 id 必须**恰好出现一次**,否则底层会报 `BAD_ANSWER`;
|
|
115
|
-
- `selected` 是选项 label 数组,多选时给多个;跳过一个问题就给空数组;
|
|
116
|
-
- 回答是插进正在等待的那个回合的(底层用 `steer`),worker 立刻继续;
|
|
117
|
-
- 只有真卡在 `ask_user_question` 上才算「等待中」——工具一旦返回结果就不再报。
|
|
118
|
-
|
|
119
|
-
## 排队还是插话(`mode`)
|
|
56
|
+
桌面 App 与 CLI 共用 `~/.codex/config.toml`,两端都会看到这条。
|
|
120
57
|
|
|
121
|
-
|
|
58
|
+
> token 放在 URL 路径里(能力 URL),所以 Codex 侧不需要配环境变量——桌面 App 从 Finder 启动时拿不到自定义环境变量。也支持 `Authorization: Bearer <token>`,需要时可用 `--bearer-token-env-var`。
|
|
122
59
|
|
|
123
|
-
|
|
124
|
-
| --- | --- | --- |
|
|
125
|
-
| `queue`(默认) | 排队发送 | 等当前回合跑完,再作为一个新回合处理 |
|
|
126
|
-
| `steer` | 插话发送 | 并入当前回合,模型在下一步就能看到 |
|
|
127
|
-
|
|
128
|
-
`dsh_task`、`dsh_spawn`、`dsh_followup` 都支持:
|
|
60
|
+
Claude Code 同理:
|
|
129
61
|
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
dsh_task({ sessionId: "session-…", prompt: "停一下,改成只跑单测", mode: "steer" })
|
|
62
|
+
```bash
|
|
63
|
+
claude mcp add --transport http dsh-bridge "<上面那个 url>" --scope user
|
|
133
64
|
```
|
|
134
65
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
1. **`steer` 不打断正在执行的工具**。实测 worker 正在跑 `sleep 90`,`steer` 的消息投递进去了,但要等那次工具调用返回、模型走到下一步才被看到——整整 70 秒。真想立刻停,是 DSH 界面上另一个动作(取消),不是投递模式。
|
|
138
|
-
2. **`steer` 不新开回合**(turn 编号不变),所以等待条件必须是「当前回合结束」。早期实现等的是「下一个回合」,答案都回来了、调用还在等,最后 120 秒超时。
|
|
139
|
-
|
|
140
|
-
`dsh_task` 对正在运行的会话默认会拒绝,错误信息里会提示改用 `steer`。
|
|
66
|
+
---
|
|
141
67
|
|
|
142
|
-
|
|
68
|
+
## 正向:Codex → DSH
|
|
143
69
|
|
|
144
|
-
|
|
70
|
+
### 工具
|
|
145
71
|
|
|
146
|
-
|
|
72
|
+
| 工具 | 作用 |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `dsh_task` | **同步**派活:开会话、投递、阻塞到这一轮结束,只返回**最终回答** |
|
|
75
|
+
| `dsh_spawn` | **异步**派活:立刻返回 `task_name` + `sessionId`,可并行开多个 |
|
|
76
|
+
| `dsh_followup` | 给已有 worker 再派一轮(复用上下文) |
|
|
77
|
+
| `dsh_wait` | 一次等多批 worker,任一到达终态即返回 |
|
|
78
|
+
| `dsh_close` | 关闭 worker,释放并发名额(不删除会话) |
|
|
79
|
+
| `dsh_answer` | 代用户回答 worker 提出的问题(见「问题上报」) |
|
|
80
|
+
| `dsh_list_workers` | 列出本桥开的 worker 及实时状态 |
|
|
81
|
+
| `dsh_dispatch` | 向指定会话投递一条用户消息并开始一轮;返回 `requestId` 与 `seqAtDispatch` |
|
|
82
|
+
| `dsh_read_result` | **长轮询**读该会话在 `seqAtDispatch` 之后的全部助手文本(看过程用;要结果用 `dsh_wait`) |
|
|
83
|
+
| `dsh_list_sessions` | 列出会话**元数据**(id、标题、工作目录、是否运行、最后活动时间) |
|
|
147
84
|
|
|
148
|
-
|
|
85
|
+
### 基本用法
|
|
149
86
|
|
|
150
87
|
```js
|
|
151
|
-
//
|
|
152
|
-
dsh_task({ prompt: "
|
|
88
|
+
// 同步:等它跑完,直接拿最终答案
|
|
89
|
+
dsh_task({ prompt: "审一下 src/bridge.ts 的错误处理", cwd: "/repo", timeoutSeconds: 300 })
|
|
153
90
|
|
|
154
|
-
//
|
|
155
|
-
dsh_spawn({ task_name: "
|
|
91
|
+
// 并行:先拿句柄,再批量等
|
|
92
|
+
dsh_spawn({ task_name: "audit_api", message: "…", cwd: "/repo" })
|
|
93
|
+
dsh_spawn({ task_name: "audit_ui", message: "…", cwd: "/repo" })
|
|
94
|
+
dsh_wait({ targets: ["audit_api", "audit_ui"], timeoutMs: 240000 })
|
|
156
95
|
|
|
157
|
-
//
|
|
158
|
-
|
|
96
|
+
// 续派:复用 worker 的上下文
|
|
97
|
+
dsh_followup({ worker: "audit_api", message: "把第 3 条展开,给出补丁" })
|
|
159
98
|
```
|
|
160
99
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
- 目录不存在时先递归创建,再做项目登记(登记要求目录已存在);
|
|
164
|
-
- 目录已经是项目 → **直接复用,不改标题**(底层 `create` 对同一路径返回已有记录);
|
|
165
|
-
- 返回结果里带上 `workspace: { id, title, path }`,方便确认落在哪个项目;
|
|
166
|
-
- 项目操作失败不影响派发本身(分组是外观,不因此让任务失败)。
|
|
167
|
-
|
|
168
|
-
## 排队还是立刻打断(`mode`)
|
|
100
|
+
边界:
|
|
169
101
|
|
|
170
|
-
|
|
102
|
+
- `dsh_list_sessions` **只返回元数据**,不含任何消息内容;
|
|
103
|
+
- `dsh_read_result` 只对**本桥派发过**的会话生效,从未派发过的会话一律拒绝。
|
|
171
104
|
|
|
172
|
-
|
|
173
|
-
| --- | --- |
|
|
174
|
-
| `queue`(默认) | 等当前回合跑完再处理这条消息 |
|
|
175
|
-
| `steer` | 把消息**插进当前回合**(当前那一步仍会跑完) |
|
|
176
|
-
| `interrupt` | **先取消当前回合**,再发送,真正立刻停下 |
|
|
105
|
+
### 指定模型
|
|
177
106
|
|
|
178
|
-
`dsh_task`、`dsh_spawn`、`dsh_followup`
|
|
107
|
+
`dsh_task`、`dsh_spawn`、`dsh_followup` 都可以带 `model`:
|
|
179
108
|
|
|
180
109
|
```js
|
|
181
|
-
|
|
182
|
-
|
|
110
|
+
dsh_task({ prompt: "…", model: "zai-coding-cn/glm-5.3", reasoningEffort: "max" })
|
|
111
|
+
dsh_spawn({ task_name: "review", message: "…", model: "opencode-go/deepseek-v4.1-flash" })
|
|
183
112
|
```
|
|
184
113
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
1. **`steer` 不会打断正在执行的工具**。实测:worker 正在跑 `sleep 90`,`steer` 进去后消息被**并入当前回合**,但要等那一步工具调用结束才会被模型看到——整整等了 70 秒。原因是 `steer` 只是投递,`cancel` 才是中止。
|
|
190
|
-
2. **`steer` 不新开回合**,所以等待条件不能是"下一个回合结束"。早期实现那样写,答案已经回来了、调用还在等,最后 120 秒超时。现在 steer 等的是「当前回合结束」。
|
|
114
|
+
- 写法是 `provider/model`;只写模型名时沿用当前默认 provider;
|
|
115
|
+
- `reasoningEffort` 可选,取值随模型;
|
|
116
|
+
- 不传就完全不动会话;
|
|
117
|
+
- 实测:请求确实切到指定模型,且**不会改写 profile 的默认模型**。
|
|
191
118
|
|
|
192
|
-
|
|
119
|
+
实现上是在建好会话、发第一个 prompt **之前**调一次 `selectModel`,所以 worker 第一轮就在新模型上跑。
|
|
193
120
|
|
|
194
|
-
|
|
121
|
+
### 项目归属(`workspace`)
|
|
195
122
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
`dsh_task`、`dsh_spawn`、`dsh_followup` 都可以带 `model`,让 worker 跑在指定模型上:
|
|
123
|
+
DSH 的侧边栏分组(项目 / workspace)和工作目录是两回事:新建会话默认落在「未分组」,除非所在目录已经是项目。
|
|
199
124
|
|
|
200
125
|
```js
|
|
201
|
-
dsh_task({ prompt: "…",
|
|
202
|
-
dsh_spawn({ task_name: "
|
|
203
|
-
|
|
126
|
+
dsh_task({ prompt: "…", cwd: "/repo/clawer", workspace: true }) // 用目录名建项目
|
|
127
|
+
dsh_spawn({ task_name: "w1", message: "…", cwd: "/repo/clawer", workspace: "Clawer" })
|
|
128
|
+
dsh_task({ prompt: "…", cwd: "/repo/clawer" }) // 只复用,不创建
|
|
204
129
|
```
|
|
205
130
|
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
实现上就是在建好会话、发第一个 prompt **之前**调一次 `selectModel`,所以 worker 的第一轮就已经在新模型上跑。
|
|
212
|
-
|
|
213
|
-
## 工具
|
|
214
|
-
|
|
215
|
-
| 工具 | 作用 |
|
|
216
|
-
| --- | --- |
|
|
217
|
-
| `dsh_task` | **同步**派活:开会话、投递、阻塞到这一轮结束,只返回**最终回答**(最后一条 assistant 消息) |
|
|
218
|
-
| `dsh_spawn` | **异步**派活:立刻返回 `task_name` + `sessionId`,可同时开多个 worker 并行跑 |
|
|
219
|
-
| `dsh_followup` | 给已有 worker 再派一轮(复用它的上下文) |
|
|
220
|
-
| `dsh_close` | 关闭 worker,释放并发名额(不删除会话) |
|
|
221
|
-
| `dsh_list_workers` | 列出本桥开的 worker 及其实时状态 |
|
|
222
|
-
| `dsh_wait` | 一次等多批 worker:`targets` + `timeoutMs`,任一到达终态即返回 `{completed}` / `{errored}` / `"running"` / `"not_found"` |
|
|
223
|
-
| `dsh_dispatch` | 向指定会话投递一条用户消息并开始一轮;返回 `requestId` 与 `seqAtDispatch` |
|
|
224
|
-
| `dsh_read_result` | **长轮询**读进度:返回该会话在 `seqAtDispatch` 之后的**全部**助手文本(用于看过程;要结果用 `dsh_wait`) |
|
|
225
|
-
| `dsh_list_sessions` | 列出会话**元数据**(id、标题、工作目录、是否运行、最后活动时间) |
|
|
131
|
+
- 目录不存在时先递归创建,再做项目登记(登记要求目录已存在);
|
|
132
|
+
- 目录已经是项目 → **复用,不改标题**;
|
|
133
|
+
- 返回里带 `workspace: { id, title, path }`;
|
|
134
|
+
- 项目操作失败不影响派发本身。
|
|
226
135
|
|
|
227
|
-
|
|
136
|
+
### 排队还是插话(`mode`)
|
|
228
137
|
|
|
229
|
-
|
|
138
|
+
DSH 的「繁忙时的发送行为」只有两种,桥原样交给调用方:
|
|
230
139
|
|
|
231
|
-
|
|
|
140
|
+
| `mode` | DSH 里的名字 | 行为 |
|
|
232
141
|
| --- | --- | --- |
|
|
233
|
-
| `
|
|
142
|
+
| `queue`(默认) | 排队发送 | 等当前回合跑完,作为新回合处理 |
|
|
143
|
+
| `steer` | 插话发送 | 并入当前回合,模型下一步就能看到 |
|
|
234
144
|
|
|
235
|
-
|
|
145
|
+
```js
|
|
146
|
+
dsh_followup({ worker: "w1", message: "接下来只改 src 下的文件", mode: "steer" })
|
|
147
|
+
```
|
|
236
148
|
|
|
237
|
-
|
|
149
|
+
两个实测结论:
|
|
238
150
|
|
|
239
|
-
|
|
151
|
+
1. **`steer` 不打断正在执行的工具**——worker 若正卡在 `sleep 90` 这类调用里,消息要等它返回才被看到(实测 70 秒)。真想立刻停下是界面上的「取消」,不是投递模式。
|
|
152
|
+
2. **`steer` 不新开回合**(turn 编号不变),所以桥等的是「当前回合结束」;等「下一回合」会一直等到超时。
|
|
240
153
|
|
|
241
|
-
|
|
242
|
-
- `dsh_read_result` 只对**本桥派发过**的会话生效;从未派发过的会话一律拒绝。
|
|
154
|
+
`dsh_task` 对正在运行的会话默认拒绝,错误信息会提示改用 `steer`。插话的回复和原任务在同一回合里,桥会定位到**自己那条 user/message** 之后再取回答,避免混进原任务后续步骤的输出。
|
|
243
155
|
|
|
244
|
-
|
|
156
|
+
### 无人值守(`assumeDefaults`)
|
|
245
157
|
|
|
246
|
-
|
|
158
|
+
worker 卡在 `ask_user_question` 上时需要有人在界面上点一下,但调用方是另一个 agent 时那个人往往不在。因此桥在派发时**自动追加一条常驻指令**:
|
|
247
159
|
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
|
|
160
|
+
```
|
|
161
|
+
(如果缺少必要信息或需要在多个方案里选择,直接采用最合理的默认做法继续做完,
|
|
162
|
+
不要把选择权抛回给用户、不要调用 ask_user_question 等回答;
|
|
163
|
+
在结果里用一行说明你假设了什么。)
|
|
251
164
|
```
|
|
252
165
|
|
|
253
|
-
|
|
166
|
+
- 默认开启,`assumeDefaults: false` 可关;
|
|
167
|
+
- `dsh_task`、`dsh_spawn`、`dsh_followup` 三条路径都会加;
|
|
168
|
+
- prompt 里本来就提到 `ask_user_question` 时不追加。
|
|
254
169
|
|
|
255
|
-
|
|
170
|
+
效果:worker 不会停下来等人,而是选一个合理默认做法做完并交代假设,调用方据此复核。
|
|
256
171
|
|
|
257
|
-
|
|
172
|
+
### 问题上报(`pendingQuestion`)
|
|
173
|
+
|
|
174
|
+
worker 真的问了问题时,`dsh_wait` 会把问题原样带出来:
|
|
175
|
+
|
|
176
|
+
```jsonc
|
|
258
177
|
{
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
|
|
263
|
-
|
|
178
|
+
"status": { "w1": "running" },
|
|
179
|
+
"timedOut": true,
|
|
180
|
+
"pendingQuestion": {
|
|
181
|
+
"callId": "call_abc",
|
|
182
|
+
"questions": [{
|
|
183
|
+
"id": "mode",
|
|
184
|
+
"question": "用哪种模式?",
|
|
185
|
+
"header": "Choose Mode",
|
|
186
|
+
"options": [{ "label": "快速", "description": "只跑单测" }, { "label": "完整" }]
|
|
187
|
+
}]
|
|
188
|
+
},
|
|
189
|
+
"next": "blocked on a question: answer it with dsh_answer, or call dsh_wait again"
|
|
264
190
|
}
|
|
265
191
|
```
|
|
266
192
|
|
|
267
|
-
|
|
193
|
+
`dsh_answer({ worker, answers: [{ id, selected: [...] }] })` 用来代答。**已知限制**:DSH 的 `userQuestions.answer()` 只接受已经超时、进入 `continued` 状态的问题;还阻塞在工具调用里的问题是 Web UI 的 answerer 接管的,桥答不了(实测返回 `answered: false`)。所以实际使用中更该依赖上面的 `assumeDefaults`,而不是等 worker 提问。
|
|
268
194
|
|
|
269
|
-
|
|
195
|
+
---
|
|
270
196
|
|
|
271
|
-
|
|
272
|
-
codex mcp add dsh-bridge --url "$(node -p "require(process.env.HOME+'/.dsh/bridge/endpoint.json').url")"
|
|
273
|
-
```
|
|
197
|
+
## 反向:DSH → Codex / Claude Code
|
|
274
198
|
|
|
275
|
-
|
|
199
|
+
两个工具都会给本部署的 Agent 注册,参数形状一致,差别只在底层 CLI。
|
|
276
200
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
201
|
+
### `codex_dispatch`
|
|
202
|
+
|
|
203
|
+
| 参数 | 说明 |
|
|
204
|
+
| --- | --- |
|
|
205
|
+
| `prompt` | 必填,任务正文 |
|
|
206
|
+
| `cwd` | 工作目录,默认当前会话目录 |
|
|
207
|
+
| `resume` | Codex 线程 id,续接同一线程 |
|
|
208
|
+
| `sandbox` | `read-only` / `workspace-write` / `danger-full-access` |
|
|
281
209
|
|
|
282
|
-
|
|
210
|
+
返回 `{ ok, text, threadId, exitCode, durationMs }`。执行 `codex exec [resume <id>] --json -o <临时文件>`,从事件流取 `thread.started`、从 `-o` 取最终消息。**`resume` 时 `sandbox` 与 `cwd` 不生效**——`codex exec resume` 不接受 `-s` / `-C`。
|
|
283
211
|
|
|
284
|
-
|
|
212
|
+
### `claude_dispatch`
|
|
285
213
|
|
|
286
|
-
|
|
214
|
+
| 参数 | 说明 |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| `prompt` | 必填,任务正文 |
|
|
217
|
+
| `cwd` | 工作目录 |
|
|
218
|
+
| `resume` | Claude Code 的 session id |
|
|
219
|
+
| `model` | 模型别名或 id;不传则用配置,再退回 CLI 自己的设置 |
|
|
287
220
|
|
|
288
|
-
|
|
289
|
-
codex mcp remove dsh-bridge
|
|
290
|
-
```
|
|
221
|
+
返回 `{ ok, text, sessionId, durationMs }`。执行 `claude -p --output-format json`,只读 stdout 的单个 JSON 对象;**stderr 的警告不算失败**(模型名带 `[1M]` 后缀时 CLI 会警告,但结果正常)。
|
|
291
222
|
|
|
292
223
|
---
|
|
293
224
|
|
|
@@ -304,81 +235,76 @@ codex mcp remove dsh-bridge
|
|
|
304
235
|
| 键 | 默认 | 说明 |
|
|
305
236
|
| --- | --- | --- |
|
|
306
237
|
| `enabled` | `true` | 总开关;关闭后不监听、不写端点文件 |
|
|
307
|
-
| `port` | `47821` |
|
|
308
|
-
| `endpointFile` | `~/.dsh/bridge/endpoint.json` |
|
|
309
|
-
| `maxResultChars` | `20000` |
|
|
310
|
-
| `taskTimeoutSeconds` | `300` | `dsh_task`
|
|
311
|
-
| `
|
|
312
|
-
| `
|
|
313
|
-
| `
|
|
238
|
+
| `port` | `47821` | 回环端口;固定端口让客户端那条 URL 重启后依然有效 |
|
|
239
|
+
| `endpointFile` | `~/.dsh/bridge/endpoint.json` | 端点与令牌落盘位置 |
|
|
240
|
+
| `maxResultChars` | `20000` | 单次结果返回的最大字符数(超出保留尾部) |
|
|
241
|
+
| `taskTimeoutSeconds` | `300` | `dsh_task` 等待上限(客户端可覆盖,最大 240) |
|
|
242
|
+
| `assumeDefaults` | `true` | 派发时追加「自行决策」的常驻指令 |
|
|
243
|
+
| `workersFile` | `~/.dsh/bridge/workers.json` | worker 注册表(0600) |
|
|
244
|
+
| `maxHops` | `1` | 跨工具委派层数上限 |
|
|
245
|
+
| `dispatchPerMinute` | `10` | 滚动窗口内的派发次数 |
|
|
314
246
|
| `dispatchWindowMs` | `60000` | 上述窗口长度 |
|
|
315
|
-
| `maxWorkersPerRoot` | `4` | **每个工作目录**的 worker
|
|
316
|
-
| `maxWorkers` | `12` |
|
|
317
|
-
| `workerTtlMs` | `86400000` |
|
|
318
|
-
| `waitTimeoutMs` | `240000` | `dsh_wait`
|
|
319
|
-
| `readWaitSeconds` | `25` | `dsh_read_result`
|
|
320
|
-
| `codexBin` | `codex` | Codex 可执行文件;含 `/`
|
|
321
|
-
| `codexSandbox` | `workspace-write` | `codex_dispatch`
|
|
322
|
-
| `codexTimeoutMs` | `300000` | 单次 `codex_dispatch`
|
|
323
|
-
|
|
324
|
-
|
|
247
|
+
| `maxWorkersPerRoot` | `4` | **每个工作目录**的 worker 上限 |
|
|
248
|
+
| `maxWorkers` | `12` | 全局上限 |
|
|
249
|
+
| `workerTtlMs` | `86400000` | 记录过期后不再占名额 |
|
|
250
|
+
| `waitTimeoutMs` | `240000` | `dsh_wait` 默认超时 |
|
|
251
|
+
| `readWaitSeconds` | `25` | `dsh_read_result` 默认长轮询时长 |
|
|
252
|
+
| `codexBin` | `codex` | Codex 可执行文件;含 `/` 时严格按路径解析 |
|
|
253
|
+
| `codexSandbox` | `workspace-write` | `codex_dispatch` 默认 sandbox |
|
|
254
|
+
| `codexTimeoutMs` | `300000` | 单次 `codex_dispatch` 超时 |
|
|
255
|
+
| `claudeBin` | `claude` | Claude Code 可执行文件 |
|
|
256
|
+
| `claudeModel` | 空 | 默认让 Claude CLI 用自己的设置 |
|
|
257
|
+
| `claudeTimeoutMs` | `300000` | 单次 `claude_dispatch` 超时 |
|
|
258
|
+
|
|
259
|
+
端口被占用时不会静默换端口:端点文件里写入 `error` 字段,宿主日志打印 `subagent-bridge: could not start on port …`。
|
|
325
260
|
|
|
326
261
|
---
|
|
327
262
|
|
|
328
|
-
##
|
|
263
|
+
## 护栏与限制
|
|
329
264
|
|
|
330
|
-
|
|
265
|
+
### 防环
|
|
331
266
|
|
|
332
|
-
|
|
267
|
+
两个 agent 都接了同一个桥时存在真实环路:DSH worker → `codex_dispatch` → 那次 Codex 也能看到 `dsh-bridge` → 又 `dsh_spawn` → …。双方进程内的深度规则都管不到这条链(桥建的 worker 是顶层会话,深度恒为 0)。
|
|
333
268
|
|
|
334
|
-
**① 深度(`maxHops`,默认 1
|
|
269
|
+
**① 深度(`maxHops`,默认 1)**:每次派发都计算 `max(在飞链路深度, 调用方深度) + 1`,超限直接报错:
|
|
335
270
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
`codex_dispatch would create delegation depth 2, but maxHops is 1. Carry the work out directly instead of delegating again.`
|
|
341
|
-
|
|
342
|
-
默认 1 的含义:**Codex 可以派给 DSH,但 DSH 不能再用桥派回去** —— 与两个产品各自的 `maxDepth: 1` 语义对齐。
|
|
343
|
-
|
|
344
|
-
**② 速率(`dispatchPerMinute`,默认 10 / 60s)**
|
|
271
|
+
```
|
|
272
|
+
codex_dispatch would create delegation depth 2, but maxHops is 1.
|
|
273
|
+
Carry the work out directly instead of delegating again.
|
|
274
|
+
```
|
|
345
275
|
|
|
346
|
-
|
|
276
|
+
**② 速率(`dispatchPerMinute`,默认 10)**:拦截广度型失控与紧循环重试:
|
|
347
277
|
|
|
348
278
|
```
|
|
349
279
|
rate limit: 10 dispatches per 60s reached; retry in 23s
|
|
350
280
|
```
|
|
351
281
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
worker 名额**按工作目录分桶**,调用方不用传任何 owner:
|
|
282
|
+
### 名额
|
|
355
283
|
|
|
356
|
-
- 每个 `cwd` 默认 **4 个**(`maxWorkersPerRoot`);
|
|
357
|
-
-
|
|
358
|
-
-
|
|
359
|
-
- 记录 **24 小时**没更新就自动不再占名额(`workerTtlMs`),不需要手动清理;
|
|
284
|
+
- 每个 `cwd` 默认 **4 个**(`maxWorkersPerRoot`),全局 **12 个**(`maxWorkers`);
|
|
285
|
+
- **完成但没 `dsh_close` 的仍占名额**;
|
|
286
|
+
- 记录 **24 小时**没更新自动释放;
|
|
360
287
|
- 阻塞式 `dsh_task` 不计入名额,只登记。
|
|
361
288
|
|
|
362
|
-
|
|
289
|
+
按 `cwd` 分桶而不是按会话,是因为 MCP 端点共享、请求里没有会话身份;`cwd` 是唯一由调用方稳定提供、且语义上对应「一个项目」的维度。
|
|
363
290
|
|
|
364
|
-
|
|
291
|
+
### 跨重启
|
|
365
292
|
|
|
366
|
-
worker
|
|
293
|
+
worker 注册表和每轮的时间基线落在 `~/.dsh/bridge/workers.json`:
|
|
367
294
|
|
|
368
|
-
-
|
|
369
|
-
-
|
|
370
|
-
-
|
|
371
|
-
-
|
|
372
|
-
-
|
|
373
|
-
- 文件最多保留 200 条,超出时先淘汰最旧的 **已关闭** worker。
|
|
295
|
+
- 重启后 `dsh_list_workers` 仍能看到之前的 worker;
|
|
296
|
+
- 已结束的 worker,`dsh_wait` 直接返回缓存里的最终回答;
|
|
297
|
+
- 重启时还在跑的,会话未打开时状态为 `offline`;
|
|
298
|
+
- `dsh_followup` 会开新一轮并清掉旧回答;
|
|
299
|
+
- 最多保留 200 条,超出先淘汰最旧的已关闭 worker。
|
|
374
300
|
|
|
375
|
-
|
|
301
|
+
### 安全模型
|
|
376
302
|
|
|
377
|
-
- 只监听 `127.0.0.1
|
|
378
|
-
-
|
|
379
|
-
-
|
|
380
|
-
-
|
|
381
|
-
- DSH
|
|
303
|
+
- 只监听 `127.0.0.1`;
|
|
304
|
+
- 请求必须带令牌:路径 `/mcp/<token>` 或 `Authorization: Bearer <token>`,定长时间比较;
|
|
305
|
+
- 令牌首次启动生成并持久化,能力 URL 稳定;
|
|
306
|
+
- 能力等价于「向指定会话投递消息」:能读到端点文件的进程就能派活,文件 0600、目录 0700;
|
|
307
|
+
- DSH 侧的权限策略不受影响,派进去的消息仍按会话自身的 sandbox / 审批策略执行。
|
|
382
308
|
|
|
383
309
|
---
|
|
384
310
|
|
|
@@ -391,22 +317,22 @@ Codex / 任意 MCP 客户端
|
|
|
391
317
|
dsh-plugin-subagent-bridge(宿主进程内)
|
|
392
318
|
· node:http 监听 127.0.0.1:47821
|
|
393
319
|
· initialize / tools/list / tools/call / ping
|
|
394
|
-
· sessionController.prompt({sessionId, requestId, mode, content})
|
|
320
|
+
· sessionController.prompt({ sessionId, requestId, mode, content })
|
|
395
321
|
· sessions.get(id).snapshotEvents() 读回助手文本
|
|
396
322
|
```
|
|
397
323
|
|
|
398
|
-
|
|
324
|
+
协议是 **streamable HTTP 的无状态子集**:每个 POST 返回一段 `application/json`,不做服务端推送,因此取结果用**长轮询**——`dsh_read_result` 在服务端等,客户端一次调用即可拿到答案;客户端断开时服务端立即停止等待。
|
|
399
325
|
|
|
400
326
|
---
|
|
401
327
|
|
|
402
328
|
## 配套 skill
|
|
403
329
|
|
|
404
|
-
`skills/` 下有两份可直接安装的 skill
|
|
330
|
+
`skills/` 下有两份可直接安装的 skill,把「什么时候该派活、怎么派、怎么收尾」写成模型能读的规则:
|
|
405
331
|
|
|
406
332
|
| Skill | 装给谁 | 作用 |
|
|
407
333
|
| --- | --- | --- |
|
|
408
334
|
| `deepseek-worker` | Codex | 让 Codex 把任务当 subagent 派给 DSH(`dsh_task`),一次调用等结果 |
|
|
409
|
-
| `delegate-to-external-agent` | DSH | 让 DSH 的 agent
|
|
335
|
+
| `delegate-to-external-agent` | DSH | 让 DSH 的 agent 在需要独立视角/复核时派给 Codex 或 Claude Code,含两者差别与续接方式 |
|
|
410
336
|
|
|
411
337
|
```bash
|
|
412
338
|
# Codex 侧
|
|
@@ -416,17 +342,15 @@ mkdir -p ~/.codex/skills && cp -R skills/deepseek-worker ~/.codex/skills/
|
|
|
416
342
|
mkdir -p ~/.dsh/skills && cp -R skills/delegate-to-external-agent ~/.dsh/skills/
|
|
417
343
|
```
|
|
418
344
|
|
|
419
|
-
两份都是标准 `SKILL.md`:YAML frontmatter(`name` + `description`)+ 正文。**触发靠 description
|
|
345
|
+
两份都是标准 `SKILL.md`:YAML frontmatter(`name` + `description`)+ 正文。**触发靠 description**——模型平时只看到名字和描述,判断相关了才读正文,所以描述里写满了中英触发语和「不要用」的边界。改完立刻生效,不用重启。
|
|
420
346
|
|
|
421
347
|
---
|
|
422
348
|
|
|
423
349
|
## 兼容性
|
|
424
350
|
|
|
425
|
-
- DSH `^0.2.1-0`(`engines.dsh
|
|
426
|
-
- MCP 协议版本:`2025-06-18`、`2025-03-26`、`2024-11-05
|
|
427
|
-
- 依赖宿主公开服务 `sessionController
|
|
428
|
-
|
|
429
|
-
---
|
|
351
|
+
- DSH `^0.2.1-0`(`engines.dsh`);
|
|
352
|
+
- MCP 协议版本:`2025-06-18`、`2025-03-26`、`2024-11-05`(按客户端请求回显,未知版本回落到最新);
|
|
353
|
+
- 依赖宿主公开服务 `sessionController`、`sessions`、`workspaceRegistry`、`userQuestions`;不可用时工具返回明确错误。
|
|
430
354
|
|
|
431
355
|
## 开发
|
|
432
356
|
|
|
@@ -434,8 +358,9 @@ mkdir -p ~/.dsh/skills && cp -R skills/delegate-to-external-agent ~/.dsh/skills/
|
|
|
434
358
|
node --test "test/*.test.mjs"
|
|
435
359
|
```
|
|
436
360
|
|
|
437
|
-
|
|
438
|
-
|
|
361
|
+
83 项,覆盖握手与版本协商、工具清单与参数校验、派发调用形状、结果读取的时间基线、长轮询、真实 HTTP 往返(含 404/405/202 与 bearer 分支)、`codex exec` 事件解析与二进制解析、成功/失败/超时三种运行结果、resume 的参数分叉、只回最终回答、`dsh_wait` 的四种终态、并发上限与释放、followup 重基线、注册表落盘与重启复原、`maxHops` 与速率限制、按 `cwd` 分桶的预算、工作区归属、`workspace` 创建与复用、`queue` / `steer` 的投递与等待、插话回答的定位、`claude` 输出解析与 resume 参数、`assumeDefaults` 指令注入、`pendingQuestion` 与 `dsh_answer`。
|
|
362
|
+
|
|
363
|
+
`test/persistence.test.mjs` 单独覆盖持久化:文件权限、缺失/损坏记录、并发写、冷会话缓存与 `offline`。
|
|
439
364
|
|
|
440
365
|
---
|
|
441
366
|
|
|
@@ -443,12 +368,11 @@ node --test "test/*.test.mjs"
|
|
|
443
368
|
|
|
444
369
|
| 插件 | 说明 |
|
|
445
370
|
| --- | --- |
|
|
371
|
+
| **dsh-plugin-subagent-bridge**(本仓库) | 让 Codex 通过本机 MCP 把 DSH 当 subagent 用,反向也能派给 Codex / Claude Code |
|
|
446
372
|
| [dsh-plugin-composer-predict](https://github.com/Dogtiti/dsh-plugin-composer-predict) | 预测下一条指令并显示为占位文字,`Tab` 采纳 |
|
|
447
373
|
| [dsh-plugin-composer-history](https://github.com/Dogtiti/dsh-plugin-composer-history) | 输入框中按 `↑` / `↓` 回填历史消息 |
|
|
448
|
-
|
|
|
449
|
-
| [dsh-plugin-
|
|
450
|
-
| [dsh-plugin-provider-icons](https://github.com/Dogtiti/dsh-plugin-provider-icons) | 在模型名旁显示供应商品牌 logo(24 个厂商,支持前缀匹配) |
|
|
451
|
-
|
|
374
|
+
| [dsh-plugin-usage-panel](https://github.com/Dogtiti/dsh-plugin-usage-panel) | 在模型选择器旁显示供应商套餐额度余量 |
|
|
375
|
+
| [dsh-plugin-provider-icons](https://github.com/Dogtiti/dsh-plugin-provider-icons) | 在模型名旁显示供应商品牌 logo |
|
|
452
376
|
|
|
453
377
|
## 许可
|
|
454
378
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subagent-bridge",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Subagent bridge: lets Codex delegate a task to DeepSeek Harness the way it would spawn a subagent (one call, waits for the answer), plus a codex_dispatch tool for the reverse direction.",
|
|
6
6
|
"keywords": [
|