@whaletalk/dsh-codex-collab 0.1.9 → 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 +9 -0
- package/README.md +105 -25
- package/codex-plugin/plugins/dsh/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/plugins/dsh/skills/dsh-collab/SKILL.md +63 -3
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/review.mjs +91 -6
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/task.mjs +443 -39
- 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/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/harness/audit-tail.mjs +32 -0
- package/harness/codex-approvals.mjs +1078 -0
- package/harness/codex-associations.mjs +39 -7
- package/harness/dsh-bridge.mjs +1950 -191
- 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 +161 -6
- package/harness/task-table.mjs +6 -4
- package/mcp/dsh-mcp.mjs +474 -12
- package/package.json +2 -2
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
// --wait 等待结果(默认)/ --list 任务列表 / --cancel <taskId> / --status <taskId>
|
|
5
5
|
// --origin <Codex对话id> 来源关联(跨回合精确收取;显式参数优先,其次本次调用的 CODEX_THREAD_ID)
|
|
6
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(任务非终态、等审批)
|
|
7
12
|
// 用法: node task.mjs --in "<指令>" [--cwd 目录] [--lane 名] [--model fast|pro] [--commit] [--timeout 秒] [--origin <id>]
|
|
8
13
|
import fs from 'node:fs'
|
|
9
14
|
import http from 'node:http'
|
|
10
15
|
import path from 'node:path'
|
|
11
16
|
import { execFileSync } from 'node:child_process'
|
|
17
|
+
import { initControlKey, controlHeaderValue } from '../../../../../../harness/codex-approvals.mjs'
|
|
12
18
|
|
|
13
19
|
const GATEWAY = process.env.DSH_BRIDGE_URL || 'http://127.0.0.1:3080'
|
|
14
20
|
|
|
@@ -24,14 +30,19 @@ const HISTORY_CANDIDATES = process.env.DSH_BRIDGE_HISTORY
|
|
|
24
30
|
const HISTORY_MAX = 40
|
|
25
31
|
|
|
26
32
|
function parseArgs(argv) {
|
|
27
|
-
const out = { wait: true, timeoutSec: 600, instruction: '', cwd: '', lane: '', model: '', commit: false, list: false, cancel: '', status: '', sessions: '', sessionId: '', find: '', steer: false, force: false, origin: '', myTasks: false, requireOrigin: 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 }
|
|
28
34
|
const rest = []
|
|
29
35
|
for (let i = 0; i < argv.length; i++) {
|
|
30
36
|
const a = argv[i]
|
|
31
37
|
if (a === '--in') { out.instruction = argv[++i] || '' }
|
|
32
38
|
else if (a === '--cwd') { out.cwd = argv[++i] || '' }
|
|
33
39
|
else if (a === '--lane') { out.lane = argv[++i] || '' }
|
|
34
|
-
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
|
+
}
|
|
35
46
|
else if (a === '--commit') { out.commit = true }
|
|
36
47
|
else if (a === '--wait') { out.wait = true }
|
|
37
48
|
else if (a === '--no-wait') { out.wait = false }
|
|
@@ -39,14 +50,48 @@ function parseArgs(argv) {
|
|
|
39
50
|
else if (a === '--list') { out.list = true }
|
|
40
51
|
else if (a === '--cancel') { out.cancel = argv[++i] || '' }
|
|
41
52
|
else if (a === '--status') { out.status = argv[++i] || '' }
|
|
53
|
+
else if (a === '--status-wait') { out.statusWait = true }
|
|
42
54
|
else if (a === '--sessions') { out.sessions = argv[++i] || '' }
|
|
43
55
|
else if (a === '--session') { out.sessionId = argv[++i] || '' }
|
|
44
56
|
else if (a === '--find') { out.find = argv[++i] || '' }
|
|
45
|
-
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 }
|
|
46
60
|
else if (a === '--dry-run') { out.dryRun = true }
|
|
47
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) }
|
|
48
93
|
else if (a === '--origin') {
|
|
49
|
-
//
|
|
94
|
+
// 来源 id 必须紧跟一个非空、不以 -- 开头的值;否则按用法错误处理(不静默降级)。
|
|
50
95
|
const v = argv[++i]
|
|
51
96
|
if (v === undefined || v.trim() === '' || v.startsWith('--')) { out.originError = '--origin 需要一个值(Codex 对话 id)'; continue }
|
|
52
97
|
out.origin = v.trim()
|
|
@@ -74,12 +119,12 @@ function resolveOrigin(opts) {
|
|
|
74
119
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
75
120
|
|
|
76
121
|
// ---- 请求层:node:http + Connection: close,避免 undici keep-alive 退出断言 ----
|
|
77
|
-
function request(method, urlStr, body) {
|
|
122
|
+
function request(method, urlStr, body, extraHeaders) {
|
|
78
123
|
return new Promise((resolve, reject) => {
|
|
79
124
|
let u
|
|
80
125
|
try { u = new URL(urlStr) } catch (e) { reject(e); return }
|
|
81
126
|
const payload = body !== undefined ? JSON.stringify(body) : undefined
|
|
82
|
-
const headers = { 'Content-Type': 'application/json', Connection: 'close' }
|
|
127
|
+
const headers = { 'Content-Type': 'application/json', Connection: 'close', ...(extraHeaders || {}) }
|
|
83
128
|
if (payload !== undefined) headers['Content-Length'] = Buffer.byteLength(payload)
|
|
84
129
|
const req = http.request({
|
|
85
130
|
hostname: u.hostname,
|
|
@@ -149,6 +194,79 @@ function unconfirmedExit(origin, extra) {
|
|
|
149
194
|
finish(8)
|
|
150
195
|
}
|
|
151
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
|
+
|
|
152
270
|
// ---- 历史:多位置读取合并 + 逐级降级写入 ----
|
|
153
271
|
function readHistory() {
|
|
154
272
|
const entries = []
|
|
@@ -224,6 +342,109 @@ async function main() {
|
|
|
224
342
|
console.error('task.mjs: ' + opts.originError)
|
|
225
343
|
finish(2)
|
|
226
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
|
+
}
|
|
227
448
|
|
|
228
449
|
// --my-tasks:只给来源时列出该对话登记过的任务,用来找回丢失的 taskId。
|
|
229
450
|
// 没有任何来源(既没显式给,也没有本次调用的 CODEX_THREAD_ID)→ 如实拒绝,绝不猜"最近对话"。
|
|
@@ -265,7 +486,61 @@ async function main() {
|
|
|
265
486
|
console.log(JSON.stringify(body, null, 2))
|
|
266
487
|
// 非 200 = 没读到任务(未知 taskId / 已关联但缺来源 / 来源不匹配)→ 明确非零。
|
|
267
488
|
// 读到任务(含旧的无来源未关联任务)仍是 0,兼容不变。
|
|
268
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
543
|
+
finish(0)
|
|
269
544
|
}
|
|
270
545
|
if (opts.sessions) {
|
|
271
546
|
const res = await request('POST', GATEWAY + '/api/dsh-bridge/sessions', { query: opts.sessions, inspect: true, limit: 20 })
|
|
@@ -322,7 +597,14 @@ async function main() {
|
|
|
322
597
|
console.error('管理: node task.mjs --list [--origin <id>] | --status <taskId> [--origin <id>] | --cancel <taskId> [--force] [--origin <id>]')
|
|
323
598
|
console.error('来源: node task.mjs --my-tasks [--origin <id>] # 列出该 Codex 对话登记过的任务(找回丢失的 taskId)')
|
|
324
599
|
console.error(' 来源 id 优先取 --origin,其次本次调用的环境变量 CODEX_THREAD_ID;--require-origin 表示没有来源就拒绝派活。')
|
|
325
|
-
console.error('
|
|
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 等待审批中(非终态)')
|
|
326
608
|
finish(2)
|
|
327
609
|
}
|
|
328
610
|
if (sessionTarget !== null && (cwd !== '' || opts.commit)) {
|
|
@@ -344,21 +626,106 @@ async function main() {
|
|
|
344
626
|
}
|
|
345
627
|
// 要带来源就必须先确认网关真的支持(只读 GET,不派活);不支持 → 零执行拒绝。
|
|
346
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
|
+
}
|
|
347
701
|
const posted = await request('POST', GATEWAY + '/api/dsh-bridge/task', {
|
|
348
702
|
instruction,
|
|
349
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
|
+
: {}),
|
|
350
718
|
...(sessionTarget === null
|
|
351
719
|
? {
|
|
352
720
|
...{ history },
|
|
353
721
|
...(cwd ? { cwd } : {}),
|
|
354
722
|
...(opts.lane ? { lane: opts.lane } : {}),
|
|
355
|
-
...(opts.model ? { model: opts.model } : {}),
|
|
356
723
|
}
|
|
357
724
|
: {
|
|
358
725
|
...sessionTarget,
|
|
359
|
-
...(
|
|
726
|
+
...(isSteer ? { deliver: 'steer' } : {}),
|
|
360
727
|
}),
|
|
361
|
-
})
|
|
728
|
+
}, dispatchHeaders)
|
|
362
729
|
const task = posted.json
|
|
363
730
|
if (task === null || typeof task !== 'object') {
|
|
364
731
|
console.error('task.mjs: 网关返回 ' + posted.status + ',响应不是 JSON —— profile 里的插件可能还是旧版本(session 目标需要 0.1.2+)')
|
|
@@ -390,38 +757,75 @@ async function main() {
|
|
|
390
757
|
console.error('task.mjs: ' + (task.error || task.reason || 'submission failed'))
|
|
391
758
|
finish(1)
|
|
392
759
|
}
|
|
393
|
-
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
|
+
}
|
|
394
776
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
])
|
|
418
824
|
}
|
|
419
|
-
|
|
825
|
+
if (opts.commit && cwd) gitCommit(cwd, '[task ' + task.taskId + '] ' + instruction)
|
|
420
826
|
}
|
|
827
|
+
finish(status.status === 'done' ? 0 : 1)
|
|
421
828
|
}
|
|
422
|
-
console.error('等待超时(' + opts.timeoutSec + 's),任务仍在后台执行。可用 status 接口继续查询:')
|
|
423
|
-
console.error(GATEWAY + '/api/dsh-bridge/status?taskId=' + encodeURIComponent(task.taskId))
|
|
424
|
-
finish(3)
|
|
425
829
|
}
|
|
426
830
|
|
|
427
831
|
// 退出收尾:先静默 stderr(抑制 Node 退出时的 libuv 断言噪音),再按码退出。
|