dsh-agent-board 1.7.5 → 1.7.7

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/lib/client.js CHANGED
@@ -231,8 +231,18 @@ function apply(ctx) {
231
231
  }
232
232
 
233
233
  var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
234
+ // 六列列头 title + 空列引导(巡检 n-muyg4fl8tm6r 六列无说明 / n-muygwnxpwcsq 空列零引导):
235
+ // 黑话→人话(title 悬浮补一句),空列给下一步动作指引(灰字)。
236
+ var COLUMN_META = {
237
+ draft: { title: '草稿:已保存未发布(补完依赖/上下文后统一发布)', empty: '暂无草稿——「+ 新建任务」勾选存为草稿后会出现在这里' },
238
+ pending: { title: '待办:已发布待派发(等 Worker 领取,或等依赖/文件锁释放)', empty: '暂无待办——已发布待派发的任务会出现在这里' },
239
+ 'in-progress': { title: '进行中:Worker 子代理正在执行', empty: '暂无执行中——Worker 领取任务后会出现在这里' },
240
+ verifying: { title: '验证中:Worker 已完成,独立 Verifier 复跑验收中', empty: '暂无卡片——Worker 提交验收后会出现在这里' },
241
+ resolved: { title: '已完成:Verifier 验收通过', empty: '暂无已完成——验收通过后会出现在这里' },
242
+ blocked: { title: '阻塞:卡住待处理(等依赖/等主窗口裁决)', empty: '暂无阻塞——任务卡住待裁决时会出现在这里' }
243
+ }
234
244
  var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
235
- var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, verifyUserGuide: true, lessonPushed: {}, isRoot: true, isRootEverTrue: false, isRootFalseN: 0, isRootStable: 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, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', mpRole: '', mpSize: '', usageSummary: null, childStats: {}, tasksErr: '', tasksHash: '' }
245
+ var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, verifyUserGuide: true, lessonPushed: {}, isRoot: true, isRootEverTrue: false, isRootFalseN: 0, isRootStable: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', filterReview: false, selectMode: false, selected: {}, undoSnapshot: null, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', mpRole: '', mpSize: '', usageSummary: null, childStats: {}, tasksErr: '', tasksHash: '' }
236
246
 
237
247
  // #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
238
248
  try {
@@ -507,19 +517,24 @@ function apply(ctx) {
507
517
  function onColDragLeave(st) { if (state.dragOver === st) { state.dragOver = null; notify() } }
508
518
  function onColDrop(e, st) { e.preventDefault(); try { var data = JSON.parse(e.dataTransfer.getData('text/plain')); state.dragOver = null; state.dragTask = null; transition(data.id, data.status, st); notify() } catch (_) {} }
509
519
 
510
- // #13 筛选:文本(标题/描述/ID)+ 优先级多选 + 标签
520
+ // ===== 风险队列「待你过目」客户端单一事实源(异常驱动审视①)=====
521
+ // 命中=host 现算的 reviewHint.score>0;待过目=命中 + 未阅(reviewedAt 空)+ 终态(resolved/archived)。
522
+ // 徽章 / chip 过滤 / digest 计数三处同用这一个谓词,避免口径分叉。
523
+ function reviewPending(t) { return !!(t && t.reviewHint && t.reviewHint.score > 0 && !t.reviewedAt && (t.status === 'resolved' || t.status === 'archived')) }
524
+ // #13 筛选:文本(标题/描述/ID)+ 优先级多选 + 标签 + 待过目队列
511
525
  function passFilter(t) {
512
526
  var q = state.filterQ.trim().toLowerCase()
513
527
  if (q && (t.title + ' ' + (t.description || '') + ' ' + t.id).toLowerCase().indexOf(q) < 0) return false
514
528
  if (state.filterPrio.length > 0 && state.filterPrio.indexOf(t.priority || 'medium') < 0) return false
515
529
  if (state.filterTag && (t.tags || []).indexOf(state.filterTag) < 0) return false
530
+ if (state.filterReview && !reviewPending(t)) return false
516
531
  return true
517
532
  }
518
533
  function allTags() { var s = {}; state.tasks.forEach(function (t) { (t.tags || []).forEach(function (g) { s[g] = 1 }) }); return Object.keys(s) }
519
534
  // #18 依赖未满足判断(依赖不存在视为阻塞——创建时已校验,防御性兜底)
520
535
  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') }) }
521
536
  // #19 管线档位元数据
522
- var pipeMeta = { full: { icon: 'flask-conical', label: '全流程(执行+验证)', short: '全流程' }, work: { icon: 'file-text', label: '免验证(只做不验)', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直接处理', short: '直办' } }
537
+ var pipeMeta = { full: { icon: 'flask-conical', label: '全流程:Worker 执行 + Verifier 独立验收', short: '全流程' }, work: { icon: 'file-text', label: '免验证:Worker 只做,不验收', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直办:不派 Worker,由当前对话直接处理', short: '直办' } }
523
538
  // ===== 耗时口径三分离(task-mutdnitw):排队 ≠ 执行 ≠ 验收,不再把排队算进耗时 =====
524
539
  // waitOf:⏳ 排队时长 = createdAt → claimedAt(未领取则统计到 now)——"等了多久"
525
540
  // execOf:⏱ 执行时长 = claimedAt → resolvedAt/archivedAt(未定则统计到 now)——"干了多久"
@@ -559,7 +574,7 @@ function apply(ctx) {
559
574
  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)
560
575
  }
561
576
 
562
- 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, '归档'))) }
577
+ 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() }, title: 'Worker / Verifier 执行池(一次性子代理,随任务结算销毁)', 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, '归档'))) }
563
578
 
564
579
  function TopPanel() {
565
580
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect
@@ -578,23 +593,23 @@ function apply(ctx) {
578
593
  if (!open) return null
579
594
  if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
580
595
  var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
581
- var hasFilter = state.filterQ.trim() || state.filterPrio.length > 0 || state.filterTag
596
+ var hasFilter = state.filterQ.trim() || state.filterPrio.length > 0 || state.filterTag || state.filterReview
582
597
  if (hasFilter) { active = active.filter(passFilter); archived = archived.filter(passFilter) }
583
598
  var content
584
599
  if (view === 'dashboard') { content = React.createElement(Dashboard) }
585
- else if (view === 'archive') { content = React.createElement(ArchiveView) }
600
+ else if (view === 'archive') { content = detailId ? React.createElement(DetailView) : React.createElement(ArchiveView) }
586
601
  else if (view === 'team') { content = React.createElement(TeamView) }
587
602
  else if (detailId) { content = React.createElement(DetailView) }
588
603
  else {
589
604
  var prioRank = { critical: 4, high: 3, medium: 2, low: 1 }
590
- var cols = COLUMNS.map(function (st) { var colTasks = active.filter(function (t) { return t.status === st }); colTasks.sort(function (a, b) { var e = (b.escalation ? 1 : 0) - (a.escalation ? 1 : 0); if (e) return e; var sk = (b.stuckSince ? 1 : 0) - (a.stuckSince ? 1 : 0); if (sk) return sk; var db = (depsBlocked(a) ? 1 : 0) - (depsBlocked(b) ? 1 : 0); if (db) return db; var p = (prioRank[b.priority] || 2) - (prioRank[a.priority] || 2); if (p) return p; return (a.createdAt || '').localeCompare(b.createdAt || '') }); var isOver = dragOver === st; return React.createElement('div', { key: st, onDragOver: function (e) { onColDragOver(e, st) }, onDragLeave: function () { onColDragLeave(st) }, onDrop: function (e) { onColDrop(e, st) }, style: { flex: '1 1 0', minWidth: 150, borderRadius: 8, padding: 6, background: isOver ? C.nested : 'transparent', border: isOver ? '1px dashed ' + C.brand : '1px dashed transparent', transition: 'background .15s, border .15s' } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4, padding: '0 2px' } }, (statusLabels[st] || st) + ' (' + colTasks.length + ')'), colTasks.map(function (t) { return React.createElement(Card, { key: t.id, task: t }) })) })
605
+ var cols = COLUMNS.map(function (st) { var colTasks = active.filter(function (t) { return t.status === st }); colTasks.sort(function (a, b) { var e = (b.escalation ? 1 : 0) - (a.escalation ? 1 : 0); if (e) return e; var sk = (b.stuckSince ? 1 : 0) - (a.stuckSince ? 1 : 0); if (sk) return sk; var db = (depsBlocked(a) ? 1 : 0) - (depsBlocked(b) ? 1 : 0); if (db) return db; var p = (prioRank[b.priority] || 2) - (prioRank[a.priority] || 2); if (p) return p; return (a.createdAt || '').localeCompare(b.createdAt || '') }); var isOver = dragOver === st; return React.createElement('div', { key: st, onDragOver: function (e) { onColDragOver(e, st) }, onDragLeave: function () { onColDragLeave(st) }, onDrop: function (e) { onColDrop(e, st) }, style: { flex: '1 1 0', minWidth: 150, borderRadius: 8, padding: 6, background: isOver ? C.nested : 'transparent', border: isOver ? '1px dashed ' + C.brand : '1px dashed transparent', transition: 'background .15s, border .15s' } }, React.createElement('div', { title: (COLUMN_META[st] || {}).title, style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4, padding: '0 2px', cursor: 'help' } }, (statusLabels[st] || st) + ' (' + colTasks.length + ')'), colTasks.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2, opacity: 0.7, padding: '2px 6px 8px', textAlign: 'center' } }, (COLUMN_META[st] || {}).empty) : colTasks.map(function (t) { return React.createElement(Card, { key: t.id, task: t }) })) })
591
606
  // 归档区:排序选择器(默认归档时间降序)+ 纵向滚动列表(不再横向拉条)
