@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
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-bridge: Codex ↔ DeepSeek Harness 双向协作网关(持久化宿主插件)。
|
|
3
|
+
*
|
|
4
|
+
* 提供:
|
|
5
|
+
* - POST /api/dsh-bridge/task 编码任务(cwd/lane/model 参数化)
|
|
6
|
+
* - POST /api/dsh-bridge/review 双向评审(独立评审子代理,必须跑通构建/测试)
|
|
7
|
+
* - GET /api/dsh-bridge/tasks 任务列表
|
|
8
|
+
* - POST /api/dsh-bridge/cancel 取消任务
|
|
9
|
+
* - GET /api/dsh-bridge/status 任务状态
|
|
10
|
+
* - GET /api/dsh-bridge/debug 调试信息
|
|
11
|
+
* - WS /api/dsh-bridge/ws 实时推送(升级协议,供可选客户端使用)
|
|
12
|
+
*
|
|
13
|
+
* 与动态插件版本的关键差异:宿主插件运行在完整 Node 环境,
|
|
14
|
+
* AbortSignal 可用,因此 followup 的 cold-resume 路径不再降级。
|
|
15
|
+
*/
|
|
16
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
17
|
+
|
|
18
|
+
export const name = 'dsh-bridge'
|
|
19
|
+
|
|
20
|
+
export const inject = ['webServer', 'agents', 'subagents', 'tools']
|
|
21
|
+
|
|
22
|
+
export async function apply(ctx) {
|
|
23
|
+
const webServer = ctx.webServer
|
|
24
|
+
const agents = ctx.agents
|
|
25
|
+
const subagents = ctx.subagents
|
|
26
|
+
const timer = ctx.get('timer')
|
|
27
|
+
const sessionQuery = ctx.get('sessionQuery')
|
|
28
|
+
const agentPresets = ctx.get('agentPresets')
|
|
29
|
+
const sessionPersistence = ctx.get('sessionPersistence')
|
|
30
|
+
const workspaceRegistry = ctx.get('workspaceRegistry')
|
|
31
|
+
const llm = ctx.get('llm')
|
|
32
|
+
|
|
33
|
+
// 清理历史版本可能泄漏的路由条目(重启前的动态插件、旧版本次插件)
|
|
34
|
+
try {
|
|
35
|
+
if (webServer.exact instanceof Map) {
|
|
36
|
+
webServer.exact.delete('/api/dsh-bridge/task')
|
|
37
|
+
webServer.exact.delete('/api/dsh-bridge/status')
|
|
38
|
+
webServer.exact.delete('/api/dsh-bridge/debug')
|
|
39
|
+
webServer.exact.delete('/api/dsh-bridge/review')
|
|
40
|
+
webServer.exact.delete('/api/dsh-bridge/tasks')
|
|
41
|
+
webServer.exact.delete('/api/dsh-bridge/cancel')
|
|
42
|
+
}
|
|
43
|
+
if (webServer.upgrades instanceof Map) webServer.upgrades.delete('/api/dsh-bridge/ws')
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error('[dsh-bridge] leaked-route cleanup failed:', e)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_WORKSPACE = process.env.DSH_BRIDGE_WORKSPACE || 'D:\\Harness'
|
|
49
|
+
const uid = () => 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
|
50
|
+
const isLoopback = (req) => {
|
|
51
|
+
const a = req.socket ? req.socket.remoteAddress : ''
|
|
52
|
+
return a === '127.0.0.1' || a === '::1' || a === '::ffff:127.0.0.1'
|
|
53
|
+
}
|
|
54
|
+
const signalOf = () => new AbortController().signal
|
|
55
|
+
|
|
56
|
+
const errText = (e) => {
|
|
57
|
+
let s = e && e.message ? String(e.message) : String(e)
|
|
58
|
+
let c = e && e.cause
|
|
59
|
+
let depth = 0
|
|
60
|
+
while (c !== undefined && c !== null && depth < 5) {
|
|
61
|
+
s += ' || cause: ' + (c && c.message ? String(c.message) : String(c))
|
|
62
|
+
c = c && c.cause
|
|
63
|
+
depth += 1
|
|
64
|
+
}
|
|
65
|
+
return s
|
|
66
|
+
}
|
|
67
|
+
const textOfBlocks = (blocks) => {
|
|
68
|
+
if (blocks === undefined || blocks === null) return ''
|
|
69
|
+
let out = ''
|
|
70
|
+
const list = Array.isArray(blocks) ? blocks : [blocks]
|
|
71
|
+
for (const b of list) {
|
|
72
|
+
if (b === undefined || b === null || typeof b !== 'object') continue
|
|
73
|
+
if (b.type === 'text' && typeof b.text === 'string') out += b.text
|
|
74
|
+
if (Array.isArray(b.content)) out += textOfBlocks(b.content)
|
|
75
|
+
}
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
const qsOf = (req) => {
|
|
79
|
+
const u = req.url || ''
|
|
80
|
+
const qi = u.indexOf('?')
|
|
81
|
+
if (qi === -1) return {}
|
|
82
|
+
const out = {}
|
|
83
|
+
for (const pair of u.slice(qi + 1).split('&')) {
|
|
84
|
+
const ei = pair.indexOf('=')
|
|
85
|
+
try {
|
|
86
|
+
if (ei === -1) out[decodeURIComponent(pair)] = ''
|
|
87
|
+
else out[decodeURIComponent(pair.slice(0, ei))] = decodeURIComponent(pair.slice(ei + 1))
|
|
88
|
+
} catch (e) { /* skip */ }
|
|
89
|
+
}
|
|
90
|
+
return out
|
|
91
|
+
}
|
|
92
|
+
const normalizeCwd = (raw) => {
|
|
93
|
+
if (typeof raw !== 'string' || raw.trim() === '') return DEFAULT_WORKSPACE
|
|
94
|
+
let p = raw.trim().replace(/\//g, '\\')
|
|
95
|
+
if (p.endsWith('\\')) p = p.slice(0, -1)
|
|
96
|
+
if (p.length >= 2 && p.charAt(1) === ':') p = p.charAt(0).toUpperCase() + p.slice(1)
|
|
97
|
+
return p
|
|
98
|
+
}
|
|
99
|
+
const normalizeLane = (raw) => {
|
|
100
|
+
if (typeof raw !== 'string' || raw.trim() === '') return 'main'
|
|
101
|
+
const lane = raw.trim().replace(/[^A-Za-z0-9_-]/g, '').slice(0, 32)
|
|
102
|
+
return lane === '' ? 'main' : lane
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pickModelOptions = () => {
|
|
106
|
+
try {
|
|
107
|
+
for (const a of agents.list()) {
|
|
108
|
+
const o = a.options
|
|
109
|
+
if (o && typeof o.provider === 'string' && typeof o.model === 'string') {
|
|
110
|
+
const out = { provider: o.provider, model: o.model }
|
|
111
|
+
if (o.maxTokens !== undefined) out.maxTokens = o.maxTokens
|
|
112
|
+
return out
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (e) { /* fall through */ }
|
|
116
|
+
try {
|
|
117
|
+
const adm = ctx.get('agentDefaultModel')
|
|
118
|
+
if (adm !== undefined) {
|
|
119
|
+
const sel = adm.currentSelection()
|
|
120
|
+
if (sel && typeof sel.provider === 'string' && typeof sel.model === 'string') {
|
|
121
|
+
return { provider: sel.provider, model: sel.model }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (e) { /* fall through */ }
|
|
125
|
+
return {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- model alias resolution ----
|
|
129
|
+
const modelAliasCache = new Map()
|
|
130
|
+
let knownModels = []
|
|
131
|
+
let modelProvider = ''
|
|
132
|
+
let modelListError = ''
|
|
133
|
+
const resolveModel = async (alias) => {
|
|
134
|
+
if (typeof alias !== 'string' || alias === '' || alias === 'default') return null
|
|
135
|
+
if (modelAliasCache.has(alias)) return modelAliasCache.get(alias)
|
|
136
|
+
if (llm === undefined) return null
|
|
137
|
+
let result = null
|
|
138
|
+
try {
|
|
139
|
+
const base = pickModelOptions()
|
|
140
|
+
const provider = base.provider
|
|
141
|
+
if (typeof provider === 'string' && provider !== '') {
|
|
142
|
+
modelProvider = provider
|
|
143
|
+
const models = await llm.listModels(provider)
|
|
144
|
+
const idOf = (m) => (typeof m === 'string' ? m : (m.id || m.name || ''))
|
|
145
|
+
knownModels = models.map(idOf).filter((x) => x !== '').slice(0, 30)
|
|
146
|
+
if (alias === 'pro') {
|
|
147
|
+
const hit = models.find((m) => /pro/i.test(idOf(m)))
|
|
148
|
+
result = hit !== undefined ? idOf(hit) : null
|
|
149
|
+
} else if (alias === 'fast') {
|
|
150
|
+
const hit = models.find((m) => /flash|lite|fast|mini|light/i.test(idOf(m)))
|
|
151
|
+
result = hit !== undefined ? idOf(hit) : null
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
modelListError = errText(e).slice(0, 300)
|
|
156
|
+
console.error('[dsh-bridge] model list failed:', modelListError)
|
|
157
|
+
}
|
|
158
|
+
modelAliasCache.set(alias, result)
|
|
159
|
+
console.log('[dsh-bridge] model alias', alias, '->', result)
|
|
160
|
+
return result
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- collaboration history(由 Codex 侧 task.mjs 维护并随请求同步)----
|
|
164
|
+
let history = []
|
|
165
|
+
const syncHistory = (incoming) => {
|
|
166
|
+
if (Array.isArray(incoming)) {
|
|
167
|
+
const valid = incoming.filter((h) => h && (h.role === 'user' || h.role === 'assistant') && typeof h.text === 'string')
|
|
168
|
+
history = valid.slice(-40)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const remember = (role, text) => {
|
|
172
|
+
history.push({ role: role, text: text })
|
|
173
|
+
if (history.length > 40) history.splice(0, history.length - 40)
|
|
174
|
+
}
|
|
175
|
+
const historyDigest = () => {
|
|
176
|
+
if (history.length === 0) return ''
|
|
177
|
+
const lines = ['【此前协作历史摘要】']
|
|
178
|
+
for (const h of history) {
|
|
179
|
+
const who = h.role === 'user' ? 'Codex 指令' : '你的汇报'
|
|
180
|
+
lines.push(who + ': ' + h.text.slice(0, 2000))
|
|
181
|
+
}
|
|
182
|
+
return lines.join('\n')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---- per-(cwd, lane) 编码桶 + per-cwd 评审桶 ----
|
|
186
|
+
const buckets = new Map()
|
|
187
|
+
const reviewerBuckets = new Map()
|
|
188
|
+
const tasks = []
|
|
189
|
+
const sockets = new Set()
|
|
190
|
+
|
|
191
|
+
const broadcast = (msg) => {
|
|
192
|
+
let line
|
|
193
|
+
try { line = JSON.stringify(msg) + '\n' } catch (e) { return }
|
|
194
|
+
for (const s of sockets) {
|
|
195
|
+
try { s.write(line) } catch (e) { /* drop */ }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const pushTask = (taskId, type, payload) => {
|
|
199
|
+
const msg = { type, taskId, time: Date.now() }
|
|
200
|
+
if (payload !== undefined && payload !== null) {
|
|
201
|
+
for (const k of Object.keys(payload)) msg[k] = payload[k]
|
|
202
|
+
}
|
|
203
|
+
broadcast(msg)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const bucketOf = (cwd, lane) => {
|
|
207
|
+
const key = cwd + '|' + lane
|
|
208
|
+
let b = buckets.get(key)
|
|
209
|
+
if (b === undefined) {
|
|
210
|
+
b = { key, cwd, lane, model: null, handle: null, agent: null, promise: null, childId: null, childPromise: null, lastSeen: 0, seenCount: 0, activeSince: 0 }
|
|
211
|
+
buckets.set(key, b)
|
|
212
|
+
}
|
|
213
|
+
return b
|
|
214
|
+
}
|
|
215
|
+
const reviewerBucketOf = (cwd) => {
|
|
216
|
+
let b = reviewerBuckets.get(cwd)
|
|
217
|
+
if (b === undefined) {
|
|
218
|
+
b = { key: cwd + '|@review', cwd, lane: '@review', model: null, handle: null, agent: null, promise: null, childId: null, childPromise: null, lastSeen: 0, seenCount: 0, activeSince: 0 }
|
|
219
|
+
reviewerBuckets.set(cwd, b)
|
|
220
|
+
}
|
|
221
|
+
return b
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const registerWorkspace = (cwd) => {
|
|
225
|
+
if (workspaceRegistry === undefined) return
|
|
226
|
+
void (async () => {
|
|
227
|
+
try {
|
|
228
|
+
const existing = await workspaceRegistry.resolveByPath(cwd)
|
|
229
|
+
if (existing === undefined) await workspaceRegistry.create(cwd)
|
|
230
|
+
} catch (e) {
|
|
231
|
+
console.error('[dsh-bridge] workspace register failed for ' + cwd + ':', errText(e).slice(0, 200))
|
|
232
|
+
}
|
|
233
|
+
})()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const createOwner = async (bucket) => {
|
|
237
|
+
const modelOptions = pickModelOptions()
|
|
238
|
+
const handle = await agents.create({
|
|
239
|
+
sessionId: 'dsh-bridge-owner-' + uid(),
|
|
240
|
+
meta: { cwd: bucket.cwd, origin: 'subagent', agentPreset: 'code' },
|
|
241
|
+
agentOptions: modelOptions,
|
|
242
|
+
setup: async (agentCtx) => {
|
|
243
|
+
if (agentPresets !== undefined) await agentPresets.mount(agentCtx, 'code')
|
|
244
|
+
},
|
|
245
|
+
})
|
|
246
|
+
bucket.handle = handle
|
|
247
|
+
bucket.agent = handle.agent
|
|
248
|
+
const b = bucket
|
|
249
|
+
bucket.agent.ctx.on('subagent/end', (info) => { onChildEnd(b, info) })
|
|
250
|
+
console.log('[dsh-bridge] owner ready for', bucket.key, ':', bucket.agent.id)
|
|
251
|
+
return bucket.agent
|
|
252
|
+
}
|
|
253
|
+
const ensureOwner = (bucket) => {
|
|
254
|
+
if (bucket.agent !== null) return Promise.resolve(bucket.agent)
|
|
255
|
+
if (bucket.promise === null) {
|
|
256
|
+
bucket.promise = createOwner(bucket).catch((e) => { bucket.promise = null; throw e })
|
|
257
|
+
}
|
|
258
|
+
return bucket.promise
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const supportsContinuable = (name) => {
|
|
262
|
+
try {
|
|
263
|
+
const p = subagents.getProvider(name)
|
|
264
|
+
return p !== undefined && typeof p.prepareContinuable === 'function'
|
|
265
|
+
} catch (e) { return false }
|
|
266
|
+
}
|
|
267
|
+
const pickProvider = () => {
|
|
268
|
+
if (supportsContinuable('subagent')) return 'subagent'
|
|
269
|
+
let names = []
|
|
270
|
+
try { names = subagents.list() } catch (e) { return undefined }
|
|
271
|
+
for (const n of names) if (supportsContinuable(n)) return n
|
|
272
|
+
return undefined
|
|
273
|
+
}
|
|
274
|
+
const ROLE_BRIEF = (cwd) => [
|
|
275
|
+
'你是在工作目录 ' + cwd + ' 工作的 DeepSeek 编码工人,负责执行 Codex 下发的编码任务。',
|
|
276
|
+
'任务完成后用中文简要汇报:做了什么、改了哪些文件(相对路径)、如何验证、以及需要 Codex 决策的问题。',
|
|
277
|
+
'你的汇报会实时转发给 Codex 供验收。没有新任务时不要主动输出。任务内容如下:',
|
|
278
|
+
].join('')
|
|
279
|
+
const REVIEWER_BRIEF = (cwd) => [
|
|
280
|
+
'你是工作目录 ' + cwd + ' 的独立代码评审员,与编码员完全分离,不信任任何口头报告。',
|
|
281
|
+
'每次评审请求包含:变更 diff 与审查重点。你必须:',
|
|
282
|
+
'1) 读取工作目录中相关文件的最新内容,核对 diff 与磁盘一致;',
|
|
283
|
+
'2) 静态审查:正确性、健壮性、风格、安全隐患、测试缺口;',
|
|
284
|
+
'3) 实际运行构建与测试命令,验证代码可以跑通——运行失败即判不通过,并附命令与输出摘要;',
|
|
285
|
+
'4) 输出结构化评审报告:【结论:通过/不通过】+【问题列表(按严重度排序,含文件:行号)】+【跑通验证(命令与输出摘要)】+【修改建议】。',
|
|
286
|
+
'不要修改任何文件。没有新请求时不要主动输出。',
|
|
287
|
+
].join('')
|
|
288
|
+
|
|
289
|
+
const startChildWith = async (bucket, firstInstruction, brief) => {
|
|
290
|
+
const parent = await ensureOwner(bucket)
|
|
291
|
+
const provider = pickProvider()
|
|
292
|
+
if (provider === undefined) throw new Error('no continuable subagent provider registered')
|
|
293
|
+
const digest = historyDigest()
|
|
294
|
+
const request = {
|
|
295
|
+
prompt: [{ type: 'text', text: (digest !== '' ? digest + '\n\n' : '') + brief + firstInstruction }],
|
|
296
|
+
parent,
|
|
297
|
+
toolFilter: { deny: ['dsh_collab_send', 'dsh_collab_review'] },
|
|
298
|
+
}
|
|
299
|
+
if (bucket.model !== null) request.agentOptions = { model: bucket.model }
|
|
300
|
+
const started = await subagents.startContinuable({
|
|
301
|
+
provider,
|
|
302
|
+
label: bucket.lane === '@review' ? 'codex-collab-reviewer' : 'codex-collab-worker-' + bucket.lane,
|
|
303
|
+
request,
|
|
304
|
+
signal: signalOf(),
|
|
305
|
+
})
|
|
306
|
+
bucket.childId = started.childId
|
|
307
|
+
console.log('[dsh-bridge] worker started for', bucket.key, ':', bucket.childId, 'model:', bucket.model)
|
|
308
|
+
return bucket.childId
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const onChildEnd = (bucket, info) => {
|
|
312
|
+
if (bucket.childId === null || info.id !== bucket.childId) return
|
|
313
|
+
const text = textOfBlocks(info.lastAssistantMessage)
|
|
314
|
+
const pending = tasks.filter((t) => (t.status === 'accepted' || t.status === 'running') && t.bucketKey === bucket.key)
|
|
315
|
+
const task = pending.length > 0 ? pending[0] : null
|
|
316
|
+
if (task !== null) {
|
|
317
|
+
task.status = 'done'
|
|
318
|
+
task.result = text.length > 0 ? text : '(无输出)'
|
|
319
|
+
task.stopReason = info.stopReason
|
|
320
|
+
remember('user', task.instruction)
|
|
321
|
+
remember('assistant', task.result)
|
|
322
|
+
pushTask(task.taskId, 'result', { text: task.result, stopReason: info.stopReason, childId: bucket.childId, cwd: bucket.cwd, kind: task.kind })
|
|
323
|
+
} else {
|
|
324
|
+
broadcast({ type: 'result', taskId: null, childId: bucket.childId, time: Date.now(), text, stopReason: info.stopReason, cwd: bucket.cwd, kind: bucket.lane === '@review' ? 'review' : 'task' })
|
|
325
|
+
}
|
|
326
|
+
bucket.activeSince = 0
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const deliver = async (bucket, instruction, brief, followupPrefix) => {
|
|
330
|
+
const parent = await ensureOwner(bucket)
|
|
331
|
+
if (bucket.childId === null) {
|
|
332
|
+
if (bucket.childPromise === null) {
|
|
333
|
+
bucket.childPromise = startChildWith(bucket, instruction, brief).catch((e) => { bucket.childPromise = null; throw e })
|
|
334
|
+
await bucket.childPromise
|
|
335
|
+
} else {
|
|
336
|
+
await bucket.childPromise
|
|
337
|
+
await subagents.followup(parent, bucket.childId, [{ type: 'text', text: followupPrefix + instruction }], {
|
|
338
|
+
source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id },
|
|
339
|
+
signal: signalOf(),
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
try {
|
|
344
|
+
await subagents.followup(parent, bucket.childId, [{ type: 'text', text: followupPrefix + instruction }], {
|
|
345
|
+
source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id },
|
|
346
|
+
signal: signalOf(),
|
|
347
|
+
})
|
|
348
|
+
} catch (followupError) {
|
|
349
|
+
// 兜底:one-shot + 历史摘要(正常情况下宿主插件的真 AbortSignal 已打通 cold-resume)
|
|
350
|
+
const digest = historyDigest()
|
|
351
|
+
const text = (digest !== '' ? digest + '\n\n' : '') + brief + instruction
|
|
352
|
+
const request = { prompt: [{ type: 'text', text }], parent, signal: signalOf() }
|
|
353
|
+
if (bucket.model !== null) request.agentOptions = { model: bucket.model }
|
|
354
|
+
const run = await subagents.start('spawn', Object.assign({ label: 'codex-collab-' + bucket.lane + '-' + uid() }, request))
|
|
355
|
+
const result = await run.result
|
|
356
|
+
const out = textOfBlocks(result.output)
|
|
357
|
+
bucket.childId = null
|
|
358
|
+
bucket.childPromise = null
|
|
359
|
+
bucket.lastSeen = 0
|
|
360
|
+
bucket.seenCount = 0
|
|
361
|
+
void run.dispose()
|
|
362
|
+
return { fallback: true, output: out.length > 0 ? out : '(无输出)', stopReason: result.stopReason, fallbackNote: 'one-shot (cold-resume unavailable: ' + errText(followupError).slice(0, 200) + ')' }
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return { fallback: false, output: '', stopReason: '', fallbackNote: '' }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const enqueue = async (instruction, incomingHistory, rawCwd, rawLane, modelAlias, kind) => {
|
|
369
|
+
if (incomingHistory !== undefined) syncHistory(incomingHistory)
|
|
370
|
+
const cwd = normalizeCwd(rawCwd)
|
|
371
|
+
const lane = normalizeLane(rawLane)
|
|
372
|
+
registerWorkspace(cwd)
|
|
373
|
+
const bucket = bucketOf(cwd, lane)
|
|
374
|
+
const requestedModel = await resolveModel(modelAlias)
|
|
375
|
+
if (requestedModel !== null && requestedModel !== bucket.model) {
|
|
376
|
+
bucket.model = requestedModel
|
|
377
|
+
bucket.childId = null
|
|
378
|
+
bucket.childPromise = null
|
|
379
|
+
bucket.lastSeen = 0
|
|
380
|
+
bucket.seenCount = 0
|
|
381
|
+
}
|
|
382
|
+
const task = { taskId: uid(), kind, instruction, cwd, lane, bucketKey: bucket.key, status: 'accepted', time: Date.now(), result: undefined, error: undefined }
|
|
383
|
+
tasks.push(task)
|
|
384
|
+
if (tasks.length > 200) tasks.splice(0, tasks.length - 200)
|
|
385
|
+
try {
|
|
386
|
+
const delivered = await deliver(bucket, instruction, ROLE_BRIEF(cwd), '[Codex 指令] ')
|
|
387
|
+
if (delivered.fallback) {
|
|
388
|
+
task.status = 'done'
|
|
389
|
+
task.result = delivered.output
|
|
390
|
+
task.stopReason = delivered.stopReason
|
|
391
|
+
task.fallback = delivered.fallbackNote
|
|
392
|
+
remember('user', instruction)
|
|
393
|
+
remember('assistant', task.result)
|
|
394
|
+
pushTask(task.taskId, 'result', { text: task.result, stopReason: delivered.stopReason, childId: null, cwd, kind })
|
|
395
|
+
} else {
|
|
396
|
+
bucket.activeSince = Date.now()
|
|
397
|
+
pushTask(task.taskId, 'task-accepted', { childId: bucket.childId, cwd, kind })
|
|
398
|
+
}
|
|
399
|
+
} catch (e) {
|
|
400
|
+
task.status = 'error'
|
|
401
|
+
task.error = errText(e)
|
|
402
|
+
pushTask(task.taskId, 'error', { error: task.error, cwd, kind })
|
|
403
|
+
}
|
|
404
|
+
return task
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const submitReview = async (rawCwd, diff, focus, incomingHistory) => {
|
|
408
|
+
if (incomingHistory !== undefined) syncHistory(incomingHistory)
|
|
409
|
+
const cwd = normalizeCwd(rawCwd)
|
|
410
|
+
const bucket = reviewerBucketOf(cwd)
|
|
411
|
+
const instruction = ['【变更 diff】\n' + (typeof diff === 'string' && diff.trim() !== '' ? diff.slice(0, 60000) : '(未提供 diff)'), '【审查重点】\n' + (typeof focus === 'string' && focus.trim() !== '' ? focus.slice(0, 4000) : '(未指定,请按一般标准审查)')].join('\n\n')
|
|
412
|
+
const task = { taskId: uid(), kind: 'review', instruction, cwd, lane: '@review', bucketKey: bucket.key, status: 'accepted', time: Date.now(), result: undefined, error: undefined }
|
|
413
|
+
tasks.push(task)
|
|
414
|
+
if (tasks.length > 200) tasks.splice(0, tasks.length - 200)
|
|
415
|
+
try {
|
|
416
|
+
const delivered = await deliver(bucket, instruction, REVIEWER_BRIEF(cwd), '[评审请求] ')
|
|
417
|
+
if (delivered.fallback) {
|
|
418
|
+
task.status = 'done'
|
|
419
|
+
task.result = delivered.output
|
|
420
|
+
task.stopReason = delivered.stopReason
|
|
421
|
+
task.fallback = delivered.fallbackNote
|
|
422
|
+
remember('user', '[评审请求] ' + (typeof focus === 'string' && focus !== '' ? focus : 'diff 审查'))
|
|
423
|
+
remember('assistant', task.result)
|
|
424
|
+
pushTask(task.taskId, 'result', { text: task.result, stopReason: delivered.stopReason, childId: null, cwd, kind: 'review' })
|
|
425
|
+
} else {
|
|
426
|
+
bucket.activeSince = Date.now()
|
|
427
|
+
pushTask(task.taskId, 'task-accepted', { childId: bucket.childId, cwd, kind: 'review' })
|
|
428
|
+
}
|
|
429
|
+
} catch (e) {
|
|
430
|
+
task.status = 'error'
|
|
431
|
+
task.error = errText(e)
|
|
432
|
+
pushTask(task.taskId, 'error', { error: task.error, cwd, kind: 'review' })
|
|
433
|
+
}
|
|
434
|
+
return task
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const assistantTextOf = (event) => {
|
|
438
|
+
if (event === undefined || event === null || event.type !== 'assistant/message') return ''
|
|
439
|
+
const data = event.data
|
|
440
|
+
const msg = data && data.message !== undefined ? data.message : data
|
|
441
|
+
const content = msg && msg.content !== undefined ? msg.content : data && data.content
|
|
442
|
+
return textOfBlocks(content)
|
|
443
|
+
}
|
|
444
|
+
const pollOne = async (bucket) => {
|
|
445
|
+
if (bucket.childId === null || sessionQuery === undefined) return
|
|
446
|
+
const hasPending = tasks.some((t) => (t.status === 'accepted' || t.status === 'running') && t.bucketKey === bucket.key)
|
|
447
|
+
let snap
|
|
448
|
+
try { snap = await sessionQuery.readSession(bucket.childId) } catch (e) { return }
|
|
449
|
+
const events = snap && Array.isArray(snap.events) ? snap.events : []
|
|
450
|
+
let total = ''
|
|
451
|
+
for (const e of events) total += assistantTextOf(e) + '\n'
|
|
452
|
+
const fresh = total.slice(bucket.lastSeen)
|
|
453
|
+
bucket.lastSeen = total.length
|
|
454
|
+
const newEvents = events.length > bucket.seenCount
|
|
455
|
+
bucket.seenCount = events.length
|
|
456
|
+
if (!hasPending) return
|
|
457
|
+
if (newEvents) bucket.activeSince = Date.now()
|
|
458
|
+
if (fresh.trim() !== '') {
|
|
459
|
+
const active = tasks.find((t) => (t.status === 'accepted' || t.status === 'running') && t.bucketKey === bucket.key)
|
|
460
|
+
if (active !== undefined) active.status = 'running'
|
|
461
|
+
pushTask(active !== undefined ? active.taskId : bucket.childId, 'progress', { text: fresh.trim(), cwd: bucket.cwd, kind: bucket.lane === '@review' ? 'review' : 'task' })
|
|
462
|
+
}
|
|
463
|
+
if (bucket.activeSince > 0 && Date.now() - bucket.activeSince > 600000) {
|
|
464
|
+
const active = tasks.find((t) => (t.status === 'accepted' || t.status === 'running') && t.bucketKey === bucket.key)
|
|
465
|
+
if (active !== undefined && total.length > 0) {
|
|
466
|
+
active.status = 'done'
|
|
467
|
+
active.result = total.slice(Math.max(0, total.length - 4000)).trim() || '(无输出)'
|
|
468
|
+
pushTask(active.taskId, 'result', { text: active.result, stopReason: 'quiet-timeout', childId: bucket.childId, cwd: bucket.cwd, kind: bucket.lane === '@review' ? 'review' : 'task' })
|
|
469
|
+
bucket.activeSince = 0
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (timer !== undefined) {
|
|
474
|
+
ctx.effect(() => timer.interval(() => {
|
|
475
|
+
void (async () => {
|
|
476
|
+
for (const bucket of buckets.values()) await pollOne(bucket)
|
|
477
|
+
for (const bucket of reviewerBuckets.values()) await pollOne(bucket)
|
|
478
|
+
})()
|
|
479
|
+
}, 2500))
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const readBody = (req) => new Promise((resolve, reject) => {
|
|
483
|
+
const chunks = []
|
|
484
|
+
let size = 0
|
|
485
|
+
req.on('data', (c) => {
|
|
486
|
+
size += c.length
|
|
487
|
+
if (size > 2 * 1024 * 1024) { req.destroy(); reject(new Error('body too large')); return }
|
|
488
|
+
chunks.push(c)
|
|
489
|
+
})
|
|
490
|
+
req.on('end', () => {
|
|
491
|
+
let total = 0
|
|
492
|
+
for (const c of chunks) total += c.length
|
|
493
|
+
const merged = new Uint8Array(total)
|
|
494
|
+
let off = 0
|
|
495
|
+
for (const c of chunks) { merged.set(c, off); off += c.length }
|
|
496
|
+
resolve(new TextDecoder().decode(merged))
|
|
497
|
+
})
|
|
498
|
+
req.on('error', reject)
|
|
499
|
+
})
|
|
500
|
+
const sendJson = (res, code, obj) => {
|
|
501
|
+
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' })
|
|
502
|
+
res.end(JSON.stringify(obj))
|
|
503
|
+
}
|
|
504
|
+
const readJsonBody = async (req, res) => {
|
|
505
|
+
let body
|
|
506
|
+
try { body = await readBody(req) } catch (e) { sendJson(res, 400, { error: errText(e) }); return null }
|
|
507
|
+
try { return JSON.parse(body || '{}') } catch (e) { sendJson(res, 400, { error: 'invalid JSON' }); return null }
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
ctx.effect(() => webServer.register({
|
|
511
|
+
kind: 'exact',
|
|
512
|
+
path: '/api/dsh-bridge/task',
|
|
513
|
+
handler: async (req, res) => {
|
|
514
|
+
if (!isLoopback(req)) { res.writeHead(403); res.end('forbidden'); return }
|
|
515
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('POST only'); return }
|
|
516
|
+
const parsed = await readJsonBody(req, res)
|
|
517
|
+
if (parsed === null) return
|
|
518
|
+
const instruction = parsed !== null && typeof parsed === 'object' && typeof parsed.instruction === 'string' ? parsed.instruction : ''
|
|
519
|
+
if (instruction.trim() === '') { sendJson(res, 400, { error: 'missing instruction' }); return }
|
|
520
|
+
const task = await enqueue(instruction, parsed.history, parsed.cwd, parsed.lane, parsed.model, 'task')
|
|
521
|
+
sendJson(res, 202, { taskId: task.taskId, status: task.status, cwd: task.cwd, lane: task.lane, model: parsed.model || null })
|
|
522
|
+
},
|
|
523
|
+
}))
|
|
524
|
+
|
|
525
|
+
ctx.effect(() => webServer.register({
|
|
526
|
+
kind: 'exact',
|
|
527
|
+
path: '/api/dsh-bridge/review',
|
|
528
|
+
handler: async (req, res) => {
|
|
529
|
+
if (!isLoopback(req)) { res.writeHead(403); res.end('forbidden'); return }
|
|
530
|
+
if (req.method !== 'POST') { res.writeHead(405); res.end('POST only'); return }
|
|
531
|
+
const parsed = await readJsonBody(req, res)
|
|
532
|
+
if (parsed === null) return
|
|
533
|
+
const task = await submitReview(parsed.cwd, parsed.diff, parsed.focus, parsed.history)
|
|
534
|
+
sendJson(res, 202, { taskId: task.taskId, status: task.status, cwd: task.cwd, kind: 'review' })
|
|
535
|
+
},
|
|
536
|
+
}))
|
|
537
|
+
|
|
538
|
+
ctx.effect(() => webServer.register({
|
|
539
|
+
kind: 'exact',
|
|
540
|
+
path: '/api/dsh-bridge/tasks',
|
|
541
|
+
handler: async (req, res) => {
|
|
542
|
+
if (!isLoopback(req)) { res.writeHead(403); res.end('forbidden'); return }
|
|
543
|
+
sendJson(res, 200, { tasks: tasks.slice(-50).map((t) => ({ taskId: t.taskId, kind: t.kind, cwd: t.cwd, lane: t.lane, status: t.status, time: t.time, instruction: t.instruction.slice(0, 200), error: t.error, fallback: t.fallback })) })
|
|
544
|
+
},
|
|
545
|
+
}))
|
|
546
|
+
|
|
547
|
+
ctx.effect(() => webServer.register({
|
|
548
|
+
kind: 'exact',
|
|
549
|
+
path: '/api/dsh-bridge/cancel',
|
|
550
|
+
handler: async (req, res) => {
|
|
551
|
+
if (!isLoopback(req)) { res.writeHead(403); res.end('forbidden'); return }
|
|
552
|
+
const parsed = await readJsonBody(req, res)
|
|
553
|
+
if (parsed === null) return
|
|
554
|
+
const taskId = parsed && parsed.taskId
|
|
555
|
+
if (typeof taskId !== 'string' || taskId === '') { sendJson(res, 400, { error: 'missing taskId' }); return }
|
|
556
|
+
const task = tasks.find((t) => t.taskId === taskId)
|
|
557
|
+
if (task === undefined) { sendJson(res, 404, { error: 'unknown taskId' }); return }
|
|
558
|
+
if (task.status !== 'accepted' && task.status !== 'running') { sendJson(res, 200, { taskId, cancelled: false, status: task.status }); return }
|
|
559
|
+
task.status = 'cancelled'
|
|
560
|
+
task.cancelReason = 'cancelled by request'
|
|
561
|
+
let interrupted = false
|
|
562
|
+
try {
|
|
563
|
+
const bucket = buckets.get(task.bucketKey) || reviewerBuckets.get(task.cwd)
|
|
564
|
+
if (bucket !== undefined && bucket.childId !== null && bucket.agent !== null) {
|
|
565
|
+
subagents.interrupt(bucket.childId, bucket.agent)
|
|
566
|
+
interrupted = true
|
|
567
|
+
}
|
|
568
|
+
} catch (e) {
|
|
569
|
+
console.error('[dsh-bridge] cancel interrupt failed:', errText(e).slice(0, 200))
|
|
570
|
+
}
|
|
571
|
+
pushTask(taskId, 'cancelled', { cwd: task.cwd })
|
|
572
|
+
sendJson(res, 200, { taskId, cancelled: true, interrupted })
|
|
573
|
+
},
|
|
574
|
+
}))
|
|
575
|
+
|
|
576
|
+
ctx.effect(() => webServer.register({
|
|
577
|
+
kind: 'exact',
|
|
578
|
+
path: '/api/dsh-bridge/status',
|
|
579
|
+
handler: async (req, res) => {
|
|
580
|
+
if (!isLoopback(req)) { res.writeHead(403); res.end('forbidden'); return }
|
|
581
|
+
const qs = qsOf(req)
|
|
582
|
+
const taskId = qs.taskId
|
|
583
|
+
if (taskId !== undefined && taskId !== '') {
|
|
584
|
+
const task = tasks.find((t) => t.taskId === taskId)
|
|
585
|
+
if (task === undefined) { sendJson(res, 404, { error: 'unknown taskId' }); return }
|
|
586
|
+
sendJson(res, 200, { taskId: task.taskId, status: task.status, kind: task.kind, cwd: task.cwd, lane: task.lane, instruction: task.instruction.slice(0, 300), result: task.result, error: task.error, fallback: task.fallback })
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
const pending = tasks.filter((t) => t.status === 'accepted' || t.status === 'running').length
|
|
590
|
+
sendJson(res, 200, { pending, lanes: Array.from(buckets.keys()), reviewers: Array.from(reviewerBuckets.keys()), tasks: tasks.slice(-20).map((t) => ({ taskId: t.taskId, kind: t.kind, status: t.status, cwd: t.cwd, lane: t.lane })) })
|
|
591
|
+
},
|
|
592
|
+
}))
|
|
593
|
+
|
|
594
|
+
ctx.effect(() => webServer.register({
|
|
595
|
+
kind: 'exact',
|
|
596
|
+
path: '/api/dsh-bridge/debug',
|
|
597
|
+
handler: async (req, res) => {
|
|
598
|
+
if (!isLoopback(req)) { res.writeHead(403); res.end('forbidden'); return }
|
|
599
|
+
const out = { pending: 0, historyCount: history.length, presets: [], providers: [], buckets: [], reviewers: [], models: knownModels, modelProvider, modelListError, workspaces: [] }
|
|
600
|
+
out.pending = tasks.filter((t) => t.status === 'accepted' || t.status === 'running').length
|
|
601
|
+
try { out.providers = subagents.list() } catch (e) { out.providers = ['list failed: ' + errText(e)] }
|
|
602
|
+
if (agentPresets !== undefined) {
|
|
603
|
+
try {
|
|
604
|
+
const list = await agentPresets.list()
|
|
605
|
+
out.presets = list.map((p) => (typeof p === 'string' ? p : (p.id || p.name || '?'))).slice(0, 20)
|
|
606
|
+
} catch (e) { out.presets = ['list failed: ' + errText(e)] }
|
|
607
|
+
}
|
|
608
|
+
if (workspaceRegistry !== undefined) {
|
|
609
|
+
try { out.workspaces = workspaceRegistry.list().map((w) => w.id) } catch (e) { out.workspaces = ['list failed: ' + errText(e)] }
|
|
610
|
+
}
|
|
611
|
+
for (const bucket of buckets.values()) {
|
|
612
|
+
out.buckets.push({ key: bucket.key, cwd: bucket.cwd, lane: bucket.lane, model: bucket.model, childId: bucket.childId })
|
|
613
|
+
}
|
|
614
|
+
for (const bucket of reviewerBuckets.values()) {
|
|
615
|
+
out.reviewers.push({ cwd: bucket.cwd, childId: bucket.childId })
|
|
616
|
+
}
|
|
617
|
+
sendJson(res, 200, out)
|
|
618
|
+
},
|
|
619
|
+
}))
|
|
620
|
+
|
|
621
|
+
ctx.effect(() => webServer.registerUpgrade({
|
|
622
|
+
path: '/api/dsh-bridge/ws',
|
|
623
|
+
handler: (req, socket) => {
|
|
624
|
+
if (!isLoopback(req)) { socket.destroy(); return }
|
|
625
|
+
const upgrade = String(req.headers.upgrade || '').toLowerCase()
|
|
626
|
+
if (upgrade !== 'dsh-bridge') { socket.destroy(); return }
|
|
627
|
+
socket.write('HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: dsh-bridge\r\n\r\n')
|
|
628
|
+
sockets.add(socket)
|
|
629
|
+
let buf = ''
|
|
630
|
+
socket.on('data', (chunk) => {
|
|
631
|
+
buf += new TextDecoder().decode(chunk)
|
|
632
|
+
let idx = buf.indexOf('\n')
|
|
633
|
+
while (idx !== -1) {
|
|
634
|
+
const line = buf.slice(0, idx).trim()
|
|
635
|
+
buf = buf.slice(idx + 1)
|
|
636
|
+
if (line !== '') {
|
|
637
|
+
let msg = null
|
|
638
|
+
try { msg = JSON.parse(line) } catch (e) { msg = null }
|
|
639
|
+
if (msg !== null) {
|
|
640
|
+
if (msg.type === 'ping') {
|
|
641
|
+
try { socket.write(JSON.stringify({ type: 'pong', time: Date.now() }) + '\n') } catch (e) {}
|
|
642
|
+
} else if (msg.type === 'instruction' && typeof msg.text === 'string' && msg.text.trim() !== '') {
|
|
643
|
+
void enqueue(msg.text, msg.history, msg.cwd, msg.lane, msg.model, 'task')
|
|
644
|
+
} else if (msg.type === 'review') {
|
|
645
|
+
void submitReview(msg.cwd, msg.diff, msg.focus, msg.history)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
idx = buf.indexOf('\n')
|
|
650
|
+
}
|
|
651
|
+
})
|
|
652
|
+
socket.on('close', () => { sockets.delete(socket) })
|
|
653
|
+
socket.on('error', () => { sockets.delete(socket) })
|
|
654
|
+
socket.write(JSON.stringify({ type: 'hello', lanes: Array.from(buckets.keys()), time: Date.now() }) + '\n')
|
|
655
|
+
},
|
|
656
|
+
}))
|
|
657
|
+
|
|
658
|
+
// ---- 原生工具(宿主全局工具层)----
|
|
659
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
660
|
+
name: 'dsh_collab_send',
|
|
661
|
+
description: '向 Codex 协作频道发送一条编码指令,由编码子代理在指定工作目录执行(DeepSeek 侧入口)。指令用中文。',
|
|
662
|
+
parameters: {
|
|
663
|
+
instruction: { type: 'string', required: true, description: '要交给编码子代理执行的指令。' },
|
|
664
|
+
cwd: { type: 'string', description: '工作目录(默认 ' + DEFAULT_WORKSPACE + ')。' },
|
|
665
|
+
lane: { type: 'string', description: '并行通道名(默认 main,同目录不同 lane 并行)。' },
|
|
666
|
+
model: { type: 'string', description: '模型别名: default | fast | pro。' },
|
|
667
|
+
wait: { type: 'boolean', description: '为 true 时等待结果(最长 15 分钟)。' },
|
|
668
|
+
},
|
|
669
|
+
output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }] },
|
|
670
|
+
async execute(args) {
|
|
671
|
+
const task = await enqueue(args.instruction, undefined, args.cwd, args.lane, args.model, 'task')
|
|
672
|
+
if (args.wait !== true || timer === undefined) {
|
|
673
|
+
return { taskId: task.taskId, status: task.status, cwd: task.cwd, lane: task.lane }
|
|
674
|
+
}
|
|
675
|
+
const t0 = Date.now()
|
|
676
|
+
while (task.status === 'accepted' || task.status === 'running') {
|
|
677
|
+
if (Date.now() - t0 > 900000) return { taskId: task.taskId, status: task.status, result: task.result, note: 'wait 超时' }
|
|
678
|
+
await timer.timeout(2500)
|
|
679
|
+
}
|
|
680
|
+
return { taskId: task.taskId, status: task.status, cwd: task.cwd, lane: task.lane, result: task.result, error: task.error }
|
|
681
|
+
},
|
|
682
|
+
})))
|
|
683
|
+
|
|
684
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
685
|
+
name: 'dsh_collab_review',
|
|
686
|
+
description: '请独立评审子代理审查某工作目录中的代码变更(diff),评审员会读真实文件并实际运行构建/测试验证可跑通(DeepSeek 侧入口)。',
|
|
687
|
+
parameters: {
|
|
688
|
+
cwd: { type: 'string', required: true, description: '评审目标所在工作目录。' },
|
|
689
|
+
diff: { type: 'string', description: '变更 diff 文本(git diff 输出)。' },
|
|
690
|
+
focus: { type: 'string', description: '审查重点。' },
|
|
691
|
+
wait: { type: 'boolean', description: '为 true 时等待评审结果(最长 15 分钟)。' },
|
|
692
|
+
},
|
|
693
|
+
output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }] },
|
|
694
|
+
async execute(args) {
|
|
695
|
+
const task = await submitReview(args.cwd, args.diff, args.focus, undefined)
|
|
696
|
+
if (args.wait !== true || timer === undefined) {
|
|
697
|
+
return { taskId: task.taskId, status: task.status, cwd: task.cwd }
|
|
698
|
+
}
|
|
699
|
+
const t0 = Date.now()
|
|
700
|
+
while (task.status === 'accepted' || task.status === 'running') {
|
|
701
|
+
if (Date.now() - t0 > 900000) return { taskId: task.taskId, status: task.status, result: task.result, note: 'wait 超时' }
|
|
702
|
+
await timer.timeout(2500)
|
|
703
|
+
}
|
|
704
|
+
return { taskId: task.taskId, status: task.status, cwd: task.cwd, result: task.result, error: task.error }
|
|
705
|
+
},
|
|
706
|
+
})))
|
|
707
|
+
|
|
708
|
+
ctx.effect(() => () => {
|
|
709
|
+
for (const s of sockets) { try { s.destroy() } catch (e) {} }
|
|
710
|
+
sockets.clear()
|
|
711
|
+
for (const bucket of buckets.values()) {
|
|
712
|
+
if (bucket.handle !== null) { void bucket.handle.dispose() }
|
|
713
|
+
}
|
|
714
|
+
for (const bucket of reviewerBuckets.values()) {
|
|
715
|
+
if (bucket.handle !== null) { void bucket.handle.dispose() }
|
|
716
|
+
}
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
console.log('[dsh-bridge] persistent gateway ready (host composition): task · review · tasks · cancel · status · debug · WS')
|
|
720
|
+
}
|