@whaletalk/dsh-codex-collab 0.1.7 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +197 -26
- package/codex-plugin/plugins/dsh/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/plugins/dsh/skills/dsh-collab/SKILL.md +102 -6
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/review.mjs +91 -6
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/task.mjs +571 -44
- package/docs/CODEX_DSH_APPROVAL_DELEGATION_PLAN.md +130 -0
- package/docs/CODEX_DSH_APPROVAL_M0_EVIDENCE.md +285 -0
- package/docs/CODEX_DSH_DELIVERY_CONTROL_PLAN.md +81 -0
- package/docs/CODEX_DSH_MODEL_ROUTING_PLAN.md +75 -0
- package/docs/CODEX_DSH_TASK_REPLY_PLAN.md +206 -0
- package/docs/DSH_APPROVAL_DELEGATION.md +142 -0
- package/docs/DSH_DELIVERY_M0_EVIDENCE.md +69 -0
- package/docs/DSH_MODEL_ROUTING_M0_EVIDENCE.md +166 -0
- package/docs/DSH_TO_CODEX_INBOX.md +167 -0
- package/docs/DSH_TO_CODEX_MESSAGING_PLAN.md +140 -0
- package/docs/codex-desktop-attachment-evidence.md +116 -0
- package/harness/audit-tail.mjs +32 -0
- package/harness/codex-approvals.mjs +1078 -0
- package/harness/codex-associations.mjs +297 -0
- package/harness/codex-outbox.mjs +432 -0
- package/harness/dsh-bridge.mjs +2429 -167
- package/harness/fs-lock.mjs +208 -0
- package/harness/model-route-host.mjs +530 -0
- package/harness/model-routing.mjs +500 -0
- package/harness/session-observer.mjs +217 -0
- package/harness/session-target.mjs +291 -2
- package/harness/task-table.mjs +34 -0
- package/mcp/codex-desktop-probe.mjs +407 -0
- package/mcp/codex-inbox.mjs +271 -0
- package/mcp/dsh-mcp.mjs +616 -24
- package/package.json +4 -2
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
// Codex → DeepSeek Harness 任务脚本:POST 指令到桥网关,可轮询等待结果。
|
|
3
3
|
// 功能:--cwd 工作目录 / --lane 并行通道 / --model 模型别名 / --commit 完成后 Git 提交
|
|
4
4
|
// --wait 等待结果(默认)/ --list 任务列表 / --cancel <taskId> / --status <taskId>
|
|
5
|
-
//
|
|
5
|
+
// --origin <Codex对话id> 来源关联(跨回合精确收取;显式参数优先,其次本次调用的 CODEX_THREAD_ID)
|
|
6
|
+
// --my-tasks 列出本来源登记过的任务(找回丢失的 taskId)/ --require-origin 无来源则如实拒绝
|
|
7
|
+
// 审批委托:--approval-key-init|--approval-key-rotate(离线初始化/轮换控制凭据,仓库外私有目录)
|
|
8
|
+
// --delegate(会话目标派活时显式开启委托;预检凭据/能力/来源,失败零执行)
|
|
9
|
+
// --approvals [--grant <id>] [--wait-approval 秒] / --approval-show <key>
|
|
10
|
+
// --approve <key> --grant <id> --fingerprint <fp> / --reject <key>(同前;仅单次)
|
|
11
|
+
// --revoke-grant <grantId>;审批类退出码 9=needs-approval(任务非终态、等审批)
|
|
12
|
+
// 用法: node task.mjs --in "<指令>" [--cwd 目录] [--lane 名] [--model fast|pro] [--commit] [--timeout 秒] [--origin <id>]
|
|
6
13
|
import fs from 'node:fs'
|
|
7
14
|
import http from 'node:http'
|
|
8
15
|
import path from 'node:path'
|
|
9
16
|
import { execFileSync } from 'node:child_process'
|
|
17
|
+
import { initControlKey, controlHeaderValue } from '../../../../../../harness/codex-approvals.mjs'
|
|
10
18
|
|
|
11
19
|
const GATEWAY = process.env.DSH_BRIDGE_URL || 'http://127.0.0.1:3080'
|
|
12
20
|
|
|
@@ -22,14 +30,19 @@ const HISTORY_CANDIDATES = process.env.DSH_BRIDGE_HISTORY
|
|
|
22
30
|
const HISTORY_MAX = 40
|
|
23
31
|
|
|
24
32
|
function parseArgs(argv) {
|
|
25
|
-
const out = { wait: true, timeoutSec: 600, instruction: '', cwd: '', lane: '', model: '', commit: false, list: false, cancel: '', status: '', sessions: '', sessionId: '', find: '', steer: false, force: false }
|
|
33
|
+
const out = { wait: true, timeoutSec: 600, instruction: '', cwd: '', lane: '', model: '', commit: false, list: false, cancel: '', status: '', statusWait: false, sessions: '', sessionId: '', find: '', steer: false, parent: '', expectedTurn: 0, force: false, origin: '', myTasks: false, requireOrigin: false, approvals: false, approvalShow: '', approvalKey: '', grantId: '', fingerprint: '', idem: '', decision: '', revokeGrant: '', renewGrant: '', renewNow: false, extendSec: 0, taskId: '', reason: '', keyInit: false, keyRotate: false, delegate: false, delegateTtl: 0, delegateLease: 0, authorization: '', approvalWait: 0, models: false, provider: '', modelFallback: undefined }
|
|
26
34
|
const rest = []
|
|
27
35
|
for (let i = 0; i < argv.length; i++) {
|
|
28
36
|
const a = argv[i]
|
|
29
37
|
if (a === '--in') { out.instruction = argv[++i] || '' }
|
|
30
38
|
else if (a === '--cwd') { out.cwd = argv[++i] || '' }
|
|
31
39
|
else if (a === '--lane') { out.lane = argv[++i] || '' }
|
|
32
|
-
else if (a === '--model') {
|
|
40
|
+
else if (a === '--model') {
|
|
41
|
+
// 新开放精确 model id 后同样要有缺值语义(--model --something 不能被当成 model id)。
|
|
42
|
+
const v = argv[++i]
|
|
43
|
+
if (v === undefined || v.trim() === '' || v.startsWith('--')) { out.valueError = '--model 需要一个值(auto/fast/pro/default 或精确 model id)'; continue }
|
|
44
|
+
out.model = v.trim()
|
|
45
|
+
}
|
|
33
46
|
else if (a === '--commit') { out.commit = true }
|
|
34
47
|
else if (a === '--wait') { out.wait = true }
|
|
35
48
|
else if (a === '--no-wait') { out.wait = false }
|
|
@@ -37,27 +50,81 @@ function parseArgs(argv) {
|
|
|
37
50
|
else if (a === '--list') { out.list = true }
|
|
38
51
|
else if (a === '--cancel') { out.cancel = argv[++i] || '' }
|
|
39
52
|
else if (a === '--status') { out.status = argv[++i] || '' }
|
|
53
|
+
else if (a === '--status-wait') { out.statusWait = true }
|
|
40
54
|
else if (a === '--sessions') { out.sessions = argv[++i] || '' }
|
|
41
55
|
else if (a === '--session') { out.sessionId = argv[++i] || '' }
|
|
42
56
|
else if (a === '--find') { out.find = argv[++i] || '' }
|
|
43
|
-
else if (a === '--steer') { out.steer = true }
|
|
57
|
+
else if (a === '--steer') { out.steer = true; out.deliver = 'steer' }
|
|
58
|
+
else if (a === '--parent') { out.parent = argv[++i] || ''; out.deliver = 'steer' }
|
|
59
|
+
else if (a === '--expected-turn') { const v = Number(argv[++i]); if (Number.isInteger(v) && v > 0) out.expectedTurn = v }
|
|
44
60
|
else if (a === '--dry-run') { out.dryRun = true }
|
|
45
61
|
else if (a === '--force') { out.force = true }
|
|
62
|
+
// ---- 动态模型路由(M2)----
|
|
63
|
+
else if (a === '--models') { out.models = true }
|
|
64
|
+
else if (a === '--provider') {
|
|
65
|
+
// 精确 provider 是 model 的身份对的一半:缺值必须显式报错,绝不静默回落 auto。
|
|
66
|
+
const v = argv[++i]
|
|
67
|
+
if (v === undefined || v.trim() === '' || v.startsWith('--')) { out.valueError = '--provider 需要一个值(已注册的 provider id)'; continue }
|
|
68
|
+
out.provider = v.trim()
|
|
69
|
+
}
|
|
70
|
+
else if (a === '--model-fallback') { out.modelFallback = true }
|
|
71
|
+
else if (a === '--no-model-fallback') { out.modelFallback = false }
|
|
72
|
+
// ---- 审批委托 ----
|
|
73
|
+
else if (a === '--approval-key-init') { out.keyInit = true }
|
|
74
|
+
else if (a === '--approval-key-rotate') { out.keyRotate = true }
|
|
75
|
+
else if (a === '--delegate') { out.delegate = true }
|
|
76
|
+
else if (a === '--delegate-ttl') { const v = Number(argv[++i]); if (Number.isFinite(v) && v > 0) out.delegateTtl = v }
|
|
77
|
+
else if (a === '--delegate-lease') { const v = Number(argv[++i]); if (Number.isFinite(v) && v > 0) out.delegateLease = v }
|
|
78
|
+
else if (a === '--authorization') { out.authorization = argv[++i] || '' }
|
|
79
|
+
else if (a === '--approvals') { out.approvals = true }
|
|
80
|
+
else if (a === '--approval-show') { out.approvalShow = argv[++i] || '' }
|
|
81
|
+
else if (a === '--approve') { out.decision = 'approve-once'; out.approvalKey = argv[++i] || '' }
|
|
82
|
+
else if (a === '--reject') { out.decision = 'reject'; out.approvalKey = argv[++i] || '' }
|
|
83
|
+
else if (a === '--revoke-grant') { out.revokeGrant = argv[++i] || '' }
|
|
84
|
+
else if (a === '--renew-grant') { out.renewGrant = argv[++i] || '' }
|
|
85
|
+
else if (a === '--renew') { out.renewNow = true }
|
|
86
|
+
else if (a === '--extend') { const v = Number(argv[++i]); if (Number.isFinite(v) && v > 0) out.extendSec = v }
|
|
87
|
+
else if (a === '--task') { out.taskId = argv[++i] || '' }
|
|
88
|
+
else if (a === '--grant') { out.grantId = argv[++i] || '' }
|
|
89
|
+
else if (a === '--fingerprint') { out.fingerprint = argv[++i] || '' }
|
|
90
|
+
else if (a === '--idem') { out.idem = argv[++i] || '' }
|
|
91
|
+
else if (a === '--reason') { out.reason = argv[++i] || '' }
|
|
92
|
+
else if (a === '--wait-approval') { const v = Number(argv[++i]); if (Number.isFinite(v)) out.approvalWait = Math.min(Math.max(v, 0), 30) }
|
|
93
|
+
else if (a === '--origin') {
|
|
94
|
+
// 来源 id 必须紧跟一个非空、不以 -- 开头的值;否则按用法错误处理(不静默降级)。
|
|
95
|
+
const v = argv[++i]
|
|
96
|
+
if (v === undefined || v.trim() === '' || v.startsWith('--')) { out.originError = '--origin 需要一个值(Codex 对话 id)'; continue }
|
|
97
|
+
out.origin = v.trim()
|
|
98
|
+
}
|
|
99
|
+
else if (a === '--my-tasks') { out.myTasks = true }
|
|
100
|
+
else if (a === '--require-origin') { out.requireOrigin = true }
|
|
46
101
|
else { rest.push(a) }
|
|
47
102
|
}
|
|
48
103
|
if (!out.instruction && rest.length > 0) out.instruction = rest.join(' ')
|
|
49
104
|
return out
|
|
50
105
|
}
|
|
51
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 解析来源对话 id:**显式参数优先**,其次本次调用的 CODEX_THREAD_ID(CLI 每次都是独立进程,
|
|
109
|
+
* 因此这个环境变量确实是"这一次调用"的上下文)。绝不使用常驻进程的启动环境或"最近对话"推断。
|
|
110
|
+
* @returns {{originThreadId?: string, source: 'explicit'|'env'|'none'}}
|
|
111
|
+
*/
|
|
112
|
+
function resolveOrigin(opts) {
|
|
113
|
+
if (typeof opts.origin === 'string' && opts.origin.trim() !== '') return { originThreadId: opts.origin.trim(), source: 'explicit' }
|
|
114
|
+
const fromEnv = typeof process.env.CODEX_THREAD_ID === 'string' ? process.env.CODEX_THREAD_ID.trim() : ''
|
|
115
|
+
if (fromEnv !== '') return { originThreadId: fromEnv, source: 'env' }
|
|
116
|
+
return { source: 'none' }
|
|
117
|
+
}
|
|
118
|
+
|
|
52
119
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
53
120
|
|
|
54
121
|
// ---- 请求层:node:http + Connection: close,避免 undici keep-alive 退出断言 ----
|
|
55
|
-
function request(method, urlStr, body) {
|
|
122
|
+
function request(method, urlStr, body, extraHeaders) {
|
|
56
123
|
return new Promise((resolve, reject) => {
|
|
57
124
|
let u
|
|
58
125
|
try { u = new URL(urlStr) } catch (e) { reject(e); return }
|
|
59
126
|
const payload = body !== undefined ? JSON.stringify(body) : undefined
|
|
60
|
-
const headers = { 'Content-Type': 'application/json', Connection: 'close' }
|
|
127
|
+
const headers = { 'Content-Type': 'application/json', Connection: 'close', ...(extraHeaders || {}) }
|
|
61
128
|
if (payload !== undefined) headers['Content-Length'] = Buffer.byteLength(payload)
|
|
62
129
|
const req = http.request({
|
|
63
130
|
hostname: u.hostname,
|
|
@@ -82,6 +149,124 @@ function request(method, urlStr, body) {
|
|
|
82
149
|
})
|
|
83
150
|
}
|
|
84
151
|
|
|
152
|
+
/**
|
|
153
|
+
* 只读核验网关是否真的支持"持久来源绑定"(GET /status,不会派活)。
|
|
154
|
+
* **不靠包版本猜**:旧网关会忽略 originThreadId 并照常 accepted → 产生"未绑定"任务。
|
|
155
|
+
*/
|
|
156
|
+
async function originBindingSupported() {
|
|
157
|
+
try {
|
|
158
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/status')
|
|
159
|
+
const body = res.json
|
|
160
|
+
return body !== null && typeof body === 'object'
|
|
161
|
+
&& body.capabilities !== null && typeof body.capabilities === 'object'
|
|
162
|
+
&& body.capabilities.originBinding !== null && typeof body.capabilities.originBinding === 'object'
|
|
163
|
+
&& body.capabilities.originBinding.supported === true
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 只要这次调用真的会带上来源,就必须先确认网关支持;否则明确拒绝(绝不静默变成无关联派活)。 */
|
|
170
|
+
async function requireOriginSupport(origin) {
|
|
171
|
+
if (origin.originThreadId === undefined) return true
|
|
172
|
+
if (await originBindingSupported()) return true
|
|
173
|
+
console.error('task.mjs: 该网关不支持持久来源绑定(/status 未上报 capabilities.originBinding)。旧网关会忽略 originThreadId 并照常 accepted,从而产生"未绑定"的任务。')
|
|
174
|
+
console.error('已拒绝执行;请先升级 DSH profile 里的 dsh-codex-collab 插件,或去掉 --origin 使用旧的无关联模式。')
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 网关自己给出的"明确拒绝"reason:原样透传(这些不是"成功了但没确认")。 */
|
|
179
|
+
const REFUSAL_REASONS = new Set(['origin-required', 'origin-mismatch', 'invalid-origin', 'unknown-task', 'execution-state-unconfirmed'])
|
|
180
|
+
|
|
181
|
+
/** 网关必须**回显并匹配**我们请求的 originThreadId,才算确认了这次绑定/取值。 */
|
|
182
|
+
function originConfirmed(payload, origin) {
|
|
183
|
+
return payload !== null && typeof payload === 'object' && payload.originThreadId === origin.originThreadId
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 能力检查通过 ≠ 这次绑定被确认:网关可能中途换过处理者(重载/旧 handler),接受了请求却没写关联。
|
|
188
|
+
* 此时任务**可能已经执行**——既不能说成功,也绝不能重发。
|
|
189
|
+
*/
|
|
190
|
+
function unconfirmedExit(origin, extra) {
|
|
191
|
+
console.error('task.mjs: 网关接受了请求但没有确认来源绑定(响应里没有匹配的 originThreadId)。这次任务可能已经在无关联模式下执行——**不要重发**;请用 --my-tasks/--status 复核,并检查 DSH profile 里的 dsh-codex-collab 插件版本。')
|
|
192
|
+
console.error('task.mjs: 来源关联 originThreadId=' + origin.originThreadId + '(' + (origin.source === 'explicit' ? '--origin' : 'CODEX_THREAD_ID') + ')')
|
|
193
|
+
console.log(JSON.stringify({ phase: 'unconfirmed', reason: 'origin-not-confirmed', resend: false, ...extra }))
|
|
194
|
+
finish(8)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* **唯一的有界等待路径**(第四轮修正):初次 `--in` 默认等待与 `--status-wait` 共用。
|
|
199
|
+
* - 显式来源时每个 status 都带 originThreadId,并**逐次**严格核对回显(缺失/错源 → 退出码 8,不透 result、不重发);
|
|
200
|
+
* - 按响应里的 grant 字段做**自适应认证续租**(轮询间隔 = 租约剩余/3;不传 extendSec 只续租约);
|
|
201
|
+
* - `needs-approval` 视为非终态 → 立即返回(调用方据此退出 9),绝不阻塞等 done;
|
|
202
|
+
* - 终态 `done`/`error`/`cancelled`/`unknown` 原样返回,语义与旧实现一致;无委托任务不做任何续租(旧兼容)。
|
|
203
|
+
* 返回 `{ reason: 'terminal'|'needs-approval'|'timeout', status }`。
|
|
204
|
+
*/
|
|
205
|
+
async function awaitTerminal({ taskId, origin, grantId, grantLeaseUntil, timeoutSec, waitHeaders }) {
|
|
206
|
+
const deadline = Date.now() + timeoutSec * 1000
|
|
207
|
+
const originQs = origin.originThreadId === undefined ? '' : '&originThreadId=' + encodeURIComponent(origin.originThreadId)
|
|
208
|
+
const renewOrigin = encodeURIComponent(origin.originThreadId === undefined ? '' : origin.originThreadId)
|
|
209
|
+
let effectiveGrantId = typeof grantId === 'string' && grantId !== '' ? grantId : ''
|
|
210
|
+
let leaseUntil = Number.isFinite(grantLeaseUntil) ? grantLeaseUntil : 0
|
|
211
|
+
let pollMs = leaseUntil > 0 ? Math.min(3000, Math.max(250, Math.floor(Math.max(leaseUntil - Date.now(), 0) / 3))) : 3000
|
|
212
|
+
let receiptDeadline = 0 // 回执等待上限(≤30s),与父任务结果等待分离(第四轮首次验收修正 C)
|
|
213
|
+
while (Date.now() < deadline) {
|
|
214
|
+
await sleep(pollMs)
|
|
215
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(taskId) + originQs)
|
|
216
|
+
const status = res.json
|
|
217
|
+
if (status === null || typeof status !== 'object') continue
|
|
218
|
+
// 逐次核对来源回显(网关的明确拒绝原样透传,不算"未确认")
|
|
219
|
+
if (origin.originThreadId !== undefined) {
|
|
220
|
+
const refused = typeof status.reason === 'string' && REFUSAL_REASONS.has(status.reason)
|
|
221
|
+
if (!refused && !originConfirmed(status, origin)) unconfirmedExit(origin, { taskId })
|
|
222
|
+
}
|
|
223
|
+
// 自适应认证续租(只用响应里的 grant 字段;未委托任务没有这些字段 → 完全不续)
|
|
224
|
+
if (typeof status.grantId === 'string' && status.grantId !== '') effectiveGrantId = status.grantId
|
|
225
|
+
if (Number.isFinite(status.grantLeaseUntil)) leaseUntil = status.grantLeaseUntil
|
|
226
|
+
if (waitHeaders !== null && effectiveGrantId !== '') {
|
|
227
|
+
const remaining = leaseUntil === 0 ? null : leaseUntil - Date.now()
|
|
228
|
+
if (remaining !== null && remaining < Math.max(800, pollMs)) {
|
|
229
|
+
const renewed = await request('GET', GATEWAY + '/api/dsh-bridge/approvals?originThreadId=' + renewOrigin + '&taskId=' + encodeURIComponent(taskId) + '&grantId=' + encodeURIComponent(effectiveGrantId), undefined, waitHeaders)
|
|
230
|
+
const g = renewed.json !== null && typeof renewed.json === 'object' && Array.isArray(renewed.json.grants) ? renewed.json.grants[0] : null
|
|
231
|
+
if (g !== null && Number.isFinite(g.leaseUntil)) leaseUntil = g.leaseUntil
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const after = leaseUntil === 0 ? null : leaseUntil - Date.now()
|
|
235
|
+
pollMs = after === null ? 3000 : Math.min(3000, Math.max(250, Math.floor(Math.max(after, 0) / 3)))
|
|
236
|
+
// 回执(第四轮首次验收修正 C):只等**回执自身**的终态,绝不等父任务的 done。
|
|
237
|
+
// observed / deviated / unconfirmed 都是回执终态;pending 到点如实返回(不重发、不套父结果等待)。
|
|
238
|
+
if (status.deliver === 'steer' || typeof status.receiptState === 'string') {
|
|
239
|
+
const state = status.receiptState
|
|
240
|
+
if (state === 'observed' || state === 'deviated' || state === 'unconfirmed') return { reason: 'receipt-terminal', status }
|
|
241
|
+
if (receiptDeadline === 0) receiptDeadline = Date.now() + Math.min(30000, Math.max(1000, timeoutSec * 1000))
|
|
242
|
+
if (Date.now() >= receiptDeadline) return { reason: 'receipt-pending', status }
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (status.status === 'needs-approval') return { reason: 'needs-approval', status }
|
|
246
|
+
if (status.status === 'done' || status.status === 'error' || status.status === 'cancelled' || status.status === 'unknown') {
|
|
247
|
+
return { reason: 'terminal', status }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { reason: 'timeout', status: null }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 回执终态输出(第四轮首次验收修正 C):回执只报自身状态,绝不等到父任务的 done。
|
|
255
|
+
* observed/pending → 退出 0;deviated/unconfirmed → 退出 4(明确"未确认/偏离",但**不要重发**)。
|
|
256
|
+
*/
|
|
257
|
+
function receiptOutcome(st, reason, origin) {
|
|
258
|
+
const state = st !== null && typeof st === 'object' ? st.receiptState : undefined
|
|
259
|
+
const phase = reason === 'receipt-pending' ? 'receipt-pending'
|
|
260
|
+
: (state === 'observed' ? 'receipt-observed' : (state === 'deviated' ? 'receipt-deviated' : 'receipt-unconfirmed'))
|
|
261
|
+
console.log(JSON.stringify({ phase, resultScope: 'parent-task', resend: false, ...st }))
|
|
262
|
+
console.error('这是**补充回执**(resultScope=parent-task, receiptState=' + String(state) + '):不代表父任务已完成,也不是独立答案。')
|
|
263
|
+
console.error(' * 父任务最终结果: node task.mjs --status ' + String(st.parentTaskId || '') + (origin !== undefined && origin.originThreadId !== undefined ? ' --origin ' + origin.originThreadId : '') + ' --status-wait')
|
|
264
|
+
if (phase === 'receipt-pending') console.error(' * 仍在观察寿命内继续求证(**不要重发**);稍后再查一次即可。')
|
|
265
|
+
if (phase === 'receipt-deviated') console.error(' * 官方把这条补充落进了另一个回合(偏离目标父回合)——**不要重发**。')
|
|
266
|
+
if (phase === 'receipt-unconfirmed') console.error(' * 未能证明自身 rpcId 落进父回合——**不要重发**。')
|
|
267
|
+
finish(phase === 'receipt-observed' || phase === 'receipt-pending' ? 0 : 4)
|
|
268
|
+
}
|
|
269
|
+
|
|
85
270
|
// ---- 历史:多位置读取合并 + 逐级降级写入 ----
|
|
86
271
|
function readHistory() {
|
|
87
272
|
const entries = []
|
|
@@ -151,20 +336,210 @@ function gitCommit(cwd, message) {
|
|
|
151
336
|
|
|
152
337
|
async function main() {
|
|
153
338
|
const opts = parseArgs(process.argv.slice(2))
|
|
339
|
+
const origin = resolveOrigin(opts)
|
|
340
|
+
|
|
341
|
+
if (opts.originError !== undefined) {
|
|
342
|
+
console.error('task.mjs: ' + opts.originError)
|
|
343
|
+
finish(2)
|
|
344
|
+
}
|
|
345
|
+
// 模型/provider 缺值:在**任何网络请求之前**退出(绝不让 --model 末尾静默变 auto)。
|
|
346
|
+
if (opts.valueError !== undefined) {
|
|
347
|
+
console.error('task.mjs: ' + opts.valueError)
|
|
348
|
+
finish(2)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---- 控制凭据离线初始化/轮换(不发任何网络请求;写仓库/工作区之外的私有目录;raw key 不回显)----
|
|
352
|
+
if (opts.keyInit || opts.keyRotate) {
|
|
353
|
+
try {
|
|
354
|
+
const created = initControlKey({ force: opts.keyRotate === true })
|
|
355
|
+
console.log(JSON.stringify({
|
|
356
|
+
ok: true,
|
|
357
|
+
path: created.path,
|
|
358
|
+
keyFp: created.fp8,
|
|
359
|
+
rotated: created.rotated,
|
|
360
|
+
note: 'raw key 不回显。host/MCP/CLI 都只按此路径内部读取,HTTP 用 X-Dsh-Control 传**私有 raw key** 做认证;服务端只存域分离单向摘要(不能用于登录)。'
|
|
361
|
+
+ (created.rotated ? ' 轮换会改变指纹,全部旧 grant 立即失效(等价撤销)。' : ' 文件已存在时不会覆盖(用 --approval-key-rotate 显式轮换)。'),
|
|
362
|
+
}, null, 2))
|
|
363
|
+
finish(0)
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.error('task.mjs: 控制凭据初始化失败: ' + e.message)
|
|
366
|
+
finish(1)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---- 审批查询/决定/撤销/续期(全部要求控制凭据 + 显式 --origin/--task 定位;退出码 9=等待审批)----
|
|
371
|
+
if (opts.approvals || opts.approvalShow !== '' || opts.decision !== '' || opts.revokeGrant !== '' || opts.renewGrant !== '') {
|
|
372
|
+
const headers = controlHeaderValue()
|
|
373
|
+
if (headers === null) {
|
|
374
|
+
console.error('task.mjs: 本机没有有效控制凭据,审批能力默认关闭。先运行: node task.mjs --approval-key-init')
|
|
375
|
+
finish(2)
|
|
376
|
+
}
|
|
377
|
+
// origin/task 只用于定位;服务端会与 pending/grant 硬核对。缺一即拒绝(避免跨来源泄露/误续租)。
|
|
378
|
+
const needTaskCtx = () => {
|
|
379
|
+
if (origin.originThreadId === undefined || opts.taskId === '') {
|
|
380
|
+
console.error('task.mjs: 审批操作必须显式给 --origin <对话id> 与 --task <taskId>(定位用;服务端会硬核对,不能只靠 grantId)。')
|
|
381
|
+
finish(2)
|
|
382
|
+
}
|
|
383
|
+
return { originThreadId: origin.originThreadId, taskId: opts.taskId }
|
|
384
|
+
}
|
|
385
|
+
if (opts.revokeGrant !== '') {
|
|
386
|
+
const ctx = needTaskCtx()
|
|
387
|
+
const res = await request('POST', GATEWAY + '/api/dsh-bridge/approvals/grants/' + encodeURIComponent(opts.revokeGrant) + '/revoke', { ...ctx, grantId: opts.revokeGrant, reason: opts.reason !== '' ? opts.reason : 'revoked via CLI' }, headers)
|
|
388
|
+
console.log(JSON.stringify(res.json, null, 2))
|
|
389
|
+
finish(res.status === 200 && res.json !== null && res.json.ok === true ? 0 : 1)
|
|
390
|
+
}
|
|
391
|
+
if (opts.renewGrant !== '') {
|
|
392
|
+
const ctx = needTaskCtx()
|
|
393
|
+
const res = await request('POST', GATEWAY + '/api/dsh-bridge/approvals/grants/' + encodeURIComponent(opts.renewGrant) + '/renew', { ...ctx, grantId: opts.renewGrant, ...(opts.extendSec > 0 ? { extendSec: Math.min(opts.extendSec, 3600) } : {}) }, headers)
|
|
394
|
+
console.log(JSON.stringify(res.json, null, 2))
|
|
395
|
+
finish(res.status === 200 && res.json !== null && res.json.ok === true ? 0 : 1)
|
|
396
|
+
}
|
|
397
|
+
if (opts.decision !== '') {
|
|
398
|
+
const ctx = needTaskCtx()
|
|
399
|
+
if (opts.approvalKey === '' || opts.grantId === '' || opts.fingerprint === '') {
|
|
400
|
+
console.error('task.mjs: 决定需要 --grant <grantId> 和 --fingerprint <fp>(都来自 --approvals / --approval-show 的输出;参数副本不被接受,服务端按不可变快照核对指纹)')
|
|
401
|
+
finish(2)
|
|
402
|
+
}
|
|
403
|
+
const res = await request('POST', GATEWAY + '/api/dsh-bridge/approvals/' + encodeURIComponent(opts.approvalKey) + '/decide', {
|
|
404
|
+
...ctx,
|
|
405
|
+
grantId: opts.grantId,
|
|
406
|
+
fingerprint: opts.fingerprint,
|
|
407
|
+
decision: opts.decision,
|
|
408
|
+
...(opts.idem !== '' ? { idempotencyKey: opts.idem } : {}),
|
|
409
|
+
}, headers)
|
|
410
|
+
const body = res.json
|
|
411
|
+
console.log(JSON.stringify(body, null, 2))
|
|
412
|
+
if (body !== null && typeof body === 'object' && body.ok === true) {
|
|
413
|
+
console.error(opts.decision === 'reject'
|
|
414
|
+
? '已提交 reject(phase=' + body.phase + ',state=' + body.state + ')。官方终态是 rejected:再查一次看到 state=rejected 即收敛,不必等只对批准有意义的 confirmed。'
|
|
415
|
+
: '已提交(phase=' + body.phase + ',state=' + body.state + ')。submitted ≠ confirmed:再查一次看 state=confirmed(官方 approval/decided=allowed-once 已观测);decision-unknown 表示确认丢失,不要重发放行。')
|
|
416
|
+
finish(0)
|
|
417
|
+
}
|
|
418
|
+
console.error('task.mjs: 决定被拒绝(reason=' + ((body && body.reason) || ('HTTP ' + res.status)) + ')——零放行。')
|
|
419
|
+
finish(1)
|
|
420
|
+
}
|
|
421
|
+
// 清单 / 详情:认证 + 顺带续租;--wait-approval 提供 ≤30s 的有界等待;--renew 显式续期
|
|
422
|
+
const ctx = needTaskCtx()
|
|
423
|
+
const qs = ['originThreadId=' + encodeURIComponent(ctx.originThreadId), 'taskId=' + encodeURIComponent(ctx.taskId)]
|
|
424
|
+
if (opts.grantId !== '') qs.push('grantId=' + encodeURIComponent(opts.grantId))
|
|
425
|
+
if (opts.renewNow) {
|
|
426
|
+
qs.push('renew=1')
|
|
427
|
+
if (opts.extendSec > 0) qs.push('extendSec=' + encodeURIComponent(String(Math.min(opts.extendSec, 3600))))
|
|
428
|
+
}
|
|
429
|
+
const deadline = Date.now() + (opts.approvalWait > 0 ? opts.approvalWait * 1000 : 0)
|
|
430
|
+
let payload
|
|
431
|
+
for (;;) {
|
|
432
|
+
const url = opts.approvalShow !== ''
|
|
433
|
+
? GATEWAY + '/api/dsh-bridge/approvals/' + encodeURIComponent(opts.approvalShow) + '?' + qs.join('&')
|
|
434
|
+
: GATEWAY + '/api/dsh-bridge/approvals?' + qs.join('&')
|
|
435
|
+
const res = await request('GET', url, undefined, headers)
|
|
436
|
+
payload = res.json
|
|
437
|
+
if (payload !== null && typeof payload === 'object' && payload.ok === true) {
|
|
438
|
+
if (opts.approvalShow !== '' || (Array.isArray(payload.approvals) && payload.approvals.length > 0)) break
|
|
439
|
+
}
|
|
440
|
+
if (Date.now() >= deadline || (payload !== null && typeof payload === 'object' && payload.ok === false)) break
|
|
441
|
+
await sleep(500)
|
|
442
|
+
}
|
|
443
|
+
console.log(JSON.stringify(payload, null, 2))
|
|
444
|
+
const pendingCount = payload !== null && typeof payload === 'object' && Array.isArray(payload.approvals)
|
|
445
|
+
? payload.approvals.filter((p) => p.state === 'pending').length : 0
|
|
446
|
+
finish(payload !== null && typeof payload === 'object' && payload.ok === true ? (pendingCount > 0 ? 9 : 0) : 1)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// --my-tasks:只给来源时列出该对话登记过的任务,用来找回丢失的 taskId。
|
|
450
|
+
// 没有任何来源(既没显式给,也没有本次调用的 CODEX_THREAD_ID)→ 如实拒绝,绝不猜"最近对话"。
|
|
451
|
+
if (opts.myTasks) {
|
|
452
|
+
if (origin.originThreadId === undefined) {
|
|
453
|
+
console.error('task.mjs: --my-tasks 需要来源对话 id:请显式传 --origin <Codex对话id>,或确保本次调用的环境里有 CODEX_THREAD_ID')
|
|
454
|
+
finish(2)
|
|
455
|
+
}
|
|
456
|
+
if (!(await requireOriginSupport(origin))) finish(2)
|
|
457
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/status?originThreadId=' + encodeURIComponent(origin.originThreadId))
|
|
458
|
+
if (res.json !== null && typeof res.json === 'object' && !originConfirmed(res.json, origin)) unconfirmedExit(origin, {})
|
|
459
|
+
console.log(JSON.stringify(res.json, null, 2))
|
|
460
|
+
finish(res.status === 200 ? 0 : 1)
|
|
461
|
+
}
|
|
154
462
|
|
|
155
463
|
if (opts.list) {
|
|
156
|
-
|
|
464
|
+
if (!(await requireOriginSupport(origin))) finish(2)
|
|
465
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/tasks' + (origin.originThreadId === undefined ? '' : '?originThreadId=' + encodeURIComponent(origin.originThreadId)))
|
|
466
|
+
if (origin.originThreadId !== undefined && res.json !== null && typeof res.json === 'object' && !originConfirmed(res.json, origin)) unconfirmedExit(origin, {})
|
|
157
467
|
console.log(JSON.stringify(res.json, null, 2))
|
|
158
468
|
finish(0)
|
|
159
469
|
}
|
|
160
470
|
if (opts.cancel) {
|
|
161
|
-
|
|
471
|
+
if (!(await requireOriginSupport(origin))) finish(2)
|
|
472
|
+
const res = await request('POST', GATEWAY + '/api/dsh-bridge/cancel', { taskId: opts.cancel, force: opts.force === true, ...(origin.originThreadId === undefined ? {} : { originThreadId: origin.originThreadId }) })
|
|
473
|
+
if (origin.originThreadId !== undefined && res.json !== null && typeof res.json === 'object' && res.json.cancelled === true && !originConfirmed(res.json, origin)) unconfirmedExit(origin, { taskId: opts.cancel })
|
|
162
474
|
console.log(JSON.stringify(res.json, null, 2))
|
|
163
475
|
finish(res.status === 409 ? 5 : 0)
|
|
164
476
|
}
|
|
165
477
|
if (opts.status) {
|
|
166
|
-
|
|
478
|
+
if (!(await requireOriginSupport(origin))) finish(2)
|
|
479
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(opts.status) + (origin.originThreadId === undefined ? '' : '&originThreadId=' + encodeURIComponent(origin.originThreadId)))
|
|
480
|
+
const body = res.json
|
|
481
|
+
const refused = body !== null && typeof body === 'object' && typeof body.reason === 'string' && REFUSAL_REASONS.has(body.reason)
|
|
482
|
+
// 精确取值必须由网关**确认来源**;没确认就不算读到(也绝不把网关 body 里的结果打出来)。
|
|
483
|
+
if (origin.originThreadId !== undefined && !refused && !originConfirmed(body, origin)) {
|
|
484
|
+
unconfirmedExit(origin, { taskId: opts.status })
|
|
485
|
+
}
|
|
486
|
+
console.log(JSON.stringify(body, null, 2))
|
|
487
|
+
// 非 200 = 没读到任务(未知 taskId / 已关联但缺来源 / 来源不匹配)→ 明确非零。
|
|
488
|
+
// 读到任务(含旧的无来源未关联任务)仍是 0,兼容不变。
|
|
489
|
+
if (res.status !== 200) finish(6)
|
|
490
|
+
// --status-wait:审批决定之后用它继续等终态(needs-approval 及时返回 9,不阻塞等 done)
|
|
491
|
+
// 与初次 --in 默认等待**共用同一条有界等待路径**(第四轮修正:来源回显 + 自适应续租只写一次)。
|
|
492
|
+
if (opts.statusWait === true && body !== null && typeof body === 'object'
|
|
493
|
+
&& (body.status === 'running' || body.status === 'accepted' || body.status === 'needs-approval')) {
|
|
494
|
+
const waited = await awaitTerminal({
|
|
495
|
+
taskId: opts.status,
|
|
496
|
+
origin,
|
|
497
|
+
grantId: typeof body.grantId === 'string' ? body.grantId : opts.grantId,
|
|
498
|
+
grantLeaseUntil: body.grantLeaseUntil,
|
|
499
|
+
timeoutSec: opts.timeoutSec,
|
|
500
|
+
waitHeaders: controlHeaderValue(),
|
|
501
|
+
})
|
|
502
|
+
if (waited.reason === 'timeout') {
|
|
503
|
+
console.error('等待超时(' + opts.timeoutSec + 's),任务仍在后台。可用 --status <taskId> 继续查询。')
|
|
504
|
+
finish(3)
|
|
505
|
+
}
|
|
506
|
+
const st = waited.status
|
|
507
|
+
if (waited.reason === 'needs-approval') {
|
|
508
|
+
console.log(JSON.stringify({ phase: 'needs-approval', ...st }))
|
|
509
|
+
console.error('任务仍在等一次单次审批。用 --approvals --origin <id> --task <taskId> 查看并 --approve/--reject 决定后,再来 --status-wait 收结果;不要重发任务。')
|
|
510
|
+
finish(9)
|
|
511
|
+
}
|
|
512
|
+
// 回执(第四轮首次验收修正 C):只报**回执自身**的终态,绝不等父任务结果、绝不返回 waiting。
|
|
513
|
+
if (waited.reason === 'receipt-terminal' || waited.reason === 'receipt-pending') receiptOutcome(st, waited.reason, origin)
|
|
514
|
+
console.log(JSON.stringify({ phase: 'finished', ...st }))
|
|
515
|
+
if (st.status === 'done') {
|
|
516
|
+
console.log('=== DeepSeek 汇报 ===')
|
|
517
|
+
console.log(st.result || '(无内容)')
|
|
518
|
+
}
|
|
519
|
+
finish(st.status === 'done' ? 0 : 1)
|
|
520
|
+
}
|
|
521
|
+
finish(0)
|
|
522
|
+
}
|
|
523
|
+
// 只读模型目录(M2):不派活、不投递;旧网关没有 capabilities.modelRouting → 明确拒绝(零副作用)。
|
|
524
|
+
if (opts.models) {
|
|
525
|
+
let cap
|
|
526
|
+
try { cap = await request('GET', GATEWAY + '/api/dsh-bridge/status') } catch (e) { cap = { json: null } }
|
|
527
|
+
const routing = cap.json !== null && typeof cap.json === 'object' && cap.json.capabilities !== null && typeof cap.json.capabilities === 'object' ? cap.json.capabilities.modelRouting : undefined
|
|
528
|
+
if (routing === undefined || routing === null || routing.supported !== true) {
|
|
529
|
+
console.error('task.mjs: 网关未声明 capabilities.modelRouting(旧网关/插件未装载)——模型目录查询拒绝,未做任何派活。')
|
|
530
|
+
finish(2)
|
|
531
|
+
}
|
|
532
|
+
const res = await request('GET', GATEWAY + '/api/dsh-bridge/models')
|
|
533
|
+
if (res.json === null || typeof res.json !== 'object') {
|
|
534
|
+
console.error('task.mjs: 网关返回 ' + res.status + ',模型目录响应不是 JSON(插件可能是旧版本)')
|
|
535
|
+
console.error(String(res.text || '').slice(0, 200))
|
|
536
|
+
finish(1)
|
|
537
|
+
}
|
|
167
538
|
console.log(JSON.stringify(res.json, null, 2))
|
|
539
|
+
if (res.json.ok === false) {
|
|
540
|
+
console.error('task.mjs: 目录查询未能完成(见上方 failures)。')
|
|
541
|
+
finish(1)
|
|
542
|
+
}
|
|
168
543
|
finish(0)
|
|
169
544
|
}
|
|
170
545
|
if (opts.sessions) {
|
|
@@ -215,12 +590,21 @@ async function main() {
|
|
|
215
590
|
}
|
|
216
591
|
|
|
217
592
|
if (!instruction) {
|
|
218
|
-
console.error('用法: node task.mjs --in "<指令>" [--cwd 目录] [--lane 名] [--model fast|pro] [--commit] [--timeout 秒] [--no-wait]')
|
|
593
|
+
console.error('用法: node task.mjs --in "<指令>" [--cwd 目录] [--lane 名] [--model fast|pro] [--commit] [--timeout 秒] [--no-wait] [--origin <Codex对话id>]')
|
|
219
594
|
console.error(' node task.mjs --in "<指令>" (--session <会话id> | --find "<标题关键词>") [--steer] # 投递进已有会话')
|
|
220
595
|
console.error('查找: node task.mjs --sessions "<标题关键词>" # 只读列出候选, 拿 sessionId 再派活')
|
|
221
596
|
console.error(' node task.mjs --dry-run (--find "<关键词>" | --session <id>) # 只解析目标, 不投递(验证命中用这个)')
|
|
222
|
-
console.error('管理: node task.mjs --list | --status <taskId> | --cancel <taskId> [--force]')
|
|
223
|
-
console.error('
|
|
597
|
+
console.error('管理: node task.mjs --list [--origin <id>] | --status <taskId> [--origin <id>] | --cancel <taskId> [--force] [--origin <id>]')
|
|
598
|
+
console.error('来源: node task.mjs --my-tasks [--origin <id>] # 列出该 Codex 对话登记过的任务(找回丢失的 taskId)')
|
|
599
|
+
console.error(' 来源 id 优先取 --origin,其次本次调用的环境变量 CODEX_THREAD_ID;--require-origin 表示没有来源就拒绝派活。')
|
|
600
|
+
console.error('审批: node task.mjs --approval-key-init | --approval-key-rotate # 离线初始化/轮换控制凭据(仓库外私有目录,raw key 不回显)')
|
|
601
|
+
console.error(' node task.mjs --in "…" --session <id> --origin <id> --delegate [--delegate-ttl 秒] [--delegate-lease 秒] [--authorization "范围说明"]')
|
|
602
|
+
console.error(' node task.mjs --approvals --origin <id> --task <taskId> [--grant <grantId>] [--wait-approval 秒] [--renew [--extend 秒]] # 退出码 9=pending')
|
|
603
|
+
console.error(' node task.mjs --approval-show <approvalKey> --origin <id> --task <taskId> # 单条详情(不可变快照/指纹/期限)')
|
|
604
|
+
console.error(' node task.mjs --approve <key> --origin <id> --task <taskId> --grant <grantId> --fingerprint <fp> # 单次放行(--reject 同形)')
|
|
605
|
+
console.error(' node task.mjs --revoke-grant <grantId> --origin <id> --task <taskId> | --renew-grant <grantId> --origin <id> --task <taskId> [--extend 秒]')
|
|
606
|
+
console.error(' node task.mjs --status <taskId> --origin <id> [--grant <grantId>] --status-wait # 审批后继续等终态(等待期间认证续租)')
|
|
607
|
+
console.error('退出码: 0 成功 / 1 失败 / 2 用法(含缺少来源/缺控制凭据/预检拒绝) / 3 超时 / 4 会话未命中或不唯一 / 5 session 目标取消被拒 / 6 查询不到该任务(未知或来源不匹配) / 9 等待审批中(非终态)')
|
|
224
608
|
finish(2)
|
|
225
609
|
}
|
|
226
610
|
if (sessionTarget !== null && (cwd !== '' || opts.commit)) {
|
|
@@ -234,27 +618,128 @@ async function main() {
|
|
|
234
618
|
}
|
|
235
619
|
}
|
|
236
620
|
const history = sessionTarget === null ? readHistory() : []
|
|
621
|
+
// --require-origin:要求这次派活必须带上来源关联;拿不到来源就如实拒绝(不静默降级成无关联)。
|
|
622
|
+
if (opts.requireOrigin === true && origin.originThreadId === undefined) {
|
|
623
|
+
console.error('task.mjs: --require-origin 已指定,但拿不到来源对话 id:请显式传 --origin <Codex对话id>,或确保本次调用的环境里有 CODEX_THREAD_ID。')
|
|
624
|
+
console.error('(不会降级成"无关联派活"——那会让结果无法按来源精确收回。)')
|
|
625
|
+
finish(2)
|
|
626
|
+
}
|
|
627
|
+
// 要带来源就必须先确认网关真的支持(只读 GET,不派活);不支持 → 零执行拒绝。
|
|
628
|
+
if (!(await requireOriginSupport(origin))) finish(2)
|
|
629
|
+
// 委托派活**客户端预检**(新客户端 vs 旧网关 / 缺 key / 不支持目标 / 缺来源)→ 任何一项不过零执行。
|
|
630
|
+
// 预检通过后,派活 POST 必须带**控制凭据头**(第四轮修正:之前漏传 request 第四参数 → 401 approval-control-invalid)。
|
|
631
|
+
let dispatchHeaders = null
|
|
632
|
+
if (opts.delegate === true || opts.steer === true || opts.parent !== '') dispatchHeaders = controlHeaderValue()
|
|
633
|
+
if (opts.delegate === true) {
|
|
634
|
+
if (origin.originThreadId === undefined) {
|
|
635
|
+
console.error('task.mjs: --delegate 需要来源对话 id(grant 必须绑定 task+origin;请显式 --origin <id>)。未发出任何指令。')
|
|
636
|
+
finish(2)
|
|
637
|
+
}
|
|
638
|
+
if (sessionTarget === null) {
|
|
639
|
+
console.error('task.mjs: --delegate 只支持会话目标(worker 子代理身份未证实,委托不支持)。worker 请去掉 --delegate 用旧路径。未发出任何指令。')
|
|
640
|
+
finish(2)
|
|
641
|
+
}
|
|
642
|
+
if (dispatchHeaders === null) {
|
|
643
|
+
console.error('task.mjs: 本机没有控制凭据,审批委托默认关闭。先运行: node task.mjs --approval-key-init。未发出任何指令。')
|
|
644
|
+
finish(2)
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
const cap = await request('GET', GATEWAY + '/api/dsh-bridge/status')
|
|
648
|
+
const approvals = cap.json !== null && typeof cap.json === 'object' && cap.json.capabilities !== null && typeof cap.json.capabilities === 'object' ? cap.json.capabilities.approvals : undefined
|
|
649
|
+
if (approvals === undefined || approvals === null || approvals.supported !== true) {
|
|
650
|
+
console.error('task.mjs: 网关未声明 capabilities.approvals(旧网关/插件未装载)——委托派活预检拒绝,未发出任何指令。')
|
|
651
|
+
finish(2)
|
|
652
|
+
}
|
|
653
|
+
if (approvals.enabled !== true) {
|
|
654
|
+
console.error('task.mjs: 网关报告审批能力未启用(host 侧没有控制 key)——未发出任何指令。')
|
|
655
|
+
finish(2)
|
|
656
|
+
}
|
|
657
|
+
} catch (e) {
|
|
658
|
+
console.error('task.mjs: 审批能力预检失败(' + (e && e.message ? e.message : e) + ')——未发出任何指令。')
|
|
659
|
+
finish(2)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// 投递控制(第四轮):steer 会改动既有任务 → 必须显式父任务 + 认证头;旧网关无 delivery 能力则零派活。
|
|
663
|
+
const isSteer = opts.steer === true || opts.parent !== ''
|
|
664
|
+
if (isSteer) {
|
|
665
|
+
if (opts.parent === '') {
|
|
666
|
+
console.error('task.mjs: --steer 必须显式给 --parent <taskId>(被补充的父任务;绝不按"最近/最老任务"推断)。未发出任何指令。')
|
|
667
|
+
finish(2)
|
|
668
|
+
}
|
|
669
|
+
if (origin.originThreadId === undefined) {
|
|
670
|
+
console.error('task.mjs: --steer 需要来源对话 id(请显式 --origin <id>),否则会改动可能是别人的在途任务。未发出任何指令。')
|
|
671
|
+
finish(2)
|
|
672
|
+
}
|
|
673
|
+
if (dispatchHeaders === null) {
|
|
674
|
+
console.error('task.mjs: --steer 需要本机控制凭据(它改动既有任务)。先运行: node task.mjs --approval-key-init。未发出任何指令。')
|
|
675
|
+
finish(2)
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const cap = await request('GET', GATEWAY + '/api/dsh-bridge/status')
|
|
679
|
+
const delivery = cap.json !== null && typeof cap.json === 'object' && cap.json.capabilities !== null && typeof cap.json.capabilities === 'object' ? cap.json.capabilities.delivery : undefined
|
|
680
|
+
if (delivery === undefined || delivery === null || delivery.supported !== true) {
|
|
681
|
+
console.error('task.mjs: 网关未声明 capabilities.delivery(旧网关/插件未装载)——steer 预检拒绝,未发出任何指令。')
|
|
682
|
+
finish(2)
|
|
683
|
+
}
|
|
684
|
+
} catch (e) {
|
|
685
|
+
console.error('task.mjs: 投递能力预检失败(' + (e && e.message ? e.message : e) + ')——未发出任何指令。')
|
|
686
|
+
finish(2)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// 模型路由(M2):新语义(--provider/--model-fallback/auto|default|精确 id)先只读预检能力,旧网关零派活。
|
|
690
|
+
const usesModelRouting = opts.provider !== '' || opts.modelFallback !== undefined
|
|
691
|
+
|| (opts.model !== '' && opts.model !== 'fast' && opts.model !== 'pro')
|
|
692
|
+
if (usesModelRouting) {
|
|
693
|
+
let cap
|
|
694
|
+
try { cap = await request('GET', GATEWAY + '/api/dsh-bridge/status') } catch (e) { cap = { json: null } }
|
|
695
|
+
const routing = cap.json !== null && typeof cap.json === 'object' && cap.json.capabilities !== null && typeof cap.json.capabilities === 'object' ? cap.json.capabilities.modelRouting : undefined
|
|
696
|
+
if (routing === undefined || routing === null || routing.supported !== true) {
|
|
697
|
+
console.error('task.mjs: 网关未声明 capabilities.modelRouting(旧网关/插件未装载)——模型路由参数预检拒绝,未发出任何指令。')
|
|
698
|
+
finish(2)
|
|
699
|
+
}
|
|
700
|
+
}
|
|
237
701
|
const posted = await request('POST', GATEWAY + '/api/dsh-bridge/task', {
|
|
238
702
|
instruction,
|
|
703
|
+
...(origin.originThreadId === undefined ? {} : { originThreadId: origin.originThreadId }),
|
|
704
|
+
// 模型控制字段是 **worker 与 session 共用** 的任务级策略:provider+model 是一对身份,
|
|
705
|
+
// 绝不能只在 worker 分支发送(否则 --session S --provider P --model M 会丢掉精确 pair 退化成 auto)。
|
|
706
|
+
...(opts.provider !== '' ? { provider: opts.provider } : {}),
|
|
707
|
+
...(opts.model !== '' ? { model: opts.model } : {}),
|
|
708
|
+
...(opts.modelFallback === undefined ? {} : { modelFallback: opts.modelFallback }),
|
|
709
|
+
...(isSteer ? { parentTaskId: opts.parent, ...(opts.expectedTurn > 0 ? { expectedTurn: opts.expectedTurn } : {}) } : {}),
|
|
710
|
+
...(opts.delegate === true
|
|
711
|
+
? {
|
|
712
|
+
delegate: true,
|
|
713
|
+
...(opts.delegateTtl > 0 ? { delegateTtlSec: opts.delegateTtl } : {}),
|
|
714
|
+
...(opts.delegateLease > 0 ? { delegateLeaseSec: opts.delegateLease } : {}),
|
|
715
|
+
...(opts.authorization !== '' ? { authorization: opts.authorization } : {}),
|
|
716
|
+
}
|
|
717
|
+
: {}),
|
|
239
718
|
...(sessionTarget === null
|
|
240
719
|
? {
|
|
241
720
|
...{ history },
|
|
242
721
|
...(cwd ? { cwd } : {}),
|
|
243
722
|
...(opts.lane ? { lane: opts.lane } : {}),
|
|
244
|
-
...(opts.model ? { model: opts.model } : {}),
|
|
245
723
|
}
|
|
246
724
|
: {
|
|
247
725
|
...sessionTarget,
|
|
248
|
-
...(
|
|
726
|
+
...(isSteer ? { deliver: 'steer' } : {}),
|
|
249
727
|
}),
|
|
250
|
-
})
|
|
728
|
+
}, dispatchHeaders)
|
|
251
729
|
const task = posted.json
|
|
252
730
|
if (task === null || typeof task !== 'object') {
|
|
253
731
|
console.error('task.mjs: 网关返回 ' + posted.status + ',响应不是 JSON —— profile 里的插件可能还是旧版本(session 目标需要 0.1.2+)')
|
|
254
732
|
console.error(String(posted.text || '').slice(0, 200))
|
|
255
733
|
finish(1)
|
|
256
734
|
}
|
|
257
|
-
|
|
735
|
+
if (origin.originThreadId !== undefined && task.taskId !== undefined && !originConfirmed(task, origin)) {
|
|
736
|
+
// 网关接受了请求,却没有回显我们的来源:绑定**未被确认**(可能已经无关联地执行了)→ 不要重发。
|
|
737
|
+
unconfirmedExit(origin, { taskId: task.taskId })
|
|
738
|
+
}
|
|
739
|
+
if (origin.originThreadId !== undefined) {
|
|
740
|
+
console.error('task.mjs: 来源关联 originThreadId=' + origin.originThreadId + '(' + (origin.source === 'explicit' ? '--origin' : 'CODEX_THREAD_ID') + ')')
|
|
741
|
+
}
|
|
742
|
+
console.log(JSON.stringify({ phase: task.taskId === undefined ? 'rejected' : 'submitted', ...task }))
|
|
258
743
|
if (task.reason === 'session-ambiguous' || task.reason === 'session-query-empty') {
|
|
259
744
|
console.error('task.mjs: ' + (task.error || task.reason))
|
|
260
745
|
if (Array.isArray(task.candidates) && task.candidates.length > 0) {
|
|
@@ -263,42 +748,84 @@ async function main() {
|
|
|
263
748
|
}
|
|
264
749
|
finish(4)
|
|
265
750
|
}
|
|
751
|
+
if (task.taskId === undefined) {
|
|
752
|
+
// 网关明确拒绝了这次派活(例如同一通道还有在途任务、来源非法)——非零退出,别让调用方以为派出去了。
|
|
753
|
+
console.error('task.mjs: 派活被拒绝: ' + (task.error || 'unknown error') + (task.reason === undefined ? '' : '(reason=' + task.reason + ')'))
|
|
754
|
+
finish(task.reason === 'worker-bucket-busy' || task.reason === 'reviewer-bucket-busy' ? 7 : 1)
|
|
755
|
+
}
|
|
266
756
|
if (posted.status !== 202 || task.status === 'error') {
|
|
267
757
|
console.error('task.mjs: ' + (task.error || task.reason || 'submission failed'))
|
|
268
758
|
finish(1)
|
|
269
759
|
}
|
|
270
|
-
if (!opts.wait)
|
|
760
|
+
if (!opts.wait) {
|
|
761
|
+
if (isSteer) {
|
|
762
|
+
console.log(JSON.stringify({ phase: 'receipt-accepted', ...task }))
|
|
763
|
+
console.error('补充回执已接收(resultScope=parent-task)。它不代表父任务结果;父任务最终结果请查: node task.mjs --status ' + (task.parentTaskId || opts.parent) + (origin.originThreadId === undefined ? '' : ' --origin ' + origin.originThreadId) + ' --status-wait')
|
|
764
|
+
}
|
|
765
|
+
finish(0)
|
|
766
|
+
}
|
|
767
|
+
// steer 回执:**不等父任务结果**(父任务唯一拥有最终结果)。只报告自身接收/观察状态。
|
|
768
|
+
if (isSteer) {
|
|
769
|
+
console.log(JSON.stringify({ phase: task.receiptState === 'observed' ? 'receipt-observed' : (task.receiptState === 'unconfirmed' ? 'receipt-unconfirmed' : 'receipt-accepted'), ...task }))
|
|
770
|
+
console.error('这是**补充回执**(resultScope=parent-task,receiptState=' + (task.receiptState || 'accepted') + (task.observedTurn === undefined ? '' : ',observedTurn=' + task.observedTurn) + '):')
|
|
771
|
+
console.error(' * 它不证明父任务已完成,也不是独立答案;')
|
|
772
|
+
console.error(' * 父任务最终结果: node task.mjs --status ' + (task.parentTaskId || opts.parent) + (origin.originThreadId === undefined ? '' : ' --origin ' + origin.originThreadId) + ' --status-wait')
|
|
773
|
+
console.error(' * 若为 receipt-unconfirmed:官方已接收但没在窗口内观察到落进父回合——**不要重发**。')
|
|
774
|
+
finish(0)
|
|
775
|
+
}
|
|
271
776
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
777
|
+
// 初次等待与 --status-wait 共用同一条有界等待路径(第四轮修正):
|
|
778
|
+
// 带 origin 的 status + 逐次回显核对 + 按响应 grant 字段自适应认证续租 + needs-approval 及时退出 9。
|
|
779
|
+
const waited = await awaitTerminal({
|
|
780
|
+
taskId: task.taskId,
|
|
781
|
+
origin,
|
|
782
|
+
grantId: task.grantId,
|
|
783
|
+
grantLeaseUntil: task.grantLeaseUntil,
|
|
784
|
+
timeoutSec: opts.timeoutSec,
|
|
785
|
+
waitHeaders: controlHeaderValue(),
|
|
786
|
+
})
|
|
787
|
+
if (waited.reason === 'timeout') {
|
|
788
|
+
console.error('等待超时(' + opts.timeoutSec + 's),任务仍在后台执行。可用 status 接口继续查询:')
|
|
789
|
+
console.error(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId) + (origin.originThreadId === undefined ? '' : '&originThreadId=' + encodeURIComponent(origin.originThreadId)))
|
|
790
|
+
finish(3)
|
|
791
|
+
}
|
|
792
|
+
if (waited.reason === 'receipt-terminal' || waited.reason === 'receipt-pending') receiptOutcome(waited.status, waited.reason, origin)
|
|
793
|
+
const status = waited.status
|
|
794
|
+
const taskCtx = origin.originThreadId === undefined ? '' : ' --origin ' + origin.originThreadId + ' --task ' + task.taskId
|
|
795
|
+
if (waited.reason === 'needs-approval') {
|
|
796
|
+
// needs-approval:非终态、不是失败——**及时返回**让调用方决策(退出码 9),绝不阻塞等 done。
|
|
797
|
+
console.log(JSON.stringify({ phase: 'needs-approval', ...status }))
|
|
798
|
+
console.error('任务正等待一次单次审批(非终态,不是失败)。下一步:')
|
|
799
|
+
console.error(' node task.mjs --approvals' + taskCtx + (typeof task.grantId === 'string' ? ' --grant ' + task.grantId : '') + ' # 取真实待审动作(含参数/指纹/期限)')
|
|
800
|
+
console.error(' node task.mjs --approve <approvalKey>' + taskCtx + ' --grant <grantId> --fingerprint <fp> # 单次放行')
|
|
801
|
+
console.error(' node task.mjs --reject <approvalKey>' + taskCtx + ' --grant <grantId> --fingerprint <fp>')
|
|
802
|
+
console.error('然后: node task.mjs --status ' + task.taskId + (origin.originThreadId === undefined ? '' : ' --origin ' + origin.originThreadId) + ' --status-wait 继续收取结果。不要重发任务。')
|
|
803
|
+
finish(9)
|
|
804
|
+
}
|
|
805
|
+
{
|
|
806
|
+
console.log(JSON.stringify({ phase: 'finished', ...status }))
|
|
807
|
+
console.log('=== DeepSeek 汇报 ===')
|
|
808
|
+
console.log(status.result || status.error || '(无内容)')
|
|
809
|
+
// delivered=true 表示 prompt 已被那条会话接受(进了它的队列):即便我们等超时了,
|
|
810
|
+
// 消息也可能会跑,**不要重发**(重发会让同一份内容在用户会话里出现两次)。
|
|
811
|
+
if (status.status === 'error' && status.delivered === true) {
|
|
812
|
+
console.error('注意: 消息已投递进该会话的队列(delivered=true),只是我们没等到它开跑。不要重发;可用 --status ' + task.taskId + ' 继续观察。')
|
|
813
|
+
}
|
|
814
|
+
if (status.status === 'unknown') {
|
|
815
|
+
console.error('执行/交付状态未确认(可能仍在后台,或已不可考)。**不要重发**;可用 --status ' + task.taskId + ' 复核。')
|
|
816
|
+
}
|
|
817
|
+
if (status.status === 'done') {
|
|
818
|
+
// session 目标的记忆就在那条会话里,不再往协作历史里塞一份。
|
|
819
|
+
if (sessionTarget === null) {
|
|
820
|
+
appendHistory([
|
|
821
|
+
{ role: 'user', text: instruction },
|
|
822
|
+
{ role: 'assistant', text: status.result || '(无输出)' },
|
|
823
|
+
])
|
|
295
824
|
}
|
|
296
|
-
|
|
825
|
+
if (opts.commit && cwd) gitCommit(cwd, '[task ' + task.taskId + '] ' + instruction)
|
|
297
826
|
}
|
|
827
|
+
finish(status.status === 'done' ? 0 : 1)
|
|
298
828
|
}
|
|
299
|
-
console.error('等待超时(' + opts.timeoutSec + 's),任务仍在后台执行。可用 status 接口继续查询:')
|
|
300
|
-
console.error(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId))
|
|
301
|
-
finish(3)
|
|
302
829
|
}
|
|
303
830
|
|
|
304
831
|
// 退出收尾:先静默 stderr(抑制 Node 退出时的 libuv 断言噪音),再按码退出。
|