592
607
  var archSorted = archived.slice().sort(function (a, b) {
593
608
  if (state.archSort === 'title') return (a.title || '').localeCompare(b.title || '')
594
609
  var ta = a.archivedAt || a.resolvedAt || a.createdAt || '', tb = b.archivedAt || b.resolvedAt || b.createdAt || ''
595
610
  return state.archSort === 'time-asc' ? ta.localeCompare(tb) : tb.localeCompare(ta)
596
611
  })
597
- 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 ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '16px 8px 12px', color: C.text2, fontSize: 12 } }, '点击右上角', React.createElement('span', { style: { color: C.brand, fontWeight: 600 } }, '「+ 新建任务」'), '创建第一张卡,或在对话里说一句 ', React.createElement('code', { style: { fontSize: 11, background: C.nested, padding: '0 4px', borderRadius: 3 } }, 'task_create')) : null, React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
612
+ 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 ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '16px 8px 12px', color: C.text2, fontSize: 12 } }, '点击右上角', React.createElement('span', { style: { color: C.brand, fontWeight: 600 } }, '「+ 新建任务」'), '创建第一张卡,或直接在对话里说:帮我建一张任务卡') : null, React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
598
613
  }
599
614
  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' } },
600
615
  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 } },
@@ -626,13 +641,14 @@ function apply(ctx) {
626
641
  // #14 批量操作条(#16 带一步撤销:快照操作前的 priority/status;delete 是真删,明确不支持撤销)
627
642
  function BatchBar() {
628
643
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect
629
- var _a = useState(0), cnt = _a[0], setCnt = _a[1]; var _b = useState(false), on = _b[0], setOn = _b[1]; var _c = useState(''), msg = _c[0], setMsg = _c[1]; var _d = useState(null), undo = _d[0], setUndo = _d[1]
644
+ var _a = useState(0), cnt = _a[0], setCnt = _a[1]; var _b = useState(false), on = _b[0], setOn = _b[1]; var _c = useState(''), msg = _c[0], setMsg = _c[1]; var _d = useState(null), undo = _d[0], setUndo = _d[1]; var _e = useState([]), reasons = _e[0], setReasons = _e[1]
630
645
  useEffect(function () { function update() { setCnt(Object.keys(state.selected).length); setOn(state.selectMode); setUndo(state.undoSnapshot) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
631
646
  if (!on) return null
632
647
  var ids = Object.keys(state.selected)
633
648
  function snapshot(op) { return { op: op, at: Date.now(), items: ids.map(function (id) { var t = getTask(id); return t ? { id: id, priority: t.priority, status: t.status } : null }).filter(Boolean) } }
634
- // 跳过原因只回显第一条(如"任务正在执行中,请先…"),避免把整屏门禁文案塞进批量条
635
- function firstReason(r) { var m = (r && r.reasons) || {}; var ks = Object.keys(m); return ks.length ? String(m[ks[0]]) : '' }
649
+ // 跳过原因逐条渲染:host batch-op 返回 reasons 对象(id → 原因),转成「标题:原因」列表
650
+ // (巡检 n-muyj5p755l3c:批量归档跳过此前只显示「跳过 N」无原因)
651
+ function reasonRows(r) { var m = (r && r.reasons) || {}; return Object.keys(m).map(function (id) { var t = getTask(id); return { title: t ? t.title : shortId(id), reason: String(m[id]) } }) }
636
652
  function run(op, value) {
637
653
  // 只有可撤销的 op 才需要快照(delete 是真删,快照里的 status/priority 复活不回整张卡)
638
654
  var snap = op === 'delete' ? null : snapshot(op)
@@ -641,8 +657,8 @@ function apply(ctx) {
641
657
  state.selected = {}
642
658
  // 删除无 undo:done>0 也不留撤销快照(否则「↩️ 撤销」点了恢复不了,纯粹误导);其他 op 维持一步撤销
643
659
  state.undoSnapshot = (r && r.done > 0 && op !== 'delete') ? snap : null
644
- var reason = (op === 'delete' && r && r.reasons) ? firstReason(r) : ''
645
- setMsg('✅ 已处理 ' + (r && r.done || 0) + ' 个' + (r && r.skipped && r.skipped.length ? ',跳过 ' + r.skipped.length + (reason ? '(' + reason + ')' : '') : ''))
660
+ setReasons(reasonRows(r))
661
+ setMsg('✅ 已处理 ' + (r && r.done || 0) + ' 个' + (r && r.skipped && r.skipped.length ? ',跳过 ' + r.skipped.length + ' 个' : ''))
646
662
  fetchTasks()
647
663
  }).catch(function (e) { setMsg('⚠️ ' + String(e)) })
648
664
  }
@@ -654,7 +670,7 @@ function apply(ctx) {
654
670
  }
655
671
  function doUndo() { if (!undo) return; rpc('batch-undo', { snapshot: undo }).then(function (r) { setMsg('↩️ 已撤销 ' + (r && r.done || 0) + ' 个'); state.undoSnapshot = null; fetchTasks() }).catch(function (e) { setMsg('⚠️ ' + String(e)) }) }
656
672
  var btn = { fontSize: 10, padding: '3px 10px', borderRadius: 4, border: 'none', cursor: 'pointer', fontWeight: 600 }
657
- 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 } },
673
+ 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, flexWrap: 'wrap' } },
658
674
  React.createElement('span', { style: { fontSize: 11, color: C.text, fontWeight: 600 } }, '已选 ' + cnt + ' 项'),
659
675
  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), '批量归档'),
660
676
  ['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]) }),
@@ -662,21 +678,28 @@ function apply(ctx) {
662
678
  React.createElement('button', { onClick: batchDelete, disabled: cnt === 0, title: '删除选中任务(不可恢复;执行中/验证中的需先终止,已落定请用归档)', style: Object.assign({}, btn, { background: C.err, color: C_INV, display: 'inline-flex', alignItems: 'center', gap: 3 }) }, ic('trash-2', 11), '批量删除'),
663
679
  undo ? React.createElement('button', { onClick: doUndo, title: '撤销最近一次批量操作(删除不可撤销)', style: Object.assign({}, btn, { background: C.brand, color: C_INV }) }, '↩️ 撤销') : null,
664
680
  msg ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, msg) : null,
665
- 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' } }, '清除选择'))
681
+ 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' } }, '清除选择'),
682
+ reasons.length > 0 ? React.createElement('div', { style: { flexBasis: '100%', marginTop: 4, padding: '4px 6px', background: C.card, border: '1px solid ' + C.border, borderRadius: 4, maxHeight: 96, overflowY: 'auto' } },
683
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2 } }, '跳过原因'),
684
+ reasons.map(function (it, i) { return React.createElement('div', { key: i, style: { fontSize: 10, color: C.text2, marginBottom: 1, wordBreak: 'break-word' } }, it.title + ':' + it.reason) })) : null)
666
685
  }
667
686
 
