dsh-agent-board 1.7.7 → 1.7.9
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 +14 -5
- package/index.mjs +1 -0
- package/lib/cleanup.mjs +256 -0
- package/lib/client/board-list.js +12 -8
- package/lib/client/dashboard.js +133 -7
- package/lib/client/kernel.js +62 -10
- package/lib/client/task-detail.js +13 -5
- package/lib/client.js +220 -30
- package/lib/core.mjs +30 -2
- package/lib/dispatch.mjs +82 -5
- package/lib/rpc.mjs +154 -25
- package/lib/store.mjs +86 -62
- 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 +294 -18
- package/test/helpers/mock-ctx.mjs +7 -2
- package/test/scoreboard-trends-client.test.mjs +21 -0
- package/test/store-data-safety.test.mjs +105 -0
- package/test/ui-zoom-client.test.mjs +76 -0
- package/test/verify-timing.test.mjs +26 -0
package/lib/client.js
CHANGED
|
@@ -207,6 +207,11 @@ function apply(ctx) {
|
|
|
207
207
|
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
208
208
|
['rect', { width: 6, height: 6, x: 9, y: 9 }]
|
|
209
209
|
],
|
|
210
|
+
// 取消入口图标(详情页「取消任务」按钮;语义=作废不派发,与删除 trash-2 区分)
|
|
211
|
+
'ban': [
|
|
212
|
+
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
213
|
+
['path', { d: 'm4.9 4.9 14.2 14.2' }]
|
|
214
|
+
],
|
|
210
215
|
'plus': [
|
|
211
216
|
['path', { d: 'M5 12h14' }],
|
|
212
217
|
['path', { d: 'M12 5v14' }]
|
|
@@ -242,7 +247,15 @@ function apply(ctx) {
|
|
|
242
247
|
blocked: { title: '阻塞:卡住待处理(等依赖/等主窗口裁决)', empty: '暂无阻塞——任务卡住待裁决时会出现在这里' }
|
|
243
248
|
}
|
|
244
249
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
245
|
-
|
|
250
|
+
// ===== 界面缩放档位(task-mv58e01j,用户 2026-10-12「字体整体偏小」)=====
|
|
251
|
+
// 面板内 ~317 处内联 fontSize px 写死(9/10/11 为主),逐处改不值 → 面板根容器 CSS zoom
|
|
252
|
+
// 等比缩放统一接管(Chromium 支持,字体/图标/间距一并放大;zoom 只作用本面板子树,不碰宿主页面)。
|
|
253
|
+
// 纯客户端偏好(不走 host set-board-config):localStorage 键 dsh-agent-board-ui-zoom 持久化,缺省 1;
|
|
254
|
+
// 脏值/档位外值/读取异常一律回退 1(白名单校验,不写回——下次选中档位时覆盖)。
|
|
255
|
+
var UI_ZOOM_KEY = 'dsh-agent-board-ui-zoom'
|
|
256
|
+
var UI_ZOOM_LEVELS = [0.9, 1, 1.1, 1.25, 1.5] // 90% / 100%(默认)/ 110% / 125% / 150%
|
|
257
|
+
function readUiZoom() { try { var v = parseFloat(localStorage.getItem(UI_ZOOM_KEY)); return UI_ZOOM_LEVELS.indexOf(v) >= 0 ? v : 1 } catch (_) { return 1 } }
|
|
258
|
+
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: '' }
|
|
246
259
|
|
|
247
260
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
248
261
|
try {
|
|
@@ -323,7 +336,12 @@ function apply(ctx) {
|
|
|
323
336
|
// 空范围(两端皆空)不传 field:host 收到 undefined 即全量聚合,与老 host 的调用形态完全一致;
|
|
324
337
|
// 绝不用空字符串占位(host 侧 `from`/`to` 空串虽也判为无范围,但少传一个字段就少一处口径分叉)。
|
|
325
338
|
var rgSend = activeRange()
|
|
326
|
-
var rpcArgs = (rgSend.from || rgSend.to) ? { range: rgSend } :
|
|
339
|
+
var rpcArgs = (rgSend.from || rgSend.to) ? { range: rgSend } : {}
|
|
340
|
+
// 条件请求瘦身(task-mv492for):带上轮 tasksHash 作 ifHash,host 比对一致则返回 unchanged:true
|
|
341
|
+
// 轻量响应(不含 tasks 全量,轮询载荷 1.34MB→KB 级);首轮/会话切换后 state.tasksHash 为空不带
|
|
342
|
+
// ifHash → 全量返回(与不带 ifHash 的老客户端同路径)。空范围时 rpcArgs 由 undefined 改 {} 是同义
|
|
343
|
+
// 形态(rpc 助手 args||{} 归一;host 只读 range/sessionId/ifHash,多传空对象无害)。
|
|
344
|
+
if (state.tasksHash) rpcArgs.ifHash = state.tasksHash
|
|
327
345
|
// 范围守卫:范围已在本轮响应回来前被改掉 → 这份响应属于旧范围,丢弃 usageSummary 赋值
|
|
328
346
|
// (否则旧范围的旧聚合会覆盖新范围的新聚合,用户看到「切了范围数字没变」直到下一次轮询)。
|
|
329
347
|
// 注意 tasksHash 短路只管 tasks:usageSummary 照常赋值(现状已对,保持)。
|
|
@@ -338,7 +356,11 @@ function apply(ctx) {
|
|
|
338
356
|
// (hash 只管任务列表渲染;这些字段随下次任意 notify 生效,不为它们单独渲染)。
|
|
339
357
|
// 老 host 不返回 tasksHash → 恒视为变化,行为与旧版完全一致。
|
|
340
358
|
var newHash = (d && d.tasksHash) || ''
|
|
341
|
-
|
|
359
|
+
// 条件请求瘦身(task-mv492for):host 判 ifHash 一致时返回 unchanged:true(响应不含 tasks 全量)。
|
|
360
|
+
// state.tasks 必须原样保留——unchanged 优先于 hash 比对:缺 tasks 的响应若误入变化分支,
|
|
361
|
+
// `(d && d.tasks) || []` 会把列表清空。轻量字段不受影响,照常走下方既有赋值链。
|
|
362
|
+
var unchanged = !!(d && d.unchanged)
|
|
363
|
+
var tasksChanged = !unchanged && (!newHash || newHash !== state.tasksHash)
|
|
342
364
|
// 设置开关的权威纠偏(task-muw5uudk):开关不走 tasksHash,hash 不变分支里照常赋值也没有 notify
|
|
343
365
|
// → 勾选框必须等下一次任意 notify 才翻面(安静板卡数秒)。这里在赋值前快照、赋值后比对,
|
|
344
366
|
// 有变化就补一次 notify:乐观更新(PoolCfgPopover)已让点击瞬时翻面,本兜底是服务端权威值纠偏
|
|
@@ -364,10 +386,14 @@ function apply(ctx) {
|
|
|
364
386
|
state.poolStatus = (d && d.poolStatus) || null
|
|
365
387
|
// token 消耗聚合(board 级,host 端现算):范围守卫——响应回来时范围若已变,保留旧值不覆盖
|
|
366
388
|
// (新范围的那次请求会带着新聚合回来;老 host 不返回该字段 → null,零渲染)
|
|
367
|
-
// ===== usageSummary
|
|
368
|
-
//
|
|
369
|
-
// →
|
|
370
|
-
//
|
|
389
|
+
// ===== usageSummary 变化检测 + React 订阅(task-muwq9u04 → task-mv10lvud 根修)=====
|
|
390
|
+
// 病根(task-mv10lvud 实测「点今天也没变化」):范围切换时 setRange 先换 state.dateRange(新对象
|
|
391
|
+
// → setDR 身份变化 → 第一帧渲染成功),随后 fetchTasks 响应把 state.usageSummary 换成新对象并 notify(),
|
|
392
|
+
// 但 TopPanel 的 update() 里所有 setX 值都 Object.is 全等(tasks 引用不变、布尔不变)→ React 18 全量
|
|
393
|
+
// bailout,零重渲染 → Token 区永远拿不到新聚合。notify ≠ React 重渲染,须有「新身份的订阅值」做载体。
|
|
394
|
+
// 修法:TopPanel 补 usageSummary 专属 React state(setUsage(state.usageSummary)),host 每次响应都是
|
|
395
|
+
// 新对象,身份变化天然触发渲染(订阅见 TopPanel 的 update())。
|
|
396
|
+
// 为什么本处用「上一次的 JSON 串」比对而不是对象引用:host 每次 get-tasks 都现算聚合、**必然返回新对象**
|
|
371
397
|
// (引用比较恒真)→ 3s 轮询每轮都 notify,tasksHash 的渲染节约当场作废。JSON 串只在这份聚合
|
|
372
398
|
// 真变了时才不等(KB 级体积、3s 一次,代价可忽略);赋值前先快照,赋值后比对。
|
|
373
399
|
var usageJsonPrev = JSON.stringify(state.usageSummary || null)
|
|
@@ -389,6 +415,9 @@ function apply(ctx) {
|
|
|
389
415
|
state.epicSplit = cfgKnobOf(d, 'epicSplit')
|
|
390
416
|
// 自测指南开关(设置区「验收」):同上——缺字段=开(详情块/报告清单段照常),只有显式 false 才关
|
|
391
417
|
state.verifyUserGuide = cfgKnobOf(d, 'verifyUserGuide')
|
|
418
|
+
// 清理保留天数(设置区「存储清理」,卡龄主闸):数值键,host 已 cfg 归一(缺字段/脏值=7、钳 1-90),
|
|
419
|
+
// 客户端读服务端权威值(与 softTimeoutMin/hardTimeoutMin 同款直接赋值,不做布尔口径纠偏)。
|
|
420
|
+
state.cleanupRetentionDays = (d && d.cleanupRetentionDays) || 7
|
|
392
421
|
// 开关有变 / Token 区聚合有变 → 补一次 notify(tasksChanged 分支已在上面 notify 过,
|
|
393
422
|
// 这里只管 hash 不变时被跳过的那两次:开关乐观更新纠偏 + 统计范围切换后的新聚合)
|
|
394
423
|
var cfgDelta = !tasksChanged && cfgKnobsChanged(cfgKnobs, state)
|
|
@@ -511,6 +540,23 @@ function apply(ctx) {
|
|
|
511
540
|
}).catch(function (e) { if (onMsg) onMsg('⚠️ ' + String(e)); return { ok: false, error: String(e) } })
|
|
512
541
|
}
|
|
513
542
|
|
|
543
|
+
// ===== 取消通道(与 host cancel-task / task_cancel 门禁一一对应)=====
|
|
544
|
+
// 可取消状态:草稿/待办/阻塞(未进入执行)。in-progress/verifying 需先终止;resolved 引导归档。
|
|
545
|
+
// 取消=显式放弃:置 cancelled + 留档可检索、可在归档 tab「恢复待办」;与删除不同,取消不丢数据。
|
|
546
|
+
function canCancel(t) { return !!t && (t.status === 'draft' || t.status === 'pending' || t.status === 'blocked') }
|
|
547
|
+
// confirm 预警(与 note「取消后不再派发,可在归档恢复」同口径);失败时 host 门禁原因原样回显。
|
|
548
|
+
function cancelTask(id, title, onMsg) {
|
|
549
|
+
var t = getTask(id)
|
|
550
|
+
var name = title || (t && t.title) || id
|
|
551
|
+
if (!window.confirm('取消后不再派发,可在归档恢复。确认取消「' + name + '」?')) return Promise.resolve({ ok: false, cancelled: true })
|
|
552
|
+
return rpc('cancel-task', { taskId: id }).then(function (r) {
|
|
553
|
+
if (r && r.ok === false) { if (onMsg) onMsg('⚠️ ' + (r.error || '取消失败')) }
|
|
554
|
+
else if (onMsg) onMsg('🚫 已取消「' + name + '」(可在归档 tab 恢复待办)')
|
|
555
|
+
fetchTasks()
|
|
556
|
+
return r
|
|
557
|
+
}).catch(function (e) { if (onMsg) onMsg('⚠️ ' + String(e)); return { ok: false, error: String(e) } })
|
|
558
|
+
}
|
|
559
|
+
|
|
514
560
|
function onDragStart(e, task) { e.dataTransfer.setData('text/plain', JSON.stringify({ id: task.id, status: task.status })); e.dataTransfer.effectAllowed = 'move'; state.dragTask = task.id; notify() }
|
|
515
561
|
function onDragEnd() { state.dragTask = null; state.dragOver = null; notify() }
|
|
516
562
|
function onColDragOver(e, st) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (state.dragOver !== st) { state.dragOver = st; notify() } }
|
|
@@ -589,7 +635,10 @@ function apply(ctx) {
|
|
|
589
635
|
var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
|
|
590
636
|
var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
|
|
591
637
|
var _vug = useState(state.verifyUserGuide), vugOn = _vug[0], setVugOn = _vug[1] // 自测指南开关勾选态(设置区「验收」)
|
|
592
|
-
|
|
638
|
+
var _crd = useState(state.cleanupRetentionDays), crd = _crd[0], setCrd = _crd[1] // 清理保留天数(设置区「存储清理」)
|
|
639
|
+
var _uzm = useState(state.uiZoom), uiZoom = _uzm[0], setUiZoom = _uzm[1] // 界面缩放档位(设置区「界面」,zoom 挂面板根容器)
|
|
640
|
+
var _usg = useState(state.usageSummary), setUsage = _usg[1] // Token 区聚合订阅(task-mv10lvud 根修):usageSummary 换新对象即触发渲染
|
|
641
|
+
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) } }, [])
|
|
593
642
|
if (!open) return null
|
|
594
643
|
if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
|
|
595
644
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -611,11 +660,14 @@ function apply(ctx) {
|
|
|
611
660
|
})
|
|
612
661
|
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))
|
|
613
662
|
}
|
|
614
|
-
|
|
663
|
+
// 面板根容器(顶层 div):zoom 挂这里——等比缩放整个面板子树(字体/图标/间距),只影响本面板
|
|
664
|
+
// 不影响宿主页面;uiZoom 为订阅值(设置弹层选档 → state.uiZoom + notify → 本组件重渲染即时生效)。
|
|
665
|
+
// React 的 zoom 属无单位数值型 style(isUnitlessNumber 含 zoom),直接传数值不会拼 px。
|
|
666
|
+
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' } },
|
|
615
667
|
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 } },
|
|
616
668
|
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)),
|
|
617
669
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
618
|
-
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 }),
|
|
670
|
+
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 }),
|
|
619
671
|
dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
|
|
620
672
|
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), '新建任务'),
|
|
621
673
|
createFlash ? React.createElement('span', { style: { fontSize: 10, color: C.ok } }, createFlash) : null,
|
|
@@ -826,18 +878,21 @@ function apply(ctx) {
|
|
|
826
878
|
function ArchiveView() {
|
|
827
879
|
var _R = React; var useState = _R.useState
|
|
828
880
|
var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
|
|
829
|
-
//
|
|
881
|
+
// 归档 tab 数据源口径对齐仪表盘(反馈 n-muyg3e0m9z7i):直接读 state.tasks(get-tasks 全量含归档),
|
|
830
882
|
// 不再依赖独立 state.archived/fetchArchived——旧路径异步填库却未接入 TopPanel 的 notify 更新项,
|
|
831
883
|
// 归档 tab 打开后停在「暂无归档任务」空态,而仪表盘「已归档」计数来自 state.tasks(全量),二者对不上。
|
|
832
|
-
// 全量 +
|
|
833
|
-
|
|
884
|
+
// 全量 + 按归档/取消时间倒序(archivedAt 优先,回退 cancelledAt/resolvedAt/createdAt)+ 顶部注明条数。
|
|
885
|
+
// 含 cancelled(反馈 n-mv0zbqk... 取消通道):取消卡在待办列消失(cancelled 不在 COLUMNS)后,
|
|
886
|
+
// 归入本 tab 可见、可「恢复待办」——否则作废卡从 UI 上彻底蒸发、无法找回。
|
|
887
|
+
var archived = state.tasks.filter(function (t) { return t.status === 'archived' || t.status === 'cancelled' })
|
|
888
|
+
var cancelledCount = archived.filter(function (t) { return t.status === 'cancelled' }).length // 文案口径修正:仪表盘「已归档」只数 archived,本 tab 含 cancelled,须单列
|
|
834
889
|
var list = archived.filter(function (t) {
|
|
835
890
|
if (!q) return true
|
|
836
891
|
var qq = q.toLowerCase()
|
|
837
892
|
return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
|
|
838
893
|
}).sort(function (a, b) {
|
|
839
|
-
//
|
|
840
|
-
function archTs(t) { return t.archivedAt || t.resolvedAt || t.createdAt || '' }
|
|
894
|
+
// 归档/取消时间倒序:archivedAt 优先,回退 cancelledAt/resolvedAt/createdAt(老卡可能缺 archivedAt)
|
|
895
|
+
function archTs(t) { return t.archivedAt || t.cancelledAt || t.resolvedAt || t.createdAt || '' }
|
|
841
896
|
return (archTs(b) || '').localeCompare(archTs(a) || '')
|
|
842
897
|
})
|
|
843
898
|
// 工作模式(与 fetchTasks 同口径):list=清单模式(恢复不重派)不弹;auto/team 恢复即重派 → confirm 门禁
|
|
@@ -850,8 +905,8 @@ function apply(ctx) {
|
|
|
850
905
|
}
|
|
851
906
|
return React.createElement('div', null,
|
|
852
907
|
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 } }),
|
|
853
|
-
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '共 ' + archived.length + '
|
|
854
|
-
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '
|
|
908
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 }, title: '归档 tab 含已归档 + 已取消两类;仪表盘「已归档」只数已归档,不数已取消' }, '共 ' + archived.length + ' 条' + (cancelledCount > 0 ? '(含已取消 ' + cancelledCount + ' 条)' : '')),
|
|
909
|
+
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档/已取消任务') :
|
|
855
910
|
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
856
911
|
list.map(function (t) {
|
|
857
912
|
// 归档行点击进详情(用户 2026-10-09 指令):复用 state.detailId 机制,与看板卡同路径;
|
|
@@ -861,7 +916,8 @@ function apply(ctx) {
|
|
|
861
916
|
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
917
|
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,
|
|
863
918
|
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,
|
|
864
|
-
React.createElement('span', { style: { fontSize: 9,
|
|
919
|
+
t.status === 'cancelled' ? React.createElement('span', { title: '已取消(可恢复待办)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 20%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0 } }, '已取消') : null,
|
|
920
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, (t.archivedAt || t.cancelledAt) ? ago(t.archivedAt || t.cancelledAt) : '')),
|
|
865
921
|
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,
|
|
866
922
|
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4, flexWrap: 'wrap' } },
|
|
867
923
|
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' } }, '↩ 恢复待办'),
|
|
@@ -1314,7 +1370,10 @@ function apply(ctx) {
|
|
|
1314
1370
|
p.then(function (r) { if (r && r.ok === false) setActionMsg('⚠️ 流转失败:' + (r.error || '未知错误')); else setActionMsg('✅ 已流转到「' + (statusLabels[to] || to) + '」'); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) })
|
|
1315
1371
|
}
|
|
1316
1372
|
return React.createElement('div', { style: { padding: '4px 2px' } },
|
|
1317
|
-
|
|
1373
|
+
// 头部行(task-mv493xjs 巡检次生观察②):左「← 返回看板」+ 右上显性 ✕ 关闭钮(原来只有文字链不够显性)
|
|
1374
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 } },
|
|
1375
|
+
React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer' } }, '← 返回看板'),
|
|
1376
|
+
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))),
|
|
1318
1377
|
React.createElement(ReviewSummary, { task: task }),
|
|
1319
1378
|
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
1379
|
React.createElement(TagsEditor, { key: task.id, task: task }),
|
|
@@ -1435,10 +1494,15 @@ function apply(ctx) {
|
|
|
1435
1494
|
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,
|
|
1436
1495
|
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,
|
|
1437
1496
|
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),
|
|
1438
|
-
//
|
|
1439
|
-
//
|
|
1440
|
-
//
|
|
1441
|
-
|
|
1497
|
+
// ===== 破坏性操作区(task-mv493xjs 消歧,巡检实证 n-mv2turadjv63 误触)=====
|
|
1498
|
+
// 视觉分层:与上方表单按钮组(保存/发布/领取/派发/通过/驳回/归档)拉开——独立一行 +
|
|
1499
|
+
// 顶部虚线分隔 + 「危险操作」前缀标签。「取消」改名「取消任务」:按表单惯例会被误读为
|
|
1500
|
+
// 「关面板」,实际是 task_cancel 破坏性流转(confirm 确认在 kernel.cancelTask 已存在)。
|
|
1501
|
+
// 取消(仅草稿/待办/阻塞)语义=作废留档(可恢复);删除=真删不可恢复,被 host 门禁拒绝时原因回显 actionMsg。
|
|
1502
|
+
(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 } },
|
|
1503
|
+
React.createElement('span', { style: { fontSize: 9, color: C.err, fontWeight: 700, marginRight: 2, letterSpacing: 1 } }, '危险操作'),
|
|
1504
|
+
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,
|
|
1505
|
+
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,
|
|
1442
1506
|
(function () {
|
|
1443
1507
|
// 历史会话:无论任务处于哪个阶段都完整列出(Worker 各次 + Verifier 各次)
|
|
1444
1508
|
var runs = historyRuns(task)
|
|
@@ -2055,13 +2119,14 @@ function apply(ctx) {
|
|
|
2055
2119
|
// 近 14 天柱形通用画法(① 通过率 / ③ 超时率共用):cells = [{ k, rate(null=当日无样本), tip }]
|
|
2056
2120
|
// 今日柱 brand 高亮 + 率值顶标;无样本日画 3px 灰基线(title 写明无样本,不假装 0%)。
|
|
2057
2121
|
function qtDayBars(cells, color, todayKey) {
|
|
2058
|
-
return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46 } },
|
|
2122
|
+
return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46, minWidth: 0, overflow: 'hidden' } },
|
|
2059
2123
|
cells.map(function (c) {
|
|
2060
2124
|
var isToday = c.k === todayKey
|
|
2061
2125
|
var h = c.rate === null ? 3 : Math.max(4, Math.round(c.rate * 34))
|
|
2126
|
+
h = Math.min(22, h) // clamp 柱高:100% 柱封顶 22px——46px 绘图容器扣掉上下率值/日期标签与 gap/描边后不越出,防单日 100% 黑柱向上盖住右列
|
|
2062
2127
|
var kp = c.k.split('-')
|
|
2063
2128
|
var dLabel = Number(kp[1]) + '/' + Number(kp[2])
|
|
2064
|
-
return React.createElement('div', { key: c.k, style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 1 }, title: c.tip },
|
|
2129
|
+
return React.createElement('div', { key: c.k, style: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 1 }, title: c.tip },
|
|
2065
2130
|
c.rate !== null ? React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, mpPct(c.rate)) : null,
|
|
2066
2131
|
React.createElement('div', { style: { width: '100%', maxWidth: 22, height: h + 'px', borderRadius: 2, background: c.rate === null ? C.nested : (isToday ? C.brand : color), border: '1px solid ' + (isToday ? C.brand : (c.rate === null ? C.border : color)) } }),
|
|
2067
2132
|
React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, dLabel))
|
|
@@ -2106,7 +2171,7 @@ function apply(ctx) {
|
|
|
2106
2171
|
})
|
|
2107
2172
|
var fpHas = false; fpCells.forEach(function (c) { if (c.rate !== null) fpHas = true })
|
|
2108
2173
|
var fpToday = fpCells[fpCells.length - 1]
|
|
2109
|
-
var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
2174
|
+
var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230, overflow: 'hidden' } },
|
|
2110
2175
|
blockHead('一次通过率(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: fpToday.rate !== null ? C.brand : C.text2, fontWeight: 600 } }, fpToday.rate !== null ? mpPct(fpToday.rate) : '—')), '一次通过率 = 当天落定「已完成」任务里零驳回(rejectCount=0)的占比;驳回 = Verifier 验收判 reject'),
|
|
2111
2176
|
fpHas ? qtDayBars(fpCells, C.ok, todayKey)
|
|
2112
2177
|
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 resolved 任务落定'))
|
|
@@ -2134,7 +2199,7 @@ function apply(ctx) {
|
|
|
2134
2199
|
})
|
|
2135
2200
|
var toHas = false; toCells.forEach(function (c) { if (c.rate !== null) toHas = true })
|
|
2136
2201
|
var toToday = toCells[toCells.length - 1]
|
|
2137
|
-
var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
2202
|
+
var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230, overflow: 'hidden' } },
|
|
2138
2203
|
blockHead('超时率走势(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: toToday.rate !== null ? C.warn : C.text2, fontWeight: 600 } }, toToday.rate !== null ? mpPct(toToday.rate) : '—')), '超时率 = 超时落定(timeout/error)的 run 占当天 run 的比例'),
|
|
2139
2204
|
toHas ? qtDayBars(toCells, C.warn, todayKey)
|
|
2140
2205
|
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 run 落定'))
|
|
@@ -2227,6 +2292,73 @@ function apply(ctx) {
|
|
|
2227
2292
|
}))
|
|
2228
2293
|
}
|
|
2229
2294
|
|
|
2295
|
+
// ===== 存储清理区(已结算会话清理,task-mv2131cz):数据源 host 端 cleanup-preview / cleanup-run =====
|
|
2296
|
+
// preview 纯读盘零副作用;run 需 confirm:true 二次确认(删除不可恢复,token 账已入卡不受影响)。
|
|
2297
|
+
// 可删判定口径在 host lib/cleanup.mjs:卡龄主闸(卡归档满 cleanupRetentionDays 天)+ 三底闸(落定/不活跃/路径安全)。
|
|
2298
|
+
function fmtBytes(n) {
|
|
2299
|
+
var v = Number(n) || 0
|
|
2300
|
+
if (v >= 1073741824) return (v / 1073741824).toFixed(1) + ' GB'
|
|
2301
|
+
if (v >= 1048576) return (v / 1048576).toFixed(v >= 10485760 ? 0 : 1) + ' MB'
|
|
2302
|
+
if (v >= 1024) return (v / 1024).toFixed(v >= 10240 ? 0 : 1) + ' KB'
|
|
2303
|
+
return String(v) + ' B'
|
|
2304
|
+
}
|
|
2305
|
+
function StorageCleanup() {
|
|
2306
|
+
var _R = React; var useState = _R.useState
|
|
2307
|
+
var _p = useState(null), prev = _p[0], setPrev = _p[1]
|
|
2308
|
+
var _o = useState(false), open = _o[0], setOpen = _o[1]
|
|
2309
|
+
var _r = useState(null), report = _r[0], setReport = _r[1]
|
|
2310
|
+
var _m = useState(''), msg = _m[0], setMsg = _m[1]
|
|
2311
|
+
var _b = useState(false), busy = _b[0], setBusy = _b[1]
|
|
2312
|
+
var _c = useState(false), confirmOn = _c[0], setConfirmOn = _c[1]
|
|
2313
|
+
function preview() {
|
|
2314
|
+
setBusy(true); setMsg('')
|
|
2315
|
+
rpc('cleanup-preview').then(function (r) {
|
|
2316
|
+
if (r && r.ok) { setPrev(r); setReport(null); setConfirmOn(false) } else { setMsg((r && r.error) || '预览失败') }
|
|
2317
|
+
setBusy(false)
|
|
2318
|
+
}).catch(function (e) { setMsg('预览失败: ' + (e && e.message || e)); setBusy(false) })
|
|
2319
|
+
}
|
|
2320
|
+
function runClean() {
|
|
2321
|
+
setBusy(true); setMsg('')
|
|
2322
|
+
rpc('cleanup-run', { confirm: true }).then(function (r) {
|
|
2323
|
+
setReport(r); setPrev(null); setConfirmOn(false); setBusy(false)
|
|
2324
|
+
if (r && r.ok) { if (typeof fetchTasks === 'function') fetchTasks() } else { setMsg((r && r.error) || '清理失败') }
|
|
2325
|
+
}).catch(function (e) { setMsg('清理失败: ' + (e && e.message || e)); setBusy(false) })
|
|
2326
|
+
}
|
|
2327
|
+
var box = { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 }
|
|
2328
|
+
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), '存储清理')
|
|
2329
|
+
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 }
|
|
2330
|
+
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 }
|
|
2331
|
+
function skippedLine(s, i) {
|
|
2332
|
+
return React.createElement('div', { key: 'sk' + i, style: { fontSize: 9, color: C.text2, marginBottom: 2 } },
|
|
2333
|
+
React.createElement('span', { style: { color: C.warn } }, '⏭ '), String(s.id || '').slice(0, 14) + '…:' + (s.text || s.reason || ''))
|
|
2334
|
+
}
|
|
2335
|
+
return React.createElement('div', { style: box },
|
|
2336
|
+
head,
|
|
2337
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
2338
|
+
React.createElement('button', { onClick: preview, disabled: busy, style: btn, title: '扫描本看板归档超过保留天数的卡片的会话,估算可释放空间(只读不删)' }, ic('refresh-cw', 11), busy ? '扫描中…' : '预览可清理会话'),
|
|
2339
|
+
prev ? React.createElement('span', { style: { fontSize: 10, color: C.text } }, prev.total > 0 ? ('可删 ' + prev.total + ' 个会话 · 约 ' + fmtBytes(prev.totalBytes)) : '无可删会话') : null,
|
|
2340
|
+
prev && prev.total > 0 ? React.createElement('button', { onClick: function () { setOpen(!open); setConfirmOn(false) }, style: btn }, ic(open ? 'chevron-up' : 'chevron-down', 10), open ? '收起明细' : '展开明细') : null,
|
|
2341
|
+
prev && prev.total > 0 ? (confirmOn ? React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } },
|
|
2342
|
+
React.createElement('button', { onClick: runClean, disabled: busy, style: dangerBtn, title: '确认删除不可恢复;token 账已入卡不受影响' }, ic('check', 11), '确认清理(不可恢复)'),
|
|
2343
|
+
React.createElement('button', { onClick: function () { setConfirmOn(false) }, style: btn }, '取消'))
|
|
2344
|
+
: React.createElement('button', { onClick: function () { setConfirmOn(true) }, disabled: busy, style: btn, title: '删除不可恢复;token 账已入卡不受影响' }, ic('trash-2', 11), '清理')) : null),
|
|
2345
|
+
msg ? React.createElement('div', { style: { fontSize: 9, color: C.err, marginTop: 4 } }, msg) : null,
|
|
2346
|
+
report && report.ok ? React.createElement('div', { style: { fontSize: 10, color: C.text, marginTop: 5 } },
|
|
2347
|
+
'✅ 已释放约 ' + fmtBytes(report.freedBytes) + '(删除 ' + report.deleted + ' 个会话' + ((report.skipped && report.skipped.length) ? ' · 跳过 ' + report.skipped.length + ' 个' : '') + ')') : null,
|
|
2348
|
+
report && report.ok && report.skipped && report.skipped.length ? React.createElement('div', { style: { marginTop: 4 } }, report.skipped.map(skippedLine)) : null,
|
|
2349
|
+
open && prev && prev.total > 0 ? React.createElement('div', { style: { marginTop: 6, borderTop: '1px solid ' + C.nested, paddingTop: 5 } },
|
|
2350
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 4 } }, '可删会话(会话目录 + projcache 缓存一并删除)'),
|
|
2351
|
+
(prev.byTask || []).map(function (b) {
|
|
2352
|
+
return React.createElement('div', { key: b.taskId, style: { marginBottom: 3, fontSize: 9, color: C.text2 } },
|
|
2353
|
+
React.createElement('span', { style: { color: C.brand } }, shortId(b.title)), ':' + b.count + ' 个 · 约 ' + fmtBytes(b.bytes))
|
|
2354
|
+
}),
|
|
2355
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2 } }, '数据来源:本看板 runs[] 里所属卡已归档超过 ' + (state.cleanupRetentionDays || 7) + ' 天、且 outcome 已落定并已不活跃的 Worker/Verifier 会话;token 账早已记入卡片,删除不影响任何统计。')
|
|
2356
|
+
) : null,
|
|
2357
|
+
prev && prev.total === 0 && prev.skipped && prev.skipped.length ? React.createElement('div', { style: { marginTop: 5 } },
|
|
2358
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 3 } }, '无符合条件可删的会话(跳过项)'),
|
|
2359
|
+
prev.skipped.slice(0, 8).map(skippedLine)) : null)
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2230
2362
|
function Dashboard() {
|
|
2231
2363
|
var stats = computeStats(state.tasks); var statusOrder = ['pending', 'in-progress', 'verifying', 'resolved', 'blocked', 'archived']; var prioOrder = ['critical', 'high', 'medium', 'low']
|
|
2232
2364
|
return React.createElement('div', null,
|
|
@@ -2234,6 +2366,7 @@ function apply(ctx) {
|
|
|
2234
2366
|
React.createElement(GlobalBoards),
|
|
2235
2367
|
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 })),
|
|
2236
2368
|
React.createElement(HealthHints),
|
|
2369
|
+
React.createElement(StorageCleanup),
|
|
2237
2370
|
React.createElement(TokenUsage, { usage: state.usageSummary }),
|
|
2238
2371
|
// 模型表现区(卡3):Token 区正下方;scoreboard 随 usageSummary 同通道透传(老 host 缺字段 → null → 空态)
|
|
2239
2372
|
React.createElement(ModelPerf, { sb: (state.usageSummary && state.usageSummary.scoreboard) || null }),
|
|
@@ -2297,6 +2430,30 @@ function apply(ctx) {
|
|
|
2297
2430
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '分'),
|
|
2298
2431
|
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)
|
|
2299
2432
|
}
|
|
2433
|
+
// 清理保留天数输入(卡龄主闸设置项 cleanupRetentionDays,1~90 天):与 MinCfg 同款受控输入 + 失焦/回车提交,
|
|
2434
|
+
// 越界值直接回弹到服务端权威值(不留脏态),成功走 set-board-config → fetchTasks 由服务端权威值纠偏。
|
|
2435
|
+
function RetentionCfg(props) {
|
|
2436
|
+
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
2437
|
+
var _a = useState(String(props.value)), val = _a[0], setVal = _a[1]
|
|
2438
|
+
var _b = useState(false), dirty = _b[0], setDirty = _b[1]
|
|
2439
|
+
useEffect(function () { setVal(String(props.value)); setDirty(false) }, [props.value])
|
|
2440
|
+
function commit(v) {
|
|
2441
|
+
var n = parseInt(v, 10)
|
|
2442
|
+
if (isNaN(n) || n < 1 || n > 90) { setVal(String(props.value)); setDirty(false); return }
|
|
2443
|
+
setVal(String(n)); setDirty(false); rpc('set-board-config', { key: 'cleanupRetentionDays', value: n }).then(fetchTasks).catch(function () {})
|
|
2444
|
+
}
|
|
2445
|
+
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } },
|
|
2446
|
+
props.label,
|
|
2447
|
+
React.createElement('input', {
|
|
2448
|
+
value: val, title: props.tip || ('1 ~ 90 天,默认 7'),
|
|
2449
|
+
onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== String(props.value)) },
|
|
2450
|
+
onBlur: function () { if (dirty) commit(val) },
|
|
2451
|
+
onKeyDown: function (e) { if (e.key === 'Enter') commit(val) },
|
|
2452
|
+
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 }
|
|
2453
|
+
}),
|
|
2454
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '天'),
|
|
2455
|
+
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)
|
|
2456
|
+
}
|
|
2300
2457
|
|
|
2301
2458
|
function PoolStatus() {
|
|
2302
2459
|
var ps = state.poolStatus
|
|
@@ -2340,6 +2497,23 @@ function apply(ctx) {
|
|
|
2340
2497
|
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)
|
|
2341
2498
|
}
|
|
2342
2499
|
|
|
2500
|
+
// 界面缩放档位选择(task-mv58e01j,用户反馈「字体整体偏小」):90/100/110/125/150% 五档下拉。
|
|
2501
|
+
// 纯客户端偏好——不走 set-board-config(host 不管 UI 偏好):选中即 localStorage.setItem 持久化
|
|
2502
|
+
// + state.uiZoom + notify,TopPanel 根容器 zoom 订阅即时生效;非法档位(脏值)不采纳。
|
|
2503
|
+
function ZoomCfg() {
|
|
2504
|
+
var cur = state.uiZoom || 1
|
|
2505
|
+
function pick(v) {
|
|
2506
|
+
var n = parseFloat(v)
|
|
2507
|
+
if (UI_ZOOM_LEVELS.indexOf(n) < 0) return
|
|
2508
|
+
state.uiZoom = n
|
|
2509
|
+
try { localStorage.setItem(UI_ZOOM_KEY, String(n)) } catch (_) {}
|
|
2510
|
+
notify()
|
|
2511
|
+
}
|
|
2512
|
+
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, '界面缩放',
|
|
2513
|
+
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 } },
|
|
2514
|
+
UI_ZOOM_LEVELS.map(function (z) { return React.createElement('option', { key: String(z), value: String(z) }, Math.round(z * 100) + '%' + (z === 1 ? '(默认)' : '')) })))
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2343
2517
|
function PoolCfgPopover(props) {
|
|
2344
2518
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
|
|
2345
2519
|
var _a = useState(false), open = _a[0], setOpen = _a[1]
|
|
@@ -2363,12 +2537,16 @@ function apply(ctx) {
|
|
|
2363
2537
|
// position:fixed + 视口坐标:面板容器是 overflow:hidden + maxHeight:60vh,
|
|
2364
2538
|
// absolute 弹窗在面板内容短时会被裁掉下半截(超时配置行曾被整个裁掉)。
|
|
2365
2539
|
// fixed 脱离任何祖先裁剪(面板无 transform/filter,不会形成包含块)。
|
|
2366
|
-
|
|
2540
|
+
// 界面缩放补偿(task-mv58e01j):根容器挂了 CSS zoom 后,getBoundingClientRect 返回缩放后的
|
|
2541
|
+
// 可视坐标,而 fixed 弹层在 zoom 子树内 top/right/maxHeight 会再被缩一次 → 各项除以档位还原成
|
|
2542
|
+
// CSS px,弹层仍贴着按钮且不溢出视口底(zoom=1 时与旧行为逐字一致)。
|
|
2543
|
+
var z = state.uiZoom || 1
|
|
2544
|
+
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) }) }
|
|
2367
2545
|
setOpen(!open)
|
|
2368
2546
|
}
|
|
2369
2547
|
return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
|
|
2370
2548
|
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)),
|
|
2371
|
-
open && pos ? React.createElement('div', { style: { position: 'fixed', top: pos.top + 'px', right: pos.right + 'px', padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 4px 16px rgba(0,0,0,0.15)', zIndex: 1000, whiteSpace: 'nowrap', maxHeight:
|
|
2549
|
+
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' } },
|
|
2372
2550
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
|
|
2373
2551
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
|
|
2374
2552
|
React.createElement(PoolCfg, { label: 'Worker 并发', cfgKey: 'maxWorkers', value: props.maxW }),
|
|
@@ -2412,7 +2590,19 @@ function apply(ctx) {
|
|
|
2412
2590
|
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
|
|
2413
2591
|
React.createElement('input', { type: 'checkbox', checked: props.epicSplit !== false, onChange: function (e) { setCfg('epicSplit', e.target.checked) } }),
|
|
2414
2592
|
React.createElement('span', null, '🧩 史诗拆分:大任务引导拆为 epic + 子任务')),
|
|
2415
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '关掉只停引导:显式 parentId 建子卡与史诗自动收口照常工作')
|
|
2593
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '关掉只停引导:显式 parentId 建子卡与史诗自动收口照常工作'),
|
|
2594
|
+
// 清理保留天数(卡龄主闸 cleanupRetentionDays,默认 7、可配 1-90):回收与归档生命周期对齐——
|
|
2595
|
+
// 卡片归档满该天数后,其会话才进入可清理池。仪表盘「存储清理」区文案读同一服务端权威值。
|
|
2596
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '存储清理'),
|
|
2597
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
2598
|
+
React.createElement(RetentionCfg, { label: '归档保留', value: props.retentionDays, tip: '卡片归档后多少天可被完全回收(1~90 天,默认 7)' }),
|
|
2599
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(归档满该天数后其会话可被清理回收)')),
|
|
2600
|
+
// 界面缩放档位(task-mv58e01j):设置弹层「界面」小节——五档下拉选中即 localStorage
|
|
2601
|
+
// 持久化 + state.uiZoom + notify 即时生效(zoom 挂在 TopPanel 根容器,只影响本面板)。
|
|
2602
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '界面'),
|
|
2603
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
2604
|
+
React.createElement(ZoomCfg, null),
|
|
2605
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(等比缩放面板字体/图标/间距,仅本面板生效并本地记忆)'))) : null)
|
|
2416
2606
|
}
|
|
2417
2607
|
|
|
2418
2608
|
function TeamView() {
|
package/lib/core.mjs
CHANGED
|
@@ -139,7 +139,11 @@ export function classifyPipeline(t) {
|
|
|
139
139
|
export function cfg(d) {
|
|
140
140
|
var soft = Math.max(1, Math.min(480, d.softTimeoutMin || 30))
|
|
141
141
|
var hard = Math.max(soft, Math.min(1440, d.hardTimeoutMin || 120))
|
|
142
|
-
|
|
142
|
+
// 清理保留天数(卡龄主闸,默认 7 天):读路径归一,缺字段/脏值=7,钳 1-90。
|
|
143
|
+
var crd = d.cleanupRetentionDays
|
|
144
|
+
if (typeof crd !== 'number' || !(crd >= 1)) crd = 7
|
|
145
|
+
crd = Math.max(1, Math.min(90, Math.round(crd)))
|
|
146
|
+
return { minWorkers: Math.max(0, Math.min(10, d.minWorkers || 1)), maxWorkers: Math.max(1, Math.min(10, d.maxWorkers || 3)), minVerifiers: Math.max(0, Math.min(5, d.minVerifiers || 0)), maxVerifiers: Math.max(0, Math.min(5, d.maxVerifiers || 2)), softTimeoutMin: soft, hardTimeoutMin: hard, cleanupRetentionDays: crd, feedbackEnabled: d.feedbackEnabled !== false, notifyDispatch: d.notifyDispatch !== false, notifyDone: d.notifyDone !== false, epicSplit: d.epicSplit !== false, workerContinuable: d.workerContinuable !== false, verifyUserGuide: d.verifyUserGuide !== false }
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
// ===== 看板数据目录(跨重启继承用)=====
|
|
@@ -156,7 +160,7 @@ export function boardHome() { return path.join(boardDirName(), '.dsh') }
|
|
|
156
160
|
// ownerCwd(跨重启继承):创建该看板的会话工作区路径,继承判定全靠它——取不到就省略字段
|
|
157
161
|
// (绝不落空串,否则「路径读不到的多个会话」会被误判成同一工作区)。
|
|
158
162
|
export function seed(sid, ownerCwd) {
|
|
159
|
-
var d = { version: 12, ownerSession: sid, boardMode: 'auto', teamMode: false, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, workerContinuable: true, verifyUserGuide: true, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, poolStatus: { workers: [], verifiers: [] }, tasks: [] }
|
|
163
|
+
var d = { version: 12, ownerSession: sid, boardMode: 'auto', teamMode: false, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, workerContinuable: true, verifyUserGuide: true, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, cleanupRetentionDays: 7, poolStatus: { workers: [], verifiers: [] }, tasks: [] }
|
|
160
164
|
if (typeof ownerCwd === 'string' && ownerCwd) d.ownerCwd = ownerCwd
|
|
161
165
|
return d
|
|
162
166
|
}
|
|
@@ -178,6 +182,9 @@ export function normalizeBoard(d) {
|
|
|
178
182
|
if (typeof d.epicSplit !== 'boolean') d.epicSplit = true
|
|
179
183
|
if (typeof d.workerContinuable !== 'boolean') d.workerContinuable = true
|
|
180
184
|
if (typeof d.verifyUserGuide !== 'boolean') d.verifyUserGuide = true
|
|
185
|
+
// 清理保留天数(卡龄主闸):缺字段/脏值(非数或 <1)→ 7;合法值四舍五入并钳 1-90。
|
|
186
|
+
if (typeof d.cleanupRetentionDays !== 'number' || !(d.cleanupRetentionDays >= 1)) d.cleanupRetentionDays = 7
|
|
187
|
+
else d.cleanupRetentionDays = Math.max(1, Math.min(90, Math.round(d.cleanupRetentionDays)))
|
|
181
188
|
if (Array.isArray(d.tasks)) {
|
|
182
189
|
for (var i = 0; i < d.tasks.length; i++) {
|
|
183
190
|
var t = d.tasks[i]
|
|
@@ -260,6 +267,27 @@ export function archiveApply(d, t, actor, note) {
|
|
|
260
267
|
if (p) r.parentUpdated = true
|
|
261
268
|
return r
|
|
262
269
|
}
|
|
270
|
+
// 取消落定(task_cancel 工具与 cancel-task RPC 共享,单一行为口径;门禁 cancelGate 留在
|
|
271
|
+
// rpc.mjs——它只需判 status + claimedBy,无需 IO,与 archiveErr/deleteGate 同居「门禁在接线层、
|
|
272
|
+
// 状态流转在 core」的分工)。置 cancelled + 记历史(actor + 理由 note 可选)。
|
|
273
|
+
// spawn-pending 占位回退:claimedBy='spawn-pending' 是派发瞬时占位(无真实 run,spawn 尚在途),
|
|
274
|
+
// 取消时与 spawn 失败回退同口径清认领位(claimedBy/claimedAt → null),不留死占位。
|
|
275
|
+
// 取消不级联子卡(删除有「未归档子卡」门禁防悬空引用;取消只标记本卡,父卡不删,子卡 parentId
|
|
276
|
+
// 仍指向有效卡);但取消子卡同样触发父卡自动收口(isChildSettled 已计 cancelled)。
|
|
277
|
+
export function cancelApply(d, t, actor, note) {
|
|
278
|
+
var ps = t.status
|
|
279
|
+
if (t.claimedBy) { t.claimedBy = null; t.claimedAt = null } // spawn-pending / 残留认领位回退
|
|
280
|
+
t.status = 'cancelled'
|
|
281
|
+
t.cancelledAt = new Date().toISOString()
|
|
282
|
+
t.resolvedAt = null
|
|
283
|
+
t.resolution = null
|
|
284
|
+
delete t.stuckSince
|
|
285
|
+
ah(t, ps, 'cancelled', actor, note || 'cancelled')
|
|
286
|
+
var r = { ok: true, task: t }
|
|
287
|
+
var p = maybeAutoCloseParent(d, t) // 取消路径同样触发父卡自动收口(isChildSettled 已计 cancelled)
|
|
288
|
+
if (p) r.parentUpdated = true
|
|
289
|
+
return r
|
|
290
|
+
}
|
|
263
291
|
|
|
264
292
|
// ===== 史诗 hooks=agent run(宿主生命周期接线)=====
|
|
265
293
|
// 定位:hook 点 = 一次**真实 agent 运行**(不是声明式命令、不走 shell),挂在 epic 卡上、
|