dsh-agent-board 1.7.8 → 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 +12 -3
- package/lib/cleanup.mjs +256 -0
- package/lib/client/dashboard.js +128 -3
- package/lib/client/kernel.js +32 -7
- package/lib/client/task-detail.js +13 -7
- package/lib/client.js +173 -17
- package/lib/core.mjs +9 -2
- package/lib/dispatch.mjs +65 -0
- package/lib/rpc.mjs +103 -18
- 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 +152 -6
- package/test/helpers/mock-ctx.mjs +7 -2
- package/test/ui-zoom-client.test.mjs +76 -0
|
@@ -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)
|
package/lib/client.js
CHANGED
|
@@ -207,7 +207,7 @@ 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
|
-
//
|
|
210
|
+
// 取消入口图标(详情页「取消任务」按钮;语义=作废不派发,与删除 trash-2 区分)
|
|
211
211
|
'ban': [
|
|
212
212
|
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
213
213
|
['path', { d: 'm4.9 4.9 14.2 14.2' }]
|
|
@@ -247,7 +247,15 @@ function apply(ctx) {
|
|
|
247
247
|
blocked: { title: '阻塞:卡住待处理(等依赖/等主窗口裁决)', empty: '暂无阻塞——任务卡住待裁决时会出现在这里' }
|
|
248
248
|
}
|
|
249
249
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
250
|
-
|
|
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: '' }
|
|
251
259
|
|
|
252
260
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
253
261
|
try {
|
|
@@ -328,7 +336,12 @@ function apply(ctx) {
|
|
|
328
336
|
// 空范围(两端皆空)不传 field:host 收到 undefined 即全量聚合,与老 host 的调用形态完全一致;
|
|
329
337
|
// 绝不用空字符串占位(host 侧 `from`/`to` 空串虽也判为无范围,但少传一个字段就少一处口径分叉)。
|
|
330
338
|
var rgSend = activeRange()
|
|
331
|
-
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
|
|
332
345
|
// 范围守卫:范围已在本轮响应回来前被改掉 → 这份响应属于旧范围,丢弃 usageSummary 赋值
|
|
333
346
|
// (否则旧范围的旧聚合会覆盖新范围的新聚合,用户看到「切了范围数字没变」直到下一次轮询)。
|
|
334
347
|
// 注意 tasksHash 短路只管 tasks:usageSummary 照常赋值(现状已对,保持)。
|
|
@@ -343,7 +356,11 @@ function apply(ctx) {
|
|
|
343
356
|
// (hash 只管任务列表渲染;这些字段随下次任意 notify 生效,不为它们单独渲染)。
|
|
344
357
|
// 老 host 不返回 tasksHash → 恒视为变化,行为与旧版完全一致。
|
|
345
358
|
var newHash = (d && d.tasksHash) || ''
|
|
346
|
-
|
|
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)
|
|
347
364
|
// 设置开关的权威纠偏(task-muw5uudk):开关不走 tasksHash,hash 不变分支里照常赋值也没有 notify
|
|
348
365
|
// → 勾选框必须等下一次任意 notify 才翻面(安静板卡数秒)。这里在赋值前快照、赋值后比对,
|
|
349
366
|
// 有变化就补一次 notify:乐观更新(PoolCfgPopover)已让点击瞬时翻面,本兜底是服务端权威值纠偏
|
|
@@ -398,6 +415,9 @@ function apply(ctx) {
|
|
|
398
415
|
state.epicSplit = cfgKnobOf(d, 'epicSplit')
|
|
399
416
|
// 自测指南开关(设置区「验收」):同上——缺字段=开(详情块/报告清单段照常),只有显式 false 才关
|
|
400
417
|
state.verifyUserGuide = cfgKnobOf(d, 'verifyUserGuide')
|
|
418
|
+
// 清理保留天数(设置区「存储清理」,卡龄主闸):数值键,host 已 cfg 归一(缺字段/脏值=7、钳 1-90),
|
|
419
|
+
// 客户端读服务端权威值(与 softTimeoutMin/hardTimeoutMin 同款直接赋值,不做布尔口径纠偏)。
|
|
420
|
+
state.cleanupRetentionDays = (d && d.cleanupRetentionDays) || 7
|
|
401
421
|
// 开关有变 / Token 区聚合有变 → 补一次 notify(tasksChanged 分支已在上面 notify 过,
|
|
402
422
|
// 这里只管 hash 不变时被跳过的那两次:开关乐观更新纠偏 + 统计范围切换后的新聚合)
|
|
403
423
|
var cfgDelta = !tasksChanged && cfgKnobsChanged(cfgKnobs, state)
|
|
@@ -615,8 +635,10 @@ function apply(ctx) {
|
|
|
615
635
|
var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
|
|
616
636
|
var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
|
|
617
637
|
var _vug = useState(state.verifyUserGuide), vugOn = _vug[0], setVugOn = _vug[1] // 自测指南开关勾选态(设置区「验收」)
|
|
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 挂面板根容器)
|
|
618
640
|
var _usg = useState(state.usageSummary), setUsage = _usg[1] // Token 区聚合订阅(task-mv10lvud 根修):usageSummary 换新对象即触发渲染
|
|
619
|
-
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) } }, [])
|
|
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) } }, [])
|
|
620
642
|
if (!open) return null
|
|
621
643
|
if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
|
|
622
644
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -638,11 +660,14 @@ function apply(ctx) {
|
|
|
638
660
|
})
|
|
639
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))
|
|
640
662
|
}
|
|
641
|
-
|
|
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' } },
|
|
642
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 } },
|
|
643
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)),
|
|
644
669
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
645
|
-
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 }),
|
|
646
671
|
dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
|
|
647
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), '新建任务'),
|
|
648
673
|
createFlash ? React.createElement('span', { style: { fontSize: 10, color: C.ok } }, createFlash) : null,
|
|
@@ -1345,7 +1370,10 @@ function apply(ctx) {
|
|
|
1345
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)) })
|
|
1346
1371
|
}
|
|
1347
1372
|
return React.createElement('div', { style: { padding: '4px 2px' } },
|
|
1348
|
-
|
|
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))),
|
|
1349
1377
|
React.createElement(ReviewSummary, { task: task }),
|
|
1350
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),
|
|
1351
1379
|
React.createElement(TagsEditor, { key: task.id, task: task }),
|
|
@@ -1466,12 +1494,15 @@ function apply(ctx) {
|
|
|
1466
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,
|
|
1467
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,
|
|
1468
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),
|
|
1469
|
-
//
|
|
1470
|
-
|
|
1471
|
-
//
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
canDelete(task) ? React.createElement('
|
|
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,
|
|
1475
1506
|
(function () {
|
|
1476
1507
|
// 历史会话:无论任务处于哪个阶段都完整列出(Worker 各次 + Verifier 各次)
|
|
1477
1508
|
var runs = historyRuns(task)
|
|
@@ -2261,6 +2292,73 @@ function apply(ctx) {
|
|
|
2261
2292
|
}))
|
|
2262
2293
|
}
|
|
2263
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
|
+
|
|
2264
2362
|
function Dashboard() {
|
|
2265
2363
|
var stats = computeStats(state.tasks); var statusOrder = ['pending', 'in-progress', 'verifying', 'resolved', 'blocked', 'archived']; var prioOrder = ['critical', 'high', 'medium', 'low']
|
|
2266
2364
|
return React.createElement('div', null,
|
|
@@ -2268,6 +2366,7 @@ function apply(ctx) {
|
|
|
2268
2366
|
React.createElement(GlobalBoards),
|
|
2269
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 })),
|
|
2270
2368
|
React.createElement(HealthHints),
|
|
2369
|
+
React.createElement(StorageCleanup),
|
|
2271
2370
|
React.createElement(TokenUsage, { usage: state.usageSummary }),
|
|
2272
2371
|
// 模型表现区(卡3):Token 区正下方;scoreboard 随 usageSummary 同通道透传(老 host 缺字段 → null → 空态)
|
|
2273
2372
|
React.createElement(ModelPerf, { sb: (state.usageSummary && state.usageSummary.scoreboard) || null }),
|
|
@@ -2331,6 +2430,30 @@ function apply(ctx) {
|
|
|
2331
2430
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '分'),
|
|
2332
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)
|
|
2333
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
|
+
}
|
|
2334
2457
|
|
|
2335
2458
|
function PoolStatus() {
|
|
2336
2459
|
var ps = state.poolStatus
|
|
@@ -2374,6 +2497,23 @@ function apply(ctx) {
|
|
|
2374
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)
|
|
2375
2498
|
}
|
|
2376
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
|
+
|
|
2377
2517
|
function PoolCfgPopover(props) {
|
|
2378
2518
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
|
|
2379
2519
|
var _a = useState(false), open = _a[0], setOpen = _a[1]
|
|
@@ -2397,12 +2537,16 @@ function apply(ctx) {
|
|
|
2397
2537
|
// position:fixed + 视口坐标:面板容器是 overflow:hidden + maxHeight:60vh,
|
|
2398
2538
|
// absolute 弹窗在面板内容短时会被裁掉下半截(超时配置行曾被整个裁掉)。
|
|
2399
2539
|
// fixed 脱离任何祖先裁剪(面板无 transform/filter,不会形成包含块)。
|
|
2400
|
-
|
|
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) }) }
|
|
2401
2545
|
setOpen(!open)
|
|
2402
2546
|
}
|
|
2403
2547
|
return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
|
|
2404
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)),
|
|
2405
|
-
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' } },
|
|
2406
2550
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
|
|
2407
2551
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
|
|
2408
2552
|
React.createElement(PoolCfg, { label: 'Worker 并发', cfgKey: 'maxWorkers', value: props.maxW }),
|
|
@@ -2446,7 +2590,19 @@ function apply(ctx) {
|
|
|
2446
2590
|
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
|
|
2447
2591
|
React.createElement('input', { type: 'checkbox', checked: props.epicSplit !== false, onChange: function (e) { setCfg('epicSplit', e.target.checked) } }),
|
|
2448
2592
|
React.createElement('span', null, '🧩 史诗拆分:大任务引导拆为 epic + 子任务')),
|
|
2449
|
-
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)
|
|
2450
2606
|
}
|
|
2451
2607
|
|
|
2452
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]
|
package/lib/dispatch.mjs
CHANGED
|
@@ -1001,6 +1001,67 @@ export function createDispatch(ctx, state, deps) {
|
|
|
1001
1001
|
}
|
|
1002
1002
|
}
|
|
1003
1003
|
|
|
1004
|
+
// ===== 僵尸 run 留档清扫(task-mv22z61v):关账「落定卡上的 running 无 endedAt 留档」+ 清卡面认领位 =====
|
|
1005
|
+
// 事故(用户实证):0.4.1/0.4.5 时代老卡,宿主多次重启后 run 留档停在 outcome:'running' 无 endedAt +
|
|
1006
|
+
// 卡面 verifierRun/claimedBy 残留指向死会话。归档门禁 activeRunOf 的卡面兜底(liveEntry 认 claimedBy/
|
|
1007
|
+
// verifierRun 匹配的 running 留档)把它们误判活跃 → archive 永久拒。mv1pqy27 的 doTerminate 状态无关
|
|
1008
|
+
// 清理只覆盖「rec 在内存表」场景;重启后 rec 消失 + 卡已落定的组合落入盲区(resolved 无分支,什么都不清),
|
|
1009
|
+
// 且没有任何工具能关一条无 rec 的 run 留档本体。
|
|
1010
|
+
// 修法:扫描本板卡片的 runs[],凡 outcome==='running' 且无 endedAt 且该 run id 不在内存活跃表(runsFor)
|
|
1011
|
+
// 且所属卡已落定(resolved/cancelled/archived)→ 关账(endedAt=now,outcome 改 'stale-closed',注记
|
|
1012
|
+
// note='host 重启僵尸留档清扫')+ 清对应卡面认领位(verifierRun/claimedBy 若指向它)。
|
|
1013
|
+
// 边界(宁少勿多):
|
|
1014
|
+
// ① 只对落定卡做——在飞卡的 running 留档是 reconcile / 结算通道的辖区,别越界;
|
|
1015
|
+
// ② 无活跃 rec 才扫——内存表里仍活着(!settled)的 run 留给结算通道 / reapGhostRecs 关账(它们用
|
|
1016
|
+
// 'completed'/'timeout-error' 等真结局,而非这里的 'stale-closed',口径更准);
|
|
1017
|
+
// ③ 幂等——关账后 outcome!=running,二次运行零改动;回调无改动时返回 null 不写盘(沿用空闲快进的
|
|
1018
|
+
// 「不写盘」纪律,安静板不因清扫产生 15s 心跳写盘);
|
|
1019
|
+
// ④ 留痕——history 行 + console 一行(有产出时)。
|
|
1020
|
+
// 'stale-closed' 是既有枚举之外的新结局值:详情页/聚合端对未知 outcome 一律按「已结束(有 endedAt)」
|
|
1021
|
+
// 兜底展示,不会把它误判成 running(running 的判据是 outcome==='running' 且无 endedAt,二者已同时改写)。
|
|
1022
|
+
async function sweepZombieRuns(sid, snap) {
|
|
1023
|
+
if (!snap || snap.__noPersist || !Array.isArray(snap.tasks)) return 0 // 瞬时读盘失败/空板:跳过,不写盘
|
|
1024
|
+
// 预扫快照:有候选(落定卡 + running 无 endedAt 留档)才进锁写;否则零 mutateLocked 调用——
|
|
1025
|
+
// 空板/安静板走空闲快进「不写盘」的纪律不受清扫破坏(每 15s 心跳也不能因清扫多出一次写盘调用)。
|
|
1026
|
+
var hasCandidate = false
|
|
1027
|
+
for (var pi = 0; pi < snap.tasks.length && !hasCandidate; pi++) {
|
|
1028
|
+
var pt = snap.tasks[pi]
|
|
1029
|
+
if (!pt || (pt.status !== 'resolved' && pt.status !== 'cancelled' && pt.status !== 'archived')) continue
|
|
1030
|
+
var prs = Array.isArray(pt.runs) ? pt.runs : []
|
|
1031
|
+
for (var pj = 0; pj < prs.length; pj++) { if (prs[pj] && prs[pj].outcome === 'running' && !prs[pj].endedAt) { hasCandidate = true; break } }
|
|
1032
|
+
}
|
|
1033
|
+
if (!hasCandidate) return 0
|
|
1034
|
+
var runs = runsFor(sid)
|
|
1035
|
+
var swept = []
|
|
1036
|
+
await mutateLocked(sid, function (d) {
|
|
1037
|
+
if (!d || !Array.isArray(d.tasks)) return null
|
|
1038
|
+
var any = false
|
|
1039
|
+
d.tasks.forEach(function (t) {
|
|
1040
|
+
if (!t) return
|
|
1041
|
+
if (t.status !== 'resolved' && t.status !== 'cancelled' && t.status !== 'archived') return // 只清扫已落定卡
|
|
1042
|
+
if (!Array.isArray(t.runs)) return
|
|
1043
|
+
var cardChanged = false
|
|
1044
|
+
t.runs.forEach(function (r) {
|
|
1045
|
+
if (!r || r.outcome !== 'running' || r.endedAt) return // 只关「还开着且无 endedAt」的条目
|
|
1046
|
+
var rid = String(r.id)
|
|
1047
|
+
var rec = runs[t.id]
|
|
1048
|
+
if (rec && !rec.settled && String(rec.id) === rid) return // 内存活跃表里还活着 → 结算通道辖区,不扫
|
|
1049
|
+
r.outcome = 'stale-closed'
|
|
1050
|
+
r.endedAt = new Date().toISOString()
|
|
1051
|
+
r.note = 'host 重启僵尸留档清扫'
|
|
1052
|
+
if (t.verifierRun === rid) { t.verifierRun = null; delete t.verifierRunAt }
|
|
1053
|
+
if (t.claimedBy === rid) { t.claimedBy = null; t.claimedAt = null }
|
|
1054
|
+
swept.push(t.id + '/' + rid + '/' + (r.role || '?'))
|
|
1055
|
+
cardChanged = true
|
|
1056
|
+
})
|
|
1057
|
+
if (cardChanged) { ah(t, t.status, t.status, 'system', '僵尸 run 留档已关账(host 重启清扫)'); any = true }
|
|
1058
|
+
})
|
|
1059
|
+
return any ? d : null // 无改动返回 null → 不写盘(安静板不因清扫产生心跳写盘)
|
|
1060
|
+
}, true) // skipKick:本轮已是 poolCycle,无需再踢
|
|
1061
|
+
if (swept.length) console.log('[task-board] 僵尸 run 留档清扫 ' + swept.length + ' 条:' + swept.join(', '))
|
|
1062
|
+
return swept.length
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1004
1065
|
// ===== 幽灵占位回收(池冻结根修,task-muwlepg6)=====
|
|
1005
1066
|
// 病根(活体实证):活跃表项(rec)的**唯一**摘除点是各结算通道(事件通道 settleRun /
|
|
1006
1067
|
// 上报通道 settleReportedRun / 手动终止)。任何一条漏走(worker 经 board_report 完成但当时
|
|
@@ -1112,6 +1173,10 @@ export function createDispatch(ctx, state, deps) {
|
|
|
1112
1173
|
// 位置取舍:root 闸门必须是第一件事(无活 root 的板零 IO),所以本段只能排在早退闸门与 rt 读盘之后;
|
|
1113
1174
|
// 而重建出来的 rec 仍在本轮被读到(活跃计数 / 池状态快照都在下面才求值),语义不受影响。
|
|
1114
1175
|
try { await reconcileRoot(sid) } catch (e) { console.error('[task-board] reconcile 失败:', String(e)) }
|
|
1176
|
+
// 僵尸 run 留档清扫(task-mv22z61v):位置在 reconcile 之后、幽灵回收与空闲快进之前。
|
|
1177
|
+
// 落定卡的僵尸留档无活跃 rec,reconcile(只在飞卡)/reapGhostRecs(只清内存表项)都管不到它,
|
|
1178
|
+
// 而它若留在 running 会卡死 archive;安静板(只有已落定卡)会走空闲快进早退,清扫必须排在其前才能兜到。
|
|
1179
|
+
try { await sweepZombieRuns(sid, snap) } catch (e) { console.error('[task-board] 僵尸留档清扫失败:', String(e)) }
|
|
1115
1180
|
// 幽灵占位回收(池冻结根修):必须排在活跃度计数与 pickDispatch 之前——残留表项正是把 capW 拉到 0、
|
|
1116
1181
|
// 并把卡挡在 Verifier 派发之外的元凶(见 reapGhostRecs 头注释)。收尾会写盘(结局/usage),
|
|
1117
1182
|
// 故有回收时重读一次快照——否则下面空闲快进的 stalePool/staleInfo 判定会拿旧快照误判(写盘已归一进锁)。
|