668
687
  function FilterBar() {
669
688
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect
670
- var _a = useState(state.filterQ), q = _a[0], setQ = _a[1]; var _b = useState(state.filterPrio), fp = _b[0], setFp = _b[1]; var _c = useState(state.filterTag), ft = _c[0], setFt = _c[1]
671
- useEffect(function () { function update() { setQ(state.filterQ); setFp(state.filterPrio); setFt(state.filterTag) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
689
+ // 优先级筛选单一事实源:chip 高亮与「清除」显隐统一读全局 state.filterPrio(消灭本地 fp 副本),
690
+ // 曾因本地态与全局 filterPrio 不同帧更新出现「清除按钮消失但高亮残留」的矛盾画面(巡检 n-muyimk1hpdxv)
691
+ var _a = useState(state.filterQ), q = _a[0], setQ = _a[1]; var _c = useState(state.filterTag), ft = _c[0], setFt = _c[1]
692
+ useEffect(function () { function update() { setQ(state.filterQ); setFt(state.filterTag) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
672
693
  function setQ2(v) { state.filterQ = v; notify() }
673
694
  function togglePrio(p) { var i = state.filterPrio.indexOf(p); if (i >= 0) state.filterPrio.splice(i, 1); else state.filterPrio.push(p); notify() }
674
- function clearAll() { state.filterQ = ''; state.filterPrio = []; state.filterTag = ''; notify() }
675
- var hasFilter = q.trim() || fp.length > 0 || ft
695
+ function toggleReview() { state.filterReview = !state.filterReview; notify() }
696
+ function clearAll() { state.filterQ = ''; state.filterPrio = []; state.filterTag = ''; state.filterReview = false; notify() }
697
+ var hasFilter = q.trim() || state.filterPrio.length > 0 || ft || state.filterReview
676
698
  var chipBase = { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: '1px solid ' + C.border, cursor: 'pointer', background: 'transparent', color: C.text2, transition: 'all .15s' }
677
699
  return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
678
700
  React.createElement('input', { value: q, onChange: function (e) { setQ2(e.target.value) }, placeholder: '🔍 搜索标题/描述/ID…', style: { flex: '0 1 200px', fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text } }),
679
- ['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]) }),
701
+ ['critical', 'high', 'medium', 'low'].map(function (p) { var on = state.filterPrio.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]) }),
702
+ React.createElement('button', { key: 'review', onClick: toggleReview, title: '只看命中风险信号且未阅的已完成/归档卡(已阅卡不计)', style: Object.assign({}, chipBase, state.filterReview ? { background: C.warn, color: C_INV, borderColor: C.warn } : {}) }, '👁 待你过目'),
680
703
  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,
681
704
  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)
682
705
  }
@@ -773,7 +796,7 @@ function apply(ctx) {
773
796
  // Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
774
797
  // focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
775
798
  function activate() { 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() } }
776
- return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, 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] || '中'), noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, 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, 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' } },
799
+ return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, 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] || '中'), reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0, marginTop: 1 } }, '👁 建议过目') : null, noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, 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, 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' } },
777
800
  epic ? React.createElement('span', { title: '史诗父卡:' + csDone + '/' + cs.total + ' 个子任务已了结(完成/取消/归档)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + csDone + '/' + cs.total) : null,
778
801
  // hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
779
802
  epic ? hookPhaseBadge(t) : null,
@@ -781,8 +804,8 @@ function apply(ctx) {
781
804
  (Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
782
805
  React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
783
806
  (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,
784
- t.status === 'in-progress' && t.claimedBy ? React.createElement('span', null, '⚡ ' + shortId(t.claimedBy)) : null,
785
- t.assignee ? React.createElement('span', null, '👤→' + shortId(t.assignee)) : null,
807
+ t.status === 'in-progress' && t.claimedBy ? React.createElement('span', { title: '执行该任务的子代理会话:' + t.claimedBy }, '⚡ ' + String(t.claimedBy).slice(0, 8) + '…') : null,
808
+ t.assignee ? React.createElement('span', { title: '指派给:' + t.assignee }, '👤→' + String(t.assignee).slice(0, 8) + '…') : null,
786
809
  durB ? React.createElement('span', { title: durB.tip }, durB.txt) : null,
787
810
  // 删除入口(仅草稿/待办/阻塞,多选模式下隐藏以免误触):hover 才由透明转红,平时不抢视觉
788
811
  delOk ? React.createElement('span', {
@@ -801,32 +824,47 @@ function apply(ctx) {
801
824
  t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null, depBlock ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '依赖全部完成后才会派发' }, '⛓ 等待「' + depWaitTitle + '」' + (depWaitN > 1 ? ' 等 ' + depWaitN + ' 个' : '')) : null) }
802
825
 
803
826
  function ArchiveView() {
804
- var _R = React; var useState = _R.useState, useEffect = _R.useEffect
827
+ var _R = React; var useState = _R.useState
805
828
  var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
806
- useEffect(function () { if (!state.archived.length) fetchArchived() }, [])
807
- var list = state.archived.filter(function (t) {
829
+ // 归档数据源口径对齐仪表盘(反馈 n-muyg3e0m9z7i):直接读 state.tasks(get-tasks 全量含归档),
830
+ // 不再依赖独立 state.archived/fetchArchived——旧路径异步填库却未接入 TopPanel 的 notify 更新项,
831
+ // 归档 tab 打开后停在「暂无归档任务」空态,而仪表盘「已归档」计数来自 state.tasks(全量),二者对不上。
832
+ // 全量 + 按归档时间倒序(archivedAt 优先,回退 resolvedAt/createdAt)+ 顶部注明条数。
833
+ var archived = state.tasks.filter(function (t) { return t.status === 'archived' })
834
+ var list = archived.filter(function (t) {
808
835
  if (!q) return true
809
836
  var qq = q.toLowerCase()
810
837
  return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
811
838
  }).sort(function (a, b) {
812
- // 完成时间最近在前:resolvedAt 优先,回退验收时间/归档时间/最后活动时间
813
- function doneTs(t) { return t.resolvedAt || (t.verification && t.verification.at) || t.archivedAt || taskLastTs(t) || '' }
814
- return (doneTs(b) || '').localeCompare(doneTs(a) || '')
839
+ // 归档时间倒序:archivedAt 优先,回退 resolvedAt/createdAt(老卡可能缺 archivedAt)
840
+ function archTs(t) { return t.archivedAt || t.resolvedAt || t.createdAt || '' }
841
+ return (archTs(b) || '').localeCompare(archTs(a) || '')
815
842
  })
816
- function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchArchived(); fetchTasks() }).catch(function () {}) }
843
+ // 工作模式(与 fetchTasks 同口径):list=清单模式(恢复不重派)不弹;auto/team 恢复即重派 → confirm 门禁
844
+ var wm = state.workMode || (state.teamMode ? 'team' : (state.boardMode === 'auto' ? 'auto' : 'list'))
845
+ function restore(id) {
846
+ if (wm !== 'list') {
847
+ if (!window.confirm('恢复待办后将按当前工作模式立即重新派发,消耗一轮 Worker+Verifier token。继续?')) return
848
+ }
849
+ rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchTasks() }).catch(function () {})
850
+ }
817
851
  return React.createElement('div', null,
818
852
  React.createElement('input', { value: q, onChange: function (e) { setQ(e.target.value); state.archQ = e.target.value }, placeholder: '检索标题 / ID / 标签…', style: { width: '100%', padding: '5px 8px', fontSize: 11, border: '1px solid ' + C.border2, borderRadius: 5, background: C.card, color: C.text, marginBottom: 8 } }),
819
- state.archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
853
+ React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '共 ' + archived.length + ' 条归档(全量,与仪表盘「已归档」同口径)'),
854
+ archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
820
855
  list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
821
856
  list.map(function (t) {
822
- return React.createElement('div', { key: t.id, style: { padding: '6px 8px', marginBottom: 5, background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } },
857
+ // 归档行点击进详情(用户 2026-10-09 指令):复用 state.detailId 机制,与看板卡同路径;
858
+ // 详情页对 archived 的操作区既有门禁不回归(恢复待办=保存并重置、删除仅 draft/pending/blocked)。
859
+ return React.createElement('div', { key: t.id, onClick: function () { state.detailId = t.id; notify() }, title: '点击查看详情', style: { padding: '6px 8px', marginBottom: 5, background: C.card, border: '1px solid ' + C.border, borderRadius: 6, cursor: 'pointer' } },
823
860
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
824
861
  React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: t.title }, t.title),
862
+ reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0 } }, '👁 建议过目') : null,
825
863
  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,
826
864
  React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, t.archivedAt ? ago(t.archivedAt) : '')),
827
865
  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,
828
866
  React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4, flexWrap: 'wrap' } },
