dsh-agent-board 1.7.6 → 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/README.md +40 -2
- package/index.mjs +3 -2
- package/lib/client/board-list.js +23 -8
- package/lib/client/dashboard.js +10 -5
- package/lib/client/kernel.js +9 -4
- package/lib/client/task-detail.js +79 -1
- package/lib/client.js +121 -18
- package/lib/core.mjs +103 -1
- package/lib/dispatch.mjs +26 -2
- package/lib/health.mjs +22 -0
- package/lib/notify.mjs +23 -1
- package/lib/rpc.mjs +96 -12
- package/lib/store.mjs +89 -11
- package/package.json +1 -1
- package/test/archive-detail.test.mjs +43 -0
- package/test/core.test.mjs +62 -5
- package/test/headless-smoke.test.mjs +25 -0
- package/test/helpers/mock-ctx.mjs +17 -3
- package/test/review-hints.test.mjs +170 -0
- package/test/store-data-safety.test.mjs +112 -0
- package/test/verify-timing.test.mjs +266 -0
package/lib/client.js
CHANGED
|
@@ -242,7 +242,7 @@ function apply(ctx) {
|
|
|
242
242
|
blocked: { title: '阻塞:卡住待处理(等依赖/等主窗口裁决)', empty: '暂无阻塞——任务卡住待裁决时会出现在这里' }
|
|
243
243
|
}
|
|
244
244
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
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: '', 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: '' }
|
|
246
246
|
|
|
247
247
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
248
248
|
try {
|
|
@@ -517,12 +517,17 @@ function apply(ctx) {
|
|
|
517
517
|
function onColDragLeave(st) { if (state.dragOver === st) { state.dragOver = null; notify() } }
|
|
518
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 (_) {} }
|
|
519
519
|
|
|
520
|
-
//
|
|
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)+ 优先级多选 + 标签 + 待过目队列
|
|
521
525
|
function passFilter(t) {
|
|
522
526
|
var q = state.filterQ.trim().toLowerCase()
|
|
523
527
|
if (q && (t.title + ' ' + (t.description || '') + ' ' + t.id).toLowerCase().indexOf(q) < 0) return false
|
|
524
528
|
if (state.filterPrio.length > 0 && state.filterPrio.indexOf(t.priority || 'medium') < 0) return false
|
|
525
529
|
if (state.filterTag && (t.tags || []).indexOf(state.filterTag) < 0) return false
|
|
530
|
+
if (state.filterReview && !reviewPending(t)) return false
|
|
526
531
|
return true
|
|
527
532
|
}
|
|
528
533
|
function allTags() { var s = {}; state.tasks.forEach(function (t) { (t.tags || []).forEach(function (g) { s[g] = 1 }) }); return Object.keys(s) }
|
|
@@ -588,11 +593,11 @@ function apply(ctx) {
|
|
|
588
593
|
if (!open) return null
|
|
589
594
|
if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
|
|
590
595
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
591
|
-
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
|
|
592
597
|
if (hasFilter) { active = active.filter(passFilter); archived = archived.filter(passFilter) }
|
|
593
598
|
var content
|
|
594
599
|
if (view === 'dashboard') { content = React.createElement(Dashboard) }
|
|
595
|
-
else if (view === 'archive') { content = React.createElement(ArchiveView) }
|
|
600
|
+
else if (view === 'archive') { content = detailId ? React.createElement(DetailView) : React.createElement(ArchiveView) }
|
|
596
601
|
else if (view === 'team') { content = React.createElement(TeamView) }
|
|
597
602
|
else if (detailId) { content = React.createElement(DetailView) }
|
|
598
603
|
else {
|
|
@@ -687,12 +692,14 @@ function apply(ctx) {
|
|
|
687
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) } }, [])
|
|
688
693
|
function setQ2(v) { state.filterQ = v; notify() }
|
|
689
694
|
function togglePrio(p) { var i = state.filterPrio.indexOf(p); if (i >= 0) state.filterPrio.splice(i, 1); else state.filterPrio.push(p); notify() }
|
|
690
|
-
function
|
|
691
|
-
|
|
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
|
|
692
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' }
|
|
693
699
|
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
|
|
694
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 } }),
|
|
695
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 } : {}) }, '👁 待你过目'),
|
|
696
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,
|
|
697
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)
|
|
698
705
|
}
|
|
@@ -789,7 +796,7 @@ function apply(ctx) {
|
|
|
789
796
|
// Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
|
|
790
797
|
// focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
|
|
791
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() } }
|
|
792
|
-
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' } },
|
|
793
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,
|
|
794
801
|
// hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
|
|
795
802
|
epic ? hookPhaseBadge(t) : null,
|
|
@@ -833,21 +840,31 @@ function apply(ctx) {
|
|
|
833
840
|
function archTs(t) { return t.archivedAt || t.resolvedAt || t.createdAt || '' }
|
|
834
841
|
return (archTs(b) || '').localeCompare(archTs(a) || '')
|
|
835
842
|
})
|
|
836
|
-
|
|
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
|
+
}
|
|
837
851
|
return React.createElement('div', null,
|
|
838
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 } }),
|
|
839
853
|
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '共 ' + archived.length + ' 条归档(全量,与仪表盘「已归档」同口径)'),
|
|
840
854
|
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
|
|
841
855
|
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
842
856
|
list.map(function (t) {
|
|
843
|
-
|
|
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' } },
|
|
844
860
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
845
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,
|
|
846
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,
|
|
847
864
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, t.archivedAt ? ago(t.archivedAt) : '')),
|
|
848
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,
|
|
849
866
|
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4, flexWrap: 'wrap' } },
|
|
850
|
-
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' } }, '↩ 恢复待办'),
|
|
851
868
|
(function () {
|
|
852
869
|
var runs = historyRuns(t)
|
|
853
870
|
if (!runs.length) return React.createElement('span', { style: { fontSize: 9, color: C.text2, opacity: 0.6, padding: '2px 0' } }, '无历史会话')
|
|
@@ -856,7 +873,7 @@ function apply(ctx) {
|
|
|
856
873
|
var seq = r.role === 'verifier' ? (++vN) : (++wN)
|
|
857
874
|
var isV = r.role === 'verifier'
|
|
858
875
|
return React.createElement('button', {
|
|
859
|
-
key: i, onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
|
|
876
|
+
key: i, onClick: function (e) { e.stopPropagation(); if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
|
|
860
877
|
title: (isV ? 'Verifier' : 'Worker') + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
|
|
861
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' }
|
|
862
879
|
}, '→ ' + (isV ? 'V' : 'W') + '#' + seq)
|
|
@@ -877,6 +894,7 @@ function apply(ctx) {
|
|
|
877
894
|
var _tc = useState(''), touchesRaw = _tc[0], setTouchesRaw = _tc[1]
|
|
878
895
|
var _dp = useState({}), depSel = _dp[0], setDepSel = _dp[1]
|
|
879
896
|
var _ac = useState(''), acc = _ac[0], setAcc = _ac[1]
|
|
897
|
+
var _tg = useState(''), tagsRaw = _tg[0], setTagsRaw = _tg[1]
|
|
880
898
|
var _dr = useState(wm === 'team'), asDraft = _dr[0], setAsDraft = _dr[1]
|
|
881
899
|
var _bs = useState(false), busy = _bs[0], setBusy = _bs[1]
|
|
882
900
|
var _er = useState(''), err = _er[0], setErr = _er[1]
|
|
@@ -891,9 +909,10 @@ function apply(ctx) {
|
|
|
891
909
|
var t = title.trim()
|
|
892
910
|
if (!t) { setErr('⚠️ 请先填写标题'); return }
|
|
893
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 })
|
|
894
913
|
var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
|
|
895
914
|
setBusy(true); setErr(''); setOkMsg(''); setWarn('')
|
|
896
|
-
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) {
|
|
897
916
|
setBusy(false)
|
|
898
917
|
if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
|
|
899
918
|
fetchTasks()
|
|
@@ -929,6 +948,7 @@ function apply(ctx) {
|
|
|
929
948
|
React.createElement('option', { value: 'full' }, '全流程(执行+验证)'),
|
|
930
949
|
React.createElement('option', { value: 'work' }, '免验证(只做不验)'),
|
|
931
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 + 去空)'),
|
|
932
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 匹配);声明后并行任务间防互踩(文件锁)'),
|
|
933
953
|
field('依赖(全部完成后才派发,可空)', cands.length === 0
|
|
934
954
|
? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '当前无草稿/待办任务可选')
|
|
@@ -1172,11 +1192,79 @@ function apply(ctx) {
|
|
|
1172
1192
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 4, lineHeight: 1.6 } }, '每个点位是一次真实 agent 运行(不是声明式命令):prompt 只给薄框架契约与上下文,动作由 hook agent 自行决策,吃不准会歧义上报;失败即转阻塞等人裁决(重试/跳过/放弃),不会自动重跑。清空 prompt 保存 = 撤掉该点位。hooks 仅主窗口可设(host 门禁,失败原因原样回显)。'))
|
|
1173
1193
|
}
|
|
1174
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
|
+
|
|
1175
1260
|
function DetailView() {
|
|
1176
1261
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
|
|
1177
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]
|
|
1178
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]
|
|
1179
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])
|
|
1180
1268
|
if (!task) { state.detailId = null; return React.createElement('div', { style: { padding: 20, color: C.text2 } }, '任务不存在') }
|
|
1181
1269
|
function doAction(fn) { fn().then(fetchTasks).catch(function () {}) }
|
|
1182
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) }) }
|
|
@@ -1227,7 +1315,9 @@ function apply(ctx) {
|
|
|
1227
1315
|
}
|
|
1228
1316
|
return React.createElement('div', { style: { padding: '4px 2px' } },
|
|
1229
1317
|
React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
|
|
1230
|
-
React.createElement(
|
|
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 }),
|
|
1231
1321
|
// 流转到按钮组(键盘可达的状态迁移入口,替代拖拽;无合法迁移的状态(draft/resolved 等)整块不渲染)
|
|
1232
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 } },
|
|
1233
1323
|
React.createElement('span', { style: { fontSize: 10, fontWeight: 600, color: C.text2 } }, '⇄ 流转到:'),
|
|
@@ -1325,6 +1415,14 @@ function apply(ctx) {
|
|
|
1325
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,
|
|
1326
1416
|
ut.expect ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '预期:' + ut.expect) : null)
|
|
1327
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,
|
|
1328
1426
|
React.createElement(MsgThread, { messages: task.messages, taskId: task.id }),
|
|
1329
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,
|
|
1330
1428
|
React.createElement('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap', marginTop: 6 } },
|
|
@@ -1585,7 +1683,7 @@ function apply(ctx) {
|
|
|
1585
1683
|
function batchDigest() {
|
|
1586
1684
|
var now = Date.now(), since = now - 86400000
|
|
1587
1685
|
var all = (Array.isArray(state.tasks) ? state.tasks : []).concat(Array.isArray(state.archived) ? state.archived : [])
|
|
1588
|
-
var resolved = 0, rejected = 0, tokTot = 0, tokEff = 0, timeouts = 0, trendResolved = 0, trendFirstPass = 0
|
|
1686
|
+
var resolved = 0, rejected = 0, tokTot = 0, tokEff = 0, timeouts = 0, trendResolved = 0, trendFirstPass = 0, reviewPending = 0
|
|
1589
1687
|
function in24(iso) { var d = Date.parse(iso); return isFinite(d) && d >= since && d <= now }
|
|
1590
1688
|
function pos(v) { var n = Number(v); return isFinite(n) && n > 0 ? n : 0 }
|
|
1591
1689
|
all.forEach(function (t) {
|
|
@@ -1604,13 +1702,15 @@ function apply(ctx) {
|
|
|
1604
1702
|
tokTot += tot; tokEff += inp + outp + cw
|
|
1605
1703
|
if (r.outcome === 'timeout/error' || r.outcome === 'timeout') timeouts++
|
|
1606
1704
|
})
|
|
1705
|
+
// ===== 未过目计数(异常驱动审视①⑤):命中风险信号且未阅的已完成/归档卡 =====
|
|
1706
|
+
if (reviewPending(t)) reviewPending++
|
|
1607
1707
|
})
|
|
1608
1708
|
var ghosts = 0
|
|
1609
1709
|
var hints = Array.isArray(state.healthHints) ? state.healthHints : []
|
|
1610
1710
|
hints.forEach(function (h) { var m = h && h.text ? String(h.text).match(/本轮回收 (\d+) 个幽灵活跃表项/) : null; if (m) ghosts += pos(m[1]) })
|
|
1611
1711
|
var trend = trendResolved > 0 ? ('一次通过率 ' + Math.round(trendFirstPass / trendResolved * 100) + '%(近 24h 完成 ' + trendResolved + ' 张中 ' + trendFirstPass + ' 张零驳回)') : '近 24h 无完成记录,暂无质量趋势'
|
|
1612
1712
|
var has = resolved > 0 || rejected > 0 || tokTot > 0 || timeouts > 0 || ghosts > 0 || trendResolved > 0
|
|
1613
|
-
return { has: has, resolved: resolved, rejected: rejected, tokTot: tokTot, tokEff: tokEff, timeouts: timeouts, ghosts: ghosts, trend: trend }
|
|
1713
|
+
return { has: has, resolved: resolved, rejected: rejected, tokTot: tokTot, tokEff: tokEff, timeouts: timeouts, ghosts: ghosts, trend: trend, reviewPending: reviewPending }
|
|
1614
1714
|
}
|
|
1615
1715
|
|
|
1616
1716
|
function buildReport() {
|
|
@@ -1624,10 +1724,12 @@ function apply(ctx) {
|
|
|
1624
1724
|
lines.push('- Token:约 ' + fmtTokens(dg.tokTot) + (dg.tokEff > 0 ? '(其中有效 ' + fmtTokens(dg.tokEff) + ')' : ''))
|
|
1625
1725
|
lines.push('- 异常事件:超时 ' + dg.timeouts + ' 次 · 幽灵回收 ' + dg.ghosts + ' 次')
|
|
1626
1726
|
lines.push('- 质量趋势:' + dg.trend)
|
|
1727
|
+
lines.push('- 未过目:' + dg.reviewPending + ' 张')
|
|
1627
1728
|
lines.push('')
|
|
1628
1729
|
} else {
|
|
1629
1730
|
lines.push('## 批次摘要(近 24 小时)')
|
|
1630
1731
|
lines.push('近 24 小时无活动记录(无完成 / 驳回 / 消耗 / 异常事件)')
|
|
1732
|
+
lines.push('- 未过目:' + dg.reviewPending + ' 张')
|
|
1631
1733
|
lines.push('')
|
|
1632
1734
|
}
|
|
1633
1735
|
var groups = { inProgress: [], verifying: [], pending: [], blocked: [], resolved: [] }
|
|
@@ -2109,7 +2211,7 @@ function apply(ctx) {
|
|
|
2109
2211
|
function HealthHints() {
|
|
2110
2212
|
var hints = (Array.isArray(state.healthHints) ? state.healthHints : []).filter(function (h) { return h && h.text })
|
|
2111
2213
|
if (!hints.length) return null
|
|
2112
|
-
//
|
|
2214
|
+
// 三级配色:err=🚨 红(C.err,数据安全红线);warn=⚠️ 琥珀(C.warn);info=ℹ️ 蓝灰(DSH 现有 business 信息色)
|
|
2113
2215
|
var infoColor = 'var(--dsw-alias-state-business-primary)'
|
|
2114
2216
|
return React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } },
|
|
2115
2217
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('activity', 11), '架构健康'),
|
|
@@ -2117,9 +2219,10 @@ function apply(ctx) {
|
|
|
2117
2219
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 5, lineHeight: 1.5 } }, '运行时自检告警,多为自动恢复的瞬时异常。术语:settle = 任务结算通道;touches = 任务声明的改动文件(文件锁);p90 = 执行时长第 90 百分位'),
|
|
2118
2220
|
hints.map(function (h, i) {
|
|
2119
2221
|
var warn = h.level === 'warn'
|
|
2120
|
-
var
|
|
2222
|
+
var err = h.level === 'err'
|
|
2223
|
+
var col = err ? C.err : (warn ? C.warn : infoColor)
|
|
2121
2224
|
return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'baseline', gap: 5, fontSize: 10, lineHeight: 1.5, color: col, marginBottom: 3 } },
|
|
2122
|
-
React.createElement('span', { style: { flexShrink: 0 } }, warn ? '⚠️' : 'ℹ️'),
|
|
2225
|
+
React.createElement('span', { style: { flexShrink: 0 } }, err ? '🚨' : (warn ? '⚠️' : 'ℹ️')),
|
|
2123
2226
|
React.createElement('span', null, String(h.text)))
|
|
2124
2227
|
}))
|
|
2125
2228
|
}
|
package/lib/core.mjs
CHANGED
|
@@ -571,6 +571,28 @@ export function pushRejection(t, summary, checks, at, by) {
|
|
|
571
571
|
return true
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
+
// ===== 迟到 verdict 落账(验收时序三洞 ③,用户 n-mv0ebretgv1o)=====
|
|
575
|
+
// 事故:主窗口在 Verifier 活跃期 task_verify 抢批(早 0.4s)+ 归档,Verifier 的独立结论与自测
|
|
576
|
+
// 指南被静默吞掉、无回执。修法:verifier 结论(工具通道 board_verdict / 文本通道 settleVerifier)
|
|
577
|
+
// 落到已 resolved/archived/cancelled 卡时不再静默丢弃——落 t.lateVerdict 留痕。
|
|
578
|
+
// 纯函数(零 IO):写 t.lateVerdict;userTest 若开关开且解析得出 → 挂 t.lateVerdict.userTest,
|
|
579
|
+
// 并补挂 t.verification.userTest(原 verification 缺 userTest 时;主窗口 task_verify approved 不写
|
|
580
|
+
// t.verification,此时用迟到结论的 verdict/summary/checks 兜底建 verification,避免详情页渲染空验收块)。
|
|
581
|
+
// 返回 { lateRejected, wasApproved }:调用方据此决定要不要发 err 级通知 + healthHint
|
|
582
|
+
// (I/O 留在调用方,core 保持零依赖)。wasApproved 必须先算——下方可能覆写 t.verification。
|
|
583
|
+
export function lateVerdictApply(t, verdict, summary, checks, userTest, at, by, verifyUserGuideOn) {
|
|
584
|
+
var approved = verdict === 'approved'
|
|
585
|
+
var wasApproved = t.status === 'resolved' || (t.status === 'archived' && !!(t.verifiedAt || t.verifiedBy || (t.verification && t.verification.verdict === 'approved')))
|
|
586
|
+
t.lateVerdict = { verdict: verdict, summary: summary || '', checks: checks || '', at: at || new Date().toISOString(), by: by || 'system', note: '迟到结论:卡在落定后收到' }
|
|
587
|
+
var ut = verifyUserGuideOn !== false ? normalizeUserTest(userTest) : null
|
|
588
|
+
if (ut) {
|
|
589
|
+
t.lateVerdict.userTest = ut
|
|
590
|
+
if (!t.verification) t.verification = { verdict: verdict, summary: summary || '', checks: checks || '', at: at || new Date().toISOString(), by: by || 'system' }
|
|
591
|
+
if (!t.verification.userTest) t.verification.userTest = ut
|
|
592
|
+
}
|
|
593
|
+
return { lateRejected: !approved, wasApproved: wasApproved }
|
|
594
|
+
}
|
|
595
|
+
|
|
574
596
|
// ===== 一次性子代理 prompt 构建(上下文由主窗口 agent 写入 description/instructions,系统只追加生命周期记录)=====
|
|
575
597
|
// 学习飞轮 v1 软召回引导(feedbackEnabled 开时才拼进 prompt):环境里若有笔记/记忆类工具,先查历史教训再动手。
|
|
576
598
|
// 只是"提示先搜"——看板不代查、不调用任何记忆工具、也无从知道有没有这类工具(零耦合)。
|
|
@@ -1104,7 +1126,10 @@ export function tasksHash(tasks) {
|
|
|
1104
1126
|
Array.isArray(t.tags) ? t.tags.join(',') : '',
|
|
1105
1127
|
t.claimedBy, t.claimedAt, t.resolvedAt, t.verifiedAt, t.archivedAt,
|
|
1106
1128
|
t.lastError, t.lastProgress && t.lastProgress.text,
|
|
1107
|
-
t.frozen ? '1' : '', t.stuckSince, t.escalation && t.escalation.question
|
|
1129
|
+
t.frozen ? '1' : '', t.stuckSince, t.escalation && t.escalation.question,
|
|
1130
|
+
t.reviewedAt,
|
|
1131
|
+
t.reviewHint ? String(t.reviewHint.score) + ':' + (Array.isArray(t.reviewHint.reasons) ? t.reviewHint.reasons.join('|') : '') : '',
|
|
1132
|
+
t.lateVerdict ? (t.lateVerdict.verdict + ':' + t.lateVerdict.at) : ''
|
|
1108
1133
|
].join('\u0001'))
|
|
1109
1134
|
}
|
|
1110
1135
|
var s = parts.join('\u0002')
|
|
@@ -1112,3 +1137,80 @@ export function tasksHash(tasks) {
|
|
|
1112
1137
|
for (var j = 0; j < s.length; j++) h = (((h << 5) + h) ^ s.charCodeAt(j)) >>> 0
|
|
1113
1138
|
return h.toString(36)
|
|
1114
1139
|
}
|
|
1140
|
+
|
|
1141
|
+
// ===== 风险队列「待你过目」:reviewHint 现算(异常驱动审视①,task-muzikj1y)=====
|
|
1142
|
+
// 纯函数零 IO 零落库:唯一会落库的新字段是 reviewedAt(rpc.mjs mark-reviewed 落),
|
|
1143
|
+
// reviewHint 每轮 get-tasks 现算(四硬信号恒计入 + 两软信号仅质量异动告警激活期间计入)。
|
|
1144
|
+
// 信号口径:
|
|
1145
|
+
// 硬① 被驳回过:rejectCount>0 或 verification.verdict='rejected' 或 messages 有 rejection 包 或 runs 含 rejected 结局
|
|
1146
|
+
// 硬② 上报过歧义:messages 有 arbitration 裁决包
|
|
1147
|
+
// 硬③ touches 碰核心文件:任一条 touches 的 basename ∈ {dispatch.mjs, rpc.mjs, core.mjs}
|
|
1148
|
+
// 硬④ full 无 acceptance:pipeline=full 且无硬性验收脚本
|
|
1149
|
+
// 软⑤ diff>300 行:deliverable.diff(git diff --stat)解析出的总增删行数 >300(仅告警态)
|
|
1150
|
+
// 软⑥ 续跑≥2 次:runs 中 resume:true 条目数 ≥2(仅告警态)
|
|
1151
|
+
export var REVIEW_CORE_FILES = ['dispatch.mjs', 'rpc.mjs', 'core.mjs']
|
|
1152
|
+
export var REVIEW_DIFF_LINES = 300
|
|
1153
|
+
export var REVIEW_RESUME_MIN = 2
|
|
1154
|
+
function reviewBaseName(p) { var s = String(p == null ? '' : p); var i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\')); return i >= 0 ? s.slice(i + 1) : s }
|
|
1155
|
+
function reviewTouchesCore(touches) {
|
|
1156
|
+
var list = Array.isArray(touches) ? touches : []
|
|
1157
|
+
for (var i = 0; i < list.length; i++) { if (REVIEW_CORE_FILES.indexOf(reviewBaseName(list[i])) >= 0) return true }
|
|
1158
|
+
return false
|
|
1159
|
+
}
|
|
1160
|
+
function reviewWasRejected(t) {
|
|
1161
|
+
if (!t) return false
|
|
1162
|
+
if ((t.rejectCount || 0) > 0) return true
|
|
1163
|
+
if (t.verification && t.verification.verdict === 'rejected') return true
|
|
1164
|
+
if (Array.isArray(t.messages)) { for (var i = 0; i < t.messages.length; i++) { var m = t.messages[i]; if (m && m.kind === 'rejection') return true } }
|
|
1165
|
+
if (Array.isArray(t.runs)) { for (var j = 0; j < t.runs.length; j++) { var r = t.runs[j]; if (r && r.outcome === 'rejected') return true } }
|
|
1166
|
+
return false
|
|
1167
|
+
}
|
|
1168
|
+
function reviewHasArbitration(t) {
|
|
1169
|
+
if (!t || !Array.isArray(t.messages)) return false
|
|
1170
|
+
for (var i = 0; i < t.messages.length; i++) { var m = t.messages[i]; if (m && m.kind === 'arbitration') return true }
|
|
1171
|
+
return false
|
|
1172
|
+
}
|
|
1173
|
+
// diffStat 解析:优先读汇总行「N files changed, X insertions(+), Y deletions(-)」的增删合计;
|
|
1174
|
+
// 无汇总行回退按每文件「 | N 」改动数累加(git diff --stat 每行路径 + 改动数)。解析不出返回 0(不计软信号)。
|
|
1175
|
+
function reviewDiffLines(t) {
|
|
1176
|
+
var text = t && t.deliverable && t.deliverable.diff ? String(t.deliverable.diff) : ''
|
|
1177
|
+
if (!text) return 0
|
|
1178
|
+
var mIns = text.match(/(\d+)\s+insertions?\(\+\)/)
|
|
1179
|
+
var mDel = text.match(/(\d+)\s+deletions?\(-\)/)
|
|
1180
|
+
var ins = mIns ? (parseInt(mIns[1], 10) || 0) : 0
|
|
1181
|
+
var del = mDel ? (parseInt(mDel[1], 10) || 0) : 0
|
|
1182
|
+
if (ins || del) return ins + del
|
|
1183
|
+
var total = 0
|
|
1184
|
+
var re = /\|\s*(\d+)\s/g
|
|
1185
|
+
var mm
|
|
1186
|
+
while ((mm = re.exec(text)) !== null) total += parseInt(mm[1], 10) || 0
|
|
1187
|
+
return total
|
|
1188
|
+
}
|
|
1189
|
+
function reviewResumeCount(t) {
|
|
1190
|
+
if (!t || !Array.isArray(t.runs)) return 0
|
|
1191
|
+
var n = 0
|
|
1192
|
+
for (var i = 0; i < t.runs.length; i++) { var r = t.runs[i]; if (r && r.resume === true) n++ }
|
|
1193
|
+
return n
|
|
1194
|
+
}
|
|
1195
|
+
// 返回 { [taskId]: { score, reasons[] } }(含 score=0 的空 reasons 条目,调用方按需透出)。
|
|
1196
|
+
// opts.qualityAlertActive = 质量异动告警黄条激活态(读 qualityChangeHints 输出非空),软信号仅此时计入。
|
|
1197
|
+
export function computeReviewHints(tasks, opts) {
|
|
1198
|
+
var list = Array.isArray(tasks) ? tasks : []
|
|
1199
|
+
var alertActive = !!(opts && opts.qualityAlertActive)
|
|
1200
|
+
var out = {}
|
|
1201
|
+
for (var i = 0; i < list.length; i++) {
|
|
1202
|
+
var t = list[i]
|
|
1203
|
+
if (!t || !t.id) continue
|
|
1204
|
+
var reasons = []
|
|
1205
|
+
if (reviewWasRejected(t)) reasons.push('曾被 Verifier 驳回(验收判 reject)')
|
|
1206
|
+
if (reviewHasArbitration(t)) reasons.push('执行中上报过歧义(待主窗口裁决)')
|
|
1207
|
+
if (reviewTouchesCore(t.touches)) reasons.push('touches 触碰核心文件(dispatch/rpc/core)')
|
|
1208
|
+
if ((t.pipeline || 'full') === 'full' && !(t.acceptance && String(t.acceptance).trim())) reasons.push('全流程任务缺硬性验收脚本(acceptance)')
|
|
1209
|
+
if (alertActive) {
|
|
1210
|
+
if (reviewDiffLines(t) > REVIEW_DIFF_LINES) reasons.push('改动规模大(diff 超 ' + REVIEW_DIFF_LINES + ' 行)')
|
|
1211
|
+
if (reviewResumeCount(t) >= REVIEW_RESUME_MIN) reasons.push('续跑 ≥' + REVIEW_RESUME_MIN + ' 次(冷复活重试偏多)')
|
|
1212
|
+
}
|
|
1213
|
+
out[t.id] = { score: reasons.length, reasons: reasons }
|
|
1214
|
+
}
|
|
1215
|
+
return out
|
|
1216
|
+
}
|
package/lib/dispatch.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { statSync } from 'node:fs'
|
|
|
7
7
|
import * as core from './core.mjs'
|
|
8
8
|
import { readRunUsage, findRunLog, readLogBytes, readLogFrames } from './usage.mjs'
|
|
9
9
|
import { splitRuleOf, pushRejectLesson } from './policy.mjs'
|
|
10
|
-
const { ah, cfg, claimApply, resolveApply, verifyApply, parseSections, outputText, pickDispatch, isOrphan, buildWorkerPrompt, buildVerifierPrompt, buildContextPackSection, parseContextFileEntry, parentKickOnDispatch, LESSON_RECALL_HINT, buildHookPrompt, applyHookSettle, hookOn, hookSetState, gsb, pushRejection, VERIFIER_PERSONA, VERIFIER_TOOL_FILTER } = core
|
|
10
|
+
const { ah, cfg, claimApply, resolveApply, verifyApply, lateVerdictApply, parseSections, outputText, pickDispatch, isOrphan, buildWorkerPrompt, buildVerifierPrompt, buildContextPackSection, parseContextFileEntry, parentKickOnDispatch, LESSON_RECALL_HINT, buildHookPrompt, applyHookSettle, hookOn, hookSetState, gsb, pushRejection, VERIFIER_PERSONA, VERIFIER_TOOL_FILTER } = core
|
|
11
11
|
|
|
12
12
|
export function createDispatch(ctx, state, deps) {
|
|
13
13
|
// 宿主 fs 句柄与「会话工作区解析根」随预研注入瘦身退役(task-muvjs392):派发侧不再读盘——
|
|
@@ -22,6 +22,7 @@ export function createDispatch(ctx, state, deps) {
|
|
|
22
22
|
// Team 提示词组装是同步函数,只能读缓存,不能读盘。
|
|
23
23
|
var epicSplitOn = deps.epicSplitOn
|
|
24
24
|
var pushSysNote = deps.pushSysNote, maybeNotify = deps.maybeNotify, notifyTaskDone = deps.notifyTaskDone
|
|
25
|
+
var notifyLateReject = (typeof deps.notifyLateReject === 'function') ? deps.notifyLateReject : null // 迟到驳回 err 级通知(未接线 → 静默跳过)
|
|
25
26
|
// 派发即回执:spawn 成功后入 45s 聚合队列的「🚀 已派发」区(老 host 未注入 → 静默跳过)
|
|
26
27
|
var notifyDispatched = deps.notifyDispatched
|
|
27
28
|
// 共享状态别名(本体由 index.mjs apply 统一构建并逐模块注入)
|
|
@@ -805,7 +806,22 @@ export function createDispatch(ctx, state, deps) {
|
|
|
805
806
|
doneOn = cfg(d).notifyDone !== false
|
|
806
807
|
var t = d.tasks.find(function (x) { return x.id === rec.taskId })
|
|
807
808
|
if (!t) return null
|
|
808
|
-
if (t.
|
|
809
|
+
if (t.escalation) return { task: t, already: true }
|
|
810
|
+
if (t.status !== 'verifying') {
|
|
811
|
+
// 迟到落账(验收时序三洞 ③):verifier 结论落到已落定卡(主窗口抢批/归档早于本 run 结算)不再静默丢弃。
|
|
812
|
+
// 但「工具通道已处理」= board_verdict 已由本 run 自己落账(t.verification.by === rec.id)→ 保持 already。
|
|
813
|
+
if (t.status === 'resolved' || t.status === 'archived' || t.status === 'cancelled') {
|
|
814
|
+
if (t.verification && String(t.verification.by) === String(rec.id)) return { task: t, already: true }
|
|
815
|
+
var ltrim = (output || '').trim()
|
|
816
|
+
var lvm = ltrim.match(/^[ \t>*#\-\s]*(APPROVED|REJECTED)\b/im)
|
|
817
|
+
if (failed || !lvm) return { task: t, already: true } // 无有效结论,不落 late
|
|
818
|
+
var lapproved = lvm[1].toUpperCase() === 'APPROVED'
|
|
819
|
+
var lsecs = parseSections(ltrim)
|
|
820
|
+
var lr = lateVerdictApply(t, lapproved ? 'approved' : 'rejected', lsecs.verifySummary || ltrim.slice(0, 600), lsecs.checks || '', lsecs.userTest, new Date().toISOString(), String(rec.id), cfg(d).verifyUserGuide)
|
|
821
|
+
return { task: t, late: true, lateRejected: lr.lateRejected && lr.wasApproved }
|
|
822
|
+
}
|
|
823
|
+
return { task: t, already: true }
|
|
824
|
+
}
|
|
809
825
|
var trimmed = (output || '').trim()
|
|
810
826
|
var vm = trimmed.match(/^[ \t>*#\-\s]*(APPROVED|REJECTED)\b/im)
|
|
811
827
|
if (failed || !vm) {
|
|
@@ -841,6 +857,14 @@ export function createDispatch(ctx, state, deps) {
|
|
|
841
857
|
if (!result) return
|
|
842
858
|
// already=true:工具通道(board_verdict)已推进状态并已发回执,settle 只负责 dispose,不再重复通知
|
|
843
859
|
if (result.already) return
|
|
860
|
+
// 迟到驳回(验收时序三洞 ③):verdict=rejected 且卡已 approved → err 级通知 + healthHint
|
|
861
|
+
if (result.late) {
|
|
862
|
+
if (result.lateRejected) {
|
|
863
|
+
if (notifyLateReject) notifyLateReject(sid, result.task)
|
|
864
|
+
poolHealthFor(sid).lateRejectNote = { taskId: result.task.id, at: Date.now() }
|
|
865
|
+
}
|
|
866
|
+
return
|
|
867
|
+
}
|
|
844
868
|
if (result.escalated) maybeNotify(sid, result.task)
|
|
845
869
|
if (doneOn && result.task && result.task.status === 'resolved') notifyTaskDone(sid, result.task, 'resolved')
|
|
846
870
|
if (doneOn && result.task && result.task.status === 'blocked') notifyTaskDone(sid, result.task, 'blocked')
|
package/lib/health.mjs
CHANGED
|
@@ -194,6 +194,28 @@ export function computeRuntimeHealthHints(board, rt, opts) {
|
|
|
194
194
|
var bornAt = typeof rt.bornAt === 'number' ? rt.bornAt : now
|
|
195
195
|
var tasks = board.tasks
|
|
196
196
|
|
|
197
|
+
// --- ④ 看板数据文件腐坏/抢救可见化(task-mv0bl9vg ②③)---
|
|
198
|
+
// store.recoverCorrupt 隔离/抢救时写 rt.corruptNote({ at, salvage, kept, file }),这里现算透出
|
|
199
|
+
// err 级 hint(客户端红色档,区别于 warn/info)。数据安全红线信号:sticky 不消费、不 TTL——
|
|
200
|
+
// 只要板还处于腐坏态就持续亮(poolHealth 纯内存,host 重启自然清)。level='err' 是通道新档,
|
|
201
|
+
// 客户端按非 warn/info 渲染红色;老客户端缺省降级为 info 色,信号仍可见。
|
|
202
|
+
var cn = rt.corruptNote
|
|
203
|
+
if (cn && typeof cn === 'object') {
|
|
204
|
+
var cnText = cn.salvage
|
|
205
|
+
? '🔴 看板数据文件腐坏,已自动抢救保留 ' + (cn.kept || 0) + ' 张卡(丢弃撕裂残片),原始文件留档 ' + (cn.file || '') + ',可联系恢复'
|
|
206
|
+
: '🔴 看板数据文件腐坏已隔离,历史在 ' + (cn.file || '') + ',可联系恢复'
|
|
207
|
+
out.hints.push({ level: 'err', text: cnText })
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// --- ⑤ 迟到驳回可见化(验收时序三洞 ③,用户 n-mv0ebretgv1o)---
|
|
211
|
+
// 主窗口 force 抢批后 Verifier 迟到驳回:err 级 hint(sticky 不 TTL——host 重启才清),
|
|
212
|
+
// 与 corruptNote 同款「进行中的事故优先」语义。rt.lateRejectNote 由 board_verdict/settleVerifier
|
|
213
|
+
// 迟到落账路径写入(dispatch.mjs poolHealthFor / rpc.mjs state.poolHealth)。
|
|
214
|
+
var lr = rt.lateRejectNote
|
|
215
|
+
if (lr && typeof lr === 'object' && lr.taskId) {
|
|
216
|
+
out.hints.push({ level: 'err', text: '迟到驳回:' + String(lr.taskId) + ' 你批早了但 Verifier 验出红的' })
|
|
217
|
+
}
|
|
218
|
+
|
|
197
219
|
if (alive) {
|
|
198
220
|
// --- ① 派发循环心跳:有可派卡 + 池有空位 + 上次成功轮 >5min ---
|
|
199
221
|
// poolLastOkAt 缺失时退到 bornAt(首次见到本板的时刻):host 重启后心跳全断也能在 5min 宽限后亮条,
|
package/lib/notify.mjs
CHANGED
|
@@ -183,5 +183,27 @@ export function createNotify(ctx, state, deps) {
|
|
|
183
183
|
} else deliver().catch(function (e) { console.error('[task-board] receipt flush failed:', String(e)) })
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
// ===== 看板数据文件腐坏/抢救通知(task-mv0bl9vg ②):P0 数据安全,直投 owner,不走回执聚合 =====
|
|
187
|
+
// store.recoverCorrupt 隔离/抢救时调用(经 storeDeps.notifyBoardCorrupt 晚绑定注入)。
|
|
188
|
+
// 与歧义裁决通知同款:任何模式都投、不读回执开关、直接 followup——数据可能丢了必须立刻提醒,
|
|
189
|
+
// 不能等 45s 聚合窗口、更不能被「回执已关」吞掉(回执开关只闸派发/完成两条回执)。
|
|
190
|
+
function notifyBoardCorrupt(sid, text) {
|
|
191
|
+
var root = rootForSession(sid)
|
|
192
|
+
if (!root) return
|
|
193
|
+
try { root.followup(makeMsg('🚨 [任务看板] ' + text + '\n\n历史看板数据未丢,可从留档文件人工恢复;如需协助请告知。', 'notice', '看板数据文件腐坏')) } catch (e) { console.error('[task-board] board corrupt notify failed:', String(e)) }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ===== 迟到驳回通知(验收时序三洞 ③,用户 n-mv0ebretgv1o):err 级,回执直投 owner,不走开关/聚合 =====
|
|
197
|
+
// 主窗口抢批(force approve)后 Verifier 迟到驳回——「你批早了但验收是红的」是 P1 信号,必须立刻直达
|
|
198
|
+
// owner:不进 45s 回执聚合、不被 notifyDone 开关吞掉(与 notifyBoardCorrupt 同一纪律)。
|
|
199
|
+
function notifyLateReject(sid, t) {
|
|
200
|
+
var root = rootForSession(sid)
|
|
201
|
+
if (!root) return
|
|
202
|
+
var lv = (t && t.lateVerdict) || {}
|
|
203
|
+
try {
|
|
204
|
+
root.followup(makeMsg('🚨 [任务看板] 迟到驳回:' + (t.id || '') + ' 你批早了但 Verifier 验出红的\n\n任务: ' + String(t.title || '') + ' (' + (t.id || '') + ')\n\nVerifier 结论(迟到): ' + String(lv.summary || '(无)') + '\n核对项: ' + String(lv.checks || '(无)') + '\n\n详情页已展示「迟到 Verifier 结论」块;如需据此返工,请把任务驳回重投或人工处置。', 'notice', '迟到驳回: ' + (t.id || '')))
|
|
205
|
+
} catch (e) { console.error('[task-board] late reject notify failed:', String(e)) }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { maybeNotify: maybeNotify, notifyTaskDone: notifyTaskDone, notifyDispatched: notifyDispatched, pushSysNote: pushSysNote, notifyBoardCorrupt: notifyBoardCorrupt, notifyLateReject: notifyLateReject }
|
|
187
209
|
}
|