dsh-agent-board 1.1.3 → 1.2.1
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/README.md +9 -2
- package/index.mjs +169 -24
- package/lib/client.js +324 -39
- package/lib/core.mjs +11 -4
- package/package.json +1 -2
- package/test/core.test.mjs +23 -0
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# dsh-agent-board
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-agent-board)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-agent-board)
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-agent-board)
|
|
6
|
+
[](https://github.com/PPawnsir/task-board-plugin/blob/main/LICENSE)
|
|
7
|
+

|
|
8
|
+
|
|
3
9
|
DeepSeek Harness 智能看板 — 让主窗口把编码工作变成可追踪、可验收、可审计的任务,而不是一段做完就散的对话。
|
|
4
10
|
|
|
5
11
|
## 背景与动机
|
|
@@ -50,5 +56,6 @@ dsh plugin --profile web add dsh-agent-board@latest
|
|
|
50
56
|
|
|
51
57
|
- 仓库:<https://github.com/PPawnsir/task-board-plugin>
|
|
52
58
|
- 本包即源码,直接维护(v68 起拆除了动态→静态转换层,v74 起去池化)
|
|
53
|
-
- 单元测试:`npm test`(node --test,
|
|
54
|
-
-
|
|
59
|
+
- 单元测试:`npm test`(node --test,39 例纯逻辑用例)
|
|
60
|
+
- E2E 回归:`npm run e2e -- --session <会话id>`(5 场景 21 断言,驱动真实实例)
|
|
61
|
+
- 改源码后重启 DSH 生效(host);`lib/client.js` 改动在 dsh ≥0.1.5-rc.2 下同样需重启(client bundle 启动时组合缓存)
|
package/index.mjs
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
// {name, description, parameters, output, execute} 普通对象,这里内联等价实现。
|
|
7
7
|
// parameters 已是完整 JSON Schema,原样透传;output 透传 schema+render。
|
|
8
8
|
import * as core from './lib/core.mjs'
|
|
9
|
+
import { zstdDecompressSync } from 'node:zlib'
|
|
10
|
+
import os from 'node:os'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import fsNode from 'node:fs'
|
|
9
13
|
const { ah, isb, gsb, gpt, vt, validateDeps, depsSatisfied, depsCancelled, classifyPipeline, seed, normalizeBoard, cfg, claimCheck, claimApply, checkParentAuto, resolveApply, verifyApply, parseSections, parseVerdict, isEscalation, outputText, histNotes, buildContextPackSection, buildWorkerPrompt, buildVerifierPrompt, pickDispatch, isOrphan, PRIO_RANK } = core
|
|
10
14
|
|
|
11
15
|
function defineTool(options) {
|
|
@@ -60,7 +64,7 @@ export function apply(ctx) {
|
|
|
60
64
|
var teamModeCache = {}
|
|
61
65
|
function fileFor(sid) { return '.dsh/tasks-' + sid + '.json' }
|
|
62
66
|
async function rt(sid) { try { var t = await fs.resolve(fileFor(sid)); var r = await fs.readText(t); var d = JSON.parse(r); if (vt(d) && d.ownerSession === sid) { teamModeCache[sid] = !!d.teamMode; return normalizeBoard(d) }; return seed(sid) } catch (_) { return seed(sid) } }
|
|
63
|
-
async function wt(sid, d) { var c = JSON.stringify(d
|
|
67
|
+
async function wt(sid, d) { var c = JSON.stringify(d); try { var t = await fs.resolve(fileFor(sid)); await fs.writeText(t, c) } catch (e) { console.error('[task-board] write:', String(e)); throw e } }
|
|
64
68
|
// 每会话一条 promise 链,串行化所有 读-改-写,消除并发写竞争
|
|
65
69
|
var fileLocks = {}
|
|
66
70
|
function withLock(sid, fn) { var prev = fileLocks[sid] || Promise.resolve(); var p = prev.then(function () { return fn() }); fileLocks[sid] = p.catch(function () {}); return p }
|
|
@@ -135,13 +139,40 @@ export function apply(ctx) {
|
|
|
135
139
|
var packByChild = {}
|
|
136
140
|
var pendingPacks = []
|
|
137
141
|
|
|
142
|
+
// 历史会话留档:t.runs = [{ role, id, at, model, outcome, endedAt }],上限 20 条
|
|
143
|
+
// 目的:任务流转到 resolved/archived 后,详情页仍能选择跳转到任一历史阶段的会话
|
|
144
|
+
// (Worker 首次/重试、Verifier 各次),而不是只剩最后一次 run id。
|
|
145
|
+
async function recordRunHistory(sid, taskId, role, runId, model, hardMin) {
|
|
146
|
+
try {
|
|
147
|
+
await mutateLocked(sid, function (d) {
|
|
148
|
+
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
149
|
+
if (!t) return
|
|
150
|
+
if (!Array.isArray(t.runs)) t.runs = []
|
|
151
|
+
t.runs.push({ role: role, id: runId, at: new Date().toISOString(), model: model || '', outcome: 'running', hardMin: hardMin || 120 })
|
|
152
|
+
if (t.runs.length > 20) t.runs = t.runs.slice(-20)
|
|
153
|
+
})
|
|
154
|
+
} catch (e) { console.error('[task-board] recordRunHistory failed:', String(e)) }
|
|
155
|
+
}
|
|
156
|
+
async function closeRunHistory(sid, taskId, runId, outcome) {
|
|
157
|
+
try {
|
|
158
|
+
await mutateLocked(sid, function (d) {
|
|
159
|
+
var t = d.tasks.find(function (x) { return x.id === taskId })
|
|
160
|
+
if (!t || !Array.isArray(t.runs)) return
|
|
161
|
+
for (var i = t.runs.length - 1; i >= 0; i--) {
|
|
162
|
+
if (t.runs[i].id === runId) { t.runs[i].outcome = outcome; t.runs[i].endedAt = new Date().toISOString(); break }
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
} catch (e) { console.error('[task-board] closeRunHistory failed:', String(e)) }
|
|
166
|
+
}
|
|
167
|
+
|
|
138
168
|
async function spawnOneShot(sid, t, role) {
|
|
139
169
|
var subagents = ctx.subagents; if (!subagents) return null
|
|
140
170
|
var parent = rootForSession(sid); if (!parent) { console.error('[task-board] no root agent for session ' + sid + ', skip spawn'); return null }
|
|
141
171
|
var providerName = pickProvider(); if (!providerName) { console.error('[task-board] no subagent provider'); return null }
|
|
142
172
|
var modelOverride = ''
|
|
143
|
-
|
|
144
|
-
|
|
173
|
+
var dsnap = await rt(sid)
|
|
174
|
+
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 = '' } }
|
|
175
|
+
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 = '' } }
|
|
145
176
|
var pack = ''
|
|
146
177
|
try { pack = await readContextPack(t) } catch (e) { console.error('[task-board] context pack read failed:', String(e)) }
|
|
147
178
|
// user prompt 只留一行指引,内容走上下文注入区块
|
|
@@ -163,13 +194,32 @@ export function apply(ctx) {
|
|
|
163
194
|
} finally {
|
|
164
195
|
if (ppEntry) { var ppi = pendingPacks.indexOf(ppEntry); if (ppi >= 0) pendingPacks.splice(ppi, 1) }
|
|
165
196
|
}
|
|
166
|
-
var
|
|
197
|
+
var c = cfg(dsnap)
|
|
198
|
+
var rec = { run: run, role: role, taskId: t.id, startedAt: Date.now(), model: modelOverride, settled: false }
|
|
167
199
|
runsFor(sid)[t.id] = rec
|
|
168
200
|
if (pack) packByChild[String(run.id)] = pack
|
|
169
201
|
if (!dispatchedEver[sid]) dispatchedEver[sid] = {}
|
|
170
202
|
dispatchedEver[sid][String(run.id)] = true
|
|
171
|
-
//
|
|
172
|
-
|
|
203
|
+
// 历史会话留档:每次派发都追加一条 {role,id,at,model},任务完成后仍可回看
|
|
204
|
+
// 各阶段(含重试的第 1/2/3 次 Worker)会话——否则 claimedBy/verifierRun 只留最后一次
|
|
205
|
+
recordRunHistory(sid, t.id, role, String(run.id), modelOverride, c.hardTimeoutMin).catch(function () {})
|
|
206
|
+
// ===== 两级超时:软超时只提醒主窗口(由人决定继续等待或终止),硬超时兜底 dispose =====
|
|
207
|
+
// 一次性 run 没有看门狗:完全依赖人工决策时,人不在线挂死的 run 会永久占用并发位,
|
|
208
|
+
// 所以保留硬上限作为最后防线(默认 120min,可配置)。
|
|
209
|
+
var startedAt = rec.startedAt
|
|
210
|
+
var softMs = c.softTimeoutMin * 60000
|
|
211
|
+
var hardMs = c.hardTimeoutMin * 60000
|
|
212
|
+
function finish(res, err) { if (rec.settled) return; rec.settled = true; settleRun(sid, rec, res, err) }
|
|
213
|
+
;(function softArm() {
|
|
214
|
+
var tm = ctx.timer; if (!tm) return
|
|
215
|
+
tm.timeout(softMs).then(function () {
|
|
216
|
+
if (rec.settled) return
|
|
217
|
+
var mins = Math.round((Date.now() - startedAt) / 60000)
|
|
218
|
+
pushSysNote(sid, '⏱ 任务「' + t.title + '」的 ' + role + '(' + t.id + ')已运行 ' + mins + ' 分钟仍未完成——如属正常长任务可忽略;需要干预可在看板详情页「立即终止」(硬超时 ' + c.hardTimeoutMin + ' 分钟后将自动终止并重试)')
|
|
219
|
+
softArm() // 持续提醒直到结算或硬超时
|
|
220
|
+
}).catch(function () {})
|
|
221
|
+
})()
|
|
222
|
+
withTimeout(run.result, hardMs, role + ':' + t.id).then(function (res) { finish(res, null) }).catch(function (e) { finish(null, e) })
|
|
173
223
|
return rec
|
|
174
224
|
}
|
|
175
225
|
|
|
@@ -186,6 +236,8 @@ export function apply(ctx) {
|
|
|
186
236
|
if (rec.role === 'worker') await settleWorker(sid, rec, output, failed, errText)
|
|
187
237
|
else await settleVerifier(sid, rec, output, failed, errText)
|
|
188
238
|
} catch (e) { console.error('[task-board] settle ' + rec.role + ' failed (task ' + rec.taskId + '):', String(e)) }
|
|
239
|
+
// 历史会话留档:记录该次 run 的结局(完成/失败/硬超时),详情页可据此标注阶段状态
|
|
240
|
+
closeRunHistory(sid, rec.taskId, String(rec.run.id), failed ? (err ? 'timeout/error' : 'incomplete') : 'completed').catch(function () {})
|
|
189
241
|
}
|
|
190
242
|
|
|
191
243
|
async function settleWorker(sid, rec, output, failed, errText) {
|
|
@@ -210,7 +262,7 @@ export function apply(ctx) {
|
|
|
210
262
|
// 文本降级路径:分段格式上报
|
|
211
263
|
var secs = parseSections(output)
|
|
212
264
|
delete t.retryCount; delete t.stuckSince
|
|
213
|
-
t.deliverable = { summary: secs.summary || output.slice(0, 600), changes: secs.changes || '', selfTest: secs.selfTest || '', at: new Date().toISOString(), by: String(rec.run.id) }
|
|
265
|
+
t.deliverable = { summary: secs.summary || output.slice(0, 600), changes: secs.changes || '', selfTest: secs.selfTest || '', diff: (secs.diff || '').slice(0, 4000), at: new Date().toISOString(), by: String(rec.run.id) }
|
|
214
266
|
resolveApply(d, t, String(rec.run.id), 'verifying', output || 'Worker 完成', 'worker 文本上报完成')
|
|
215
267
|
return { task: t }
|
|
216
268
|
})
|
|
@@ -232,7 +284,7 @@ export function apply(ctx) {
|
|
|
232
284
|
var vm = trimmed.match(/^[ \t>*#\-\s]*(APPROVED|REJECTED)\b/im)
|
|
233
285
|
if (failed || !vm) {
|
|
234
286
|
// 失败/空输出/无法判定:verifyRetries 计数,>=3 转人工验收(deliverable 已完成,是 verifier 故障不是任务故障)
|
|
235
|
-
if (failed && rec.model) { badModels[modelKey(sid, rec.model)] = true }
|
|
287
|
+
if (failed && rec.model) { badModels[modelKey(sid, rec.model)] = true; pushSysNote(sid, '模型 ' + rec.model + ' 验收连续失败,已熔断回退父级模型') }
|
|
236
288
|
t.verifyRetries = (t.verifyRetries || 0) + 1
|
|
237
289
|
if (t.verifyRetries >= 3) { t.escalation = { question: 'Verifier 连续 ' + t.verifyRetries + ' 次未能给出有效结论(' + (failed ? String(errText).slice(0, 150) : '输出格式异常') + ')。交付物已完成,请人工验收:看板详情页直接通过/驳回,或 task_verify 裁决。', at: new Date().toISOString(), by: 'system' }; ah(t, 'verifying', 'verifying', 'system', 'verifier 故障,转人工验收'); return { task: t, escalated: true } }
|
|
238
290
|
ah(t, 'verifying', 'verifying', 'system', 'verifier 未给出有效结论,重新排队审查 (' + t.verifyRetries + '/3)')
|
|
@@ -295,14 +347,23 @@ export function apply(ctx) {
|
|
|
295
347
|
else flushReceipts(sid)
|
|
296
348
|
}
|
|
297
349
|
}
|
|
350
|
+
// 系统级异常通知队列(易失,随回执冲刷):模型熔断/spawn 失败/孤儿回收/看门狗标记
|
|
351
|
+
var sysNotesBuf = {}
|
|
352
|
+
function pushSysNote(sid, text) {
|
|
353
|
+
var arr = (sysNotesBuf[sid] = sysNotesBuf[sid] || [])
|
|
354
|
+
arr.push({ text: text, at: new Date().toISOString() })
|
|
355
|
+
if (arr.length > 10) arr.splice(0, arr.length - 10)
|
|
356
|
+
}
|
|
298
357
|
function flushReceipts(sid) {
|
|
299
|
-
var buf = receiptBuf[sid]
|
|
358
|
+
var buf = receiptBuf[sid]
|
|
359
|
+
var notes = sysNotesBuf[sid] || []
|
|
360
|
+
if ((!buf || !buf.items.length) && !notes.length) return
|
|
300
361
|
receiptBuf[sid] = null
|
|
301
|
-
|
|
362
|
+
sysNotesBuf[sid] = []
|
|
302
363
|
var root = rootForSession(sid); if (!root) return
|
|
303
364
|
var done = [], blocked = []
|
|
304
|
-
for (var i = 0; i < buf.items.length; i++) { (buf.items[i].kind === 'resolved' ? done : blocked).push(buf.items[i]) }
|
|
305
|
-
var lines = ['📋 [任务看板] 回执摘要(' + buf.items.length + ' 条)', '']
|
|
365
|
+
if (buf) for (var i = 0; i < buf.items.length; i++) { (buf.items[i].kind === 'resolved' ? done : blocked).push(buf.items[i]) }
|
|
366
|
+
var lines = [done.length || blocked.length ? '📋 [任务看板] 回执摘要(' + buf.items.length + ' 条)' : '📋 [任务看板] 系统通知', '']
|
|
306
367
|
if (done.length) {
|
|
307
368
|
lines.push('✅ 完成 ' + done.length + ' 个:')
|
|
308
369
|
for (var j = 0; j < done.length && j < 8; j++) lines.push(' · ' + done[j].title + ' (' + done[j].id + ')' + (done[j].summary ? ' — ' + done[j].summary.slice(0, 120) : ''))
|
|
@@ -311,6 +372,10 @@ export function apply(ctx) {
|
|
|
311
372
|
lines.push('🛑 阻塞 ' + blocked.length + ' 个(需关注):')
|
|
312
373
|
for (var k = 0; k < blocked.length && k < 8; k++) lines.push(' · ' + blocked[k].title + ' (' + blocked[k].id + ')' + (blocked[k].note ? ' — ' + blocked[k].note.slice(0, 150) : ''))
|
|
313
374
|
}
|
|
375
|
+
if (notes.length) {
|
|
376
|
+
lines.push('', '⚠️ 系统异常 ' + notes.length + ' 条:')
|
|
377
|
+
for (var n = 0; n < notes.length && n < 8; n++) lines.push(' · ' + notes[n].text)
|
|
378
|
+
}
|
|
314
379
|
lines.push('', '可用 task_list 查看全部;阻塞项可在看板拖回待办重新投放。')
|
|
315
380
|
var text = lines.join('\n')
|
|
316
381
|
function send() { try { root.followup(makeMsg(text, 'recall')) } catch (e) { console.error('[task-board] receipt flush failed:', String(e)) } }
|
|
@@ -329,7 +394,14 @@ export function apply(ctx) {
|
|
|
329
394
|
Object.keys(runs).forEach(function (k) { if (runs[k].role === 'worker') activeW++; else activeV++ })
|
|
330
395
|
// 空闲快进:无活跃任务且无活跃 run → 不写盘直接返回(心跳每 15s 跑一次,不能每次都写文件)
|
|
331
396
|
var hasActive = snap.tasks.some(function (t) { return t.status === 'pending' || t.status === 'verifying' || t.status === 'in-progress' })
|
|
332
|
-
if (!hasActive && activeW + activeV === 0) {
|
|
397
|
+
if (!hasActive && activeW + activeV === 0) {
|
|
398
|
+
var emptyPool = { workers: [], verifiers: [] }
|
|
399
|
+
// 快进不写盘的前提是磁盘上的 poolStatus 已经是空的——否则(典型:DSH 重启清空了
|
|
400
|
+
// 内存 runs 表,文件里残留重启前的忙碌快照)幽灵 Worker 会永远显示执行中
|
|
401
|
+
var stalePool = snap.poolStatus && (((snap.poolStatus.workers || []).length + (snap.poolStatus.verifiers || []).length) > 0)
|
|
402
|
+
if (stalePool) { snap.poolStatus = emptyPool; try { await wt(sid, snap) } catch (_) {} }
|
|
403
|
+
return snap
|
|
404
|
+
}
|
|
333
405
|
var c = cfg(snap)
|
|
334
406
|
var isAuto = (snap.boardMode || 'auto') === 'auto'
|
|
335
407
|
|
|
@@ -339,7 +411,7 @@ export function apply(ctx) {
|
|
|
339
411
|
var result = await mutateLocked(sid, function (d) {
|
|
340
412
|
var now = Date.now()
|
|
341
413
|
d.tasks.forEach(function (t) {
|
|
342
|
-
if (isOrphan(d, t, runs, now)) { t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; ah(t, 'in-progress', 'pending', 'system', '执行 run 已结束/丢失,回收重新排队'); info.push('reclaim ' + t.id) }
|
|
414
|
+
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) }
|
|
343
415
|
})
|
|
344
416
|
// worker 派发仅 auto;verifier 派发两种模式都跑(manual 模式主窗口 claim 做完的 full 档任务需要验收)
|
|
345
417
|
var capW = isAuto ? Math.max(0, c.maxWorkers - activeW) : 0
|
|
@@ -363,6 +435,7 @@ export function apply(ctx) {
|
|
|
363
435
|
} else if (sp.role === 'worker') {
|
|
364
436
|
// spawn 失败 → 回 pending(verifier spawn 失败无需处理,下轮 cycle 会重试)
|
|
365
437
|
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)
|
|
438
|
+
pushSysNote(sid, '任务「' + sp.t.title + '」Worker 启动失败,已重新排队')
|
|
366
439
|
}
|
|
367
440
|
}
|
|
368
441
|
return result
|
|
@@ -403,9 +476,8 @@ export function apply(ctx) {
|
|
|
403
476
|
var agent = assembleCtx && assembleCtx.agent
|
|
404
477
|
if (!agent) return ''
|
|
405
478
|
var aid = String(agent.id)
|
|
406
|
-
console.error('[task-board][ctxpack] assemble agent=' + aid + ' packKeys=' + Object.keys(packByChild).join(',') + ' pending=' + pendingPacks.length)
|
|
407
479
|
var hit = packByChild[aid]
|
|
408
|
-
if (hit)
|
|
480
|
+
if (hit) return hit
|
|
409
481
|
// 首轮竞速自愈:start() 返回前的首次组装按父子归属从 pendingPacks 认领
|
|
410
482
|
var agentsSvc = ctx.agents
|
|
411
483
|
if (!agentsSvc) return ''
|
|
@@ -414,10 +486,8 @@ export function apply(ctx) {
|
|
|
414
486
|
var pp = pendingPacks[i]
|
|
415
487
|
if (now - pp.at > 60000) { pendingPacks.splice(i, 1); continue }
|
|
416
488
|
try {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (owned) { packByChild[aid] = pp.pack; return pp.pack }
|
|
420
|
-
} catch (e) { console.error('[task-board][ctxpack] isOwnedBy threw: ' + String(e)) }
|
|
489
|
+
if (agentsSvc.isOwnedBy(aid, pp.parent)) { packByChild[aid] = pp.pack; return pp.pack }
|
|
490
|
+
} catch (e) { console.error('[task-board] ctxpack isOwnedBy threw: ' + String(e)) }
|
|
421
491
|
}
|
|
422
492
|
return ''
|
|
423
493
|
},
|
|
@@ -428,6 +498,7 @@ export function apply(ctx) {
|
|
|
428
498
|
// ===== Tools =====
|
|
429
499
|
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 } }))
|
|
430
500
|
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 || {} } } } }))
|
|
501
|
+
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) } } } }))
|
|
431
502
|
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 || {} } } }) } }))
|
|
432
503
|
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) }) } }))
|
|
433
504
|
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) }) } }))
|
|
@@ -438,7 +509,81 @@ export function apply(ctx) {
|
|
|
438
509
|
// ===== RPC =====
|
|
439
510
|
// get-tasks 是纯读路径(rt 只读文件)——poolCycle 由 15s 心跳 + 写入后 kickCycle 驱动,
|
|
440
511
|
// 客户端 3s 轮询不再触发池计算/写盘(之前每轮询一次就 poolCycle+写盘一次,切会话时多会话轮询挤在文件锁上)
|
|
441
|
-
handle('get-tasks', async function (args) { var sid = rpcSessionId(args); var d = await rt(sid); d.sessionId = sid; var __ag = ctx.agents; d.isRoot = true; if (__ag) { var __roots = __ag.roots(); var __rids = []; for (var __i = 0; __i < __roots.length; __i++) __rids.push(String(__roots[__i].id)); d.isRoot = __rids.indexOf(sid) >= 0 }
|
|
512
|
+
handle('get-tasks', async function (args) { var sid = rpcSessionId(args); var d = await rt(sid); d.sessionId = sid; var __ag = ctx.agents; d.isRoot = true; if (__ag) { var __roots = __ag.roots(); var __rids = []; for (var __i = 0; __i < __roots.length; __i++) __rids.push(String(__roots[__i].id)); d.isRoot = __rids.indexOf(sid) >= 0 }
|
|
513
|
+
// poolStatus 防幽灵:只保留指向当前活跃任务的条目(重启后内存 runs 清空,文件快照可能残留)
|
|
514
|
+
if (d.poolStatus) { var __act = {}; (d.tasks || []).forEach(function (t) { if (t.status === 'in-progress' || t.status === 'verifying') __act[t.id] = true }); d.poolStatus.workers = (d.poolStatus.workers || []).filter(function (w) { return __act[w.taskId] }); d.poolStatus.verifiers = (d.poolStatus.verifiers || []).filter(function (v) { return __act[v.taskId] }) }
|
|
515
|
+
return d })
|
|
516
|
+
// 派发前上下文预览:主 agent 用它确认"我将注入给子代理的材料"是否足够(不发任务、不落盘)
|
|
517
|
+
handle('preview-context', async function (args) {
|
|
518
|
+
var files = Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []
|
|
519
|
+
var notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''
|
|
520
|
+
if (!files.length && !notes.trim()) return { ok: false, error: 'contextFiles/contextNotes 至少提供一个' }
|
|
521
|
+
var pack = ''
|
|
522
|
+
try { pack = await readContextPack({ context: { files: files, notes: notes } }) } catch (e) { return { ok: false, error: '读取失败: ' + String(e).slice(0, 120) } }
|
|
523
|
+
return { ok: true, filesCount: files.length, notesLen: notes.length, pack: pack, empty: !pack }
|
|
524
|
+
})
|
|
525
|
+
// 活动心跳:读子代理会话日志的最后一帧,提取最近的动作摘要(卡片/详情页展示"现在跑到哪了")
|
|
526
|
+
handle('agent-activity', async function (args) {
|
|
527
|
+
var sid = rpcSessionId(args)
|
|
528
|
+
var rec = runsFor(sid)[args.taskId]
|
|
529
|
+
if (!rec || !rec.run) return { ok: true, activity: null, reason: 'no active run' }
|
|
530
|
+
var child = String(rec.run.id)
|
|
531
|
+
try {
|
|
532
|
+
var boardAbs = await fs.resolve(fileFor(sid))
|
|
533
|
+
var ws = path.dirname(path.dirname(boardAbs))
|
|
534
|
+
var bucket = '--' + ws.replace(/[\\/:]/g, '-') + '--'
|
|
535
|
+
var log = path.join(os.homedir(), '.dsh', 'sessions', bucket, child, 'session.jsonl.zstd')
|
|
536
|
+
if (!fsNode.existsSync(log)) return { ok: true, activity: null, reason: 'log not found' }
|
|
537
|
+
// 只同步读末尾 2MB(日志追加写,末帧必在尾部)——整文件 readFileSync 在大日志上
|
|
538
|
+
// 会造成数十毫秒级同步 I/O,多任务轮询时叠加成全局卡顿
|
|
539
|
+
var sz = fsNode.statSync(log).size
|
|
540
|
+
var TAIL = 2 * 1024 * 1024
|
|
541
|
+
var start = sz > TAIL ? sz - TAIL : 0
|
|
542
|
+
var buf = Buffer.alloc(sz - start)
|
|
543
|
+
var fd = fsNode.openSync(log, 'r')
|
|
544
|
+
try { fsNode.readSync(fd, buf, 0, buf.length, start) } finally { fsNode.closeSync(fd) }
|
|
545
|
+
var MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
|
546
|
+
var last = buf.lastIndexOf(MAGIC) // 追加写多帧格式:最新事件在末帧,只解一帧控成本
|
|
547
|
+
if (last < 0) return { ok: true, activity: null }
|
|
548
|
+
var text = zstdDecompressSync(buf.subarray(last)).toString('utf8')
|
|
549
|
+
var lines = text.split('\n').filter(Boolean)
|
|
550
|
+
var activity = null
|
|
551
|
+
for (var i = lines.length - 1; i >= 0 && !activity; i--) {
|
|
552
|
+
try {
|
|
553
|
+
var e = JSON.parse(lines[i])
|
|
554
|
+
var dta = e.data || {}
|
|
555
|
+
if (e.type === 'tool/call' && dta.name) activity = '🔧 ' + dta.name + ' ' + String(dta.arguments || '').replace(/\s+/g, ' ').slice(0, 90)
|
|
556
|
+
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)
|
|
557
|
+
} catch (_) {}
|
|
558
|
+
}
|
|
559
|
+
return { ok: true, activity: activity }
|
|
560
|
+
} catch (e) { return { ok: true, activity: null, reason: String(e).slice(0, 80) } }
|
|
561
|
+
})
|
|
562
|
+
// 全局多会话总览:聚合本机所有看板的任务计数(只读,供 dashboard 跨会话视图)
|
|
563
|
+
handle('list-boards', async function (args) {
|
|
564
|
+
var home = path.join(os.homedir(), '.dsh')
|
|
565
|
+
var out = []
|
|
566
|
+
var files
|
|
567
|
+
try { files = fsNode.readdirSync(home).filter(function (f) { return f.indexOf('tasks-') === 0 && f.slice(-5) === '.json' }) } catch (e) { return { ok: true, boards: [] } }
|
|
568
|
+
for (var i = 0; i < files.length; i++) {
|
|
569
|
+
try {
|
|
570
|
+
var d = JSON.parse(fsNode.readFileSync(path.join(home, files[i]), 'utf8'))
|
|
571
|
+
if (!vt(d)) continue
|
|
572
|
+
var counts = { pending: 0, inProgress: 0, verifying: 0, resolved: 0, blocked: 0 }
|
|
573
|
+
var titles = []
|
|
574
|
+
var lastTs = ''
|
|
575
|
+
d.tasks.forEach(function (t) {
|
|
576
|
+
if (t.status === 'archived') return
|
|
577
|
+
if (counts[t.status] !== undefined) counts[t.status]++
|
|
578
|
+
if (t.status === 'in-progress' || t.status === 'verifying' || t.status === 'blocked') titles.push(t.title)
|
|
579
|
+
;(t.history || []).forEach(function (h) { if (h.timestamp > lastTs) lastTs = h.timestamp })
|
|
580
|
+
})
|
|
581
|
+
out.push({ session: d.ownerSession, boardMode: d.boardMode || 'auto', teamMode: !!d.teamMode, counts: counts, activeTitles: titles.slice(0, 3), lastActivity: lastTs })
|
|
582
|
+
} catch (_) {}
|
|
583
|
+
}
|
|
584
|
+
out.sort(function (a, b) { return (b.lastActivity || '').localeCompare(a.lastActivity || '') })
|
|
585
|
+
return { ok: true, boards: out }
|
|
586
|
+
})
|
|
442
587
|
handle('claim-task', async function (args) { var sid = rpcSessionId(args); 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, 'manual claim via board'); return { ok: true, task: t } }) })
|
|
443
588
|
handle('resolve-task', async function (args) { var sid = rpcSessionId(args); 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' }; return resolveApply(d, t, actor, args.status, args.resolution, args.resolution || args.status) }) })
|
|
444
589
|
handle('verify-task', async function (args) { var sid = rpcSessionId(args); 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' }; delete t.escalation; delete t.verifyRetries; return verifyApply(d, t, actor, args.verdict, args.comment) }) })
|
|
@@ -532,7 +677,7 @@ export function apply(ctx) {
|
|
|
532
677
|
ctx.tools.register(defineTool({ name: 'task_arbitrate', description: 'Team 模式:裁决 Worker 上报的歧义(escalation)。答案直接转达给原 Worker 继续执行。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, answer: { type: 'string' } }, required: ['taskId', 'answer'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; return doResolveEscalation(toolSessionId(), getActorId(), args.taskId, args.answer) } }))
|
|
533
678
|
ctx.tools.register(defineTool({ name: 'task_intervene', description: 'Team 模式:向执行某任务的池中 Agent 发起高优先级指令(插入其队列头部,当前 turn 结束后优先处理)。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, message: { type: 'string' } }, required: ['taskId', 'message'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; return doIntervene(toolSessionId(), getActorId(), args.taskId, args.message) } }))
|
|
534
679
|
// ===== 池中 Agent 结构化回报工具(双模:工具优先,文本分段为降级路径)=====
|
|
535
|
-
ctx.tools.register(defineTool({ name: 'board_report', description: '[任务看板 Worker 专用] 上报任务结果。kind=complete 时填 summary/changes/selfTest
|
|
680
|
+
ctx.tools.register(defineTool({ name: 'board_report', description: '[任务看板 Worker 专用] 上报任务结果。kind=complete 时填 summary/changes/selfTest/diffStat(git 仓库内改动附 git diff --stat 概要);kind=escalate 时填 question(歧义上报,等待主窗口裁决)。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, kind: { type: 'string', enum: ['complete', 'escalate'] }, summary: { type: 'string' }, changes: { type: 'string' }, selfTest: { type: 'string' }, diffStat: { type: 'string', description: '变更概要:git diff --stat(含 git status --short)输出,≤1500 字符' }, question: { type: 'string' } }, required: ['taskId', 'kind'] }, output: jo(), execute: async function (args) {
|
|
536
681
|
var sid = toolSessionId(); var actor = getActorId()
|
|
537
682
|
var result = await mutateLocked(sid, function (d) {
|
|
538
683
|
var t = d.tasks.find(function (x) { return x.id === args.taskId })
|
|
@@ -549,7 +694,7 @@ export function apply(ctx) {
|
|
|
549
694
|
delete t.escalation
|
|
550
695
|
delete t.retryCount // v72:成功完成清零超时重试计数(工具直报路径)
|
|
551
696
|
delete t.stuckSince
|
|
552
|
-
t.deliverable = { summary: args.summary || '', changes: args.changes || '', selfTest: args.selfTest || '', at: new Date().toISOString(), by: actor }
|
|
697
|
+
t.deliverable = { summary: args.summary || '', changes: args.changes || '', selfTest: args.selfTest || '', diff: (args.diffStat || '').slice(0, 4000), at: new Date().toISOString(), by: actor }
|
|
553
698
|
var r = resolveApply(d, t, actor, 'verifying', (args.summary || '') + (args.selfTest ? '\n\n自测: ' + args.selfTest.slice(0, 300) : ''), 'worker 工具上报完成')
|
|
554
699
|
r.ok = true; r.task = t
|
|
555
700
|
return r
|
|
@@ -595,7 +740,7 @@ export function apply(ctx) {
|
|
|
595
740
|
}
|
|
596
741
|
return { ok: true, models: out }
|
|
597
742
|
})
|
|
598
|
-
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 } }) })
|
|
743
|
+
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 } }) })
|
|
599
744
|
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 } }) })
|
|
600
745
|
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) } } })
|
|
601
746
|
// ===== #14 批量操作:archive(仅 resolved/cancelled)/ set-priority(全部)=====
|
package/lib/client.js
CHANGED
|
@@ -17,6 +17,7 @@ function apply(ctx) {
|
|
|
17
17
|
if (slots === undefined) return
|
|
18
18
|
var sessionsSvc = ctx.get('sessions')
|
|
19
19
|
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)', 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
|
+
var C_INV = 'var(--dsw-alias-label-primary-inverted)' // 品牌底/彩色底上的反色文字(深浅模式自动反转)
|
|
20
21
|
var prioColor = { critical: C.err, high: C.warn, medium: C.brand, low: C.text2 }
|
|
21
22
|
var prioLabel = { critical: '紧急', high: '高', medium: '中', low: '低' }
|
|
22
23
|
var statusLabels = { draft: '草稿', pending: '待办', 'in-progress': '进行中', verifying: '验证中', resolved: '已完成', blocked: '阻塞', cancelled: '已取消', archived: '已归档' }
|
|
@@ -120,6 +121,28 @@ function apply(ctx) {
|
|
|
120
121
|
['path', { d: 'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2' }],
|
|
121
122
|
['circle', { cx: 12, cy: 7, r: 4 }]
|
|
122
123
|
],
|
|
124
|
+
'calendar-days': [
|
|
125
|
+
['rect', { x: 3, y: 4, width: 18, height: 18, rx: 2 }],
|
|
126
|
+
['path', { d: 'M16 2v4M8 2v4M3 10h18' }],
|
|
127
|
+
['path', { d: 'M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01' }]
|
|
128
|
+
],
|
|
129
|
+
'chevron-up': [
|
|
130
|
+
['path', { d: 'm18 15-6-6-6 6' }]
|
|
131
|
+
],
|
|
132
|
+
'layout-grid': [
|
|
133
|
+
['rect', { x: 3, y: 3, width: 7, height: 7, rx: 1 }],
|
|
134
|
+
['rect', { x: 14, y: 3, width: 7, height: 7, rx: 1 }],
|
|
135
|
+
['rect', { x: 3, y: 14, width: 7, height: 7, rx: 1 }],
|
|
136
|
+
['rect', { x: 14, y: 14, width: 7, height: 7, rx: 1 }]
|
|
137
|
+
],
|
|
138
|
+
'activity': [
|
|
139
|
+
['path', { d: 'M22 12h-4l-3 9L9 3l-3 9H2' }]
|
|
140
|
+
],
|
|
141
|
+
'file-down': [
|
|
142
|
+
['path', { d: 'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z' }],
|
|
143
|
+
['path', { d: 'M14 2v4a2 2 0 0 0 2 2h4' }],
|
|
144
|
+
['path', { d: 'M12 18v-6M9 15l3 3 3-3' }]
|
|
145
|
+
],
|
|
123
146
|
'chevron-down': [
|
|
124
147
|
['path', { d: 'm6 9 6 6 6-6' }]
|
|
125
148
|
],
|
|
@@ -176,7 +199,8 @@ function apply(ctx) {
|
|
|
176
199
|
}, def.map(function (p, i) { return React.createElement(p[0], Object.assign({ key: i }, p[1])) }))
|
|
177
200
|
}
|
|
178
201
|
var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
|
|
179
|
-
var
|
|
202
|
+
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
203
|
+
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: [] }
|
|
180
204
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
181
205
|
try {
|
|
182
206
|
var onKey = function (e) {
|
|
@@ -199,7 +223,9 @@ function apply(ctx) {
|
|
|
199
223
|
function rpc(method, args) { var a = args || {}; a.sessionId = state.sessionId; return fetch('/dsh-agent-board', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: method, args: a }) }).then(function (r) { return r.json() }) }
|
|
200
224
|
function fetchTasks() {
|
|
201
225
|
if (!state.sessionId) return
|
|
226
|
+
var epoch = reqEpoch
|
|
202
227
|
rpc('get-tasks').then(function (d) {
|
|
228
|
+
if (epoch !== reqEpoch) return // 会话已切换,丢弃过期响应
|
|
203
229
|
state.tasks = (d && d.tasks) || []
|
|
204
230
|
state.boardMode = (d && d.boardMode) || 'auto'
|
|
205
231
|
state.teamMode = !!(d && d.teamMode)
|
|
@@ -209,6 +235,8 @@ function apply(ctx) {
|
|
|
209
235
|
state.maxVerifiers = (d && d.maxVerifiers) || 2
|
|
210
236
|
state.verifierModel = (d && d.verifierModel) || ''
|
|
211
237
|
state.workerModel = (d && d.workerModel) || ''
|
|
238
|
+
state.softTimeoutMin = (d && d.softTimeoutMin) || 30
|
|
239
|
+
state.hardTimeoutMin = (d && d.hardTimeoutMin) || 120
|
|
212
240
|
state.poolStatus = (d && d.poolStatus) || null
|
|
213
241
|
state.isRoot = !d || d.isRoot !== false
|
|
214
242
|
if (state.isRoot === false && state.open) { state.open = false; state.detailId = null } // 子代理会话:强制收起看板
|
|
@@ -220,10 +248,37 @@ function apply(ctx) {
|
|
|
220
248
|
notify()
|
|
221
249
|
}).catch(function () {})
|
|
222
250
|
}
|
|
223
|
-
function fetchChildren() { if (!state.sessionId) return; rpc('list-children').then(function (d) { state.children = (d && d.children) || []; notify() }).catch(function () {}) }
|
|
251
|
+
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 () {}) }
|
|
252
|
+
// 活动心跳:对进行中/验收中的任务轮询子代理最近动作(卡片与详情展示"现在跑到哪了")
|
|
253
|
+
function fetchActivity() {
|
|
254
|
+
if (!state.sessionId || !state.isRoot || !state.open) return // 面板关闭时不轮询活动(省同步 I/O)
|
|
255
|
+
var running = state.tasks.filter(function (t) { return t.status === 'in-progress' || t.status === 'verifying' })
|
|
256
|
+
if (!running.length) { if (Object.keys(state.activity).length) { state.activity = {}; notify() } return }
|
|
257
|
+
var pending = running.length
|
|
258
|
+
running.forEach(function (t) {
|
|
259
|
+
var epoch = reqEpoch
|
|
260
|
+
rpc('agent-activity', { taskId: t.id }).then(function (r) {
|
|
261
|
+
if (epoch !== reqEpoch) return // 会话已切换,丢弃过期响应
|
|
262
|
+
pending--
|
|
263
|
+
var act = (r && r.activity) || null
|
|
264
|
+
if (state.activity[t.id] !== act) { state.activity[t.id] = act; notify() }
|
|
265
|
+
else if (pending === 0) notify()
|
|
266
|
+
}).catch(function () { pending-- })
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
function fetchArchived() {
|
|
270
|
+
if (!state.sessionId) return
|
|
271
|
+
var epoch = reqEpoch
|
|
272
|
+
rpc('get-tasks', { includeArchived: true }).then(function (d) {
|
|
273
|
+
if (epoch !== reqEpoch) return
|
|
274
|
+
state.archived = ((d && d.tasks) || []).filter(function (t) { return t.status === 'archived' })
|
|
275
|
+
notify()
|
|
276
|
+
}).catch(function () {})
|
|
277
|
+
}
|
|
224
278
|
var __timer = ctx.get('timer')
|
|
225
279
|
// 轮询:get-tasks 已是纯读(v71),3s 开销低;面板关闭时也轮询以更新角标
|
|
226
280
|
if (__timer) ctx.effect(function () { return __timer.interval(fetchTasks, 3000) })
|
|
281
|
+
if (__timer) ctx.effect(function () { return __timer.interval(fetchActivity, 12000) })
|
|
227
282
|
function readLayoutOffsets() {
|
|
228
283
|
try {
|
|
229
284
|
var frames = document.querySelectorAll('[data-sidebar-collapsed], [data-details-collapsed]')
|
|
@@ -292,9 +347,125 @@ function apply(ctx) {
|
|
|
292
347
|
}
|
|
293
348
|
function StatCard(props) { return React.createElement('div', { style: { flex: '1 1 0', minWidth: 80, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, textAlign: 'center' } }, React.createElement('div', { style: { fontSize: 20, fontWeight: 700, color: props.color || C.text } }, String(props.value)), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 2 } }, props.label)) }
|
|
294
349
|
function BarRow(props) { var pct = props.total > 0 ? (props.count / props.total * 100) : 0; return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 } }, React.createElement('span', { style: { width: 40, fontSize: 10, color: C.text2, textAlign: 'right', flexShrink: 0 } }, props.label), React.createElement('div', { style: { flex: 1, height: 8, background: C.nested, borderRadius: 4, overflow: 'hidden' } }, React.createElement('div', { style: { height: '100%', width: pct + '%', background: props.color || C.brand, borderRadius: 4, transition: 'width .3s' } })), React.createElement('span', { style: { width: 24, fontSize: 10, color: C.text2, flexShrink: 0 } }, String(props.count))) }
|
|
350
|
+
// ===== 仪表盘:日期范围筛选(共享给报告与全局总览)=====
|
|
351
|
+
function tsInRange(iso, from, to) {
|
|
352
|
+
if (!iso) return false
|
|
353
|
+
var d = new Date(iso); if (isNaN(d.getTime())) return false
|
|
354
|
+
var ds = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2)
|
|
355
|
+
if (from && ds < from) return false
|
|
356
|
+
if (to && ds > to) return false
|
|
357
|
+
return true
|
|
358
|
+
}
|
|
359
|
+
function taskLastTs(t) {
|
|
360
|
+
var last = t.createdAt || ''
|
|
361
|
+
;(t.history || []).forEach(function (h) { if (h.timestamp > last) last = h.timestamp })
|
|
362
|
+
if (t.deliverable && t.deliverable.at > last) last = t.deliverable.at
|
|
363
|
+
if (t.verification && t.verification.at > last) last = t.verification.at
|
|
364
|
+
if (t.archivedAt && t.archivedAt > last) last = t.archivedAt
|
|
365
|
+
return last
|
|
366
|
+
}
|
|
367
|
+
function activeRange() { var f = state.dateRange || {}; return { from: f.from || '', to: f.to || '' } }
|
|
368
|
+
function rangeLabel() { var r = activeRange(); return !r.from && !r.to ? '全部时间' : (r.from || '…') + ' ~ ' + (r.to || '…') }
|
|
369
|
+
function RangeFilter() {
|
|
370
|
+
var _R = React; var useState = _R.useState
|
|
371
|
+
var _a = useState(state.rfOpen || false), open = _a[0], setOpen = _a[1]
|
|
372
|
+
var rg = activeRange()
|
|
373
|
+
function setRange(from, to) { state.dateRange = { from: from, to: to }; notify() }
|
|
374
|
+
function preset(days) {
|
|
375
|
+
if (days === 0) { setRange('', ''); return }
|
|
376
|
+
var to = new Date(); var from = new Date(Date.now() - (days - 1) * 86400000)
|
|
377
|
+
function fmt(d) { return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2) }
|
|
378
|
+
setRange(fmt(from), fmt(to))
|
|
379
|
+
}
|
|
380
|
+
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 }
|
|
381
|
+
var chip = { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.border, borderRadius: 10, background: C.card, color: C.text2, cursor: 'pointer' }
|
|
382
|
+
var dateInput = { fontSize: 11, padding: '2px 6px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text }
|
|
383
|
+
return React.createElement('div', { style: { marginBottom: 10 } },
|
|
384
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
385
|
+
React.createElement('button', { onClick: function () { state.rfOpen = !open; setOpen(!open) }, style: headBtn },
|
|
386
|
+
ic('calendar-days', 11),
|
|
387
|
+
React.createElement('span', null, '统计范围: ' + rangeLabel()),
|
|
388
|
+
ic(open ? 'chevron-up' : 'chevron-down', 10))),
|
|
389
|
+
open ? React.createElement('div', { style: { marginTop: 6, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } },
|
|
390
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
391
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '快捷:'),
|
|
392
|
+
React.createElement('button', { onClick: function () { preset(1) }, style: chip }, '今天'),
|
|
393
|
+
React.createElement('button', { onClick: function () { preset(7) }, style: chip }, '近 7 天'),
|
|
394
|
+
React.createElement('button', { onClick: function () { preset(30) }, style: chip }, '近 30 天'),
|
|
395
|
+
React.createElement('button', { onClick: function () { preset(0) }, style: chip }, '全部'),
|
|
396
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
397
|
+
React.createElement('label', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 4 } }, '开始',
|
|
398
|
+
React.createElement('input', { type: 'date', value: rg.from, onChange: function (e) { setRange(e.target.value, rg.to) }, style: dateInput })),
|
|
399
|
+
React.createElement('label', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 4 } }, '截至',
|
|
400
|
+
React.createElement('input', { type: 'date', value: rg.to, onChange: function (e) { setRange(rg.from, e.target.value) }, style: dateInput })),
|
|
401
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '作用于报告与全局总览')),
|
|
402
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 5 } }, '报告统计「时间范围内有活动」的任务;总览只显示范围内有活跃的会话。')) : null)
|
|
403
|
+
}
|
|
404
|
+
function GlobalBoards() {
|
|
405
|
+
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
406
|
+
var _a = useState(state.globalBoards), boards = _a[0], setBoards = _a[1]
|
|
407
|
+
var _b = useState(state.gboOpen || false), open = _b[0], setOpen = _b[1]
|
|
408
|
+
function load() { rpc('list-boards').then(function (r) { var b = (r && r.boards) || []; state.globalBoards = b; setBoards(b) }).catch(function () {}) }
|
|
409
|
+
useEffect(function () { load() }, [])
|
|
410
|
+
if (!boards.length) return null
|
|
411
|
+
var rg = activeRange()
|
|
412
|
+
var shown = boards.filter(function (b) { return !rg.from && !rg.to ? true : tsInRange(b.lastActivity, rg.from, rg.to) })
|
|
413
|
+
var activeN = boards.filter(function (b) { return b.counts.inProgress || b.counts.verifying || b.counts.pending || b.counts.blocked }).length
|
|
414
|
+
return React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } },
|
|
415
|
+
React.createElement('div', { onClick: function () { state.gboOpen = !open; setOpen(!open); if (!open) load() }, style: { fontSize: 11, fontWeight: 600, color: C.text2, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4 } },
|
|
416
|
+
ic('layout-grid', 11),
|
|
417
|
+
React.createElement('span', null, '全局会话总览(本机 ' + boards.length + ' 个看板 · ' + activeN + ' 个活跃)'),
|
|
418
|
+
open && (rg.from || rg.to) ? React.createElement('span', { style: { fontSize: 9, color: C.brand } }, '范围: ' + rangeLabel()) : null,
|
|
419
|
+
React.createElement('span', { style: { marginLeft: 'auto', display: 'inline-flex', color: C.text2 } }, ic(open ? 'chevron-up' : 'chevron-down', 11))),
|
|
420
|
+
open ? (shown.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2, padding: '6px 0' } }, '时间范围内无活跃会话') : shown.map(function (b, i) {
|
|
421
|
+
var isSelf = b.session === state.sessionId
|
|
422
|
+
return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 10, padding: '3px 0', borderBottom: '1px solid ' + C.nested } },
|
|
423
|
+
React.createElement('span', { onClick: function () { if (!isSelf && sessionsSvc) sessionsSvc.open(b.session) }, style: { color: isSelf ? C.text2 : C.brand, cursor: isSelf ? 'default' : 'pointer', textDecoration: isSelf ? 'none' : 'underline', minWidth: 90 } }, (isSelf ? '★ ' : '') + shortId(b.session)),
|
|
424
|
+
React.createElement('span', { style: { color: C.text2 } }, b.teamMode ? 'Team' : (b.boardMode === 'auto' ? '自动' : '手动')),
|
|
425
|
+
React.createElement('span', { style: { display: 'inline-flex', gap: 5 } },
|
|
426
|
+
b.counts.inProgress ? React.createElement('span', { style: { color: C.brand } }, '▶' + b.counts.inProgress) : null,
|
|
427
|
+
b.counts.verifying ? React.createElement('span', { style: { color: C.warn } }, '验' + b.counts.verifying) : null,
|
|
428
|
+
b.counts.pending ? React.createElement('span', { style: { color: C.text } }, '待' + b.counts.pending) : null,
|
|
429
|
+
b.counts.blocked ? React.createElement('span', { style: { color: C.err } }, '🛑' + b.counts.blocked) : null,
|
|
430
|
+
!(b.counts.inProgress || b.counts.verifying || b.counts.pending || b.counts.blocked) ? React.createElement('span', { style: { color: C.text2 } }, '空闲') : null),
|
|
431
|
+
b.activeTitles.length ? React.createElement('span', { style: { color: C.text2, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: b.activeTitles.join(';') }, b.activeTitles.join(';')) : null,
|
|
432
|
+
b.lastActivity ? React.createElement('span', { style: { color: C.text2 } }, ago(b.lastActivity)) : null)
|
|
433
|
+
})) : null)
|
|
434
|
+
}
|
|
435
|
+
function buildReport() {
|
|
436
|
+
var lines = ['# 任务看板报告', '', '生成时间: ' + new Date().toLocaleString(), '模式: ' + (state.teamMode ? 'Team' : (state.boardMode === 'auto' ? '自动' : '手动')), '', '统计范围: ' + rangeLabel()]
|
|
437
|
+
var groups = { inProgress: [], verifying: [], pending: [], blocked: [], resolved: [] }
|
|
438
|
+
var rg = activeRange()
|
|
439
|
+
state.tasks.forEach(function (t) {
|
|
440
|
+
if (t.status === 'archived') return
|
|
441
|
+
if ((rg.from || rg.to) && !tsInRange(taskLastTs(t), rg.from, rg.to)) return
|
|
442
|
+
var g = groups[t.status]; if (g) g.push(t)
|
|
443
|
+
})
|
|
444
|
+
if (groups.inProgress.length) { lines.push('## 进行中'); groups.inProgress.forEach(function (t) { lines.push('- ' + t.title + '(' + (t.claimedBy ? shortId(t.claimedBy) : '-') + ')') }); lines.push('') }
|
|
445
|
+
if (groups.verifying.length) { lines.push('## 验证中'); groups.verifying.forEach(function (t) { lines.push('- ' + t.title) }); lines.push('') }
|
|
446
|
+
if (groups.pending.length) { lines.push('## 待办'); groups.pending.forEach(function (t) { lines.push('- ' + t.title + ((t.retryCount || 0) + (t.rejectCount || 0) > 0 ? '(⟳' + ((t.retryCount || 0) + (t.rejectCount || 0)) + ')' : '')) }); lines.push('') }
|
|
447
|
+
if (groups.blocked.length) { lines.push('## 阻塞'); groups.blocked.forEach(function (t) { lines.push('- ' + t.title + (t.escalation ? '(待裁决)' : '')) }); lines.push('') }
|
|
448
|
+
var doneAll = state.archived.length ? state.archived : state.tasks.filter(function (t) { return t.status === 'resolved' }); var done = doneAll.filter(function (t) { return !(rg.from || rg.to) || tsInRange(taskLastTs(t), rg.from, rg.to) })
|
|
449
|
+
if (done.length) { lines.push('## 已完成(含归档,近 ' + Math.min(done.length, 20) + ' 条)'); done.slice(0, 20).forEach(function (t) { lines.push('- ' + t.title + (t.verification ? '|验收: ' + t.verification.verdict : '') + (t.deliverable && t.deliverable.summary ? '|' + t.deliverable.summary.slice(0, 80) : '')) }); lines.push('') }
|
|
450
|
+
return lines.join('\n')
|
|
451
|
+
}
|
|
452
|
+
function ReportButton() {
|
|
453
|
+
var _R = React; var useState = _R.useState
|
|
454
|
+
var _a = useState(''), msg = _a[0], setMsg = _a[1]
|
|
455
|
+
function gen() {
|
|
456
|
+
var md = buildReport()
|
|
457
|
+
try { navigator.clipboard.writeText(md).then(function () { setMsg('✅ 已复制到剪贴板'); setTimeout(function () { setMsg('') }, 2500) }, function () { download(md) }) } catch (_) { download(md) }
|
|
458
|
+
function download(text) {
|
|
459
|
+
try { var blob = new Blob([text], { type: 'text/markdown' }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'board-report.md'; a.click(); setMsg('✅ 已下载 board-report.md'); setTimeout(function () { setMsg('') }, 2500) } catch (_) { setMsg('⚠️ 导出失败') }
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 5 } }, React.createElement('button', { onClick: gen, title: '生成本看板 markdown 报告并复制', style: { fontSize: 11, padding: '3px 10px', border: '1px solid ' + C.border, borderRadius: 5, background: C.card, color: C.text, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('file-down', 12), '生成报告'), msg ? React.createElement('span', { style: { fontSize: 10, color: C.ok } }, msg) : null)
|
|
463
|
+
}
|
|
295
464
|
function Dashboard() {
|
|
296
465
|
var stats = computeStats(state.tasks); var statusOrder = ['pending', 'in-progress', 'verifying', 'resolved', 'blocked', 'archived']; var prioOrder = ['critical', 'high', 'medium', 'low']
|
|
297
466
|
return React.createElement('div', null,
|
|
467
|
+
React.createElement(RangeFilter),
|
|
468
|
+
React.createElement(GlobalBoards),
|
|
298
469
|
React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' } }, React.createElement(StatCard, { label: '总任务', value: stats.total, color: C.text }), React.createElement(StatCard, { label: '待办', value: stats.byStatus['pending'] || 0, color: C.text2 }), React.createElement(StatCard, { label: '进行中', value: stats.byStatus['in-progress'] || 0, color: C.brand }), React.createElement(StatCard, { label: '验证中', value: stats.byStatus['verifying'] || 0, color: C.warn }), React.createElement(StatCard, { label: '已完成', value: stats.byStatus['resolved'] || 0, color: C.ok }), React.createElement(StatCard, { label: '已归档', value: stats.byStatus['archived'] || 0, color: C.text2 })),
|
|
299
470
|
React.createElement('div', { style: { display: 'flex', gap: 12, marginBottom: 12, flexWrap: 'wrap' } },
|
|
300
471
|
React.createElement('div', { style: { flex: '1 1 0', minWidth: 200, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, '按状态分布'), statusOrder.map(function (s) { return React.createElement(BarRow, { key: s, label: statusLabels[s] || s, count: stats.byStatus[s] || 0, total: stats.total, color: statusColors[s] || C.brand }) })),
|
|
@@ -307,15 +478,37 @@ function apply(ctx) {
|
|
|
307
478
|
function BoardButton(props) {
|
|
308
479
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
309
480
|
var _a = useState(0), pendingCount = _a[0], setPendingCount = _a[1]; var _b = useState(false), isOpen = _b[0], setIsOpen = _b[1]; var _c = useState(0), escCount = _c[0], setEscCount = _c[1]; var _d2 = useState(true), isRoot = _d2[0], setIsRoot = _d2[1]
|
|
310
|
-
useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; fetchTasks(); fetchChildren() } } }, [props && props.sessionId])
|
|
481
|
+
useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; reqEpoch++; state.tasks = []; state.children = []; state.activity = {}; state.archived = []; state.detailId = null; notify(); fetchTasks(); fetchChildren() } } }, [props && props.sessionId])
|
|
311
482
|
useEffect(function () { function update() { var n = 0, e = 0; for (var i = 0; i < state.tasks.length; i++) { if (state.tasks[i].status === 'pending') n++; if (state.tasks[i].escalation) e++ }; setPendingCount(n); setEscCount(e); setIsOpen(state.open); setIsRoot(state.isRoot) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
312
483
|
if (!isRoot) return null // 子代理会话不显示看板入口
|
|
313
|
-
return React.createElement('button', { onClick: function () { state.open = !state.open; notify() }, title: '智能看板' + (pendingCount > 0 ? '(' + pendingCount + ' 待办)' : '') + (escCount > 0 ? '(' + escCount + ' 待裁决)' : ''), style: { display: 'inline-flex', alignItems: 'center', gap: 4, padding: '3px 10px', border: '1px solid ' + (escCount > 0 ? C.err : C.border), borderRadius: 6, background: isOpen ? C.nested : 'transparent', color: C.text, cursor: 'pointer', fontSize: 12 } }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center' } }, ic('clipboard-list', 14)), React.createElement('span', null, '智能看板'), escCount > 0 ? React.createElement('span', { style: { minWidth: 16, height: 16, padding: '0 4px', borderRadius: 8, background: C.err, color:
|
|
484
|
+
return React.createElement('button', { onClick: function () { state.open = !state.open; notify() }, title: '智能看板' + (pendingCount > 0 ? '(' + pendingCount + ' 待办)' : '') + (escCount > 0 ? '(' + escCount + ' 待裁决)' : ''), style: { display: 'inline-flex', alignItems: 'center', gap: 4, padding: '3px 10px', border: '1px solid ' + (escCount > 0 ? C.err : C.border), borderRadius: 6, background: isOpen ? C.nested : 'transparent', color: C.text, cursor: 'pointer', fontSize: 12 } }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center' } }, ic('clipboard-list', 14)), React.createElement('span', null, '智能看板'), escCount > 0 ? React.createElement('span', { style: { minWidth: 16, height: 16, padding: '0 4px', borderRadius: 8, background: C.err, color: C_INV, fontSize: 10, fontWeight: 700, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 1, animation: 'tskb-pulse 1s ease-in-out infinite' }, title: escCount + ' 个任务待裁决' }, ic('alert-triangle', 10), escCount) : null, pendingCount > 0 ? React.createElement('span', { style: { minWidth: 16, height: 16, padding: '0 4px', borderRadius: 8, background: C.brand, color: C_INV, fontSize: 10, fontWeight: 600, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' } }, String(pendingCount)) : null)
|
|
485
|
+
}
|
|
486
|
+
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, '手动'))) }
|
|
487
|
+
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' : ''))) }
|
|
488
|
+
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, '归档'))) }
|
|
489
|
+
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.border, 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.border), 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) }
|
|
490
|
+
function MinCfg(props) {
|
|
491
|
+
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
492
|
+
var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]
|
|
493
|
+
var _b = useState(false), dirty = _b[0], setDirty = _b[1]
|
|
494
|
+
useEffect(function () { setVal(String(props.value)); setDirty(false) }, [props.value])
|
|
495
|
+
function commit(v) {
|
|
496
|
+
var n = parseInt(v, 10)
|
|
497
|
+
if (isNaN(n) || n < props.min || n > props.max) { setVal(String(props.value)); setDirty(false); return }
|
|
498
|
+
setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(fetchTasks).catch(function () {})
|
|
499
|
+
}
|
|
500
|
+
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } },
|
|
501
|
+
props.label,
|
|
502
|
+
React.createElement('input', {
|
|
503
|
+
value: val, title: props.tip || (props.min + ' ~ ' + props.max + ' 分钟'),
|
|
504
|
+
onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== String(props.value)) },
|
|
505
|
+
onBlur: function () { if (dirty) commit(val) },
|
|
506
|
+
onKeyDown: function (e) { if (e.key === 'Enter') commit(val) },
|
|
507
|
+
style: { width: 38, padding: '0px 3px', fontSize: 9, textAlign: 'center', border: '1px solid ' + (dirty ? C.brand : C.border), borderRadius: 2, background: C.card, color: C.text }
|
|
508
|
+
}),
|
|
509
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '分'),
|
|
510
|
+
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)
|
|
314
511
|
}
|
|
315
|
-
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: '#fff' } : { 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: '#fff' } : { background: 'transparent', color: manDis ? C.text2 + '88' : C.text2 }, manDis ? { cursor: 'not-allowed', opacity: 0.5 } : {}) }, icText('user', 12, '手动'))) }
|
|
316
|
-
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 ? '#fff' : C.text2, transition: 'all .15s' } }, icText('users', 12, 'Team' + (on ? ' ON' : ''))) }
|
|
317
|
-
function ViewTab() { var btnBase = { fontSize: 11, padding: '3px 10px', border: 'none', cursor: 'pointer', fontWeight: 500, borderRadius: 5, transition: 'all .15s' }; 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: '#fff' } : { 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: '#fff' } : { 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: '#fff' } : { background: 'transparent', color: C.text2 }) }, icText('bar-chart-3', 12, '仪表盘'))) }
|
|
318
|
-
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.border, 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.border), 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: '#fff', cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null) }
|
|
319
512
|
function PoolStatus() {
|
|
320
513
|
var ps = state.poolStatus
|
|
321
514
|
if (!ps) return null
|
|
@@ -353,7 +546,7 @@ function apply(ctx) {
|
|
|
353
546
|
// 降级:文本输入(模型枚举不可用时)
|
|
354
547
|
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
|
|
355
548
|
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.border), borderRadius: 2, background: C.card, color: C.text } }),
|
|
356
|
-
dirty ? React.createElement('button', { onClick: save, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color:
|
|
549
|
+
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)
|
|
357
550
|
}
|
|
358
551
|
function PoolCfgPopover(props) {
|
|
359
552
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
|
|
@@ -372,7 +565,11 @@ function apply(ctx) {
|
|
|
372
565
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Worker 执行模型)')),
|
|
373
566
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
374
567
|
React.createElement(ModelCfg, { label: 'V模型', cfgKey: 'verifierModel', value: props.verifierModel }),
|
|
375
|
-
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)'))
|
|
568
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)')),
|
|
569
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '两级超时(软超时只提醒主窗口,由你决定继续等待或终止)'),
|
|
570
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
571
|
+
React.createElement(MinCfg, { label: '软超时', cfgKey: 'softTimeoutMin', value: props.softT, min: 1, max: 480, tip: '超过后在看板提醒主窗口,不终止 run(1~480 分钟)' }),
|
|
572
|
+
React.createElement(MinCfg, { label: '硬超时', cfgKey: 'hardTimeoutMin', value: props.hardT, min: 1, max: 1440, tip: '兜底:人不在线时自动终止挂死 run 并重试(1~1440 分钟)' }))) : null)
|
|
376
573
|
}
|
|
377
574
|
// #13 筛选:文本(标题/描述/ID)+ 优先级多选 + 标签
|
|
378
575
|
function passFilter(t) {
|
|
@@ -386,7 +583,19 @@ function apply(ctx) {
|
|
|
386
583
|
// #18 依赖未满足判断(依赖不存在视为阻塞——创建时已校验,防御性兜底)
|
|
387
584
|
function depsBlocked(t) { if (!Array.isArray(t.dependsOn) || t.dependsOn.length === 0) return false; return t.dependsOn.some(function (id) { var d = getTask(id); return !d || (d.status !== 'resolved' && d.status !== 'archived') }) }
|
|
388
585
|
// #19 管线档位元数据
|
|
389
|
-
var pipeMeta = { full: { icon: 'flask-conical', label: '全流程(执行+验证)' }, work: { icon: 'file-text', label: '免验证(只做不验)' }, direct: { icon: 'message-square', label: '主窗口直接处理' } }
|
|
586
|
+
var pipeMeta = { full: { icon: 'flask-conical', label: '全流程(执行+验证)', short: '全流程' }, work: { icon: 'file-text', label: '免验证(只做不验)', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直接处理', short: '直办' } }
|
|
587
|
+
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 '' } }
|
|
588
|
+
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)) }
|
|
589
|
+
// 历史会话列表:优先用 host 留档的 t.runs(含全部重试);老任务回退到 claimedBy/verifierRun
|
|
590
|
+
function historyRuns(t) {
|
|
591
|
+
var out = Array.isArray(t.runs) ? t.runs.slice() : []
|
|
592
|
+
if (!out.length) {
|
|
593
|
+
if (t.claimedBy && t.claimedBy !== 'auto-dispatch' && t.claimedBy !== 'system') out.push({ role: 'worker', id: t.claimedBy, at: t.claimedAt })
|
|
594
|
+
if (t.verifierRun) out.push({ role: 'verifier', id: t.verifierRun, at: t.verifiedAt })
|
|
595
|
+
}
|
|
596
|
+
return out.filter(function (r) { return r && r.id })
|
|
597
|
+
}
|
|
598
|
+
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' : '') }
|
|
390
599
|
function pipeOf(t) { return pipeMeta[t.pipeline] || pipeMeta.full }
|
|
391
600
|
// #14 批量操作条(#16 带一步撤销:快照操作前的 priority/status)
|
|
392
601
|
function BatchBar() {
|
|
@@ -401,9 +610,9 @@ function apply(ctx) {
|
|
|
401
610
|
var btn = { fontSize: 10, padding: '3px 10px', borderRadius: 4, border: 'none', cursor: 'pointer', fontWeight: 600 }
|
|
402
611
|
return React.createElement('div', { style: { position: 'sticky', bottom: 0, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 8px', marginTop: 8, background: C.nested, border: '1px solid ' + C.border, borderRadius: 6 } },
|
|
403
612
|
React.createElement('span', { style: { fontSize: 11, color: C.text, fontWeight: 600 } }, '已选 ' + cnt + ' 项'),
|
|
404
|
-
React.createElement('button', { onClick: function () { run('archive') }, disabled: cnt === 0, style: Object.assign({}, btn, { background: C.text2, color:
|
|
405
|
-
['critical', 'high', 'medium', 'low'].map(function (p) { return React.createElement('button', { key: p, onClick: function () { run('set-priority', p) }, disabled: cnt === 0, style: Object.assign({}, btn, { background: prioColor[p], color:
|
|
406
|
-
undo ? React.createElement('button', { onClick: doUndo, title: '撤销最近一次批量操作', style: Object.assign({}, btn, { background: C.brand, color:
|
|
613
|
+
React.createElement('button', { onClick: function () { run('archive') }, disabled: cnt === 0, style: Object.assign({}, btn, { background: C.text2, color: C_INV, display: 'inline-flex', alignItems: 'center', gap: 3 }) }, ic('archive', 11), '批量归档'),
|
|
614
|
+
['critical', 'high', 'medium', 'low'].map(function (p) { return React.createElement('button', { key: p, onClick: function () { run('set-priority', p) }, disabled: cnt === 0, style: Object.assign({}, btn, { background: prioColor[p], color: C_INV }) }, prioLabel[p]) }),
|
|
615
|
+
undo ? React.createElement('button', { onClick: doUndo, title: '撤销最近一次批量操作', style: Object.assign({}, btn, { background: C.brand, color: C_INV }) }, '↩️ 撤销') : null,
|
|
407
616
|
msg ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, msg) : null,
|
|
408
617
|
React.createElement('button', { onClick: function () { state.selected = {}; notify() }, style: { marginLeft: 'auto', fontSize: 10, padding: '3px 8px', border: 'none', background: 'transparent', color: C.text2, cursor: 'pointer' } }, '清除选择'))
|
|
409
618
|
}
|
|
@@ -418,11 +627,18 @@ function apply(ctx) {
|
|
|
418
627
|
var chipBase = { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: '1px solid ' + C.border, cursor: 'pointer', background: 'transparent', color: C.text2, transition: 'all .15s' }
|
|
419
628
|
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
|
|
420
629
|
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.border, borderRadius: 4, background: C.card, color: C.text } }),
|
|
421
|
-
['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:
|
|
630
|
+
['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]) }),
|
|
422
631
|
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,
|
|
423
632
|
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)
|
|
424
633
|
}
|
|
425
|
-
function Card(props) { var t = props.task; var pc = prioColor[t.priority] || prioColor.low; var dragging = state.dragTask === t.id; var preview = (t.deliverable && t.deliverable.summary) || t.resolution; var critGlow = t.priority === 'critical' && !t.escalation; var sel = !!state.selected[t.id]; var pm = pipeOf(t); var depBlock = t.status === 'pending' && depsBlocked(t); return React.createElement('div', { draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onClick: function () { if (state.selectMode) { if (state.selected[t.id]) delete state.selected[t.id]; else state.selected[t.id] = true; notify() } else { state.detailId = t.id; notify() } }, style: { border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: pc, color:
|
|
634
|
+
function Card(props) { var t = props.task; var pc = prioColor[t.priority] || prioColor.low; var dragging = state.dragTask === t.id; var preview = (t.deliverable && t.deliverable.summary) || t.resolution; var critGlow = t.priority === 'critical' && !t.escalation; var sel = !!state.selected[t.id]; var pm = pipeOf(t); var depBlock = t.status === 'pending' && depsBlocked(t); return React.createElement('div', { draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onClick: function () { if (state.selectMode) { if (state.selected[t.id]) delete state.selected[t.id]; else state.selected[t.id] = true; notify() } else { state.detailId = t.id; notify() } }, style: { border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中')), t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, depBlock ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2 } }, '⛓ 被 ' + t.dependsOn.filter(function (id) { var d = getTask(id); return !d || (d.status !== 'resolved' && d.status !== 'archived') }).length + ' 个依赖阻塞') : null, t.stuckSince ? React.createElement('div', { style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡死 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
|
|
635
|
+
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
636
|
+
(t.retryCount || 0) + (t.rejectCount || 0) > 0 ? React.createElement('span', { title: '重试 ' + (t.retryCount || 0) + ' 次 / 驳回 ' + (t.rejectCount || 0) + ' 次', style: { color: C.warn, fontWeight: 600 } }, '⟳' + ((t.retryCount || 0) + (t.rejectCount || 0))) : null,
|
|
637
|
+
t.status === 'in-progress' && t.claimedBy ? React.createElement('span', null, '⚡ ' + shortId(t.claimedBy)) : null,
|
|
638
|
+
t.assignee ? React.createElement('span', null, '👤→' + shortId(t.assignee)) : null,
|
|
639
|
+
durOf(t) ? React.createElement('span', { title: '创建至今耗时' }, '⏱ ' + durOf(t)) : null),
|
|
640
|
+
(t.status === 'in-progress' || t.status === 'verifying') && state.activity[t.id] ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: state.activity[t.id] }, '👁 ' + state.activity[t.id]) : null,
|
|
641
|
+
t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null) }
|
|
426
642
|
function ActorLink(props) { var id = props.id; if (!id || id === 'system' || id === 'unknown' || id === 'auto-dispatch') return React.createElement('span', { style: { color: C.text2 } }, id || '-'); return React.createElement('span', { onClick: function (e) { e.stopPropagation(); jumpTo(id) }, style: { color: C.brand, cursor: 'pointer', textDecoration: 'underline' }, title: '跳转到 ' + id }, shortId(id)) }
|
|
427
643
|
function MsgThread(props) {
|
|
428
644
|
var msgs = props.messages || []
|
|
@@ -437,6 +653,49 @@ function apply(ctx) {
|
|
|
437
653
|
React.createElement('div', { style: { fontSize: 10, color: C.text, whiteSpace: 'pre-wrap', maxHeight: 140, overflowY: 'auto' } }, m.text))
|
|
438
654
|
}))
|
|
439
655
|
}
|
|
656
|
+
function ArchiveView() {
|
|
657
|
+
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
658
|
+
var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
|
|
659
|
+
useEffect(function () { if (!state.archived.length) fetchArchived() }, [])
|
|
660
|
+
var list = state.archived.filter(function (t) {
|
|
661
|
+
if (!q) return true
|
|
662
|
+
var qq = q.toLowerCase()
|
|
663
|
+
return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
|
|
664
|
+
}).sort(function (a, b) {
|
|
665
|
+
// 完成时间最近在前:resolvedAt 优先,回退验收时间/归档时间/最后活动时间
|
|
666
|
+
function doneTs(t) { return t.resolvedAt || (t.verification && t.verification.at) || t.archivedAt || taskLastTs(t) || '' }
|
|
667
|
+
return (doneTs(b) || '').localeCompare(doneTs(a) || '')
|
|
668
|
+
})
|
|
669
|
+
function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchArchived(); fetchTasks() }).catch(function () {}) }
|
|
670
|
+
return React.createElement('div', null,
|
|
671
|
+
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.border, borderRadius: 5, background: C.card, color: C.text, marginBottom: 8 } }),
|
|
672
|
+
state.archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
|
|
673
|
+
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
674
|
+
list.map(function (t) {
|
|
675
|
+
return React.createElement('div', { key: t.id, style: { padding: '6px 8px', marginBottom: 5, background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } },
|
|
676
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
677
|
+
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: t.title }, t.title),
|
|
678
|
+
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,
|
|
679
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, t.archivedAt ? ago(t.archivedAt) : '')),
|
|
680
|
+
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,
|
|
681
|
+
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4, flexWrap: 'wrap' } },
|
|
682
|
+
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' } }, '↩ 恢复待办'),
|
|
683
|
+
(function () {
|
|
684
|
+
var runs = historyRuns(t)
|
|
685
|
+
if (!runs.length) return React.createElement('span', { style: { fontSize: 9, color: C.text2, opacity: 0.6, padding: '2px 0' } }, '无历史会话')
|
|
686
|
+
var wN = 0, vN = 0
|
|
687
|
+
return React.createElement('span', { style: { display: 'inline-flex', gap: 5, flexWrap: 'wrap' } }, runs.map(function (r, i) {
|
|
688
|
+
var seq = r.role === 'verifier' ? (++vN) : (++wN)
|
|
689
|
+
var isV = r.role === 'verifier'
|
|
690
|
+
return React.createElement('button', {
|
|
691
|
+
key: i, onClick: function () { if (sessionsSvc) sessionsSvc.open(r.id) },
|
|
692
|
+
title: (isV ? 'Verifier' : 'Worker') + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
|
|
693
|
+
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' }
|
|
694
|
+
}, '→ ' + (isV ? 'V' : 'W') + '#' + seq)
|
|
695
|
+
}))
|
|
696
|
+
})()))
|
|
697
|
+
}))
|
|
698
|
+
}
|
|
440
699
|
function TeamView() {
|
|
441
700
|
var ps = state.poolStatus || { workers: [], verifiers: [] }
|
|
442
701
|
// v74 一次性模型:每个活跃卡片 = 一个在跑的 run(随任务结算销毁),没有常驻成员
|
|
@@ -447,7 +706,7 @@ function apply(ctx) {
|
|
|
447
706
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 } },
|
|
448
707
|
React.createElement('span', { style: { display: 'inline-flex', color: isW ? C.brand : C.warn } }, ic(isW ? 'zap' : 'clipboard-check', 13)),
|
|
449
708
|
React.createElement('span', { style: { fontSize: 12, fontWeight: 700, color: C.text } }, (isW ? 'Worker' : 'Verifier') + ' · 执行中'),
|
|
450
|
-
React.createElement('span', { style: { marginLeft: 'auto', fontSize: 9, padding: '1px 6px', borderRadius: 3, background: C.brand, color:
|
|
709
|
+
React.createElement('span', { style: { marginLeft: 'auto', fontSize: 9, padding: '1px 6px', borderRadius: 3, background: C.brand, color: C_INV } }, '忙碌')),
|
|
451
710
|
m.model ? React.createElement('div', { style: { fontSize: 9, color: C.warn, marginBottom: 3 }, title: '异构模型审查' }, '🧬 ' + m.model) : null,
|
|
452
711
|
curTask ? React.createElement('div', { onClick: function () { state.detailId = curTask.id; state.view = 'board'; notify() }, style: { fontSize: 10, color: C.brand, cursor: 'pointer', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: curTask.title }, '→ ' + curTask.title) : React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '→ ' + m.taskId),
|
|
453
712
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '一次性子代理会话' }, '会话 ' + String(m.runId || '').slice(0, 8) + '…(用完即销毁)'))
|
|
@@ -473,7 +732,7 @@ function apply(ctx) {
|
|
|
473
732
|
var dt = getTask(id)
|
|
474
733
|
var satisfied = dt && (dt.status === 'resolved' || dt.status === 'archived')
|
|
475
734
|
return React.createElement('div', { key: id, style: { display: 'flex', alignItems: 'center', gap: 5, marginBottom: 3, fontSize: 10 } },
|
|
476
|
-
React.createElement('span', { style: { padding: '0 5px', borderRadius: 3, fontSize: 9, background: satisfied ? C.ok : (statusColors[dt && dt.status] || C.text2), color:
|
|
735
|
+
React.createElement('span', { style: { padding: '0 5px', borderRadius: 3, fontSize: 9, background: satisfied ? C.ok : (statusColors[dt && dt.status] || C.text2), color: C_INV, flexShrink: 0 } }, dt ? (statusLabels[dt.status] || dt.status) : '不存在'),
|
|
477
736
|
React.createElement('span', { onClick: function () { if (dt) { state.detailId = id; notify() } }, style: { color: dt ? C.brand : C.text2, cursor: dt ? 'pointer' : 'default', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }, title: id }, dt ? dt.title : id),
|
|
478
737
|
React.createElement('span', { onClick: function () { saveDeps(deps.filter(function (x) { return x !== id })) }, title: '移除依赖', style: { cursor: 'pointer', color: C.text2, flexShrink: 0, display: 'inline-flex' } }, ic('x', 10)))
|
|
479
738
|
}),
|
|
@@ -491,7 +750,6 @@ function apply(ctx) {
|
|
|
491
750
|
function doAction(fn) { fn().then(fetchTasks).catch(function () {}) }
|
|
492
751
|
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) }) }
|
|
493
752
|
function jumpToAgent() { if (sessionsSvc && task.claimedBy) sessionsSvc.open(task.claimedBy) }
|
|
494
|
-
function jumpToVerifier() { if (sessionsSvc && task.verifierRun) sessionsSvc.open(task.verifierRun) }
|
|
495
753
|
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)) }) }
|
|
496
754
|
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)) }) }
|
|
497
755
|
function doDismiss() { rpc('dismiss-suspect', { taskId: task.id }).then(function () { setActionMsg('✅ 已清除卡死标记,继续观察'); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
@@ -500,19 +758,25 @@ function apply(ctx) {
|
|
|
500
758
|
var canIntervene = task.status === 'in-progress' || task.status === 'verifying'
|
|
501
759
|
return React.createElement('div', { style: { padding: '4px 2px' } },
|
|
502
760
|
React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
|
|
503
|
-
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 } }, React.createElement('span', { style: { fontSize: 10, padding: '1px 6px', borderRadius: 3, background: prioColor[task.priority] || prioColor.low, color:
|
|
761
|
+
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),
|
|
762
|
+
(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,
|
|
763
|
+
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' } },
|
|
764
|
+
ic('activity', 10),
|
|
765
|
+
React.createElement('span', null, '已运行 ' + elapsedSince(task.claimedAt) + '(软超时 ' + state.softTimeoutMin + ' 分提醒 · 硬超时 ' + state.hardTimeoutMin + ' 分自动终止)'),
|
|
766
|
+
elapsedMin(task.claimedAt) > state.softTimeoutMin ? React.createElement('span', { style: { color: C.warn, fontWeight: 600 } }, '⏱ 已超软超时') : null,
|
|
767
|
+
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,
|
|
504
768
|
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)' } },
|
|
505
769
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), 'Worker 上报歧义 — 等待主窗口裁决'),
|
|
506
770
|
React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, whiteSpace: 'pre-wrap', maxHeight: 160, overflowY: 'auto' } }, task.escalation.question),
|
|
507
771
|
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '上报于 ' + ago(task.escalation.at) + ' · ' + (task.escalation.by || '')),
|
|
508
772
|
React.createElement('textarea', { value: arbAnswer, onChange: function (e) { setArbAnswer(e.target.value) }, rows: 2, placeholder: '输入裁决指示,将直接转达给原 Worker(保有上下文)…', style: { width: '100%', fontSize: 11, padding: '5px 8px', marginBottom: 4, border: '1px solid ' + C.err, borderRadius: 4, background: C.card, color: C.text, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' } }),
|
|
509
|
-
React.createElement('button', { onClick: submitArbitration, disabled: !arbAnswer.trim(), style: { fontSize: 11, padding: '4px 12px', border: 'none', borderRadius: 4, background: C.err, color:
|
|
773
|
+
React.createElement('button', { onClick: submitArbitration, disabled: !arbAnswer.trim(), 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('scale', 12), '提交裁决')) : null,
|
|
510
774
|
actionMsg ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6 } }, actionMsg) : null,
|
|
511
775
|
task.stuckSince ? React.createElement('div', { style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
|
|
512
776
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), '执行 Agent 疑似卡死'),
|
|
513
777
|
React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6 } }, '标记于 ' + ago(task.stuckSince) + '(运行超 5 分钟且事件流停滞超 1 分钟)。你可以查看其会话后决定:'),
|
|
514
778
|
React.createElement('div', { style: { display: 'flex', gap: 6 } },
|
|
515
|
-
React.createElement('button', { onClick: doTerminate, style: { fontSize: 11, padding: '4px 12px', border: 'none', borderRadius: 4, background: C.err, color:
|
|
779
|
+
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), '终止任务(重新排队)'),
|
|
516
780
|
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' } }, '继续观察'),
|
|
517
781
|
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,
|
|
518
782
|
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.border, borderRadius: 4, background: C.card, color: C.text, boxSizing: 'border-box' } }),
|
|
@@ -537,28 +801,48 @@ function apply(ctx) {
|
|
|
537
801
|
React.createElement(MsgThread, { messages: task.messages }),
|
|
538
802
|
Array.isArray(task.history) && task.history.length > 0 ? React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 3 } }, '流转轨迹'), React.createElement('div', { style: { fontSize: 10, color: C.text2, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, task.history.map(function (h, i) { return React.createElement('div', { key: i, style: { marginBottom: 2 } }, React.createElement('span', { style: { color: C.brand } }, statusLabels[h.to] || h.to), ' · ' + ago(h.timestamp) + ' · ', React.createElement(ActorLink, { id: h.actor }), h.note ? ' · ' + h.note : '') }))) : null,
|
|
539
803
|
React.createElement('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap', marginTop: 6 } },
|
|
540
|
-
React.createElement('button', { onClick: saveEdit, disabled: saving, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color:
|
|
541
|
-
task.status === 'draft' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('update-task', { taskId: task.id, publish: true }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color:
|
|
542
|
-
(task.status === 'pending' || task.status === 'blocked') ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('claim-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color:
|
|
543
|
-
task.status === 'pending' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('dispatch-task', { taskId: task.id, role: 'worker' }) }) }, title: '手动派发给一次性 Worker 子代理', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.warn, color:
|
|
544
|
-
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:
|
|
545
|
-
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:
|
|
546
|
-
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:
|
|
547
|
-
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:
|
|
548
|
-
|
|
549
|
-
|
|
804
|
+
React.createElement('button', { onClick: saveEdit, disabled: saving, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, saving ? '保存中…' : [ic('save', 10), ' 保存并重置']),
|
|
805
|
+
task.status === 'draft' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('update-task', { taskId: task.id, publish: true }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('rocket', 10), '发布(进入派发池)') : null,
|
|
806
|
+
(task.status === 'pending' || task.status === 'blocked') ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('claim-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: 'pointer' } }, '领取') : null,
|
|
807
|
+
task.status === 'pending' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('dispatch-task', { taskId: task.id, role: 'worker' }) }) }, title: '手动派发给一次性 Worker 子代理', 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,
|
|
808
|
+
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,
|
|
809
|
+
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,
|
|
810
|
+
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,
|
|
811
|
+
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),
|
|
812
|
+
(function () {
|
|
813
|
+
// 历史会话:无论任务处于哪个阶段都完整列出(Worker 各次 + Verifier 各次)
|
|
814
|
+
var runs = historyRuns(task)
|
|
815
|
+
if (!runs.length) return null
|
|
816
|
+
var roleMeta = { worker: { label: 'W', color: C.brand, tip: 'Worker' }, verifier: { label: 'V', color: C.warn, tip: 'Verifier' } }
|
|
817
|
+
var wN = 0, vN = 0
|
|
818
|
+
return React.createElement('div', { style: { marginTop: 8, padding: '6px 8px', background: C.nested, borderRadius: 6 } },
|
|
819
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('users', 10), '历史会话(点击跳转查看,含全部重试)'),
|
|
820
|
+
React.createElement('div', { style: { display: 'flex', gap: 5, flexWrap: 'wrap' } },
|
|
821
|
+
runs.map(function (r, i) {
|
|
822
|
+
var rm = roleMeta[r.role] || roleMeta.worker
|
|
823
|
+
var seq = r.role === 'verifier' ? (++vN) : (++wN)
|
|
824
|
+
var isCur = (r.role === 'worker' && task.claimedBy === r.id) || (r.role === 'verifier' && task.verifierRun === r.id)
|
|
825
|
+
var okMark = r.outcome === 'completed' ? ' ✅' : (r.outcome === 'running' ? ' ⏳' : (r.outcome ? ' ⚠' : ''))
|
|
826
|
+
return React.createElement('button', {
|
|
827
|
+
key: i, onClick: function () { if (sessionsSvc) sessionsSvc.open(r.id) },
|
|
828
|
+
title: rm.tip + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + (r.outcome ? ' · ' + r.outcome : '') + '(' + r.id + ')',
|
|
829
|
+
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 }
|
|
830
|
+
}, '→ ' + rm.tip + ' #' + seq + (r.at ? ' · ' + ago(r.at) : '') + okMark)
|
|
831
|
+
})))
|
|
832
|
+
})(),
|
|
550
833
|
canIntervene ? React.createElement('div', { style: { marginTop: 8, padding: '6px 8px', border: '1px dashed ' + C.warn, borderRadius: 6 } },
|
|
551
834
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('zap', 11), '高优先级介入(插入执行 Agent 队首)'),
|
|
552
835
|
React.createElement('div', { style: { display: 'flex', gap: 4 } },
|
|
553
836
|
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.border, borderRadius: 4, background: C.card, color: C.text } }),
|
|
554
|
-
React.createElement('button', { onClick: submitIntervene, disabled: !interveneMsg.trim(), style: { fontSize: 11, padding: '4px 10px', border: 'none', borderRadius: 4, background: C.warn, color:
|
|
837
|
+
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)
|
|
555
838
|
}
|
|
556
839
|
function TopPanel() {
|
|
557
840
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
558
|
-
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
|
|
841
|
+
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]
|
|
559
842
|
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]
|
|
560
|
-
var _q = useState(state.teamMode), teamMode = _q[0], setTeamMode = _q[1]
|
|
561
|
-
|
|
843
|
+
var _q = useState(state.teamMode), teamMode = _q[0], setTeamMode = _q[1]; var _dr = useState(state.dateRange), setDR = _dr[1]
|
|
844
|
+
var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
|
|
845
|
+
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) } }, [])
|
|
562
846
|
if (!open) return null
|
|
563
847
|
if (state.isRoot === false) return null // 子代理会话不渲染看板面板
|
|
564
848
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -566,6 +850,7 @@ function apply(ctx) {
|
|
|
566
850
|
if (hasFilter) { active = active.filter(passFilter); archived = archived.filter(passFilter) }
|
|
567
851
|
var content
|
|
568
852
|
if (view === 'dashboard') { content = React.createElement(Dashboard) }
|
|
853
|
+
else if (view === 'archive') { content = React.createElement(ArchiveView) }
|
|
569
854
|
else if (view === 'team') { content = React.createElement(TeamView) }
|
|
570
855
|
else if (detailId) { content = React.createElement(DetailView) }
|
|
571
856
|
else {
|
|
@@ -577,15 +862,15 @@ function apply(ctx) {
|
|
|
577
862
|
var ta = a.archivedAt || a.resolvedAt || a.createdAt || '', tb = b.archivedAt || b.resolvedAt || b.createdAt || ''
|
|
578
863
|
return state.archSort === 'time-asc' ? ta.localeCompare(tb) : tb.localeCompare(ta)
|
|
579
864
|
})
|
|
580
|
-
content = React.createElement('div', null, React.createElement(FilterBar, null), hasFilter && active.length === 0 && archived.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 16, color: C.text2, fontSize: 11 } }, '无匹配任务') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, state.selectMode ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols),
|
|
865
|
+
content = React.createElement('div', null, React.createElement(FilterBar, null), hasFilter && active.length === 0 && archived.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 16, color: C.text2, fontSize: 11 } }, '无匹配任务') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, state.selectMode ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
|
|
581
866
|
}
|
|
582
867
|
return React.createElement('div', { style: { position: 'fixed', top: 44, left: (layL + 8) + 'px', right: (layR + 8) + 'px', maxHeight: '60vh', zIndex: 900, background: C.bg, border: '1px solid ' + C.border, borderRadius: '0 0 10px 10px', boxShadow: '0 8px 24px rgba(0,0,0,0.12)', display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
583
868
|
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 } },
|
|
584
869
|
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)),
|
|
585
870
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
586
|
-
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel }),
|
|
871
|
+
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT }),
|
|
587
872
|
dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
|
|
588
|
-
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 ?
|
|
873
|
+
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), '多选'),
|
|
589
874
|
React.createElement(TeamSwitch, { on: teamMode }),
|
|
590
875
|
React.createElement(ModeSwitch, { mode: mode, teamOn: teamMode }),
|
|
591
876
|
React.createElement('button', { onClick: function () { fetchTasks(); fetchChildren() }, title: '刷新', style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 12, color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('refresh-cw', 13)),
|
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
|
|
|
@@ -86,6 +92,7 @@ export function parseSections(text) {
|
|
|
86
92
|
if (/开发描述/.test(title)) out.summary = body
|
|
87
93
|
else if (/改动/.test(title)) out.changes = body
|
|
88
94
|
else if (/自测/.test(title)) out.selfTest = body
|
|
95
|
+
else if (/diff|变更概要/i.test(title)) out.diff = body
|
|
89
96
|
else if (/测试概要|验证概要|审查概要/.test(title)) out.verifySummary = body
|
|
90
97
|
else if (/核对项|核验项|检查项/.test(title)) out.checks = body
|
|
91
98
|
}
|
|
@@ -132,7 +139,7 @@ export function buildWorkerPrompt(t, pack) {
|
|
|
132
139
|
if (notes) p += '\n\n该任务的过程记录(歧义上报/主窗口裁决/驳回/干预,请务必遵循最新裁决方向):\n' + notes
|
|
133
140
|
if (msgs) p += '\n\n该任务的详细消息(裁决答案/干预指令/歧义原文等,请务必遵循):\n' + msgs
|
|
134
141
|
if (pack) p += '\n\n' + pack
|
|
135
|
-
p += '\n\n完成契约(双模,工具优先):\n1. 完成时:优先调用 board_report 工具(kind=complete, taskId=' + t.id + ',summary=开发描述/changes=改动清单/selfTest
|
|
142
|
+
p += '\n\n完成契约(双模,工具优先):\n1. 完成时:优先调用 board_report 工具(kind=complete, taskId=' + t.id + ',summary=开发描述/changes=改动清单/selfTest=自测情况/diffStat=变更概要);工具不可用则按分段格式输出(## 开发描述 / ## 改动清单 / ## 自测情况 / ## diff 概要)。\n diffStat 要求:若本次改动发生在 git 仓库内,运行 git diff --stat(含 git status --short),把输出贴进 diffStat(≤1500 字符);关键逻辑变更可附 ≤20 行核心片段。非代码任务/无 git 仓库可省略。\n2. 歧义/信息不足/需用户决策时:优先调用 board_report(kind=escalate, taskId=' + t.id + ', question=疑问);工具不可用则输出以 [ESCALATE] 开头的说明。不要猜测。上报歧义后直接结束本轮——裁决后会有新 Worker 带着裁决答案接手。'
|
|
136
143
|
return p
|
|
137
144
|
}
|
|
138
145
|
export function buildVerifierPrompt(t, pack) {
|
|
@@ -140,7 +147,7 @@ export function buildVerifierPrompt(t, pack) {
|
|
|
140
147
|
var msgs = buildMessages(t)
|
|
141
148
|
var p = '你是一个一次性任务审核 Verifier。审查下面这个任务的完成质量,给出结论后本会话即销毁。\n\ntaskId: ' + t.id + '\n任务: ' + t.title + '\n描述: ' + (t.description || '').slice(0, 500)
|
|
142
149
|
p += '\n完成说明: ' + (t.resolution || '(无)')
|
|
143
|
-
p += '\n交付物: ' + (t.deliverable ? ('开发描述: ' + (t.deliverable.summary || '') + '\n改动清单: ' + (t.deliverable.changes || '') + '\n自测情况: ' + (t.deliverable.selfTest || '')) : '(无)').slice(0,
|
|
150
|
+
p += '\n交付物: ' + (t.deliverable ? ('开发描述: ' + (t.deliverable.summary || '') + '\n改动清单: ' + (t.deliverable.changes || '') + '\n自测情况: ' + (t.deliverable.selfTest || '') + (t.deliverable.diff ? '\nWorker 变更 diff 概要(git diff --stat 等,作为改动范围核对的第材料):\n' + t.deliverable.diff : '')) : '(无)').slice(0, 4000)
|
|
144
151
|
if (t.context && t.context.instructions) p += '\n指引: ' + t.context.instructions
|
|
145
152
|
if (t.acceptance) p += '\n硬性验收脚本: ' + t.acceptance + '\n(必须独立复跑该命令并把真实输出贴进核对项;脚本失败必须 REJECTED)'
|
|
146
153
|
if (notes) p += '\n\n该任务的过程记录(歧义上报/主窗口裁决/驳回/干预,若有):\n' + notes + '\n注意:若过程记录显示主窗口已裁决改变任务方向,以裁决后的方向为验收标准。'
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agent-board",
|
|
3
3
|
"description": "DeepSeek Harness 智能看板:任务卡片 + 一次性 Worker/Verifier 派发 + 依赖调度 + 管线分档 + Team 模式(歧义上报主窗口裁决)",
|
|
4
|
-
"version": "1.1
|
|
4
|
+
"version": "1.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "node --test test/core.test.mjs",
|
|
9
9
|
"prepublishOnly": "node --check index.mjs && node --check lib/client.js && node --check lib/core.mjs && npm test",
|
|
10
|
-
"dev": "node scripts/dev-watch.cjs",
|
|
11
10
|
"e2e": "node scripts/e2e.cjs"
|
|
12
11
|
},
|
|
13
12
|
"exports": {
|
package/test/core.test.mjs
CHANGED
|
@@ -106,6 +106,29 @@ 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
|
+
|
|
126
|
+
test('parseSections: diff 概要分段解析(Worker 变更概要 → deliverable.diff)', () => {
|
|
127
|
+
const s = core.parseSections('## 开发描述\n做了\n## diff 概要\n gen.py | 3 + ++')
|
|
128
|
+
assert.equal(s.diff, 'gen.py | 3 + ++')
|
|
129
|
+
assert.equal(s.summary, '做了')
|
|
130
|
+
})
|
|
131
|
+
|
|
109
132
|
test('parseVerdict: 行首锚定,历史提及不误判', () => {
|
|
110
133
|
assert.equal(core.parseVerdict('APPROVED: 通过'), 'APPROVED')
|
|
111
134
|
assert.equal(core.parseVerdict('REJECTED: 不达标'), 'REJECTED')
|