829
- 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' } }, '↩ 恢复待办'),
867
+ React.createElement('button', { onClick: function (e) { e.stopPropagation(); restore(t.id) }, title: wm === 'list' ? '恢复为待办' : '恢复为待办(将按当前工作模式立即重新派发,消耗一轮 Worker+Verifier token)', style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '↩ 恢复待办'),
830
868
  (function () {
831
869
  var runs = historyRuns(t)
832
870
  if (!runs.length) return React.createElement('span', { style: { fontSize: 9, color: C.text2, opacity: 0.6, padding: '2px 0' } }, '无历史会话')
@@ -835,7 +873,7 @@ function apply(ctx) {
835
873
  var seq = r.role === 'verifier' ? (++vN) : (++wN)
836
874
  var isV = r.role === 'verifier'
837
875
  return React.createElement('button', {
838
- key: i, onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
876
+ key: i, onClick: function (e) { e.stopPropagation(); if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
839
877
  title: (isV ? 'Verifier' : 'Worker') + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
840
878
  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' }
841
879
  }, '→ ' + (isV ? 'V' : 'W') + '#' + seq)
@@ -856,6 +894,7 @@ function apply(ctx) {
856
894
  var _tc = useState(''), touchesRaw = _tc[0], setTouchesRaw = _tc[1]
857
895
  var _dp = useState({}), depSel = _dp[0], setDepSel = _dp[1]
858
896
  var _ac = useState(''), acc = _ac[0], setAcc = _ac[1]
897
+ var _tg = useState(''), tagsRaw = _tg[0], setTagsRaw = _tg[1]
859
898
  var _dr = useState(wm === 'team'), asDraft = _dr[0], setAsDraft = _dr[1]
860
899
  var _bs = useState(false), busy = _bs[0], setBusy = _bs[1]
861
900
  var _er = useState(''), err = _er[0], setErr = _er[1]
@@ -870,9 +909,10 @@ function apply(ctx) {
870
909
  var t = title.trim()
871
910
  if (!t) { setErr('⚠️ 请先填写标题'); return }
872
911
  var touches = touchesRaw.split(/[\n,,;;]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
912
+ var tags = tagsRaw.split(/[,,]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
873
913
  var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
874
914
  setBusy(true); setErr(''); setOkMsg(''); setWarn('')
875
- rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
915
+ rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, tags: tags, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
876
916
  setBusy(false)
877
917
  if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
878
918
  fetchTasks()
@@ -887,7 +927,7 @@ function apply(ctx) {
887
927
  }
888
928
  var inp = { width: '100%', boxSizing: 'border-box', fontSize: 11, padding: '4px 6px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit' }
889
929
  var lblStyle = { fontSize: 10, color: C.text2, marginBottom: 3 }
890
- function field(label, node) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node) }
930
+ function field(label, node, hint) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node, hint ? React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.4 } }, hint) : null) }
891
931
  var btnGhost = { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 5, background: 'transparent', color: C.text2, cursor: 'pointer' }
892
932
  var btnPrimary = { fontSize: 11, padding: '4px 14px', border: 'none', borderRadius: 5, background: C.brand, color: C_INV, fontWeight: 600, cursor: busy ? 'default' : 'pointer', opacity: busy ? 0.6 : 1 }
893
933
  // 视口级 overlay(修裁切,反馈 n-mut9rzl4mxe4):fixed 全屏遮罩脱离看板抽屉(maxHeight 60vh + overflow hidden)
@@ -907,8 +947,9 @@ function apply(ctx) {
907
947
  React.createElement('div', { style: { flex: '1 1 0', minWidth: 0 } }, field('管线', React.createElement('select', { value: pipe, onChange: function (e) { setPipe(e.target.value) }, style: inp },
908
948
  React.createElement('option', { value: 'full' }, '全流程(执行+验证)'),
909
949
  React.createElement('option', { value: 'work' }, '免验证(只做不验)'),
910
- React.createElement('option', { value: 'direct' }, '主窗口处理'))))),
911
- field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp })),
950
+ React.createElement('option', { value: 'direct' }, '主窗口处理')), '管线 = 任务走哪种流程:全流程 = Worker 做 + Verifier 验;免验证 = 只做不验;主窗口处理 = 你在对话里直接办'))),
951
+ field('标签(逗号分隔,可空)', React.createElement('input', { value: tagsRaw, onChange: function (e) { setTagsRaw(e.target.value) }, placeholder: '如 bug, epic, 待验收', style: inp }), '标签 = 用于筛选的逗号分隔关键字(自动 trim + 去空)'),
952
+ field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp }), 'touches = 本卡要改动的文件(glob 匹配);声明后并行任务间防互踩(文件锁)'),
912
953
  field('依赖(全部完成后才派发,可空)', cands.length === 0
913
954
  ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '当前无草稿/待办任务可选')
914
955
  : React.createElement('div', { style: { maxHeight: 96, overflowY: 'auto', border: '1px solid ' + C.border, borderRadius: 4, padding: '4px 6px', background: C.card } }, cands.map(function (x) {
@@ -916,7 +957,7 @@ function apply(ctx) {
916
957
  React.createElement('input', { type: 'checkbox', checked: !!depSel[x.id], onChange: function () { toggleDep(x.id) } }),
917
958
  React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, (statusLabels[x.status] || x.status) + ' · ' + (x.title || x.id)))
918
959
  }))),
919
- field('验收脚本(命令,可空)', React.createElement('input', { value: acc, onChange: function (e) { setAcc(e.target.value) }, placeholder: '如 node --test test/x.test.js', style: Object.assign({}, inp, { fontFamily: 'monospace' }) })),
960
+ field('验收脚本(命令,可空)', React.createElement('input', { value: acc, onChange: function (e) { setAcc(e.target.value) }, placeholder: '如 node --test test/x.test.js', style: Object.assign({}, inp, { fontFamily: 'monospace' }) }), '验收脚本 = 硬性验收命令:Worker 必须真实运行,Verifier 独立复跑'),
920
961
  React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: C.text, cursor: 'pointer', marginBottom: 6, flexWrap: 'wrap' } },
921
962
  React.createElement('input', { type: 'checkbox', checked: asDraft, onChange: function (e) { setAsDraft(e.target.checked) } }),
922
963
  '存为草稿',
@@ -928,7 +969,7 @@ function apply(ctx) {
928
969
  err ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 6 } }, err) : null,
929
970
  React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
930
971
  React.createElement('button', { onClick: close, style: btnGhost }, '取消'),
931
- React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即进入派发池', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并派发'))))))
972
+ React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即交给 Worker 执行', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并交给 Worker'))))))
932
973
  }
933
974
 
934
975
  // ============================================================================
@@ -1151,11 +1192,79 @@ function apply(ctx) {
1151
1192
  React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 4, lineHeight: 1.6 } }, '每个点位是一次真实 agent 运行(不是声明式命令):prompt 只给薄框架契约与上下文,动作由 hook agent 自行决策,吃不准会歧义上报;失败即转阻塞等人裁决(重试/跳过/放弃),不会自动重跑。清空 prompt 保存 = 撤掉该点位。hooks 仅主窗口可设(host 门禁,失败原因原样回显)。'))
1152
1193
  }
1153
1194
 
