dsh-agent-board 1.7.4 → 1.7.6
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 +39 -14
- package/index.mjs +9 -1
- package/lib/client/board-list.js +37 -26
- package/lib/client/dashboard.js +407 -67
- package/lib/client/kernel.js +23 -10
- package/lib/client/task-detail.js +21 -2
- package/lib/client.js +488 -105
- package/lib/core.mjs +202 -8
- package/lib/dispatch.mjs +54 -7
- package/lib/health.mjs +68 -1
- package/lib/rpc.mjs +86 -16
- package/lib/usage.mjs +608 -0
- package/package.json +2 -2
- package/test/core.test.mjs +648 -70
- package/test/headless-smoke.test.mjs +209 -0
- package/test/helpers/mock-ctx.mjs +327 -0
- package/test/integration.test.mjs +543 -0
- package/test/quality-alert-digest.test.mjs +128 -0
- package/test/runtime-health.test.mjs +158 -0
- package/test/scoreboard-client.test.mjs +106 -0
- package/test/scoreboard-trends-client.test.mjs +104 -0
- package/test/scoreboard.test.mjs +271 -0
- package/test/scorecard-clusters.test.mjs +225 -0
- package/test/scorecard-data.test.mjs +214 -0
- package/test/usage-mainwindow.test.mjs +7 -5
- package/test/user-guide-client.test.mjs +79 -0
package/lib/client.js
CHANGED
|
@@ -231,8 +231,18 @@ function apply(ctx) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
|
|
234
|
+
// 六列列头 title + 空列引导(巡检 n-muyg4fl8tm6r 六列无说明 / n-muygwnxpwcsq 空列零引导):
|
|
235
|
+
// 黑话→人话(title 悬浮补一句),空列给下一步动作指引(灰字)。
|
|
236
|
+
var COLUMN_META = {
|
|
237
|
+
draft: { title: '草稿:已保存未发布(补完依赖/上下文后统一发布)', empty: '暂无草稿——「+ 新建任务」勾选存为草稿后会出现在这里' },
|
|
238
|
+
pending: { title: '待办:已发布待派发(等 Worker 领取,或等依赖/文件锁释放)', empty: '暂无待办——已发布待派发的任务会出现在这里' },
|
|
239
|
+
'in-progress': { title: '进行中:Worker 子代理正在执行', empty: '暂无执行中——Worker 领取任务后会出现在这里' },
|
|
240
|
+
verifying: { title: '验证中:Worker 已完成,独立 Verifier 复跑验收中', empty: '暂无卡片——Worker 提交验收后会出现在这里' },
|
|
241
|
+
resolved: { title: '已完成:Verifier 验收通过', empty: '暂无已完成——验收通过后会出现在这里' },
|
|
242
|
+
blocked: { title: '阻塞:卡住待处理(等依赖/等主窗口裁决)', empty: '暂无阻塞——任务卡住待裁决时会出现在这里' }
|
|
243
|
+
}
|
|
234
244
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
235
|
-
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, lessonPushed: {}, isRoot: true, isRootEverTrue: false, isRootFalseN: 0, isRootStable: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', selectMode: false, selected: {}, undoSnapshot: null, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', usageSummary: null, childStats: {}, tasksErr: '', tasksHash: '' }
|
|
245
|
+
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, verifyUserGuide: true, lessonPushed: {}, isRoot: true, isRootEverTrue: false, isRootFalseN: 0, isRootStable: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', selectMode: false, selected: {}, undoSnapshot: null, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', mpRole: '', mpSize: '', usageSummary: null, childStats: {}, tasksErr: '', tasksHash: '' }
|
|
236
246
|
|
|
237
247
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
238
248
|
try {
|
|
@@ -298,11 +308,11 @@ function apply(ctx) {
|
|
|
298
308
|
}
|
|
299
309
|
|
|
300
310
|
// ===== 设置开关权威纠偏(反馈:勾选几秒才同步,task-muw5uudk)=====
|
|
301
|
-
//
|
|
311
|
+
// 开关字段(学习反馈/派发回执/完成回执/史诗拆分/自测指南)在 get-tasks 里**无条件**照常赋值,但取值口径是
|
|
302
312
|
// 「缺字段/脏值=开,只有显式 false 才关」,与 host core.cfg 同口径——单点定义,避免纠偏把脏值当真值。
|
|
303
313
|
function cfgKnobOf(src, key) { return !(src && src[key] === false) }
|
|
304
314
|
// 变更检测:与 notify 同类的轻量浅比较(只看这几个布尔开关;settings 无嵌套对象)。
|
|
305
|
-
function cfgKnobsChanged(a, b) { return a.feedbackEnabled !== b.feedbackEnabled || a.notifyDispatch !== b.notifyDispatch || a.notifyDone !== b.notifyDone || a.epicSplit !== b.epicSplit }
|
|
315
|
+
function cfgKnobsChanged(a, b) { return a.feedbackEnabled !== b.feedbackEnabled || a.notifyDispatch !== b.notifyDispatch || a.notifyDone !== b.notifyDone || a.epicSplit !== b.epicSplit || a.verifyUserGuide !== b.verifyUserGuide }
|
|
306
316
|
|
|
307
317
|
function fetchTasks() {
|
|
308
318
|
if (!state.sessionId) return
|
|
@@ -333,7 +343,7 @@ function apply(ctx) {
|
|
|
333
343
|
// → 勾选框必须等下一次任意 notify 才翻面(安静板卡数秒)。这里在赋值前快照、赋值后比对,
|
|
334
344
|
// 有变化就补一次 notify:乐观更新(PoolCfgPopover)已让点击瞬时翻面,本兜底是服务端权威值纠偏
|
|
335
345
|
// (乐观值与服务端不一致时以服务端为准,失败回滚亦由此收敛)。
|
|
336
|
-
var cfgKnobs = { feedbackEnabled: state.feedbackEnabled, notifyDispatch: state.notifyDispatch, notifyDone: state.notifyDone, epicSplit: state.epicSplit }
|
|
346
|
+
var cfgKnobs = { feedbackEnabled: state.feedbackEnabled, notifyDispatch: state.notifyDispatch, notifyDone: state.notifyDone, epicSplit: state.epicSplit, verifyUserGuide: state.verifyUserGuide }
|
|
337
347
|
if (tasksChanged) {
|
|
338
348
|
state.tasksHash = newHash
|
|
339
349
|
state.tasks = (d && d.tasks) || []
|
|
@@ -377,6 +387,8 @@ function apply(ctx) {
|
|
|
377
387
|
state.notifyDone = cfgKnobOf(d, 'notifyDone')
|
|
378
388
|
// 史诗拆分总开关(设置区「功能」):同上——老 host 不返回 = 开(引导照旧),只有显式 false 才关
|
|
379
389
|
state.epicSplit = cfgKnobOf(d, 'epicSplit')
|
|
390
|
+
// 自测指南开关(设置区「验收」):同上——缺字段=开(详情块/报告清单段照常),只有显式 false 才关
|
|
391
|
+
state.verifyUserGuide = cfgKnobOf(d, 'verifyUserGuide')
|
|
380
392
|
// 开关有变 / Token 区聚合有变 → 补一次 notify(tasksChanged 分支已在上面 notify 过,
|
|
381
393
|
// 这里只管 hash 不变时被跳过的那两次:开关乐观更新纠偏 + 统计范围切换后的新聚合)
|
|
382
394
|
var cfgDelta = !tasksChanged && cfgKnobsChanged(cfgKnobs, state)
|
|
@@ -517,7 +529,7 @@ function apply(ctx) {
|
|
|
517
529
|
// #18 依赖未满足判断(依赖不存在视为阻塞——创建时已校验,防御性兜底)
|
|
518
530
|
function depsBlocked(t) { if (!Array.isArray(t.dependsOn) || t.dependsOn.length === 0) return false; return t.dependsOn.some(function (id) { var d = getTask(id); return !d || (d.status !== 'resolved' && d.status !== 'archived') }) }
|
|
519
531
|
// #19 管线档位元数据
|
|
520
|
-
var pipeMeta = { full: { icon: 'flask-conical', label: '
|
|
532
|
+
var pipeMeta = { full: { icon: 'flask-conical', label: '全流程:Worker 执行 + Verifier 独立验收', short: '全流程' }, work: { icon: 'file-text', label: '免验证:Worker 只做,不验收', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直办:不派 Worker,由当前对话直接处理', short: '直办' } }
|
|
521
533
|
// ===== 耗时口径三分离(task-mutdnitw):排队 ≠ 执行 ≠ 验收,不再把排队算进耗时 =====
|
|
522
534
|
// waitOf:⏳ 排队时长 = createdAt → claimedAt(未领取则统计到 now)——"等了多久"
|
|
523
535
|
// execOf:⏱ 执行时长 = claimedAt → resolvedAt/archivedAt(未定则统计到 now)——"干了多久"
|
|
@@ -557,7 +569,7 @@ function apply(ctx) {
|
|
|
557
569
|
return React.createElement('button', { onClick: function () { state.open = !state.open; notify() }, title: '智能看板' + (pendingCount > 0 ? '(' + pendingCount + ' 待办)' : '') + (escCount > 0 ? '(' + escCount + ' 待裁决)' : ''), style: { display: 'inline-flex', alignItems: 'center', gap: 4, padding: '3px 10px', border: '1px solid ' + (escCount > 0 ? C.err : C.border), borderRadius: 6, background: isOpen ? C.nested : 'transparent', color: C.text, cursor: 'pointer', fontSize: 12 } }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center' } }, ic('clipboard-list', 14)), React.createElement('span', null, '智能看板'), escCount > 0 ? React.createElement('span', { style: { minWidth: 16, height: 16, padding: '0 4px', borderRadius: 8, background: C.err, color: C_INV, fontSize: 10, fontWeight: 700, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 1, animation: 'tskb-pulse 1s ease-in-out infinite' }, title: escCount + ' 个任务待裁决' }, ic('alert-triangle', 10), escCount) : null, pendingCount > 0 ? React.createElement('span', { style: { minWidth: 16, height: 16, padding: '0 4px', borderRadius: 8, background: C.brand, color: C_INV, fontSize: 10, fontWeight: 600, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' } }, String(pendingCount)) : null)
|
|
558
570
|
}
|
|
559
571
|
|
|
560
|
-
function ViewTab() { var btnBase = { fontSize: 11, padding: '3px 10px', border: 'none', cursor: 'pointer', fontWeight: 500, borderRadius: 5, transition: 'all .15s' }; function goArch() { state.view = 'archive'; notify(); fetchArchived() } return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 6, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { state.view = 'board'; notify() }, style: Object.assign({}, btnBase, state.view === 'board' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('clipboard-list', 12), '看板')), React.createElement('button', { onClick: function () { state.view = 'team'; notify() }, style: Object.assign({}, btnBase, state.view === 'team' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('users', 12, '
|
|
572
|
+
function ViewTab() { var btnBase = { fontSize: 11, padding: '3px 10px', border: 'none', cursor: 'pointer', fontWeight: 500, borderRadius: 5, transition: 'all .15s' }; function goArch() { state.view = 'archive'; notify(); fetchArchived() } return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 6, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { state.view = 'board'; notify() }, style: Object.assign({}, btnBase, state.view === 'board' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('clipboard-list', 12), '看板')), React.createElement('button', { onClick: function () { state.view = 'team'; notify() }, title: 'Worker / Verifier 执行池(一次性子代理,随任务结算销毁)', style: Object.assign({}, btnBase, state.view === 'team' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('users', 12, '执行池')), React.createElement('button', { onClick: function () { state.view = 'dashboard'; notify() }, style: Object.assign({}, btnBase, state.view === 'dashboard' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('bar-chart-3', 12, '仪表盘')), React.createElement('button', { onClick: goArch, title: '已归档任务(可检索、可恢复)', style: Object.assign({}, btnBase, state.view === 'archive' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('archive', 12, '归档'))) }
|
|
561
573
|
|
|
562
574
|
function TopPanel() {
|
|
563
575
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
@@ -571,7 +583,8 @@ function apply(ctx) {
|
|
|
571
583
|
var _fbo = useState(state.feedbackEnabled), fbEnabled = _fbo[0], setFbEnabled = _fbo[1]
|
|
572
584
|
var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
|
|
573
585
|
var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
|
|
574
|
-
|
|
586
|
+
var _vug = useState(state.verifyUserGuide), vugOn = _vug[0], setVugOn = _vug[1] // 自测指南开关勾选态(设置区「验收」)
|
|
587
|
+
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) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
575
588
|
if (!open) return null
|
|
576
589
|
if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
|
|
577
590
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -584,20 +597,20 @@ function apply(ctx) {
|
|
|
584
597
|
else if (detailId) { content = React.createElement(DetailView) }
|
|
585
598
|
else {
|
|
586
599
|
var prioRank = { critical: 4, high: 3, medium: 2, low: 1 }
|
|
587
|
-
var cols = COLUMNS.map(function (st) { var colTasks = active.filter(function (t) { return t.status === st }); colTasks.sort(function (a, b) { var e = (b.escalation ? 1 : 0) - (a.escalation ? 1 : 0); if (e) return e; var sk = (b.stuckSince ? 1 : 0) - (a.stuckSince ? 1 : 0); if (sk) return sk; var db = (depsBlocked(a) ? 1 : 0) - (depsBlocked(b) ? 1 : 0); if (db) return db; var p = (prioRank[b.priority] || 2) - (prioRank[a.priority] || 2); if (p) return p; return (a.createdAt || '').localeCompare(b.createdAt || '') }); var isOver = dragOver === st; return React.createElement('div', { key: st, onDragOver: function (e) { onColDragOver(e, st) }, onDragLeave: function () { onColDragLeave(st) }, onDrop: function (e) { onColDrop(e, st) }, style: { flex: '1 1 0', minWidth: 150, borderRadius: 8, padding: 6, background: isOver ? C.nested : 'transparent', border: isOver ? '1px dashed ' + C.brand : '1px dashed transparent', transition: 'background .15s, border .15s' } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4, padding: '0 2px' } }, (statusLabels[st] || st) + ' (' + colTasks.length + ')'), colTasks.map(function (t) { return React.createElement(Card, { key: t.id, task: t }) })) })
|
|
600
|
+
var cols = COLUMNS.map(function (st) { var colTasks = active.filter(function (t) { return t.status === st }); colTasks.sort(function (a, b) { var e = (b.escalation ? 1 : 0) - (a.escalation ? 1 : 0); if (e) return e; var sk = (b.stuckSince ? 1 : 0) - (a.stuckSince ? 1 : 0); if (sk) return sk; var db = (depsBlocked(a) ? 1 : 0) - (depsBlocked(b) ? 1 : 0); if (db) return db; var p = (prioRank[b.priority] || 2) - (prioRank[a.priority] || 2); if (p) return p; return (a.createdAt || '').localeCompare(b.createdAt || '') }); var isOver = dragOver === st; return React.createElement('div', { key: st, onDragOver: function (e) { onColDragOver(e, st) }, onDragLeave: function () { onColDragLeave(st) }, onDrop: function (e) { onColDrop(e, st) }, style: { flex: '1 1 0', minWidth: 150, borderRadius: 8, padding: 6, background: isOver ? C.nested : 'transparent', border: isOver ? '1px dashed ' + C.brand : '1px dashed transparent', transition: 'background .15s, border .15s' } }, React.createElement('div', { title: (COLUMN_META[st] || {}).title, style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4, padding: '0 2px', cursor: 'help' } }, (statusLabels[st] || st) + ' (' + colTasks.length + ')'), colTasks.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2, opacity: 0.7, padding: '2px 6px 8px', textAlign: 'center' } }, (COLUMN_META[st] || {}).empty) : colTasks.map(function (t) { return React.createElement(Card, { key: t.id, task: t }) })) })
|
|
588
601
|
// 归档区:排序选择器(默认归档时间降序)+ 纵向滚动列表(不再横向拉条)
|
|
589
602
|
var archSorted = archived.slice().sort(function (a, b) {
|
|
590
603
|
if (state.archSort === 'title') return (a.title || '').localeCompare(b.title || '')
|
|
591
604
|
var ta = a.archivedAt || a.resolvedAt || a.createdAt || '', tb = b.archivedAt || b.resolvedAt || b.createdAt || ''
|
|
592
605
|
return state.archSort === 'time-asc' ? ta.localeCompare(tb) : tb.localeCompare(ta)
|
|
593
606
|
})
|
|
594
|
-
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 } }, '「+ 新建任务」'), '
|
|
607
|
+
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))
|
|
595
608
|
}
|
|
596
609
|
return React.createElement('div', { style: { position: 'fixed', top: 44, left: (layL + 8) + 'px', right: (layR + 8) + 'px', maxHeight: '60vh', zIndex: 900, background: C.bg, border: '1px solid ' + C.border, borderRadius: '0 0 10px 10px', boxShadow: '0 8px 24px rgba(0,0,0,0.12)', display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
597
610
|
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 } },
|
|
598
611
|
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)),
|
|
599
612
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
600
|
-
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 }),
|
|
613
|
+
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 }),
|
|
601
614
|
dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
|
|
602
615
|
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), '新建任务'),
|
|
603
616
|
createFlash ? React.createElement('span', { style: { fontSize: 10, color: C.ok } }, createFlash) : null,
|
|
@@ -623,13 +636,14 @@ function apply(ctx) {
|
|
|
623
636
|
// #14 批量操作条(#16 带一步撤销:快照操作前的 priority/status;delete 是真删,明确不支持撤销)
|
|
624
637
|
function BatchBar() {
|
|
625
638
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
626
|
-
var _a = useState(0), cnt = _a[0], setCnt = _a[1]; var _b = useState(false), on = _b[0], setOn = _b[1]; var _c = useState(''), msg = _c[0], setMsg = _c[1]; var _d = useState(null), undo = _d[0], setUndo = _d[1]
|
|
639
|
+
var _a = useState(0), cnt = _a[0], setCnt = _a[1]; var _b = useState(false), on = _b[0], setOn = _b[1]; var _c = useState(''), msg = _c[0], setMsg = _c[1]; var _d = useState(null), undo = _d[0], setUndo = _d[1]; var _e = useState([]), reasons = _e[0], setReasons = _e[1]
|
|
627
640
|
useEffect(function () { function update() { setCnt(Object.keys(state.selected).length); setOn(state.selectMode); setUndo(state.undoSnapshot) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
628
641
|
if (!on) return null
|
|
629
642
|
var ids = Object.keys(state.selected)
|
|
630
643
|
function snapshot(op) { return { op: op, at: Date.now(), items: ids.map(function (id) { var t = getTask(id); return t ? { id: id, priority: t.priority, status: t.status } : null }).filter(Boolean) } }
|
|
631
|
-
//
|
|
632
|
-
|
|
644
|
+
// 跳过原因逐条渲染:host batch-op 返回 reasons 对象(id → 原因),转成「标题:原因」列表
|
|
645
|
+
// (巡检 n-muyj5p755l3c:批量归档跳过此前只显示「跳过 N」无原因)
|
|
646
|
+
function reasonRows(r) { var m = (r && r.reasons) || {}; return Object.keys(m).map(function (id) { var t = getTask(id); return { title: t ? t.title : shortId(id), reason: String(m[id]) } }) }
|
|
633
647
|
function run(op, value) {
|
|
634
648
|
// 只有可撤销的 op 才需要快照(delete 是真删,快照里的 status/priority 复活不回整张卡)
|
|
635
649
|
var snap = op === 'delete' ? null : snapshot(op)
|
|
@@ -638,8 +652,8 @@ function apply(ctx) {
|
|
|
638
652
|
state.selected = {}
|
|
639
653
|
// 删除无 undo:done>0 也不留撤销快照(否则「↩️ 撤销」点了恢复不了,纯粹误导);其他 op 维持一步撤销
|
|
640
654
|
state.undoSnapshot = (r && r.done > 0 && op !== 'delete') ? snap : null
|
|
641
|
-
|
|
642
|
-
setMsg('✅ 已处理 ' + (r && r.done || 0) + ' 个' + (r && r.skipped && r.skipped.length ? ',跳过 ' + r.skipped.length +
|
|
655
|
+
setReasons(reasonRows(r))
|
|
656
|
+
setMsg('✅ 已处理 ' + (r && r.done || 0) + ' 个' + (r && r.skipped && r.skipped.length ? ',跳过 ' + r.skipped.length + ' 个' : ''))
|
|
643
657
|
fetchTasks()
|
|
644
658
|
}).catch(function (e) { setMsg('⚠️ ' + String(e)) })
|
|
645
659
|
}
|
|
@@ -651,7 +665,7 @@ function apply(ctx) {
|
|
|
651
665
|
}
|
|
652
666
|
function doUndo() { if (!undo) return; rpc('batch-undo', { snapshot: undo }).then(function (r) { setMsg('↩️ 已撤销 ' + (r && r.done || 0) + ' 个'); state.undoSnapshot = null; fetchTasks() }).catch(function (e) { setMsg('⚠️ ' + String(e)) }) }
|
|
653
667
|
var btn = { fontSize: 10, padding: '3px 10px', borderRadius: 4, border: 'none', cursor: 'pointer', fontWeight: 600 }
|
|
654
|
-
return React.createElement('div', { style: { position: 'sticky', bottom: 0, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 8px', marginTop: 8, background: C.nested, border: '1px solid ' + C.border, borderRadius: 6 } },
|
|
668
|
+
return React.createElement('div', { style: { position: 'sticky', bottom: 0, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 8px', marginTop: 8, background: C.nested, border: '1px solid ' + C.border, borderRadius: 6, flexWrap: 'wrap' } },
|
|
655
669
|
React.createElement('span', { style: { fontSize: 11, color: C.text, fontWeight: 600 } }, '已选 ' + cnt + ' 项'),
|
|
656
670
|
React.createElement('button', { onClick: function () { run('archive') }, disabled: cnt === 0, style: Object.assign({}, btn, { background: C.text2, color: C_INV, display: 'inline-flex', alignItems: 'center', gap: 3 }) }, ic('archive', 11), '批量归档'),
|
|
657
671
|
['critical', 'high', 'medium', 'low'].map(function (p) { return React.createElement('button', { key: p, onClick: function () { run('set-priority', p) }, disabled: cnt === 0, style: Object.assign({}, btn, { background: prioColor[p], color: C_INV }) }, prioLabel[p]) }),
|
|
@@ -659,21 +673,26 @@ function apply(ctx) {
|
|
|
659
673
|
React.createElement('button', { onClick: batchDelete, disabled: cnt === 0, title: '删除选中任务(不可恢复;执行中/验证中的需先终止,已落定请用归档)', style: Object.assign({}, btn, { background: C.err, color: C_INV, display: 'inline-flex', alignItems: 'center', gap: 3 }) }, ic('trash-2', 11), '批量删除'),
|
|
660
674
|
undo ? React.createElement('button', { onClick: doUndo, title: '撤销最近一次批量操作(删除不可撤销)', style: Object.assign({}, btn, { background: C.brand, color: C_INV }) }, '↩️ 撤销') : null,
|
|
661
675
|
msg ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, msg) : null,
|
|
662
|
-
React.createElement('button', { onClick: function () { state.selected = {}; notify() }, style: { marginLeft: 'auto', fontSize: 10, padding: '3px 8px', border: 'none', background: 'transparent', color: C.text2, cursor: 'pointer' } }, '清除选择')
|
|
676
|
+
React.createElement('button', { onClick: function () { state.selected = {}; notify() }, style: { marginLeft: 'auto', fontSize: 10, padding: '3px 8px', border: 'none', background: 'transparent', color: C.text2, cursor: 'pointer' } }, '清除选择'),
|
|
677
|
+
reasons.length > 0 ? React.createElement('div', { style: { flexBasis: '100%', marginTop: 4, padding: '4px 6px', background: C.card, border: '1px solid ' + C.border, borderRadius: 4, maxHeight: 96, overflowY: 'auto' } },
|
|
678
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2 } }, '跳过原因'),
|
|
679
|
+
reasons.map(function (it, i) { return React.createElement('div', { key: i, style: { fontSize: 10, color: C.text2, marginBottom: 1, wordBreak: 'break-word' } }, it.title + ':' + it.reason) })) : null)
|
|
663
680
|
}
|
|
664
681
|
|
|
665
682
|
function FilterBar() {
|
|
666
683
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
667
|
-
|
|
668
|
-
|
|
684
|
+
// 优先级筛选单一事实源:chip 高亮与「清除」显隐统一读全局 state.filterPrio(消灭本地 fp 副本),
|
|
685
|
+
// 曾因本地态与全局 filterPrio 不同帧更新出现「清除按钮消失但高亮残留」的矛盾画面(巡检 n-muyimk1hpdxv)
|
|
686
|
+
var _a = useState(state.filterQ), q = _a[0], setQ = _a[1]; var _c = useState(state.filterTag), ft = _c[0], setFt = _c[1]
|
|
687
|
+
useEffect(function () { function update() { setQ(state.filterQ); setFt(state.filterTag) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
669
688
|
function setQ2(v) { state.filterQ = v; notify() }
|
|
670
689
|
function togglePrio(p) { var i = state.filterPrio.indexOf(p); if (i >= 0) state.filterPrio.splice(i, 1); else state.filterPrio.push(p); notify() }
|
|
671
690
|
function clearAll() { state.filterQ = ''; state.filterPrio = []; state.filterTag = ''; notify() }
|
|
672
|
-
var hasFilter = q.trim() ||
|
|
691
|
+
var hasFilter = q.trim() || state.filterPrio.length > 0 || ft
|
|
673
692
|
var chipBase = { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: '1px solid ' + C.border, cursor: 'pointer', background: 'transparent', color: C.text2, transition: 'all .15s' }
|
|
674
693
|
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
|
|
675
694
|
React.createElement('input', { value: q, onChange: function (e) { setQ2(e.target.value) }, placeholder: '🔍 搜索标题/描述/ID…', style: { flex: '0 1 200px', fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text } }),
|
|
676
|
-
['critical', 'high', 'medium', 'low'].map(function (p) { var on =
|
|
695
|
+
['critical', 'high', 'medium', 'low'].map(function (p) { var on = state.filterPrio.indexOf(p) >= 0; return React.createElement('button', { key: p, onClick: function () { togglePrio(p) }, style: Object.assign({}, chipBase, on ? { background: prioColor[p], color: C_INV, borderColor: prioColor[p] } : {}) }, prioLabel[p]) }),
|
|
677
696
|
allTags().length > 0 ? React.createElement('select', { value: ft, onChange: function (e) { state.filterTag = e.target.value; notify() }, style: { fontSize: 10, padding: '2px 4px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text } }, React.createElement('option', { value: '' }, '🏷 全部标签'), allTags().map(function (g) { return React.createElement('option', { key: g, value: g }, g) })) : null,
|
|
678
697
|
hasFilter ? React.createElement('button', { onClick: clearAll, style: { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: 'none', background: C.nested, color: C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('x', 9), '清除') : null)
|
|
679
698
|
}
|
|
@@ -778,8 +797,8 @@ function apply(ctx) {
|
|
|
778
797
|
(Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
|
|
779
798
|
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
780
799
|
(t.retryCount || 0) + (t.rejectCount || 0) > 0 ? React.createElement('span', { title: '重试 ' + (t.retryCount || 0) + ' 次 / 驳回 ' + (t.rejectCount || 0) + ' 次', style: { color: C.warn, fontWeight: 600 } }, '⟳' + ((t.retryCount || 0) + (t.rejectCount || 0))) : null,
|
|
781
|
-
t.status === 'in-progress' && t.claimedBy ? React.createElement('span',
|
|
782
|
-
t.assignee ? React.createElement('span',
|
|
800
|
+
t.status === 'in-progress' && t.claimedBy ? React.createElement('span', { title: '执行该任务的子代理会话:' + t.claimedBy }, '⚡ ' + String(t.claimedBy).slice(0, 8) + '…') : null,
|
|
801
|
+
t.assignee ? React.createElement('span', { title: '指派给:' + t.assignee }, '👤→' + String(t.assignee).slice(0, 8) + '…') : null,
|
|
783
802
|
durB ? React.createElement('span', { title: durB.tip }, durB.txt) : null,
|
|
784
803
|
// 删除入口(仅草稿/待办/阻塞,多选模式下隐藏以免误触):hover 才由透明转红,平时不抢视觉
|
|
785
804
|
delOk ? React.createElement('span', {
|
|
@@ -798,22 +817,27 @@ function apply(ctx) {
|
|
|
798
817
|
t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null, depBlock ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '依赖全部完成后才会派发' }, '⛓ 等待「' + depWaitTitle + '」' + (depWaitN > 1 ? ' 等 ' + depWaitN + ' 个' : '')) : null) }
|
|
799
818
|
|
|
800
819
|
function ArchiveView() {
|
|
801
|
-
var _R = React; var useState = _R.useState
|
|
820
|
+
var _R = React; var useState = _R.useState
|
|
802
821
|
var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
|
|
803
|
-
|
|
804
|
-
|
|
822
|
+
// 归档数据源口径对齐仪表盘(反馈 n-muyg3e0m9z7i):直接读 state.tasks(get-tasks 全量含归档),
|
|
823
|
+
// 不再依赖独立 state.archived/fetchArchived——旧路径异步填库却未接入 TopPanel 的 notify 更新项,
|
|
824
|
+
// 归档 tab 打开后停在「暂无归档任务」空态,而仪表盘「已归档」计数来自 state.tasks(全量),二者对不上。
|
|
825
|
+
// 全量 + 按归档时间倒序(archivedAt 优先,回退 resolvedAt/createdAt)+ 顶部注明条数。
|
|
826
|
+
var archived = state.tasks.filter(function (t) { return t.status === 'archived' })
|
|
827
|
+
var list = archived.filter(function (t) {
|
|
805
828
|
if (!q) return true
|
|
806
829
|
var qq = q.toLowerCase()
|
|
807
830
|
return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
|
|
808
831
|
}).sort(function (a, b) {
|
|
809
|
-
//
|
|
810
|
-
function
|
|
811
|
-
return (
|
|
832
|
+
// 归档时间倒序:archivedAt 优先,回退 resolvedAt/createdAt(老卡可能缺 archivedAt)
|
|
833
|
+
function archTs(t) { return t.archivedAt || t.resolvedAt || t.createdAt || '' }
|
|
834
|
+
return (archTs(b) || '').localeCompare(archTs(a) || '')
|
|
812
835
|
})
|
|
813
|
-
function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () {
|
|
836
|
+
function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchTasks() }).catch(function () {}) }
|
|
814
837
|
return React.createElement('div', null,
|
|
815
838
|
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 } }),
|
|
816
|
-
|
|
839
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '共 ' + archived.length + ' 条归档(全量,与仪表盘「已归档」同口径)'),
|
|
840
|
+
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
|
|
817
841
|
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
818
842
|
list.map(function (t) {
|
|
819
843
|
return React.createElement('div', { key: t.id, style: { padding: '6px 8px', marginBottom: 5, background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } },
|
|
@@ -884,7 +908,7 @@ function apply(ctx) {
|
|
|
884
908
|
}
|
|
885
909
|
var inp = { width: '100%', boxSizing: 'border-box', fontSize: 11, padding: '4px 6px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit' }
|
|
886
910
|
var lblStyle = { fontSize: 10, color: C.text2, marginBottom: 3 }
|
|
887
|
-
function field(label, node) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node) }
|
|
911
|
+
function field(label, node, hint) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node, hint ? React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.4 } }, hint) : null) }
|
|
888
912
|
var btnGhost = { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 5, background: 'transparent', color: C.text2, cursor: 'pointer' }
|
|
889
913
|
var btnPrimary = { fontSize: 11, padding: '4px 14px', border: 'none', borderRadius: 5, background: C.brand, color: C_INV, fontWeight: 600, cursor: busy ? 'default' : 'pointer', opacity: busy ? 0.6 : 1 }
|
|
890
914
|
// 视口级 overlay(修裁切,反馈 n-mut9rzl4mxe4):fixed 全屏遮罩脱离看板抽屉(maxHeight 60vh + overflow hidden)
|
|
@@ -904,8 +928,8 @@ function apply(ctx) {
|
|
|
904
928
|
React.createElement('div', { style: { flex: '1 1 0', minWidth: 0 } }, field('管线', React.createElement('select', { value: pipe, onChange: function (e) { setPipe(e.target.value) }, style: inp },
|
|
905
929
|
React.createElement('option', { value: 'full' }, '全流程(执行+验证)'),
|
|
906
930
|
React.createElement('option', { value: 'work' }, '免验证(只做不验)'),
|
|
907
|
-
React.createElement('option', { value: 'direct' }, '主窗口处理'))))),
|
|
908
|
-
field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp })),
|
|
931
|
+
React.createElement('option', { value: 'direct' }, '主窗口处理')), '管线 = 任务走哪种流程:全流程 = Worker 做 + Verifier 验;免验证 = 只做不验;主窗口处理 = 你在对话里直接办'))),
|
|
932
|
+
field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp }), 'touches = 本卡要改动的文件(glob 匹配);声明后并行任务间防互踩(文件锁)'),
|
|
909
933
|
field('依赖(全部完成后才派发,可空)', cands.length === 0
|
|
910
934
|
? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '当前无草稿/待办任务可选')
|
|
911
935
|
: React.createElement('div', { style: { maxHeight: 96, overflowY: 'auto', border: '1px solid ' + C.border, borderRadius: 4, padding: '4px 6px', background: C.card } }, cands.map(function (x) {
|
|
@@ -913,7 +937,7 @@ function apply(ctx) {
|
|
|
913
937
|
React.createElement('input', { type: 'checkbox', checked: !!depSel[x.id], onChange: function () { toggleDep(x.id) } }),
|
|
914
938
|
React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, (statusLabels[x.status] || x.status) + ' · ' + (x.title || x.id)))
|
|
915
939
|
}))),
|
|
916
|
-
field('验收脚本(命令,可空)', React.createElement('input', { value: acc, onChange: function (e) { setAcc(e.target.value) }, placeholder: '如 node --test test/x.test.js', style: Object.assign({}, inp, { fontFamily: 'monospace' }) })),
|
|
940
|
+
field('验收脚本(命令,可空)', React.createElement('input', { value: acc, onChange: function (e) { setAcc(e.target.value) }, placeholder: '如 node --test test/x.test.js', style: Object.assign({}, inp, { fontFamily: 'monospace' }) }), '验收脚本 = 硬性验收命令:Worker 必须真实运行,Verifier 独立复跑'),
|
|
917
941
|
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: C.text, cursor: 'pointer', marginBottom: 6, flexWrap: 'wrap' } },
|
|
918
942
|
React.createElement('input', { type: 'checkbox', checked: asDraft, onChange: function (e) { setAsDraft(e.target.checked) } }),
|
|
919
943
|
'存为草稿',
|
|
@@ -925,7 +949,7 @@ function apply(ctx) {
|
|
|
925
949
|
err ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 6 } }, err) : null,
|
|
926
950
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
|
|
927
951
|
React.createElement('button', { onClick: close, style: btnGhost }, '取消'),
|
|
928
|
-
React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '
|
|
952
|
+
React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即交给 Worker 执行', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并交给 Worker'))))))
|
|
929
953
|
}
|
|
930
954
|
|
|
931
955
|
// ============================================================================
|
|
@@ -1220,11 +1244,16 @@ function apply(ctx) {
|
|
|
1220
1244
|
React.createElement('div', { style: { fontSize: 10, display: 'flex', flexWrap: 'wrap', gap: 5 } }, task.waitingForTouches.map(function (id) { var t2 = getTask(id); return React.createElement('span', { key: id, onClick: function () { if (t2) { state.detailId = id; notify() } }, title: t2 ? (t2.title + ' (' + id + ')') : id, style: { padding: '1px 5px', borderRadius: 3, background: C.nested, border: '1px solid ' + C.border, color: t2 ? C.brand : C.text2, cursor: t2 ? 'pointer' : 'default' } }, (t2 ? t2.title.slice(0, 20) : shortId(id)) + ' · ' + shortId(id)) })),
|
|
1221
1245
|
Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 4 } }, '本任务 touches: ' + task.touches.join('、')) : null) : null,
|
|
1222
1246
|
Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '🔒 文件级排他(touches): ' + task.touches.join('、')) : null,
|
|
1247
|
+
// 起草 lint(task.lintWarnings[],host 建卡/更新卡现算落盘,get-tasks 透出):
|
|
1248
|
+
// 四规则软警告(粒度过粗/双仓库歧义/缺验收/标题缺动词或描述空)随卡展示,缺字段/空数组整块不渲染(老卡零影响)。
|
|
1249
|
+
Array.isArray(task.lintWarnings) && task.lintWarnings.length ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
|
|
1250
|
+
task.lintWarnings.map(function (w, i) { return React.createElement('div', { key: i, style: { fontSize: 10, color: C.text2, lineHeight: 1.6, display: 'flex', alignItems: 'flex-start', gap: 4 } }, React.createElement('span', { style: { flexShrink: 0 } }, '⚠️'), React.createElement('span', null, w)) })) : null,
|
|
1223
1251
|
(task.status === 'in-progress' || task.status === 'verifying') && state.activity[task.id] ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginBottom: 8, padding: '4px 8px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'center', gap: 4, animation: 'tskb-pulse 2s ease-in-out infinite' } }, ic('activity', 10), React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '当前动作: ' + state.activity[task.id])) : null,
|
|
1224
1252
|
task.status === 'in-progress' && task.claimedBy ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 8, padding: '5px 8px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
1225
1253
|
ic('activity', 10),
|
|
1226
1254
|
React.createElement('span', null, '已运行 ' + elapsedSince(task.claimedAt) + '(软超时 ' + state.softTimeoutMin + ' 分提醒 · 硬超时 ' + state.hardTimeoutMin + ' 分自动终止)'),
|
|
1227
1255
|
elapsedMin(task.claimedAt) > state.softTimeoutMin ? React.createElement('span', { style: { color: C.warn, fontWeight: 600 } }, '⏱ 已超软超时') : null,
|
|
1256
|
+
React.createElement('span', { title: '当前活跃 run 的子会话 id(continuable 即 childId);终止前先到子代理列表核对该 id 的同名条目,避免误杀别的合法 Worker', style: { fontSize: 10, color: C.text2, fontFamily: 'monospace', wordBreak: 'break-all', flexBasis: '100%' } }, '子会话 id: ' + task.claimedBy + '(对应子代理列表同名条目)'),
|
|
1228
1257
|
React.createElement('button', { onClick: doTerminate, title: '终止该执行 Agent,任务回 pending 重新排队', style: { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.err, borderRadius: 3, background: 'transparent', color: C.err, cursor: 'pointer', marginLeft: 'auto' } }, '⏹ 立即终止')) : null,
|
|
1229
1258
|
task.escalation ? React.createElement('div', { id: 'tskb-escalation', style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)' } },
|
|
1230
1259
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), 'Worker 上报歧义 — 等待主窗口裁决'),
|
|
@@ -1282,11 +1311,25 @@ function apply(ctx) {
|
|
|
1282
1311
|
task.verification ? React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + (task.verification.verdict === 'approved' ? C.ok : C.err), borderRadius: 6, background: C.card } },
|
|
1283
1312
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 700, color: task.verification.verdict === 'approved' ? C.ok : C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic(task.verification.verdict === 'approved' ? 'clipboard-check' : 'clipboard-x', 12), (task.verification.verdict === 'approved' ? '验收通过' : '验收驳回') + ' · ' + (task.verification.by || '') + ' · ' + ago(task.verification.at)),
|
|
1284
1313
|
React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 4, whiteSpace: 'pre-wrap' } }, task.verification.summary || '(无测试概要)'),
|
|
1285
|
-
task.verification.checks ? React.createElement('div', { style: { marginTop: 4 } }, React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2 } }, '核对项'), React.createElement('div', { style: { fontSize: 10, color: C.text2, whiteSpace: 'pre-wrap', maxHeight: 120, overflowY: 'auto' } }, task.verification.checks)) : null
|
|
1314
|
+
task.verification.checks ? React.createElement('div', { style: { marginTop: 4 } }, React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2 } }, '核对项'), React.createElement('div', { style: { fontSize: 10, color: C.text2, whiteSpace: 'pre-wrap', maxHeight: 120, overflowY: 'auto' } }, task.verification.checks)) : null,
|
|
1315
|
+
// ===== 自测指南块(task-muxyyvg0):双门禁——板级开关 verifyUserGuide 关 / 未挂 userTest 字段,
|
|
1316
|
+
// 任一整块不渲染。tier 徽章三档配色:ui 绿(界面可操作)/ metric 蓝(看指标变化)/ internal 灰(纯内部)。
|
|
1317
|
+
task.verification.userTest && state.verifyUserGuide !== false ? (function () {
|
|
1318
|
+
var ut = task.verification.userTest
|
|
1319
|
+
var tierMeta = ut.tier === 'ui' ? { c: C.ok, label: 'UI 可操作' } : (ut.tier === 'metric' ? { c: C.brand, label: '看指标' } : { c: C.text2, label: '纯内部' })
|
|
1320
|
+
return React.createElement('div', { style: { marginTop: 6, padding: '5px 8px', background: C.nested, borderRadius: 4 } },
|
|
1321
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 3, display: 'flex', alignItems: 'center', gap: 5 } },
|
|
1322
|
+
'📋 自测指南',
|
|
1323
|
+
React.createElement('span', { style: { fontSize: 9, padding: '0 5px', borderRadius: 3, background: tierMeta.c, color: C_INV, fontWeight: 700 }, title: 'tier=' + (ut.tier || 'internal') + '(ui=界面可操作 / metric=看指标变化 / internal=纯内部无用户可感知面)' }, tierMeta.label)),
|
|
1324
|
+
ut.gist ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 3 } }, ut.gist) : null,
|
|
1325
|
+
ut.steps && ut.steps.length ? React.createElement('div', { style: { fontSize: 10, color: C.text, marginBottom: 3 } }, ut.steps.map(function (s, i) { return React.createElement('div', { key: i, style: { marginBottom: 1 } }, (i + 1) + '. ' + s) })) : null,
|
|
1326
|
+
ut.expect ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '预期:' + ut.expect) : null)
|
|
1327
|
+
})() : null) : null,
|
|
1286
1328
|
React.createElement(MsgThread, { messages: task.messages, taskId: task.id }),
|
|
1287
1329
|
Array.isArray(task.history) && task.history.length > 0 ? React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 3 } }, '流转轨迹'), React.createElement('div', { style: { fontSize: 10, color: C.text2, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, task.history.map(function (h, i) { return React.createElement('div', { key: i, style: { marginBottom: 2 } }, React.createElement('span', { style: { color: C.brand } }, statusLabels[h.to] || h.to), ' · ' + ago(h.timestamp) + ' · ', React.createElement(ActorLink, { id: h.actor }), h.note ? ' · ' + h.note : '') }))) : null,
|
|
1288
1330
|
React.createElement('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap', marginTop: 6 } },
|
|
1289
|
-
|
|
1331
|
+
// 草稿态「保存并重置」实为 resetToPending 静默发布:按钮文案改「保存并发布」+ title 发布预警(巡检 n-muyi5yfec8m0)
|
|
1332
|
+
React.createElement('button', { onClick: saveEdit, disabled: saving, title: task.status === 'draft' ? '发布后按当前工作模式可能被立即派发' : '保存并重置为待办', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, saving ? '保存中…' : [ic('save', 10), task.status === 'draft' ? ' 保存并发布' : ' 保存并重置']),
|
|
1290
1333
|
task.status === 'draft' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('update-task', { taskId: task.id, publish: true }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('rocket', 10), '发布(进入派发池)') : null,
|
|
1291
1334
|
(task.status === 'pending' || task.status === 'blocked') ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('claim-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: 'pointer' } }, '领取') : null,
|
|
1292
1335
|
task.status === 'pending' ? React.createElement('button', { onClick: function () { doAction(function () { return doManualDispatch('worker') }) }, title: '手动派发给一次性 Worker 子代理', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.warn, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('zap', 10), '派发') : null,
|
|
@@ -1534,8 +1577,59 @@ function apply(ctx) {
|
|
|
1534
1577
|
})) : null)
|
|
1535
1578
|
}
|
|
1536
1579
|
|
|
1580
|
+
// ===== 每日批次摘要(近 24h,异常驱动审视③):报告导出「批次一页纸」段的数据源 =====
|
|
1581
|
+
// 纯聚合现有字段(不新建状态、不重拉):完成/驳回/token/超时按 run 与消息的 at 滚 24h 窗口现算,
|
|
1582
|
+
// 幽灵回收读当前 healthHints 里的一次性 info(「本轮回收 N 个幽灵活跃表项」,无则 0);
|
|
1583
|
+
// 一句话质量趋势按近 24h 落定的 resolved 任务一次通过率(rejectCount=0 占比)。
|
|
1584
|
+
// 范围无关:报告顶部的批次摘要永远反映「最近 24h」实况,不受「统计范围」筛选影响。
|
|
1585
|
+
function batchDigest() {
|
|
1586
|
+
var now = Date.now(), since = now - 86400000
|
|
1587
|
+
var all = (Array.isArray(state.tasks) ? state.tasks : []).concat(Array.isArray(state.archived) ? state.archived : [])
|
|
1588
|
+
var resolved = 0, rejected = 0, tokTot = 0, tokEff = 0, timeouts = 0, trendResolved = 0, trendFirstPass = 0
|
|
1589
|
+
function in24(iso) { var d = Date.parse(iso); return isFinite(d) && d >= since && d <= now }
|
|
1590
|
+
function pos(v) { var n = Number(v); return isFinite(n) && n > 0 ? n : 0 }
|
|
1591
|
+
all.forEach(function (t) {
|
|
1592
|
+
if (!t) return
|
|
1593
|
+
var doneAt = t.resolvedAt || t.verifiedAt || ''
|
|
1594
|
+
if (doneAt && in24(doneAt)) {
|
|
1595
|
+
resolved++
|
|
1596
|
+
if (t.status === 'resolved') { trendResolved++; if (!(t.rejectCount > 0)) trendFirstPass++ }
|
|
1597
|
+
}
|
|
1598
|
+
if (Array.isArray(t.messages)) t.messages.forEach(function (m) { if (m && m.kind === 'rejection' && m.at && in24(m.at)) rejected++ })
|
|
1599
|
+
if (Array.isArray(t.runs)) t.runs.forEach(function (r) {
|
|
1600
|
+
if (!r || !r.at || !in24(r.at)) return
|
|
1601
|
+
var u = (r.usage && typeof r.usage === 'object') ? r.usage : {}
|
|
1602
|
+
var inp = pos(u.input), outp = pos(u.output), cr = pos(u.cacheRead), cw = pos(u.cacheWrite)
|
|
1603
|
+
var tot = pos(u.total) || (inp + outp + cr + cw)
|
|
1604
|
+
tokTot += tot; tokEff += inp + outp + cw
|
|
1605
|
+
if (r.outcome === 'timeout/error' || r.outcome === 'timeout') timeouts++
|
|
1606
|
+
})
|
|
1607
|
+
})
|
|
1608
|
+
var ghosts = 0
|
|
1609
|
+
var hints = Array.isArray(state.healthHints) ? state.healthHints : []
|
|
1610
|
+
hints.forEach(function (h) { var m = h && h.text ? String(h.text).match(/本轮回收 (\d+) 个幽灵活跃表项/) : null; if (m) ghosts += pos(m[1]) })
|
|
1611
|
+
var trend = trendResolved > 0 ? ('一次通过率 ' + Math.round(trendFirstPass / trendResolved * 100) + '%(近 24h 完成 ' + trendResolved + ' 张中 ' + trendFirstPass + ' 张零驳回)') : '近 24h 无完成记录,暂无质量趋势'
|
|
1612
|
+
var has = resolved > 0 || rejected > 0 || tokTot > 0 || timeouts > 0 || ghosts > 0 || trendResolved > 0
|
|
1613
|
+
return { has: has, resolved: resolved, rejected: rejected, tokTot: tokTot, tokEff: tokEff, timeouts: timeouts, ghosts: ghosts, trend: trend }
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1537
1616
|
function buildReport() {
|
|
1538
1617
|
var lines = ['# 任务看板报告', '', '生成时间: ' + new Date().toLocaleString(), '工作模式: ' + (workModeNames[state.workMode || (state.teamMode ? 'team' : (state.boardMode === 'auto' ? 'auto' : 'list'))] || '自动派发'), '', '统计范围: ' + rangeLabel()]
|
|
1618
|
+
// ===== 每日批次摘要(异常驱动审视③):报告顶部一页纸,近 24h 五字段 + 一句话质量趋势 =====
|
|
1619
|
+
var dg = batchDigest()
|
|
1620
|
+
if (dg.has) {
|
|
1621
|
+
lines.push('## 批次摘要(近 24 小时)')
|
|
1622
|
+
lines.push('- 完成:' + dg.resolved + ' 张')
|
|
1623
|
+
lines.push('- 驳回:' + dg.rejected + ' 次')
|
|
1624
|
+
lines.push('- Token:约 ' + fmtTokens(dg.tokTot) + (dg.tokEff > 0 ? '(其中有效 ' + fmtTokens(dg.tokEff) + ')' : ''))
|
|
1625
|
+
lines.push('- 异常事件:超时 ' + dg.timeouts + ' 次 · 幽灵回收 ' + dg.ghosts + ' 次')
|
|
1626
|
+
lines.push('- 质量趋势:' + dg.trend)
|
|
1627
|
+
lines.push('')
|
|
1628
|
+
} else {
|
|
1629
|
+
lines.push('## 批次摘要(近 24 小时)')
|
|
1630
|
+
lines.push('近 24 小时无活动记录(无完成 / 驳回 / 消耗 / 异常事件)')
|
|
1631
|
+
lines.push('')
|
|
1632
|
+
}
|
|
1539
1633
|
var groups = { inProgress: [], verifying: [], pending: [], blocked: [], resolved: [] }
|
|
1540
1634
|
var rg = activeRange()
|
|
1541
1635
|
state.tasks.forEach(function (t) {
|
|
@@ -1549,6 +1643,29 @@ function apply(ctx) {
|
|
|
1549
1643
|
if (groups.blocked.length) { lines.push('## 阻塞'); groups.blocked.forEach(function (t) { lines.push('- ' + t.title + (t.escalation ? '(待裁决)' : '')) }); lines.push('') }
|
|
1550
1644
|
var doneAll = state.archived.length ? state.archived : state.tasks.filter(function (t) { return t.status === 'resolved' }); var done = doneAll.filter(function (t) { return !(rg.from || rg.to) || tsInRange(taskLastTs(t), rg.from, rg.to) })
|
|
1551
1645
|
if (done.length) { lines.push('## 已完成(含归档,近 ' + Math.min(done.length, 20) + ' 条)'); done.slice(0, 20).forEach(function (t) { lines.push('- ' + t.title + (t.verification ? '|验收: ' + t.verification.verdict : '') + (t.deliverable && t.deliverable.summary ? '|' + t.deliverable.summary.slice(0, 80) : '')) }); lines.push('') }
|
|
1646
|
+
// ===== 本版自测清单(verifyUserGuide 开关门禁,task-muxyyvg0)=====
|
|
1647
|
+
// 已验收卡的 userTest 聚合:按验收通过时间(resolvedAt,兜底 verification.at)倒序取近 10 张,
|
|
1648
|
+
// tier=internal 的收末尾并标注「无用户可感知面」——用户照着清单逐张自测;开关关掉整段不出现。
|
|
1649
|
+
if (state.verifyUserGuide !== false) {
|
|
1650
|
+
var ugList = done.filter(function (t) { return t.verification && t.verification.userTest })
|
|
1651
|
+
ugList.sort(function (a, b) { return String(b.resolvedAt || b.verification.at || '').localeCompare(String(a.resolvedAt || a.verification.at || '')) })
|
|
1652
|
+
var ugTop = ugList.slice(0, 10)
|
|
1653
|
+
var ugAct = ugTop.filter(function (t) { return t.verification.userTest.tier !== 'internal' })
|
|
1654
|
+
var ugInternal = ugTop.filter(function (t) { return t.verification.userTest.tier === 'internal' })
|
|
1655
|
+
var ugOrdered = ugAct.concat(ugInternal)
|
|
1656
|
+
if (ugOrdered.length) {
|
|
1657
|
+
lines.push('## 本版自测清单(验收通过时间倒序 · 近 ' + ugOrdered.length + ' 张)')
|
|
1658
|
+
ugOrdered.forEach(function (t) {
|
|
1659
|
+
var ut = t.verification.userTest
|
|
1660
|
+
var tierTag = ut.tier === 'ui' ? 'UI 可操作' : (ut.tier === 'metric' ? '看指标' : '纯内部 · 无用户可感知面')
|
|
1661
|
+
lines.push('### ' + t.title + '(' + tierTag + ')')
|
|
1662
|
+
if (ut.gist) lines.push('- 改动:' + ut.gist)
|
|
1663
|
+
if (ut.steps && ut.steps.length) { lines.push('- 自测步骤:'); ut.steps.forEach(function (s, i) { lines.push(' ' + (i + 1) + '. ' + s) }) }
|
|
1664
|
+
if (ut.expect) lines.push('- 预期:' + ut.expect)
|
|
1665
|
+
lines.push('')
|
|
1666
|
+
})
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1552
1669
|
return lines.join('\n')
|
|
1553
1670
|
}
|
|
1554
1671
|
|
|
@@ -1571,9 +1688,10 @@ function apply(ctx) {
|
|
|
1571
1688
|
// usageSummary.total 为 0,一律显示「暂无数据」。
|
|
1572
1689
|
// 日账(usageSummary.byDayFull:{'YYYY-MM-DD': {t,e}})用于「今日」大数字与「近 7 天」条形。
|
|
1573
1690
|
// 双指标口径:t = 总量(含缓存读)、e = 有效消耗(输入+输出+缓存写,不含缓存读)。
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
//
|
|
1691
|
+
// 主显口径(task-muxnqunk,用户 2026-10-07 裁决「算总的吧」,推翻 task-muwq9u04 的有效主显):
|
|
1692
|
+
// 本区主数字一律总量 t(含缓存读),有效消耗 e 退居悬浮 title / 次要小字;e === null 的旧口径日
|
|
1693
|
+
// (老 number 日账 / 存量兜底,没有逐 run 拆分)现在就是正常柱(t 恒有值)——
|
|
1694
|
+
// task-muwsnyqv ③ 的「今日显— / 矮灰柱」旧口径规则顺势退役。
|
|
1577
1695
|
// dayKey 口径与 host 记账完全一致——本地 getters 拼,绝不用 toISOString()(UTC 会让
|
|
1578
1696
|
// 晚上 8 点后的消耗落到次日,今日消耗直接错位)。
|
|
1579
1697
|
function localDayKey(d) {
|
|
@@ -1617,44 +1735,43 @@ function apply(ctx) {
|
|
|
1617
1735
|
// ===== 主窗口(本会话对话)消耗(task-muwsol23):host 增量尾读主会话 v4 日志聚合 =====
|
|
1618
1736
|
// 隔离口径:只在 Token 区单列一行 + 模型分布尾部追加一条;不进累计 / Top 8 / 模型归属摊派。
|
|
1619
1737
|
// 缺字段(老 host)/ 读不到日志 / 无 usage → mw=null → 两处都不渲染(静默降级,不炸轮询)。
|
|
1738
|
+
// 口径翻转(task-muxnqunk):今日/累计主数字显总量(含缓存读——缓存读不再单列,已含在 total 里),
|
|
1739
|
+
// 有效消耗进悬浮 title。
|
|
1620
1740
|
var mw = (u && u.mainWindow && typeof u.mainWindow === 'object' && typeof u.mainWindow.effective === 'number') ? u.mainWindow : null
|
|
1621
1741
|
var todayKey0 = localDayKey()
|
|
1622
1742
|
var mwToday = 0
|
|
1623
1743
|
if (mw && mw.byDay && typeof mw.byDay === 'object') {
|
|
1624
1744
|
var mwc0 = mw.byDay[todayKey0]
|
|
1625
|
-
mwToday = (mwc0 && typeof mwc0.
|
|
1745
|
+
mwToday = (mwc0 && typeof mwc0.t === 'number') ? mwc0.t : 0
|
|
1626
1746
|
}
|
|
1627
|
-
//
|
|
1747
|
+
// 主窗口单列行(累计行下方):「主窗口(本会话):今日 X · 累计 Y」(总量口径,有效值见悬浮 title)
|
|
1628
1748
|
function mwLine() {
|
|
1629
1749
|
if (!mw) return null
|
|
1630
|
-
return React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 8 }, title: '
|
|
1631
|
-
'
|
|
1750
|
+
return React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 8 }, title: '主窗口(本会话对话)消耗:总量口径(含缓存读);有效消耗(输入+输出+缓存写,不含缓存读)= ' + String(mw.effective) + ' tok。host 增量尾读本会话 v4 日志聚合' + (tgOn ? ';随统计范围裁剪(按助手消息的本地日落点)' : '') + '。与看板派发口径并列展示——不进累计 / Top 8 / 模型归属摊派' },
|
|
1751
|
+
'主窗口(本会话):今日 ',
|
|
1632
1752
|
React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(mwToday)),
|
|
1633
1753
|
' · 累计 ',
|
|
1634
|
-
React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(mw.
|
|
1635
|
-
'(缓存读 ',
|
|
1636
|
-
React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(mw.cacheRead)),
|
|
1637
|
-
')')
|
|
1754
|
+
React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(mw.total)))
|
|
1638
1755
|
}
|
|
1639
1756
|
if (!u || !u.total) return React.createElement('div', { style: box }, head, React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据(Worker/Verifier 会话日志里还没有 usage 记录)'), mwLine())
|
|
1640
|
-
// =====
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1644
|
-
//
|
|
1757
|
+
// ===== 主数字口径翻转(task-muxnqunk,用户 2026-10-07 裁决「算总的吧」,推翻 task-muwq9u04 的
|
|
1758
|
+
// 有效主显):模型分布行与 Top8 行的主数字一律显示**总量**(byModel / topTasks[].total,含缓存读),
|
|
1759
|
+
// 有效消耗(byModelEff / topTasks[].effective)退到 title 悬浮;老 host 缺 byModelEff / 老卡无
|
|
1760
|
+
// 五分量留账时 title 标「有效不可知」(口径不伪造)。
|
|
1761
|
+
// 排序按**显示口径**(总量)降序:条形长度与行序一致,否则首行不是最长的条。
|
|
1645
1762
|
var models = Object.keys(u.byModel || {}).map(function (m) {
|
|
1646
1763
|
var tt = u.byModel[m] || 0
|
|
1647
1764
|
var ee = (u.byModelEff && typeof u.byModelEff[m] === 'number') ? u.byModelEff[m] : null
|
|
1648
|
-
return { model: m, total: tt, eff:
|
|
1649
|
-
}).sort(function (a, b) { return b.
|
|
1650
|
-
var maxM = models.length ? (models[0].
|
|
1765
|
+
return { model: m, total: tt, eff: ee }
|
|
1766
|
+
}).sort(function (a, b) { return b.total - a.total })
|
|
1767
|
+
var maxM = models.length ? (models[0].total || 1) : 1
|
|
1651
1768
|
// 主窗口行与 run 模型同一把尺(参与归一,条形长度才可比),但固定尾部追加、不参与排序(见渲染处)
|
|
1652
|
-
if (mw && mw.
|
|
1769
|
+
if (mw && mw.total > maxM) maxM = mw.total
|
|
1653
1770
|
var top = (u.topTasks || []).map(function (x) {
|
|
1654
1771
|
var ee = (typeof x.effective === 'number') ? x.effective : null
|
|
1655
|
-
return { id: x.id, title: x.title, total: x.total, cacheRead: x.cacheRead, runs: x.runs, eff:
|
|
1656
|
-
}).sort(function (a, b) { return b.
|
|
1657
|
-
var maxT = top.length ? (top[0].
|
|
1772
|
+
return { id: x.id, title: x.title, total: x.total, cacheRead: x.cacheRead, runs: x.runs, eff: ee }
|
|
1773
|
+
}).sort(function (a, b) { return b.total - a.total })
|
|
1774
|
+
var maxT = top.length ? (top[0].total || 1) : 1
|
|
1658
1775
|
// 日账:今日数字取本地日 key,没有日账(缺字段/老 host)时退化为 0,不炸也不误报。
|
|
1659
1776
|
// 读侧兼容两种单元形态:老 number(只有总量,有效值不可知)→ { t: n, e: null }。
|
|
1660
1777
|
// 固定口径数据源(task-muwsnyqv ②):「今日」「近 7 天」读**未过滤**的 byDayFull——caption
|
|
@@ -1667,70 +1784,321 @@ function apply(ctx) {
|
|
|
1667
1784
|
}
|
|
1668
1785
|
var todayKey = localDayKey()
|
|
1669
1786
|
var todayCell = dayOf(byDay[todayKey])
|
|
1670
|
-
//
|
|
1671
|
-
//
|
|
1672
|
-
var todayLegacy = todayCell.e === null && todayCell.t > 0
|
|
1673
|
-
var todayEff = todayCell.e === null ? 0 : todayCell.e
|
|
1787
|
+
// 口径翻转(task-muxnqunk):今日大数字显总量 t(含缓存读),有效 e 退次要小字/悬浮;
|
|
1788
|
+
// e===null 的旧口径日不影响主显(t 恒有值),只在悬浮注明有效不可知;t=0 且无日账 = 今日尚无结算,照常显示 0。
|
|
1674
1789
|
var days = lastNDays(7)
|
|
1675
1790
|
var maxDay = 1
|
|
1676
1791
|
var hasDayData = false
|
|
1677
|
-
|
|
1678
|
-
//
|
|
1679
|
-
|
|
1680
|
-
for (var di = 0; di < days.length; di++) { var dc = dayOf(byDay[days[di]]); if (dc.e !== null && dc.e > maxDay) maxDay = dc.e; if (dc.e === null && dc.t > 0) hasLegacy = true; if (dc.t > 0 || dc.e > 0) hasDayData = true }
|
|
1792
|
+
// maxDay 按总量归一(主显口径)——task-muwsnyqv ③ 的「只按有效归一 + 旧口径灰柱」规则随
|
|
1793
|
+
// 口径翻转退役:总量就是主口径,不再存在「缓存读撑歪有效柱」的问题。
|
|
1794
|
+
for (var di = 0; di < days.length; di++) { var dc = dayOf(byDay[days[di]]); if (dc.t > maxDay) maxDay = dc.t; if (dc.t > 0 || dc.e > 0) hasDayData = true }
|
|
1681
1795
|
function mini(label, value) { return React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, label + ' ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(value))) }
|
|
1682
1796
|
// 累计三分量:有效(真实成本)+ 缓存读(占了 total 的大头,必须单列才看得出虚高来源)+ 合计
|
|
1683
1797
|
var effTotal = (typeof u.effective === 'number') ? u.effective : ((u.input || 0) + (u.output || 0) + (u.cacheWrite || 0))
|
|
1684
1798
|
return React.createElement('div', { style: box },
|
|
1685
1799
|
head,
|
|
1686
1800
|
React.createElement('div', { style: { display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap', marginBottom: 8 } },
|
|
1687
|
-
React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color:
|
|
1688
|
-
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens
|
|
1689
|
-
todayCell.t >
|
|
1801
|
+
React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color: C.brand }, title: '今日总量(本地日 ' + todayKey + ')= 输入+输出+缓存写+缓存读(含缓存读);一次 run 的消耗整笔记在结算日;有效消耗(不含缓存读)' + (todayCell.e === null ? '不可知:该日账为旧口径数据,只有总量' : '= ' + String(todayCell.e) + ' tok') }, fmtTokens(todayCell.t)),
|
|
1802
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens(今日总量,含缓存读)'),
|
|
1803
|
+
(todayCell.e !== null && todayCell.t > todayCell.e) ? React.createElement('span', { style: { fontSize: 10, color: C.text2 }, title: '今日有效消耗 = 输入+输出+缓存写(不含缓存读);与总量的差额就是缓存读' }, '其中有效 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(todayCell.e))) : null,
|
|
1690
1804
|
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '累计(本看板) 有效 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(effTotal)), ' · 缓存读 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(u.cacheRead)), ' · 合计 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(u.total))),
|
|
1691
1805
|
mini('输入', u.input), mini('输出', u.output), u.cacheWrite ? mini('缓存写', u.cacheWrite) : null),
|
|
1692
1806
|
// 主窗口(本会话对话)单列行:累计行下方,与看板派发口径并列不混入(mw=null 静默不渲染)
|
|
1693
1807
|
mwLine(),
|
|
1694
1808
|
// 近 7 天迷你条形:高按区间 max 归一(今天高亮 brand,其余浅底 + 边框),
|
|
1695
|
-
//
|
|
1696
|
-
//
|
|
1809
|
+
// 柱高一律取**总量**(含缓存读,口径翻转 task-muxnqunk);e===null 的旧口径日就是正常柱
|
|
1810
|
+
// (t 恒有值),有效值在 tooltip 里对照(不可知则注明)。7 天全为 0 时整块不渲染。
|
|
1697
1811
|
hasDayData ? React.createElement('div', { style: { marginBottom: 8 } },
|
|
1698
|
-
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '近 7
|
|
1812
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '近 7 天(总量,含缓存读)'),
|
|
1699
1813
|
React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 4 } },
|
|
1700
1814
|
days.map(function (k) {
|
|
1701
1815
|
var c = dayOf(byDay[k])
|
|
1702
|
-
|
|
1703
|
-
// 画固定矮灰柱 + tooltip 标明;有效日柱高照常按有效消耗归一。
|
|
1704
|
-
var legacy = c.e === null && c.t > 0
|
|
1705
|
-
var v = legacy ? 0 : (c.e || 0)
|
|
1816
|
+
var v = c.t || 0
|
|
1706
1817
|
var isToday = k === todayKey
|
|
1707
|
-
var h =
|
|
1708
|
-
return React.createElement('div', { key: k, title:
|
|
1709
|
-
React.createElement('div', { style: { width: '100%', height: h, background:
|
|
1818
|
+
var h = v > 0 ? Math.max(3, Math.round(v / maxDay * 32)) : 3
|
|
1819
|
+
return React.createElement('div', { key: k, title: k.slice(5) + ':总量 ' + String(v) + ' tok(含缓存读)' + (c.e !== null ? ' / 有效 ' + String(c.e) + ' tok' : ';有效不可知(旧口径日账)'), style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 } },
|
|
1820
|
+
React.createElement('div', { style: { width: '100%', height: h, background: isToday ? C.brand : C.nested, border: '1px solid ' + (isToday ? C.brand : C.border), borderRadius: 2 } }),
|
|
1710
1821
|
React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2, whiteSpace: 'nowrap' } }, k.slice(5)))
|
|
1711
1822
|
}))) : null,
|
|
1712
1823
|
React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } },
|
|
1713
1824
|
React.createElement('div', { style: { flex: '1 1 240px', minWidth: 200 } },
|
|
1714
|
-
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '
|
|
1825
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '按模型分布(总量)'),
|
|
1715
1826
|
models.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : models.map(function (m) {
|
|
1716
|
-
return React.createElement(UsageRow, { key: m.model, label: m.model, value: m.
|
|
1827
|
+
return React.createElement(UsageRow, { key: m.model, label: m.model, value: m.total, max: maxM, color: C.brand, title: m.model + ':总量 ' + String(m.total) + ' tokens(含缓存读)' + (m.eff === null ? ';有效不可知(本模型无分量留账)' : ' · 有效 ' + String(m.eff) + ' tokens(输入+输出+缓存写)') })
|
|
1717
1828
|
}),
|
|
1718
|
-
// 模型分布尾部追加「主窗口(对话)」一条(task-muwsol23
|
|
1829
|
+
// 模型分布尾部追加「主窗口(对话)」一条(task-muwsol23):总量为 0 不渲染;
|
|
1719
1830
|
// 琥珀色与 run 模型(brand)区分,固定尾部不参与排序,与派发口径并列不混入
|
|
1720
|
-
(mw && mw.
|
|
1831
|
+
(mw && mw.total > 0) ? React.createElement(UsageRow, { key: 'main-window', label: '主窗口(对话)', value: mw.total, max: maxM, color: C.warn, title: '主窗口(对话):总量 ' + String(mw.total) + ' tokens(含缓存读)· 有效 ' + String(mw.effective) + ' tokens(不含缓存读)——本会话对话消耗,与看板派发口径并列不混入(不进累计 / Top 8)' }) : null),
|
|
1721
1832
|
React.createElement('div', { style: { flex: '1 1 240px', minWidth: 200 } },
|
|
1722
|
-
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '任务消耗 Top 8
|
|
1833
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '任务消耗 Top 8(总量)'),
|
|
1723
1834
|
top.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : top.map(function (x) {
|
|
1724
1835
|
var t = getTask(x.id)
|
|
1725
|
-
return React.createElement(UsageRow, { key: x.id, label: x.title || x.id, value: x.
|
|
1836
|
+
return React.createElement(UsageRow, { key: x.id, label: x.title || x.id, value: x.total, max: maxT, color: C.ok, labelColor: t ? C.brand : C.text2, title: x.title + '(总量 ' + String(x.total) + ' tokens(含缓存读) · 其中缓存读 ' + String(x.cacheRead || 0) + (x.eff === null ? ';有效不可知(老卡无五分量留账)' : ' · 有效 ' + String(x.eff) + ' tokens(输入+输出+缓存写)') + ' · ' + (x.runs || 0) + ' 次 run)' + (t ? '——点击查看详情' : ''), onClick: t ? function () { state.detailId = x.id; notify() } : undefined })
|
|
1726
1837
|
}))),
|
|
1727
1838
|
// 口径边界:累计 / Top 8 只统计看板派发的 Worker/Verifier run;主窗口对话消耗自 task-muwsol23 起
|
|
1728
1839
|
// 单列一行 + 模型分布尾部一条并列展示(不越界混入累计/Top8/归属摊派),caption 必须讲清并列关系。
|
|
1729
1840
|
// 范围说明(task-muwc7hjd):用户常把 RangeFilter 当成「整页过滤」,但今日大数字与近 7 天柱子
|
|
1730
1841
|
// 是自身固定口径(今日=本地今天、近 7 天=最近 7 个本地日)——不随范围变,必须在文案里讲清,
|
|
1731
1842
|
// 否则「选了范围数字没变」看起来像 bug。模型分布/Top8/累计才是范围生效的三处。
|
|
1732
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:累计与 Top 8 仅看板派发的 Worker/Verifier run
|
|
1733
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。')
|
|
1843
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:累计与 Top 8 仅看板派发的 Worker/Verifier run(= 一次执行)消耗;主窗口行=本会话对话消耗,与看板派发口径并列不混入(单列一行 + 模型分布尾部一条,不进累计 / Top 8);主数字(今日 / 近 7 天 / 模型分布 / Top 8 / 主窗口行)均为总量口径(含缓存读);有效消耗(input+output+cacheWrite,不含缓存读)见悬浮 title'),
|
|
1844
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。'),
|
|
1845
|
+
// 「→ 模型表现」滚动锚链接(task-muxhtgi2 卡3):一键滚到下方模型×场景七指标表
|
|
1846
|
+
// (与详情页 goArbitration 同一 scrollIntoView 原语;目标区空态也挂锚 id,链接始终可达)
|
|
1847
|
+
React.createElement('div', { style: { fontSize: 9, marginTop: 4 } },
|
|
1848
|
+
React.createElement('span', { onClick: function () { var el = document.getElementById('tskb-model-perf'); if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' }) }, style: { color: C.brand, cursor: 'pointer' }, title: '滚动到下方「模型表现」区:模型 × 场景(角色 × 规模段)七指标对比表' }, '→ 模型表现')))
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// ===== 模型表现区(记分卡卡3,task-muxhtgi2):七指标表格 × 场景筛选器(本地过滤不重拉)=====
|
|
1852
|
+
// 数据源:state.usageSummary.scoreboard——卡2 host 聚合底座(buildScoreboard)挂进 get-tasks 响应,
|
|
1853
|
+
// 与 Token 区同通道透传、同一 range 口径(范围切换重拉后随 notify 同步刷新,本区零自持请求)。
|
|
1854
|
+
// 每桶 = 模型 × 角色(worker/verifier)× 规模段(small/medium/large,按任务有效 token 分桶)。
|
|
1855
|
+
// 本区三件事全部本地现算:
|
|
1856
|
+
// ① 场景筛选器:角色(全部/worker/verifier)+ 规模段(全部/小/中/大)——scoreboard 已在响应里,
|
|
1857
|
+
// 筛选只是 buckets 数组 pass,绝不重拉(组件内无 rpc/fetchTasks);
|
|
1858
|
+
// ② 七指标表格:每 模型×场景桶 一行,七列——有效均值/耗时中位/驳回率/一次通过率/超时率/续跑率/
|
|
1859
|
+
// 缓存命中率;表头列名带 title 口径说明(与卡2 host 注释同源);
|
|
1860
|
+
// ③ 样本护栏:insufficient(runs<5)行整行灰显 + title 前缀「样本 <5,仅供参考」——
|
|
1861
|
+
// 仅供参考,不参与任何「最优」视觉强调(本区 v1 不做任何高亮排名,从源头规避)。
|
|
1862
|
+
// 空态:scoreboard 缺字段 / buckets 空 → 「暂无足够 run 数据」(锚 id 照挂,Token 区链接始终可达);
|
|
1863
|
+
// 筛选后空 → 「当前筛选无匹配桶」。
|
|
1864
|
+
// 锚点:区根 id='tskb-model-perf',Token 区 caption 的「→ 模型表现」链接 scrollIntoView 过来。
|
|
1865
|
+
var MP_SIZE_LABEL = { small: '小', medium: '中', large: '大' }
|
|
1866
|
+
// 率类/耗时格式化:null = 无样本(不假装 0,与 host 同哲学)
|
|
1867
|
+
function mpPct(r) { return (r === null || r === undefined || !isFinite(r)) ? '—' : Math.round(r * 100) + '%' }
|
|
1868
|
+
function mpDur(ms) { if (ms === null || ms === undefined || !isFinite(ms)) return '—'; var m = Math.round(ms / 60000); if (m < 1) return '<1m'; if (m < 60) return m + 'm'; return (m / 60).toFixed(1) + 'h' }
|
|
1869
|
+
// 七列定义:label + title 口径说明 + 取值格式化(口径一句话版与 README 仪表盘段同步维护)
|
|
1870
|
+
var MP_COLS = [
|
|
1871
|
+
{ k: 'effAvg', label: '有效均值', title: '有效 token 均值 = 桶内 run 有效消耗(输入+输出+缓存写,不含缓存读)的均值;无 usage 留账的 run 不进均值(不伪造 0)', fmt: function (b) { return b.effAvg === null ? '—' : fmtTokens(b.effAvg) } },
|
|
1872
|
+
{ k: 'durMedianMs', label: '耗时中位', title: 'run 耗时(endedAt - at)中位数;P90 见行 title;无合法时刻的 run 不进分布', fmt: function (b) { return mpDur(b.durMedianMs) } },
|
|
1873
|
+
{ k: 'rejectRate', label: '驳回率', title: '驳回率 = 归因到本桶 run 的验收驳回次数 / 桶内 runs(归因 = 驳回前最近 worker run)', fmt: function (b) { return mpPct(b.rejectRate) } },
|
|
1874
|
+
{ k: 'firstPassRate', label: '一次通过率', title: '一次通过率 = 桶内 resolved 任务中 rejectCount=0(零驳回)的占比;桶内无 resolved 任务 → —', fmt: function (b) { return mpPct(b.firstPassRate) } },
|
|
1875
|
+
{ k: 'timeoutRate', label: '超时率', title: '超时率 = outcome 超时落定(timeout/error)的 run / 桶内 runs', fmt: function (b) { return mpPct(b.timeoutRate) } },
|
|
1876
|
+
{ k: 'resumeRate', label: '续跑率', title: '续跑率 = resume:true 续跑记录 / 桶内 runs', fmt: function (b) { return mpPct(b.resumeRate) } },
|
|
1877
|
+
{ k: 'cacheHitRate', label: '缓存命中率', title: '缓存命中率 = cacheRead / (input + cacheRead);⚠️ 跨 provider 口径可能不一(input 是否含缓存读各家不同),对比前先核口径;无 usage 样本 → —', fmt: function (b) { return mpPct(b.cacheHitRate) } }
|
|
1878
|
+
]
|
|
1879
|
+
function ModelPerf(props) {
|
|
1880
|
+
var _R = React; var useState = _R.useState
|
|
1881
|
+
var _a = useState(state.mpRole || ''), roleF = _a[0], setRoleF = _a[1]
|
|
1882
|
+
var _b = useState(state.mpSize || ''), sizeF = _b[0], setSizeF = _b[1]
|
|
1883
|
+
function pickRole(v) { state.mpRole = v; setRoleF(v) }
|
|
1884
|
+
function pickSize(v) { state.mpSize = v; setSizeF(v) }
|
|
1885
|
+
var box = { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 }
|
|
1886
|
+
// 范围激活标记与 Token 区同源:scoreboard 由 host 按同一 range 裁剪,范围内数据必须亮明身份
|
|
1887
|
+
var tg = activeRange()
|
|
1888
|
+
var tgOn = !!(tg.from || tg.to)
|
|
1889
|
+
var head = React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('bot', 11), '模型表现',
|
|
1890
|
+
tgOn ? React.createElement('span', { style: { fontSize: 9, fontWeight: 400, color: C.brand, border: '1px solid ' + C.brand, borderRadius: 8, padding: '0 6px' }, title: '本区桶数据已按统计范围过滤(与 Token 区同一 range 口径):' + rangeLabel() }, '范围内: ' + rangeLabel()) : null)
|
|
1891
|
+
var sb = props.sb
|
|
1892
|
+
var buckets = (sb && Array.isArray(sb.buckets)) ? sb.buckets : []
|
|
1893
|
+
// 空态:无 scoreboard(老 host)/ 无入桶 run → 一句话空态(锚 id 照挂,Token 区链接始终可达)
|
|
1894
|
+
if (!buckets.length) return React.createElement('div', { id: 'tskb-model-perf', style: box }, head, React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无足够 run 数据(还没有可入桶的已落定 Worker/Verifier run 留账)'))
|
|
1895
|
+
// 本地筛选:角色 × 规模段,纯数组 pass 不重拉
|
|
1896
|
+
var shown = buckets.filter(function (b) { return (!roleF || b.role === roleF) && (!sizeF || b.size === sizeF) })
|
|
1897
|
+
function chip(cur, setter, v, label) {
|
|
1898
|
+
var on = cur === v
|
|
1899
|
+
return React.createElement('button', { onClick: function () { setter(v) }, style: { fontSize: 10, padding: '1px 8px', border: '1px solid ' + (on ? C.brand : C.border2), borderRadius: 10, background: on ? C.nested : C.card, color: on ? C.brand : C.text2, cursor: 'pointer' } }, label)
|
|
1900
|
+
}
|
|
1901
|
+
var thL = { fontSize: 9, color: C.text2, fontWeight: 600, textAlign: 'left', padding: '2px 4px', borderBottom: '1px solid ' + C.border, whiteSpace: 'nowrap' }
|
|
1902
|
+
var thR = { fontSize: 9, color: C.text2, fontWeight: 600, textAlign: 'right', padding: '2px 4px', borderBottom: '1px solid ' + C.border, whiteSpace: 'nowrap', cursor: 'help' }
|
|
1903
|
+
var tdL = { fontSize: 10, color: C.text, textAlign: 'left', padding: '3px 4px', borderBottom: '1px solid ' + C.nested, whiteSpace: 'nowrap' }
|
|
1904
|
+
var tdR = { fontSize: 10, color: C.text, textAlign: 'right', padding: '3px 4px', borderBottom: '1px solid ' + C.nested, whiteSpace: 'nowrap' }
|
|
1905
|
+
var insufN = shown.filter(function (b) { return b.insufficient }).length
|
|
1906
|
+
return React.createElement('div', { id: 'tskb-model-perf', style: box },
|
|
1907
|
+
head,
|
|
1908
|
+
// 场景筛选器(本地):角色 + 规模段两组 chip;右侧计数「显示/总数 · 样本不足 N」
|
|
1909
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap', marginBottom: 6 } },
|
|
1910
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '角色'),
|
|
1911
|
+
chip(roleF, pickRole, '', '全部'), chip(roleF, pickRole, 'worker', 'worker'), chip(roleF, pickRole, 'verifier', 'verifier'),
|
|
1912
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2, marginLeft: 6 } }, '规模段'),
|
|
1913
|
+
chip(sizeF, pickSize, '', '全部'), chip(sizeF, pickSize, 'small', '小'), chip(sizeF, pickSize, 'medium', '中'), chip(sizeF, pickSize, 'large', '大'),
|
|
1914
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2, marginLeft: 'auto' } }, shown.length + '/' + buckets.length + ' 桶' + (insufN ? ' · ' + insufN + ' 桶样本不足' : ''))),
|
|
1915
|
+
shown.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2, padding: '4px 0' } }, '当前筛选无匹配桶(试试放宽角色/规模段)')
|
|
1916
|
+
: React.createElement('div', { style: { overflowX: 'auto' } },
|
|
1917
|
+
React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse' } },
|
|
1918
|
+
React.createElement('thead', null, React.createElement('tr', null,
|
|
1919
|
+
React.createElement('th', { style: thL }, '模型'),
|
|
1920
|
+
React.createElement('th', { style: thL }, '场景'),
|
|
1921
|
+
React.createElement('th', { style: thR, title: '桶内已落定 run 数;<5 为样本不足,整行灰显仅供参考' }, '样本'),
|
|
1922
|
+
MP_COLS.map(function (c) { return React.createElement('th', { key: c.k, style: thR, title: c.title }, c.label) }))),
|
|
1923
|
+
React.createElement('tbody', null, shown.map(function (b) {
|
|
1924
|
+
// 样本护栏:insufficient 行整行灰显 + title 前缀「样本 <5,仅供参考」,不进任何最优强调
|
|
1925
|
+
var tip = (b.insufficient ? '样本 <5,仅供参考——' : '') + b.model + ' × ' + b.role + ' × ' + (MP_SIZE_LABEL[b.size] || b.size) + ':runs=' + b.runs + ';有效总额 ' + fmtTokens(b.effSum) + ' tok;耗时 P90 ' + mpDur(b.durP90Ms) + '(七指标口径见表头 title)'
|
|
1926
|
+
return React.createElement('tr', { key: b.model + '|' + b.role + '|' + b.size, title: tip, style: { opacity: b.insufficient ? 0.45 : 1 } },
|
|
1927
|
+
React.createElement('td', { style: tdL }, b.model),
|
|
1928
|
+
React.createElement('td', { style: tdL }, b.role + '·' + (MP_SIZE_LABEL[b.size] || b.size)),
|
|
1929
|
+
React.createElement('td', { style: tdR }, String(b.runs)),
|
|
1930
|
+
MP_COLS.map(function (c) { return React.createElement('td', { key: c.k, style: tdR }, c.fmt(b)) }))
|
|
1931
|
+
})))),
|
|
1932
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:每行 = 模型 × 角色 × 规模段桶(规模按任务有效 token:小 <1M / 中 1~10M / 大 >10M);率类分母为桶内 runs,一次通过率分母为桶内 resolved 任务;runs<5 行灰显 = 样本不足仅供参考;随统计范围裁剪(与 Token 区同一 range)'))
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// ===== 质量趋势区(记分卡卡4,task-muxhtgil):一次通过率趋势 / 卡时长分布 / 超时率走势 / 续跑成功率 =====
|
|
1936
|
+
// 数据源:state.usageSummary.scoreboard.trends——卡2 host 聚合底座(assembleScoreboard)的全局 rollup,
|
|
1937
|
+
// 与模型表现区同通道(get-tasks 响应透传)、同一 range 口径(host 已按 run/落定日裁剪,client 只渲染,
|
|
1938
|
+
// 组件体内零 rpc/fetchTasks 不重拉)。
|
|
1939
|
+
// 四块(P1 质量 KPI 并入的全局视图):
|
|
1940
|
+
// ① 一次通过率趋势:近 14 天柱形(firstPassByDay:resolved 任务按落定日,当日零驳回占比),今日柱高亮 + 块头标今日值;
|
|
1941
|
+
// ② 卡时长分布:四桶横条(durationBuckets:<10m / 10-30m / 30-60m / >60m,创建→落定墙钟,60m 端点归 30-60m 桶);
|
|
1942
|
+
// ③ 超时率走势:近 14 天迷你柱行(timeoutByDay:当日超时落定 run / 当日 runs);
|
|
1943
|
+
// ④ 续跑成功率:resume 续跑 run 的 completed 占比(卡2出参为范围总量口径、无 byDay——迷你行显率 + 成功/次数,不伪造逐日)。
|
|
1944
|
+
// 空态灰显:trends 缺字段(老 host)/ 四块皆无数据 → 整区一句灰字(锚 id 照挂);单块无数据 → 块内灰字「暂无…」。
|
|
1945
|
+
// 范围联动:trends 各字段已被 host 按统计范围裁剪,区头带「范围内: …」徽章(与 Token / 模型表现区同源 activeRange);
|
|
1946
|
+
// 近 14 天为固定窗口(设计口径:标今日值),范围落在窗口外时对应日柱为空,caption 写明。
|
|
1947
|
+
var QT_DUR_BUCKETS = [
|
|
1948
|
+
{ k: 'lt10m', label: '<10m', tip: '卡时长 <10 分钟' },
|
|
1949
|
+
{ k: 'm10to30', label: '10-30m', tip: '卡时长 10~30 分钟' },
|
|
1950
|
+
{ k: 'm30to60', label: '30-60m', tip: '卡时长 30~60 分钟(含 60m 端点)' },
|
|
1951
|
+
{ k: 'gt60m', label: '>60m', tip: '卡时长 >60 分钟' }
|
|
1952
|
+
]
|
|
1953
|
+
// 近 14 天柱形通用画法(① 通过率 / ③ 超时率共用):cells = [{ k, rate(null=当日无样本), tip }]
|
|
1954
|
+
// 今日柱 brand 高亮 + 率值顶标;无样本日画 3px 灰基线(title 写明无样本,不假装 0%)。
|
|
1955
|
+
function qtDayBars(cells, color, todayKey) {
|
|
1956
|
+
return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46 } },
|
|
1957
|
+
cells.map(function (c) {
|
|
1958
|
+
var isToday = c.k === todayKey
|
|
1959
|
+
var h = c.rate === null ? 3 : Math.max(4, Math.round(c.rate * 34))
|
|
1960
|
+
var kp = c.k.split('-')
|
|
1961
|
+
var dLabel = Number(kp[1]) + '/' + Number(kp[2])
|
|
1962
|
+
return React.createElement('div', { key: c.k, style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 1 }, title: c.tip },
|
|
1963
|
+
c.rate !== null ? React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, mpPct(c.rate)) : null,
|
|
1964
|
+
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)) } }),
|
|
1965
|
+
React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, dLabel))
|
|
1966
|
+
}))
|
|
1967
|
+
}
|
|
1968
|
+
function QualityTrends(props) {
|
|
1969
|
+
var sb = props.sb
|
|
1970
|
+
var tr = (sb && sb.trends && typeof sb.trends === 'object') ? sb.trends : null
|
|
1971
|
+
var box = { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 }
|
|
1972
|
+
// 范围激活标记与 Token / 模型表现区同源:trends 由 host 按同一 range 裁剪,范围内数据必须亮明身份
|
|
1973
|
+
var tg = activeRange()
|
|
1974
|
+
var tgOn = !!(tg.from || tg.to)
|
|
1975
|
+
var head = React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('check-circle', 11), '质量趋势',
|
|
1976
|
+
tgOn ? React.createElement('span', { style: { fontSize: 9, fontWeight: 400, color: C.brand, border: '1px solid ' + C.brand, borderRadius: 8, padding: '0 6px' }, title: '本区趋势数据已按统计范围过滤(与 Token 区同一 range 口径):' + rangeLabel() }, '范围内: ' + rangeLabel()) : null)
|
|
1977
|
+
var fpByDay = (tr && tr.firstPassByDay && typeof tr.firstPassByDay === 'object') ? tr.firstPassByDay : {}
|
|
1978
|
+
var durB = (tr && tr.durationBuckets && typeof tr.durationBuckets === 'object') ? tr.durationBuckets : null
|
|
1979
|
+
var toByDay = (tr && tr.timeoutByDay && typeof tr.timeoutByDay === 'object') ? tr.timeoutByDay : {}
|
|
1980
|
+
var rsm = (tr && tr.resume && typeof tr.resume === 'object') ? tr.resume : null
|
|
1981
|
+
// 整区空态判定:四块皆无数据(率类按日单元求和 / 时长按四桶求和 / 续跑按 runs)
|
|
1982
|
+
var fpSum = 0; for (var fk in fpByDay) { if (Object.prototype.hasOwnProperty.call(fpByDay, fk)) fpSum += (fpByDay[fk] && fpByDay[fk].resolved) || 0 }
|
|
1983
|
+
var toSum = 0; for (var tk2 in toByDay) { if (Object.prototype.hasOwnProperty.call(toByDay, tk2)) toSum += (toByDay[tk2] && toByDay[tk2].runs) || 0 }
|
|
1984
|
+
var durSum = 0
|
|
1985
|
+
if (durB) QT_DUR_BUCKETS.forEach(function (b) { durSum += Number(durB[b.k]) || 0 })
|
|
1986
|
+
var rsmRuns = (rsm && rsm.runs) || 0
|
|
1987
|
+
if (!tr || (fpSum + toSum + durSum + rsmRuns) === 0) {
|
|
1988
|
+
return React.createElement('div', { id: 'tskb-quality-trends', style: box }, head,
|
|
1989
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无足够 run 数据(还没有可统计的已落定 Worker/Verifier run / resolved 任务)'))
|
|
1990
|
+
}
|
|
1991
|
+
var days14 = lastNDays(14)
|
|
1992
|
+
var todayKey = localDayKey()
|
|
1993
|
+
// 块头:左块名 + 右侧今日/汇总值(fontWeight 400 小字)
|
|
1994
|
+
function blockHead(label, rightEl, tip) {
|
|
1995
|
+
return React.createElement('div', { title: tip, style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'baseline', gap: 6 } }, label,
|
|
1996
|
+
rightEl ? React.createElement('span', { style: { marginLeft: 'auto', fontWeight: 400, fontSize: 9 } }, rightEl) : null)
|
|
1997
|
+
}
|
|
1998
|
+
// ===== ① 一次通过率趋势(近 14 天):当日率 = 落定 resolved 任务中零驳回(rejectCount=0)占比 =====
|
|
1999
|
+
var fpCells = days14.map(function (k) {
|
|
2000
|
+
var c = fpByDay[k]
|
|
2001
|
+
var resolved = (c && c.resolved) || 0
|
|
2002
|
+
var firstPass = (c && c.firstPass) || 0
|
|
2003
|
+
return { k: k, rate: resolved ? firstPass / resolved : null, tip: k + ':' + (resolved ? ('一次通过率 ' + mpPct(firstPass / resolved) + '(零驳回 ' + firstPass + ' / 落定 ' + resolved + ')') : '无 resolved 任务落定') }
|
|
2004
|
+
})
|
|
2005
|
+
var fpHas = false; fpCells.forEach(function (c) { if (c.rate !== null) fpHas = true })
|
|
2006
|
+
var fpToday = fpCells[fpCells.length - 1]
|
|
2007
|
+
var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
2008
|
+
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'),
|
|
2009
|
+
fpHas ? qtDayBars(fpCells, C.ok, todayKey)
|
|
2010
|
+
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 resolved 任务落定'))
|
|
2011
|
+
// ===== ② 卡时长分布(四桶横条):创建 → 落定(verifiedAt||resolvedAt)墙钟,含排队/执行/验收全程 =====
|
|
2012
|
+
var durMax = 1
|
|
2013
|
+
if (durB) QT_DUR_BUCKETS.forEach(function (b) { var v = Number(durB[b.k]) || 0; if (v > durMax) durMax = v })
|
|
2014
|
+
var block2 = React.createElement('div', { style: { flex: '1 1 180px', minWidth: 170 } },
|
|
2015
|
+
blockHead('卡时长分布', durSum > 0 ? React.createElement('span', null, React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, String(durSum)), ' 张落定卡') : null, '卡时长 = 创建 → 落定的墙钟(含排队/执行/验收全程)'),
|
|
2016
|
+
durSum === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无时长样本')
|
|
2017
|
+
: React.createElement('div', null, QT_DUR_BUCKETS.map(function (b) {
|
|
2018
|
+
var v = Number(durB[b.k]) || 0
|
|
2019
|
+
var pct = v > 0 ? Math.max(2, Math.round(v / durMax * 100)) : 0
|
|
2020
|
+
return React.createElement('div', { key: b.k, style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }, title: b.tip + ':' + v + ' 张(创建→落定墙钟,含排队 / 执行 / 验收全程)' },
|
|
2021
|
+
React.createElement('span', { style: { width: 44, fontSize: 10, color: C.text2, textAlign: 'right', flexShrink: 0 } }, b.label),
|
|
2022
|
+
React.createElement('div', { style: { flex: 1, height: 8, background: C.nested, borderRadius: 4, overflow: 'hidden' } },
|
|
2023
|
+
React.createElement('div', { style: { height: '100%', width: pct + '%', background: C.brand, borderRadius: 4, transition: 'width .3s' } })),
|
|
2024
|
+
React.createElement('span', { style: { width: 24, fontSize: 10, color: C.text2, flexShrink: 0 } }, String(v)))
|
|
2025
|
+
})))
|
|
2026
|
+
// ===== ③ 超时率走势(近 14 天迷你柱行):当日率 = 超时落定(timeout/error)run / 当日 runs =====
|
|
2027
|
+
var toCells = days14.map(function (k) {
|
|
2028
|
+
var c = toByDay[k]
|
|
2029
|
+
var runs = (c && c.runs) || 0
|
|
2030
|
+
var timeouts = (c && c.timeout) || 0
|
|
2031
|
+
return { k: k, rate: runs ? timeouts / runs : null, tip: k + ':' + (runs ? ('超时率 ' + mpPct(timeouts / runs) + '(超时 ' + timeouts + ' / run ' + runs + ')') : '无 run 落定') }
|
|
2032
|
+
})
|
|
2033
|
+
var toHas = false; toCells.forEach(function (c) { if (c.rate !== null) toHas = true })
|
|
2034
|
+
var toToday = toCells[toCells.length - 1]
|
|
2035
|
+
var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
2036
|
+
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 的比例'),
|
|
2037
|
+
toHas ? qtDayBars(toCells, C.warn, todayKey)
|
|
2038
|
+
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 run 落定'))
|
|
2039
|
+
// ===== ④ 续跑成功率(范围总量口径,卡2出参无 byDay):resume:true 续跑记录中 completed 占比 =====
|
|
2040
|
+
var rsmRate = (rsm && typeof rsm.rate === 'number' && isFinite(rsm.rate)) ? rsm.rate : null
|
|
2041
|
+
var block4 = React.createElement('div', { style: { flex: '1 1 180px', minWidth: 170 } },
|
|
2042
|
+
blockHead('续跑成功率', rsmRuns > 0 ? React.createElement('span', null, React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, String((rsm && rsm.completed) || 0) + '/' + rsmRuns), ' 续跑成功') : null, '续跑成功率 = resume 续跑记录中 outcome=completed 的占比;续跑 = 前次超时/失败后继续跑'),
|
|
2043
|
+
rsmRuns === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无续跑记录')
|
|
2044
|
+
: React.createElement('div', { title: '续跑成功率 = resume:true 续跑记录中 outcome=completed 的占比(范围总量口径,host 出参无逐日);续跑 = 前次 run 超时/失败后续跑' },
|
|
2045
|
+
React.createElement('div', { style: { fontSize: 16, fontWeight: 700, color: rsmRate !== null ? C.ok : C.text2 } }, rsmRate !== null ? mpPct(rsmRate) : '—'),
|
|
2046
|
+
React.createElement('div', { style: { height: 6, background: C.nested, borderRadius: 3, overflow: 'hidden', marginTop: 4 } },
|
|
2047
|
+
React.createElement('div', { style: { height: '100%', width: (rsmRate !== null ? Math.round(rsmRate * 100) : 0) + '%', background: C.ok, borderRadius: 3, transition: 'width .3s' } }))))
|
|
2048
|
+
return React.createElement('div', { id: 'tskb-quality-trends', style: box },
|
|
2049
|
+
head,
|
|
2050
|
+
React.createElement('div', { style: { display: 'flex', gap: 16, flexWrap: 'wrap', marginBottom: 8 } }, block1, block2),
|
|
2051
|
+
React.createElement('div', { style: { display: 'flex', gap: 16, flexWrap: 'wrap' } }, block3, block4),
|
|
2052
|
+
// 口径 caption:一句讲清统计本体(看板派发 run / 任务落定)+ 驳回定义 + 固定窗口与范围的关系
|
|
2053
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 8, lineHeight: 1.5 } }, '口径:仅看板派发的 Worker/Verifier run 与任务落定记录(按本地日落点,随统计范围裁剪);一次通过率 = 当日落定 resolved 任务中零驳回占比(驳回 = 验收 verdict rejected,归因驳回前最近 worker run);超时 = run 落定 timeout/error;续跑成功率 = resume 续跑记录中 completed 占比(范围总量口径,无逐日);卡时长 = 创建 → 落定墙钟(含排队 / 验收)。通过率与超时率为近 14 天固定窗口——范围落在窗口外时对应日柱为空'))
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// ===== 驳回聚类区(记分卡卡5,task-muxhu1zv):驳回原因 Top3 +「建议沉淀为约定」文本(一键复制)=====
|
|
2057
|
+
// 数据源:state.usageSummary.scoreboard.rejectionClusters——host 规则法聚类(关键词模板,首命中归类;
|
|
2058
|
+
// 按驳回消息自身日落点随统计范围裁剪),与卡3/卡4 同通道透传,本区只渲染零 rpc(不重拉)。
|
|
2059
|
+
// 每行 = TopN 类目 + 占比(count/total)+ 结构化建议文本 + 「复制」按钮(建议文本进剪贴板,
|
|
2060
|
+
// 沉淀动作人工——v1 只输出文本,不代调 notes 插件);代表原文(截断 80 字 ×≤3 条)放行 title 悬浮。
|
|
2061
|
+
// 缺省兼容:老 host 无 rejectionClusters 字段 → 返回 null 整块不渲染(与 HealthHints 同哲学);
|
|
2062
|
+
// 有字段但 total=0 → 区头 + 一句空态「暂无驳回记录」。
|
|
2063
|
+
function RejectionClusters(props) {
|
|
2064
|
+
var _R = React; var useState = _R.useState
|
|
2065
|
+
var _c = useState(''), copiedId = _c[0], setCopiedId = _c[1]
|
|
2066
|
+
var sb = props.sb
|
|
2067
|
+
var rc = sb && sb.rejectionClusters
|
|
2068
|
+
if (!rc || typeof rc !== 'object') return null // 老 host 缺字段:整块不渲染(零残留)
|
|
2069
|
+
var box = { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 }
|
|
2070
|
+
var tg = activeRange()
|
|
2071
|
+
var tgOn = !!(tg.from || tg.to)
|
|
2072
|
+
var head = React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('flask-conical', 11), '驳回聚类',
|
|
2073
|
+
tgOn ? React.createElement('span', { style: { fontSize: 9, fontWeight: 400, color: C.brand, border: '1px solid ' + C.brand, borderRadius: 8, padding: '0 6px' }, title: '本区已按统计范围过滤(按驳回消息本地日落点,与 Token 区同一 range):' + rangeLabel() }, '范围内: ' + rangeLabel()) : null)
|
|
2074
|
+
var top = Array.isArray(rc.top) ? rc.top : []
|
|
2075
|
+
if (!rc.total || !top.length) {
|
|
2076
|
+
return React.createElement('div', { id: 'tskb-rejection-clusters', style: box }, head,
|
|
2077
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无驳回记录(验收驳回后会按关键词模板聚类出 Top3 类目与约定建议)'))
|
|
2078
|
+
}
|
|
2079
|
+
// 复制建议文本:clipboard API 不可用/被拒时静默降级(按钮不变样,下轮再试)
|
|
2080
|
+
function copySugg(c) {
|
|
2081
|
+
var text = String(c.suggestion || '')
|
|
2082
|
+
if (!text) return
|
|
2083
|
+
try {
|
|
2084
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
2085
|
+
setCopiedId(c.id); setTimeout(function () { setCopiedId('') }, 2000)
|
|
2086
|
+
}, function () {})
|
|
2087
|
+
} catch (_) {}
|
|
2088
|
+
}
|
|
2089
|
+
return React.createElement('div', { id: 'tskb-rejection-clusters', style: box },
|
|
2090
|
+
head,
|
|
2091
|
+
top.map(function (c, i) {
|
|
2092
|
+
var tip = (c.samples && c.samples.length) ? '代表原文:\n' + c.samples.map(function (s) { return '· ' + s }).join('\n') : '(无代表原文)'
|
|
2093
|
+
return React.createElement('div', { key: c.id, title: tip, style: { marginBottom: 6, paddingBottom: 6, borderBottom: i < top.length - 1 ? '1px solid ' + C.nested : 'none' } },
|
|
2094
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11 } },
|
|
2095
|
+
React.createElement('span', { style: { color: C.warn, fontWeight: 700 } }, 'Top' + (i + 1)),
|
|
2096
|
+
React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, c.label),
|
|
2097
|
+
React.createElement('span', { style: { color: C.text2, fontSize: 10 } }, c.pct + '%(' + c.count + '/' + rc.total + ')'),
|
|
2098
|
+
React.createElement('button', { onClick: function () { copySugg(c) }, title: '复制「建议沉淀为约定」文本(粘贴到派发约定/Worker prompt,沉淀动作人工)', style: { marginLeft: 'auto', fontSize: 9, padding: '1px 8px', border: '1px solid ' + C.border2, borderRadius: 9, background: C.card, color: copiedId === c.id ? C.ok : C.text2, cursor: 'pointer' } }, copiedId === c.id ? '✅ 已复制' : '📋 复制约定建议')),
|
|
2099
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, lineHeight: 1.5, marginTop: 2 } }, String(c.suggestion || '')))
|
|
2100
|
+
}),
|
|
2101
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 4, lineHeight: 1.5 } }, '口径:规则法关键词模板聚类(首命中归类,不上 embedding);占比分母 = ' + (tgOn ? '范围内' : '') + '驳回总数(' + rc.total + ' 条)' + (rc.restCount ? ';另有 ' + rc.restCount + ' 类未进 Top3' : '') + ';建议文本一键复制后人工沉淀为约定'))
|
|
1734
2102
|
}
|
|
1735
2103
|
|
|
1736
2104
|
// ===== 架构健康提示区(架构自省 L1 · 数据源:state.healthHints)=====
|
|
@@ -1745,6 +2113,8 @@ function apply(ctx) {
|
|
|
1745
2113
|
var infoColor = 'var(--dsw-alias-state-business-primary)'
|
|
1746
2114
|
return React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } },
|
|
1747
2115
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('activity', 11), '架构健康'),
|
|
2116
|
+
// 术语白话(巡检 n-muyg5m7o5r00):settle/touches/p90 等黑话第一屏无解,补一行术语说明
|
|
2117
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 5, lineHeight: 1.5 } }, '运行时自检告警,多为自动恢复的瞬时异常。术语:settle = 任务结算通道;touches = 任务声明的改动文件(文件锁);p90 = 执行时长第 90 百分位'),
|
|
1748
2118
|
hints.map(function (h, i) {
|
|
1749
2119
|
var warn = h.level === 'warn'
|
|
1750
2120
|
var col = warn ? C.warn : infoColor
|
|
@@ -1762,6 +2132,12 @@ function apply(ctx) {
|
|
|
1762
2132
|
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 })),
|
|
1763
2133
|
React.createElement(HealthHints),
|
|
1764
2134
|
React.createElement(TokenUsage, { usage: state.usageSummary }),
|
|
2135
|
+
// 模型表现区(卡3):Token 区正下方;scoreboard 随 usageSummary 同通道透传(老 host 缺字段 → null → 空态)
|
|
2136
|
+
React.createElement(ModelPerf, { sb: (state.usageSummary && state.usageSummary.scoreboard) || null }),
|
|
2137
|
+
// 质量趋势区(卡4):模型表现区正下方;同一 scoreboard 出参的 trends 全局 rollup(老 host 缺字段 → null → 空态灰显)
|
|
2138
|
+
React.createElement(QualityTrends, { sb: (state.usageSummary && state.usageSummary.scoreboard) || null }),
|
|
2139
|
+
// 驳回聚类区(卡5):质量趋势区正下方;scoreboard.rejectionClusters(老 host 缺字段 → 组件返回 null 整块不渲染)
|
|
2140
|
+
React.createElement(RejectionClusters, { sb: (state.usageSummary && state.usageSummary.scoreboard) || null }),
|
|
1765
2141
|
React.createElement('div', { style: { display: 'flex', gap: 12, marginBottom: 12, flexWrap: 'wrap' } },
|
|
1766
2142
|
React.createElement('div', { style: { flex: '1 1 0', minWidth: 200, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, '按状态分布'), statusOrder.map(function (s) { return React.createElement(BarRow, { key: s, label: statusLabels[s] || s, count: stats.byStatus[s] || 0, total: stats.total, color: statusColors[s] || C.brand }) })),
|
|
1767
2143
|
React.createElement('div', { style: { flex: '1 1 0', minWidth: 200, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, '按优先级分布'), prioOrder.map(function (p) { return React.createElement(BarRow, { key: p, label: prioLabel[p] || p, count: stats.byPriority[p] || 0, total: stats.total, color: prioColor[p] || C.brand }) }))),
|
|
@@ -1779,12 +2155,12 @@ function apply(ctx) {
|
|
|
1779
2155
|
function pick(m) { if (m === cur) return; rpc('set-work-mode', { mode: m }).then(fetchTasks).catch(function () {}) }
|
|
1780
2156
|
// 档位文案:名称 + 一句说明(说明常驻显示在按钮内,选中态高亮不可用 title 才看得到)
|
|
1781
2157
|
var opts = [
|
|
1782
|
-
{ k: 'list', name: '清单模式', desc: '
|
|
1783
|
-
{ k: 'auto', name: '自动派发', desc: '
|
|
1784
|
-
{ k: 'team', name: 'Team 托管', desc: '
|
|
2158
|
+
{ k: 'list', name: '清单模式', desc: '只读清单,不自动派发', icon: 'clipboard-list' },
|
|
2159
|
+
{ k: 'auto', name: '自动派发', desc: '建卡即派给后台子代理', icon: 'zap' },
|
|
2160
|
+
{ k: 'team', name: 'Team 托管', desc: '草稿起步,你裁决全流程', icon: 'users' }
|
|
1785
2161
|
]
|
|
1786
2162
|
var btnBase = { border: 'none', cursor: 'pointer', fontWeight: 500, textAlign: 'center', display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 1, padding: '3px 9px', transition: 'all .15s', lineHeight: 1.25 }
|
|
1787
|
-
return React.createElement('div', { title: '工作模式(清单 / 自动派发 / Team 托管)——歧义裁决、Verifier
|
|
2163
|
+
return React.createElement('div', { title: '工作模式(清单 / 自动派发 / Team 托管)——歧义裁决、Verifier 验收、文件锁(touches)排他在三档下全部生效', style: { display: 'inline-flex', borderRadius: 8, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, opts.map(function (o) {
|
|
1788
2164
|
var on = cur === o.k
|
|
1789
2165
|
return React.createElement('button', {
|
|
1790
2166
|
key: o.k, onClick: function () { pick(o.k) }, title: o.name + ':' + o.desc,
|
|
@@ -1823,13 +2199,13 @@ function apply(ctx) {
|
|
|
1823
2199
|
var ps = state.poolStatus
|
|
1824
2200
|
if (!ps) return null
|
|
1825
2201
|
var wActive = (ps.workers || []).filter(function (w) { return w.busy }).length
|
|
1826
|
-
var wTotal =
|
|
2202
|
+
var wTotal = state.maxWorkers // 分母=并发上限而非当前池实例数,空闲也显示 0/上限
|
|
1827
2203
|
var vActive = (ps.verifiers || []).filter(function (v) { return v.busy }).length
|
|
1828
|
-
var vTotal =
|
|
2204
|
+
var vTotal = state.maxVerifiers
|
|
1829
2205
|
return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
|
|
1830
|
-
//
|
|
1831
|
-
React.createElement('span', { title: 'Worker
|
|
1832
|
-
React.createElement('span', { title: 'Verifier
|
|
2206
|
+
// 池水位标注(反馈 n-mut9rzoq3flu / n-muyg4wl9208c):裸数字第一屏无解,补可见文字标签 + title
|
|
2207
|
+
React.createElement('span', { title: '活跃 Worker 数/上限(⚡ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), 'Worker ' + wActive + '/' + wTotal),
|
|
2208
|
+
React.createElement('span', { title: '活跃 Verifier 数/上限(✓ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), 'Verifier ' + vActive + '/' + vTotal))
|
|
1833
2209
|
}
|
|
1834
2210
|
|
|
1835
2211
|
// #11 头部减负:池配置收纳进 ⚙️ 弹出层(含 #17 verifier 异构模型设置)
|
|
@@ -1839,7 +2215,7 @@ function apply(ctx) {
|
|
|
1839
2215
|
var _b = useState(false), dirty = _b[0], setDirty = _b[1]
|
|
1840
2216
|
var _m = useState(null), models = _m[0], setModels = _m[1]
|
|
1841
2217
|
var cfgKey = props.cfgKey || 'verifierModel'
|
|
1842
|
-
var label = props.label || '
|
|
2218
|
+
var label = props.label || 'Verifier 模型'
|
|
1843
2219
|
// 下拉列出当前网关可用模型(host list-models RPC);枚举失败降级为文本输入
|
|
1844
2220
|
useEffect(function () {
|
|
1845
2221
|
var cancelled = false
|
|
@@ -1892,13 +2268,13 @@ function apply(ctx) {
|
|
|
1892
2268
|
open && pos ? React.createElement('div', { style: { position: 'fixed', top: pos.top + 'px', right: pos.right + 'px', padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 4px 16px rgba(0,0,0,0.15)', zIndex: 1000, whiteSpace: 'nowrap', maxHeight: 'calc(100vh - ' + (pos.top + 12) + 'px)', overflowY: 'auto' } },
|
|
1893
2269
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
|
|
1894
2270
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
|
|
1895
|
-
React.createElement(PoolCfg, { label: '
|
|
1896
|
-
React.createElement(PoolCfg, { label: '
|
|
2271
|
+
React.createElement(PoolCfg, { label: 'Worker 并发', cfgKey: 'maxWorkers', value: props.maxW }),
|
|
2272
|
+
React.createElement(PoolCfg, { label: 'Verifier 并发', cfgKey: 'maxVerifiers', value: props.maxV })),
|
|
1897
2273
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
|
|
1898
|
-
React.createElement(ModelCfg, { label: '
|
|
2274
|
+
React.createElement(ModelCfg, { label: 'Worker 模型', cfgKey: 'workerModel', value: props.workerModel }),
|
|
1899
2275
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Worker 执行模型)')),
|
|
1900
2276
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
1901
|
-
React.createElement(ModelCfg, { label: '
|
|
2277
|
+
React.createElement(ModelCfg, { label: 'Verifier 模型', cfgKey: 'verifierModel', value: props.verifierModel }),
|
|
1902
2278
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)')),
|
|
1903
2279
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '两级超时(软超时只提醒主窗口,由你决定继续等待或终止)'),
|
|
1904
2280
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
@@ -1919,6 +2295,13 @@ function apply(ctx) {
|
|
|
1919
2295
|
React.createElement('input', { type: 'checkbox', checked: props.notifyDone !== false, onChange: function (e) { setCfg('notifyDone', e.target.checked) } }),
|
|
1920
2296
|
React.createElement('span', null, '✅ 完成回执(任务完成或阻塞时聚合播报)')),
|
|
1921
2297
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '歧义裁决通知不受这两个开关影响(任务等人裁决必须提醒)'),
|
|
2298
|
+
// 自测指南开关(verifyUserGuide,缺省 true,「通知」小节旁的「验收」小节):关掉后
|
|
2299
|
+
// ① Verifier prompt 不再要求「## 自测指南」段(省 token);② 验收落账不挂 userTest;
|
|
2300
|
+
// ③ 详情页自测指南块与报告「本版自测清单」段整块不渲染。老看板缺字段=开。
|
|
2301
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '验收'),
|
|
2302
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
|
|
2303
|
+
React.createElement('input', { type: 'checkbox', checked: props.verifyUserGuide !== false, onChange: function (e) { setCfg('verifyUserGuide', e.target.checked) } }),
|
|
2304
|
+
React.createElement('span', null, '📋 自测指南(验收结论附用户自测步骤,详情页与报告展示)')),
|
|
1922
2305
|
// 史诗拆分总开关(板级 epicSplit,缺省 true):**只关引导,不禁机制**——关掉后 Team 提示词不再
|
|
1923
2306
|
// 注入「大任务必须拆分」第 6 条、create-task 响应不再附 suggestSplit 软提示;显式传 parentId 建
|
|
1924
2307
|
// 子卡、史诗自动收口/hooks 状态机照常(用户/主窗口明确要拆时不受阻)。勾选态缺字段=开,与 host 同口径。
|
|
@@ -1942,7 +2325,7 @@ function apply(ctx) {
|
|
|
1942
2325
|
React.createElement('span', { style: { marginLeft: 'auto', fontSize: 9, padding: '1px 6px', borderRadius: 3, background: C.brand, color: C_INV } }, '忙碌')),
|
|
1943
2326
|
m.model ? React.createElement('div', { style: { fontSize: 9, color: C.warn, marginBottom: 3 }, title: '异构模型审查' }, '🧬 ' + m.model) : null,
|
|
1944
2327
|
curTask ? React.createElement('div', { onClick: function () { state.detailId = curTask.id; state.view = 'board'; notify() }, style: { fontSize: 10, color: C.brand, cursor: 'pointer', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: curTask.title }, '→ ' + curTask.title) : React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '→ ' + m.taskId),
|
|
1945
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '
|
|
2328
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '一次性子代理会话:' + (m.runId || '') }, '会话 ' + String(m.runId || '').slice(0, 8) + '…(用完即销毁)'))
|
|
1946
2329
|
}
|
|
1947
2330
|
var ws = ps.workers || [], vs = ps.verifiers || []
|
|
1948
2331
|
var _R2 = React; var _st = _R2.useState(''), editMsg = _st[0], setEditMsg = _st[1]
|