dsh-agent-board 1.2.1 → 1.2.3
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 +16 -0
- package/index.mjs +97 -32
- package/lib/client.js +50 -21
- package/lib/core.mjs +1 -1
- package/package.json +1 -1
- package/test/core.test.mjs +13 -0
package/README.md
CHANGED
|
@@ -39,6 +39,22 @@ DeepSeek Harness 智能看板 — 让主窗口把编码工作变成可追踪、
|
|
|
39
39
|
|
|
40
40
|
> 宿主要求:Node ≥ 22;DSH ≥ `0.1.5-rc.1`(已通过 `peerDependencies` 声明,含预发布分支的版本范围见 package.json)
|
|
41
41
|
|
|
42
|
+
### 版本兼容性(先按宿主选插件版本)
|
|
43
|
+
|
|
44
|
+
| 宿主 DSH 版本 | 应装插件版本 | 原因 |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| **≥ 0.1.7**(含 rc) | **≥ 1.2.2(必须)** | 0.1.7 会话日志升级为 format v4:插件消息 `source.kind` 必须是生产者自有 kind。1.2.1 及更早会在**任务完成/阻塞回执**落盘时抛 `SessionFormatError: format v4 message requires a producer-owned source kind`,并连带使主窗口当前轮次失败(表现为「本轮运行失败」);同时「跳转会话」因宿主移除 `sessions.open` 而失效(控制台 `sessionsSvc.open is not a function`),卡片活动心跳读不到 v4 日志(`session.v4.jsonl.zstd`) |
|
|
47
|
+
| 0.1.5-rc.1 ~ 0.1.6 | ≤ 1.2.1 | 1.2.2 起按 0.1.7 协议编写(v4 source kind、`uiWorkspace.openSession` 跳转),旧宿主未做回归验证,建议停留 1.2.1 |
|
|
48
|
+
|
|
49
|
+
指定版本安装:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
dsh plugin --profile web add dsh-agent-board@1.2.2 # 0.1.7+ 宿主
|
|
53
|
+
dsh plugin --profile web add dsh-agent-board@1.2.1 # 0.1.5/0.1.6 宿主
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> 从 ≤1.2.1 升到 ≥1.2.2 必须重启 DSH(host 端代码在启动时加载;1.2.2 之前的老版本还有一个路由残留 bug:禁用/启用热重载会撞 `duplicate exact route`,只能重启恢复,1.2.2 已修复)。
|
|
57
|
+
|
|
42
58
|
```sh
|
|
43
59
|
dsh plugin --profile web add dsh-agent-board
|
|
44
60
|
```
|
package/index.mjs
CHANGED
|
@@ -62,9 +62,28 @@ export function apply(ctx) {
|
|
|
62
62
|
function touchSession(sid) { if (sid && typeof sid === 'string' && sid !== 'unknown') knownSessions[sid] = Date.now() }
|
|
63
63
|
// teamMode 缓存:由 rt() 同步,供 systemPrompt 动态引导段读取(v65)
|
|
64
64
|
var teamModeCache = {}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
// 看板文件用「家目录绝对路径 + node:fs」直读写——不再走 fs 服务的相对路径解析
|
|
66
|
+
// (fs 服务的相对路径解析根 = 进程启动 cwd,cwd 一变所有看板静默读成空板;
|
|
67
|
+
// 曾因此导致整个看板状态异常。绝对路径对此永久免疫)
|
|
68
|
+
function boardPath(sid) { return path.join(os.homedir(), '.dsh', 'tasks-' + sid + '.json') }
|
|
69
|
+
function fileFor(sid) { return boardPath(sid) } // 保留旧名兼容调用点,但已是绝对路径
|
|
70
|
+
async function rt(sid) {
|
|
71
|
+
var r
|
|
72
|
+
try { r = await fsNode.promises.readFile(boardPath(sid), 'utf8') } catch (_) { return seed(sid) } // 不存在/不可读 → 空板
|
|
73
|
+
try {
|
|
74
|
+
var d = JSON.parse(r)
|
|
75
|
+
if (vt(d) && d.ownerSession === sid) { teamModeCache[sid] = !!d.teamMode; return normalizeBoard(d) }
|
|
76
|
+
return seed(sid)
|
|
77
|
+
} catch (_) {
|
|
78
|
+
// JSON 截断/损坏(如强杀打断写盘):隔离留档再种新板——数据不丢,坏文件也不反复 poison
|
|
79
|
+
console.error('[task-board] board file corrupt, quarantining: ' + boardPath(sid))
|
|
80
|
+
fsNode.promises.rename(boardPath(sid), boardPath(sid) + '.corrupt-' + Date.now()).catch(function () {})
|
|
81
|
+
return seed(sid)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// 原子写盘:先写临时文件再 rename——强杀若发生在写盘中途,磁盘上最多留个 .tmp 残件,
|
|
85
|
+
// 看板本体永远不会是截断的半个 JSON(此前非原子直写,kill 中写 = 看板被 seed 清空)
|
|
86
|
+
async function wt(sid, d) { var c = JSON.stringify(d); var p = boardPath(sid); var tmp = p + '.tmp'; try { await fsNode.promises.writeFile(tmp, c, 'utf8'); await fsNode.promises.rename(tmp, p) } catch (e) { console.error('[task-board] write:', String(e)); throw e } }
|
|
68
87
|
// 每会话一条 promise 链,串行化所有 读-改-写,消除并发写竞争
|
|
69
88
|
var fileLocks = {}
|
|
70
89
|
function withLock(sid, fn) { var prev = fileLocks[sid] || Promise.resolve(); var p = prev.then(function () { return fn() }); fileLocks[sid] = p.catch(function () {}); return p }
|
|
@@ -80,7 +99,12 @@ export function apply(ctx) {
|
|
|
80
99
|
// makeMsg 支持插件来源标记(参考 dsh-notes 派发模式):
|
|
81
100
|
// form 'recall' = 背景回执(召回上下文,非指令);'notice' = 需注意的通知(带一行 summary)
|
|
82
101
|
// 不再用 kind:'user'——插件消息不该冒充用户在说话,模型可据 form 正确理解语义
|
|
83
|
-
|
|
102
|
+
// v0.1.7 起会话日志为 format v4:source.kind 必须是「生产者自有 kind」,kind:'plugin'
|
|
103
|
+
// 是已退役的 v3 包装写法,落盘时 persistence 直接抛 SessionFormatError
|
|
104
|
+
// (format v4 message requires a producer-owned source kind)并连带炸掉主窗口当前轮次。
|
|
105
|
+
// v3→v4 迁移把 {kind:'plugin',plugin:'dsh-agent-board'} 映射为 {kind:'plugin:dsh-agent-board'},
|
|
106
|
+
// 这里直接写迁移后的形态,与存量历史一致;form/summary 作为 source 元数据字段保留。
|
|
107
|
+
function makeMsg(text, form, summary) { var src = { kind: 'plugin:dsh-agent-board' }; if (form) { src.form = form; if (form === 'notice' && summary) src.summary = summary }; return { id: 'm' + Date.now() + Math.random().toString(36).slice(2, 6), role: 'user', content: [{ type: 'text', text: text }], source: src } }
|
|
84
108
|
// 超时保护:run 挂死时走失败重试路径
|
|
85
109
|
function withTimeout(promise, ms, label) { var timer = ctx.timer; if (!timer) return promise; return Promise.race([promise, timer.timeout(ms).then(function () { throw new Error(label + ' timeout ' + ms + 'ms') })]) }
|
|
86
110
|
|
|
@@ -110,16 +134,21 @@ export function apply(ctx) {
|
|
|
110
134
|
// 主窗口预研文件:t.context.files 里的路径由主 agent 选择性指定(它调研时读过哪些文件),
|
|
111
135
|
// host 在派发时从磁盘读最新内容注入 prompt——Worker/Verifier 不用从零重复调研。
|
|
112
136
|
// 上限:单文件 8KB、总计 40KB,超出截断并标注。
|
|
113
|
-
|
|
137
|
+
// 相对路径的解析根必须是「该会话的工作区」(root agent 的 session.header.cwd),
|
|
138
|
+
// 不能靠进程 cwd——dsh web 从家目录启动时相对路径会解析到 ~/.dsh 之外的家目录下,
|
|
139
|
+
// 全部读成「读取失败」(v0.1.7 实测:dsh-notes-plugin/... → C:\Users\<user>\dsh-notes-plugin)。
|
|
140
|
+
function sessionCwd(sid) { try { var root = rootForSession(sid); var cwd = root && root.session && root.session.header && root.session.header.cwd; return (typeof cwd === 'string' && cwd) ? cwd : '' } catch (_) { return '' } }
|
|
141
|
+
async function readContextPack(sid, t) {
|
|
114
142
|
var paths = (t.context && Array.isArray(t.context.files)) ? t.context.files : []
|
|
115
143
|
var notes = (t.context && typeof t.context.notes === 'string') ? t.context.notes : ''
|
|
116
144
|
if (!paths.length && !notes.trim()) return ''
|
|
145
|
+
var cwd = sessionCwd(sid)
|
|
117
146
|
var out = [], total = 0
|
|
118
147
|
for (var i = 0; i < paths.length && total < 40960; i++) {
|
|
119
148
|
var p = String(paths[i] || '')
|
|
120
149
|
if (!p) continue
|
|
121
150
|
try {
|
|
122
|
-
var content = await fs.readText(await fs.resolve(p))
|
|
151
|
+
var content = await fs.readText(await fs.resolve(p, cwd ? { cwd: cwd } : undefined))
|
|
123
152
|
var truncated = false
|
|
124
153
|
if (content.length > 8192) { content = content.slice(0, 8192); truncated = true }
|
|
125
154
|
if (total + content.length > 40960) { content = content.slice(0, 40960 - total); truncated = true }
|
|
@@ -174,7 +203,7 @@ export function apply(ctx) {
|
|
|
174
203
|
if (role === 'verifier') { modelOverride = (typeof dsnap.verifierModel === 'string' && dsnap.verifierModel.trim()) ? dsnap.verifierModel.trim() : ''; if (modelOverride && badModels[modelKey(sid, modelOverride)]) { console.error('[task-board] model ' + modelOverride + ' circuited, using parent model'); modelOverride = '' } }
|
|
175
204
|
else if (role === 'worker') { modelOverride = (typeof dsnap.workerModel === 'string' && dsnap.workerModel.trim()) ? dsnap.workerModel.trim() : ''; if (modelOverride && badModels[modelKey(sid, modelOverride)]) { console.error('[task-board] model ' + modelOverride + ' circuited, using parent model'); modelOverride = '' } }
|
|
176
205
|
var pack = ''
|
|
177
|
-
try { pack = await readContextPack(t) } catch (e) { console.error('[task-board] context pack read failed:', String(e)) }
|
|
206
|
+
try { pack = await readContextPack(sid, t) } catch (e) { console.error('[task-board] context pack read failed:', String(e)) }
|
|
178
207
|
// user prompt 只留一行指引,内容走上下文注入区块
|
|
179
208
|
var packNote = pack ? '本任务附带主窗口预研文件,已通过「上下文注入」区提供(含文件完整内容),直接基于其内容工作,不要重复读取这些文件。' : ''
|
|
180
209
|
var req = { label: role + ':' + t.id, prompt: [{ type: 'text', text: role === 'worker' ? buildWorkerPrompt(t, packNote) : buildVerifierPrompt(t, packNote) }], parent: parent, signal: makeSignal() }
|
|
@@ -399,7 +428,10 @@ export function apply(ctx) {
|
|
|
399
428
|
// 快进不写盘的前提是磁盘上的 poolStatus 已经是空的——否则(典型:DSH 重启清空了
|
|
400
429
|
// 内存 runs 表,文件里残留重启前的忙碌快照)幽灵 Worker 会永远显示执行中
|
|
401
430
|
var stalePool = snap.poolStatus && (((snap.poolStatus.workers || []).length + (snap.poolStatus.verifiers || []).length) > 0)
|
|
402
|
-
|
|
431
|
+
// dispatchInfo 是瞬时通知(90s TTL):空闲周期也要负责过期清理,否则永久残留
|
|
432
|
+
var staleInfo = snap.dispatchInfo && (!snap.dispatchInfoAt || Date.now() - new Date(snap.dispatchInfoAt).getTime() > 90000)
|
|
433
|
+
if (staleInfo) { delete snap.dispatchInfo; delete snap.dispatchInfoAt }
|
|
434
|
+
if (stalePool || staleInfo) { if (stalePool) snap.poolStatus = emptyPool; try { await wt(sid, snap) } catch (_) {} }
|
|
403
435
|
return snap
|
|
404
436
|
}
|
|
405
437
|
var c = cfg(snap)
|
|
@@ -412,16 +444,19 @@ export function apply(ctx) {
|
|
|
412
444
|
var now = Date.now()
|
|
413
445
|
d.tasks.forEach(function (t) {
|
|
414
446
|
if (isOrphan(d, t, runs, now)) { t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; ah(t, 'in-progress', 'pending', 'system', '执行 run 已结束/丢失,回收重新排队'); pushSysNote(sid, '任务「' + t.title + '」执行 run 丢失,已回收重新排队'); info.push('reclaim ' + t.id) }
|
|
447
|
+
// verifier 派发占位超时回收:占位后强杀/spawn 中断会留 spawn-pending 死占位,超 2min 清掉恢复可派发
|
|
448
|
+
if (t.status === 'verifying' && t.verifierRun === 'spawn-pending' && (now - new Date(t.verifierRunAt || 0).getTime()) > 120000) { t.verifierRun = null; delete t.verifierRunAt; ah(t, 'verifying', 'verifying', 'system', 'Verifier 派发占位超时,回收重新排队'); info.push('reclaim-verifier ' + t.id) }
|
|
415
449
|
})
|
|
416
450
|
// worker 派发仅 auto;verifier 派发两种模式都跑(manual 模式主窗口 claim 做完的 full 档任务需要验收)
|
|
417
451
|
var capW = isAuto ? Math.max(0, c.maxWorkers - activeW) : 0
|
|
418
|
-
var picked = pickDispatch(d, capW, Math.max(0, c.maxVerifiers - activeV),
|
|
452
|
+
var picked = pickDispatch(d, capW, Math.max(0, c.maxVerifiers - activeV), runs)
|
|
419
453
|
picked.pendings.forEach(function (t) { claimApply(d, t, 'spawn-pending', 'dispatch'); toSpawn.push({ role: 'worker', t: t }); info.push('dispatch ' + t.id) })
|
|
420
|
-
picked.verifs.forEach(function (t) { toSpawn.push({ role: 'verifier', t: t }); info.push('verify ' + t.id) })
|
|
454
|
+
picked.verifs.forEach(function (t) { t.verifierRun = 'spawn-pending'; t.verifierRunAt = new Date().toISOString(); toSpawn.push({ role: 'verifier', t: t }); info.push('verify ' + t.id) })
|
|
421
455
|
// UI 池状态:来自活跃 run(一次性模型:没有成员名册,只有在跑的任务)
|
|
422
456
|
d.poolStatus = { workers: [], verifiers: [] }
|
|
423
457
|
Object.keys(runs).forEach(function (k) { var rc = runs[k]; d.poolStatus[rc.role === 'worker' ? 'workers' : 'verifiers'].push({ id: k, num: '-', busy: true, taskId: rc.taskId, runId: String(rc.run.id), done: 0, queueLen: 0, suspect: false, model: rc.model || '' }) })
|
|
424
|
-
if (info.length > 0) d.dispatchInfo = info.join('; ')
|
|
458
|
+
if (info.length > 0) { d.dispatchInfo = info.join('; '); d.dispatchInfoAt = new Date().toISOString() }
|
|
459
|
+
else if (d.dispatchInfo && (!d.dispatchInfoAt || Date.now() - new Date(d.dispatchInfoAt).getTime() > 90000)) { delete d.dispatchInfo; delete d.dispatchInfoAt } // 瞬时通知:90s TTL 过期即清,不再永久残留
|
|
425
460
|
return d
|
|
426
461
|
}, true) // skipKick:poolCycle 自写不触发 kickCycle(防无限循环)
|
|
427
462
|
|
|
@@ -431,11 +466,15 @@ export function apply(ctx) {
|
|
|
431
466
|
var rec = await spawnOneShot(sid, sp.t, sp.role)
|
|
432
467
|
if (rec) {
|
|
433
468
|
// claim 占位换成真实 run id;verifier run 单独记(claimedBy 保留 worker 的,供详情页跳转会话)
|
|
434
|
-
await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === sp.t.id }); if (t) { if (sp.role === 'worker' && t.claimedBy === 'spawn-pending') t.claimedBy = String(rec.run.id); if (sp.role === 'verifier') t.verifierRun = String(rec.run.id) }; return t }, true)
|
|
469
|
+
await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === sp.t.id }); if (t) { if (sp.role === 'worker' && t.claimedBy === 'spawn-pending') t.claimedBy = String(rec.run.id); if (sp.role === 'verifier' && t.verifierRun === 'spawn-pending') { t.verifierRun = String(rec.run.id); t.verifierRunAt = new Date().toISOString() } }; return t }, true)
|
|
435
470
|
} else if (sp.role === 'worker') {
|
|
436
|
-
// spawn 失败 → 回 pending
|
|
471
|
+
// spawn 失败 → 回 pending
|
|
437
472
|
await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === sp.t.id }); if (t && t.status === 'in-progress' && t.claimedBy === 'spawn-pending') { t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; ah(t, 'in-progress', 'pending', 'system', 'spawn 失败,回收重新排队') }; return t }, true)
|
|
438
473
|
pushSysNote(sid, '任务「' + sp.t.title + '」Worker 启动失败,已重新排队')
|
|
474
|
+
} else if (sp.role === 'verifier') {
|
|
475
|
+
// verifier spawn 失败 → 清占位,下轮 cycle 重试(占位不清会永远卡住派发)
|
|
476
|
+
await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === sp.t.id }); if (t && t.verifierRun === 'spawn-pending') { t.verifierRun = null; delete t.verifierRunAt }; return t }, true)
|
|
477
|
+
pushSysNote(sid, '任务「' + sp.t.title + '」Verifier 启动失败,下轮自动重试')
|
|
439
478
|
}
|
|
440
479
|
}
|
|
441
480
|
return result
|
|
@@ -498,7 +537,7 @@ export function apply(ctx) {
|
|
|
498
537
|
// ===== Tools =====
|
|
499
538
|
ctx.tools.register(defineTool({ name: 'task_list', description: '列出当前会话任务。', parameters: { type: 'object', properties: { status: { type: 'string', enum: ['pending', 'in-progress', 'verifying', 'resolved', 'blocked', 'cancelled'] }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, tag: { type: 'string' }, parentId: { type: 'string' }, includeArchived: { type: 'boolean' }, limit: { type: 'number' } }, required: [] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var d = await rt(sid); var a = d.tasks; var ts = a; if (!args.includeArchived) ts = ts.filter(function (x) { return x.status !== 'archived' }); if (args.status) ts = ts.filter(function (x) { return x.status === args.status }); if (args.priority) ts = ts.filter(function (x) { return x.priority === args.priority }); if (args.tag) ts = ts.filter(function (x) { return (x.tags || []).indexOf(args.tag) >= 0 }); if (args.parentId === 'null') ts = ts.filter(function (x) { return !isb(x) }); else if (args.parentId) ts = ts.filter(function (x) { return x.parentId === args.parentId }); var po = PRIO_RANK; ts.sort(function (a, b) { var dd = (po[b.priority] || 0) - (po[a.priority] || 0); return dd !== 0 ? dd : (a.createdAt || '').localeCompare(b.createdAt || '') }); var lim = Math.min(args.limit || 20, 100); var res = ts.slice(0, lim).map(function (x) { var e = Object.assign({}, x); if (isb(x)) { var p = gpt(x, a); if (p) e.parentSummary = { id: p.id, title: p.title, status: p.status } }; var ch = gsb(x.id, a); if (ch.length) { e.subtaskCount = ch.length; e.subtaskResolved = ch.filter(function (y) { return y.status === 'resolved' }).length }; return e }); var out = { tasks: res, total: ts.length, session: sid, actor: getActorId(), boardMode: d.boardMode || 'auto', teamMode: !!d.teamMode, poolStatus: d.poolStatus }; if (d.teamMode) out.teamHint = 'Team 模式已开启:实质性改动请优先 task_create 提交看板由池执行;调研/读取/讨论可直接进行;Worker 歧义会上报等你裁决。'; return out } }))
|
|
500
539
|
ctx.tools.register(defineTool({ name: 'task_context', description: '获取任务完整上下文。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, expandFiles: { type: 'boolean' }, includeParent: { type: 'boolean' }, includeSubtasks: { type: 'boolean' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var d = await rt(sid); var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found: ' + args.taskId }; return { ok: true, context: { task: t, inheritedContext: t.context || {} } } } }))
|
|
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) } } } }))
|
|
540
|
+
ctx.tools.register(defineTool({ name: 'task_preview_context', description: '派发前上下文预览:预演 task_create 的 contextFiles/contextNotes 将注入给子代理的实际内容(读盘后的最终形态),用于确认材料是否足够。不创建任务。返回 ok=false/empty=true 说明无可注入内容。', parameters: { type: 'object', properties: { contextFiles: { type: 'array', items: { type: 'string' }, description: '预演的文件路径列表' }, contextNotes: { type: 'string', description: '预演的调研笔记' } }, required: [] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var files = Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []; var notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''; if (!files.length && !notes.trim()) return { ok: false, error: 'contextFiles/contextNotes 至少提供一个' }; try { var pack = await readContextPack(sid, { context: { files: files, notes: notes } }); return { ok: true, empty: !pack, pack: pack } } catch (e) { return { ok: false, error: String(e).slice(0, 200) } } } }))
|
|
502
541
|
ctx.tools.register(defineTool({ name: 'task_claim', description: '领取待办任务→in-progress。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, reason: { type: 'string' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; var err = claimCheck(d, t, actor); if (err) return { ok: false, error: err }; claimApply(d, t, actor, args.reason || 'claimed'); return { ok: true, task: t, context: { task: t, inheritedContext: t.context || {} } } }) } }))
|
|
503
542
|
ctx.tools.register(defineTool({ name: 'task_resolve', description: '提交验证(verifying)或阻塞(blocked)。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, status: { type: 'string', enum: ['verifying', 'blocked'] }, resolution: { type: 'string' } }, required: ['taskId', 'status'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'in-progress') return { ok: false, error: 'not in-progress' }; if (t.claimedBy !== actor) return { ok: false, error: 'not claimed by you' }; if (args.status === 'verifying' && !args.resolution) return { ok: false, error: 'resolution required' }; return resolveApply(d, t, actor, args.status, args.resolution, args.resolution || args.status) }) } }))
|
|
504
543
|
ctx.tools.register(defineTool({ name: 'task_verify', description: '验收:approved→resolved,rejected→in-progress。子任务全完成父任务自动verifying。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, verdict: { type: 'string', enum: ['approved', 'rejected'] }, comment: { type: 'string' } }, required: ['taskId', 'verdict'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var actor = getActorId(); return mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found' }; if (t.status !== 'verifying') return { ok: false, error: 'not verifying' }; return verifyApply(d, t, actor, args.verdict, args.comment) }) } }))
|
|
@@ -519,7 +558,7 @@ export function apply(ctx) {
|
|
|
519
558
|
var notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''
|
|
520
559
|
if (!files.length && !notes.trim()) return { ok: false, error: 'contextFiles/contextNotes 至少提供一个' }
|
|
521
560
|
var pack = ''
|
|
522
|
-
try { pack = await readContextPack({ context: { files: files, notes: notes } }) } catch (e) { return { ok: false, error: '读取失败: ' + String(e).slice(0, 120) } }
|
|
561
|
+
try { pack = await readContextPack(rpcSessionId(args), { context: { files: files, notes: notes } }) } catch (e) { return { ok: false, error: '读取失败: ' + String(e).slice(0, 120) } }
|
|
523
562
|
return { ok: true, filesCount: files.length, notesLen: notes.length, pack: pack, empty: !pack }
|
|
524
563
|
})
|
|
525
564
|
// 活动心跳:读子代理会话日志的最后一帧,提取最近的动作摘要(卡片/详情页展示"现在跑到哪了")
|
|
@@ -529,11 +568,21 @@ export function apply(ctx) {
|
|
|
529
568
|
if (!rec || !rec.run) return { ok: true, activity: null, reason: 'no active run' }
|
|
530
569
|
var child = String(rec.run.id)
|
|
531
570
|
try {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
571
|
+
// 子会话日志定位:直接扫描 ~/.dsh/sessions/*/<child>/session.vN.jsonl.zstd
|
|
572
|
+
// (不再从看板路径反推 workspace——那依赖进程 cwd 凑巧等于工作区,曾是隐性 bug)
|
|
573
|
+
// v0.1.7 起日志文件名带格式版本号(session.v4.jsonl.zstd),旧会话是
|
|
574
|
+
// session.v3.jsonl.zstd / session.jsonl.zstd——按新到旧逐个探测。
|
|
575
|
+
var sessionsRoot = path.join(os.homedir(), '.dsh', 'sessions')
|
|
576
|
+
var log = null
|
|
577
|
+
var LOG_NAMES = ['session.v4.jsonl.zstd', 'session.v3.jsonl.zstd', 'session.v2.jsonl.zstd', 'session.jsonl.zstd']
|
|
578
|
+
var buckets = fsNode.readdirSync(sessionsRoot)
|
|
579
|
+
for (var bi = 0; bi < buckets.length && !log; bi++) {
|
|
580
|
+
for (var ni = 0; ni < LOG_NAMES.length; ni++) {
|
|
581
|
+
var cand = path.join(sessionsRoot, buckets[bi], child, LOG_NAMES[ni])
|
|
582
|
+
if (fsNode.existsSync(cand)) { log = cand; break }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (!log) return { ok: true, activity: null, reason: 'log not found' }
|
|
537
586
|
// 只同步读末尾 2MB(日志追加写,末帧必在尾部)——整文件 readFileSync 在大日志上
|
|
538
587
|
// 会造成数十毫秒级同步 I/O,多任务轮询时叠加成全局卡顿
|
|
539
588
|
var sz = fsNode.statSync(log).size
|
|
@@ -543,18 +592,29 @@ export function apply(ctx) {
|
|
|
543
592
|
var fd = fsNode.openSync(log, 'r')
|
|
544
593
|
try { fsNode.readSync(fd, buf, 0, buf.length, start) } finally { fsNode.closeSync(fd) }
|
|
545
594
|
var MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
var lines = text.split('\n').filter(Boolean)
|
|
595
|
+
// 追加写多帧格式:最新事件在末帧。但末帧可能只有 step/end、turn/end 这类
|
|
596
|
+
// 结算事件(v4 一帧只装一个step的增量),单解一帧经常捞不到动作 → 从新到旧
|
|
597
|
+
// 最多回扫 3 帧,找到第一个动作摘要即止(成本仍受控:每帧只是一次 zstd 解压)
|
|
550
598
|
var activity = null
|
|
551
|
-
for (var
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
599
|
+
for (var frame = 0; frame < 3 && !activity; frame++) {
|
|
600
|
+
var last = buf.lastIndexOf(MAGIC, frame === 0 ? buf.length - 1 : last - 1)
|
|
601
|
+
if (last < 0) break
|
|
602
|
+
var text = zstdDecompressSync(buf.subarray(last)).toString('utf8')
|
|
603
|
+
var lines = text.split('\n').filter(Boolean)
|
|
604
|
+
for (var i = lines.length - 1; i >= 0 && !activity; i--) {
|
|
605
|
+
try {
|
|
606
|
+
var e = JSON.parse(lines[i])
|
|
607
|
+
var dta = e.data || {}
|
|
608
|
+
if (e.type === 'tool/call' && dta.name) activity = '🔧 ' + dta.name + ' ' + String(dta.arguments || '').replace(/\s+/g, ' ').slice(0, 90)
|
|
609
|
+
// v4:助手文本在 assistant/message 的 content 块里;v3 及以前是 assistant/chunk 流片
|
|
610
|
+
else if (e.type === 'assistant/message') {
|
|
611
|
+
var msg = dta.message || {}
|
|
612
|
+
var blocks = Array.isArray(msg.content) ? msg.content : []
|
|
613
|
+
for (var b = blocks.length - 1; b >= 0; b--) { if (blocks[b] && blocks[b].type === 'text' && String(blocks[b].text || '').trim()) { activity = '💬 ' + String(blocks[b].text).replace(/\s+/g, ' ').slice(0, 120); break } }
|
|
614
|
+
}
|
|
615
|
+
else if (e.type === 'assistant/chunk' && dta.block && dta.block.type === 'text' && dta.block.text && dta.block.text.trim()) activity = '💬 ' + dta.block.text.replace(/\s+/g, ' ').slice(0, 120)
|
|
616
|
+
} catch (_) {}
|
|
617
|
+
}
|
|
558
618
|
}
|
|
559
619
|
return { ok: true, activity: activity }
|
|
560
620
|
} catch (e) { return { ok: true, activity: null, reason: String(e).slice(0, 80) } }
|
|
@@ -799,7 +859,11 @@ export function apply(ctx) {
|
|
|
799
859
|
req.on('error', reject)
|
|
800
860
|
})
|
|
801
861
|
}
|
|
802
|
-
ctx.webServer.register
|
|
862
|
+
// ctx.webServer.register 只返回释放器、不绑定调用方 fiber(tools.register 才会),
|
|
863
|
+
// 必须自己 ctx.effect 包住——否则插件禁用/重载后路由残留,handler 闭包指向已销毁的
|
|
864
|
+
// fiber 内状态(500),且再次启用时撞 "duplicate exact route" 永远起不来。
|
|
865
|
+
ctx.effect(function () {
|
|
866
|
+
return ctx.webServer.register({
|
|
803
867
|
kind: 'exact',
|
|
804
868
|
path: '/dsh-agent-board',
|
|
805
869
|
handler: async function (req, res) {
|
|
@@ -812,6 +876,7 @@ export function apply(ctx) {
|
|
|
812
876
|
if (!fn) { res.writeHead(404); res.end(JSON.stringify({ ok: false, message: 'unknown method: ' + payload.method })); return }
|
|
813
877
|
try { var out = await fn(payload.args); res.writeHead(200); res.end(JSON.stringify(out === undefined ? null : out)) } catch (e) { res.writeHead(500); res.end(JSON.stringify({ ok: false, message: String(e) })) }
|
|
814
878
|
},
|
|
879
|
+
})
|
|
815
880
|
})
|
|
816
881
|
|
|
817
882
|
console.log('[task-board] v74 loaded (pool removed: one-shot dispatch, context injected per task, dispose on settle)')
|
package/lib/client.js
CHANGED
|
@@ -16,7 +16,10 @@ function apply(ctx) {
|
|
|
16
16
|
var slots = ctx.get('slots')
|
|
17
17
|
if (slots === undefined) return
|
|
18
18
|
var sessionsSvc = ctx.get('sessions')
|
|
19
|
-
|
|
19
|
+
// 会话跳转走 uiWorkspace.openSession(0.1.7 起 sessions 服务已无 open 方法,
|
|
20
|
+
// 旧调用 sessionsSvc.open 会抛 TypeError: sessionsSvc.open is not a function)
|
|
21
|
+
var uiWorkspaceSvc = ctx.get('uiWorkspace')
|
|
22
|
+
var C = { bg: 'var(--dsw-alias-bg-base)', card: 'var(--dsw-alias-bg-layer-1)', nested: 'var(--dsw-alias-bg-layer-2)', border: 'var(--dsw-alias-border-l1)', border2: 'var(--dsw-alias-border-l2)', brand: 'var(--dsw-alias-brand-primary)', text: 'var(--dsw-alias-label-primary)', text2: 'var(--dsw-alias-label-secondary)', err: 'var(--dsw-alias-state-error-primary)', ok: 'var(--dsw-alias-state-success-primary)', warn: 'var(--dsw-alias-state-warn-primary)' }
|
|
20
23
|
var C_INV = 'var(--dsw-alias-label-primary-inverted)' // 品牌底/彩色底上的反色文字(深浅模式自动反转)
|
|
21
24
|
var prioColor = { critical: C.err, high: C.warn, medium: C.brand, low: C.text2 }
|
|
22
25
|
var prioLabel = { critical: '紧急', high: '高', medium: '中', low: '低' }
|
|
@@ -240,7 +243,7 @@ function apply(ctx) {
|
|
|
240
243
|
state.poolStatus = (d && d.poolStatus) || null
|
|
241
244
|
state.isRoot = !d || d.isRoot !== false
|
|
242
245
|
if (state.isRoot === false && state.open) { state.open = false; state.detailId = null } // 子代理会话:强制收起看板
|
|
243
|
-
if (d && d.dispatchInfo) { state.dispatchInfo = d.dispatchInfo }
|
|
246
|
+
if (d && d.dispatchInfo && d.dispatchInfoAt && Date.now() - new Date(d.dispatchInfoAt).getTime() < 120000) { state.dispatchInfo = d.dispatchInfo } else { state.dispatchInfo = '' } // 瞬时通知 2min 内有效,过期强制清空(服务端写后不清曾致残留数天)
|
|
244
247
|
// escalation 一等公民:出现新的待裁决任务 → 面板自动弹开直达该任务详情
|
|
245
248
|
var newEsc = state.tasks.filter(function (t) { return t.escalation && state.escalatedIds.indexOf(t.id) < 0 })
|
|
246
249
|
state.escalatedIds = state.tasks.filter(function (t) { return t.escalation }).map(function (t) { return t.id })
|
|
@@ -303,7 +306,7 @@ function apply(ctx) {
|
|
|
303
306
|
function fmtTime(iso) { if (!iso) return '-'; try { return new Date(iso).toLocaleString() } catch (_) { return iso } }
|
|
304
307
|
function getTask(id) { return state.tasks.find(function (t) { return t.id === id }) }
|
|
305
308
|
function shortId(sid) { return sid ? String(sid).slice(0, 14) : '' }
|
|
306
|
-
function jumpTo(actorId) { if (
|
|
309
|
+
function jumpTo(actorId) { if (uiWorkspaceSvc && actorId && actorId !== 'system' && actorId !== 'unknown' && actorId !== 'auto-dispatch') uiWorkspaceSvc.openSession(actorId) }
|
|
307
310
|
function transition(taskId, from, to) {
|
|
308
311
|
if (from === to) return
|
|
309
312
|
var ok = function () { fetchTasks() }; var fail = function () { fetchTasks() }
|
|
@@ -378,8 +381,8 @@ function apply(ctx) {
|
|
|
378
381
|
setRange(fmt(from), fmt(to))
|
|
379
382
|
}
|
|
380
383
|
var headBtn = { fontSize: 11, padding: '3px 10px', border: '1px solid ' + (rg.from || rg.to ? C.brand : C.border), borderRadius: 5, background: (rg.from || rg.to) ? C.nested : C.card, color: (rg.from || rg.to) ? C.brand : C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }
|
|
381
|
-
var chip = { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.
|
|
382
|
-
var dateInput = { fontSize: 11, padding: '2px 6px', border: '1px solid ' + C.
|
|
384
|
+
var chip = { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.border2, borderRadius: 10, background: C.card, color: C.text2, cursor: 'pointer' }
|
|
385
|
+
var dateInput = { fontSize: 11, padding: '2px 6px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text }
|
|
383
386
|
return React.createElement('div', { style: { marginBottom: 10 } },
|
|
384
387
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
385
388
|
React.createElement('button', { onClick: function () { state.rfOpen = !open; setOpen(!open) }, style: headBtn },
|
|
@@ -420,7 +423,7 @@ function apply(ctx) {
|
|
|
420
423
|
open ? (shown.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2, padding: '6px 0' } }, '时间范围内无活跃会话') : shown.map(function (b, i) {
|
|
421
424
|
var isSelf = b.session === state.sessionId
|
|
422
425
|
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 &&
|
|
426
|
+
React.createElement('span', { onClick: function () { if (!isSelf && uiWorkspaceSvc) uiWorkspaceSvc.openSession(b.session) }, style: { color: isSelf ? C.text2 : C.brand, cursor: isSelf ? 'default' : 'pointer', textDecoration: isSelf ? 'none' : 'underline', minWidth: 90 } }, (isSelf ? '★ ' : '') + shortId(b.session)),
|
|
424
427
|
React.createElement('span', { style: { color: C.text2 } }, b.teamMode ? 'Team' : (b.boardMode === 'auto' ? '自动' : '手动')),
|
|
425
428
|
React.createElement('span', { style: { display: 'inline-flex', gap: 5 } },
|
|
426
429
|
b.counts.inProgress ? React.createElement('span', { style: { color: C.brand } }, '▶' + b.counts.inProgress) : null,
|
|
@@ -486,7 +489,7 @@ function apply(ctx) {
|
|
|
486
489
|
function ModeSwitch(props) { var mode = props.mode; var teamOn = props.teamOn; function pick(m) { if (teamOn && m === 'manual') return; rpc('set-board-mode', { mode: m }).then(fetchTasks).catch(function () {}) } var btnBase = { fontSize: 12, padding: '4px 14px', border: 'none', cursor: 'pointer', fontWeight: 500, flex: 1, textAlign: 'center', borderRadius: 6, transition: 'all .15s' }; var manDis = teamOn; return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 8, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { pick('auto') }, style: Object.assign({}, btnBase, mode === 'auto' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('bot', 12, '自动')), React.createElement('button', { onClick: function () { pick('manual') }, disabled: manDis, title: manDis ? 'Team 模式开启时不可切换到手动' : '', style: Object.assign({}, btnBase, mode === 'manual' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: manDis ? C.text2 + '88' : C.text2 }, manDis ? { cursor: 'not-allowed', opacity: 0.5 } : {}) }, icText('user', 12, '手动'))) }
|
|
487
490
|
function TeamSwitch(props) { var on = props.on; function toggle() { rpc('set-team-mode', { enabled: !on }).then(fetchTasks).catch(function () {}) } return React.createElement('button', { onClick: toggle, title: on ? 'Team 模式已开启:任务走看板派发,Worker 歧义自动上报主窗口裁决(不拦截主窗口编辑)' : '开启 Team 模式:任务全走看板,Worker 歧义上报主窗口裁决', style: { fontSize: 12, padding: '4px 10px', border: '1px solid ' + (on ? C.brand : C.border), borderRadius: 8, cursor: 'pointer', fontWeight: 500, background: on ? C.brand : 'transparent', color: on ? C_INV : C.text2, transition: 'all .15s' } }, icText('users', 12, 'Team' + (on ? ' ON' : ''))) }
|
|
488
491
|
function ViewTab() { var btnBase = { fontSize: 11, padding: '3px 10px', border: 'none', cursor: 'pointer', fontWeight: 500, borderRadius: 5, transition: 'all .15s' }; function goArch() { state.view = 'archive'; notify(); fetchArchived() } return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 6, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { state.view = 'board'; notify() }, style: Object.assign({}, btnBase, state.view === 'board' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('clipboard-list', 12), '看板')), React.createElement('button', { onClick: function () { state.view = 'team'; notify() }, style: Object.assign({}, btnBase, state.view === 'team' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('users', 12, '团队')), React.createElement('button', { onClick: function () { state.view = 'dashboard'; notify() }, style: Object.assign({}, btnBase, state.view === 'dashboard' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('bar-chart-3', 12, '仪表盘')), React.createElement('button', { onClick: goArch, title: '已归档任务(可检索、可恢复)', style: Object.assign({}, btnBase, state.view === 'archive' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('archive', 12, '归档'))) }
|
|
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.
|
|
492
|
+
function PoolCfg(props) { var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]; var _b = useState(false), dirty = _b[0], setDirty = _b[1]; useEffect(function () { setVal(String(props.value)); setDirty(false) }, [props.value]); function commit(v) { var n = parseInt(v, 10); if (!isNaN(n) && n >= 0 && n <= 10) { setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(fetchTasks).catch(function () {}) } } function step(d) { var n = parseInt(val, 10) || 0; commit(String(Math.max(0, Math.min(10, n + d)))) } var miniBtn = { fontSize: 9, padding: '0px 4px', border: '1px solid ' + C.border2, borderRadius: 2, background: C.nested, color: C.text2, cursor: 'pointer', lineHeight: '14px' }; return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 1, fontSize: 9, color: C.text2 } }, props.label, React.createElement('button', { onClick: function () { step(-1) }, title: '减 1', style: miniBtn }, '−'), React.createElement('input', { value: val, onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== String(props.value)) }, onBlur: function () { if (dirty) commit(val) }, onKeyDown: function (e) { if (e.key === 'Enter') commit(val) }, style: { width: 22, padding: '0px 2px', fontSize: 9, textAlign: 'center', border: '1px solid ' + (dirty ? C.brand : C.border2), borderRadius: 2, background: C.card, color: C.text } }), React.createElement('button', { onClick: function () { step(1) }, title: '加 1', style: miniBtn }, '+'), dirty ? React.createElement('button', { onClick: function () { commit(val) }, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null) }
|
|
490
493
|
function MinCfg(props) {
|
|
491
494
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
492
495
|
var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]
|
|
@@ -504,7 +507,7 @@ function apply(ctx) {
|
|
|
504
507
|
onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== String(props.value)) },
|
|
505
508
|
onBlur: function () { if (dirty) commit(val) },
|
|
506
509
|
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.
|
|
510
|
+
style: { width: 38, padding: '0px 3px', fontSize: 9, textAlign: 'center', border: '1px solid ' + (dirty ? C.brand : C.border2), borderRadius: 2, background: C.card, color: C.text }
|
|
508
511
|
}),
|
|
509
512
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '分'),
|
|
510
513
|
dirty ? React.createElement('button', { onClick: function () { commit(val) }, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null)
|
|
@@ -545,17 +548,25 @@ function apply(ctx) {
|
|
|
545
548
|
}
|
|
546
549
|
// 降级:文本输入(模型枚举不可用时)
|
|
547
550
|
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
|
|
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.
|
|
551
|
+
React.createElement('input', { value: val, onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== (props.value || '')) }, onBlur: function () { if (dirty) save() }, onKeyDown: function (e) { if (e.key === 'Enter') save() }, placeholder: '空=同父级', style: { width: 110, padding: '0px 4px', fontSize: 9, border: '1px solid ' + (dirty ? C.brand : C.border2), borderRadius: 2, background: C.card, color: C.text } }),
|
|
549
552
|
dirty ? React.createElement('button', { onClick: save, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null)
|
|
550
553
|
}
|
|
551
554
|
function PoolCfgPopover(props) {
|
|
552
555
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
|
|
553
556
|
var _a = useState(false), open = _a[0], setOpen = _a[1]
|
|
554
|
-
var
|
|
557
|
+
var _p = useState(null), pos = _p[0], setPos = _p[1]
|
|
558
|
+
var ref = useRef(null); var btnRef = useRef(null)
|
|
555
559
|
useEffect(function () { if (!open) return; function onDown(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }; document.addEventListener('mousedown', onDown); return function () { document.removeEventListener('mousedown', onDown) } }, [open])
|
|
560
|
+
function toggle() {
|
|
561
|
+
// position:fixed + 视口坐标:面板容器是 overflow:hidden + maxHeight:60vh,
|
|
562
|
+
// absolute 弹窗在面板内容短时会被裁掉下半截(超时配置行曾被整个裁掉)。
|
|
563
|
+
// fixed 脱离任何祖先裁剪(面板无 transform/filter,不会形成包含块)。
|
|
564
|
+
if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos({ top: r.bottom + 4, right: Math.max(8, window.innerWidth - r.right) }) }
|
|
565
|
+
setOpen(!open)
|
|
566
|
+
}
|
|
556
567
|
return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
|
|
557
|
-
React.createElement('button', {
|
|
558
|
-
open ? React.createElement('div', { style: { position: '
|
|
568
|
+
React.createElement('button', { ref: btnRef, onClick: toggle, title: '池配置', style: { fontSize: 12, padding: '3px 8px', border: '1px solid ' + (open ? C.brand : C.border), borderRadius: 6, cursor: 'pointer', background: open ? C.nested : 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('settings', 13)),
|
|
569
|
+
open && pos ? React.createElement('div', { style: { position: 'fixed', top: pos.top + 'px', right: pos.right + 'px', padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 4px 16px rgba(0,0,0,0.15)', zIndex: 1000, whiteSpace: 'nowrap', maxHeight: 'calc(100vh - ' + (pos.top + 12) + 'px)', overflowY: 'auto' } },
|
|
559
570
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
|
|
560
571
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
|
|
561
572
|
React.createElement(PoolCfg, { label: 'W并发', cfgKey: 'maxWorkers', value: props.maxW }),
|
|
@@ -626,7 +637,7 @@ function apply(ctx) {
|
|
|
626
637
|
var hasFilter = q.trim() || fp.length > 0 || ft
|
|
627
638
|
var chipBase = { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: '1px solid ' + C.border, cursor: 'pointer', background: 'transparent', color: C.text2, transition: 'all .15s' }
|
|
628
639
|
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
|
|
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.
|
|
640
|
+
React.createElement('input', { value: q, onChange: function (e) { setQ2(e.target.value) }, placeholder: '🔍 搜索标题/描述/ID…', style: { flex: '0 1 200px', fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text } }),
|
|
630
641
|
['critical', 'high', 'medium', 'low'].map(function (p) { var on = fp.indexOf(p) >= 0; return React.createElement('button', { key: p, onClick: function () { togglePrio(p) }, style: Object.assign({}, chipBase, on ? { background: prioColor[p], color: C_INV, borderColor: prioColor[p] } : {}) }, prioLabel[p]) }),
|
|
631
642
|
allTags().length > 0 ? React.createElement('select', { value: ft, onChange: function (e) { state.filterTag = e.target.value; notify() }, style: { fontSize: 10, padding: '2px 4px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text } }, React.createElement('option', { value: '' }, '🏷 全部标签'), allTags().map(function (g) { return React.createElement('option', { key: g, value: g }, g) })) : null,
|
|
632
643
|
hasFilter ? React.createElement('button', { onClick: clearAll, style: { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: 'none', background: C.nested, color: C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('x', 9), '清除') : null)
|
|
@@ -668,7 +679,7 @@ function apply(ctx) {
|
|
|
668
679
|
})
|
|
669
680
|
function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchArchived(); fetchTasks() }).catch(function () {}) }
|
|
670
681
|
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.
|
|
682
|
+
React.createElement('input', { value: q, onChange: function (e) { setQ(e.target.value); state.archQ = e.target.value }, placeholder: '检索标题 / ID / 标签…', style: { width: '100%', padding: '5px 8px', fontSize: 11, border: '1px solid ' + C.border2, borderRadius: 5, background: C.card, color: C.text, marginBottom: 8 } }),
|
|
672
683
|
state.archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
|
|
673
684
|
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
674
685
|
list.map(function (t) {
|
|
@@ -688,7 +699,7 @@ function apply(ctx) {
|
|
|
688
699
|
var seq = r.role === 'verifier' ? (++vN) : (++wN)
|
|
689
700
|
var isV = r.role === 'verifier'
|
|
690
701
|
return React.createElement('button', {
|
|
691
|
-
key: i, onClick: function () { if (
|
|
702
|
+
key: i, onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
|
|
692
703
|
title: (isV ? 'Verifier' : 'Worker') + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
|
|
693
704
|
style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + (isV ? C.warn : C.brand), borderRadius: 3, background: 'transparent', color: isV ? C.warn : C.brand, cursor: 'pointer' }
|
|
694
705
|
}, '→ ' + (isV ? 'V' : 'W') + '#' + seq)
|
|
@@ -712,7 +723,25 @@ function apply(ctx) {
|
|
|
712
723
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '一次性子代理会话' }, '会话 ' + String(m.runId || '').slice(0, 8) + '…(用完即销毁)'))
|
|
713
724
|
}
|
|
714
725
|
var ws = ps.workers || [], vs = ps.verifiers || []
|
|
726
|
+
var _R2 = React; var _st = _R2.useState(''), editMsg = _st[0], setEditMsg = _st[1]
|
|
727
|
+
function saveTimeout(cfgKey, v, min, max) {
|
|
728
|
+
var n = parseInt(v, 10)
|
|
729
|
+
if (isNaN(n) || n < min || n > max) { setEditMsg('范围 ' + min + '~' + max + ' 分钟'); setTimeout(function () { setEditMsg('') }, 2500); return }
|
|
730
|
+
rpc('set-board-config', { key: cfgKey, value: n }).then(function () { setEditMsg('✅ 已保存'); setTimeout(function () { setEditMsg('') }, 2000); fetchTasks() }).catch(function () { setEditMsg('⚠️ 保存失败'); setTimeout(function () { setEditMsg('') }, 2500) })
|
|
731
|
+
}
|
|
732
|
+
var tIn = { width: 44, padding: '1px 3px', fontSize: 10, textAlign: 'center', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.text }
|
|
715
733
|
return React.createElement('div', null,
|
|
734
|
+
React.createElement('div', { style: { marginBottom: 10, padding: '6px 8px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', fontSize: 10, color: C.text2 } },
|
|
735
|
+
ic('activity', 11),
|
|
736
|
+
React.createElement('span', null, '超时策略:'),
|
|
737
|
+
React.createElement('span', { style: { color: C.brand, fontWeight: 600 } }, '软超时'),
|
|
738
|
+
React.createElement('input', { type: 'number', defaultValue: String(state.softTimeoutMin || 30), min: 1, max: 480, title: '超过该时长只提醒主窗口,不终止 run', onBlur: function (e) { saveTimeout('softTimeoutMin', e.target.value, 1, 480) }, onKeyDown: function (e) { if (e.key === 'Enter') saveTimeout('softTimeoutMin', e.target.value, 1, 480) }, style: tIn }),
|
|
739
|
+
React.createElement('span', null, '分(仅提醒,由你决定继续等待或终止)'),
|
|
740
|
+
React.createElement('span', { style: { margin: '0 3px', color: C.border } }, '·'),
|
|
741
|
+
React.createElement('span', { style: { color: C.warn, fontWeight: 600 } }, '硬超时'),
|
|
742
|
+
React.createElement('input', { type: 'number', defaultValue: String(state.hardTimeoutMin || 120), min: 1, max: 1440, title: '人不在线时兜底:自动终止挂死 run 并重试', onBlur: function (e) { saveTimeout('hardTimeoutMin', e.target.value, 1, 1440) }, onKeyDown: function (e) { if (e.key === 'Enter') saveTimeout('hardTimeoutMin', e.target.value, 1, 1440) }, style: tIn }),
|
|
743
|
+
React.createElement('span', null, '分(兜底自动终止并重试)'),
|
|
744
|
+
editMsg ? React.createElement('span', { style: { color: editMsg.indexOf('✅') === 0 ? C.ok : C.err, fontWeight: 600 } }, editMsg) : null),
|
|
716
745
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: C.text, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('zap', 12), 'Worker 池 (' + ws.length + ')'),
|
|
717
746
|
ws.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 10 } }, '暂无 Worker(有待办任务时自动扩容)') : React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 12 } }, ws.map(function (m) { return memberCard(m, 'worker') })),
|
|
718
747
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: C.text, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('clipboard-check', 12), 'Verifier 池 (' + vs.length + ')'),
|
|
@@ -749,7 +778,7 @@ function apply(ctx) {
|
|
|
749
778
|
if (!task) { state.detailId = null; return React.createElement('div', { style: { padding: 20, color: C.text2 } }, '任务不存在') }
|
|
750
779
|
function doAction(fn) { fn().then(fetchTasks).catch(function () {}) }
|
|
751
780
|
function saveEdit() { setSaving(true); rpc('update-task', { taskId: task.id, title: editTitle, description: editDesc, resetToPending: true }).then(function () { setSaving(false); fetchTasks() }).catch(function () { setSaving(false) }) }
|
|
752
|
-
function jumpToAgent() { if (
|
|
781
|
+
function jumpToAgent() { if (uiWorkspaceSvc && task.claimedBy) uiWorkspaceSvc.openSession(task.claimedBy) }
|
|
753
782
|
function submitArbitration() { if (!arbAnswer.trim()) return; rpc('resolve-escalation', { taskId: task.id, answer: arbAnswer }).then(function (r) { setActionMsg(r && r.ok ? '✅ 裁决已转达给 Worker' : '⚠️ ' + ((r && r.error) || '失败')); setArbAnswer(''); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
754
783
|
function doTerminate() { rpc('terminate-agent', { taskId: task.id }).then(function (r) { setActionMsg(r && r.ok ? '⏹ 已终止 ' + (r.terminated || '') + ',任务重新排队' : '⚠️ ' + ((r && r.error) || '无活动 Agent')); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
755
784
|
function doDismiss() { rpc('dismiss-suspect', { taskId: task.id }).then(function () { setActionMsg('✅ 已清除卡死标记,继续观察'); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
|
|
@@ -779,10 +808,10 @@ function apply(ctx) {
|
|
|
779
808
|
React.createElement('button', { onClick: doTerminate, style: { fontSize: 11, padding: '4px 12px', border: 'none', borderRadius: 4, background: C.err, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('stop-circle', 12), '终止任务(重新排队)'),
|
|
780
809
|
React.createElement('button', { onClick: doDismiss, style: { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 4, background: 'transparent', color: C.text2, cursor: 'pointer' } }, '继续观察'),
|
|
781
810
|
canJump ? React.createElement('button', { onClick: jumpToAgent, style: { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.brand, borderRadius: 4, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '→ 查看会话') : null)) : null,
|
|
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.
|
|
811
|
+
React.createElement('input', { value: editTitle, onChange: function (e) { setEditTitle(e.target.value) }, style: { width: '100%', fontSize: 13, fontWeight: 600, padding: '4px 6px', marginBottom: 6, border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, boxSizing: 'border-box' } }),
|
|
783
812
|
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 8, lineHeight: 1.7 } }, React.createElement('div', null, 'ID: ' + task.id), React.createElement('div', null, '创建: ' + fmtTime(task.createdAt) + '(' + ago(task.createdAt) + ')'), task.claimedBy ? React.createElement('div', null, '领取人: ', React.createElement(ActorLink, { id: task.claimedBy }), ' · ' + ago(task.claimedAt)) : null, task.resolvedAt ? React.createElement('div', null, '提交: ' + fmtTime(task.resolvedAt)) : null, task.verifiedAt ? React.createElement('div', null, '验收: ' + fmtTime(task.verifiedAt) + ' by ', React.createElement(ActorLink, { id: task.verifiedBy })) : null),
|
|
784
813
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 3 } }, '任务描述'),
|
|
785
|
-
React.createElement('textarea', { value: editDesc, onChange: function (e) { setEditDesc(e.target.value) }, rows: 3, style: { width: '100%', fontSize: 12, padding: '6px 8px', marginBottom: 6, border: '1px solid ' + C.
|
|
814
|
+
React.createElement('textarea', { value: editDesc, onChange: function (e) { setEditDesc(e.target.value) }, rows: 3, style: { width: '100%', fontSize: 12, padding: '6px 8px', marginBottom: 6, border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' } }),
|
|
786
815
|
task.context && task.context.instructions ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '指引: ' + task.context.instructions) : null,
|
|
787
816
|
task.context && Array.isArray(task.context.files) && task.context.files.length > 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'flex-start', gap: 4 } }, ic('file-text', 11), React.createElement('span', null, '预研文件(派发时注入子代理): ' + task.context.files.join('、'))) : null,
|
|
788
817
|
task.context && task.context.notes ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4, whiteSpace: 'pre-wrap', maxHeight: 100, overflowY: 'auto' } }, '调研笔记: ' + task.context.notes) : null,
|
|
@@ -824,7 +853,7 @@ function apply(ctx) {
|
|
|
824
853
|
var isCur = (r.role === 'worker' && task.claimedBy === r.id) || (r.role === 'verifier' && task.verifierRun === r.id)
|
|
825
854
|
var okMark = r.outcome === 'completed' ? ' ✅' : (r.outcome === 'running' ? ' ⏳' : (r.outcome ? ' ⚠' : ''))
|
|
826
855
|
return React.createElement('button', {
|
|
827
|
-
key: i, onClick: function () { if (
|
|
856
|
+
key: i, onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
|
|
828
857
|
title: rm.tip + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + (r.outcome ? ' · ' + r.outcome : '') + '(' + r.id + ')',
|
|
829
858
|
style: { fontSize: 10, padding: '2px 8px', border: '1px solid ' + (isCur ? rm.color : C.border), borderRadius: 3, background: isCur ? C.card : 'transparent', color: rm.color, cursor: 'pointer', fontWeight: isCur ? 600 : 400 }
|
|
830
859
|
}, '→ ' + rm.tip + ' #' + seq + (r.at ? ' · ' + ago(r.at) : '') + okMark)
|
|
@@ -833,7 +862,7 @@ function apply(ctx) {
|
|
|
833
862
|
canIntervene ? React.createElement('div', { style: { marginTop: 8, padding: '6px 8px', border: '1px dashed ' + C.warn, borderRadius: 6 } },
|
|
834
863
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('zap', 11), '高优先级介入(插入执行 Agent 队首)'),
|
|
835
864
|
React.createElement('div', { style: { display: 'flex', gap: 4 } },
|
|
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.
|
|
865
|
+
React.createElement('input', { value: interveneMsg, onChange: function (e) { setInterveneMsg(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') submitIntervene() }, placeholder: '给执行中的 Agent 下达高优指令…', style: { flex: 1, fontSize: 11, padding: '4px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text } }),
|
|
837
866
|
React.createElement('button', { onClick: submitIntervene, disabled: !interveneMsg.trim(), style: { fontSize: 11, padding: '4px 10px', border: 'none', borderRadius: 4, background: C.warn, color: C_INV, cursor: 'pointer', fontWeight: 600 } }, '介入'))) : null)
|
|
838
867
|
}
|
|
839
868
|
function TopPanel() {
|
|
@@ -881,7 +910,7 @@ function apply(ctx) {
|
|
|
881
910
|
slots.inject('shell.overlay', function () { return slots.register({ name: 'shell.overlay', id: 'task-board-top-panel' }, function () { return React.createElement(TopPanel) }) })
|
|
882
911
|
}
|
|
883
912
|
|
|
884
|
-
module.exports = { name: 'dsh-agent-board', inject: ['slots', 'sessions', 'timer'], apply: apply }
|
|
913
|
+
module.exports = { name: 'dsh-agent-board', inject: ['slots', 'sessions', 'uiWorkspace', 'timer'], apply: apply }
|
|
885
914
|
return module.exports
|
|
886
915
|
}
|
|
887
916
|
})
|
package/lib/core.mjs
CHANGED
|
@@ -162,7 +162,7 @@ export function buildVerifierPrompt(t, pack) {
|
|
|
162
162
|
export function pickDispatch(d, capW, capV, busyTaskIds) {
|
|
163
163
|
var pendings = capW > 0 ? d.tasks.filter(function (t) { return t.status === 'pending' && !t.claimedBy && t.assignMode !== 'manual' && t.pipeline !== 'direct' && depsSatisfied(d, t) && !t.escalation })
|
|
164
164
|
.sort(function (a, b) { var p = (PRIO_RANK[b.priority] || 2) - (PRIO_RANK[a.priority] || 2); return p !== 0 ? p : (a.createdAt || '').localeCompare(b.createdAt || '') }).slice(0, capW) : []
|
|
165
|
-
var verifs = capV > 0 ? d.tasks.filter(function (t) { return t.status === 'verifying' && (!t.pipeline || t.pipeline === 'full') && !t.escalation && !(busyTaskIds && busyTaskIds[t.id]) }).slice(0, capV) : []
|
|
165
|
+
var verifs = capV > 0 ? d.tasks.filter(function (t) { return t.status === 'verifying' && (!t.pipeline || t.pipeline === 'full') && !t.escalation && t.verifierRun !== 'spawn-pending' && !(busyTaskIds && busyTaskIds[t.id]) }).slice(0, capV) : []
|
|
166
166
|
return { pendings: pendings, verifs: verifs }
|
|
167
167
|
}
|
|
168
168
|
// 孤儿回收判定:in-progress 且 claimedBy 非主会话、无活跃 run、无 escalation、超 2 分钟
|
package/package.json
CHANGED
package/test/core.test.mjs
CHANGED
|
@@ -274,6 +274,19 @@ test('pickDispatch: verifying 只派 full 档,排除 escalation 和忙中', ()
|
|
|
274
274
|
assert.deepEqual(r.verifs.map(t => t.id), ['vf']) // work 档不派审、escalation 不派、忙中不派
|
|
275
275
|
})
|
|
276
276
|
|
|
277
|
+
test('pickDispatch: verifying spawn-pending 占位不重复派发(防双 Verifier)', () => {
|
|
278
|
+
const tasks = [
|
|
279
|
+
mkTask({ id: 'vp', status: 'verifying', pipeline: 'full', verifierRun: 'spawn-pending' }),
|
|
280
|
+
mkTask({ id: 'vd', status: 'verifying', pipeline: 'full', verifierRun: '12345' }), // 有真实 run id 但 run 已死(崩溃恢复场景)→ 应重派
|
|
281
|
+
mkTask({ id: 'vn', status: 'verifying', pipeline: 'full' }), // 从未派发 → 应派
|
|
282
|
+
]
|
|
283
|
+
const r = core.pickDispatch(mkBoard(tasks), 0, 5, null)
|
|
284
|
+
assert.deepEqual(r.verifs.map(t => t.id), ['vd', 'vn'])
|
|
285
|
+
// 占位 + 忙中双重排除
|
|
286
|
+
const r2 = core.pickDispatch(mkBoard(tasks), 0, 5, { vd: true })
|
|
287
|
+
assert.deepEqual(r2.verifs.map(t => t.id), ['vn'])
|
|
288
|
+
})
|
|
289
|
+
|
|
277
290
|
// ===== 孤儿回收 =====
|
|
278
291
|
test('isOrphan: 认领者非主会话 + 无活跃 run + 超 2 分钟', () => {
|
|
279
292
|
const old = new Date(Date.now() - 200000).toISOString()
|