1195
+ // ===== 审视摘要区(异常驱动审视①④):详情顶部新块,仅 reviewHint 命中时显示 =====
1196
+ // 四问动线 + 来源徽标,素材全用现有字段纯展示拼装(reviewHint 由 host 现算、get-tasks 透出):
1197
+ // ①为什么在这 → reviewHint.reasons(host 现算中文理由行)
1198
+ // ②改了什么 → deliverable.summary + deliverable.diff(git diff --stat 原文),标「Worker 汇报」来源徽标
1199
+ // ③机器怎么验的 → verification 结论(标「Verifier 实证」)+ 验收脚本 + 驳回/仲裁史(标「系统记录」)
1200
+ // ④怎么亲自确认 → verification.userTest 自测指南复用渲染,缺省显示 tier=internal 提示
1201
+ // 已阅(reviewedAt 非空)仍显示:已阅只收敛列表徽章/chip,详情块不消失——用户要真正过目,不能打开即闪退。
1202
+ function ReviewSummary(props) {
1203
+ var task = props.task
1204
+ var rh = task.reviewHint
1205
+ if (!rh || !(rh.score > 0)) return null
1206
+ function srcBadge(text, color) { return React.createElement('span', { style: { fontSize: 9, padding: '0 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + color + ' 16%, transparent)', color: color, fontWeight: 600, flexShrink: 0 } }, text) }
1207
+ // ③ 驳回/仲裁史:history(歧义/裁决/驳回/干预/rejected)+ messages(rejection/arbitration)
1208
+ var histLines = []
1209
+ if (Array.isArray(task.history)) task.history.forEach(function (h) { if (h && h.note && /歧义|裁决|驳回|干预|rejected/i.test(h.note)) histLines.push('[' + (h.timestamp || '') + '] ' + String(h.note).slice(0, 160)) })
1210
+ if (Array.isArray(task.messages)) task.messages.forEach(function (m) { if (m && (m.kind === 'rejection' || m.kind === 'arbitration')) histLines.push('[' + (m.kind === 'rejection' ? '驳回' : '裁决') + '] ' + String(m.text || '').slice(0, 160)) })
1211
+ histLines = histLines.slice(0, 5)
1212
+ var q3rows = []
1213
+ if (task.verification) q3rows.push(React.createElement('div', { key: 'v', style: { display: 'flex', alignItems: 'flex-start', gap: 4, marginBottom: 2 } }, srcBadge('Verifier 实证', C.ok), React.createElement('span', { style: { fontSize: 11, color: C.text } }, (task.verification.verdict === 'approved' ? '验收通过' : '验收驳回') + (task.verification.summary ? ':' + task.verification.summary : ''))))
1214
+ if (task.acceptance) q3rows.push(React.createElement('div', { key: 'a', style: { display: 'flex', alignItems: 'flex-start', gap: 4, marginBottom: 2 } }, srcBadge('系统记录', C.text2), React.createElement('span', { style: { fontSize: 10, color: C.text2, fontFamily: 'monospace' } }, '验收脚本:' + task.acceptance)))
1215
+ if (histLines.length) q3rows.push(React.createElement('div', { key: 'h', style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, srcBadge('系统记录', C.text2), React.createElement('span', { style: { fontSize: 10, color: C.text2, whiteSpace: 'pre-wrap', lineHeight: 1.5 } }, '驳回/仲裁史:\n' + histLines.join('\n'))))
1216
+ // ④ 自测指南:复用现有 userTest 渲染口径,缺省 tier=internal 提示
1217
+ var q4
1218
+ var ut = task.verification && task.verification.userTest
1219
+ if (ut) {
1220
+ var tierMeta = ut.tier === 'ui' ? { c: C.ok, label: 'UI 可操作' } : (ut.tier === 'metric' ? { c: C.brand, label: '看指标' } : { c: C.text2, label: '纯内部' })
1221
+ q4 = React.createElement('div', { style: { marginBottom: 6 } },
1222
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 4 } }, '④ 怎么亲自确认', srcBadge('自测指南 · ' + tierMeta.label, tierMeta.c)),
1223
+ ut.gist ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 2 } }, ut.gist) : null,
1224
+ ut.steps && ut.steps.length ? React.createElement('div', { style: { fontSize: 10, color: C.text } }, ut.steps.map(function (s, i) { return React.createElement('div', { key: i, style: { marginBottom: 1 } }, (i + 1) + '. ' + s) })) : null,
1225
+ ut.expect ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '预期:' + ut.expect) : null)
1226
+ } else {
1227
+ q4 = React.createElement('div', { style: { fontSize: 10, color: C.text2, lineHeight: 1.5 } }, '④ 怎么亲自确认:本卡无用户自测指南(tier=internal / 未附 userTest)——纯内部改动,验证靠测试套件')
1228
+ }
1229
+ return React.createElement('div', { id: 'tskb-review-summary', style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 7%, transparent)' } },
1230
+ React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.warn, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), '待你过目 · 命中 ' + rh.score + ' 条风险信号'),
1231
+ React.createElement('div', { style: { marginBottom: 6 } },
1232
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2 } }, '① 为什么在这'),
1233
+ (rh.reasons || []).map(function (r, i) { return React.createElement('div', { key: i, style: { fontSize: 11, color: C.text, lineHeight: 1.5 } }, '· ' + r) })),
1234
+ React.createElement('div', { style: { marginBottom: 6 } },
1235
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 4 } }, '② 改了什么', srcBadge('Worker 汇报', C.brand)),
1236
+ (task.deliverable && task.deliverable.summary) ? React.createElement('div', { style: { fontSize: 11, color: C.text, whiteSpace: 'pre-wrap', marginBottom: 2 } }, task.deliverable.summary) : React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2 } }, '(无开发描述)'),
1237
+ (task.deliverable && task.deliverable.diff) ? React.createElement('div', { style: { fontSize: 10, color: C.text2, whiteSpace: 'pre-wrap', maxHeight: 80, overflowY: 'auto', fontFamily: 'monospace' } }, task.deliverable.diff) : null),
1238
+ q3rows.length ? React.createElement('div', { style: { marginBottom: 6 } },
1239
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2 } }, '③ 机器怎么验的'),
1240
+ q3rows) : null,
1241
+ q4)
1242
+ }
1243
+
1244
+ // 标签编辑(巡检三连①):逗号分隔 input,提交走 update-task 的 tags 通道(trim + 去空)
1245
+ function TagsEditor(props) {
1246
+ var task = props.task
1247
+ var _R = React; var useState = _R.useState
1248
+ var _s = useState((task.tags || []).join(', ')), draft = _s[0], setDraft = _s[1]
1249
+ function commit() {
1250
+ var tags = draft.split(/[,,]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
1251
+ rpc('update-task', { taskId: task.id, tags: tags }).then(function () { fetchTasks() }).catch(function () {})
1252
+ }
1253
+ return React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
1254
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '🏷 标签(逗号分隔)'),
1255
+ React.createElement('div', { style: { display: 'flex', gap: 4 } },
1256
+ React.createElement('input', { value: draft, onChange: function (e) { setDraft(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') commit() }, placeholder: '如 bug, epic, 待验收', style: { flex: 1, minWidth: 0, fontSize: 11, padding: '4px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit', boxSizing: 'border-box' } }),
1257
+ React.createElement('button', { onClick: commit, title: '保存标签(逗号分隔,自动 trim + 去空)', style: { fontSize: 10, padding: '3px 10px', border: 'none', borderRadius: 4, background: C.brand, color: C_INV, cursor: 'pointer', flexShrink: 0 } }, '保存')))
1258
+ }
1259
+
1154
1260
  function DetailView() {
1155
1261
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
1156
1262
  var _a = useState(task ? task.title : ''), editTitle = _a[0], setEditTitle = _a[1]; var _b = useState(task ? task.description || '' : ''), editDesc = _b[0], setEditDesc = _b[1]; var _c = useState(false), saving = _c[0], setSaving = _c[1]; var _d = useState(state.boardMode), mode = _d[0], setMode = _d[1]
1157
1263
  var _e2 = useState(''), arbAnswer = _e2[0], setArbAnswer = _e2[1]; var _f2 = useState(''), interveneMsg = _f2[0], setInterveneMsg = _f2[1]; var _g2 = useState(''), actionMsg = _g2[0], setActionMsg = _g2[1]; var _h2 = useState('resume'), arbAction = _h2[0], setArbAction = _h2[1]
1158
1264
  useEffect(function () { function update() { setMode(state.boardMode) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
1265
+ // 已阅落账(异常驱动审视①③):打开命中且未阅的详情页即幂等 mark-reviewed——
1266
+ // host 幂等只写一次;本轮渲染仍按「未阅」展示审视摘要区(fetchTasks 回来前不闪退)。
1267
+ useEffect(function () { var t = getTask(state.detailId); if (t && t.reviewHint && t.reviewHint.score > 0 && !t.reviewedAt) { rpc('mark-reviewed', { taskId: t.id }).then(fetchTasks).catch(function () {}) } }, [state.detailId])
1159
1268
  if (!task) { state.detailId = null; return React.createElement('div', { style: { padding: 20, color: C.text2 } }, '任务不存在') }
1160
1269
  function doAction(fn) { fn().then(fetchTasks).catch(function () {}) }
1161
1270
  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) }) }
@@ -1206,7 +1315,9 @@ function apply(ctx) {
1206
1315
  }
1207
1316
  return React.createElement('div', { style: { padding: '4px 2px' } },
1208
1317
  React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
1209
- 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),
1318
+ React.createElement(ReviewSummary, { task: task }),
1319
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 } }, React.createElement('select', { value: task.priority || 'medium', onChange: function (e) { rpc('update-task', { taskId: task.id, priority: 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: (prioColor[task.priority] || prioColor.low), fontWeight: 600 } }, ['critical', 'high', 'medium', 'low'].map(function (p) { return React.createElement('option', { key: p, value: p }, prioLabel[p] + '(' + p + ')') })), 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),
1320
+ React.createElement(TagsEditor, { key: task.id, task: task }),
1210
1321
  // 流转到按钮组(键盘可达的状态迁移入口,替代拖拽;无合法迁移的状态(draft/resolved 等)整块不渲染)
1211
1322
  flowBtns.length > 0 ? React.createElement('div', { role: 'group', 'aria-label': '状态流转', style: { display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap', marginBottom: 8, padding: '5px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
1212
1323
  React.createElement('span', { style: { fontSize: 10, fontWeight: 600, color: C.text2 } }, '⇄ 流转到:'),
@@ -1223,11 +1334,16 @@ function apply(ctx) {
1223
1334
  React.createElement('div', { style: { fontSize: 10, display: 'flex', flexWrap: 'wrap', gap: 5 } }, task.waitingForTouches.map(function (id) { var t2 = getTask(id); return React.createElement('span', { key: id, onClick: function () { if (t2) { state.detailId = id; notify() } }, title: t2 ? (t2.title + ' (' + id + ')') : id, style: { padding: '1px 5px', borderRadius: 3, background: C.nested, border: '1px solid ' + C.border, color: t2 ? C.brand : C.text2, cursor: t2 ? 'pointer' : 'default' } }, (t2 ? t2.title.slice(0, 20) : shortId(id)) + ' · ' + shortId(id)) })),
1224
1335
  Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 4 } }, '本任务 touches: ' + task.touches.join('、')) : null) : null,
1225
1336
  Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '🔒 文件级排他(touches): ' + task.touches.join('、')) : null,
1337
+ // 起草 lint(task.lintWarnings[],host 建卡/更新卡现算落盘,get-tasks 透出):
1338
+ // 四规则软警告(粒度过粗/双仓库歧义/缺验收/标题缺动词或描述空)随卡展示,缺字段/空数组整块不渲染(老卡零影响)。
1339
+ Array.isArray(task.lintWarnings) && task.lintWarnings.length ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
1340
+ task.lintWarnings.map(function (w, i) { return React.createElement('div', { key: i, style: { fontSize: 10, color: C.text2, lineHeight: 1.6, display: 'flex', alignItems: 'flex-start', gap: 4 } }, React.createElement('span', { style: { flexShrink: 0 } }, '⚠️'), React.createElement('span', null, w)) })) : null,
1226
1341
  (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,
1227
1342
  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' } },
1228
1343
  ic('activity', 10),
1229
1344
  React.createElement('span', null, '已运行 ' + elapsedSince(task.claimedAt) + '(软超时 ' + state.softTimeoutMin + ' 分提醒 · 硬超时 ' + state.hardTimeoutMin + ' 分自动终止)'),
1230
1345
  elapsedMin(task.claimedAt) > state.softTimeoutMin ? React.createElement('span', { style: { color: C.warn, fontWeight: 600 } }, '⏱ 已超软超时') : null,
1346
+ React.createElement('span', { title: '当前活跃 run 的子会话 id(continuable 即 childId);终止前先到子代理列表核对该 id 的同名条目,避免误杀别的合法 Worker', style: { fontSize: 10, color: C.text2, fontFamily: 'monospace', wordBreak: 'break-all', flexBasis: '100%' } }, '子会话 id: ' + task.claimedBy + '(对应子代理列表同名条目)'),
1231
1347
  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,
1232
1348
  task.escalation ? React.createElement('div', { id: 'tskb-escalation', style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)' } },
1233
1349
  React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), 'Worker 上报歧义 — 等待主窗口裁决'),
