@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/mcp/dsh-mcp.mjs
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dsh-mcp: Codex / ChatGPT MCP 服务器,把 DeepSeek Harness 协作网关暴露为 MCP 工具。
|
|
3
|
+
// 两种传输:
|
|
4
|
+
// stdio(默认):每行一个 JSON-RPC 2.0 消息,供 Codex 等本地 MCP 客户端使用。
|
|
5
|
+
// --http [host:port]:Streamable HTTP(POST /mcp,JSON 请求/响应),供隧道
|
|
6
|
+
// 客户端(Secure MCP Tunnel / cloudflared)桥接给 ChatGPT 等远程产品。
|
|
7
|
+
const GATEWAY = process.env.DSH_BRIDGE_URL || 'http://127.0.0.1:3080'
|
|
8
|
+
const SERVER_NAME = 'dsh-bridge'
|
|
9
|
+
const SERVER_VERSION = '0.2.0'
|
|
10
|
+
const PROTOCOL_VERSION = '2024-11-05'
|
|
11
|
+
|
|
12
|
+
const log = (...args) => console.error('[dsh-mcp]', ...args)
|
|
13
|
+
|
|
14
|
+
const tools = [
|
|
15
|
+
{
|
|
16
|
+
name: 'dsh_task',
|
|
17
|
+
description:
|
|
18
|
+
'把编码任务交给本机 DeepSeek Harness 的编码子代理执行(DeepSeek 写代码,ChatGPT/Codex 验收)。' +
|
|
19
|
+
'参数: instruction(必填,完整中文指令:目标/产出目录/验收标准/约束), cwd(工作目录,默认 D:\\Harness,通常传当前项目目录), ' +
|
|
20
|
+
'lane(并行通道名,默认 main), model(fast=deepseek-v4-flash 快而省 / pro=deepseek-v4-pro 质量优先), ' +
|
|
21
|
+
'wait(默认 true,阻塞至任务完成并返回 DeepSeek 汇报), timeoutSec(默认 600,最长 900)。',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
instruction: { type: 'string', description: '完整中文编码指令(目标、产出目录、验收标准、约束)。' },
|
|
26
|
+
cwd: { type: 'string', description: '工作目录(绝对路径)。默认 D:\\Harness。' },
|
|
27
|
+
lane: { type: 'string', description: '并行通道名(默认 main)。' },
|
|
28
|
+
model: { type: 'string', enum: ['fast', 'pro'], description: '模型别名:fast=deepseek-v4-flash,pro=deepseek-v4-pro。' },
|
|
29
|
+
wait: { type: 'boolean', description: '默认 true:阻塞直到任务完成并返回汇报。' },
|
|
30
|
+
timeoutSec: { type: 'number', description: '最长等待秒数(默认 600,最大 900)。' },
|
|
31
|
+
},
|
|
32
|
+
required: ['instruction'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'dsh_task_status',
|
|
37
|
+
description: '查询 DeepSeek 协作任务的当前状态与结果。参数: taskId(必填)。',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: { taskId: { type: 'string', description: '任务 ID(dsh_task 返回)。' } },
|
|
41
|
+
required: ['taskId'],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'dsh_task_cancel',
|
|
46
|
+
description: '取消一个运行中的 DeepSeek 协作任务。参数: taskId(必填)。',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: { taskId: { type: 'string', description: '任务 ID。' } },
|
|
50
|
+
required: ['taskId'],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'dsh_review',
|
|
55
|
+
description:
|
|
56
|
+
'让独立评审子代理审查某工作目录的代码变更。评审员会读磁盘真实文件、静态审查,并实际运行构建/测试验证可跑通,返回结构化评审报告。' +
|
|
57
|
+
'参数: cwd(必填), diff(变更 diff 文本), focus(审查重点), wait(默认 true), timeoutSec(默认 900)。',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
cwd: { type: 'string', description: '评审目标所在工作目录(绝对路径)。' },
|
|
62
|
+
diff: { type: 'string', description: '变更 diff 文本(git diff 输出)。省略时评审员自行读取目录现状。' },
|
|
63
|
+
focus: { type: 'string', description: '审查重点。' },
|
|
64
|
+
wait: { type: 'boolean', description: '默认 true:阻塞直到评审完成。' },
|
|
65
|
+
timeoutSec: { type: 'number', description: '最长等待秒数(默认 900)。' },
|
|
66
|
+
},
|
|
67
|
+
required: ['cwd'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'dsh_read_file',
|
|
72
|
+
description:
|
|
73
|
+
'读取共享工作目录中的文件内容(用于验收 DeepSeek 的产出,无需本地文件权限)。' +
|
|
74
|
+
'参数: path(必填,绝对路径或相对 cwd 的路径), maxBytes(默认 100000)。仅限 UTF-8 文本文件。',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
path: { type: 'string', description: '要读的文件路径(绝对路径,或相对 D:\\Harness 的路径)。' },
|
|
79
|
+
maxBytes: { type: 'number', description: '最大读取字节数(默认 100000)。' },
|
|
80
|
+
},
|
|
81
|
+
required: ['path'],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
87
|
+
|
|
88
|
+
async function callGateway(method, body) {
|
|
89
|
+
const res = await fetch(GATEWAY + method, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
body: JSON.stringify(body || {}),
|
|
93
|
+
})
|
|
94
|
+
return res.json()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function waitTask(taskId, timeoutSec) {
|
|
98
|
+
const deadline = Date.now() + Math.min(Math.max(timeoutSec || 600, 5), 900) * 1000
|
|
99
|
+
while (Date.now() < deadline) {
|
|
100
|
+
await sleep(3000)
|
|
101
|
+
const res = await fetch(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(taskId))
|
|
102
|
+
const status = await res.json()
|
|
103
|
+
if (status.status === 'done' || status.status === 'error' || status.status === 'cancelled') return status
|
|
104
|
+
}
|
|
105
|
+
return { taskId, status: 'waiting', note: '仍在后台执行,可用 dsh_task_status 查询' }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleToolCall(name, args) {
|
|
109
|
+
if (name === 'dsh_task') {
|
|
110
|
+
const task = await callGateway('/api/dsh-bridge/task', {
|
|
111
|
+
instruction: args.instruction,
|
|
112
|
+
cwd: args.cwd,
|
|
113
|
+
lane: args.lane,
|
|
114
|
+
model: args.model,
|
|
115
|
+
})
|
|
116
|
+
if (task.status === 'error') return { ok: false, ...task }
|
|
117
|
+
if (args.wait === false) return { ok: true, phase: 'submitted', ...task }
|
|
118
|
+
const done = await waitTask(task.taskId, args.timeoutSec || 600)
|
|
119
|
+
return { ok: done.status === 'done', phase: 'finished', ...done }
|
|
120
|
+
}
|
|
121
|
+
if (name === 'dsh_task_status') {
|
|
122
|
+
const res = await fetch(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(args.taskId))
|
|
123
|
+
return res.json()
|
|
124
|
+
}
|
|
125
|
+
if (name === 'dsh_task_cancel') {
|
|
126
|
+
return callGateway('/api/dsh-bridge/cancel', { taskId: args.taskId })
|
|
127
|
+
}
|
|
128
|
+
if (name === 'dsh_review') {
|
|
129
|
+
const task = await callGateway('/api/dsh-bridge/review', { cwd: args.cwd, diff: args.diff, focus: args.focus })
|
|
130
|
+
if (task.status === 'error') return { ok: false, ...task }
|
|
131
|
+
if (args.wait === false) return { ok: true, phase: 'submitted', ...task }
|
|
132
|
+
const done = await waitTask(task.taskId, args.timeoutSec || 900)
|
|
133
|
+
return { ok: done.status === 'done', phase: 'finished', ...done }
|
|
134
|
+
}
|
|
135
|
+
if (name === 'dsh_read_file') {
|
|
136
|
+
const fs = await import('node:fs')
|
|
137
|
+
const path = await import('node:path')
|
|
138
|
+
const DEFAULT_ROOT = process.env.DSH_BRIDGE_ROOT || 'D:\\Harness'
|
|
139
|
+
const raw = String(args.path || '')
|
|
140
|
+
let full = raw
|
|
141
|
+
if (!path.isAbsolute(raw)) full = path.join(DEFAULT_ROOT, raw)
|
|
142
|
+
const maxBytes = Math.min(Math.max(Number(args.maxBytes) || 100000, 1), 1000000)
|
|
143
|
+
const buf = fs.readFileSync(full)
|
|
144
|
+
if (buf.length > maxBytes) {
|
|
145
|
+
return { ok: false, error: 'file too large (' + buf.length + ' bytes, max ' + maxBytes + ')' }
|
|
146
|
+
}
|
|
147
|
+
return { ok: true, path: full, bytes: buf.length, content: buf.toString('utf8') }
|
|
148
|
+
}
|
|
149
|
+
throw new Error('unknown tool: ' + name)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- JSON-RPC 核心(两种传输共用)----
|
|
153
|
+
async function handleMessage(msg) {
|
|
154
|
+
const id = msg.id
|
|
155
|
+
if (msg.method === 'initialize') {
|
|
156
|
+
return { jsonrpc: '2.0', id, result: { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { name: SERVER_NAME, version: SERVER_VERSION } } }
|
|
157
|
+
}
|
|
158
|
+
if (msg.method === 'notifications/initialized' || msg.method === 'notifications/cancelled') return null
|
|
159
|
+
if (msg.method === 'ping') return { jsonrpc: '2.0', id, result: {} }
|
|
160
|
+
if (msg.method === 'tools/list') return { jsonrpc: '2.0', id, result: { tools } }
|
|
161
|
+
if (msg.method === 'tools/call') {
|
|
162
|
+
const name = msg.params && msg.params.name
|
|
163
|
+
const args = (msg.params && msg.params.arguments) || {}
|
|
164
|
+
try {
|
|
165
|
+
const value = await handleToolCall(name, args)
|
|
166
|
+
return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] } }
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: 'error: ' + (e && e.message || e) }], isError: true } }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (id !== undefined) {
|
|
172
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + msg.method } }
|
|
173
|
+
}
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- stdio 传输 ----
|
|
178
|
+
function runStdio() {
|
|
179
|
+
let buffer = ''
|
|
180
|
+
process.stdin.setEncoding('utf8')
|
|
181
|
+
process.stdin.on('data', (chunk) => {
|
|
182
|
+
buffer += chunk
|
|
183
|
+
let idx = buffer.indexOf('\n')
|
|
184
|
+
while (idx !== -1) {
|
|
185
|
+
const line = buffer.slice(0, idx).trim()
|
|
186
|
+
buffer = buffer.slice(idx + 1)
|
|
187
|
+
if (line !== '') {
|
|
188
|
+
let msg
|
|
189
|
+
try { msg = JSON.parse(line) } catch (e) { idx = buffer.indexOf('\n'); continue }
|
|
190
|
+
void handleMessage(msg).then((resp) => {
|
|
191
|
+
if (resp !== null) process.stdout.write(JSON.stringify(resp) + '\n')
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
idx = buffer.indexOf('\n')
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
process.stdin.on('end', () => {
|
|
198
|
+
log('stdio closed')
|
|
199
|
+
setTimeout(() => process.exit(0), 200)
|
|
200
|
+
})
|
|
201
|
+
log('stdio MCP ready, gateway=' + GATEWAY)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---- Streamable HTTP 传输 ----
|
|
205
|
+
async function runHttp(bindSpec) {
|
|
206
|
+
const { createServer } = await import('node:http')
|
|
207
|
+
const [host, portStr] = bindSpec.split(':')
|
|
208
|
+
const port = Number(portStr) || 4800
|
|
209
|
+
const server = createServer((req, res) => {
|
|
210
|
+
if (req.method === 'GET' && (req.url === '/healthz' || req.url === '/health')) {
|
|
211
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
212
|
+
res.end(JSON.stringify({ status: 'ok', server: SERVER_NAME }))
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
if (req.method === 'POST' && (req.url === '/mcp' || req.url === '/')) {
|
|
216
|
+
const chunks = []
|
|
217
|
+
let size = 0
|
|
218
|
+
req.on('data', (c) => { size += c.length; if (size <= 2 * 1024 * 1024) chunks.push(c) })
|
|
219
|
+
req.on('end', () => {
|
|
220
|
+
void (async () => {
|
|
221
|
+
try {
|
|
222
|
+
const body = Buffer.concat(chunks).toString('utf8')
|
|
223
|
+
const parsed = JSON.parse(body)
|
|
224
|
+
const list = Array.isArray(parsed) ? parsed : [parsed]
|
|
225
|
+
const results = []
|
|
226
|
+
for (const msg of list) {
|
|
227
|
+
const resp = await handleMessage(msg)
|
|
228
|
+
if (resp !== null) results.push(resp)
|
|
229
|
+
}
|
|
230
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' })
|
|
231
|
+
res.end(JSON.stringify(Array.isArray(parsed) ? results : (results[0] || {})))
|
|
232
|
+
} catch (e) {
|
|
233
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
234
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error: ' + (e && e.message || e) } }))
|
|
235
|
+
}
|
|
236
|
+
})()
|
|
237
|
+
})
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
241
|
+
res.end(JSON.stringify({ error: 'not found' }))
|
|
242
|
+
})
|
|
243
|
+
server.listen(port, host, () => {
|
|
244
|
+
log('HTTP MCP ready at http://' + host + ':' + port + '/mcp, gateway=' + GATEWAY)
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---- 入口 ----
|
|
249
|
+
const httpArg = process.argv.findIndex((a) => a === '--http')
|
|
250
|
+
if (httpArg !== -1) {
|
|
251
|
+
const spec = process.argv[httpArg + 1] || '127.0.0.1:4800'
|
|
252
|
+
runHttp(spec)
|
|
253
|
+
} else {
|
|
254
|
+
runStdio()
|
|
255
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@whaletalk/dsh-codex-collab",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Codex × DeepSeek Harness 双向编码协作:Codex 主导派活与验收,DeepSeek Harness 编码子代理在共享工作目录执行并回传,独立评审子代理实际跑通构建/测试后出具报告。提供 DSH 宿主网关插件、零依赖 MCP 服务器与 Codex 技能三件套。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.11.0"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"dsh-mcp": "./mcp/dsh-mcp.mjs",
|
|
12
|
+
"dsh-task": "./codex-plugin/plugins/dsh/skills/dsh-collab/scripts/task.mjs",
|
|
13
|
+
"dsh-review": "./codex-plugin/plugins/dsh/skills/dsh-collab/scripts/review.mjs"
|
|
14
|
+
},
|
|
15
|
+
"main": "./harness/dsh-bridge.mjs",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./harness/dsh-bridge.mjs",
|
|
18
|
+
"./bridge": "./harness/dsh-bridge.mjs",
|
|
19
|
+
"./mcp": "./mcp/dsh-mcp.mjs",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"harness/",
|
|
24
|
+
"mcp/",
|
|
25
|
+
"codex-plugin/",
|
|
26
|
+
"cordis.patch.yml",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"dsh-banner.png.png"
|
|
30
|
+
],
|
|
31
|
+
"keywords": [
|
|
32
|
+
"deepseek",
|
|
33
|
+
"deepseek-harness",
|
|
34
|
+
"dsh",
|
|
35
|
+
"dsh-plugin",
|
|
36
|
+
"codex",
|
|
37
|
+
"mcp",
|
|
38
|
+
"collaboration",
|
|
39
|
+
"coding-agent",
|
|
40
|
+
"code-review",
|
|
41
|
+
"pair-programming"
|
|
42
|
+
],
|
|
43
|
+
"author": "FuJackGao",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/Whale-Talk/dsh-codex-collab.git"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/Whale-Talk/dsh-codex-collab#readme",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/Whale-Talk/dsh-codex-collab/issues"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"dsh": {
|
|
56
|
+
"bundle": {
|
|
57
|
+
"patch": "./cordis.patch.yml"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@deepseek-ai/dsh-tools": "*"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@deepseek-ai/dsh-tools": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|