dsh-agent-board 1.7.8 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -8
- package/index.mjs +1 -0
- package/lib/cleanup.mjs +256 -0
- package/lib/client/dashboard.js +139 -11
- package/lib/client/kernel.js +32 -7
- package/lib/client/task-detail.js +13 -7
- package/lib/client.js +184 -25
- package/lib/core.mjs +24 -4
- package/lib/dispatch.mjs +187 -5
- package/lib/rpc.mjs +173 -26
- package/lib/usage.mjs +13 -4
- package/package.json +1 -1
- package/test/cleanup.test.mjs +341 -0
- package/test/conditional-get-tasks.test.mjs +96 -0
- package/test/core.test.mjs +410 -7
- package/test/helpers/mock-ctx.mjs +7 -2
- package/test/integration.test.mjs +153 -0
- package/test/scoreboard-client.test.mjs +15 -9
- package/test/scoreboard.test.mjs +44 -4
- package/test/ui-zoom-client.test.mjs +76 -0
package/lib/client/dashboard.js
CHANGED
|
@@ -471,21 +471,21 @@
|
|
|
471
471
|
// 否则「选了范围数字没变」看起来像 bug。模型分布/Top8/累计才是范围生效的三处。
|
|
472
472
|
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'),
|
|
473
473
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。'),
|
|
474
|
-
// 「→ 模型表现」滚动锚链接(task-muxhtgi2 卡3
|
|
474
|
+
// 「→ 模型表现」滚动锚链接(task-muxhtgi2 卡3):一键滚到下方模型×场景八指标表
|
|
475
475
|
// (与详情页 goArbitration 同一 scrollIntoView 原语;目标区空态也挂锚 id,链接始终可达)
|
|
476
476
|
React.createElement('div', { style: { fontSize: 9, marginTop: 4 } },
|
|
477
|
-
React.createElement('span', { onClick: function () { var el = document.getElementById('tskb-model-perf'); if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' }) }, style: { color: C.brand, cursor: 'pointer' }, title: '滚动到下方「模型表现」区:模型 × 场景(角色 ×
|
|
477
|
+
React.createElement('span', { onClick: function () { var el = document.getElementById('tskb-model-perf'); if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' }) }, style: { color: C.brand, cursor: 'pointer' }, title: '滚动到下方「模型表现」区:模型 × 场景(角色 × 规模段)八指标对比表' }, '→ 模型表现')))
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
// ===== 模型表现区(记分卡卡3,task-muxhtgi2
|
|
480
|
+
// ===== 模型表现区(记分卡卡3,task-muxhtgi2):八指标表格 × 场景筛选器(本地过滤不重拉)=====
|
|
481
481
|
// 数据源:state.usageSummary.scoreboard——卡2 host 聚合底座(buildScoreboard)挂进 get-tasks 响应,
|
|
482
482
|
// 与 Token 区同通道透传、同一 range 口径(范围切换重拉后随 notify 同步刷新,本区零自持请求)。
|
|
483
483
|
// 每桶 = 模型 × 角色(worker/verifier)× 规模段(small/medium/large,按任务有效 token 分桶)。
|
|
484
484
|
// 本区三件事全部本地现算:
|
|
485
485
|
// ① 场景筛选器:角色(全部/worker/verifier)+ 规模段(全部/小/中/大)——scoreboard 已在响应里,
|
|
486
486
|
// 筛选只是 buckets 数组 pass,绝不重拉(组件内无 rpc/fetchTasks);
|
|
487
|
-
// ②
|
|
488
|
-
//
|
|
487
|
+
// ② 八指标表格:每 模型×场景桶 一行,八列——有效均值/耗时中位/驳回率/一次通过率/超时率/失败率/
|
|
488
|
+
// 续跑率/缓存命中率;表头列名带 title 口径说明(与卡2 host 注释同源);
|
|
489
489
|
// ③ 样本护栏:insufficient(runs<5)行整行灰显 + title 前缀「样本 <5,仅供参考」——
|
|
490
490
|
// 仅供参考,不参与任何「最优」视觉强调(本区 v1 不做任何高亮排名,从源头规避)。
|
|
491
491
|
// 空态:scoreboard 缺字段 / buckets 空 → 「暂无足够 run 数据」(锚 id 照挂,Token 区链接始终可达);
|
|
@@ -495,13 +495,16 @@
|
|
|
495
495
|
// 率类/耗时格式化:null = 无样本(不假装 0,与 host 同哲学)
|
|
496
496
|
function mpPct(r) { return (r === null || r === undefined || !isFinite(r)) ? '—' : Math.round(r * 100) + '%' }
|
|
497
497
|
function mpDur(ms) { if (ms === null || ms === undefined || !isFinite(ms)) return '—'; var m = Math.round(ms / 60000); if (m < 1) return '<1m'; if (m < 60) return m + 'm'; return (m / 60).toFixed(1) + 'h' }
|
|
498
|
-
//
|
|
498
|
+
// 八列定义:label + title 口径说明 + 取值格式化(口径一句话版与 README 仪表盘段同步维护)
|
|
499
499
|
var MP_COLS = [
|
|
500
500
|
{ k: 'effAvg', label: '有效均值', title: '有效 token 均值 = 桶内 run 有效消耗(输入+输出+缓存写,不含缓存读)的均值;无 usage 留账的 run 不进均值(不伪造 0)', fmt: function (b) { return b.effAvg === null ? '—' : fmtTokens(b.effAvg) } },
|
|
501
501
|
{ k: 'durMedianMs', label: '耗时中位', title: 'run 耗时(endedAt - at)中位数;P90 见行 title;无合法时刻的 run 不进分布', fmt: function (b) { return mpDur(b.durMedianMs) } },
|
|
502
502
|
{ k: 'rejectRate', label: '驳回率', title: '驳回率 = 归因到本桶 run 的验收驳回次数 / 桶内 runs(归因 = 驳回前最近 worker run)', fmt: function (b) { return mpPct(b.rejectRate) } },
|
|
503
503
|
{ k: 'firstPassRate', label: '一次通过率', title: '一次通过率 = 桶内 resolved 任务中 rejectCount=0(零驳回)的占比;桶内无 resolved 任务 → —', fmt: function (b) { return mpPct(b.firstPassRate) } },
|
|
504
504
|
{ k: 'timeoutRate', label: '超时率', title: '超时率 = outcome 超时落定(timeout/error)的 run / 桶内 runs', fmt: function (b) { return mpPct(b.timeoutRate) } },
|
|
505
|
+
// 失败率列(task-mv73ym6i):跑崩的 run 不再隐形——incomplete(猝死/未产出结算)/error/failed
|
|
506
|
+
// 计入;terminated 人工终止不计入(人工动作非模型表现);超时落定走「超时率」列不重复计。
|
|
507
|
+
{ k: 'failRate', label: '失败率', title: '失败率 = 跑崩落定(outcome 为 incomplete/error/failed)的 run / 桶内 runs;terminated 人工终止不计入(人工动作非模型表现);超时落定走「超时率」列,不重复计', fmt: function (b) { return mpPct(b.failRate) } },
|
|
505
508
|
{ k: 'resumeRate', label: '续跑率', title: '续跑率 = resume:true 续跑记录 / 桶内 runs', fmt: function (b) { return mpPct(b.resumeRate) } },
|
|
506
509
|
{ k: 'cacheHitRate', label: '缓存命中率', title: '缓存命中率 = cacheRead / (input + cacheRead);⚠️ 跨 provider 口径可能不一(input 是否含缓存读各家不同),对比前先核口径;无 usage 样本 → —', fmt: function (b) { return mpPct(b.cacheHitRate) } }
|
|
507
510
|
]
|
|
@@ -551,14 +554,14 @@
|
|
|
551
554
|
MP_COLS.map(function (c) { return React.createElement('th', { key: c.k, style: thR, title: c.title }, c.label) }))),
|
|
552
555
|
React.createElement('tbody', null, shown.map(function (b) {
|
|
553
556
|
// 样本护栏:insufficient 行整行灰显 + title 前缀「样本 <5,仅供参考」,不进任何最优强调
|
|
554
|
-
var tip = (b.insufficient ? '样本 <5,仅供参考——' : '') + b.model + ' × ' + b.role + ' × ' + (MP_SIZE_LABEL[b.size] || b.size) + ':runs=' + b.runs + ';有效总额 ' + fmtTokens(b.effSum) + ' tok;耗时 P90 ' + mpDur(b.durP90Ms) + '
|
|
557
|
+
var tip = (b.insufficient ? '样本 <5,仅供参考——' : '') + b.model + ' × ' + b.role + ' × ' + (MP_SIZE_LABEL[b.size] || b.size) + ':runs=' + b.runs + ';有效总额 ' + fmtTokens(b.effSum) + ' tok;耗时 P90 ' + mpDur(b.durP90Ms) + '(八指标口径见表头 title)'
|
|
555
558
|
return React.createElement('tr', { key: b.model + '|' + b.role + '|' + b.size, title: tip, style: { opacity: b.insufficient ? 0.45 : 1 } },
|
|
556
559
|
React.createElement('td', { style: tdL }, b.model),
|
|
557
560
|
React.createElement('td', { style: tdL }, b.role + '·' + (MP_SIZE_LABEL[b.size] || b.size)),
|
|
558
561
|
React.createElement('td', { style: tdR }, String(b.runs)),
|
|
559
562
|
MP_COLS.map(function (c) { return React.createElement('td', { key: c.k, style: tdR }, c.fmt(b)) }))
|
|
560
563
|
})))),
|
|
561
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:每行 = 模型 × 角色 × 规模段桶(规模按任务有效 token:小 <1M / 中 1~10M / 大 >10M);率类分母为桶内 runs,一次通过率分母为桶内 resolved
|
|
564
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:每行 = 模型 × 角色 × 规模段桶(规模按任务有效 token:小 <1M / 中 1~10M / 大 >10M);率类分母为桶内 runs,一次通过率分母为桶内 resolved 任务;失败率 = 跑崩落定(outcome 为 incomplete/error/failed)的 run / 桶内 runs,terminated 人工终止不计入;runs<5 行灰显 = 样本不足仅供参考;随统计范围裁剪(与 Token 区同一 range)'))
|
|
562
565
|
}
|
|
563
566
|
|
|
564
567
|
// ===== 质量趋势区(记分卡卡4,task-muxhtgil):一次通过率趋势 / 卡时长分布 / 超时率走势 / 续跑成功率 =====
|
|
@@ -755,6 +758,73 @@
|
|
|
755
758
|
}))
|
|
756
759
|
}
|
|
757
760
|
|
|
761
|
+
// ===== 存储清理区(已结算会话清理,task-mv2131cz):数据源 host 端 cleanup-preview / cleanup-run =====
|
|
762
|
+
// preview 纯读盘零副作用;run 需 confirm:true 二次确认(删除不可恢复,token 账已入卡不受影响)。
|
|
763
|
+
// 可删判定口径在 host lib/cleanup.mjs:卡龄主闸(卡归档满 cleanupRetentionDays 天)+ 三底闸(落定/不活跃/路径安全)。
|
|
764
|
+
function fmtBytes(n) {
|
|
765
|
+
var v = Number(n) || 0
|
|
766
|
+
if (v >= 1073741824) return (v / 1073741824).toFixed(1) + ' GB'
|
|
767
|
+
if (v >= 1048576) return (v / 1048576).toFixed(v >= 10485760 ? 0 : 1) + ' MB'
|
|
768
|
+
if (v >= 1024) return (v / 1024).toFixed(v >= 10240 ? 0 : 1) + ' KB'
|
|
769
|
+
return String(v) + ' B'
|
|
770
|
+
}
|
|
771
|
+
function StorageCleanup() {
|
|
772
|
+
var _R = React; var useState = _R.useState
|
|
773
|
+
var _p = useState(null), prev = _p[0], setPrev = _p[1]
|
|
774
|
+
var _o = useState(false), open = _o[0], setOpen = _o[1]
|
|
775
|
+
var _r = useState(null), report = _r[0], setReport = _r[1]
|
|
776
|
+
var _m = useState(''), msg = _m[0], setMsg = _m[1]
|
|
777
|
+
var _b = useState(false), busy = _b[0], setBusy = _b[1]
|
|
778
|
+
var _c = useState(false), confirmOn = _c[0], setConfirmOn = _c[1]
|
|
779
|
+
function preview() {
|
|
780
|
+
setBusy(true); setMsg('')
|
|
781
|
+
rpc('cleanup-preview').then(function (r) {
|
|
782
|
+
if (r && r.ok) { setPrev(r); setReport(null); setConfirmOn(false) } else { setMsg((r && r.error) || '预览失败') }
|
|
783
|
+
setBusy(false)
|
|
784
|
+
}).catch(function (e) { setMsg('预览失败: ' + (e && e.message || e)); setBusy(false) })
|
|
785
|
+
}
|
|
786
|
+
function runClean() {
|
|
787
|
+
setBusy(true); setMsg('')
|
|
788
|
+
rpc('cleanup-run', { confirm: true }).then(function (r) {
|
|
789
|
+
setReport(r); setPrev(null); setConfirmOn(false); setBusy(false)
|
|
790
|
+
if (r && r.ok) { if (typeof fetchTasks === 'function') fetchTasks() } else { setMsg((r && r.error) || '清理失败') }
|
|
791
|
+
}).catch(function (e) { setMsg('清理失败: ' + (e && e.message || e)); setBusy(false) })
|
|
792
|
+
}
|
|
793
|
+
var box = { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 }
|
|
794
|
+
var head = React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('trash-2', 11), '存储清理')
|
|
795
|
+
var btn = { fontSize: 10, padding: '2px 10px', border: '1px solid ' + C.border, borderRadius: 5, background: C.card, color: C.text, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }
|
|
796
|
+
var dangerBtn = { fontSize: 10, padding: '2px 10px', border: '1px solid ' + C.err, borderRadius: 5, background: C.card, color: C.err, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }
|
|
797
|
+
function skippedLine(s, i) {
|
|
798
|
+
return React.createElement('div', { key: 'sk' + i, style: { fontSize: 9, color: C.text2, marginBottom: 2 } },
|
|
799
|
+
React.createElement('span', { style: { color: C.warn } }, '⏭ '), String(s.id || '').slice(0, 14) + '…:' + (s.text || s.reason || ''))
|
|
800
|
+
}
|
|
801
|
+
return React.createElement('div', { style: box },
|
|
802
|
+
head,
|
|
803
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
804
|
+
React.createElement('button', { onClick: preview, disabled: busy, style: btn, title: '扫描本看板归档超过保留天数的卡片的会话,估算可释放空间(只读不删)' }, ic('refresh-cw', 11), busy ? '扫描中…' : '预览可清理会话'),
|
|
805
|
+
prev ? React.createElement('span', { style: { fontSize: 10, color: C.text } }, prev.total > 0 ? ('可删 ' + prev.total + ' 个会话 · 约 ' + fmtBytes(prev.totalBytes)) : '无可删会话') : null,
|
|
806
|
+
prev && prev.total > 0 ? React.createElement('button', { onClick: function () { setOpen(!open); setConfirmOn(false) }, style: btn }, ic(open ? 'chevron-up' : 'chevron-down', 10), open ? '收起明细' : '展开明细') : null,
|
|
807
|
+
prev && prev.total > 0 ? (confirmOn ? React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } },
|
|
808
|
+
React.createElement('button', { onClick: runClean, disabled: busy, style: dangerBtn, title: '确认删除不可恢复;token 账已入卡不受影响' }, ic('check', 11), '确认清理(不可恢复)'),
|
|
809
|
+
React.createElement('button', { onClick: function () { setConfirmOn(false) }, style: btn }, '取消'))
|
|
810
|
+
: React.createElement('button', { onClick: function () { setConfirmOn(true) }, disabled: busy, style: btn, title: '删除不可恢复;token 账已入卡不受影响' }, ic('trash-2', 11), '清理')) : null),
|
|
811
|
+
msg ? React.createElement('div', { style: { fontSize: 9, color: C.err, marginTop: 4 } }, msg) : null,
|
|
812
|
+
report && report.ok ? React.createElement('div', { style: { fontSize: 10, color: C.text, marginTop: 5 } },
|
|
813
|
+
'✅ 已释放约 ' + fmtBytes(report.freedBytes) + '(删除 ' + report.deleted + ' 个会话' + ((report.skipped && report.skipped.length) ? ' · 跳过 ' + report.skipped.length + ' 个' : '') + ')') : null,
|
|
814
|
+
report && report.ok && report.skipped && report.skipped.length ? React.createElement('div', { style: { marginTop: 4 } }, report.skipped.map(skippedLine)) : null,
|
|
815
|
+
open && prev && prev.total > 0 ? React.createElement('div', { style: { marginTop: 6, borderTop: '1px solid ' + C.nested, paddingTop: 5 } },
|
|
816
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 4 } }, '可删会话(会话目录 + projcache 缓存一并删除)'),
|
|
817
|
+
(prev.byTask || []).map(function (b) {
|
|
818
|
+
return React.createElement('div', { key: b.taskId, style: { marginBottom: 3, fontSize: 9, color: C.text2 } },
|
|
819
|
+
React.createElement('span', { style: { color: C.brand } }, shortId(b.title)), ':' + b.count + ' 个 · 约 ' + fmtBytes(b.bytes))
|
|
820
|
+
}),
|
|
821
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2 } }, '数据来源:本看板 runs[] 里所属卡已归档超过 ' + (state.cleanupRetentionDays || 7) + ' 天、且 outcome 已落定并已不活跃的 Worker/Verifier 会话;token 账早已记入卡片,删除不影响任何统计。')
|
|
822
|
+
) : null,
|
|
823
|
+
prev && prev.total === 0 && prev.skipped && prev.skipped.length ? React.createElement('div', { style: { marginTop: 5 } },
|
|
824
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 3 } }, '无符合条件可删的会话(跳过项)'),
|
|
825
|
+
prev.skipped.slice(0, 8).map(skippedLine)) : null)
|
|
826
|
+
}
|
|
827
|
+
|
|
758
828
|
function Dashboard() {
|
|
759
829
|
var stats = computeStats(state.tasks); var statusOrder = ['pending', 'in-progress', 'verifying', 'resolved', 'blocked', 'archived']; var prioOrder = ['critical', 'high', 'medium', 'low']
|
|
760
830
|
return React.createElement('div', null,
|
|
@@ -762,6 +832,7 @@
|
|
|
762
832
|
React.createElement(GlobalBoards),
|
|
763
833
|
React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' } }, React.createElement(StatCard, { label: '总任务', value: stats.total, color: C.text }), React.createElement(StatCard, { label: '待办', value: stats.byStatus['pending'] || 0, color: C.text2 }), React.createElement(StatCard, { label: '进行中', value: stats.byStatus['in-progress'] || 0, color: C.brand }), React.createElement(StatCard, { label: '验证中', value: stats.byStatus['verifying'] || 0, color: C.warn }), React.createElement(StatCard, { label: '已完成', value: stats.byStatus['resolved'] || 0, color: C.ok }), React.createElement(StatCard, { label: '已归档', value: stats.byStatus['archived'] || 0, color: C.text2 })),
|
|
764
834
|
React.createElement(HealthHints),
|
|
835
|
+
React.createElement(StorageCleanup),
|
|
765
836
|
React.createElement(TokenUsage, { usage: state.usageSummary }),
|
|
766
837
|
// 模型表现区(卡3):Token 区正下方;scoreboard 随 usageSummary 同通道透传(老 host 缺字段 → null → 空态)
|
|
767
838
|
React.createElement(ModelPerf, { sb: (state.usageSummary && state.usageSummary.scoreboard) || null }),
|
|
@@ -825,6 +896,30 @@
|
|
|
825
896
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '分'),
|
|
826
897
|
dirty ? React.createElement('button', { onClick: function () { commit(val) }, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null)
|
|
827
898
|
}
|
|
899
|
+
// 清理保留天数输入(卡龄主闸设置项 cleanupRetentionDays,1~90 天):与 MinCfg 同款受控输入 + 失焦/回车提交,
|
|
900
|
+
// 越界值直接回弹到服务端权威值(不留脏态),成功走 set-board-config → fetchTasks 由服务端权威值纠偏。
|
|
901
|
+
function RetentionCfg(props) {
|
|
902
|
+
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
903
|
+
var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]
|
|
904
|
+
var _b = useState(false), dirty = _b[0], setDirty = _b[1]
|
|
905
|
+
useEffect(function () { setVal(String(props.value)); setDirty(false) }, [props.value])
|
|
906
|
+
function commit(v) {
|
|
907
|
+
var n = parseInt(v, 10)
|
|
908
|
+
if (isNaN(n) || n < 1 || n > 90) { setVal(String(props.value)); setDirty(false); return }
|
|
909
|
+
setVal(String(n)); setDirty(false); rpc('set-board-config', { key: 'cleanupRetentionDays', value: n }).then(fetchTasks).catch(function () {})
|
|
910
|
+
}
|
|
911
|
+
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } },
|
|
912
|
+
props.label,
|
|
913
|
+
React.createElement('input', {
|
|
914
|
+
value: val, title: props.tip || ('1 ~ 90 天,默认 7'),
|
|
915
|
+
onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== String(props.value)) },
|
|
916
|
+
onBlur: function () { if (dirty) commit(val) },
|
|
917
|
+
onKeyDown: function (e) { if (e.key === 'Enter') commit(val) },
|
|
918
|
+
style: { width: 38, padding: '0px 3px', fontSize: 9, textAlign: 'center', border: '1px solid ' + (dirty ? C.brand : C.border2), borderRadius: 2, background: C.card, color: C.text }
|
|
919
|
+
}),
|
|
920
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '天'),
|
|
921
|
+
dirty ? React.createElement('button', { onClick: function () { commit(val) }, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null)
|
|
922
|
+
}
|
|
828
923
|
|
|
829
924
|
function PoolStatus() {
|
|
830
925
|
var ps = state.poolStatus
|
|
@@ -868,6 +963,23 @@
|
|
|
868
963
|
dirty ? React.createElement('button', { onClick: save, title: '应用', style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null)
|
|
869
964
|
}
|
|
870
965
|
|
|
966
|
+
// 界面缩放档位选择(task-mv58e01j,用户反馈「字体整体偏小」):90/100/110/125/150% 五档下拉。
|
|
967
|
+
// 纯客户端偏好——不走 set-board-config(host 不管 UI 偏好):选中即 localStorage.setItem 持久化
|
|
968
|
+
// + state.uiZoom + notify,TopPanel 根容器 zoom 订阅即时生效;非法档位(脏值)不采纳。
|
|
969
|
+
function ZoomCfg() {
|
|
970
|
+
var cur = state.uiZoom || 1
|
|
971
|
+
function pick(v) {
|
|
972
|
+
var n = parseFloat(v)
|
|
973
|
+
if (UI_ZOOM_LEVELS.indexOf(n) < 0) return
|
|
974
|
+
state.uiZoom = n
|
|
975
|
+
try { localStorage.setItem(UI_ZOOM_KEY, String(n)) } catch (_) {}
|
|
976
|
+
notify()
|
|
977
|
+
}
|
|
978
|
+
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, '界面缩放',
|
|
979
|
+
React.createElement('select', { value: String(cur), onChange: function (e) { pick(e.target.value) }, title: '界面缩放:等比放大/缩小面板内字体、图标与间距(选中即时生效,本地记忆)', style: { padding: '0px 4px', fontSize: 9, border: '1px solid ' + C.border, borderRadius: 2, background: C.card, color: C.text } },
|
|
980
|
+
UI_ZOOM_LEVELS.map(function (z) { return React.createElement('option', { key: String(z), value: String(z) }, Math.round(z * 100) + '%' + (z === 1 ? '(默认)' : '')) })))
|
|
981
|
+
}
|
|
982
|
+
|
|
871
983
|
function PoolCfgPopover(props) {
|
|
872
984
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
|
|
873
985
|
var _a = useState(false), open = _a[0], setOpen = _a[1]
|
|
@@ -891,12 +1003,16 @@
|
|
|
891
1003
|
// position:fixed + 视口坐标:面板容器是 overflow:hidden + maxHeight:60vh,
|
|
892
1004
|
// absolute 弹窗在面板内容短时会被裁掉下半截(超时配置行曾被整个裁掉)。
|
|
893
1005
|
// fixed 脱离任何祖先裁剪(面板无 transform/filter,不会形成包含块)。
|
|
894
|
-
|
|
1006
|
+
// 界面缩放补偿(task-mv58e01j):根容器挂了 CSS zoom 后,getBoundingClientRect 返回缩放后的
|
|
1007
|
+
// 可视坐标,而 fixed 弹层在 zoom 子树内 top/right/maxHeight 会再被缩一次 → 各项除以档位还原成
|
|
1008
|
+
// CSS px,弹层仍贴着按钮且不溢出视口底(zoom=1 时与旧行为逐字一致)。
|
|
1009
|
+
var z = state.uiZoom || 1
|
|
1010
|
+
if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos({ top: (r.bottom + 4) / z, right: Math.max(8, window.innerWidth - r.right) / z, maxH: Math.max(140, (window.innerHeight - r.bottom - 16) / z) }) }
|
|
895
1011
|
setOpen(!open)
|
|
896
1012
|
}
|
|
897
1013
|
return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
|
|
898
1014
|
React.createElement('button', { ref: btnRef, onClick: toggle, title: '池配置', style: { fontSize: 12, padding: '3px 8px', border: '1px solid ' + (open ? C.brand : C.border), borderRadius: 6, cursor: 'pointer', background: open ? C.nested : 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('settings', 13)),
|
|
899
|
-
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:
|
|
1015
|
+
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: (pos.maxH || 300) + 'px', overflowY: 'auto' } },
|
|
900
1016
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
|
|
901
1017
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
|
|
902
1018
|
React.createElement(PoolCfg, { label: 'Worker 并发', cfgKey: 'maxWorkers', value: props.maxW }),
|
|
@@ -940,7 +1056,19 @@
|
|
|
940
1056
|
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
|
|
941
1057
|
React.createElement('input', { type: 'checkbox', checked: props.epicSplit !== false, onChange: function (e) { setCfg('epicSplit', e.target.checked) } }),
|
|
942
1058
|
React.createElement('span', null, '🧩 史诗拆分:大任务引导拆为 epic + 子任务')),
|
|
943
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '关掉只停引导:显式 parentId 建子卡与史诗自动收口照常工作')
|
|
1059
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '关掉只停引导:显式 parentId 建子卡与史诗自动收口照常工作'),
|
|
1060
|
+
// 清理保留天数(卡龄主闸 cleanupRetentionDays,默认 7、可配 1-90):回收与归档生命周期对齐——
|
|
1061
|
+
// 卡片归档满该天数后,其会话才进入可清理池。仪表盘「存储清理」区文案读同一服务端权威值。
|
|
1062
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '存储清理'),
|
|
1063
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
1064
|
+
React.createElement(RetentionCfg, { label: '归档保留', value: props.retentionDays, tip: '卡片归档后多少天可被完全回收(1~90 天,默认 7)' }),
|
|
1065
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(归档满该天数后其会话可被清理回收)')),
|
|
1066
|
+
// 界面缩放档位(task-mv58e01j):设置弹层「界面」小节——五档下拉选中即 localStorage
|
|
1067
|
+
// 持久化 + state.uiZoom + notify 即时生效(zoom 挂在 TopPanel 根容器,只影响本面板)。
|
|
1068
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '界面'),
|
|
1069
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
1070
|
+
React.createElement(ZoomCfg, null),
|
|
1071
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(等比缩放面板字体/图标/间距,仅本面板生效并本地记忆)'))) : null)
|
|
944
1072
|
}
|
|
945
1073
|
|
|
946
1074
|
function TeamView() {
|
package/lib/client/kernel.js
CHANGED
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
195
195
|
['rect', { width: 6, height: 6, x: 9, y: 9 }]
|
|
196
196
|
],
|
|
197
|
-
//
|
|
197
|
+
// 取消入口图标(详情页「取消任务」按钮;语义=作废不派发,与删除 trash-2 区分)
|
|
198
198
|
'ban': [
|
|
199
199
|
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
200
200
|
['path', { d: 'm4.9 4.9 14.2 14.2' }]
|
|
@@ -234,7 +234,15 @@
|
|
|
234
234
|
blocked: { title: '阻塞:卡住待处理(等依赖/等主窗口裁决)', empty: '暂无阻塞——任务卡住待裁决时会出现在这里' }
|
|
235
235
|
}
|
|
236
236
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
237
|
-
|
|
237
|
+
// ===== 界面缩放档位(task-mv58e01j,用户 2026-10-12「字体整体偏小」)=====
|
|
238
|
+
// 面板内 ~317 处内联 fontSize px 写死(9/10/11 为主),逐处改不值 → 面板根容器 CSS zoom
|
|
239
|
+
// 等比缩放统一接管(Chromium 支持,字体/图标/间距一并放大;zoom 只作用本面板子树,不碰宿主页面)。
|
|
240
|
+
// 纯客户端偏好(不走 host set-board-config):localStorage 键 dsh-agent-board-ui-zoom 持久化,缺省 1;
|
|
241
|
+
// 脏值/档位外值/读取异常一律回退 1(白名单校验,不写回——下次选中档位时覆盖)。
|
|
242
|
+
var UI_ZOOM_KEY = 'dsh-agent-board-ui-zoom'
|
|
243
|
+
var UI_ZOOM_LEVELS = [0.9, 1, 1.1, 1.25, 1.5] // 90% / 100%(默认)/ 110% / 125% / 150%
|
|
244
|
+
function readUiZoom() { try { var v = parseFloat(localStorage.getItem(UI_ZOOM_KEY)); return UI_ZOOM_LEVELS.indexOf(v) >= 0 ? v : 1 } catch (_) { return 1 } }
|
|
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, cleanupRetentionDays: 7, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, verifyUserGuide: true, uiZoom: readUiZoom(), 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: '' }
|
|
238
246
|
|
|
239
247
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
240
248
|
try {
|
|
@@ -315,7 +323,12 @@
|
|
|
315
323
|
// 空范围(两端皆空)不传 field:host 收到 undefined 即全量聚合,与老 host 的调用形态完全一致;
|
|
316
324
|
// 绝不用空字符串占位(host 侧 `from`/`to` 空串虽也判为无范围,但少传一个字段就少一处口径分叉)。
|
|
317
325
|
var rgSend = activeRange()
|
|
318
|
-
var rpcArgs = (rgSend.from || rgSend.to) ? { range: rgSend } :
|
|
326
|
+
var rpcArgs = (rgSend.from || rgSend.to) ? { range: rgSend } : {}
|
|
327
|
+
// 条件请求瘦身(task-mv492for):带上轮 tasksHash 作 ifHash,host 比对一致则返回 unchanged:true
|
|
328
|
+
// 轻量响应(不含 tasks 全量,轮询载荷 1.34MB→KB 级);首轮/会话切换后 state.tasksHash 为空不带
|
|
329
|
+
// ifHash → 全量返回(与不带 ifHash 的老客户端同路径)。空范围时 rpcArgs 由 undefined 改 {} 是同义
|
|
330
|
+
// 形态(rpc 助手 args||{} 归一;host 只读 range/sessionId/ifHash,多传空对象无害)。
|
|
331
|
+
if (state.tasksHash) rpcArgs.ifHash = state.tasksHash
|
|
319
332
|
// 范围守卫:范围已在本轮响应回来前被改掉 → 这份响应属于旧范围,丢弃 usageSummary 赋值
|
|
320
333
|
// (否则旧范围的旧聚合会覆盖新范围的新聚合,用户看到「切了范围数字没变」直到下一次轮询)。
|
|
321
334
|
// 注意 tasksHash 短路只管 tasks:usageSummary 照常赋值(现状已对,保持)。
|
|
@@ -330,7 +343,11 @@
|
|
|
330
343
|
// (hash 只管任务列表渲染;这些字段随下次任意 notify 生效,不为它们单独渲染)。
|
|
331
344
|
// 老 host 不返回 tasksHash → 恒视为变化,行为与旧版完全一致。
|
|
332
345
|
var newHash = (d && d.tasksHash) || ''
|
|
333
|
-
|
|
346
|
+
// 条件请求瘦身(task-mv492for):host 判 ifHash 一致时返回 unchanged:true(响应不含 tasks 全量)。
|
|
347
|
+
// state.tasks 必须原样保留——unchanged 优先于 hash 比对:缺 tasks 的响应若误入变化分支,
|
|
348
|
+
// `(d && d.tasks) || []` 会把列表清空。轻量字段不受影响,照常走下方既有赋值链。
|
|
349
|
+
var unchanged = !!(d && d.unchanged)
|
|
350
|
+
var tasksChanged = !unchanged && (!newHash || newHash !== state.tasksHash)
|
|
334
351
|
// 设置开关的权威纠偏(task-muw5uudk):开关不走 tasksHash,hash 不变分支里照常赋值也没有 notify
|
|
335
352
|
// → 勾选框必须等下一次任意 notify 才翻面(安静板卡数秒)。这里在赋值前快照、赋值后比对,
|
|
336
353
|
// 有变化就补一次 notify:乐观更新(PoolCfgPopover)已让点击瞬时翻面,本兜底是服务端权威值纠偏
|
|
@@ -385,6 +402,9 @@
|
|
|
385
402
|
state.epicSplit = cfgKnobOf(d, 'epicSplit')
|
|
386
403
|
// 自测指南开关(设置区「验收」):同上——缺字段=开(详情块/报告清单段照常),只有显式 false 才关
|
|
387
404
|
state.verifyUserGuide = cfgKnobOf(d, 'verifyUserGuide')
|
|
405
|
+
// 清理保留天数(设置区「存储清理」,卡龄主闸):数值键,host 已 cfg 归一(缺字段/脏值=7、钳 1-90),
|
|
406
|
+
// 客户端读服务端权威值(与 softTimeoutMin/hardTimeoutMin 同款直接赋值,不做布尔口径纠偏)。
|
|
407
|
+
state.cleanupRetentionDays = (d && d.cleanupRetentionDays) || 7
|
|
388
408
|
// 开关有变 / Token 区聚合有变 → 补一次 notify(tasksChanged 分支已在上面 notify 过,
|
|
389
409
|
// 这里只管 hash 不变时被跳过的那两次:开关乐观更新纠偏 + 统计范围切换后的新聚合)
|
|
390
410
|
var cfgDelta = !tasksChanged && cfgKnobsChanged(cfgKnobs, state)
|
|
@@ -602,8 +622,10 @@
|
|
|
602
622
|
var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
|
|
603
623
|
var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
|
|
604
624
|
var _vug = useState(state.verifyUserGuide), vugOn = _vug[0], setVugOn = _vug[1] // 自测指南开关勾选态(设置区「验收」)
|
|
625
|
+
var _crd = useState(state.cleanupRetentionDays), crd = _crd[0], setCrd = _crd[1] // 清理保留天数(设置区「存储清理」)
|
|
626
|
+
var _uzm = useState(state.uiZoom), uiZoom = _uzm[0], setUiZoom = _uzm[1] // 界面缩放档位(设置区「界面」,zoom 挂面板根容器)
|
|
605
627
|
var _usg = useState(state.usageSummary), setUsage = _usg[1] // Token 区聚合订阅(task-mv10lvud 根修):usageSummary 换新对象即触发渲染
|
|
606
|
-
useEffect(function () { function update() { setOpen(state.open); setTasksState(state.tasks); setModeState(state.boardMode); setDetailId(state.detailId); setDragOver(state.dragOver); setDispatchInfo(state.dispatchInfo); setViewState(state.view); setLayL(state.layoutLeft); setLayR(state.layoutRight); setMinW(state.minWorkers); setMaxW(state.maxWorkers); setMinV(state.minVerifiers); setMaxV(state.maxVerifiers); setWorkerModel(state.workerModel); setVerifierModel(state.verifierModel); setTeamMode(state.teamMode); setWorkModeState(state.workMode); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin); setFbEnabled(state.feedbackEnabled); setNdOn(state.notifyDispatch); setNnOn(state.notifyDone); setEsOn(state.epicSplit); setVugOn(state.verifyUserGuide); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash); setTasksErr(state.tasksErr); setUsage(state.usageSummary) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
628
|
+
useEffect(function () { function update() { setOpen(state.open); setTasksState(state.tasks); setModeState(state.boardMode); setDetailId(state.detailId); setDragOver(state.dragOver); setDispatchInfo(state.dispatchInfo); setViewState(state.view); setLayL(state.layoutLeft); setLayR(state.layoutRight); setMinW(state.minWorkers); setMaxW(state.maxWorkers); setMinV(state.minVerifiers); setMaxV(state.maxVerifiers); setWorkerModel(state.workerModel); setVerifierModel(state.verifierModel); setTeamMode(state.teamMode); setWorkModeState(state.workMode); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin); setFbEnabled(state.feedbackEnabled); setNdOn(state.notifyDispatch); setNnOn(state.notifyDone); setEsOn(state.epicSplit); setVugOn(state.verifyUserGuide); setCrd(state.cleanupRetentionDays); setUiZoom(state.uiZoom); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash); setTasksErr(state.tasksErr); setUsage(state.usageSummary) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
607
629
|
if (!open) return null
|
|
608
630
|
if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
|
|
609
631
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -625,11 +647,14 @@
|
|
|
625
647
|
})
|
|
626
648
|
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))
|
|
627
649
|
}
|
|
628
|
-
|
|
650
|
+
// 面板根容器(顶层 div):zoom 挂这里——等比缩放整个面板子树(字体/图标/间距),只影响本面板
|
|
651
|
+
// 不影响宿主页面;uiZoom 为订阅值(设置弹层选档 → state.uiZoom + notify → 本组件重渲染即时生效)。
|
|
652
|
+
// React 的 zoom 属无单位数值型 style(isUnitlessNumber 含 zoom),直接传数值不会拼 px。
|
|
653
|
+
return React.createElement('div', { style: { position: 'fixed', top: 44, left: (layL + 8) + 'px', right: (layR + 8) + 'px', maxHeight: '60vh', zIndex: 900, zoom: uiZoom, 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' } },
|
|
629
654
|
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 } },
|
|
630
655
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } }, React.createElement('span', { style: { fontWeight: 600, fontSize: 13, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 5 } }, ic('clipboard-list', 15), '智能看板'), React.createElement(ViewTab, null), React.createElement(PoolStatus, null)),
|
|
631
656
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
632
|
-
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT, feedbackEnabled: fbEnabled, notifyDispatch: ndOn, notifyDone: nnOn, epicSplit: esOn, verifyUserGuide: vugOn }),
|
|
657
|
+
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT, feedbackEnabled: fbEnabled, notifyDispatch: ndOn, notifyDone: nnOn, epicSplit: esOn, verifyUserGuide: vugOn, retentionDays: crd }),
|
|
633
658
|
dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
|
|
634
659
|
React.createElement('button', { onClick: function () { state.createOpen = true; notify() }, title: '新建任务(可存为草稿)', style: { fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border, borderRadius: 6, cursor: 'pointer', background: 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('plus', 11), '新建任务'),
|
|
635
660
|
createFlash ? React.createElement('span', { style: { fontSize: 10, color: C.ok } }, createFlash) : null,
|
|
@@ -340,7 +340,10 @@
|
|
|
340
340
|
p.then(function (r) { if (r && r.ok === false) setActionMsg('⚠️ 流转失败:' + (r.error || '未知错误')); else setActionMsg('✅ 已流转到「' + (statusLabels[to] || to) + '」'); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) })
|
|
341
341
|
}
|
|
342
342
|
return React.createElement('div', { style: { padding: '4px 2px' } },
|
|
343
|
-
|
|
343
|
+
// 头部行(task-mv493xjs 巡检次生观察②):左「← 返回看板」+ 右上显性 ✕ 关闭钮(原来只有文字链不够显性)
|
|
344
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 } },
|
|
345
|
+
React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer' } }, '← 返回看板'),
|
|
346
|
+
React.createElement('button', { onClick: function () { state.detailId = null; notify() }, title: '关闭详情面板(返回看板)', style: { fontSize: 11, padding: '1px 6px', border: '1px solid ' + C.border, borderRadius: 3, background: 'transparent', color: C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', lineHeight: 1.4 } }, ic('x', 11))),
|
|
344
347
|
React.createElement(ReviewSummary, { task: task }),
|
|
345
348
|
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),
|
|
346
349
|
React.createElement(TagsEditor, { key: task.id, task: task }),
|
|
@@ -461,12 +464,15 @@
|
|
|
461
464
|
task.status === 'verifying' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'approved' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('check-circle', 10), '通过') : null,
|
|
462
465
|
task.status === 'verifying' ? React.createElement('button', { onClick: function () { var r = window.prompt('驳回原因:'); doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'rejected', comment: r || '' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.err, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('x-circle', 10), '驳回') : null,
|
|
463
466
|
task.status === 'resolved' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('archive-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.text2, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('archive', 10), '归档') : null),
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
canDelete(task) ? React.createElement('
|
|
467
|
+
// ===== 破坏性操作区(task-mv493xjs 消歧,巡检实证 n-mv2turadjv63 误触)=====
|
|
468
|
+
// 视觉分层:与上方表单按钮组(保存/发布/领取/派发/通过/驳回/归档)拉开——独立一行 +
|
|
469
|
+
// 顶部虚线分隔 + 「危险操作」前缀标签。「取消」改名「取消任务」:按表单惯例会被误读为
|
|
470
|
+
// 「关面板」,实际是 task_cancel 破坏性流转(confirm 确认在 kernel.cancelTask 已存在)。
|
|
471
|
+
// 取消(仅草稿/待办/阻塞)语义=作废留档(可恢复);删除=真删不可恢复,被 host 门禁拒绝时原因回显 actionMsg。
|
|
472
|
+
(canCancel(task) || canDelete(task)) ? React.createElement('div', { role: 'group', 'aria-label': '危险操作', style: { display: 'flex', gap: 4, flexWrap: 'wrap', alignItems: 'center', marginTop: 8, paddingTop: 6, borderTop: '1px dashed ' + C.err } },
|
|
473
|
+
React.createElement('span', { style: { fontSize: 9, color: C.err, fontWeight: 700, marginRight: 2, letterSpacing: 1 } }, '危险操作'),
|
|
474
|
+
canCancel(task) ? React.createElement('button', { onClick: function () { cancelTask(task.id, task.title, setActionMsg) }, title: '取消任务(取消后不再派发,可在归档恢复待办)', style: { fontSize: 10, padding: '3px 8px', border: '1px solid ' + C.warn, borderRadius: 3, background: 'transparent', color: C.warn, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('ban', 10), '取消任务') : null,
|
|
475
|
+
canDelete(task) ? React.createElement('button', { onClick: function () { deleteTask(task.id, task.title, setActionMsg) }, title: '删除任务(不可恢复;执行中请先终止,已落定请用归档)', style: { fontSize: 10, padding: '3px 8px', border: '1px solid ' + C.err, borderRadius: 3, background: 'transparent', color: C.err, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('trash-2', 10), '删除') : null) : null,
|
|
470
476
|
(function () {
|
|
471
477
|
// 历史会话:无论任务处于哪个阶段都完整列出(Worker 各次 + Verifier 各次)
|
|
472
478
|
var runs = historyRuns(task)
|