@@ -1299,10 +1415,19 @@ function apply(ctx) {
1299
1415
  ut.steps && ut.steps.length ? React.createElement('div', { style: { fontSize: 10, color: C.text, marginBottom: 3 } }, ut.steps.map(function (s, i) { return React.createElement('div', { key: i, style: { marginBottom: 1 } }, (i + 1) + '. ' + s) })) : null,
1300
1416
  ut.expect ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '预期:' + ut.expect) : null)
1301
1417
  })() : null) : null,
1418
+ // ===== 迟到 Verifier 结论块(验收时序三洞 ③):verifier 结论落到已落定卡后的留痕展示 =====
1419
+ // 主窗口抢批/归档早于 Verifier 结算时,其独立结论落 t.lateVerdict 而非静默丢弃——此处整块展示
1420
+ // (verdict/summary/checks + note 说明 + 时间来源),与上方正常 verification 块并列可对照。
1421
+ task.lateVerdict ? React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + (task.lateVerdict.verdict === 'approved' ? C.ok : C.err), borderRadius: 6, background: 'color-mix(in srgb, ' + (task.lateVerdict.verdict === 'approved' ? C.ok : C.err) + ' 8%, transparent)' } },
1422
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 700, color: task.lateVerdict.verdict === 'approved' ? C.ok : C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 12), '迟到 Verifier 结论 · ' + (task.lateVerdict.by || '') + ' · ' + ago(task.lateVerdict.at)),
1423
+ React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 4 } }, String(task.lateVerdict.note || '')),
1424
+ React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 4, whiteSpace: 'pre-wrap' } }, (task.lateVerdict.verdict === 'approved' ? '验收通过' : '验收驳回') + (task.lateVerdict.summary ? ':' + task.lateVerdict.summary : '')),
1425
+ task.lateVerdict.checks ? React.createElement('div', { style: { marginTop: 4 } }, React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2 } }, '核对项'), React.createElement('div', { style: { fontSize: 10, color: C.text2, whiteSpace: 'pre-wrap', maxHeight: 120, overflowY: 'auto' } }, task.lateVerdict.checks)) : null) : null,
1302
1426
  React.createElement(MsgThread, { messages: task.messages, taskId: task.id }),
1303
1427
  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,
1304
1428
  React.createElement('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap', marginTop: 6 } },
1305
- 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), ' 保存并重置']),
1429
+ // 草稿态「保存并重置」实为 resetToPending 静默发布:按钮文案改「保存并发布」+ title 发布预警(巡检 n-muyi5yfec8m0)
1430
+ React.createElement('button', { onClick: saveEdit, disabled: saving, title: task.status === 'draft' ? '发布后按当前工作模式可能被立即派发' : '保存并重置为待办', 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), task.status === 'draft' ? ' 保存并发布' : ' 保存并重置']),
1306
1431
  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,
