dsh-agent-board 1.1.3 → 1.2.0
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 +107 -17
- package/lib/client.js +243 -34
- package/lib/core.mjs +3 -2
- package/package.json +1 -2
- package/test/core.test.mjs +6 -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) {
|
|
@@ -210,7 +214,7 @@ export function apply(ctx) {
|
|
|
210
214
|
// 文本降级路径:分段格式上报
|
|
211
215
|
var secs = parseSections(output)
|
|
212
216
|
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) }
|
|
217
|
+
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
218
|
resolveApply(d, t, String(rec.run.id), 'verifying', output || 'Worker 完成', 'worker 文本上报完成')
|
|
215
219
|
return { task: t }
|
|
216
220
|
})
|
|
@@ -232,7 +236,7 @@ export function apply(ctx) {
|
|
|
232
236
|
var vm = trimmed.match(/^[ \t>*#\-\s]*(APPROVED|REJECTED)\b/im)
|
|
233
237
|
if (failed || !vm) {
|
|
234
238
|
// 失败/空输出/无法判定:verifyRetries 计数,>=3 转人工验收(deliverable 已完成,是 verifier 故障不是任务故障)
|
|
235
|
-
if (failed && rec.model) { badModels[modelKey(sid, rec.model)] = true }
|
|
239
|
+
if (failed && rec.model) { badModels[modelKey(sid, rec.model)] = true; pushSysNote(sid, '模型 ' + rec.model + ' 验收连续失败,已熔断回退父级模型') }
|
|
236
240
|
t.verifyRetries = (t.verifyRetries || 0) + 1
|
|
237
241
|
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
242
|
ah(t, 'verifying', 'verifying', 'system', 'verifier 未给出有效结论,重新排队审查 (' + t.verifyRetries + '/3)')
|
|
@@ -295,14 +299,23 @@ export function apply(ctx) {
|
|
|
295
299
|
else flushReceipts(sid)
|
|
296
300
|
}
|
|
297
301
|
}
|
|
302
|
+
// 系统级异常通知队列(易失,随回执冲刷):模型熔断/spawn 失败/孤儿回收/看门狗标记
|
|
303
|
+
var sysNotesBuf = {}
|
|
304
|
+
function pushSysNote(sid, text) {
|
|
305
|
+
var arr = (sysNotesBuf[sid] = sysNotesBuf[sid] || [])
|
|
306
|
+
arr.push({ text: text, at: new Date().toISOString() })
|
|
307
|
+
if (arr.length > 10) arr.splice(0, arr.length - 10)
|
|
308
|
+
}
|
|
298
309
|
function flushReceipts(sid) {
|
|
299
|
-
var buf = receiptBuf[sid]
|
|
310
|
+
var buf = receiptBuf[sid]
|
|
311
|
+
var notes = sysNotesBuf[sid] || []
|
|
312
|
+
if ((!buf || !buf.items.length) && !notes.length) return
|
|
300
313
|
receiptBuf[sid] = null
|
|
301
|
-
|
|
314
|
+
sysNotesBuf[sid] = []
|
|
302
315
|
var root = rootForSession(sid); if (!root) return
|
|
303
316
|
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 + ' 条)', '']
|
|
317
|
+
if (buf) for (var i = 0; i < buf.items.length; i++) { (buf.items[i].kind === 'resolved' ? done : blocked).push(buf.items[i]) }
|
|
318
|
+
var lines = [done.length || blocked.length ? '📋 [任务看板] 回执摘要(' + buf.items.length + ' 条)' : '📋 [任务看板] 系统通知', '']
|
|
306
319
|
if (done.length) {
|
|
307
320
|
lines.push('✅ 完成 ' + done.length + ' 个:')
|
|
308
321
|
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 +324,10 @@ export function apply(ctx) {
|
|
|
311
324
|
lines.push('🛑 阻塞 ' + blocked.length + ' 个(需关注):')
|
|
312
325
|
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
326
|
}
|
|
327
|
+
if (notes.length) {
|
|
328
|
+
lines.push('', '⚠️ 系统异常 ' + notes.length + ' 条:')
|
|
329
|
+
for (var n = 0; n < notes.length && n < 8; n++) lines.push(' · ' + notes[n].text)
|
|
330
|
+
}
|
|
314
331
|
lines.push('', '可用 task_list 查看全部;阻塞项可在看板拖回待办重新投放。')
|
|
315
332
|
var text = lines.join('\n')
|
|
316
333
|
function send() { try { root.followup(makeMsg(text, 'recall')) } catch (e) { console.error('[task-board] receipt flush failed:', String(e)) } }
|
|
@@ -329,7 +346,14 @@ export function apply(ctx) {
|
|
|
329
346
|
Object.keys(runs).forEach(function (k) { if (runs[k].role === 'worker') activeW++; else activeV++ })
|
|
330
347
|
// 空闲快进:无活跃任务且无活跃 run → 不写盘直接返回(心跳每 15s 跑一次,不能每次都写文件)
|
|
331
348
|
var hasActive = snap.tasks.some(function (t) { return t.status === 'pending' || t.status === 'verifying' || t.status === 'in-progress' })
|
|
332
|
-
if (!hasActive && activeW + activeV === 0) {
|
|
349
|
+
if (!hasActive && activeW + activeV === 0) {
|
|
350
|
+
var emptyPool = { workers: [], verifiers: [] }
|
|
351
|
+
// 快进不写盘的前提是磁盘上的 poolStatus 已经是空的——否则(典型:DSH 重启清空了
|
|
352
|
+
// 内存 runs 表,文件里残留重启前的忙碌快照)幽灵 Worker 会永远显示执行中
|
|
353
|
+
var stalePool = snap.poolStatus && (((snap.poolStatus.workers || []).length + (snap.poolStatus.verifiers || []).length) > 0)
|
|
354
|
+
if (stalePool) { snap.poolStatus = emptyPool; try { await wt(sid, snap) } catch (_) {} }
|
|
355
|
+
return snap
|
|
356
|
+
}
|
|
333
357
|
var c = cfg(snap)
|
|
334
358
|
var isAuto = (snap.boardMode || 'auto') === 'auto'
|
|
335
359
|
|
|
@@ -339,7 +363,7 @@ export function apply(ctx) {
|
|
|
339
363
|
var result = await mutateLocked(sid, function (d) {
|
|
340
364
|
var now = Date.now()
|
|
341
365
|
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) }
|
|
366
|
+
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
367
|
})
|
|
344
368
|
// worker 派发仅 auto;verifier 派发两种模式都跑(manual 模式主窗口 claim 做完的 full 档任务需要验收)
|
|
345
369
|
var capW = isAuto ? Math.max(0, c.maxWorkers - activeW) : 0
|
|
@@ -363,6 +387,7 @@ export function apply(ctx) {
|
|
|
363
387
|
} else if (sp.role === 'worker') {
|
|
364
388
|
// spawn 失败 → 回 pending(verifier spawn 失败无需处理,下轮 cycle 会重试)
|
|
365
389
|
await mutateLocked(sid, function (d) { var t = d.tasks.find(function (x) { return x.id === sp.t.id }); if (t && t.status === 'in-progress' && t.claimedBy === 'spawn-pending') { t.status = 'pending'; t.claimedBy = null; t.claimedAt = null; ah(t, 'in-progress', 'pending', 'system', 'spawn 失败,回收重新排队') }; return t }, true)
|
|
390
|
+
pushSysNote(sid, '任务「' + sp.t.title + '」Worker 启动失败,已重新排队')
|
|
366
391
|
}
|
|
367
392
|
}
|
|
368
393
|
return result
|
|
@@ -403,9 +428,8 @@ export function apply(ctx) {
|
|
|
403
428
|
var agent = assembleCtx && assembleCtx.agent
|
|
404
429
|
if (!agent) return ''
|
|
405
430
|
var aid = String(agent.id)
|
|
406
|
-
console.error('[task-board][ctxpack] assemble agent=' + aid + ' packKeys=' + Object.keys(packByChild).join(',') + ' pending=' + pendingPacks.length)
|
|
407
431
|
var hit = packByChild[aid]
|
|
408
|
-
if (hit)
|
|
432
|
+
if (hit) return hit
|
|
409
433
|
// 首轮竞速自愈:start() 返回前的首次组装按父子归属从 pendingPacks 认领
|
|
410
434
|
var agentsSvc = ctx.agents
|
|
411
435
|
if (!agentsSvc) return ''
|
|
@@ -414,10 +438,8 @@ export function apply(ctx) {
|
|
|
414
438
|
var pp = pendingPacks[i]
|
|
415
439
|
if (now - pp.at > 60000) { pendingPacks.splice(i, 1); continue }
|
|
416
440
|
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)) }
|
|
441
|
+
if (agentsSvc.isOwnedBy(aid, pp.parent)) { packByChild[aid] = pp.pack; return pp.pack }
|
|
442
|
+
} catch (e) { console.error('[task-board] ctxpack isOwnedBy threw: ' + String(e)) }
|
|
421
443
|
}
|
|
422
444
|
return ''
|
|
423
445
|
},
|
|
@@ -428,6 +450,7 @@ export function apply(ctx) {
|
|
|
428
450
|
// ===== Tools =====
|
|
429
451
|
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
452
|
ctx.tools.register(defineTool({ name: 'task_context', description: '获取任务完整上下文。', parameters: { type: 'object', properties: { taskId: { type: 'string' }, expandFiles: { type: 'boolean' }, includeParent: { type: 'boolean' }, includeSubtasks: { type: 'boolean' } }, required: ['taskId'] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var d = await rt(sid); var t = d.tasks.find(function (x) { return x.id === args.taskId }); if (!t) return { ok: false, error: 'not found: ' + args.taskId }; return { ok: true, context: { task: t, inheritedContext: t.context || {} } } } }))
|
|
453
|
+
ctx.tools.register(defineTool({ name: 'task_preview_context', description: '派发前上下文预览:预演 task_create 的 contextFiles/contextNotes 将注入给子代理的实际内容(读盘后的最终形态),用于确认材料是否足够。不创建任务。返回 ok=false/empty=true 说明无可注入内容。', parameters: { type: 'object', properties: { contextFiles: { type: 'array', items: { type: 'string' }, description: '预演的文件路径列表' }, contextNotes: { type: 'string', description: '预演的调研笔记' } }, required: [] }, output: jo(), execute: async function (args) { var __ra = getActorId(); if (resolveRoot(__ra) !== __ra) return { ok: false, error: '看板管理工具仅主窗口可用(子代理无看板权限)' }; var sid = toolSessionId(); var files = Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []; var notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''; if (!files.length && !notes.trim()) return { ok: false, error: 'contextFiles/contextNotes 至少提供一个' }; try { var pack = await readContextPack({ context: { files: files, notes: notes } }); return { ok: true, empty: !pack, pack: pack } } catch (e) { return { ok: false, error: String(e).slice(0, 200) } } } }))
|
|
431
454
|
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
455
|
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
456
|
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 +461,74 @@ export function apply(ctx) {
|
|
|
438
461
|
// ===== RPC =====
|
|
439
462
|
// get-tasks 是纯读路径(rt 只读文件)——poolCycle 由 15s 心跳 + 写入后 kickCycle 驱动,
|
|
440
463
|
// 客户端 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 }
|
|
464
|
+
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 }
|
|
465
|
+
// poolStatus 防幽灵:只保留指向当前活跃任务的条目(重启后内存 runs 清空,文件快照可能残留)
|
|
466
|
+
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] }) }
|
|
467
|
+
return d })
|
|
468
|
+
// 派发前上下文预览:主 agent 用它确认"我将注入给子代理的材料"是否足够(不发任务、不落盘)
|
|
469
|
+
handle('preview-context', async function (args) {
|
|
470
|
+
var files = Array.isArray(args.contextFiles) ? args.contextFiles.map(String).slice(0, 20) : []
|
|
471
|
+
var notes = typeof args.contextNotes === 'string' ? args.contextNotes.slice(0, 8000) : ''
|
|
472
|
+
if (!files.length && !notes.trim()) return { ok: false, error: 'contextFiles/contextNotes 至少提供一个' }
|
|
473
|
+
var pack = ''
|
|
474
|
+
try { pack = await readContextPack({ context: { files: files, notes: notes } }) } catch (e) { return { ok: false, error: '读取失败: ' + String(e).slice(0, 120) } }
|
|
475
|
+
return { ok: true, filesCount: files.length, notesLen: notes.length, pack: pack, empty: !pack }
|
|
476
|
+
})
|
|
477
|
+
// 活动心跳:读子代理会话日志的最后一帧,提取最近的动作摘要(卡片/详情页展示"现在跑到哪了")
|
|
478
|
+
handle('agent-activity', async function (args) {
|
|
479
|
+
var sid = rpcSessionId(args)
|
|
480
|
+
var rec = runsFor(sid)[args.taskId]
|
|
481
|
+
if (!rec || !rec.run) return { ok: true, activity: null, reason: 'no active run' }
|
|
482
|
+
var child = String(rec.run.id)
|
|
483
|
+
try {
|
|
484
|
+
var boardAbs = await fs.resolve(fileFor(sid))
|
|
485
|
+
var ws = path.dirname(path.dirname(boardAbs))
|
|
486
|
+
var bucket = '--' + ws.replace(/[\\/:]/g, '-') + '--'
|
|
487
|
+
var log = path.join(os.homedir(), '.dsh', 'sessions', bucket, child, 'session.jsonl.zstd')
|
|
488
|
+
if (!fsNode.existsSync(log)) return { ok: true, activity: null, reason: 'log not found' }
|
|
489
|
+
var buf = fsNode.readFileSync(log)
|
|
490
|
+
var MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
|
491
|
+
var last = buf.lastIndexOf(MAGIC) // 追加写多帧格式:最新事件在末帧,只解一帧控成本
|
|
492
|
+
if (last < 0) return { ok: true, activity: null }
|
|
493
|
+
var text = zstdDecompressSync(buf.subarray(last)).toString('utf8')
|
|
494
|
+
var lines = text.split('\n').filter(Boolean)
|
|
495
|
+
var activity = null
|
|
496
|
+
for (var i = lines.length - 1; i >= 0 && !activity; i--) {
|
|
497
|
+
try {
|
|
498
|
+
var e = JSON.parse(lines[i])
|
|
499
|
+
var dta = e.data || {}
|
|
500
|
+
if (e.type === 'tool/call' && dta.name) activity = '🔧 ' + dta.name + ' ' + String(dta.arguments || '').replace(/\s+/g, ' ').slice(0, 90)
|
|
501
|
+
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)
|
|
502
|
+
} catch (_) {}
|
|
503
|
+
}
|
|
504
|
+
return { ok: true, activity: activity }
|
|
505
|
+
} catch (e) { return { ok: true, activity: null, reason: String(e).slice(0, 80) } }
|
|
506
|
+
})
|
|
507
|
+
// 全局多会话总览:聚合本机所有看板的任务计数(只读,供 dashboard 跨会话视图)
|
|
508
|
+
handle('list-boards', async function (args) {
|
|
509
|
+
var home = path.join(os.homedir(), '.dsh')
|
|
510
|
+
var out = []
|
|
511
|
+
var files
|
|
512
|
+
try { files = fsNode.readdirSync(home).filter(function (f) { return f.indexOf('tasks-') === 0 && f.slice(-5) === '.json' }) } catch (e) { return { ok: true, boards: [] } }
|
|
513
|
+
for (var i = 0; i < files.length; i++) {
|
|
514
|
+
try {
|
|
515
|
+
var d = JSON.parse(fsNode.readFileSync(path.join(home, files[i]), 'utf8'))
|
|
516
|
+
if (!vt(d)) continue
|
|
517
|
+
var counts = { pending: 0, inProgress: 0, verifying: 0, resolved: 0, blocked: 0 }
|
|
518
|
+
var titles = []
|
|
519
|
+
var lastTs = ''
|
|
520
|
+
d.tasks.forEach(function (t) {
|
|
521
|
+
if (t.status === 'archived') return
|
|
522
|
+
if (counts[t.status] !== undefined) counts[t.status]++
|
|
523
|
+
if (t.status === 'in-progress' || t.status === 'verifying' || t.status === 'blocked') titles.push(t.title)
|
|
524
|
+
;(t.history || []).forEach(function (h) { if (h.timestamp > lastTs) lastTs = h.timestamp })
|
|
525
|
+
})
|
|
526
|
+
out.push({ session: d.ownerSession, boardMode: d.boardMode || 'auto', teamMode: !!d.teamMode, counts: counts, activeTitles: titles.slice(0, 3), lastActivity: lastTs })
|
|
527
|
+
} catch (_) {}
|
|
528
|
+
}
|
|
529
|
+
out.sort(function (a, b) { return (b.lastActivity || '').localeCompare(a.lastActivity || '') })
|
|
530
|
+
return { ok: true, boards: out }
|
|
531
|
+
})
|
|
442
532
|
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
533
|
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
534
|
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 +622,7 @@ export function apply(ctx) {
|
|
|
532
622
|
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
623
|
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
624
|
// ===== 池中 Agent 结构化回报工具(双模:工具优先,文本分段为降级路径)=====
|
|
535
|
-
ctx.tools.register(defineTool({ name: 'board_report', description: '[任务看板 Worker 专用] 上报任务结果。kind=complete 时填 summary/changes/selfTest
|
|
625
|
+
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
626
|
var sid = toolSessionId(); var actor = getActorId()
|
|
537
627
|
var result = await mutateLocked(sid, function (d) {
|
|
538
628
|
var t = d.tasks.find(function (x) { return x.id === args.taskId })
|
|
@@ -549,7 +639,7 @@ export function apply(ctx) {
|
|
|
549
639
|
delete t.escalation
|
|
550
640
|
delete t.retryCount // v72:成功完成清零超时重试计数(工具直报路径)
|
|
551
641
|
delete t.stuckSince
|
|
552
|
-
t.deliverable = { summary: args.summary || '', changes: args.changes || '', selfTest: args.selfTest || '', at: new Date().toISOString(), by: actor }
|
|
642
|
+
t.deliverable = { summary: args.summary || '', changes: args.changes || '', selfTest: args.selfTest || '', diff: (args.diffStat || '').slice(0, 4000), at: new Date().toISOString(), by: actor }
|
|
553
643
|
var r = resolveApply(d, t, actor, 'verifying', (args.summary || '') + (args.selfTest ? '\n\n自测: ' + args.selfTest.slice(0, 300) : ''), 'worker 工具上报完成')
|
|
554
644
|
r.ok = true; r.task = t
|
|
555
645
|
return r
|
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: '', 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)
|
|
@@ -220,10 +246,37 @@ function apply(ctx) {
|
|
|
220
246
|
notify()
|
|
221
247
|
}).catch(function () {})
|
|
222
248
|
}
|
|
223
|
-
function fetchChildren() { if (!state.sessionId) return; rpc('list-children').then(function (d) { state.children = (d && d.children) || []; notify() }).catch(function () {}) }
|
|
249
|
+
function fetchChildren() { if (!state.sessionId) return; var epoch = reqEpoch; rpc('list-children').then(function (d) { if (epoch !== reqEpoch) return; state.children = (d && d.children) || []; notify() }).catch(function () {}) }
|
|
250
|
+
// 活动心跳:对进行中/验收中的任务轮询子代理最近动作(卡片与详情展示"现在跑到哪了")
|
|
251
|
+
function fetchActivity() {
|
|
252
|
+
if (!state.sessionId || !state.isRoot) return
|
|
253
|
+
var running = state.tasks.filter(function (t) { return t.status === 'in-progress' || t.status === 'verifying' })
|
|
254
|
+
if (!running.length) { if (Object.keys(state.activity).length) { state.activity = {}; notify() } return }
|
|
255
|
+
var pending = running.length
|
|
256
|
+
running.forEach(function (t) {
|
|
257
|
+
var epoch = reqEpoch
|
|
258
|
+
rpc('agent-activity', { taskId: t.id }).then(function (r) {
|
|
259
|
+
if (epoch !== reqEpoch) return // 会话已切换,丢弃过期响应
|
|
260
|
+
pending--
|
|
261
|
+
var act = (r && r.activity) || null
|
|
262
|
+
if (state.activity[t.id] !== act) { state.activity[t.id] = act; notify() }
|
|
263
|
+
else if (pending === 0) notify()
|
|
264
|
+
}).catch(function () { pending-- })
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
function fetchArchived() {
|
|
268
|
+
if (!state.sessionId) return
|
|
269
|
+
var epoch = reqEpoch
|
|
270
|
+
rpc('get-tasks', { includeArchived: true }).then(function (d) {
|
|
271
|
+
if (epoch !== reqEpoch) return
|
|
272
|
+
state.archived = ((d && d.tasks) || []).filter(function (t) { return t.status === 'archived' })
|
|
273
|
+
notify()
|
|
274
|
+
}).catch(function () {})
|
|
275
|
+
}
|
|
224
276
|
var __timer = ctx.get('timer')
|
|
225
277
|
// 轮询:get-tasks 已是纯读(v71),3s 开销低;面板关闭时也轮询以更新角标
|
|
226
278
|
if (__timer) ctx.effect(function () { return __timer.interval(fetchTasks, 3000) })
|
|
279
|
+
if (__timer) ctx.effect(function () { return __timer.interval(fetchActivity, 12000) })
|
|
227
280
|
function readLayoutOffsets() {
|
|
228
281
|
try {
|
|
229
282
|
var frames = document.querySelectorAll('[data-sidebar-collapsed], [data-details-collapsed]')
|
|
@@ -292,9 +345,125 @@ function apply(ctx) {
|
|
|
292
345
|
}
|
|
293
346
|
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
347
|
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))) }
|
|
348
|
+
// ===== 仪表盘:日期范围筛选(共享给报告与全局总览)=====
|
|
349
|
+
function tsInRange(iso, from, to) {
|
|
350
|
+
if (!iso) return false
|
|
351
|
+
var d = new Date(iso); if (isNaN(d.getTime())) return false
|
|
352
|
+
var ds = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2)
|
|
353
|
+
if (from && ds < from) return false
|
|
354
|
+
if (to && ds > to) return false
|
|
355
|
+
return true
|
|
356
|
+
}
|
|
357
|
+
function taskLastTs(t) {
|
|
358
|
+
var last = t.createdAt || ''
|
|
359
|
+
;(t.history || []).forEach(function (h) { if (h.timestamp > last) last = h.timestamp })
|
|
360
|
+
if (t.deliverable && t.deliverable.at > last) last = t.deliverable.at
|
|
361
|
+
if (t.verification && t.verification.at > last) last = t.verification.at
|
|
362
|
+
if (t.archivedAt && t.archivedAt > last) last = t.archivedAt
|
|
363
|
+
return last
|
|
364
|
+
}
|
|
365
|
+
function activeRange() { var f = state.dateRange || {}; return { from: f.from || '', to: f.to || '' } }
|
|
366
|
+
function rangeLabel() { var r = activeRange(); return !r.from && !r.to ? '全部时间' : (r.from || '…') + ' ~ ' + (r.to || '…') }
|
|
367
|
+
function RangeFilter() {
|
|
368
|
+
var _R = React; var useState = _R.useState
|
|
369
|
+
var _a = useState(state.rfOpen || false), open = _a[0], setOpen = _a[1]
|
|
370
|
+
var rg = activeRange()
|
|
371
|
+
function setRange(from, to) { state.dateRange = { from: from, to: to }; notify() }
|
|
372
|
+
function preset(days) {
|
|
373
|
+
if (days === 0) { setRange('', ''); return }
|
|
374
|
+
var to = new Date(); var from = new Date(Date.now() - (days - 1) * 86400000)
|
|
375
|
+
function fmt(d) { return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2) }
|
|
376
|
+
setRange(fmt(from), fmt(to))
|
|
377
|
+
}
|
|
378
|
+
var headBtn = { fontSize: 11, padding: '3px 10px', border: '1px solid ' + (rg.from || rg.to ? C.brand : C.border), borderRadius: 5, background: (rg.from || rg.to) ? C.nested : C.card, color: (rg.from || rg.to) ? C.brand : C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }
|
|
379
|
+
var chip = { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.border, borderRadius: 10, background: C.card, color: C.text2, cursor: 'pointer' }
|
|
380
|
+
var dateInput = { fontSize: 11, padding: '2px 6px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text }
|
|
381
|
+
return React.createElement('div', { style: { marginBottom: 10 } },
|
|
382
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
383
|
+
React.createElement('button', { onClick: function () { state.rfOpen = !open; setOpen(!open) }, style: headBtn },
|
|
384
|
+
ic('calendar-days', 11),
|
|
385
|
+
React.createElement('span', null, '统计范围: ' + rangeLabel()),
|
|
386
|
+
ic(open ? 'chevron-up' : 'chevron-down', 10))),
|
|
387
|
+
open ? React.createElement('div', { style: { marginTop: 6, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } },
|
|
388
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
389
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '快捷:'),
|
|
390
|
+
React.createElement('button', { onClick: function () { preset(1) }, style: chip }, '今天'),
|
|
391
|
+
React.createElement('button', { onClick: function () { preset(7) }, style: chip }, '近 7 天'),
|
|
392
|
+
React.createElement('button', { onClick: function () { preset(30) }, style: chip }, '近 30 天'),
|
|
393
|
+
React.createElement('button', { onClick: function () { preset(0) }, style: chip }, '全部'),
|
|
394
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
395
|
+
React.createElement('label', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 4 } }, '开始',
|
|
396
|
+
React.createElement('input', { type: 'date', value: rg.from, onChange: function (e) { setRange(e.target.value, rg.to) }, style: dateInput })),
|
|
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.to, onChange: function (e) { setRange(rg.from, e.target.value) }, style: dateInput })),
|
|
399
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '作用于报告与全局总览')),
|
|
400
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 5 } }, '报告统计「时间范围内有活动」的任务;总览只显示范围内有活跃的会话。')) : null)
|
|
401
|
+
}
|
|
402
|
+
function GlobalBoards() {
|
|
403
|
+
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
404
|
+
var _a = useState(state.globalBoards), boards = _a[0], setBoards = _a[1]
|
|
405
|
+
var _b = useState(state.gboOpen || false), open = _b[0], setOpen = _b[1]
|
|
406
|
+
function load() { rpc('list-boards').then(function (r) { var b = (r && r.boards) || []; state.globalBoards = b; setBoards(b) }).catch(function () {}) }
|
|
407
|
+
useEffect(function () { load() }, [])
|
|
408
|
+
if (!boards.length) return null
|
|
409
|
+
var rg = activeRange()
|
|
410
|
+
var shown = boards.filter(function (b) { return !rg.from && !rg.to ? true : tsInRange(b.lastActivity, rg.from, rg.to) })
|
|
411
|
+
var activeN = boards.filter(function (b) { return b.counts.inProgress || b.counts.verifying || b.counts.pending || b.counts.blocked }).length
|
|
412
|
+
return React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } },
|
|
413
|
+
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 } },
|
|
414
|
+
ic('layout-grid', 11),
|
|
415
|
+
React.createElement('span', null, '全局会话总览(本机 ' + boards.length + ' 个看板 · ' + activeN + ' 个活跃)'),
|
|
416
|
+
open && (rg.from || rg.to) ? React.createElement('span', { style: { fontSize: 9, color: C.brand } }, '范围: ' + rangeLabel()) : null,
|
|
417
|
+
React.createElement('span', { style: { marginLeft: 'auto', display: 'inline-flex', color: C.text2 } }, ic(open ? 'chevron-up' : 'chevron-down', 11))),
|
|
418
|
+
open ? (shown.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2, padding: '6px 0' } }, '时间范围内无活跃会话') : shown.map(function (b, i) {
|
|
419
|
+
var isSelf = b.session === state.sessionId
|
|
420
|
+
return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 10, padding: '3px 0', borderBottom: '1px solid ' + C.nested } },
|
|
421
|
+
React.createElement('span', { onClick: function () { if (!isSelf && 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)),
|
|
422
|
+
React.createElement('span', { style: { color: C.text2 } }, b.teamMode ? 'Team' : (b.boardMode === 'auto' ? '自动' : '手动')),
|
|
423
|
+
React.createElement('span', { style: { display: 'inline-flex', gap: 5 } },
|
|
424
|
+
b.counts.inProgress ? React.createElement('span', { style: { color: C.brand } }, '▶' + b.counts.inProgress) : null,
|
|
425
|
+
b.counts.verifying ? React.createElement('span', { style: { color: C.warn } }, '验' + b.counts.verifying) : null,
|
|
426
|
+
b.counts.pending ? React.createElement('span', { style: { color: C.text } }, '待' + b.counts.pending) : null,
|
|
427
|
+
b.counts.blocked ? React.createElement('span', { style: { color: C.err } }, '🛑' + b.counts.blocked) : null,
|
|
428
|
+
!(b.counts.inProgress || b.counts.verifying || b.counts.pending || b.counts.blocked) ? React.createElement('span', { style: { color: C.text2 } }, '空闲') : null),
|
|
429
|
+
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,
|
|
430
|
+
b.lastActivity ? React.createElement('span', { style: { color: C.text2 } }, ago(b.lastActivity)) : null)
|
|
431
|
+
})) : null)
|
|
432
|
+
}
|
|
433
|
+
function buildReport() {
|
|
434
|
+
var lines = ['# 任务看板报告', '', '生成时间: ' + new Date().toLocaleString(), '模式: ' + (state.teamMode ? 'Team' : (state.boardMode === 'auto' ? '自动' : '手动')), '', '统计范围: ' + rangeLabel()]
|
|
435
|
+
var groups = { inProgress: [], verifying: [], pending: [], blocked: [], resolved: [] }
|
|
436
|
+
var rg = activeRange()
|
|
437
|
+
state.tasks.forEach(function (t) {
|
|
438
|
+
if (t.status === 'archived') return
|
|
439
|
+
if ((rg.from || rg.to) && !tsInRange(taskLastTs(t), rg.from, rg.to)) return
|
|
440
|
+
var g = groups[t.status]; if (g) g.push(t)
|
|
441
|
+
})
|
|
442
|
+
if (groups.inProgress.length) { lines.push('## 进行中'); groups.inProgress.forEach(function (t) { lines.push('- ' + t.title + '(' + (t.claimedBy ? shortId(t.claimedBy) : '-') + ')') }); lines.push('') }
|
|
443
|
+
if (groups.verifying.length) { lines.push('## 验证中'); groups.verifying.forEach(function (t) { lines.push('- ' + t.title) }); lines.push('') }
|
|
444
|
+
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('') }
|
|
445
|
+
if (groups.blocked.length) { lines.push('## 阻塞'); groups.blocked.forEach(function (t) { lines.push('- ' + t.title + (t.escalation ? '(待裁决)' : '')) }); lines.push('') }
|
|
446
|
+
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) })
|
|
447
|
+
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('') }
|
|
448
|
+
return lines.join('\n')
|
|
449
|
+
}
|
|
450
|
+
function ReportButton() {
|
|
451
|
+
var _R = React; var useState = _R.useState
|
|
452
|
+
var _a = useState(''), msg = _a[0], setMsg = _a[1]
|
|
453
|
+
function gen() {
|
|
454
|
+
var md = buildReport()
|
|
455
|
+
try { navigator.clipboard.writeText(md).then(function () { setMsg('✅ 已复制到剪贴板'); setTimeout(function () { setMsg('') }, 2500) }, function () { download(md) }) } catch (_) { download(md) }
|
|
456
|
+
function download(text) {
|
|
457
|
+
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('⚠️ 导出失败') }
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
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)
|
|
461
|
+
}
|
|
295
462
|
function Dashboard() {
|
|
296
463
|
var stats = computeStats(state.tasks); var statusOrder = ['pending', 'in-progress', 'verifying', 'resolved', 'blocked', 'archived']; var prioOrder = ['critical', 'high', 'medium', 'low']
|
|
297
464
|
return React.createElement('div', null,
|
|
465
|
+
React.createElement(RangeFilter),
|
|
466
|
+
React.createElement(GlobalBoards),
|
|
298
467
|
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
468
|
React.createElement('div', { style: { display: 'flex', gap: 12, marginBottom: 12, flexWrap: 'wrap' } },
|
|
300
469
|
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 +476,15 @@ function apply(ctx) {
|
|
|
307
476
|
function BoardButton(props) {
|
|
308
477
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
309
478
|
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])
|
|
479
|
+
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
480
|
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
481
|
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:
|
|
482
|
+
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)
|
|
314
483
|
}
|
|
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:
|
|
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 ?
|
|
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:
|
|
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:
|
|
484
|
+
function ModeSwitch(props) { var mode = props.mode; var teamOn = props.teamOn; function pick(m) { if (teamOn && m === 'manual') return; rpc('set-board-mode', { mode: m }).then(fetchTasks).catch(function () {}) } var btnBase = { fontSize: 12, padding: '4px 14px', border: 'none', cursor: 'pointer', fontWeight: 500, flex: 1, textAlign: 'center', borderRadius: 6, transition: 'all .15s' }; var manDis = teamOn; return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 8, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { pick('auto') }, style: Object.assign({}, btnBase, mode === 'auto' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('bot', 12, '自动')), React.createElement('button', { onClick: function () { pick('manual') }, disabled: manDis, title: manDis ? 'Team 模式开启时不可切换到手动' : '', style: Object.assign({}, btnBase, mode === 'manual' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: manDis ? C.text2 + '88' : C.text2 }, manDis ? { cursor: 'not-allowed', opacity: 0.5 } : {}) }, icText('user', 12, '手动'))) }
|
|
485
|
+
function TeamSwitch(props) { var on = props.on; function toggle() { rpc('set-team-mode', { enabled: !on }).then(fetchTasks).catch(function () {}) } return React.createElement('button', { onClick: toggle, title: on ? 'Team 模式已开启:任务走看板派发,Worker 歧义自动上报主窗口裁决(不拦截主窗口编辑)' : '开启 Team 模式:任务全走看板,Worker 歧义上报主窗口裁决', style: { fontSize: 12, padding: '4px 10px', border: '1px solid ' + (on ? C.brand : C.border), borderRadius: 8, cursor: 'pointer', fontWeight: 500, background: on ? C.brand : 'transparent', color: on ? C_INV : C.text2, transition: 'all .15s' } }, icText('users', 12, 'Team' + (on ? ' ON' : ''))) }
|
|
486
|
+
function ViewTab() { var btnBase = { fontSize: 11, padding: '3px 10px', border: 'none', cursor: 'pointer', fontWeight: 500, borderRadius: 5, transition: 'all .15s' }; function goArch() { state.view = 'archive'; notify(); fetchArchived() } return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 6, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { state.view = 'board'; notify() }, style: Object.assign({}, btnBase, state.view === 'board' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('clipboard-list', 12), '看板')), React.createElement('button', { onClick: function () { state.view = 'team'; notify() }, style: Object.assign({}, btnBase, state.view === 'team' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('users', 12, '团队')), React.createElement('button', { onClick: function () { state.view = 'dashboard'; notify() }, style: Object.assign({}, btnBase, state.view === 'dashboard' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('bar-chart-3', 12, '仪表盘')), React.createElement('button', { onClick: goArch, title: '已归档任务(可检索、可恢复)', style: Object.assign({}, btnBase, state.view === 'archive' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('archive', 12, '归档'))) }
|
|
487
|
+
function PoolCfg(props) { var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]; var _b = useState(false), dirty = _b[0], setDirty = _b[1]; useEffect(function () { setVal(String(props.value)); setDirty(false) }, [props.value]); function commit(v) { var n = parseInt(v, 10); if (!isNaN(n) && n >= 0 && n <= 10) { setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(fetchTasks).catch(function () {}) } } function step(d) { var n = parseInt(val, 10) || 0; commit(String(Math.max(0, Math.min(10, n + d)))) } var miniBtn = { fontSize: 9, padding: '0px 4px', border: '1px solid ' + C.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) }
|
|
319
488
|
function PoolStatus() {
|
|
320
489
|
var ps = state.poolStatus
|
|
321
490
|
if (!ps) return null
|
|
@@ -353,7 +522,7 @@ function apply(ctx) {
|
|
|
353
522
|
// 降级:文本输入(模型枚举不可用时)
|
|
354
523
|
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
|
|
355
524
|
React.createElement('input', { value: val, onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== (props.value || '')) }, onBlur: function () { if (dirty) save() }, onKeyDown: function (e) { if (e.key === 'Enter') save() }, placeholder: '空=同父级', style: { width: 110, padding: '0px 4px', fontSize: 9, border: '1px solid ' + (dirty ? C.brand : C.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:
|
|
525
|
+
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
526
|
}
|
|
358
527
|
function PoolCfgPopover(props) {
|
|
359
528
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
|
|
@@ -386,7 +555,8 @@ function apply(ctx) {
|
|
|
386
555
|
// #18 依赖未满足判断(依赖不存在视为阻塞——创建时已校验,防御性兜底)
|
|
387
556
|
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
557
|
// #19 管线档位元数据
|
|
389
|
-
var pipeMeta = { full: { icon: 'flask-conical', label: '全流程(执行+验证)' }, work: { icon: 'file-text', label: '免验证(只做不验)' }, direct: { icon: 'message-square', label: '主窗口直接处理' } }
|
|
558
|
+
var pipeMeta = { full: { icon: 'flask-conical', label: '全流程(执行+验证)', short: '全流程' }, work: { icon: 'file-text', label: '免验证(只做不验)', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直接处理', short: '直办' } }
|
|
559
|
+
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 '' } }
|
|
390
560
|
function pipeOf(t) { return pipeMeta[t.pipeline] || pipeMeta.full }
|
|
391
561
|
// #14 批量操作条(#16 带一步撤销:快照操作前的 priority/status)
|
|
392
562
|
function BatchBar() {
|
|
@@ -401,9 +571,9 @@ function apply(ctx) {
|
|
|
401
571
|
var btn = { fontSize: 10, padding: '3px 10px', borderRadius: 4, border: 'none', cursor: 'pointer', fontWeight: 600 }
|
|
402
572
|
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
573
|
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:
|
|
574
|
+
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), '批量归档'),
|
|
575
|
+
['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]) }),
|
|
576
|
+
undo ? React.createElement('button', { onClick: doUndo, title: '撤销最近一次批量操作', style: Object.assign({}, btn, { background: C.brand, color: C_INV }) }, '↩️ 撤销') : null,
|
|
407
577
|
msg ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, msg) : null,
|
|
408
578
|
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
579
|
}
|
|
@@ -418,11 +588,18 @@ function apply(ctx) {
|
|
|
418
588
|
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
589
|
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
|
|
420
590
|
React.createElement('input', { value: q, onChange: function (e) { setQ2(e.target.value) }, placeholder: '🔍 搜索标题/描述/ID…', style: { flex: '0 1 200px', fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.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:
|
|
591
|
+
['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
592
|
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
593
|
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
594
|
}
|
|
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:
|
|
595
|
+
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' } },
|
|
596
|
+
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
597
|
+
(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,
|
|
598
|
+
t.status === 'in-progress' && t.claimedBy ? React.createElement('span', null, '⚡ ' + shortId(t.claimedBy)) : null,
|
|
599
|
+
t.assignee ? React.createElement('span', null, '👤→' + shortId(t.assignee)) : null,
|
|
600
|
+
durOf(t) ? React.createElement('span', { title: '创建至今耗时' }, '⏱ ' + durOf(t)) : null),
|
|
601
|
+
(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,
|
|
602
|
+
t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null) }
|
|
426
603
|
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
604
|
function MsgThread(props) {
|
|
428
605
|
var msgs = props.messages || []
|
|
@@ -437,6 +614,36 @@ function apply(ctx) {
|
|
|
437
614
|
React.createElement('div', { style: { fontSize: 10, color: C.text, whiteSpace: 'pre-wrap', maxHeight: 140, overflowY: 'auto' } }, m.text))
|
|
438
615
|
}))
|
|
439
616
|
}
|
|
617
|
+
function ArchiveView() {
|
|
618
|
+
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
619
|
+
var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
|
|
620
|
+
useEffect(function () { if (!state.archived.length) fetchArchived() }, [])
|
|
621
|
+
var list = state.archived.filter(function (t) {
|
|
622
|
+
if (!q) return true
|
|
623
|
+
var qq = q.toLowerCase()
|
|
624
|
+
return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
|
|
625
|
+
}).sort(function (a, b) {
|
|
626
|
+
// 完成时间最近在前:resolvedAt 优先,回退验收时间/归档时间/最后活动时间
|
|
627
|
+
function doneTs(t) { return t.resolvedAt || (t.verification && t.verification.at) || t.archivedAt || taskLastTs(t) || '' }
|
|
628
|
+
return (doneTs(b) || '').localeCompare(doneTs(a) || '')
|
|
629
|
+
})
|
|
630
|
+
function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchArchived(); fetchTasks() }).catch(function () {}) }
|
|
631
|
+
return React.createElement('div', null,
|
|
632
|
+
React.createElement('input', { value: q, onChange: function (e) { setQ(e.target.value); state.archQ = e.target.value }, placeholder: '检索标题 / ID / 标签…', style: { width: '100%', padding: '5px 8px', fontSize: 11, border: '1px solid ' + C.border, borderRadius: 5, background: C.card, color: C.text, marginBottom: 8 } }),
|
|
633
|
+
state.archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
|
|
634
|
+
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
635
|
+
list.map(function (t) {
|
|
636
|
+
return React.createElement('div', { key: t.id, style: { padding: '6px 8px', marginBottom: 5, background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } },
|
|
637
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
638
|
+
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: t.title }, t.title),
|
|
639
|
+
t.verification ? React.createElement('span', { title: '验收结论: ' + (t.verification.summary || '').slice(0, 100), style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.verification.verdict === 'approved' ? C.ok : C.err, color: C_INV } }, t.verification.verdict === 'approved' ? '过' : '驳') : null,
|
|
640
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, t.archivedAt ? ago(t.archivedAt) : '')),
|
|
641
|
+
t.verification && t.verification.summary ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + t.verification.summary) : null,
|
|
642
|
+
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4 } },
|
|
643
|
+
React.createElement('button', { onClick: function () { restore(t.id) }, title: '恢复为待办', style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '↩ 恢复待办'),
|
|
644
|
+
React.createElement('button', { onClick: function () { if (sessionsSvc && t.claimedBy) sessionsSvc.open(t.claimedBy) }, disabled: !t.claimedBy, title: t.claimedBy ? '打开 Worker 会话' : '无执行会话', style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + C.border, borderRadius: 3, background: 'transparent', color: t.claimedBy ? C.brand : C.text2, cursor: t.claimedBy ? 'pointer' : 'not-allowed', opacity: t.claimedBy ? 1 : 0.5 } }, '→ Worker 会话')))
|
|
645
|
+
}))
|
|
646
|
+
}
|
|
440
647
|
function TeamView() {
|
|
441
648
|
var ps = state.poolStatus || { workers: [], verifiers: [] }
|
|
442
649
|
// v74 一次性模型:每个活跃卡片 = 一个在跑的 run(随任务结算销毁),没有常驻成员
|
|
@@ -447,7 +654,7 @@ function apply(ctx) {
|
|
|
447
654
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 } },
|
|
448
655
|
React.createElement('span', { style: { display: 'inline-flex', color: isW ? C.brand : C.warn } }, ic(isW ? 'zap' : 'clipboard-check', 13)),
|
|
449
656
|
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:
|
|
657
|
+
React.createElement('span', { style: { marginLeft: 'auto', fontSize: 9, padding: '1px 6px', borderRadius: 3, background: C.brand, color: C_INV } }, '忙碌')),
|
|
451
658
|
m.model ? React.createElement('div', { style: { fontSize: 9, color: C.warn, marginBottom: 3 }, title: '异构模型审查' }, '🧬 ' + m.model) : null,
|
|
452
659
|
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
660
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '一次性子代理会话' }, '会话 ' + String(m.runId || '').slice(0, 8) + '…(用完即销毁)'))
|
|
@@ -473,7 +680,7 @@ function apply(ctx) {
|
|
|
473
680
|
var dt = getTask(id)
|
|
474
681
|
var satisfied = dt && (dt.status === 'resolved' || dt.status === 'archived')
|
|
475
682
|
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:
|
|
683
|
+
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
684
|
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
685
|
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
686
|
}),
|
|
@@ -500,19 +707,20 @@ function apply(ctx) {
|
|
|
500
707
|
var canIntervene = task.status === 'in-progress' || task.status === 'verifying'
|
|
501
708
|
return React.createElement('div', { style: { padding: '4px 2px' } },
|
|
502
709
|
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:
|
|
710
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 } }, React.createElement('span', { style: { fontSize: 10, padding: '1px 6px', borderRadius: 3, background: 'color-mix(in srgb, ' + (prioColor[task.priority] || prioColor.low) + ' 20%, transparent)', color: (prioColor[task.priority] || prioColor.low) } }, prioLabel[task.priority] || '中'), React.createElement('span', { style: { fontSize: 11, padding: '1px 8px', borderRadius: 3, background: C.nested, color: C.text } }, statusLabels[task.status] || task.status), React.createElement('select', { value: task.pipeline || 'full', onChange: function (e) { rpc('update-task', { taskId: task.id, pipeline: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '管线档位', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.text2 } }, React.createElement('option', { value: 'full' }, '全流程(执行+验证)'), React.createElement('option', { value: 'work' }, '免验证(只做不验)'), React.createElement('option', { value: 'direct' }, '主窗口处理')), task.pipelineAuto ? React.createElement('span', { style: { fontSize: 9, color: C.text2 }, title: '由规则自动分类,可手动覆盖' }, 'auto') : null, isManual ? React.createElement('span', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('user', 10), '手动派发') : null),
|
|
711
|
+
(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,
|
|
504
712
|
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
713
|
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
714
|
React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, whiteSpace: 'pre-wrap', maxHeight: 160, overflowY: 'auto' } }, task.escalation.question),
|
|
507
715
|
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '上报于 ' + ago(task.escalation.at) + ' · ' + (task.escalation.by || '')),
|
|
508
716
|
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:
|
|
717
|
+
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
718
|
actionMsg ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6 } }, actionMsg) : null,
|
|
511
719
|
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
720
|
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
721
|
React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6 } }, '标记于 ' + ago(task.stuckSince) + '(运行超 5 分钟且事件流停滞超 1 分钟)。你可以查看其会话后决定:'),
|
|
514
722
|
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:
|
|
723
|
+
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
724
|
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
725
|
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
726
|
React.createElement('input', { value: editTitle, onChange: function (e) { setEditTitle(e.target.value) }, style: { width: '100%', fontSize: 13, fontWeight: 600, padding: '4px 6px', marginBottom: 6, border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text, boxSizing: 'border-box' } }),
|
|
@@ -537,28 +745,28 @@ function apply(ctx) {
|
|
|
537
745
|
React.createElement(MsgThread, { messages: task.messages }),
|
|
538
746
|
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
747
|
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:
|
|
748
|
+
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), ' 保存并重置']),
|
|
749
|
+
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,
|
|
750
|
+
(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,
|
|
751
|
+
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,
|
|
752
|
+
task.status === 'verifying' && !task.escalation ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('dispatch-task', { taskId: task.id, role: 'verifier' }) }) }, title: '手动派发给一次性 Verifier 子代理', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.warn, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('zap', 10), '派发验收') : null,
|
|
753
|
+
task.status === 'verifying' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'approved' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('check-circle', 10), '通过') : null,
|
|
754
|
+
task.status === 'verifying' ? React.createElement('button', { onClick: function () { var r = window.prompt('驳回原因:'); doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'rejected', comment: r || '' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.err, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('x-circle', 10), '驳回') : null,
|
|
755
|
+
task.status === 'resolved' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('archive-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.text2, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('archive', 10), '归档') : null,
|
|
548
756
|
canJump ? React.createElement('button', { onClick: jumpToAgent, style: { fontSize: 10, padding: '3px 8px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '→ Worker 会话') : null,
|
|
549
757
|
(task.verifierRun && (task.status === 'verifying' || task.status === 'resolved')) ? React.createElement('button', { onClick: jumpToVerifier, style: { fontSize: 10, padding: '3px 8px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '→ Verifier 会话') : null),
|
|
550
758
|
canIntervene ? React.createElement('div', { style: { marginTop: 8, padding: '6px 8px', border: '1px dashed ' + C.warn, borderRadius: 6 } },
|
|
551
759
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('zap', 11), '高优先级介入(插入执行 Agent 队首)'),
|
|
552
760
|
React.createElement('div', { style: { display: 'flex', gap: 4 } },
|
|
553
761
|
React.createElement('input', { value: interveneMsg, onChange: function (e) { setInterveneMsg(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') submitIntervene() }, placeholder: '给执行中的 Agent 下达高优指令…', style: { flex: 1, fontSize: 11, padding: '4px 8px', border: '1px solid ' + C.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:
|
|
762
|
+
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
763
|
}
|
|
556
764
|
function TopPanel() {
|
|
557
765
|
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
|
|
766
|
+
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
767
|
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
|
-
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) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
768
|
+
var _q = useState(state.teamMode), teamMode = _q[0], setTeamMode = _q[1]; var _dr = useState(state.dateRange), setDR = _dr[1]
|
|
769
|
+
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) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
562
770
|
if (!open) return null
|
|
563
771
|
if (state.isRoot === false) return null // 子代理会话不渲染看板面板
|
|
564
772
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -566,6 +774,7 @@ function apply(ctx) {
|
|
|
566
774
|
if (hasFilter) { active = active.filter(passFilter); archived = archived.filter(passFilter) }
|
|
567
775
|
var content
|
|
568
776
|
if (view === 'dashboard') { content = React.createElement(Dashboard) }
|
|
777
|
+
else if (view === 'archive') { content = React.createElement(ArchiveView) }
|
|
569
778
|
else if (view === 'team') { content = React.createElement(TeamView) }
|
|
570
779
|
else if (detailId) { content = React.createElement(DetailView) }
|
|
571
780
|
else {
|
|
@@ -577,7 +786,7 @@ function apply(ctx) {
|
|
|
577
786
|
var ta = a.archivedAt || a.resolvedAt || a.createdAt || '', tb = b.archivedAt || b.resolvedAt || b.createdAt || ''
|
|
578
787
|
return state.archSort === 'time-asc' ? ta.localeCompare(tb) : tb.localeCompare(ta)
|
|
579
788
|
})
|
|
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),
|
|
789
|
+
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
790
|
}
|
|
582
791
|
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
792
|
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 } },
|
|
@@ -585,7 +794,7 @@ function apply(ctx) {
|
|
|
585
794
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
586
795
|
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel }),
|
|
587
796
|
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 ?
|
|
797
|
+
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
798
|
React.createElement(TeamSwitch, { on: teamMode }),
|
|
590
799
|
React.createElement(ModeSwitch, { mode: mode, teamOn: teamMode }),
|
|
591
800
|
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
|
@@ -86,6 +86,7 @@ export function parseSections(text) {
|
|
|
86
86
|
if (/开发描述/.test(title)) out.summary = body
|
|
87
87
|
else if (/改动/.test(title)) out.changes = body
|
|
88
88
|
else if (/自测/.test(title)) out.selfTest = body
|
|
89
|
+
else if (/diff|变更概要/i.test(title)) out.diff = body
|
|
89
90
|
else if (/测试概要|验证概要|审查概要/.test(title)) out.verifySummary = body
|
|
90
91
|
else if (/核对项|核验项|检查项/.test(title)) out.checks = body
|
|
91
92
|
}
|
|
@@ -132,7 +133,7 @@ export function buildWorkerPrompt(t, pack) {
|
|
|
132
133
|
if (notes) p += '\n\n该任务的过程记录(歧义上报/主窗口裁决/驳回/干预,请务必遵循最新裁决方向):\n' + notes
|
|
133
134
|
if (msgs) p += '\n\n该任务的详细消息(裁决答案/干预指令/歧义原文等,请务必遵循):\n' + msgs
|
|
134
135
|
if (pack) p += '\n\n' + pack
|
|
135
|
-
p += '\n\n完成契约(双模,工具优先):\n1. 完成时:优先调用 board_report 工具(kind=complete, taskId=' + t.id + ',summary=开发描述/changes=改动清单/selfTest
|
|
136
|
+
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
137
|
return p
|
|
137
138
|
}
|
|
138
139
|
export function buildVerifierPrompt(t, pack) {
|
|
@@ -140,7 +141,7 @@ export function buildVerifierPrompt(t, pack) {
|
|
|
140
141
|
var msgs = buildMessages(t)
|
|
141
142
|
var p = '你是一个一次性任务审核 Verifier。审查下面这个任务的完成质量,给出结论后本会话即销毁。\n\ntaskId: ' + t.id + '\n任务: ' + t.title + '\n描述: ' + (t.description || '').slice(0, 500)
|
|
142
143
|
p += '\n完成说明: ' + (t.resolution || '(无)')
|
|
143
|
-
p += '\n交付物: ' + (t.deliverable ? ('开发描述: ' + (t.deliverable.summary || '') + '\n改动清单: ' + (t.deliverable.changes || '') + '\n自测情况: ' + (t.deliverable.selfTest || '')) : '(无)').slice(0,
|
|
144
|
+
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
145
|
if (t.context && t.context.instructions) p += '\n指引: ' + t.context.instructions
|
|
145
146
|
if (t.acceptance) p += '\n硬性验收脚本: ' + t.acceptance + '\n(必须独立复跑该命令并把真实输出贴进核对项;脚本失败必须 REJECTED)'
|
|
146
147
|
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.
|
|
4
|
+
"version": "1.2.0",
|
|
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,12 @@ test('parseSections: 分段解析', () => {
|
|
|
106
106
|
assert.deepEqual(core.parseSections('没有分段的纯文本'), {})
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
+
test('parseSections: diff 概要分段解析(Worker 变更概要 → deliverable.diff)', () => {
|
|
110
|
+
const s = core.parseSections('## 开发描述\n做了\n## diff 概要\n gen.py | 3 + ++')
|
|
111
|
+
assert.equal(s.diff, 'gen.py | 3 + ++')
|
|
112
|
+
assert.equal(s.summary, '做了')
|
|
113
|
+
})
|
|
114
|
+
|
|
109
115
|
test('parseVerdict: 行首锚定,历史提及不误判', () => {
|
|
110
116
|
assert.equal(core.parseVerdict('APPROVED: 通过'), 'APPROVED')
|
|
111
117
|
assert.equal(core.parseVerdict('REJECTED: 不达标'), 'REJECTED')
|