dsh-agent-board 1.2.0 → 1.2.2
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/index.mjs +159 -39
- package/lib/client.js +134 -29
- package/lib/core.mjs +9 -3
- package/package.json +1 -1
- package/test/core.test.mjs +30 -0
package/index.mjs
CHANGED
|
@@ -62,9 +62,28 @@ export function apply(ctx) {
|
|
|
62
62
|
function touchSession(sid) { if (sid && typeof sid === 'string' && sid !== 'unknown') knownSessions[sid] = Date.now() }
|
|
63
63
|
// teamMode 缓存:由 rt() 同步,供 systemPrompt 动态引导段读取(v65)
|
|
64
64
|
var teamModeCache = {}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
// 看板文件用「家目录绝对路径 + node:fs」直读写——不再走 fs 服务的相对路径解析
|
|
66
|
+
// (fs 服务的相对路径解析根 = 进程启动 cwd,cwd 一变所有看板静默读成空板;
|
|
67
|
+
// 曾因此导致整个看板状态异常。绝对路径对此永久免疫)
|
|
68
|
+
function boardPath(sid) { return path.join(os.homedir(), '.dsh', 'tasks-' + sid + '.json') }
|
|
69
|
+
function fileFor(sid) { return boardPath(sid) } // 保留旧名兼容调用点,但已是绝对路径
|
|
70
|
+
async function rt(sid) {
|
|
71
|
+
var r
|
|
72
|
+
try { r = await fsNode.promises.readFile(boardPath(sid), 'utf8') } catch (_) { return seed(sid) } // 不存在/不可读 → 空板
|
|
73
|
+
try {
|
|
74
|
+
var d = JSON.parse(r)
|
|
75
|
+
if (vt(d) && d.ownerSession === sid) { teamModeCache[sid] = !!d.teamMode; return normalizeBoard(d) }
|
|
76
|
+
return seed(sid)
|
|
77
|
+
} catch (_) {
|
|
78
|
+
// JSON 截断/损坏(如强杀打断写盘):隔离留档再种新板——数据不丢,坏文件也不反复 poison
|
|
79
|
+
console.error('[task-board] board file corrupt, quarantining: ' + boardPath(sid))
|
|
80
|
+
fsNode.promises.rename(boardPath(sid), boardPath(sid) + '.corrupt-' + Date.now()).catch(function () {})
|
|
81
|
+
return seed(sid)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// 原子写盘:先写临时文件再 rename——强杀若发生在写盘中途,磁盘上最多留个 .tmp 残件,
|
|
85
|
+
// 看板本体永远不会是截断的半个 JSON(此前非原子直写,kill 中写 = 看板被 seed 清空)
|
|
86
|
+
async function wt(sid, d) { var c = JSON.stringify(d); var p = boardPath(sid); var tmp = p + '.tmp'; try { await fsNode.promises.writeFile(tmp, c, 'utf8'); await fsNode.promises.rename(tmp, p) } catch (e) { console.error('[task-board] write:', String(e)); throw e } }
|
|
68
87
|
// 每会话一条 promise 链,串行化所有 读-改-写,消除并发写竞争
|
|
69
88
|
var fileLocks = {}
|
|
70
89
|
function withLock(sid, fn) { var prev = fileLocks[sid] || Promise.resolve(); var p = prev.then(function () { return fn() }); fileLocks[sid] = p.catch(function () {}); return p }
|
|
@@ -80,7 +99,12 @@ export function apply(ctx) {
|
|
|
80
99
|
// makeMsg 支持插件来源标记(参考 dsh-notes 派发模式):
|
|
81
100
|
// form 'recall' = 背景回执(召回上下文,非指令);'notice' = 需注意的通知(带一行 summary)
|
|
82
101
|
// 不再用 kind:'user'——插件消息不该冒充用户在说话,模型可据 form 正确理解语义
|
|
83
|
-
|
|
102
|
+
// v0.1.7 起会话日志为 format v4:source.kind 必须是「生产者自有 kind」,kind:'plugin'
|
|
103
|
+
// 是已退役的 v3 包装写法,落盘时 persistence 直接抛 SessionFormatError
|
|
104
|
+
// (format v4 message requires a producer-owned source kind)并连带炸掉主窗口当前轮次。
|
|
105
|
+
// v3→v4 迁移把 {kind:'plugin',plugin:'dsh-agent-board'} 映射为 {kind:'plugin:dsh-agent-board'},
|
|
106
|
+
// 这里直接写迁移后的形态,与存量历史一致;form/summary 作为 source 元数据字段保留。
|
|
107
|
+
function makeMsg(text, form, summary) { var src = { kind: 'plugin:dsh-agent-board' }; if (form) { src.form = form; if (form === 'notice' && summary) src.summary = summary }; return { id: 'm' + Date.now() + Math.random().toString(36).slice(2, 6), role: 'user', content: [{ type: 'text', text: text }], source: src } }
|
|
84
108
|
// 超时保护:run 挂死时走失败重试路径
|
|
85
109
|
function withTimeout(promise, ms, label) { var timer = ctx.timer; if (!timer) return promise; return Promise.race([promise, timer.timeout(ms).then(function () { throw new Error(label + ' timeout ' + ms + 'ms') })]) }
|
|
86
110
|
|
|
@@ -110,16 +134,21 @@ export function apply(ctx) {
|
|
|
110
134
|
// 主窗口预研文件:t.context.files 里的路径由主 agent 选择性指定(它调研时读过哪些文件),
|
|
111
135
|
// host 在派发时从磁盘读最新内容注入 prompt——Worker/Verifier 不用从零重复调研。
|
|
112
136
|
// 上限:单文件 8KB、总计 40KB,超出截断并标注。
|
|
113
|
-
|
|
137
|
+
// 相对路径的解析根必须是「该会话的工作区」(root agent 的 session.header.cwd),
|
|
138
|
+
// 不能靠进程 cwd——dsh web 从家目录启动时相对路径会解析到 ~/.dsh 之外的家目录下,
|
|
139
|
+
// 全部读成「读取失败」(v0.1.7 实测:dsh-notes-plugin/... → C:\Users\<user>\dsh-notes-plugin)。
|
|
140
|
+
function sessionCwd(sid) { try { var root = rootForSession(sid); var cwd = root && root.session && root.session.header && root.session.header.cwd; return (typeof cwd === 'string' && cwd) ? cwd : '' } catch (_) { return '' } }
|
|
141
|
+
async function readContextPack(sid, t) {
|
|
114
142
|
var paths = (t.context && Array.isArray(t.context.files)) ? t.context.files : []
|
|
115
143
|
var notes = (t.context && typeof t.context.notes === 'string') ? t.context.notes : ''
|
|
116
144
|
if (!paths.length && !notes.trim()) return ''
|
|
145
|
+
var cwd = sessionCwd(sid)
|
|
117
146
|
var out = [], total = 0
|
|
118
147
|
for (var i = 0; i < paths.length && total < 40960; i++) {
|
|
119
148
|
var p = String(paths[i] || '')
|
|
120
149
|
if (!p) continue
|
|
121
150
|
try {
|
|
122
|
-
var content = await fs.readText(await fs.resolve(p))
|
|
151
|
+
var content = await fs.readText(await fs.resolve(p, cwd ? { cwd: cwd } : undefined))
|
|
123
152
|
var truncated = false
|
|
124
153
|
if (content.length > 8192) { content = content.slice(0, 8192); truncated = true }
|
|
125
154
|
if (total + content.length > 40960) { content = content.slice(0, 40960 - total); truncated = true }
|
|
@@ -139,15 +168,42 @@ export function apply(ctx) {
|
|
|
139
168
|
var packByChild = {}
|
|
140
169
|
var pendingPacks = []
|
|
141
170
|
|
|
171
|
+
// 历史会话留档:t.runs = [{ role, id, at, model, outcome, endedAt }],上限 20 条
|
|
172
|
+
// 目的:任务流转到 resolved/archived 后,详情页仍能选择跳转到任一历史阶段的会话
|
|
173
|
+
// (Worker 首次/重试、Verifier 各次),而不是只剩最后一次 run id。
|
|
174
|
+
async function recordRunHistory(sid, taskId, role, runId, model, hardMin) {
|
|
175
|
+
try {
|
|
176
|
+
await mutateLocked(sid, function (d) {
|
|
177
|
+
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
178
|
+
if (!t) return
|
|
179
|
+
if (!Array.isArray(t.runs)) t.runs = []
|
|
180
|
+
t.runs.push({ role: role, id: runId, at: new Date().toISOString(), model: model || '', outcome: 'running', hardMin: hardMin || 120 })
|
|
181
|
+
if (t.runs.length > 20) t.runs = t.runs.slice(-20)
|
|
182
|
+
})
|
|
183
|
+
} catch (e) { console.error('[task-board] recordRunHistory failed:', String(e)) }
|
|
184
|
+
}
|
|
185
|
+
async function closeRunHistory(sid, taskId, runId, outcome) {
|
|
186
|
+
try {
|
|
187
|
+
await mutateLocked(sid, function (d) {
|
|
188
|
+
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
189
|
+
if (!t || !Array.isArray(t.runs)) return
|
|
190
|
+
for (var i = t.runs.length - 1; i >= 0; i--) {
|
|
191
|
+
if (t.runs[i].id === runId) { t.runs[i].outcome = outcome; t.runs[i].endedAt = new Date().toISOString(); break }
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
} catch (e) { console.error('[task-board] closeRunHistory failed:', String(e)) }
|
|
195
|
+
}
|
|
196
|
+
|
|
142
197
|
async function spawnOneShot(sid, t, role) {
|
|
143
198
|
var subagents = ctx.subagents; if (!subagents) return null
|
|
144
199
|
var parent = rootForSession(sid); if (!parent) { console.error('[task-board] no root agent for session ' + sid + ', skip spawn'); return null }
|
|
145
200
|
var providerName = pickProvider(); if (!providerName) { console.error('[task-board] no subagent provider'); return null }
|
|
146
201
|
var modelOverride = ''
|
|
147
|
-
|
|
148
|
-
|
|
202
|
+
var dsnap = await rt(sid)
|
|
203
|
+
if (role === 'verifier') { modelOverride = (typeof dsnap.verifierModel === 'string' && dsnap.verifierModel.trim()) ? dsnap.verifierModel.trim() : ''; if (modelOverride && badModels[modelKey(sid, modelOverride)]) { console.error('[task-board] model ' + modelOverride + ' circuited, using parent model'); modelOverride = '' } }
|
|
204
|
+
else if (role === 'worker') { modelOverride = (typeof dsnap.workerModel === 'string' && dsnap.workerModel.trim()) ? dsnap.workerModel.trim() : ''; if (modelOverride && badModels[modelKey(sid, modelOverride)]) { console.error('[task-board] model ' + modelOverride + ' circuited, using parent model'); modelOverride = '' } }
|
|
149
205
|
var pack = ''
|
|
150
|
-
try { pack = await readContextPack(t) } catch (e) { console.error('[task-board] context pack read failed:', String(e)) }
|
|
206
|
+
try { pack = await readContextPack(sid, t) } catch (e) { console.error('[task-board] context pack read failed:', String(e)) }
|
|
151
207
|
// user prompt 只留一行指引,内容走上下文注入区块
|
|
152
208
|
var packNote = pack ? '本任务附带主窗口预研文件,已通过「上下文注入」区提供(含文件完整内容),直接基于其内容工作,不要重复读取这些文件。' : ''
|
|
153
209
|
var req = { label: role + ':' + t.id, prompt: [{ type: 'text', text: role === 'worker' ? buildWorkerPrompt(t, packNote) : buildVerifierPrompt(t, packNote) }], parent: parent, signal: makeSignal() }
|
|
@@ -167,13 +223,32 @@ export function apply(ctx) {
|
|
|
167
223
|
} finally {
|
|
168
224
|
if (ppEntry) { var ppi = pendingPacks.indexOf(ppEntry); if (ppi >= 0) pendingPacks.splice(ppi, 1) }
|
|
169
225
|
}
|
|
170
|
-
var
|
|
226
|
+
var c = cfg(dsnap)
|
|
227
|
+
var rec = { run: run, role: role, taskId: t.id, startedAt: Date.now(), model: modelOverride, settled: false }
|
|
171
228
|
runsFor(sid)[t.id] = rec
|
|
172
229
|
if (pack) packByChild[String(run.id)] = pack
|
|
173
230
|
if (!dispatchedEver[sid]) dispatchedEver[sid] = {}
|
|
174
231
|
dispatchedEver[sid][String(run.id)] = true
|
|
175
|
-
//
|
|
176
|
-
|
|
232
|
+
// 历史会话留档:每次派发都追加一条 {role,id,at,model},任务完成后仍可回看
|
|
233
|
+
// 各阶段(含重试的第 1/2/3 次 Worker)会话——否则 claimedBy/verifierRun 只留最后一次
|
|
234
|
+
recordRunHistory(sid, t.id, role, String(run.id), modelOverride, c.hardTimeoutMin).catch(function () {})
|
|
235
|
+
// ===== 两级超时:软超时只提醒主窗口(由人决定继续等待或终止),硬超时兜底 dispose =====
|
|
236
|
+
// 一次性 run 没有看门狗:完全依赖人工决策时,人不在线挂死的 run 会永久占用并发位,
|
|
237
|
+
// 所以保留硬上限作为最后防线(默认 120min,可配置)。
|
|
238
|
+
var startedAt = rec.startedAt
|
|
239
|
+
var softMs = c.softTimeoutMin * 60000
|
|
240
|
+
var hardMs = c.hardTimeoutMin * 60000
|
|
241
|
+
function finish(res, err) { if (rec.settled) return; rec.settled = true; settleRun(sid, rec, res, err) }
|
|
242
|
+
;(function softArm() {
|
|
243
|
+
var tm = ctx.timer; if (!tm) return
|
|
244
|
+
tm.timeout(softMs).then(function () {
|
|
245
|
+
if (rec.settled) return
|
|
246
|
+
var mins = Math.round((Date.now() - startedAt) / 60000)
|
|
247
|
+
pushSysNote(sid, '⏱ 任务「' + t.title + '」的 ' + role + '(' + t.id + ')已运行 ' + mins + ' 分钟仍未完成——如属正常长任务可忽略;需要干预可在看板详情页「立即终止」(硬超时 ' + c.hardTimeoutMin + ' 分钟后将自动终止并重试)')
|
|
248
|
+
softArm() // 持续提醒直到结算或硬超时
|
|
249
|
+
}).catch(function () {})
|
|
250
|
+
})()
|
|
251
|
+
withTimeout(run.result, hardMs, role + ':' + t.id).then(function (res) { finish(res, null) }).catch(function (e) { finish(null, e) })
|
|
177
252
|
return rec
|
|
178
253
|
}
|
|
179
254
|
|
|
@@ -190,6 +265,8 @@ export function apply(ctx) {
|
|
|
190
265
|
if (rec.role === 'worker') await settleWorker(sid, rec, output, failed, errText)
|
|
191
266
|
else await settleVerifier(sid, rec, output, failed, errText)
|
|
192
267
|
} catch (e) { console.error('[task-board] settle ' + rec.role + ' failed (task ' + rec.taskId + '):', String(e)) }
|
|
268
|
+
// 历史会话留档:记录该次 run 的结局(完成/失败/硬超时),详情页可据此标注阶段状态
|
|
269
|
+
closeRunHistory(sid, rec.taskId, String(rec.run.id), failed ? (err ? 'timeout/error' : 'incomplete') : 'completed').catch(function () {})
|
|
193
270
|
}
|
|
194
271
|
|
|
195
272
|
async function settleWorker(sid, rec, output, failed, errText) {
|
|
@@ -351,7 +428,10 @@ export function apply(ctx) {
|
|
|
351
428
|
// 快进不写盘的前提是磁盘上的 poolStatus 已经是空的——否则(典型:DSH 重启清空了
|
|
352
429
|
// 内存 runs 表,文件里残留重启前的忙碌快照)幽灵 Worker 会永远显示执行中
|
|
353
430
|
var stalePool = snap.poolStatus && (((snap.poolStatus.workers || []).length + (snap.poolStatus.verifiers || []).length) > 0)
|
|
354
|
-
|
|
431
|
+
// dispatchInfo 是瞬时通知(90s TTL):空闲周期也要负责过期清理,否则永久残留
|
|
432
|
+
var staleInfo = snap.dispatchInfo && (!snap.dispatchInfoAt || Date.now() - new Date(snap.dispatchInfoAt).getTime() > 90000)
|
|
433
|
+
if (staleInfo) { delete snap.dispatchInfo; delete snap.dispatchInfoAt }
|
|
434
|
+
if (stalePool || staleInfo) { if (stalePool) snap.poolStatus = emptyPool; try { await wt(sid, snap) } catch (_) {} }
|
|
355
435
|
return snap
|
|
356
436
|
}
|
|
357
437
|
var c = cfg(snap)
|
|
@@ -364,16 +444,19 @@ export function apply(ctx) {
|
|
|
364
444
|
var now = Date.now()
|
|
365
445
|
d.tasks.forEach(function (t) {
|
|
366
446
|
if (isOrphan(d, t, runs, now)) { t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; ah(t, 'in-progress', 'pending', 'system', '执行 run 已结束/丢失,回收重新排队'); pushSysNote(sid, '任务「' + t.title + '」执行 run 丢失,已回收重新排队'); info.push('reclaim ' + t.id) }
|
|
447
|
+
// verifier 派发占位超时回收:占位后强杀/spawn 中断会留 spawn-pending 死占位,超 2min 清掉恢复可派发
|
|
448
|
+
if (t.status === 'verifying' && t.verifierRun === 'spawn-pending' && (now - new Date(t.verifierRunAt || 0).getTime()) > 120000) { t.verifierRun = null; delete t.verifierRunAt; ah(t, 'verifying', 'verifying', 'system', 'Verifier 派发占位超时,回收重新排队'); info.push('reclaim-verifier ' + t.id) }
|
|
367
449
|
})
|
|
368
450
|
// worker 派发仅 auto;verifier 派发两种模式都跑(manual 模式主窗口 claim 做完的 full 档任务需要验收)
|
|
369
451
|
var capW = isAuto ? Math.max(0, c.maxWorkers - activeW) : 0
|
|
370
|
-
var picked = pickDispatch(d, capW, Math.max(0, c.maxVerifiers - activeV),
|
|
452
|
+
var picked = pickDispatch(d, capW, Math.max(0, c.maxVerifiers - activeV), runs)
|
|
371
453
|
picked.pendings.forEach(function (t) { claimApply(d, t, 'spawn-pending', 'dispatch'); toSpawn.push({ role: 'worker', t: t }); info.push('dispatch ' + t.id) })
|
|
372
|
-
picked.verifs.forEach(function (t) { toSpawn.push({ role: 'verifier', t: t }); info.push('verify ' + t.id) })
|
|
454
|
+
picked.verifs.forEach(function (t) { t.verifierRun = 'spawn-pending'; t.verifierRunAt = new Date().toISOString(); toSpawn.push({ role: 'verifier', t: t }); info.push('verify ' + t.id) })
|
|
373
455
|
// UI 池状态:来自活跃 run(一次性模型:没有成员名册,只有在跑的任务)
|
|
374
456
|
d.poolStatus = { workers: [], verifiers: [] }
|
|
375
457
|
Object.keys(runs).forEach(function (k) { var rc = runs[k]; d.poolStatus[rc.role === 'worker' ? 'workers' : 'verifiers'].push({ id: k, num: '-', busy: true, taskId: rc.taskId, runId: String(rc.run.id), done: 0, queueLen: 0, suspect: false, model: rc.model || '' }) })
|
|
376
|
-
if (info.length > 0) d.dispatchInfo = info.join('; ')
|
|
458
|
+
if (info.length > 0) { d.dispatchInfo = info.join('; '); d.dispatchInfoAt = new Date().toISOString() }
|
|
459
|
+
else if (d.dispatchInfo && (!d.dispatchInfoAt || Date.now() - new Date(d.dispatchInfoAt).getTime() > 90000)) { delete d.dispatchInfo; delete d.dispatchInfoAt } // 瞬时通知:90s TTL 过期即清,不再永久残留
|
|
377
460
|
return d
|
|
378
461
|
}, true) // skipKick:poolCycle 自写不触发 kickCycle(防无限循环)
|
|
379
462
|
|
|
@@ -383,11 +466,15 @@ export function apply(ctx) {
|
|
|
383
466
|
var rec = await spawnOneShot(sid, sp.t, sp.role)
|
|
384
467
|
if (rec) {
|
|
385
468
|
// claim 占位换成真实 run id;verifier run 单独记(claimedBy 保留 worker 的,供详情页跳转会话)
|
|
386
|
-
await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === sp.t.id }); if (t) { if (sp.role === 'worker' && t.claimedBy === 'spawn-pending') t.claimedBy = String(rec.run.id); if (sp.role === 'verifier') t.verifierRun = String(rec.run.id) }; return t }, true)
|
|
469
|
+
await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === sp.t.id }); if (t) { if (sp.role === 'worker' && t.claimedBy === 'spawn-pending') t.claimedBy = String(rec.run.id); if (sp.role === 'verifier' && t.verifierRun === 'spawn-pending') { t.verifierRun = String(rec.run.id); t.verifierRunAt = new Date().toISOString() } }; return t }, true)
|
|
387
470
|
} else if (sp.role === 'worker') {
|
|
388
|
-
// spawn 失败 → 回 pending
|
|
471
|
+
// spawn 失败 → 回 pending
|
|
389
472
|
await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === sp.t.id }); if (t && t.status === 'in-progress' && t.claimedBy === 'spawn-pending') { t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; ah(t, 'in-progress', 'pending', 'system', 'spawn 失败,回收重新排队') }; return t }, true)
|
|
390
473
|
pushSysNote(sid, '任务「' + sp.t.title + '」Worker 启动失败,已重新排队')
|
|
474
|
+
} else if (sp.role === 'verifier') {
|
|
475
|
+
// verifier spawn 失败 → 清占位,下轮 cycle 重试(占位不清会永远卡住派发)
|
|
476
|
+
await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === sp.t.id }); if (t && t.verifierRun === 'spawn-pending') { t.verifierRun = null; delete t.verifierRunAt }; return t }, true)
|
|
477
|
+
pushSysNote(sid, '任务「' + sp.t.title + '」Verifier 启动失败,下轮自动重试')
|
|
391
478
|
}
|
|
392
479
|
}
|
|
393
480
|
return result
|
|
@@ -450,7 +537,7 @@ export function apply(ctx) {
|
|
|
450
537
|
// ===== Tools =====
|
|
451
538
|
ctx.tools.register(defineTool({ name: 'task_list', description: '列出当前会话任务。', parameters: { type: 'object', properties: { status: { type: 'string', enum: ['pending', 'in-progress', 'verifying', 'resolved', 'blocked', 'cancelled'] }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, tag: { type: 'string' }, parentId: { type: 'string' }, includeArchived: { type: 'boolean' }, limit: { type: 'number' } }, required: [] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var d = await rt(sid); var a = d.tasks; var ts = a; if (!args.includeArchived) ts = ts.filter(function (x) { return x.status !== 'archived' }); if (args.status) ts = ts.filter(function (x) { return x.status === args.status }); if (args.priority) ts = ts.filter(function (x) { return x.priority === args.priority }); if (args.tag) ts = ts.filter(function (x) { return (x.tags || []).indexOf(args.tag) >= 0 }); if (args.parentId === 'null') ts = ts.filter(function (x) { return !isb(x) }); else if (args.parentId) ts = ts.filter(function (x) { return x.parentId === args.parentId }); var po = PRIO_RANK; ts.sort(function (a, b) { var dd = (po[b.priority] || 0) - (po[a.priority] || 0); return dd !== 0 ? dd : (a.createdAt || '').localeCompare(b.createdAt || '') }); var lim = Math.min(args.limit || 20, 100); var res = ts.slice(0, lim).map(function (x) { var e = Object.assign({}, x); if (isb(x)) { var p = gpt(x, a); if (p) e.parentSummary = { id: p.id, title: p.title, status: p.status } }; var ch = gsb(x.id, a); if (ch.length) { e.subtaskCount = ch.length; e.subtaskResolved = ch.filter(function (y) { return y.status === 'resolved' }).length }; return e }); var out = { tasks: res, total: ts.length, session: sid, actor: getActorId(), boardMode: d.boardMode || 'auto', teamMode: !!d.teamMode, poolStatus: d.poolStatus }; if (d.teamMode) out.teamHint = 'Team 模式已开启:实质性改动请优先 task_create 提交看板由池执行;调研/读取/讨论可直接进行;Worker 歧义会上报等你裁决。'; return out } }))
|
|
452
539
|
ctx.tools.register(defineTool({ name: 'task_context', description: '获取任务完整上下文。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, expandFiles: { type: 'boolean' }, includeParent: { type: 'boolean' }, includeSubtasks: { type: 'boolean' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var d = await rt(sid); var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found: ' + args.taskId }; return { ok: true, context: { task: t, inheritedContext: t.context || {} } } } }))
|
|
453
|
-
ctx.tools.register(defineTool({ name: 'task_preview_context', description: '派发前上下文预览:预演 task_create 的 contextFiles/contextNotes 将注入给子代理的实际内容(读盘后的最终形态),用于确认材料是否足够。不创建任务。返回 ok=false/empty=true 说明无可注入内容。', parameters: { type: 'object', properties: { contextFiles: { type: 'array', items: { type: 'string' }, description: '预演的文件路径列表' }, contextNotes: { type: 'string', description: '预演的调研笔记' } }, required: [] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var files = Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []; var notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''; if (!files.length && !notes.trim()) return { ok: false, error: 'contextFiles/contextNotes 至少提供一个' }; try { var pack = await readContextPack({ context: { files: files, notes: notes } }); return { ok: true, empty: !pack, pack: pack } } catch (e) { return { ok: false, error: String(e).slice(0, 200) } } } }))
|
|
540
|
+
ctx.tools.register(defineTool({ name: 'task_preview_context', description: '派发前上下文预览:预演 task_create 的 contextFiles/contextNotes 将注入给子代理的实际内容(读盘后的最终形态),用于确认材料是否足够。不创建任务。返回 ok=false/empty=true 说明无可注入内容。', parameters: { type: 'object', properties: { contextFiles: { type: 'array', items: { type: 'string' }, description: '预演的文件路径列表' }, contextNotes: { type: 'string', description: '预演的调研笔记' } }, required: [] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var files = Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []; var notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''; if (!files.length && !notes.trim()) return { ok: false, error: 'contextFiles/contextNotes 至少提供一个' }; try { var pack = await readContextPack(sid, { context: { files: files, notes: notes } }); return { ok: true, empty: !pack, pack: pack } } catch (e) { return { ok: false, error: String(e).slice(0, 200) } } } }))
|
|
454
541
|
ctx.tools.register(defineTool({ name: 'task_claim', description: '领取待办任务→in-progress。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, reason: { type: 'string' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; var err = claimCheck(d, t, actor); if (err) return { ok: false, error: err }; claimApply(d, t, actor, args.reason || 'claimed'); return { ok: true, task: t, context: { task: t, inheritedContext: t.context || {} } } }) } }))
|
|
455
542
|
ctx.tools.register(defineTool({ name: 'task_resolve', description: '提交验证(verifying)或阻塞(blocked)。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, status: { type: 'string', enum: ['verifying', 'blocked'] }, resolution: { type: 'string' } }, required: ['taskId', 'status'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'in-progress') return { ok: false, error: 'not in-progress' }; if (t.claimedBy !== actor) return { ok: false, error: 'not claimed by you' }; if (args.status === 'verifying' && !args.resolution) return { ok: false, error: 'resolution required' }; return resolveApply(d, t, actor, args.status, args.resolution, args.resolution || args.status) }) } }))
|
|
456
543
|
ctx.tools.register(defineTool({ name: 'task_verify', description: '验收:approved→resolved,rejected→in-progress。子任务全完成父任务自动verifying。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, verdict: { type: 'string', enum: ['approved', 'rejected'] }, comment: { type: 'string' } }, required: ['taskId', 'verdict'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'verifying') return { ok: false, error: 'not verifying' }; return verifyApply(d, t, actor, args.verdict, args.comment) }) } }))
|
|
@@ -471,7 +558,7 @@ export function apply(ctx) {
|
|
|
471
558
|
var notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''
|
|
472
559
|
if (!files.length && !notes.trim()) return { ok: false, error: 'contextFiles/contextNotes 至少提供一个' }
|
|
473
560
|
var pack = ''
|
|
474
|
-
try { pack = await readContextPack({ context: { files: files, notes: notes } }) } catch (e) { return { ok: false, error: '读取失败: ' + String(e).slice(0, 120) } }
|
|
561
|
+
try { pack = await readContextPack(rpcSessionId(args), { context: { files: files, notes: notes } }) } catch (e) { return { ok: false, error: '读取失败: ' + String(e).slice(0, 120) } }
|
|
475
562
|
return { ok: true, filesCount: files.length, notesLen: notes.length, pack: pack, empty: !pack }
|
|
476
563
|
})
|
|
477
564
|
// 活动心跳:读子代理会话日志的最后一帧,提取最近的动作摘要(卡片/详情页展示"现在跑到哪了")
|
|
@@ -481,25 +568,53 @@ export function apply(ctx) {
|
|
|
481
568
|
if (!rec || !rec.run) return { ok: true, activity: null, reason: 'no active run' }
|
|
482
569
|
var child = String(rec.run.id)
|
|
483
570
|
try {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
var
|
|
571
|
+
// 子会话日志定位:直接扫描 ~/.dsh/sessions/*/<child>/session.vN.jsonl.zstd
|
|
572
|
+
// (不再从看板路径反推 workspace——那依赖进程 cwd 凑巧等于工作区,曾是隐性 bug)
|
|
573
|
+
// v0.1.7 起日志文件名带格式版本号(session.v4.jsonl.zstd),旧会话是
|
|
574
|
+
// session.v3.jsonl.zstd / session.jsonl.zstd——按新到旧逐个探测。
|
|
575
|
+
var sessionsRoot = path.join(os.homedir(), '.dsh', 'sessions')
|
|
576
|
+
var log = null
|
|
577
|
+
var LOG_NAMES = ['session.v4.jsonl.zstd', 'session.v3.jsonl.zstd', 'session.v2.jsonl.zstd', 'session.jsonl.zstd']
|
|
578
|
+
var buckets = fsNode.readdirSync(sessionsRoot)
|
|
579
|
+
for (var bi = 0; bi < buckets.length && !log; bi++) {
|
|
580
|
+
for (var ni = 0; ni < LOG_NAMES.length; ni++) {
|
|
581
|
+
var cand = path.join(sessionsRoot, buckets[bi], child, LOG_NAMES[ni])
|
|
582
|
+
if (fsNode.existsSync(cand)) { log = cand; break }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (!log) return { ok: true, activity: null, reason: 'log not found' }
|
|
586
|
+
// 只同步读末尾 2MB(日志追加写,末帧必在尾部)——整文件 readFileSync 在大日志上
|
|
587
|
+
// 会造成数十毫秒级同步 I/O,多任务轮询时叠加成全局卡顿
|
|
588
|
+
var sz = fsNode.statSync(log).size
|
|
589
|
+
var TAIL = 2 * 1024 * 1024
|
|
590
|
+
var start = sz > TAIL ? sz - TAIL : 0
|
|
591
|
+
var buf = Buffer.alloc(sz - start)
|
|
592
|
+
var fd = fsNode.openSync(log, 'r')
|
|
593
|
+
try { fsNode.readSync(fd, buf, 0, buf.length, start) } finally { fsNode.closeSync(fd) }
|
|
490
594
|
var MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
var lines = text.split('\n').filter(Boolean)
|
|
595
|
+
// 追加写多帧格式:最新事件在末帧。但末帧可能只有 step/end、turn/end 这类
|
|
596
|
+
// 结算事件(v4 一帧只装一个step的增量),单解一帧经常捞不到动作 → 从新到旧
|
|
597
|
+
// 最多回扫 3 帧,找到第一个动作摘要即止(成本仍受控:每帧只是一次 zstd 解压)
|
|
495
598
|
var activity = null
|
|
496
|
-
for (var
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
599
|
+
for (var frame = 0; frame < 3 && !activity; frame++) {
|
|
600
|
+
var last = buf.lastIndexOf(MAGIC, frame === 0 ? buf.length - 1 : last - 1)
|
|
601
|
+
if (last < 0) break
|
|
602
|
+
var text = zstdDecompressSync(buf.subarray(last)).toString('utf8')
|
|
603
|
+
var lines = text.split('\n').filter(Boolean)
|
|
604
|
+
for (var i = lines.length - 1; i >= 0 && !activity; i--) {
|
|
605
|
+
try {
|
|
606
|
+
var e = JSON.parse(lines[i])
|
|
607
|
+
var dta = e.data || {}
|
|
608
|
+
if (e.type === 'tool/call' && dta.name) activity = '🔧 ' + dta.name + ' ' + String(dta.arguments || '').replace(/\s+/g, ' ').slice(0, 90)
|
|
609
|
+
// v4:助手文本在 assistant/message 的 content 块里;v3 及以前是 assistant/chunk 流片
|
|
610
|
+
else if (e.type === 'assistant/message') {
|
|
611
|
+
var msg = dta.message || {}
|
|
612
|
+
var blocks = Array.isArray(msg.content) ? msg.content : []
|
|
613
|
+
for (var b = blocks.length - 1; b >= 0; b--) { if (blocks[b] && blocks[b].type === 'text' && String(blocks[b].text || '').trim()) { activity = '💬 ' + String(blocks[b].text).replace(/\s+/g, ' ').slice(0, 120); break } }
|
|
614
|
+
}
|
|
615
|
+
else if (e.type === 'assistant/chunk' && dta.block && dta.block.type === 'text' && dta.block.text && dta.block.text.trim()) activity = '💬 ' + dta.block.text.replace(/\s+/g, ' ').slice(0, 120)
|
|
616
|
+
} catch (_) {}
|
|
617
|
+
}
|
|
503
618
|
}
|
|
504
619
|
return { ok: true, activity: activity }
|
|
505
620
|
} catch (e) { return { ok: true, activity: null, reason: String(e).slice(0, 80) } }
|
|
@@ -685,7 +800,7 @@ export function apply(ctx) {
|
|
|
685
800
|
}
|
|
686
801
|
return { ok: true, models: out }
|
|
687
802
|
})
|
|
688
|
-
handle('set-board-config', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { if (args.key === 'maxWorkers') d.maxWorkers = Math.max(1, Math.min(10, args.value || 3)); else if (args.key === 'maxVerifiers') d.maxVerifiers = Math.max(0, Math.min(5, args.value || 0)); else if (args.key === 'workerModel') d.workerModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'verifierModel') d.verifierModel = typeof args.value === 'string' ? args.value.trim() : ''; return { ok: true } }) })
|
|
803
|
+
handle('set-board-config', async function (args) { var sid = rpcSessionId(args); return mutateLocked(sid, function (d) { if (args.key === 'maxWorkers') d.maxWorkers = Math.max(1, Math.min(10, args.value || 3)); else if (args.key === 'maxVerifiers') d.maxVerifiers = Math.max(0, Math.min(5, args.value || 0)); else if (args.key === 'workerModel') d.workerModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'verifierModel') d.verifierModel = typeof args.value === 'string' ? args.value.trim() : ''; else if (args.key === 'softTimeoutMin') d.softTimeoutMin = Math.max(1, Math.min(480, Number(args.value) || 30)); else if (args.key === 'hardTimeoutMin') d.hardTimeoutMin = Math.max(1, Math.min(1440, Number(args.value) || 120)); return { ok: true } }) })
|
|
689
804
|
handle('create-task', async function (args) { var sid = rpcSessionId(args); var actor = getActorId(); return mutateLocked(sid, function (d) { if (args.id && d.tasks.find(function (x) { return x.id === args.id })) return { ok: false, error: 'duplicate id' }; if (args.dependsOn && args.dependsOn.length) { var derr = validateDeps(d, args.id || '(pending)', args.dependsOn); if (derr) return { ok: false, error: derr } }; var now = new Date().toISOString(); var t = { id: args.id || ('task-' + Date.now().toString(36)), title: args.title || 'Untitled', description: args.description || '', status: args.draft ? 'draft' : 'pending', priority: args.priority || 'medium', tags: args.tags || [], parentId: args.parentId || null, subtaskStrategy: null, assignMode: 'auto', assignee: null, context: { files: (Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []), docs: [], instructions: args.instructions || '', notes: (typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''), relatedTasks: [], prerequisites: '' }, acceptance: args.acceptance || '', dependsOn: args.dependsOn || [], pipeline: args.pipeline || '', claimedBy: null, claimedAt: null, createdAt: now, resolvedAt: null, verifiedAt: null, verifiedBy: null, archivedAt: null, resolution: null, messages: [], history: [{ from: 'created', to: args.draft ? 'draft' : 'pending', timestamp: now, actor: actor, note: args.draft ? 'created as draft' : 'created' }] }; if (!t.pipeline) { t.pipeline = classifyPipeline(t); t.pipelineAuto = true }; d.tasks.push(t); return { ok: true, task: t } }) })
|
|
690
805
|
handle('list-children', async function (args) { var sid = rpcSessionId(args); var subs = ctx.subagents; if (!subs) return { ok: true, children: [] }; try { var list = await subs.listChildren(sid); var children = (list || []).map(function (c) { return { id: String(c.sessionId || c.id || ''), label: String(c.label || c.title || c.mode || '') } }).filter(function (c) { return c.id.length > 0 }); return { ok: true, children: children } } catch (e) { return { ok: true, children: [], error: String(e) } } })
|
|
691
806
|
// ===== #14 批量操作:archive(仅 resolved/cancelled)/ set-priority(全部)=====
|
|
@@ -744,7 +859,11 @@ export function apply(ctx) {
|
|
|
744
859
|
req.on('error', reject)
|
|
745
860
|
})
|
|
746
861
|
}
|
|
747
|
-
ctx.webServer.register
|
|
862
|
+
// ctx.webServer.register 只返回释放器、不绑定调用方 fiber(tools.register 才会),
|
|
863
|
+
// 必须自己 ctx.effect 包住——否则插件禁用/重载后路由残留,handler 闭包指向已销毁的
|
|
864
|
+
// fiber 内状态(500),且再次启用时撞 "duplicate exact route" 永远起不来。
|
|
865
|
+
ctx.effect(function () {
|
|
866
|
+
return ctx.webServer.register({
|
|
748
867
|
kind: 'exact',
|
|
749
868
|
path: '/dsh-agent-board',
|
|
750
869
|
handler: async function (req, res) {
|
|
@@ -757,6 +876,7 @@ export function apply(ctx) {
|
|
|
757
876
|
if (!fn) { res.writeHead(404); res.end(JSON.stringify({ ok: false, message: 'unknown method: ' + payload.method })); return }
|
|
758
877
|
try { var out = await fn(payload.args); res.writeHead(200); res.end(JSON.stringify(out === undefined ? null : out)) } catch (e) { res.writeHead(500); res.end(JSON.stringify({ ok: false, message: String(e) })) }
|
|
759
878
|
},
|
|
879
|
+
})
|
|
760
880
|
})
|
|
761
881
|
|
|
762
882
|
console.log('[task-board] v74 loaded (pool removed: one-shot dispatch, context injected per task, dispose on settle)')
|
package/lib/client.js
CHANGED
|
@@ -16,7 +16,10 @@ function apply(ctx) {
|
|
|
16
16
|
var slots = ctx.get('slots')
|
|
17
17
|
if (slots === undefined) return
|
|
18
18
|
var sessionsSvc = ctx.get('sessions')
|
|
19
|
-
|
|
19
|
+
// 会话跳转走 uiWorkspace.openSession(0.1.7 起 sessions 服务已无 open 方法,
|
|
20
|
+
// 旧调用 sessionsSvc.open 会抛 TypeError: sessionsSvc.open is not a function)
|
|
21
|
+
var uiWorkspaceSvc = ctx.get('uiWorkspace')
|
|
22
|
+
var C = { bg: 'var(--dsw-alias-bg-base)', card: 'var(--dsw-alias-bg-layer-1)', nested: 'var(--dsw-alias-bg-layer-2)', border: 'var(--dsw-alias-border-l1)', border2: 'var(--dsw-alias-border-l2)', brand: 'var(--dsw-alias-brand-primary)', text: 'var(--dsw-alias-label-primary)', text2: 'var(--dsw-alias-label-secondary)', err: 'var(--dsw-alias-state-error-primary)', ok: 'var(--dsw-alias-state-success-primary)', warn: 'var(--dsw-alias-state-warn-primary)' }
|
|
20
23
|
var C_INV = 'var(--dsw-alias-label-primary-inverted)' // 品牌底/彩色底上的反色文字(深浅模式自动反转)
|
|
21
24
|
var prioColor = { critical: C.err, high: C.warn, medium: C.brand, low: C.text2 }
|
|
22
25
|
var prioLabel = { critical: '紧急', high: '高', medium: '中', low: '低' }
|
|
@@ -200,7 +203,7 @@ function apply(ctx) {
|
|
|
200
203
|
}
|
|
201
204
|
var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
|
|
202
205
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
203
|
-
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', isRoot: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', selectMode: false, selected: {}, undoSnapshot: null, archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [] }
|
|
206
|
+
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, isRoot: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', selectMode: false, selected: {}, undoSnapshot: null, archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [] }
|
|
204
207
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
205
208
|
try {
|
|
206
209
|
var onKey = function (e) {
|
|
@@ -235,10 +238,12 @@ function apply(ctx) {
|
|
|
235
238
|
state.maxVerifiers = (d && d.maxVerifiers) || 2
|
|
236
239
|
state.verifierModel = (d && d.verifierModel) || ''
|
|
237
240
|
state.workerModel = (d && d.workerModel) || ''
|
|
241
|
+
state.softTimeoutMin = (d && d.softTimeoutMin) || 30
|
|
242
|
+
state.hardTimeoutMin = (d && d.hardTimeoutMin) || 120
|
|
238
243
|
state.poolStatus = (d && d.poolStatus) || null
|
|
239
244
|
state.isRoot = !d || d.isRoot !== false
|
|
240
245
|
if (state.isRoot === false && state.open) { state.open = false; state.detailId = null } // 子代理会话:强制收起看板
|
|
241
|
-
if (d && d.dispatchInfo) { state.dispatchInfo = d.dispatchInfo }
|
|
246
|
+
if (d && d.dispatchInfo && d.dispatchInfoAt && Date.now() - new Date(d.dispatchInfoAt).getTime() < 120000) { state.dispatchInfo = d.dispatchInfo } else { state.dispatchInfo = '' } // 瞬时通知 2min 内有效,过期强制清空(服务端写后不清曾致残留数天)
|
|
242
247
|
// escalation 一等公民:出现新的待裁决任务 → 面板自动弹开直达该任务详情
|
|
243
248
|
var newEsc = state.tasks.filter(function (t) { return t.escalation && state.escalatedIds.indexOf(t.id) < 0 })
|
|
244
249
|
state.escalatedIds = state.tasks.filter(function (t) { return t.escalation }).map(function (t) { return t.id })
|
|
@@ -249,7 +254,7 @@ function apply(ctx) {
|
|
|
249
254
|
function fetchChildren() { if (!state.sessionId) return; var epoch = reqEpoch; rpc('list-children').then(function (d) { if (epoch !== reqEpoch) return; state.children = (d && d.children) || []; notify() }).catch(function () {}) }
|
|
250
255
|
// 活动心跳:对进行中/验收中的任务轮询子代理最近动作(卡片与详情展示"现在跑到哪了")
|
|
251
256
|
function fetchActivity() {
|
|
252
|
-
if (!state.sessionId || !state.isRoot) return
|
|
257
|
+
if (!state.sessionId || !state.isRoot || !state.open) return // 面板关闭时不轮询活动(省同步 I/O)
|
|
253
258
|
var running = state.tasks.filter(function (t) { return t.status === 'in-progress' || t.status === 'verifying' })
|
|
254
259
|
if (!running.length) { if (Object.keys(state.activity).length) { state.activity = {}; notify() } return }
|
|
255
260
|
var pending = running.length
|
|
@@ -301,7 +306,7 @@ function apply(ctx) {
|
|
|
301
306
|
function fmtTime(iso) { if (!iso) return '-'; try { return new Date(iso).toLocaleString() } catch (_) { return iso } }
|
|
302
307
|
function getTask(id) { return state.tasks.find(function (t) { return t.id === id }) }
|
|
303
308
|
function shortId(sid) { return sid ? String(sid).slice(0, 14) : '' }
|
|
304
|
-
function jumpTo(actorId) { if (
|
|
309
|
+
function jumpTo(actorId) { if (uiWorkspaceSvc && actorId && actorId !== 'system' && actorId !== 'unknown' && actorId !== 'auto-dispatch') uiWorkspaceSvc.openSession(actorId) }
|
|
305
310
|
function transition(taskId, from, to) {
|
|
306
311
|
if (from === to) return
|
|
307
312
|
var ok = function () { fetchTasks() }; var fail = function () { fetchTasks() }
|
|
@@ -376,8 +381,8 @@ function apply(ctx) {
|
|
|
376
381
|
setRange(fmt(from), fmt(to))
|
|
377
382
|
}
|
|
378
383
|
var headBtn = { fontSize: 11, padding: '3px 10px', border: '1px solid ' + (rg.from || rg.to ? C.brand : C.border), borderRadius: 5, background: (rg.from || rg.to) ? C.nested : C.card, color: (rg.from || rg.to) ? C.brand : C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }
|
|
379
|
-
var chip = { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.
|
|
380
|
-
var dateInput = { fontSize: 11, padding: '2px 6px', border: '1px solid ' + C.
|
|
384
|
+
var chip = { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.border2, borderRadius: 10, background: C.card, color: C.text2, cursor: 'pointer' }
|
|
385
|
+
var dateInput = { fontSize: 11, padding: '2px 6px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text }
|
|
381
386
|
return React.createElement('div', { style: { marginBottom: 10 } },
|
|
382
387
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
383
388
|
React.createElement('button', { onClick: function () { state.rfOpen = !open; setOpen(!open) }, style: headBtn },
|
|
@@ -418,7 +423,7 @@ function apply(ctx) {
|
|
|
418
423
|
open ? (shown.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2, padding: '6px 0' } }, '时间范围内无活跃会话') : shown.map(function (b, i) {
|
|
419
424
|
var isSelf = b.session === state.sessionId
|
|
420
425
|
return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 10, padding: '3px 0', borderBottom: '1px solid ' + C.nested } },
|
|
421
|
-
React.createElement('span', { onClick: function () { if (!isSelf &&
|
|
426
|
+
React.createElement('span', { onClick: function () { if (!isSelf && uiWorkspaceSvc) uiWorkspaceSvc.openSession(b.session) }, style: { color: isSelf ? C.text2 : C.brand, cursor: isSelf ? 'default' : 'pointer', textDecoration: isSelf ? 'none' : 'underline', minWidth: 90 } }, (isSelf ? '★ ' : '') + shortId(b.session)),
|
|
422
427
|
React.createElement('span', { style: { color: C.text2 } }, b.teamMode ? 'Team' : (b.boardMode === 'auto' ? '自动' : '手动')),
|
|
423
428
|
React.createElement('span', { style: { display: 'inline-flex', gap: 5 } },
|
|
424
429
|
b.counts.inProgress ? React.createElement('span', { style: { color: C.brand } }, '▶' + b.counts.inProgress) : null,
|
|
@@ -484,7 +489,29 @@ function apply(ctx) {
|
|
|
484
489
|
function ModeSwitch(props) { var mode = props.mode; var teamOn = props.teamOn; function pick(m) { if (teamOn && m === 'manual') return; rpc('set-board-mode', { mode: m }).then(fetchTasks).catch(function () {}) } var btnBase = { fontSize: 12, padding: '4px 14px', border: 'none', cursor: 'pointer', fontWeight: 500, flex: 1, textAlign: 'center', borderRadius: 6, transition: 'all .15s' }; var manDis = teamOn; return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 8, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { pick('auto') }, style: Object.assign({}, btnBase, mode === 'auto' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('bot', 12, '自动')), React.createElement('button', { onClick: function () { pick('manual') }, disabled: manDis, title: manDis ? 'Team 模式开启时不可切换到手动' : '', style: Object.assign({}, btnBase, mode === 'manual' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: manDis ? C.text2 + '88' : C.text2 }, manDis ? { cursor: 'not-allowed', opacity: 0.5 } : {}) }, icText('user', 12, '手动'))) }
|
|
485
490
|
function TeamSwitch(props) { var on = props.on; function toggle() { rpc('set-team-mode', { enabled: !on }).then(fetchTasks).catch(function () {}) } return React.createElement('button', { onClick: toggle, title: on ? 'Team 模式已开启:任务走看板派发,Worker 歧义自动上报主窗口裁决(不拦截主窗口编辑)' : '开启 Team 模式:任务全走看板,Worker 歧义上报主窗口裁决', style: { fontSize: 12, padding: '4px 10px', border: '1px solid ' + (on ? C.brand : C.border), borderRadius: 8, cursor: 'pointer', fontWeight: 500, background: on ? C.brand : 'transparent', color: on ? C_INV : C.text2, transition: 'all .15s' } }, icText('users', 12, 'Team' + (on ? ' ON' : ''))) }
|
|
486
491
|
function ViewTab() { var btnBase = { fontSize: 11, padding: '3px 10px', border: 'none', cursor: 'pointer', fontWeight: 500, borderRadius: 5, transition: 'all .15s' }; function goArch() { state.view = 'archive'; notify(); fetchArchived() } return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 6, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { state.view = 'board'; notify() }, style: Object.assign({}, btnBase, state.view === 'board' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('clipboard-list', 12), '看板')), React.createElement('button', { onClick: function () { state.view = 'team'; notify() }, style: Object.assign({}, btnBase, state.view === 'team' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('users', 12, '团队')), React.createElement('button', { onClick: function () { state.view = 'dashboard'; notify() }, style: Object.assign({}, btnBase, state.view === 'dashboard' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('bar-chart-3', 12, '仪表盘')), React.createElement('button', { onClick: goArch, title: '已归档任务(可检索、可恢复)', style: Object.assign({}, btnBase, state.view === 'archive' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('archive', 12, '归档'))) }
|
|
487
|
-
function PoolCfg(props) { var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]; var _b = useState(false), dirty = _b[0], setDirty = _b[1]; useEffect(function () { setVal(String(props.value)); setDirty(false) }, [props.value]); function commit(v) { var n = parseInt(v, 10); if (!isNaN(n) && n >= 0 && n <= 10) { setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(fetchTasks).catch(function () {}) } } function step(d) { var n = parseInt(val, 10) || 0; commit(String(Math.max(0, Math.min(10, n + d)))) } var miniBtn = { fontSize: 9, padding: '0px 4px', border: '1px solid ' + C.
|
|
492
|
+
function PoolCfg(props) { var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]; var _b = useState(false), dirty = _b[0], setDirty = _b[1]; useEffect(function () { setVal(String(props.value)); setDirty(false) }, [props.value]); function commit(v) { var n = parseInt(v, 10); if (!isNaN(n) && n >= 0 && n <= 10) { setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(fetchTasks).catch(function () {}) } } function step(d) { var n = parseInt(val, 10) || 0; commit(String(Math.max(0, Math.min(10, n + d)))) } var miniBtn = { fontSize: 9, padding: '0px 4px', border: '1px solid ' + C.border2, borderRadius: 2, background: C.nested, color: C.text2, cursor: 'pointer', lineHeight: '14px' }; return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 1, fontSize: 9, color: C.text2 } }, props.label, React.createElement('button', { onClick: function () { step(-1) }, title: '减 1', style: miniBtn }, '−'), React.createElement('input', { value: val, onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== String(props.value)) }, onBlur: function () { if (dirty) commit(val) }, onKeyDown: function (e) { if (e.key === 'Enter') commit(val) }, style: { width: 22, padding: '0px 2px', fontSize: 9, textAlign: 'center', border: '1px solid ' + (dirty ? C.brand : C.border2), borderRadius: 2, background: C.card, color: C.text } }), React.createElement('button', { onClick: function () { step(1) }, title: '加 1', style: miniBtn }, '+'), dirty ? React.createElement('button', { onClick: function () { commit(val) }, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null) }
|
|
493
|
+
function MinCfg(props) {
|
|
494
|
+
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
495
|
+
var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]
|
|
496
|
+
var _b = useState(false), dirty = _b[0], setDirty = _b[1]
|
|
497
|
+
useEffect(function () { setVal(String(props.value)); setDirty(false) }, [props.value])
|
|
498
|
+
function commit(v) {
|
|
499
|
+
var n = parseInt(v, 10)
|
|
500
|
+
if (isNaN(n) || n < props.min || n > props.max) { setVal(String(props.value)); setDirty(false); return }
|
|
501
|
+
setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(fetchTasks).catch(function () {})
|
|
502
|
+
}
|
|
503
|
+
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } },
|
|
504
|
+
props.label,
|
|
505
|
+
React.createElement('input', {
|
|
506
|
+
value: val, title: props.tip || (props.min + ' ~ ' + props.max + ' 分钟'),
|
|
507
|
+
onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== String(props.value)) },
|
|
508
|
+
onBlur: function () { if (dirty) commit(val) },
|
|
509
|
+
onKeyDown: function (e) { if (e.key === 'Enter') commit(val) },
|
|
510
|
+
style: { width: 38, padding: '0px 3px', fontSize: 9, textAlign: 'center', border: '1px solid ' + (dirty ? C.brand : C.border2), borderRadius: 2, background: C.card, color: C.text }
|
|
511
|
+
}),
|
|
512
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '分'),
|
|
513
|
+
dirty ? React.createElement('button', { onClick: function () { commit(val) }, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null)
|
|
514
|
+
}
|
|
488
515
|
function PoolStatus() {
|
|
489
516
|
var ps = state.poolStatus
|
|
490
517
|
if (!ps) return null
|
|
@@ -521,17 +548,25 @@ function apply(ctx) {
|
|
|
521
548
|
}
|
|
522
549
|
// 降级:文本输入(模型枚举不可用时)
|
|
523
550
|
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
|
|
524
|
-
React.createElement('input', { value: val, onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== (props.value || '')) }, onBlur: function () { if (dirty) save() }, onKeyDown: function (e) { if (e.key === 'Enter') save() }, placeholder: '空=同父级', style: { width: 110, padding: '0px 4px', fontSize: 9, border: '1px solid ' + (dirty ? C.brand : C.
|
|
551
|
+
React.createElement('input', { value: val, onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== (props.value || '')) }, onBlur: function () { if (dirty) save() }, onKeyDown: function (e) { if (e.key === 'Enter') save() }, placeholder: '空=同父级', style: { width: 110, padding: '0px 4px', fontSize: 9, border: '1px solid ' + (dirty ? C.brand : C.border2), borderRadius: 2, background: C.card, color: C.text } }),
|
|
525
552
|
dirty ? React.createElement('button', { onClick: save, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null)
|
|
526
553
|
}
|
|
527
554
|
function PoolCfgPopover(props) {
|
|
528
555
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
|
|
529
556
|
var _a = useState(false), open = _a[0], setOpen = _a[1]
|
|
530
|
-
var
|
|
557
|
+
var _p = useState(null), pos = _p[0], setPos = _p[1]
|
|
558
|
+
var ref = useRef(null); var btnRef = useRef(null)
|
|
531
559
|
useEffect(function () { if (!open) return; function onDown(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }; document.addEventListener('mousedown', onDown); return function () { document.removeEventListener('mousedown', onDown) } }, [open])
|
|
560
|
+
function toggle() {
|
|
561
|
+
// position:fixed + 视口坐标:面板容器是 overflow:hidden + maxHeight:60vh,
|
|
562
|
+
// absolute 弹窗在面板内容短时会被裁掉下半截(超时配置行曾被整个裁掉)。
|
|
563
|
+
// fixed 脱离任何祖先裁剪(面板无 transform/filter,不会形成包含块)。
|
|
564
|
+
if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos({ top: r.bottom + 4, right: Math.max(8, window.innerWidth - r.right) }) }
|
|
565
|
+
setOpen(!open)
|
|
566
|
+
}
|
|
532
567
|
return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
|
|
533
|
-
React.createElement('button', {
|
|
534
|
-
open ? React.createElement('div', { style: { position: '
|
|
568
|
+
React.createElement('button', { ref: btnRef, onClick: toggle, title: '池配置', style: { fontSize: 12, padding: '3px 8px', border: '1px solid ' + (open ? C.brand : C.border), borderRadius: 6, cursor: 'pointer', background: open ? C.nested : 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('settings', 13)),
|
|
569
|
+
open && pos ? React.createElement('div', { style: { position: 'fixed', top: pos.top + 'px', right: pos.right + 'px', padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 4px 16px rgba(0,0,0,0.15)', zIndex: 1000, whiteSpace: 'nowrap', maxHeight: 'calc(100vh - ' + (pos.top + 12) + 'px)', overflowY: 'auto' } },
|
|
535
570
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
|
|
536
571
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
|
|
537
572
|
React.createElement(PoolCfg, { label: 'W并发', cfgKey: 'maxWorkers', value: props.maxW }),
|
|
@@ -541,7 +576,11 @@ function apply(ctx) {
|
|
|
541
576
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Worker 执行模型)')),
|
|
542
577
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
543
578
|
React.createElement(ModelCfg, { label: 'V模型', cfgKey: 'verifierModel', value: props.verifierModel }),
|
|
544
|
-
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)'))
|
|
579
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)')),
|
|
580
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '两级超时(软超时只提醒主窗口,由你决定继续等待或终止)'),
|
|
581
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
582
|
+
React.createElement(MinCfg, { label: '软超时', cfgKey: 'softTimeoutMin', value: props.softT, min: 1, max: 480, tip: '超过后在看板提醒主窗口,不终止 run(1~480 分钟)' }),
|
|
583
|
+
React.createElement(MinCfg, { label: '硬超时', cfgKey: 'hardTimeoutMin', value: props.hardT, min: 1, max: 1440, tip: '兜底:人不在线时自动终止挂死 run 并重试(1~1440 分钟)' }))) : null)
|
|
545
584
|
}
|
|
546
585
|
// #13 筛选:文本(标题/描述/ID)+ 优先级多选 + 标签
|
|
547
586
|
function passFilter(t) {
|
|
@@ -557,6 +596,17 @@ function apply(ctx) {
|
|
|
557
596
|
// #19 管线档位元数据
|
|
558
597
|
var pipeMeta = { full: { icon: 'flask-conical', label: '全流程(执行+验证)', short: '全流程' }, work: { icon: 'file-text', label: '免验证(只做不验)', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直接处理', short: '直办' } }
|
|
559
598
|
function durOf(t) { try { var s = t.createdAt ? new Date(t.createdAt).getTime() : 0; if (!s) return ''; var eRaw = t.resolvedAt || t.archivedAt; var e = eRaw ? new Date(eRaw).getTime() : Date.now(); var m = Math.max(0, Math.round((e - s) / 60000)); if (m < 1) return '<1m'; if (m < 60) return m + 'm'; var h = Math.floor(m / 60); return h + 'h' + (m % 60 ? (m % 60) + 'm' : '') } catch (_) { return '' } }
|
|
599
|
+
function elapsedMin(iso) { var s = iso ? new Date(iso).getTime() : 0; if (!s) return 0; return Math.max(0, Math.round((Date.now() - s) / 60000)) }
|
|
600
|
+
// 历史会话列表:优先用 host 留档的 t.runs(含全部重试);老任务回退到 claimedBy/verifierRun
|
|
601
|
+
function historyRuns(t) {
|
|
602
|
+
var out = Array.isArray(t.runs) ? t.runs.slice() : []
|
|
603
|
+
if (!out.length) {
|
|
604
|
+
if (t.claimedBy && t.claimedBy !== 'auto-dispatch' && t.claimedBy !== 'system') out.push({ role: 'worker', id: t.claimedBy, at: t.claimedAt })
|
|
605
|
+
if (t.verifierRun) out.push({ role: 'verifier', id: t.verifierRun, at: t.verifiedAt })
|
|
606
|
+
}
|
|
607
|
+
return out.filter(function (r) { return r && r.id })
|
|
608
|
+
}
|
|
609
|
+
function elapsedSince(iso) { var m = elapsedMin(iso); if (!m) return '<1m'; if (m < 60) return m + 'm'; var h = Math.floor(m / 60); return h + 'h' + (m % 60 ? (m % 60) + 'm' : '') }
|
|
560
610
|
function pipeOf(t) { return pipeMeta[t.pipeline] || pipeMeta.full }
|
|
561
611
|
// #14 批量操作条(#16 带一步撤销:快照操作前的 priority/status)
|
|
562
612
|
function BatchBar() {
|
|
@@ -587,7 +637,7 @@ function apply(ctx) {
|
|
|
587
637
|
var hasFilter = q.trim() || fp.length > 0 || ft
|
|
588
638
|
var chipBase = { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: '1px solid ' + C.border, cursor: 'pointer', background: 'transparent', color: C.text2, transition: 'all .15s' }
|
|
589
639
|
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
|
|
590
|
-
React.createElement('input', { value: q, onChange: function (e) { setQ2(e.target.value) }, placeholder: '🔍 搜索标题/描述/ID…', style: { flex: '0 1 200px', fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.
|
|
640
|
+
React.createElement('input', { value: q, onChange: function (e) { setQ2(e.target.value) }, placeholder: '🔍 搜索标题/描述/ID…', style: { flex: '0 1 200px', fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text } }),
|
|
591
641
|
['critical', 'high', 'medium', 'low'].map(function (p) { var on = fp.indexOf(p) >= 0; return React.createElement('button', { key: p, onClick: function () { togglePrio(p) }, style: Object.assign({}, chipBase, on ? { background: prioColor[p], color: C_INV, borderColor: prioColor[p] } : {}) }, prioLabel[p]) }),
|
|
592
642
|
allTags().length > 0 ? React.createElement('select', { value: ft, onChange: function (e) { state.filterTag = e.target.value; notify() }, style: { fontSize: 10, padding: '2px 4px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text } }, React.createElement('option', { value: '' }, '🏷 全部标签'), allTags().map(function (g) { return React.createElement('option', { key: g, value: g }, g) })) : null,
|
|
593
643
|
hasFilter ? React.createElement('button', { onClick: clearAll, style: { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: 'none', background: C.nested, color: C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('x', 9), '清除') : null)
|
|
@@ -629,7 +679,7 @@ function apply(ctx) {
|
|
|
629
679
|
})
|
|
630
680
|
function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchArchived(); fetchTasks() }).catch(function () {}) }
|
|
631
681
|
return React.createElement('div', null,
|
|
632
|
-
React.createElement('input', { value: q, onChange: function (e) { setQ(e.target.value); state.archQ = e.target.value }, placeholder: '检索标题 / ID / 标签…', style: { width: '100%', padding: '5px 8px', fontSize: 11, border: '1px solid ' + C.
|
|
682
|
+
React.createElement('input', { value: q, onChange: function (e) { setQ(e.target.value); state.archQ = e.target.value }, placeholder: '检索标题 / ID / 标签…', style: { width: '100%', padding: '5px 8px', fontSize: 11, border: '1px solid ' + C.border2, borderRadius: 5, background: C.card, color: C.text, marginBottom: 8 } }),
|
|
633
683
|
state.archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
|
|
634
684
|
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
635
685
|
list.map(function (t) {
|
|
@@ -639,9 +689,22 @@ function apply(ctx) {
|
|
|
639
689
|
t.verification ? React.createElement('span', { title: '验收结论: ' + (t.verification.summary || '').slice(0, 100), style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.verification.verdict === 'approved' ? C.ok : C.err, color: C_INV } }, t.verification.verdict === 'approved' ? '过' : '驳') : null,
|
|
640
690
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, t.archivedAt ? ago(t.archivedAt) : '')),
|
|
641
691
|
t.verification && t.verification.summary ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + t.verification.summary) : null,
|
|
642
|
-
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4 } },
|
|
692
|
+
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4, flexWrap: 'wrap' } },
|
|
643
693
|
React.createElement('button', { onClick: function () { restore(t.id) }, title: '恢复为待办', style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '↩ 恢复待办'),
|
|
644
|
-
|
|
694
|
+
(function () {
|
|
695
|
+
var runs = historyRuns(t)
|
|
696
|
+
if (!runs.length) return React.createElement('span', { style: { fontSize: 9, color: C.text2, opacity: 0.6, padding: '2px 0' } }, '无历史会话')
|
|
697
|
+
var wN = 0, vN = 0
|
|
698
|
+
return React.createElement('span', { style: { display: 'inline-flex', gap: 5, flexWrap: 'wrap' } }, runs.map(function (r, i) {
|
|
699
|
+
var seq = r.role === 'verifier' ? (++vN) : (++wN)
|
|
700
|
+
var isV = r.role === 'verifier'
|
|
701
|
+
return React.createElement('button', {
|
|
702
|
+
key: i, onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
|
|
703
|
+
title: (isV ? 'Verifier' : 'Worker') + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
|
|
704
|
+
style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + (isV ? C.warn : C.brand), borderRadius: 3, background: 'transparent', color: isV ? C.warn : C.brand, cursor: 'pointer' }
|
|
705
|
+
}, '→ ' + (isV ? 'V' : 'W') + '#' + seq)
|
|
706
|
+
}))
|
|
707
|
+
})()))
|
|
645
708
|
}))
|
|
646
709
|
}
|
|
647
710
|
function TeamView() {
|
|
@@ -660,7 +723,25 @@ function apply(ctx) {
|
|
|
660
723
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '一次性子代理会话' }, '会话 ' + String(m.runId || '').slice(0, 8) + '…(用完即销毁)'))
|
|
661
724
|
}
|
|
662
725
|
var ws = ps.workers || [], vs = ps.verifiers || []
|
|
726
|
+
var _R2 = React; var _st = _R2.useState(''), editMsg = _st[0], setEditMsg = _st[1]
|
|
727
|
+
function saveTimeout(cfgKey, v, min, max) {
|
|
728
|
+
var n = parseInt(v, 10)
|
|
729
|
+
if (isNaN(n) || n < min || n > max) { setEditMsg('范围 ' + min + '~' + max + ' 分钟'); setTimeout(function () { setEditMsg('') }, 2500); return }
|
|
730
|
+
rpc('set-board-config', { key: cfgKey, value: n }).then(function () { setEditMsg('✅ 已保存'); setTimeout(function () { setEditMsg('') }, 2000); fetchTasks() }).catch(function () { setEditMsg('⚠️ 保存失败'); setTimeout(function () { setEditMsg('') }, 2500) })
|
|
731
|
+
}
|
|
732
|
+
var tIn = { width: 44, padding: '1px 3px', fontSize: 10, textAlign: 'center', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.text }
|
|
663
733
|
return React.createElement('div', null,
|
|
734
|
+
React.createElement('div', { style: { marginBottom: 10, padding: '6px 8px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', fontSize: 10, color: C.text2 } },
|
|
735
|
+
ic('activity', 11),
|
|
736
|
+
React.createElement('span', null, '超时策略:'),
|
|
737
|
+
React.createElement('span', { style: { color: C.brand, fontWeight: 600 } }, '软超时'),
|
|
738
|
+
React.createElement('input', { type: 'number', defaultValue: String(state.softTimeoutMin || 30), min: 1, max: 480, title: '超过该时长只提醒主窗口,不终止 run', onBlur: function (e) { saveTimeout('softTimeoutMin', e.target.value, 1, 480) }, onKeyDown: function (e) { if (e.key === 'Enter') saveTimeout('softTimeoutMin', e.target.value, 1, 480) }, style: tIn }),
|
|
739
|
+
React.createElement('span', null, '分(仅提醒,由你决定继续等待或终止)'),
|
|
740
|
+
React.createElement('span', { style: { margin: '0 3px', color: C.border } }, '·'),
|
|
741
|
+
React.createElement('span', { style: { color: C.warn, fontWeight: 600 } }, '硬超时'),
|
|
742
|
+
React.createElement('input', { type: 'number', defaultValue: String(state.hardTimeoutMin || 120), min: 1, max: 1440, title: '人不在线时兜底:自动终止挂死 run 并重试', onBlur: function (e) { saveTimeout('hardTimeoutMin', e.target.value, 1, 1440) }, onKeyDown: function (e) { if (e.key === 'Enter') saveTimeout('hardTimeoutMin', e.target.value, 1, 1440) }, style: tIn }),
|
|
743
|
+
React.createElement('span', null, '分(兜底自动终止并重试)'),
|
|
744
|
+
editMsg ? React.createElement('span', { style: { color: editMsg.indexOf('✅') === 0 ? C.ok : C.err, fontWeight: 600 } }, editMsg) : null),
|
|
664
745
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: C.text, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('zap', 12), 'Worker 池 (' + ws.length + ')'),
|
|
665
746
|
ws.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 10 } }, '暂无 Worker(有待办任务时自动扩容)') : React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 12 } }, ws.map(function (m) { return memberCard(m, 'worker') })),
|
|
666
747
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: C.text, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('clipboard-check', 12), 'Verifier 池 (' + vs.length + ')'),
|
|
@@ -697,8 +778,7 @@ function apply(ctx) {
|
|
|
697
778
|
if (!task) { state.detailId = null; return React.createElement('div', { style: { padding: 20, color: C.text2 } }, '任务不存在') }
|
|
698
779
|
function doAction(fn) { fn().then(fetchTasks).catch(function () {}) }
|
|
699
780
|
function saveEdit() { setSaving(true); rpc('update-task', { taskId: task.id, title: editTitle, description: editDesc, resetToPending: true }).then(function () { setSaving(false); fetchTasks() }).catch(function () { setSaving(false) }) }
|
|
700
|
-
function jumpToAgent() { if (
|
|
701
|
-
function jumpToVerifier() { if (sessionsSvc && task.verifierRun) sessionsSvc.open(task.verifierRun) }
|
|
781
|
+
function jumpToAgent() { if (uiWorkspaceSvc && task.claimedBy) uiWorkspaceSvc.openSession(task.claimedBy) }
|
|
702
782
|
function submitArbitration() { if (!arbAnswer.trim()) return; rpc('resolve-escalation', { taskId: task.id, answer: arbAnswer }).then(function (r) { setActionMsg(r && r.ok ? '✅ 裁决已转达给 Worker' : '⚠️ ' + ((r && r.error) || '失败')); setArbAnswer(''); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
703
783
|
function doTerminate() { rpc('terminate-agent', { taskId: task.id }).then(function (r) { setActionMsg(r && r.ok ? '⏹ 已终止 ' + (r.terminated || '') + ',任务重新排队' : '⚠️ ' + ((r && r.error) || '无活动 Agent')); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
704
784
|
function doDismiss() { rpc('dismiss-suspect', { taskId: task.id }).then(function () { setActionMsg('✅ 已清除卡死标记,继续观察'); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
@@ -709,6 +789,11 @@ function apply(ctx) {
|
|
|
709
789
|
React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
|
|
710
790
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 } }, React.createElement('span', { style: { fontSize: 10, padding: '1px 6px', borderRadius: 3, background: 'color-mix(in srgb, ' + (prioColor[task.priority] || prioColor.low) + ' 20%, transparent)', color: (prioColor[task.priority] || prioColor.low) } }, prioLabel[task.priority] || '中'), React.createElement('span', { style: { fontSize: 11, padding: '1px 8px', borderRadius: 3, background: C.nested, color: C.text } }, statusLabels[task.status] || task.status), React.createElement('select', { value: task.pipeline || 'full', onChange: function (e) { rpc('update-task', { taskId: task.id, pipeline: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '管线档位', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.text2 } }, React.createElement('option', { value: 'full' }, '全流程(执行+验证)'), React.createElement('option', { value: 'work' }, '免验证(只做不验)'), React.createElement('option', { value: 'direct' }, '主窗口处理')), task.pipelineAuto ? React.createElement('span', { style: { fontSize: 9, color: C.text2 }, title: '由规则自动分类,可手动覆盖' }, 'auto') : null, isManual ? React.createElement('span', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('user', 10), '手动派发') : null),
|
|
711
791
|
(task.status === 'in-progress' || task.status === 'verifying') && state.activity[task.id] ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginBottom: 8, padding: '4px 8px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'center', gap: 4, animation: 'tskb-pulse 2s ease-in-out infinite' } }, ic('activity', 10), React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '当前动作: ' + state.activity[task.id])) : null,
|
|
792
|
+
task.status === 'in-progress' && task.claimedBy ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 8, padding: '5px 8px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
793
|
+
ic('activity', 10),
|
|
794
|
+
React.createElement('span', null, '已运行 ' + elapsedSince(task.claimedAt) + '(软超时 ' + state.softTimeoutMin + ' 分提醒 · 硬超时 ' + state.hardTimeoutMin + ' 分自动终止)'),
|
|
795
|
+
elapsedMin(task.claimedAt) > state.softTimeoutMin ? React.createElement('span', { style: { color: C.warn, fontWeight: 600 } }, '⏱ 已超软超时') : null,
|
|
796
|
+
React.createElement('button', { onClick: doTerminate, title: '终止该执行 Agent,任务回 pending 重新排队', style: { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.err, borderRadius: 3, background: 'transparent', color: C.err, cursor: 'pointer', marginLeft: 'auto' } }, '⏹ 立即终止')) : null,
|
|
712
797
|
task.escalation ? React.createElement('div', { style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)' } },
|
|
713
798
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), 'Worker 上报歧义 — 等待主窗口裁决'),
|
|
714
799
|
React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, whiteSpace: 'pre-wrap', maxHeight: 160, overflowY: 'auto' } }, task.escalation.question),
|
|
@@ -723,10 +808,10 @@ function apply(ctx) {
|
|
|
723
808
|
React.createElement('button', { onClick: doTerminate, style: { fontSize: 11, padding: '4px 12px', border: 'none', borderRadius: 4, background: C.err, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('stop-circle', 12), '终止任务(重新排队)'),
|
|
724
809
|
React.createElement('button', { onClick: doDismiss, style: { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 4, background: 'transparent', color: C.text2, cursor: 'pointer' } }, '继续观察'),
|
|
725
810
|
canJump ? React.createElement('button', { onClick: jumpToAgent, style: { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.brand, borderRadius: 4, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '→ 查看会话') : null)) : null,
|
|
726
|
-
React.createElement('input', { value: editTitle, onChange: function (e) { setEditTitle(e.target.value) }, style: { width: '100%', fontSize: 13, fontWeight: 600, padding: '4px 6px', marginBottom: 6, border: '1px solid ' + C.
|
|
811
|
+
React.createElement('input', { value: editTitle, onChange: function (e) { setEditTitle(e.target.value) }, style: { width: '100%', fontSize: 13, fontWeight: 600, padding: '4px 6px', marginBottom: 6, border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, boxSizing: 'border-box' } }),
|
|
727
812
|
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 8, lineHeight: 1.7 } }, React.createElement('div', null, 'ID: ' + task.id), React.createElement('div', null, '创建: ' + fmtTime(task.createdAt) + '(' + ago(task.createdAt) + ')'), task.claimedBy ? React.createElement('div', null, '领取人: ', React.createElement(ActorLink, { id: task.claimedBy }), ' · ' + ago(task.claimedAt)) : null, task.resolvedAt ? React.createElement('div', null, '提交: ' + fmtTime(task.resolvedAt)) : null, task.verifiedAt ? React.createElement('div', null, '验收: ' + fmtTime(task.verifiedAt) + ' by ', React.createElement(ActorLink, { id: task.verifiedBy })) : null),
|
|
728
813
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 3 } }, '任务描述'),
|
|
729
|
-
React.createElement('textarea', { value: editDesc, onChange: function (e) { setEditDesc(e.target.value) }, rows: 3, style: { width: '100%', fontSize: 12, padding: '6px 8px', marginBottom: 6, border: '1px solid ' + C.
|
|
814
|
+
React.createElement('textarea', { value: editDesc, onChange: function (e) { setEditDesc(e.target.value) }, rows: 3, style: { width: '100%', fontSize: 12, padding: '6px 8px', marginBottom: 6, border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' } }),
|
|
730
815
|
task.context && task.context.instructions ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '指引: ' + task.context.instructions) : null,
|
|
731
816
|
task.context && Array.isArray(task.context.files) && task.context.files.length > 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'flex-start', gap: 4 } }, ic('file-text', 11), React.createElement('span', null, '预研文件(派发时注入子代理): ' + task.context.files.join('、'))) : null,
|
|
732
817
|
task.context && task.context.notes ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4, whiteSpace: 'pre-wrap', maxHeight: 100, overflowY: 'auto' } }, '调研笔记: ' + task.context.notes) : null,
|
|
@@ -752,13 +837,32 @@ function apply(ctx) {
|
|
|
752
837
|
task.status === 'verifying' && !task.escalation ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('dispatch-task', { taskId: task.id, role: 'verifier' }) }) }, title: '手动派发给一次性 Verifier 子代理', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.warn, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('zap', 10), '派发验收') : null,
|
|
753
838
|
task.status === 'verifying' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'approved' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('check-circle', 10), '通过') : null,
|
|
754
839
|
task.status === 'verifying' ? React.createElement('button', { onClick: function () { var r = window.prompt('驳回原因:'); doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'rejected', comment: r || '' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.err, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('x-circle', 10), '驳回') : null,
|
|
755
|
-
task.status === 'resolved' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('archive-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.text2, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('archive', 10), '归档') : null,
|
|
756
|
-
|
|
757
|
-
|
|
840
|
+
task.status === 'resolved' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('archive-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.text2, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('archive', 10), '归档') : null),
|
|
841
|
+
(function () {
|
|
842
|
+
// 历史会话:无论任务处于哪个阶段都完整列出(Worker 各次 + Verifier 各次)
|
|
843
|
+
var runs = historyRuns(task)
|
|
844
|
+
if (!runs.length) return null
|
|
845
|
+
var roleMeta = { worker: { label: 'W', color: C.brand, tip: 'Worker' }, verifier: { label: 'V', color: C.warn, tip: 'Verifier' } }
|
|
846
|
+
var wN = 0, vN = 0
|
|
847
|
+
return React.createElement('div', { style: { marginTop: 8, padding: '6px 8px', background: C.nested, borderRadius: 6 } },
|
|
848
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('users', 10), '历史会话(点击跳转查看,含全部重试)'),
|
|
849
|
+
React.createElement('div', { style: { display: 'flex', gap: 5, flexWrap: 'wrap' } },
|
|
850
|
+
runs.map(function (r, i) {
|
|
851
|
+
var rm = roleMeta[r.role] || roleMeta.worker
|
|
852
|
+
var seq = r.role === 'verifier' ? (++vN) : (++wN)
|
|
853
|
+
var isCur = (r.role === 'worker' && task.claimedBy === r.id) || (r.role === 'verifier' && task.verifierRun === r.id)
|
|
854
|
+
var okMark = r.outcome === 'completed' ? ' ✅' : (r.outcome === 'running' ? ' ⏳' : (r.outcome ? ' ⚠' : ''))
|
|
855
|
+
return React.createElement('button', {
|
|
856
|
+
key: i, onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
|
|
857
|
+
title: rm.tip + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + (r.outcome ? ' · ' + r.outcome : '') + '(' + r.id + ')',
|
|
858
|
+
style: { fontSize: 10, padding: '2px 8px', border: '1px solid ' + (isCur ? rm.color : C.border), borderRadius: 3, background: isCur ? C.card : 'transparent', color: rm.color, cursor: 'pointer', fontWeight: isCur ? 600 : 400 }
|
|
859
|
+
}, '→ ' + rm.tip + ' #' + seq + (r.at ? ' · ' + ago(r.at) : '') + okMark)
|
|
860
|
+
})))
|
|
861
|
+
})(),
|
|
758
862
|
canIntervene ? React.createElement('div', { style: { marginTop: 8, padding: '6px 8px', border: '1px dashed ' + C.warn, borderRadius: 6 } },
|
|
759
863
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('zap', 11), '高优先级介入(插入执行 Agent 队首)'),
|
|
760
864
|
React.createElement('div', { style: { display: 'flex', gap: 4 } },
|
|
761
|
-
React.createElement('input', { value: interveneMsg, onChange: function (e) { setInterveneMsg(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') submitIntervene() }, placeholder: '给执行中的 Agent 下达高优指令…', style: { flex: 1, fontSize: 11, padding: '4px 8px', border: '1px solid ' + C.
|
|
865
|
+
React.createElement('input', { value: interveneMsg, onChange: function (e) { setInterveneMsg(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') submitIntervene() }, placeholder: '给执行中的 Agent 下达高优指令…', style: { flex: 1, fontSize: 11, padding: '4px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text } }),
|
|
762
866
|
React.createElement('button', { onClick: submitIntervene, disabled: !interveneMsg.trim(), style: { fontSize: 11, padding: '4px 10px', border: 'none', borderRadius: 4, background: C.warn, color: C_INV, cursor: 'pointer', fontWeight: 600 } }, '介入'))) : null)
|
|
763
867
|
}
|
|
764
868
|
function TopPanel() {
|
|
@@ -766,7 +870,8 @@ function apply(ctx) {
|
|
|
766
870
|
var _a = useState(state.open), open = _a[0], setOpen = _a[1]; var _b = useState(state.tasks), tasks = _b[0], setTasksState = _b[1]; var _c = useState(state.boardMode), mode = _c[0], setModeState = _c[1]; var _d = useState(state.detailId), detailId = _d[0], setDetailId = _d[1]; var _f = useState(state.dragOver), dragOver = _f[0], setDragOver = _f[1]; var _g = useState(state.dispatchInfo), dispatchInfo = _g[0], setDispatchInfo = _g[1]; var _j = useState(state.view), view = _j[0], setViewState = _j[1]; var _k = useState(state.layoutLeft), layL = _k[0], setLayL = _k[1]; var _l = useState(state.layoutRight), layR = _l[0], setLayR = _l[1]
|
|
767
871
|
var _m = useState(state.minWorkers), minW = _m[0], setMinW = _m[1]; var _n = useState(state.maxWorkers), maxW = _n[0], setMaxW = _n[1]; var _o = useState(state.minVerifiers), minV = _o[0], setMinV = _o[1]; var _p = useState(state.maxVerifiers), maxV = _p[0], setMaxV = _p[1]; var _wm = useState(state.workerModel), workerModel = _wm[0], setWorkerModel = _wm[1]; var _vm = useState(state.verifierModel), verifierModel = _vm[0], setVerifierModel = _vm[1]
|
|
768
872
|
var _q = useState(state.teamMode), teamMode = _q[0], setTeamMode = _q[1]; var _dr = useState(state.dateRange), setDR = _dr[1]
|
|
769
|
-
|
|
873
|
+
var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
|
|
874
|
+
useEffect(function () { function update() { setOpen(state.open); setTasksState(state.tasks); setModeState(state.boardMode); setDetailId(state.detailId); setDragOver(state.dragOver); setDispatchInfo(state.dispatchInfo); setViewState(state.view); setLayL(state.layoutLeft); setLayR(state.layoutRight); setMinW(state.minWorkers); setMaxW(state.maxWorkers); setMinV(state.minVerifiers); setMaxV(state.maxVerifiers); setWorkerModel(state.workerModel); setVerifierModel(state.verifierModel); setTeamMode(state.teamMode); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
770
875
|
if (!open) return null
|
|
771
876
|
if (state.isRoot === false) return null // 子代理会话不渲染看板面板
|
|
772
877
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -792,7 +897,7 @@ function apply(ctx) {
|
|
|
792
897
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 12px', borderBottom: '1px solid ' + C.border, flexShrink: 0, flexWrap: 'wrap', gap: 4 } },
|
|
793
898
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } }, React.createElement('span', { style: { fontWeight: 600, fontSize: 13, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 5 } }, ic('clipboard-list', 15), '智能看板'), React.createElement(ViewTab, null), React.createElement(PoolStatus, null)),
|
|
794
899
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
795
|
-
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel }),
|
|
900
|
+
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT }),
|
|
796
901
|
dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
|
|
797
902
|
React.createElement('button', { onClick: function () { state.selectMode = !state.selectMode; if (!state.selectMode) state.selected = {}; notify() }, title: '多选批量操作', style: { fontSize: 11, padding: '3px 8px', border: '1px solid ' + (state.selectMode ? C.brand : C.border), borderRadius: 6, cursor: 'pointer', background: state.selectMode ? C.brand : 'transparent', color: state.selectMode ? C_INV : C.text2, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('square-check-big', 11), '多选'),
|
|
798
903
|
React.createElement(TeamSwitch, { on: teamMode }),
|
|
@@ -805,7 +910,7 @@ function apply(ctx) {
|
|
|
805
910
|
slots.inject('shell.overlay', function () { return slots.register({ name: 'shell.overlay', id: 'task-board-top-panel' }, function () { return React.createElement(TopPanel) }) })
|
|
806
911
|
}
|
|
807
912
|
|
|
808
|
-
module.exports = { name: 'dsh-agent-board', inject: ['slots', 'sessions', 'timer'], apply: apply }
|
|
913
|
+
module.exports = { name: 'dsh-agent-board', inject: ['slots', 'sessions', 'uiWorkspace', 'timer'], apply: apply }
|
|
809
914
|
return module.exports
|
|
810
915
|
}
|
|
811
916
|
})
|
package/lib/core.mjs
CHANGED
|
@@ -57,12 +57,18 @@ export function classifyPipeline(t) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// ===== 配置(一次性派发模型:max*=并发上限;min* 字段保留仅为兼容旧看板文件,引擎不使用)=====
|
|
60
|
-
|
|
60
|
+
// 两级超时:软超时(默认 30min)只上报主窗口提醒,不杀 run;硬超时(默认 120min)才兜底
|
|
61
|
+
// dispose 释放并发位——人在线时由人决策,人不在时系统兜底。
|
|
62
|
+
export function cfg(d) {
|
|
63
|
+
var soft = Math.max(1, Math.min(480, d.softTimeoutMin || 30))
|
|
64
|
+
var hard = Math.max(soft, Math.min(1440, d.hardTimeoutMin || 120))
|
|
65
|
+
return { minWorkers: Math.max(0, Math.min(10, d.minWorkers || 1)), maxWorkers: Math.max(1, Math.min(10, d.maxWorkers || 3)), minVerifiers: Math.max(0, Math.min(5, d.minVerifiers || 0)), maxVerifiers: Math.max(0, Math.min(5, d.maxVerifiers || 2)), softTimeoutMin: soft, hardTimeoutMin: hard }
|
|
66
|
+
}
|
|
61
67
|
|
|
62
68
|
// ===== 看板文件种子 =====
|
|
63
69
|
// poolStatus 必须始终在种子/归一化里存在:task_list 工具输出 poolStatus: d.poolStatus,
|
|
64
70
|
// 缺字段 = undefined → 工具结果的 lossless-JSON 校验会拒("value is not lossless JSON")
|
|
65
|
-
export function seed(sid) { return { version: 12, ownerSession: sid, boardMode: 'auto', teamMode: false, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', poolStatus: { workers: [], verifiers: [] }, tasks: [] } }
|
|
71
|
+
export function seed(sid) { return { version: 12, ownerSession: sid, boardMode: 'auto', teamMode: false, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, poolStatus: { workers: [], verifiers: [] }, tasks: [] } }
|
|
66
72
|
// 旧文件缺 poolStatus 的归一化(读路径兜底,保证任何历史文件都满足工具输出契约)
|
|
67
73
|
export function normalizeBoard(d) { if (d && typeof d === 'object') { if (!d.poolStatus || typeof d.poolStatus !== 'object' || !Array.isArray(d.poolStatus.workers) || !Array.isArray(d.poolStatus.verifiers)) d.poolStatus = { workers: [], verifiers: [] } }; return d }
|
|
68
74
|
|
|
@@ -156,7 +162,7 @@ export function buildVerifierPrompt(t, pack) {
|
|
|
156
162
|
export function pickDispatch(d, capW, capV, busyTaskIds) {
|
|
157
163
|
var pendings = capW > 0 ? d.tasks.filter(function (t) { return t.status === 'pending' && !t.claimedBy && t.assignMode !== 'manual' && t.pipeline !== 'direct' && depsSatisfied(d, t) && !t.escalation })
|
|
158
164
|
.sort(function (a, b) { var p = (PRIO_RANK[b.priority] || 2) - (PRIO_RANK[a.priority] || 2); return p !== 0 ? p : (a.createdAt || '').localeCompare(b.createdAt || '') }).slice(0, capW) : []
|
|
159
|
-
var verifs = capV > 0 ? d.tasks.filter(function (t) { return t.status === 'verifying' && (!t.pipeline || t.pipeline === 'full') && !t.escalation && !(busyTaskIds && busyTaskIds[t.id]) }).slice(0, capV) : []
|
|
165
|
+
var verifs = capV > 0 ? d.tasks.filter(function (t) { return t.status === 'verifying' && (!t.pipeline || t.pipeline === 'full') && !t.escalation && t.verifierRun !== 'spawn-pending' && !(busyTaskIds && busyTaskIds[t.id]) }).slice(0, capV) : []
|
|
160
166
|
return { pendings: pendings, verifs: verifs }
|
|
161
167
|
}
|
|
162
168
|
// 孤儿回收判定:in-progress 且 claimedBy 非主会话、无活跃 run、无 escalation、超 2 分钟
|
package/package.json
CHANGED
package/test/core.test.mjs
CHANGED
|
@@ -106,6 +106,23 @@ test('parseSections: 分段解析', () => {
|
|
|
106
106
|
assert.deepEqual(core.parseSections('没有分段的纯文本'), {})
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
+
test('cfg: 两级超时默认值/夹取/hard≥soft(软超时只提醒,硬超时兜底)', () => {
|
|
110
|
+
assert.equal(core.cfg({}).softTimeoutMin, 30)
|
|
111
|
+
assert.equal(core.cfg({}).hardTimeoutMin, 120)
|
|
112
|
+
assert.equal(core.cfg({ softTimeoutMin: 45, hardTimeoutMin: 240 }).softTimeoutMin, 45)
|
|
113
|
+
assert.equal(core.cfg({ softTimeoutMin: 0 }).softTimeoutMin, 30) // 0 → 默认
|
|
114
|
+
assert.equal(core.cfg({ softTimeoutMin: 9999 }).softTimeoutMin, 480) // 上限夹取
|
|
115
|
+
assert.equal(core.cfg({ hardTimeoutMin: 99999 }).hardTimeoutMin, 1440)
|
|
116
|
+
// hard 不得低于 soft,避免硬超时早于软提醒触发
|
|
117
|
+
assert.equal(core.cfg({ softTimeoutMin: 300, hardTimeoutMin: 60 }).hardTimeoutMin, 300)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('seed: 新看板自带两级超时默认值', () => {
|
|
121
|
+
const s = core.seed('session-x')
|
|
122
|
+
assert.equal(s.softTimeoutMin, 30)
|
|
123
|
+
assert.equal(s.hardTimeoutMin, 120)
|
|
124
|
+
})
|
|
125
|
+
|
|
109
126
|
test('parseSections: diff 概要分段解析(Worker 变更概要 → deliverable.diff)', () => {
|
|
110
127
|
const s = core.parseSections('## 开发描述\n做了\n## diff 概要\n gen.py | 3 + ++')
|
|
111
128
|
assert.equal(s.diff, 'gen.py | 3 + ++')
|
|
@@ -257,6 +274,19 @@ test('pickDispatch: verifying 只派 full 档,排除 escalation 和忙中', ()
|
|
|
257
274
|
assert.deepEqual(r.verifs.map(t => t.id), ['vf']) // work 档不派审、escalation 不派、忙中不派
|
|
258
275
|
})
|
|
259
276
|
|
|
277
|
+
test('pickDispatch: verifying spawn-pending 占位不重复派发(防双 Verifier)', () => {
|
|
278
|
+
const tasks = [
|
|
279
|
+
mkTask({ id: 'vp', status: 'verifying', pipeline: 'full', verifierRun: 'spawn-pending' }),
|
|
280
|
+
mkTask({ id: 'vd', status: 'verifying', pipeline: 'full', verifierRun: '12345' }), // 有真实 run id 但 run 已死(崩溃恢复场景)→ 应重派
|
|
281
|
+
mkTask({ id: 'vn', status: 'verifying', pipeline: 'full' }), // 从未派发 → 应派
|
|
282
|
+
]
|
|
283
|
+
const r = core.pickDispatch(mkBoard(tasks), 0, 5, null)
|
|
284
|
+
assert.deepEqual(r.verifs.map(t => t.id), ['vd', 'vn'])
|
|
285
|
+
// 占位 + 忙中双重排除
|
|
286
|
+
const r2 = core.pickDispatch(mkBoard(tasks), 0, 5, { vd: true })
|
|
287
|
+
assert.deepEqual(r2.verifs.map(t => t.id), ['vn'])
|
|
288
|
+
})
|
|
289
|
+
|
|
260
290
|
// ===== 孤儿回收 =====
|
|
261
291
|
test('isOrphan: 认领者非主会话 + 无活跃 run + 超 2 分钟', () => {
|
|
262
292
|
const old = new Date(Date.now() - 200000).toISOString()
|