dsh-agent-board 1.1.3 → 1.2.1

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