@whaletalk/dsh-codex-collab 0.1.0
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/LICENSE +21 -0
- package/README.md +188 -0
- package/codex-plugin/.agents/plugins/marketplace.json +20 -0
- package/codex-plugin/plugins/dsh/.codex-plugin/plugin.json +34 -0
- package/codex-plugin/plugins/dsh/skills/dsh-collab/SKILL.md +59 -0
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/review.mjs +122 -0
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/task.mjs +221 -0
- package/cordis.patch.yml +20 -0
- package/dsh-banner.png.png +0 -0
- package/harness/dsh-bridge.mjs +720 -0
- package/mcp/dsh-mcp.mjs +255 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DSH Collab contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./dsh-banner.png.png" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# DSH Collab — Codex × DeepSeek Harness 双向编码协作
|
|
6
|
+
|
|
7
|
+
**Codex 主导,DeepSeek 执行,Codex 验收。** 一个插件体系,让 ChatGPT(Codex)像项目经理一样把编码任务派给本机 DeepSeek Harness 的编码子代理,实时回传结果,读文件验收,并由独立评审员实际跑通构建/测试后出具评审报告。
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Codex 工作区 (ChatGPT)
|
|
11
|
+
│ 派活 / 评审 / 验收
|
|
12
|
+
▼
|
|
13
|
+
DeepSeek Harness 网关 ── 编码子代理(多工作区、多 lane、fast/pro 模型)
|
|
14
|
+
│ 独立评审子代理(必须跑通构建/测试)
|
|
15
|
+
▼
|
|
16
|
+
共享工作目录(任意目录,--cwd 指定)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
flowchart LR
|
|
21
|
+
subgraph Codex["Codex (ChatGPT) — 项目经理 + 验收员"]
|
|
22
|
+
A1[dsh_task 派活]
|
|
23
|
+
A2[dsh_review 评审]
|
|
24
|
+
A3[dsh_read_file 验收]
|
|
25
|
+
end
|
|
26
|
+
subgraph DSH["DeepSeek Harness"]
|
|
27
|
+
B1[HTTP 网关 /api/dsh-bridge]
|
|
28
|
+
B2[编码子代理<br/>多工作区 · 多 lane · fast/pro]
|
|
29
|
+
B3[独立评审子代理<br/>必须跑通构建/测试]
|
|
30
|
+
end
|
|
31
|
+
subgraph FS["共享工作目录"]
|
|
32
|
+
C1[任意项目目录]
|
|
33
|
+
end
|
|
34
|
+
A1 --> B1
|
|
35
|
+
A2 --> B1
|
|
36
|
+
A3 --> C1
|
|
37
|
+
B1 --> B2 --> C1
|
|
38
|
+
B1 --> B3 --> C1
|
|
39
|
+
B2 -.实时回传汇报.-> B1
|
|
40
|
+
B1 -.任务结果.-> A1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 组件
|
|
46
|
+
|
|
47
|
+
| 目录 | 内容 | 说明 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `codex-plugin/` | Codex 本地市场插件包 | 标准 marketplace 结构(`.agents/plugins/marketplace.json` + 插件目录),含协作技能 `dsh-collab` |
|
|
50
|
+
| `mcp/dsh-mcp.mjs` | MCP 服务器(零依赖 Node) | stdio 传输(Codex `[mcp_servers]` 直连)+ Streamable HTTP 传输(`--http`,供 Secure MCP Tunnel / cloudflared 桥接 ChatGPT);5 个工具:`dsh_task` / `dsh_task_status` / `dsh_task_cancel` / `dsh_review` / `dsh_read_file` |
|
|
51
|
+
| `harness/dsh-bridge.mjs` | DeepSeek Harness 宿主组合网关插件 | 提供 HTTP 网关(`/api/dsh-bridge/*`)、WS 推送、原生工具,管理编码/评审子代理会话。**必须装进 harness 宿主组合**(见下文) |
|
|
52
|
+
| `cordis.patch.yml` | bundle patch | 把网关插件挂进 DSH 宿主组合;`dsh.bundle.patch` 指向它,所以 `dsh plugin add` 能自动 reconcile |
|
|
53
|
+
|
|
54
|
+
## 功能
|
|
55
|
+
|
|
56
|
+
- **派活**:`dsh_task` / `task.mjs`,任意工作目录(`--cwd`,不存在自动创建并注册为工作区)
|
|
57
|
+
- **并行分工**:同目录多 lane(`--lane`)+ 跨目录天然并行
|
|
58
|
+
- **模型切换**:`fast`(deepseek-v4-flash)或 `pro`(deepseek-v4-pro),动态解析不写死
|
|
59
|
+
- **双向评审**:独立评审子代理与编码员分离,读磁盘真实文件、静态审查、**实际运行构建/测试验证可跑通**,输出结构化报告
|
|
60
|
+
- **Git 自动提交**:`--commit`,任务完成后在 cwd 自动 `git add -A` 并提交
|
|
61
|
+
- **跨任务记忆**:持久子代理会话 + 历史摘要注入(冷恢复失败自动降级为一次性执行,功能不中断)
|
|
62
|
+
- **任务管理**:`--list` / `--status <id>` / `--cancel <id>`
|
|
63
|
+
- **结果回传**:任务汇报直接回 Codex 对话(`--wait` 默认)
|
|
64
|
+
|
|
65
|
+
## 安装
|
|
66
|
+
|
|
67
|
+
### 0. 一条命令装全部(推荐,npm)
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
# 1) 网关插件装进 harness profile(自动 reconcile bundles,无需手工复制文件)
|
|
71
|
+
dsh plugin --profile web add @whaletalk/dsh-codex-collab
|
|
72
|
+
|
|
73
|
+
# 2) MCP 服务器交给 Codex
|
|
74
|
+
npm install -g @whaletalk/dsh-codex-collab
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
之后 `dsh-mcp` / `dsh-task` / `dsh-review` 三个命令进入 PATH,`~/.codex/config.toml` 里写:
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
[mcp_servers.dsh]
|
|
81
|
+
command = 'dsh-mcp'
|
|
82
|
+
startup_timeout_sec = 120
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
装完**必须重启 profile 进程**——`dsh plugin add` 改的是 bundles 列表与 node_modules,HMR 不监控这两处。
|
|
86
|
+
|
|
87
|
+
> 本包只提供 bundle patch,不含 profile 之外的强依赖:`@deepseek-ai/dsh-tools` 是 optional peer,由 DSH 安装树在运行时提供。因此 `npm install` 不会去 registry 拉它,也不会因它缺失而报错。
|
|
88
|
+
|
|
89
|
+
<details>
|
|
90
|
+
<summary>不想用 npm?仍可手工安装(源码方式)</summary>
|
|
91
|
+
|
|
92
|
+
### 1. DeepSeek Harness 网关(必需,后端)
|
|
93
|
+
|
|
94
|
+
把 `harness/dsh-bridge.mjs` 复制到你的 dsh profile 目录(例如 `$DSH_HOME/profiles/web/`),并在该目录的 `cordis.patch.yml` 中追加:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
- insert:
|
|
98
|
+
- id: dsh-bridge
|
|
99
|
+
name: './dsh-bridge.mjs'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
重启 harness。默认共享工作目录为 `D:\Harness`,可用环境变量 `DSH_BRIDGE_WORKSPACE` 覆盖。验证:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
GET http://127.0.0.1:3080/api/dsh-bridge/status
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> 手工方式必须用相对路径 `'./dsh-bridge.mjs'`,因为文件就在 profile 目录里;npm 方式则用包子路径 `@whaletalk/dsh-codex-collab/bridge`,两者不要混用。
|
|
109
|
+
|
|
110
|
+
### 2. Codex 侧(二选一或都用)
|
|
111
|
+
|
|
112
|
+
**A. MCP 工具(推荐,走 Codex 官方通道)**:把 `mcp/dsh-mcp.mjs` 放到本机任意位置,在 `~/.codex/config.toml` 追加:
|
|
113
|
+
|
|
114
|
+
```toml
|
|
115
|
+
[mcp_servers.dsh]
|
|
116
|
+
command = 'node'
|
|
117
|
+
args = ['<绝对路径>/dsh-mcp.mjs']
|
|
118
|
+
startup_timeout_sec = 120
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
重启 Codex,会话中出现 `mcp__dsh__*` 工具。
|
|
122
|
+
|
|
123
|
+
**B. 本地市场插件(技能形式)**:把 `codex-plugin/` 放到任意位置,在个人市场清单 `~/.agents/plugins/marketplace.json` 中追加插件条目(`"path": "./plugins/dsh"`),并把插件目录放到 `~/plugins/dsh/`,然后:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
codex plugin add dsh@personal
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
npm 安装后,插件目录已在包内,用一行拿到绝对路径:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
node -p "require.resolve('@whaletalk/dsh-codex-collab/package.json').replace(/package\.json$/,'codex-plugin')"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> ⚠️ 已知问题:Codex Desktop 26.803 在 Windows 上存在插件技能不注入会话的 bug([openai/codex#26037](https://github.com/openai/codex/issues/26037)、[#22078](https://github.com/openai/codex/issues/22078))。技能形式可能不生效,**以 MCP 方式为准**。
|
|
136
|
+
|
|
137
|
+
### 3. ChatGPT 普通聊天区(可选,受账户限制)
|
|
138
|
+
|
|
139
|
+
ChatGPT 不能直连 localhost MCP。官方通道是 [Secure MCP Tunnel](https://github.com/openai/tunnel-client):
|
|
140
|
+
|
|
141
|
+
1. 在 `https://platform.openai.com/settings/organization/tunnels` 创建隧道(需组织上下文),创建 Runtime API key;
|
|
142
|
+
2. 启动本地 HTTP MCP:`node dsh-mcp.mjs --http 127.0.0.1:4800`;
|
|
143
|
+
3. 启动隧道客户端(含 `HTTPS_PROXY` 环境变量若需代理):
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
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
|
|
147
|
+
tunnel-client run --profile dsh
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
4. ChatGPT → 设置 → Connectors 中连接。
|
|
151
|
+
|
|
152
|
+
> 注意:写入型 MCP 工具对 ChatGPT 个人订阅(Plus)的开放度存在产品级限制,此路径仅供有能力/有组织的账户使用。
|
|
153
|
+
|
|
154
|
+
</details>
|
|
155
|
+
|
|
156
|
+
## 使用
|
|
157
|
+
|
|
158
|
+
**Codex 工作区会话**(MCP 工具就绪后):
|
|
159
|
+
|
|
160
|
+
> 用 dsh_task 让 DeepSeek 生成一个随机密码 CLI + 测试 + README,放在当前项目目录,完成后你用 dsh_read_file 验收,再 dsh_review 评审
|
|
161
|
+
|
|
162
|
+
**命令行**(npm 安装后直接用;源码方式用 `node <脚本路径>`):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
dsh-task --in "<指令>" --cwd "<目录>" [--lane backend] [--model fast|pro] [--commit]
|
|
166
|
+
dsh-review --cwd "<目录>" [--diff git|@文件|"diff文本"] [--focus "<重点>"]
|
|
167
|
+
dsh-task --list | --status <taskId> | --cancel <taskId>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## 环境变量
|
|
171
|
+
|
|
172
|
+
| 变量 | 默认 | 说明 |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `DSH_BRIDGE_URL` | `http://127.0.0.1:3080` | 网关地址 |
|
|
175
|
+
| `DSH_BRIDGE_WORKSPACE` | `D:\Harness` | 网关默认共享工作目录 |
|
|
176
|
+
| `DSH_BRIDGE_ROOT` | `D:\Harness` | `dsh_read_file` 相对路径基准 |
|
|
177
|
+
| `DSH_BRIDGE_HISTORY` | 多级降级 | 协作历史文件位置(默认 D 盘 → 用户目录 → 临时目录逐级降级) |
|
|
178
|
+
|
|
179
|
+
## 已知限制
|
|
180
|
+
|
|
181
|
+
- 网关插件需要 `webServer` 服务(由 `dsh-web-app` 提供),因此**只能挂 web / desktop profile**;headless profile 会一直 pending
|
|
182
|
+
- Codex Desktop Windows 本地市场技能注入 bug(见上,MCP 通道不受影响)
|
|
183
|
+
- 动态插件环境无 `AbortSignal`,冷恢复失败时自动降级为一次性执行;宿主组合持久化版无此问题
|
|
184
|
+
- ChatGPT Plus 写入型 MCP 的开放度取决于 OpenAI 产品策略
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-local",
|
|
3
|
+
"interface": {
|
|
4
|
+
"displayName": "DeepSeek Harness (Local)"
|
|
5
|
+
},
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "dsh",
|
|
9
|
+
"source": {
|
|
10
|
+
"source": "local",
|
|
11
|
+
"path": "./plugins/dsh"
|
|
12
|
+
},
|
|
13
|
+
"policy": {
|
|
14
|
+
"installation": "AVAILABLE",
|
|
15
|
+
"authentication": "ON_INSTALL"
|
|
16
|
+
},
|
|
17
|
+
"category": "Developer Tools"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Codex 主导的双向编码协作插件:ChatGPT 把编码任务下发给本机 DeepSeek Harness 的持久编码子代理执行,DeepSeek 在共享工作目录 D:\\Harness 中写代码并实时回传进度与汇报,Codex 读文件验收、反馈修正,直到通过。",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "FuJackGao"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"deepseek",
|
|
10
|
+
"harness",
|
|
11
|
+
"collaboration",
|
|
12
|
+
"coding",
|
|
13
|
+
"pair-programming"
|
|
14
|
+
],
|
|
15
|
+
"skills": "./skills/",
|
|
16
|
+
"interface": {
|
|
17
|
+
"displayName": "DeepSeek Harness 协作",
|
|
18
|
+
"shortDescription": "DeepSeek 写代码,Codex 验收",
|
|
19
|
+
"longDescription": "通过本机 DeepSeek Harness 的持久编码子代理执行编码任务:你下指令、DeepSeek 在共享目录 D:\\Harness 写代码并实时回传汇报、你在 Codex 中读文件验收并迭代修正,直到验收通过。",
|
|
20
|
+
"developerName": "FuJackGao",
|
|
21
|
+
"category": "Developer Tools",
|
|
22
|
+
"capabilities": [
|
|
23
|
+
"Interactive",
|
|
24
|
+
"Read",
|
|
25
|
+
"Write"
|
|
26
|
+
],
|
|
27
|
+
"defaultPrompt": [
|
|
28
|
+
"让 DeepSeek 帮我在共享目录里写一个待办事项应用,我负责验收",
|
|
29
|
+
"把这个 bug 交给 DeepSeek 修复,修完我来检查"
|
|
30
|
+
],
|
|
31
|
+
"brandColor": "#4D6BFE",
|
|
32
|
+
"screenshots": []
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dsh-collab
|
|
3
|
+
description: "与 DeepSeek Harness 编码协作。当用户要写代码、建项目、改代码时使用:把编码任务交给本机 DeepSeek Harness 的编码子代理执行(DeepSeek 写代码,你负责验收),在 --cwd 指定的工作目录(通常是 Codex 当前项目目录)中读文件验收;DeepSeek 完成后可自动 git 提交,并可用独立评审子代理审查 diff(评审员会实际跑通构建/测试)。不通过则发修正指令迭代,直到通过再向用户汇报。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DeepSeek Harness 编码协作
|
|
7
|
+
|
|
8
|
+
## 角色
|
|
9
|
+
|
|
10
|
+
- 你 = 项目经理 + 验收员。DeepSeek 编码子代理 = 程序员,在 `--cwd` 指定的工作目录中写代码(任意目录,Codex 打开哪个项目就传哪个目录)。
|
|
11
|
+
- 子代理保留跨任务记忆;独立评审子代理与编码员分离,不信任口头报告,只认真实文件与真实运行结果。
|
|
12
|
+
|
|
13
|
+
## 工具脚本(固定位置)
|
|
14
|
+
|
|
15
|
+
- 派活:`D:/Harness/.codex-collab-tools/task.mjs`
|
|
16
|
+
- 评审:`D:/Harness/.codex-collab-tools/review.mjs`
|
|
17
|
+
|
|
18
|
+
## 下发任务
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
node "D:/Harness/.codex-collab-tools/task.mjs" --in "<完整中文指令>" --cwd "<工作目录>" [--lane <通道>] [--model fast|pro] [--commit] [--timeout 秒]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `--cwd`:**默认传 Codex 当前打开的项目目录**(绝对路径,正斜杠);不存在时自动创建并注册为 harness 工作区。
|
|
25
|
+
- `--lane`:并行通道名(默认 main)。同一目录不同 lane 的任务**并行执行**(如 backend 与 frontend 各一路);跨目录天然并行。
|
|
26
|
+
- `--model`:`fast`(deepseek-v4-flash,快而省)或 `pro`(deepseek-v4-pro,质量优先);默认继承当前模型。
|
|
27
|
+
- `--commit`:任务完成后在 `--cwd` 自动 `git add -A` 并提交(提交信息含任务摘要);非 git 仓库自动跳过。
|
|
28
|
+
- 指令必须包含:目标与产出物、验收标准、约束。`--wait` 为默认行为(结果直接回本对话),最长 10 分钟,长任务加 `--timeout 1800`。
|
|
29
|
+
|
|
30
|
+
## 读取汇报与验收
|
|
31
|
+
|
|
32
|
+
1. 脚本最后输出 `=== DeepSeek 汇报 ===`(做了什么、改动文件、如何验证、待决策问题)。
|
|
33
|
+
2. 读 `--cwd` 中它声称改动的文件核对;必要时自己跑构建/测试。
|
|
34
|
+
3. 通过 → 向用户汇报;不通过 → 发修正指令(子代理记得该目录上下文),迭代到通过。
|
|
35
|
+
|
|
36
|
+
## 双向评审(Codex 验收 DeepSeek 的变更)
|
|
37
|
+
|
|
38
|
+
DeepSeek 任务完成后,把它的变更交给独立评审子代理:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node "D:/Harness/.codex-collab-tools/review.mjs" --cwd "<工作目录>" [--diff git|git-staged|@文件|"diff 文本"] [--focus "<审查重点>"]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `--diff` 默认取 `git diff`(未提交变更);`git-staged` 取暂存区;也可直接给 diff 文本或文件(`@路径`)。
|
|
45
|
+
- 评审员会:读 `--cwd` 真实文件核对 diff → 静态审查(正确性/健壮性/安全/测试缺口)→ **实际运行构建与测试验证可跑通** → 输出结构化报告(结论 + 问题列表 + 跑通验证 + 修改建议)。
|
|
46
|
+
- 你阅读 `=== 评审报告 ===`,有问题就发修正指令给编码员,直到评审通过。
|
|
47
|
+
|
|
48
|
+
## 任务管理
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
node "D:/Harness/.codex-collab-tools/task.mjs" --list # 最近任务列表
|
|
52
|
+
node "D:/Harness/.codex-collab-tools/task.mjs" --status <taskId> # 查询单个任务
|
|
53
|
+
node "D:/Harness/.codex-collab-tools/task.mjs" --cancel <taskId> # 取消运行中的任务
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 故障排查
|
|
57
|
+
|
|
58
|
+
- 连接失败:DeepSeek Harness 未运行(应监听 http://127.0.0.1:3080),请提示用户启动。
|
|
59
|
+
- 读不到 `--cwd` 目录:提示用户在 Codex 设置中把该目录加入可访问/可写目录。
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Codex → DeepSeek Harness 评审脚本:把代码变更 diff 交给独立评审子代理。
|
|
3
|
+
// 评审员会读 cwd 真实文件、静态审查、并实际运行构建/测试验证可跑通。
|
|
4
|
+
// 用法: node review.mjs --cwd <目录> [--diff <diff 文本|@文件|git|git-staged>] [--focus <审查重点>] [--timeout 秒]
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import http from 'node:http'
|
|
7
|
+
import { execFileSync } from 'node:child_process'
|
|
8
|
+
|
|
9
|
+
const GATEWAY = process.env.DSH_BRIDGE_URL || 'http://127.0.0.1:3080'
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const out = { timeoutSec: 900, cwd: '', diff: '', focus: '' }
|
|
13
|
+
const rest = []
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
const a = argv[i]
|
|
16
|
+
if (a === '--cwd') { out.cwd = argv[++i] || '' }
|
|
17
|
+
else if (a === '--diff') { out.diff = argv[++i] || '' }
|
|
18
|
+
else if (a === '--focus') { out.focus = argv[++i] || '' }
|
|
19
|
+
else if (a === '--timeout') { const v = Number(argv[++i]); if (Number.isFinite(v)) out.timeoutSec = v }
|
|
20
|
+
else { rest.push(a) }
|
|
21
|
+
}
|
|
22
|
+
return out
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
26
|
+
|
|
27
|
+
// node:http + Connection: close,避免 undici keep-alive 退出断言
|
|
28
|
+
function request(method, urlStr, body) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
let u
|
|
31
|
+
try { u = new URL(urlStr) } catch (e) { reject(e); return }
|
|
32
|
+
const payload = body !== undefined ? JSON.stringify(body) : undefined
|
|
33
|
+
const headers = { 'Content-Type': 'application/json', Connection: 'close' }
|
|
34
|
+
if (payload !== undefined) headers['Content-Length'] = Buffer.byteLength(payload)
|
|
35
|
+
const req = http.request({
|
|
36
|
+
hostname: u.hostname,
|
|
37
|
+
port: u.port ? Number(u.port) : 80,
|
|
38
|
+
path: u.pathname + u.search,
|
|
39
|
+
method,
|
|
40
|
+
headers,
|
|
41
|
+
}, (res) => {
|
|
42
|
+
const chunks = []
|
|
43
|
+
res.on('data', (c) => chunks.push(c))
|
|
44
|
+
res.on('end', () => {
|
|
45
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
46
|
+
let json = null
|
|
47
|
+
try { json = text === '' ? null : JSON.parse(text) } catch (e) { /* non-JSON */ }
|
|
48
|
+
resolve({ status: res.statusCode || 0, text, json })
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
req.setTimeout(30000, () => req.destroy(new Error('request timeout')))
|
|
52
|
+
req.on('error', reject)
|
|
53
|
+
if (payload !== undefined) req.write(payload)
|
|
54
|
+
req.end()
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveDiff(cwd, diffSpec) {
|
|
59
|
+
if (!diffSpec || diffSpec === '' || diffSpec === 'git') {
|
|
60
|
+
try {
|
|
61
|
+
return execFileSync('git', ['-C', cwd, 'diff'], { stdio: 'pipe', windowsHide: true, maxBuffer: 1024 * 1024 * 4 }).toString('utf8')
|
|
62
|
+
} catch (e) { return '' }
|
|
63
|
+
}
|
|
64
|
+
if (diffSpec === 'git-staged' || diffSpec === 'git-cached') {
|
|
65
|
+
try {
|
|
66
|
+
return execFileSync('git', ['-C', cwd, 'diff', '--cached'], { stdio: 'pipe', windowsHide: true, maxBuffer: 1024 * 1024 * 4 }).toString('utf8')
|
|
67
|
+
} catch (e) { return '' }
|
|
68
|
+
}
|
|
69
|
+
if (diffSpec.startsWith('@')) {
|
|
70
|
+
const file = diffSpec.slice(1)
|
|
71
|
+
try { return fs.readFileSync(file, 'utf8') } catch (e) {
|
|
72
|
+
console.error('review.mjs: 无法读取 diff 文件 ' + file)
|
|
73
|
+
finish(2)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return diffSpec
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function main() {
|
|
80
|
+
const opts = parseArgs(process.argv.slice(2))
|
|
81
|
+
const cwd = (opts.cwd || '').trim()
|
|
82
|
+
if (!cwd) {
|
|
83
|
+
console.error('用法: node review.mjs --cwd <目录> [--diff git|git-staged|@文件|"diff 文本"] [--focus <重点>] [--timeout 秒]')
|
|
84
|
+
finish(2)
|
|
85
|
+
}
|
|
86
|
+
if (!fs.existsSync(cwd)) {
|
|
87
|
+
console.error('review.mjs: 目录不存在: ' + cwd)
|
|
88
|
+
finish(2)
|
|
89
|
+
}
|
|
90
|
+
const diff = resolveDiff(cwd, opts.diff)
|
|
91
|
+
console.log('[review] diff 长度: ' + diff.length + ' 字符')
|
|
92
|
+
const posted = await request('POST', GATEWAY + '/api/dsh-bridge/review', {
|
|
93
|
+
cwd, diff,
|
|
94
|
+
...(opts.focus ? { focus: opts.focus } : {}),
|
|
95
|
+
})
|
|
96
|
+
const task = posted.json
|
|
97
|
+
console.log(JSON.stringify({ phase: 'submitted', ...task }))
|
|
98
|
+
if (posted.status !== 202 || task.status === 'error') finish(posted.status === 202 ? 0 : 1)
|
|
99
|
+
|
|
100
|
+
const deadline = Date.now() + opts.timeoutSec * 1000
|
|
101
|
+
while (Date.now() < deadline) {
|
|
102
|
+
await sleep(3000)
|
|
103
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId))
|
|
104
|
+
const status = res.json
|
|
105
|
+
if (status.status === 'done' || status.status === 'error' || status.status === 'cancelled') {
|
|
106
|
+
console.log(JSON.stringify({ phase: 'finished', ...status }))
|
|
107
|
+
console.log('=== 评审报告 ===')
|
|
108
|
+
console.log(status.result || status.error || '(无内容)')
|
|
109
|
+
finish(status.status === 'done' ? 0 : 1)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
console.error('等待超时(' + opts.timeoutSec + 's),评审仍在后台执行:')
|
|
113
|
+
console.error(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId))
|
|
114
|
+
finish(3)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function finish(code) {
|
|
118
|
+
try { process.stderr.write = () => true } catch (e) { /* ignore */ }
|
|
119
|
+
process.exit(code)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main().catch((e) => { console.error('review.mjs 失败:', (e && e.message) || e); finish(1) })
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Codex → DeepSeek Harness 任务脚本:POST 指令到桥网关,可轮询等待结果。
|
|
3
|
+
// 功能:--cwd 工作目录 / --lane 并行通道 / --model 模型别名 / --commit 完成后 Git 提交
|
|
4
|
+
// --wait 等待结果(默认)/ --list 任务列表 / --cancel <taskId> / --status <taskId>
|
|
5
|
+
// 用法: node task.mjs --in "<指令>" [--cwd 目录] [--lane 名] [--model fast|pro] [--commit] [--timeout 秒]
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import http from 'node:http'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { execFileSync } from 'node:child_process'
|
|
10
|
+
|
|
11
|
+
const GATEWAY = process.env.DSH_BRIDGE_URL || 'http://127.0.0.1:3080'
|
|
12
|
+
|
|
13
|
+
// 历史文件候选位置(按优先顺序):Codex 沙箱可能拒绝写入工作目录,逐级降级。
|
|
14
|
+
// 可用环境变量 DSH_BRIDGE_HISTORY 指定唯一位置。
|
|
15
|
+
const HISTORY_CANDIDATES = process.env.DSH_BRIDGE_HISTORY
|
|
16
|
+
? [process.env.DSH_BRIDGE_HISTORY]
|
|
17
|
+
: [
|
|
18
|
+
path.join('D:', path.sep, 'Harness', '.dsh-collab', 'history.ndjson'),
|
|
19
|
+
path.join(process.env.USERPROFILE || '.', '.dsh-collab', 'history.ndjson'),
|
|
20
|
+
path.join(process.env.TEMP || '.', 'dsh-collab-history.ndjson'),
|
|
21
|
+
]
|
|
22
|
+
const HISTORY_MAX = 40
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const out = { wait: true, timeoutSec: 600, instruction: '', cwd: '', lane: '', model: '', commit: false, list: false, cancel: '', status: '' }
|
|
26
|
+
const rest = []
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const a = argv[i]
|
|
29
|
+
if (a === '--in') { out.instruction = argv[++i] || '' }
|
|
30
|
+
else if (a === '--cwd') { out.cwd = argv[++i] || '' }
|
|
31
|
+
else if (a === '--lane') { out.lane = argv[++i] || '' }
|
|
32
|
+
else if (a === '--model') { out.model = argv[++i] || '' }
|
|
33
|
+
else if (a === '--commit') { out.commit = true }
|
|
34
|
+
else if (a === '--wait') { out.wait = true }
|
|
35
|
+
else if (a === '--no-wait') { out.wait = false }
|
|
36
|
+
else if (a === '--timeout') { const v = Number(argv[++i]); if (Number.isFinite(v)) out.timeoutSec = v }
|
|
37
|
+
else if (a === '--list') { out.list = true }
|
|
38
|
+
else if (a === '--cancel') { out.cancel = argv[++i] || '' }
|
|
39
|
+
else if (a === '--status') { out.status = argv[++i] || '' }
|
|
40
|
+
else { rest.push(a) }
|
|
41
|
+
}
|
|
42
|
+
if (!out.instruction && rest.length > 0) out.instruction = rest.join(' ')
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
47
|
+
|
|
48
|
+
// ---- 请求层:node:http + Connection: close,避免 undici keep-alive 退出断言 ----
|
|
49
|
+
function request(method, urlStr, body) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
let u
|
|
52
|
+
try { u = new URL(urlStr) } catch (e) { reject(e); return }
|
|
53
|
+
const payload = body !== undefined ? JSON.stringify(body) : undefined
|
|
54
|
+
const headers = { 'Content-Type': 'application/json', Connection: 'close' }
|
|
55
|
+
if (payload !== undefined) headers['Content-Length'] = Buffer.byteLength(payload)
|
|
56
|
+
const req = http.request({
|
|
57
|
+
hostname: u.hostname,
|
|
58
|
+
port: u.port ? Number(u.port) : 80,
|
|
59
|
+
path: u.pathname + u.search,
|
|
60
|
+
method,
|
|
61
|
+
headers,
|
|
62
|
+
}, (res) => {
|
|
63
|
+
const chunks = []
|
|
64
|
+
res.on('data', (c) => chunks.push(c))
|
|
65
|
+
res.on('end', () => {
|
|
66
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
67
|
+
let json = null
|
|
68
|
+
try { json = text === '' ? null : JSON.parse(text) } catch (e) { /* non-JSON body */ }
|
|
69
|
+
resolve({ status: res.statusCode || 0, text, json })
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
req.setTimeout(30000, () => req.destroy(new Error('request timeout')))
|
|
73
|
+
req.on('error', reject)
|
|
74
|
+
if (payload !== undefined) req.write(payload)
|
|
75
|
+
req.end()
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- 历史:多位置读取合并 + 逐级降级写入 ----
|
|
80
|
+
function readHistory() {
|
|
81
|
+
const entries = []
|
|
82
|
+
const seen = new Set()
|
|
83
|
+
for (const file of HISTORY_CANDIDATES) {
|
|
84
|
+
try {
|
|
85
|
+
if (!fs.existsSync(file)) continue
|
|
86
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
87
|
+
const t = line.trim()
|
|
88
|
+
if (!t) continue
|
|
89
|
+
try {
|
|
90
|
+
const h = JSON.parse(t)
|
|
91
|
+
if (h && (h.role === 'user' || h.role === 'assistant') && typeof h.text === 'string') {
|
|
92
|
+
const key = h.role + ':' + h.text
|
|
93
|
+
if (!seen.has(key)) { seen.add(key); entries.push(h) }
|
|
94
|
+
}
|
|
95
|
+
} catch (e) { /* skip */ }
|
|
96
|
+
}
|
|
97
|
+
} catch (e) { /* skip */ }
|
|
98
|
+
}
|
|
99
|
+
while (entries.length > HISTORY_MAX) entries.shift()
|
|
100
|
+
return entries
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function appendHistory(newEntries) {
|
|
104
|
+
const all = readHistory().concat(newEntries)
|
|
105
|
+
while (all.length > HISTORY_MAX) all.shift()
|
|
106
|
+
const content = all.map((h) => JSON.stringify(h)).join('\n') + '\n'
|
|
107
|
+
for (const file of HISTORY_CANDIDATES) {
|
|
108
|
+
try {
|
|
109
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
110
|
+
fs.writeFileSync(file, content)
|
|
111
|
+
return
|
|
112
|
+
} catch (e) { /* try next candidate */ }
|
|
113
|
+
}
|
|
114
|
+
// 全部位置不可写:不阻塞任务结果,仅提示
|
|
115
|
+
console.error('task.mjs: 协作历史无法落盘(沙箱限制),跨进程记忆将在本次会话后丢失')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function gitCommit(cwd, message) {
|
|
119
|
+
try {
|
|
120
|
+
const isRepo = (() => {
|
|
121
|
+
try { execFileSync('git', ['-C', cwd, 'rev-parse', '--is-inside-work-tree'], { stdio: 'pipe', windowsHide: true }); return true } catch (e) { return false }
|
|
122
|
+
})()
|
|
123
|
+
if (!isRepo) {
|
|
124
|
+
console.log('[commit] ' + cwd + ' 不是 git 仓库,跳过自动提交')
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
execFileSync('git', ['-C', cwd, 'add', '-A'], { stdio: 'pipe', windowsHide: true })
|
|
128
|
+
const changed = (() => {
|
|
129
|
+
try { execFileSync('git', ['-C', cwd, 'diff', '--cached', '--quiet'], { stdio: 'pipe', windowsHide: true }); return false } catch (e) { return true }
|
|
130
|
+
})()
|
|
131
|
+
if (!changed) {
|
|
132
|
+
console.log('[commit] 无变更,跳过提交')
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
const msg = message.replace(/\r?\n/g, ' ').slice(0, 200)
|
|
136
|
+
execFileSync('git', ['-C', cwd, 'commit', '-m', '[dsh-collab] ' + msg], { stdio: 'pipe', windowsHide: true })
|
|
137
|
+
const hash = execFileSync('git', ['-C', cwd, 'rev-parse', 'HEAD'], { stdio: 'pipe', windowsHide: true }).toString('utf8').trim()
|
|
138
|
+
console.log('[commit] 已提交: ' + hash)
|
|
139
|
+
return hash
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error('[commit] 提交失败:', e.message)
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function main() {
|
|
147
|
+
const opts = parseArgs(process.argv.slice(2))
|
|
148
|
+
|
|
149
|
+
if (opts.list) {
|
|
150
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/tasks')
|
|
151
|
+
console.log(JSON.stringify(res.json, null, 2))
|
|
152
|
+
finish(0)
|
|
153
|
+
}
|
|
154
|
+
if (opts.cancel) {
|
|
155
|
+
const res = await request('POST', GATEWAY + '/api/dsh-bridge/cancel', { taskId: opts.cancel })
|
|
156
|
+
console.log(JSON.stringify(res.json, null, 2))
|
|
157
|
+
finish(0)
|
|
158
|
+
}
|
|
159
|
+
if (opts.status) {
|
|
160
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(opts.status))
|
|
161
|
+
console.log(JSON.stringify(res.json, null, 2))
|
|
162
|
+
finish(0)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const instruction = opts.instruction.trim()
|
|
166
|
+
if (!instruction) {
|
|
167
|
+
console.error('用法: node task.mjs --in "<指令>" [--cwd 目录] [--lane 名] [--model fast|pro] [--commit] [--timeout 秒] [--no-wait]')
|
|
168
|
+
console.error('管理: node task.mjs --list | --status <taskId> | --cancel <taskId>')
|
|
169
|
+
finish(2)
|
|
170
|
+
}
|
|
171
|
+
const cwd = (opts.cwd || process.env.DSH_BRIDGE_CWD || '').trim()
|
|
172
|
+
if (cwd) {
|
|
173
|
+
try { fs.mkdirSync(cwd, { recursive: true }) } catch (e) {
|
|
174
|
+
console.error('task.mjs: 无法创建目录 ' + cwd + ':', e.message)
|
|
175
|
+
finish(2)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const history = readHistory()
|
|
179
|
+
const posted = await request('POST', GATEWAY + '/api/dsh-bridge/task', {
|
|
180
|
+
instruction, history,
|
|
181
|
+
...(cwd ? { cwd } : {}),
|
|
182
|
+
...(opts.lane ? { lane: opts.lane } : {}),
|
|
183
|
+
...(opts.model ? { model: opts.model } : {}),
|
|
184
|
+
})
|
|
185
|
+
const task = posted.json
|
|
186
|
+
console.log(JSON.stringify({ phase: 'submitted', ...task }))
|
|
187
|
+
if (posted.status !== 202 || task.status === 'error') finish(posted.status === 202 ? 0 : 1)
|
|
188
|
+
if (!opts.wait) finish(0)
|
|
189
|
+
|
|
190
|
+
const deadline = Date.now() + opts.timeoutSec * 1000
|
|
191
|
+
while (Date.now() < deadline) {
|
|
192
|
+
await sleep(3000)
|
|
193
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId))
|
|
194
|
+
const status = res.json
|
|
195
|
+
if (status.status === 'done' || status.status === 'error' || status.status === 'cancelled') {
|
|
196
|
+
console.log(JSON.stringify({ phase: 'finished', ...status }))
|
|
197
|
+
console.log('=== DeepSeek 汇报 ===')
|
|
198
|
+
console.log(status.result || status.error || '(无内容)')
|
|
199
|
+
if (status.status === 'done') {
|
|
200
|
+
appendHistory([
|
|
201
|
+
{ role: 'user', text: instruction },
|
|
202
|
+
{ role: 'assistant', text: status.result || '(无输出)' },
|
|
203
|
+
])
|
|
204
|
+
if (opts.commit && cwd) gitCommit(cwd, '[task ' + task.taskId + '] ' + instruction)
|
|
205
|
+
}
|
|
206
|
+
finish(status.status === 'done' ? 0 : 1)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
console.error('等待超时(' + opts.timeoutSec + 's),任务仍在后台执行。可用 status 接口继续查询:')
|
|
210
|
+
console.error(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId))
|
|
211
|
+
finish(3)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 退出收尾:先静默 stderr(抑制 Node 退出时的 libuv 断言噪音),再按码退出。
|
|
215
|
+
// 任务结果以 stdout 中 "=== DeepSeek 汇报 ===" 为准。
|
|
216
|
+
function finish(code) {
|
|
217
|
+
try { process.stderr.write = () => true } catch (e) { /* ignore */ }
|
|
218
|
+
process.exit(code)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
main().catch((e) => { console.error('task.mjs 失败:', (e && e.message) || e); finish(1) })
|