1307
1432
  (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,
1308
1433
  task.status === 'pending' ? React.createElement('button', { onClick: function () { doAction(function () { return doManualDispatch('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,
@@ -1550,8 +1675,63 @@ function apply(ctx) {
1550
1675
  })) : null)
1551
1676
  }
1552
1677
 
1678
+ // ===== 每日批次摘要(近 24h,异常驱动审视③):报告导出「批次一页纸」段的数据源 =====
1679
+ // 纯聚合现有字段(不新建状态、不重拉):完成/驳回/token/超时按 run 与消息的 at 滚 24h 窗口现算,
1680
+ // 幽灵回收读当前 healthHints 里的一次性 info(「本轮回收 N 个幽灵活跃表项」,无则 0);
1681
+ // 一句话质量趋势按近 24h 落定的 resolved 任务一次通过率(rejectCount=0 占比)。
1682
+ // 范围无关:报告顶部的批次摘要永远反映「最近 24h」实况,不受「统计范围」筛选影响。
1683
+ function batchDigest() {
1684
+ var now = Date.now(), since = now - 86400000
1685
+ var all = (Array.isArray(state.tasks) ? state.tasks : []).concat(Array.isArray(state.archived) ? state.archived : [])
1686
+ var resolved = 0, rejected = 0, tokTot = 0, tokEff = 0, timeouts = 0, trendResolved = 0, trendFirstPass = 0, reviewPending = 0
1687
+ function in24(iso) { var d = Date.parse(iso); return isFinite(d) && d >= since && d <= now }
1688
+ function pos(v) { var n = Number(v); return isFinite(n) && n > 0 ? n : 0 }
1689
+ all.forEach(function (t) {
1690
+ if (!t) return
1691
+ var doneAt = t.resolvedAt || t.verifiedAt || ''
1692
+ if (doneAt && in24(doneAt)) {
1693
+ resolved++
1694
+ if (t.status === 'resolved') { trendResolved++; if (!(t.rejectCount > 0)) trendFirstPass++ }
1695
+ }
1696
+ if (Array.isArray(t.messages)) t.messages.forEach(function (m) { if (m && m.kind === 'rejection' && m.at && in24(m.at)) rejected++ })
1697
+ if (Array.isArray(t.runs)) t.runs.forEach(function (r) {
1698
+ if (!r || !r.at || !in24(r.at)) return
1699
+ var u = (r.usage && typeof r.usage === 'object') ? r.usage : {}
1700
+ var inp = pos(u.input), outp = pos(u.output), cr = pos(u.cacheRead), cw = pos(u.cacheWrite)
1701
+ var tot = pos(u.total) || (inp + outp + cr + cw)
1702
+ tokTot += tot; tokEff += inp + outp + cw
1703
+ if (r.outcome === 'timeout/error' || r.outcome === 'timeout') timeouts++
1704
+ })
1705
+ // ===== 未过目计数(异常驱动审视①⑤):命中风险信号且未阅的已完成/归档卡 =====
1706
+ if (reviewPending(t)) reviewPending++
1707
+ })
1708
+ var ghosts = 0
1709
+ var hints = Array.isArray(state.healthHints) ? state.healthHints : []
1710
+ hints.forEach(function (h) { var m = h && h.text ? String(h.text).match(/本轮回收 (\d+) 个幽灵活跃表项/) : null; if (m) ghosts += pos(m[1]) })
1711
+ var trend = trendResolved > 0 ? ('一次通过率 ' + Math.round(trendFirstPass / trendResolved * 100) + '%(近 24h 完成 ' + trendResolved + ' 张中 ' + trendFirstPass + ' 张零驳回)') : '近 24h 无完成记录,暂无质量趋势'
1712
+ var has = resolved > 0 || rejected > 0 || tokTot > 0 || timeouts > 0 || ghosts > 0 || trendResolved > 0
1713
+ return { has: has, resolved: resolved, rejected: rejected, tokTot: tokTot, tokEff: tokEff, timeouts: timeouts, ghosts: ghosts, trend: trend, reviewPending: reviewPending }
1714
+ }
1715
+
1553
1716
  function buildReport() {
1554
1717
  var lines = ['# 任务看板报告', '', '生成时间: ' + new Date().toLocaleString(), '工作模式: ' + (workModeNames[state.workMode || (state.teamMode ? 'team' : (state.boardMode === 'auto' ? 'auto' : 'list'))] || '自动派发'), '', '统计范围: ' + rangeLabel()]
1718
+ // ===== 每日批次摘要(异常驱动审视③):报告顶部一页纸,近 24h 五字段 + 一句话质量趋势 =====
1719
+ var dg = batchDigest()
1720
+ if (dg.has) {
1721
+ lines.push('## 批次摘要(近 24 小时)')
1722
+ lines.push('- 完成:' + dg.resolved + ' 张')
1723
+ lines.push('- 驳回:' + dg.rejected + ' 次')
1724
+ lines.push('- Token:约 ' + fmtTokens(dg.tokTot) + (dg.tokEff > 0 ? '(其中有效 ' + fmtTokens(dg.tokEff) + ')' : ''))
1725
+ lines.push('- 异常事件:超时 ' + dg.timeouts + ' 次 · 幽灵回收 ' + dg.ghosts + ' 次')
1726
+ lines.push('- 质量趋势:' + dg.trend)
1727
+ lines.push('- 未过目:' + dg.reviewPending + ' 张')
1728
+ lines.push('')
1729
+ } else {
1730
+ lines.push('## 批次摘要(近 24 小时)')
1731
+ lines.push('近 24 小时无活动记录(无完成 / 驳回 / 消耗 / 异常事件)')
1732
+ lines.push('- 未过目:' + dg.reviewPending + ' 张')
1733
+ lines.push('')
1734
+ }
1555
1735
  var groups = { inProgress: [], verifying: [], pending: [], blocked: [], resolved: [] }
1556
1736
  var rg = activeRange()
1557
1737
  state.tasks.forEach(function (t) {
@@ -1762,7 +1942,7 @@ function apply(ctx) {
1762
1942
  // 范围说明(task-muwc7hjd):用户常把 RangeFilter 当成「整页过滤」,但今日大数字与近 7 天柱子
1763
1943
  // 是自身固定口径(今日=本地今天、近 7 天=最近 7 个本地日)——不随范围变,必须在文案里讲清,
1764
1944
  // 否则「选了范围数字没变」看起来像 bug。模型分布/Top8/累计才是范围生效的三处。
1765
- React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:累计与 Top 8 仅看板派发的 Worker/Verifier run 消耗;主窗口行=本会话对话消耗,与看板派发口径并列不混入(单列一行 + 模型分布尾部一条,不进累计 / Top 8);主数字(今日 / 近 7 天 / 模型分布 / Top 8 / 主窗口行)均为总量口径(含缓存读);有效消耗(input+output+cacheWrite,不含缓存读)见悬浮 title'),
1945
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:累计与 Top 8 仅看板派发的 Worker/Verifier run(= 一次执行)消耗;主窗口行=本会话对话消耗,与看板派发口径并列不混入(单列一行 + 模型分布尾部一条,不进累计 / Top 8);主数字(今日 / 近 7 天 / 模型分布 / Top 8 / 主窗口行)均为总量口径(含缓存读);有效消耗(input+output+cacheWrite,不含缓存读)见悬浮 title'),
1766
1946
  React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。'),
1767
1947
  // 「→ 模型表现」滚动锚链接(task-muxhtgi2 卡3):一键滚到下方模型×场景七指标表
1768
1948
  // (与详情页 goArbitration 同一 scrollIntoView 原语;目标区空态也挂锚 id,链接始终可达)
@@ -1913,8 +2093,8 @@ function apply(ctx) {
1913
2093
  var days14 = lastNDays(14)
1914
2094
  var todayKey = localDayKey()
1915
2095
  // 块头:左块名 + 右侧今日/汇总值(fontWeight 400 小字)
1916
- function blockHead(label, rightEl) {
1917
- return React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'baseline', gap: 6 } }, label,
2096
+ function blockHead(label, rightEl, tip) {
2097
+ return React.createElement('div', { title: tip, style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'baseline', gap: 6 } }, label,
1918
2098
  rightEl ? React.createElement('span', { style: { marginLeft: 'auto', fontWeight: 400, fontSize: 9 } }, rightEl) : null)
1919
2099
  }
1920
2100
  // ===== ① 一次通过率趋势(近 14 天):当日率 = 落定 resolved 任务中零驳回(rejectCount=0)占比 =====
@@ -1927,14 +2107,14 @@ function apply(ctx) {
1927
2107
  var fpHas = false; fpCells.forEach(function (c) { if (c.rate !== null) fpHas = true })
1928
2108
  var fpToday = fpCells[fpCells.length - 1]
1929
2109
  var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
1930
- blockHead('一次通过率(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: fpToday.rate !== null ? C.brand : C.text2, fontWeight: 600 } }, fpToday.rate !== null ? mpPct(fpToday.rate) : '—'))),
2110
+ blockHead('一次通过率(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: fpToday.rate !== null ? C.brand : C.text2, fontWeight: 600 } }, fpToday.rate !== null ? mpPct(fpToday.rate) : '—')), '一次通过率 = 当天落定「已完成」任务里零驳回(rejectCount=0)的占比;驳回 = Verifier 验收判 reject'),
1931
2111
  fpHas ? qtDayBars(fpCells, C.ok, todayKey)
1932
2112
  : React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 resolved 任务落定'))
1933
2113
  // ===== ② 卡时长分布(四桶横条):创建 → 落定(verifiedAt||resolvedAt)墙钟,含排队/执行/验收全程 =====
1934
2114
  var durMax = 1
1935
2115
  if (durB) QT_DUR_BUCKETS.forEach(function (b) { var v = Number(durB[b.k]) || 0; if (v > durMax) durMax = v })
1936
2116
  var block2 = React.createElement('div', { style: { flex: '1 1 180px', minWidth: 170 } },
1937
- blockHead('卡时长分布', durSum > 0 ? React.createElement('span', null, React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, String(durSum)), ' 张落定卡') : null),
2117
+ blockHead('卡时长分布', durSum > 0 ? React.createElement('span', null, React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, String(durSum)), ' 张落定卡') : null, '卡时长 = 创建 → 落定的墙钟(含排队/执行/验收全程)'),
1938
2118
  durSum === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无时长样本')
1939
2119
  : React.createElement('div', null, QT_DUR_BUCKETS.map(function (b) {
1940
2120
  var v = Number(durB[b.k]) || 0
@@ -1955,13 +2135,13 @@ function apply(ctx) {
1955
2135
  var toHas = false; toCells.forEach(function (c) { if (c.rate !== null) toHas = true })
1956
2136
  var toToday = toCells[toCells.length - 1]
1957
2137
  var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
1958
- blockHead('超时率走势(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: toToday.rate !== null ? C.warn : C.text2, fontWeight: 600 } }, toToday.rate !== null ? mpPct(toToday.rate) : '—'))),
2138
+ blockHead('超时率走势(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: toToday.rate !== null ? C.warn : C.text2, fontWeight: 600 } }, toToday.rate !== null ? mpPct(toToday.rate) : '—')), '超时率 = 超时落定(timeout/error)的 run 占当天 run 的比例'),
1959
2139
  toHas ? qtDayBars(toCells, C.warn, todayKey)
1960
2140
  : React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 run 落定'))
1961
2141
  // ===== ④ 续跑成功率(范围总量口径,卡2出参无 byDay):resume:true 续跑记录中 completed 占比 =====
1962
2142
  var rsmRate = (rsm && typeof rsm.rate === 'number' && isFinite(rsm.rate)) ? rsm.rate : null
1963
2143
  var block4 = React.createElement('div', { style: { flex: '1 1 180px', minWidth: 170 } },
1964
- blockHead('续跑成功率', rsmRuns > 0 ? React.createElement('span', null, React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, String((rsm && rsm.completed) || 0) + '/' + rsmRuns), ' 续跑成功') : null),
2144
+ blockHead('续跑成功率', rsmRuns > 0 ? React.createElement('span', null, React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, String((rsm && rsm.completed) || 0) + '/' + rsmRuns), ' 续跑成功') : null, '续跑成功率 = resume 续跑记录中 outcome=completed 的占比;续跑 = 前次超时/失败后继续跑'),
1965
2145
  rsmRuns === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无续跑记录')
1966
2146
  : React.createElement('div', { title: '续跑成功率 = resume:true 续跑记录中 outcome=completed 的占比(范围总量口径,host 出参无逐日);续跑 = 前次 run 超时/失败后续跑' },
1967
2147
  React.createElement('div', { style: { fontSize: 16, fontWeight: 700, color: rsmRate !== null ? C.ok : C.text2 } }, rsmRate !== null ? mpPct(rsmRate) : '—'),
@@ -2031,15 +2211,18 @@ function apply(ctx) {
2031
2211
  function HealthHints() {
2032
2212
  var hints = (Array.isArray(state.healthHints) ? state.healthHints : []).filter(function (h) { return h && h.text })
2033
2213
  if (!hints.length) return null
2034
- // 两级配色:warn=⚠️ 琥珀(C.warn);info=ℹ️ 蓝灰(DSH 现有 business 信息色——色板无独立 info 档)
2214
+ // 三级配色:err=🚨 红(C.err,数据安全红线);warn=⚠️ 琥珀(C.warn);info=ℹ️ 蓝灰(DSH 现有 business 信息色)
2035
2215
  var infoColor = 'var(--dsw-alias-state-business-primary)'
2036
2216
  return React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } },
2037
2217
  React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('activity', 11), '架构健康'),
2218
+ // 术语白话(巡检 n-muyg5m7o5r00):settle/touches/p90 等黑话第一屏无解,补一行术语说明
2219
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 5, lineHeight: 1.5 } }, '运行时自检告警,多为自动恢复的瞬时异常。术语:settle = 任务结算通道;touches = 任务声明的改动文件(文件锁);p90 = 执行时长第 90 百分位'),
2038
2220
  hints.map(function (h, i) {
2039
2221
  var warn = h.level === 'warn'
2040
- var col = warn ? C.warn : infoColor
2222
+ var err = h.level === 'err'
2223
+ var col = err ? C.err : (warn ? C.warn : infoColor)
2041
2224
  return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'baseline', gap: 5, fontSize: 10, lineHeight: 1.5, color: col, marginBottom: 3 } },
2042
- React.createElement('span', { style: { flexShrink: 0 } }, warn ? '⚠️' : 'ℹ️'),
2225
+ React.createElement('span', { style: { flexShrink: 0 } }, err ? '🚨' : (warn ? '⚠️' : 'ℹ️')),
2043
2226
  React.createElement('span', null, String(h.text)))
2044
2227
  }))
2045
2228
  }
@@ -2075,12 +2258,12 @@ function apply(ctx) {
2075
2258
  function pick(m) { if (m === cur) return; rpc('set-work-mode', { mode: m }).then(fetchTasks).catch(function () {}) }
2076
2259
  // 档位文案:名称 + 一句说明(说明常驻显示在按钮内,选中态高亮不可用 title 才看得到)
2077
2260
  var opts = [
2078
- { k: 'list', name: '清单模式', desc: '看板当 TODO 列表', icon: 'clipboard-list' },
2079
- { k: 'auto', name: '自动派发', desc: '即建即派给 Worker', icon: 'zap' },
2080
- { k: 'team', name: 'Team 托管', desc: '草稿起步,主窗口裁决', icon: 'users' }
2261
+ { k: 'list', name: '清单模式', desc: '只读清单,不自动派发', icon: 'clipboard-list' },
2262
+ { k: 'auto', name: '自动派发', desc: '建卡即派给后台子代理', icon: 'zap' },
2263
+ { k: 'team', name: 'Team 托管', desc: '草稿起步,你裁决全流程', icon: 'users' }
2081
2264
  ]
2082
2265
  var btnBase = { border: 'none', cursor: 'pointer', fontWeight: 500, textAlign: 'center', display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 1, padding: '3px 9px', transition: 'all .15s', lineHeight: 1.25 }
2083
- return React.createElement('div', { title: '工作模式(清单 / 自动派发 / Team 托管)——歧义裁决、Verifier 验收、touches 排他在三档下全部生效', style: { display: 'inline-flex', borderRadius: 8, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, opts.map(function (o) {
2266
+ return React.createElement('div', { title: '工作模式(清单 / 自动派发 / Team 托管)——歧义裁决、Verifier 验收、文件锁(touches)排他在三档下全部生效', style: { display: 'inline-flex', borderRadius: 8, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, opts.map(function (o) {
2084
2267
  var on = cur === o.k
2085
2268
  return React.createElement('button', {
2086
2269
  key: o.k, onClick: function () { pick(o.k) }, title: o.name + ':' + o.desc,
@@ -2119,13 +2302,13 @@ function apply(ctx) {
2119
2302
  var ps = state.poolStatus
2120
2303
  if (!ps) return null
2121
2304
  var wActive = (ps.workers || []).filter(function (w) { return w.busy }).length
2122
- var wTotal = (ps.workers || []).length
2305
+ var wTotal = state.maxWorkers // 分母=并发上限而非当前池实例数,空闲也显示 0/上限
2123
2306
  var vActive = (ps.verifiers || []).filter(function (v) { return v.busy }).length
2124
- var vTotal = (ps.verifiers || []).length
2307
+ var vTotal = state.maxVerifiers
2125
2308
  return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
2126
- // 池水位 title 标注(反馈 n-mut9rzoq3flu):裸数字新用户看不懂,⚡=Worker 在跑/上限,✓=Verifier 在跑/上限
2127
- React.createElement('span', { title: 'Worker 在跑/上限', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), wActive + '/' + wTotal),
2128
- React.createElement('span', { title: 'Verifier 在跑/上限', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), vActive + '/' + vTotal))
2309
+ // 池水位标注(反馈 n-mut9rzoq3flu / n-muyg4wl9208c):裸数字第一屏无解,补可见文字标签 + title
2310
+ React.createElement('span', { title: '活跃 Worker 数/上限(⚡ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), 'Worker ' + wActive + '/' + wTotal),
2311
+ React.createElement('span', { title: '活跃 Verifier 数/上限(✓ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), 'Verifier ' + vActive + '/' + vTotal))
2129
2312
  }
2130
2313
 
2131
2314
  // #11 头部减负:池配置收纳进 ⚙️ 弹出层(含 #17 verifier 异构模型设置)
@@ -2135,7 +2318,7 @@ function apply(ctx) {
2135
2318
  var _b = useState(false), dirty = _b[0], setDirty = _b[1]
2136
2319
  var _m = useState(null), models = _m[0], setModels = _m[1]
2137
2320
  var cfgKey = props.cfgKey || 'verifierModel'
2138
- var label = props.label || 'V模型'
2321
+ var label = props.label || 'Verifier 模型'
2139
2322
  // 下拉列出当前网关可用模型(host list-models RPC);枚举失败降级为文本输入
2140
2323
  useEffect(function () {
2141
2324
  var cancelled = false
@@ -2188,13 +2371,13 @@ function apply(ctx) {
2188
2371
  open && pos ? React.createElement('div', { style: { position: 'fixed', top: pos.top + 'px', right: pos.right + 'px', padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 4px 16px rgba(0,0,0,0.15)', zIndex: 1000, whiteSpace: 'nowrap', maxHeight: 'calc(100vh - ' + (pos.top + 12) + 'px)', overflowY: 'auto' } },
2189
2372
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
2190
2373
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
2191
- React.createElement(PoolCfg, { label: 'W并发', cfgKey: 'maxWorkers', value: props.maxW }),
2192
- React.createElement(PoolCfg, { label: 'V并发', cfgKey: 'maxVerifiers', value: props.maxV })),
2374
+ React.createElement(PoolCfg, { label: 'Worker 并发', cfgKey: 'maxWorkers', value: props.maxW }),
2375
+ React.createElement(PoolCfg, { label: 'Verifier 并发', cfgKey: 'maxVerifiers', value: props.maxV })),
2193
2376
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
2194
- React.createElement(ModelCfg, { label: 'W模型', cfgKey: 'workerModel', value: props.workerModel }),
2377
+ React.createElement(ModelCfg, { label: 'Worker 模型', cfgKey: 'workerModel', value: props.workerModel }),
2195
2378
  React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Worker 执行模型)')),
2196
2379
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
2197
- React.createElement(ModelCfg, { label: 'V模型', cfgKey: 'verifierModel', value: props.verifierModel }),
2380
+ React.createElement(ModelCfg, { label: 'Verifier 模型', cfgKey: 'verifierModel', value: props.verifierModel }),
2198
2381
  React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)')),
2199
2382
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '两级超时(软超时只提醒主窗口,由你决定继续等待或终止)'),
2200
2383
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
@@ -2245,7 +2428,7 @@ function apply(ctx) {
2245
2428
  React.createElement('span', { style: { marginLeft: 'auto', fontSize: 9, padding: '1px 6px', borderRadius: 3, background: C.brand, color: C_INV } }, '忙碌')),
2246
2429
  m.model ? React.createElement('div', { style: { fontSize: 9, color: C.warn, marginBottom: 3 }, title: '异构模型审查' }, '🧬 ' + m.model) : null,
2247
2430
  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),
2248
- React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '一次性子代理会话' }, '会话 ' + String(m.runId || '').slice(0, 8) + '…(用完即销毁)'))
2431
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '一次性子代理会话:' + (m.runId || '') }, '会话 ' + String(m.runId || '').slice(0, 8) + '…(用完即销毁)'))
2249
2432
  }
2250
2433
  var ws = ps.workers || [], vs = ps.verifiers || []
2251
2434
  var _R2 = React; var _st = _R2.useState(''), editMsg = _st[0], setEditMsg = _st[1]