dsh-agent-board 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/index.mjs +1 -0
- package/lib/client/board-list.js +81 -14
- package/lib/client/dashboard.js +34 -6
- package/lib/client/kernel.js +47 -8
- package/lib/client/task-detail.js +47 -2
- package/lib/client.js +209 -30
- package/lib/core.mjs +93 -1
- package/lib/dispatch.mjs +19 -143
- package/lib/health.mjs +131 -0
- package/lib/rpc.mjs +26 -5
- package/package.json +2 -2
- package/test/core.test.mjs +313 -0
- package/test/health.test.mjs +155 -0
package/lib/client.js
CHANGED
|
@@ -232,7 +232,7 @@ function apply(ctx) {
|
|
|
232
232
|
|
|
233
233
|
var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
|
|
234
234
|
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, lessonPushed: {}, isRoot: 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 }
|
|
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, lessonPushed: {}, isRoot: 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: '' }
|
|
236
236
|
|
|
237
237
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
238
238
|
try {
|
|
@@ -258,11 +258,26 @@ function apply(ctx) {
|
|
|
258
258
|
function notify() { listeners.forEach(function (fn) { try { fn(state) } catch (_) {} }) }
|
|
259
259
|
function rpc(method, args) { var a = args || {}; a.sessionId = state.sessionId; return fetch('/dsh-agent-board', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: method, args: a }) }).then(function (r) { return r.json() }) }
|
|
260
260
|
|
|
261
|
+
// ===== 读路径错误防线(反馈 n-mut9q600tnpx)=====
|
|
262
|
+
// {error} 响应 / 网络 reject 一律保留旧数据(旧卡片继续显示)+ 面板头部下方非阻断错误条,
|
|
263
|
+
// 下次成功自动消失;绝不把 {error} 当空板渲染。console.warn 30s 去抖——3s 轮询失败每次都打会刷屏。
|
|
264
|
+
var readErrWarnAt = 0
|
|
265
|
+
function reportReadErr(msg) {
|
|
266
|
+
state.tasksErr = msg
|
|
267
|
+
var now = Date.now()
|
|
268
|
+
if (now - readErrWarnAt > 30000) { readErrWarnAt = now; try { console.warn('[task-board] ' + msg) } catch (_) {} }
|
|
269
|
+
notify()
|
|
270
|
+
}
|
|
271
|
+
function clearReadErr() { state.tasksErr = '' } // 仅 fetchTasks 成功路径调用(3s 轮询兜底,任何错误条都能随之消失)
|
|
272
|
+
function readErrText(e) { return String((e && e.message) || e || '网络异常') }
|
|
273
|
+
|
|
261
274
|
function fetchTasks() {
|
|
262
275
|
if (!state.sessionId) return
|
|
263
276
|
var epoch = reqEpoch
|
|
264
277
|
rpc('get-tasks').then(function (d) {
|
|
265
278
|
if (epoch !== reqEpoch) return // 会话已切换,丢弃过期响应
|
|
279
|
+
if (d && d.error) { reportReadErr('看板数据刷新失败:' + d.error); return } // error 分支:保留旧 tasks/设置,绝不当空板渲染
|
|
280
|
+
clearReadErr()
|
|
266
281
|
state.tasks = (d && d.tasks) || []
|
|
267
282
|
state.boardMode = (d && d.boardMode) || 'auto'
|
|
268
283
|
state.teamMode = !!(d && d.teamMode)
|
|
@@ -279,6 +294,12 @@ function apply(ctx) {
|
|
|
279
294
|
state.hardTimeoutMin = (d && d.hardTimeoutMin) || 120
|
|
280
295
|
state.poolStatus = (d && d.poolStatus) || null
|
|
281
296
|
state.usageSummary = (d && d.usageSummary) || null // token 消耗聚合(board 级,host 端现算)
|
|
297
|
+
// 架构健康提示(架构自省 L1):与 tasks 同源透传,HealthHints 组件直接读 state.healthHints,
|
|
298
|
+
// 不再自持 rpc('get-tasks')——消灭仪表盘打开期间的双轮询。老 host 无此字段 → 空数组零渲染
|
|
299
|
+
state.healthHints = (d && Array.isArray(d.healthHints)) ? d.healthHints : []
|
|
300
|
+
// 史诗父卡语义层:childStats 由 host 按 tasks 现算({parentId:{total,resolved,active,activeTitle}})。
|
|
301
|
+
// 缺省兼容——老 host 不返回该字段时置空对象,卡片/详情层遇空一律不渲染相关元素
|
|
302
|
+
state.childStats = (d && d.childStats) || {}
|
|
282
303
|
// 学习飞轮 v1 能力检测:老 host 不返回该字段 → 视为开启(默认开);只有显式 false 才关。
|
|
283
304
|
state.feedbackEnabled = !(d && d.feedbackEnabled === false)
|
|
284
305
|
state.isRoot = !d || d.isRoot !== false
|
|
@@ -289,10 +310,11 @@ function apply(ctx) {
|
|
|
289
310
|
state.escalatedIds = state.tasks.filter(function (t) { return t.escalation }).map(function (t) { return t.id })
|
|
290
311
|
if (newEsc.length > 0) { state.open = true; state.view = 'board'; state.detailId = newEsc[0].id }
|
|
291
312
|
notify()
|
|
292
|
-
}).catch(function () {})
|
|
313
|
+
}).catch(function (e) { if (epoch !== reqEpoch) return; reportReadErr('看板数据刷新失败:' + readErrText(e)) }) // 网络 reject 同口径
|
|
293
314
|
}
|
|
294
315
|
|
|
295
|
-
|
|
316
|
+
// fetchChildren 同病同药:{error} 保留旧 children + 错误条;成功路径不清条(交给 fetchTasks 成功兜底清除,避免并发响应互相抢)
|
|
317
|
+
function fetchChildren() { if (!state.sessionId) return; var epoch = reqEpoch; rpc('list-children').then(function (d) { if (epoch !== reqEpoch) return; if (d && d.error) { reportReadErr('子任务列表刷新失败:' + d.error); return } state.children = (d && d.children) || []; notify() }).catch(function (e) { if (epoch !== reqEpoch) return; reportReadErr('子任务列表刷新失败:' + readErrText(e)) }) }
|
|
296
318
|
|
|
297
319
|
// 活动心跳:对进行中/验收中的任务轮询子代理最近动作(卡片与详情展示"现在跑到哪了")
|
|
298
320
|
function fetchActivity() {
|
|
@@ -414,7 +436,21 @@ function apply(ctx) {
|
|
|
414
436
|
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') }) }
|
|
415
437
|
// #19 管线档位元数据
|
|
416
438
|
var pipeMeta = { full: { icon: 'flask-conical', label: '全流程(执行+验证)', short: '全流程' }, work: { icon: 'file-text', label: '免验证(只做不验)', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直接处理', short: '直办' } }
|
|
417
|
-
|
|
439
|
+
// ===== 耗时口径三分离(task-mutdnitw):排队 ≠ 执行 ≠ 验收,不再把排队算进耗时 =====
|
|
440
|
+
// waitOf:⏳ 排队时长 = createdAt → claimedAt(未领取则统计到 now)——"等了多久"
|
|
441
|
+
// execOf:⏱ 执行时长 = claimedAt → resolvedAt/archivedAt(未定则统计到 now)——"干了多久"
|
|
442
|
+
// 无 claimedAt(手工/direct 卡没有领取动作)回退原口径 createdAt → resolvedAt/archivedAt/now
|
|
443
|
+
// (验收时长 = resolvedAt → verifiedAt,仪表盘 verifyTimes 已有此口径,卡片不展示)
|
|
444
|
+
function fmtDur(s, e) { var m = Math.max(0, Math.round((e - s) / 60000)); if (m < 1) return '<1m'; if (m < 60) return m + 'm'; var h = Math.floor(m / 60); return h + 'h' + (m % 60 ? (m % 60) + 'm' : '') }
|
|
445
|
+
function waitOf(t) { try { var s = t.createdAt ? new Date(t.createdAt).getTime() : 0; if (!s) return ''; var e = t.claimedAt ? new Date(t.claimedAt).getTime() : Date.now(); return fmtDur(s, e) } catch (_) { return '' } }
|
|
446
|
+
function execOf(t) { try { var sRaw = t.claimedAt || t.createdAt; var s = sRaw ? new Date(sRaw).getTime() : 0; if (!s) return ''; var eRaw = t.resolvedAt || t.archivedAt; var e = eRaw ? new Date(eRaw).getTime() : Date.now(); return fmtDur(s, e) } catch (_) { return '' } }
|
|
447
|
+
// 卡片耗时徽章:draft/pending(还在排队)显「⏳ 等待」;执行后(含 blocked/归档)显「⏱ 执行」;tooltip 写清口径
|
|
448
|
+
function cardDur(t) {
|
|
449
|
+
var st = t.status
|
|
450
|
+
if (st === 'draft' || st === 'pending') { var w = waitOf(t); return w ? { txt: '⏳ 等待 ' + w, tip: '⏳ 排队时长:创建 → 被领取(还没被领取则统计创建至今)——"等了多久",不含执行' } : null }
|
|
451
|
+
var x = execOf(t)
|
|
452
|
+
return x ? { txt: '⏱ 执行 ' + x, tip: '⏱ 执行时长:被领取 → 完成/归档(进行中则统计领取至今)——"干了多久",不含排队与验收' + (t.claimedAt ? '' : ';本卡无领取记录(手工/直办),按创建起算') } : null
|
|
453
|
+
}
|
|
418
454
|
function elapsedMin(iso) { var s = iso ? new Date(iso).getTime() : 0; if (!s) return 0; return Math.max(0, Math.round((Date.now() - s) / 60000)) }
|
|
419
455
|
// 历史会话列表:优先用 host 留档的 t.runs(含全部重试);老任务回退到 claimedBy/verifierRun
|
|
420
456
|
function historyRuns(t) {
|
|
@@ -433,7 +469,7 @@ function apply(ctx) {
|
|
|
433
469
|
function BoardButton(props) {
|
|
434
470
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
435
471
|
var _a = useState(0), pendingCount = _a[0], setPendingCount = _a[1]; var _b = useState(false), isOpen = _b[0], setIsOpen = _b[1]; var _c = useState(0), escCount = _c[0], setEscCount = _c[1]; var _d2 = useState(true), isRoot = _d2[0], setIsRoot = _d2[1]
|
|
436
|
-
useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; reqEpoch++; state.tasks = []; state.children = []; state.activity = {}; state.archived = []; state.detailId = null; notify(); fetchTasks(); fetchChildren() } } }, [props && props.sessionId])
|
|
472
|
+
useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; reqEpoch++; state.tasks = []; state.children = []; state.activity = {}; state.archived = []; state.detailId = null; state.childStats = {}; state.tasksErr = ''; notify(); fetchTasks(); fetchChildren() } } }, [props && props.sessionId])
|
|
437
473
|
useEffect(function () { function update() { var n = 0, e = 0; for (var i = 0; i < state.tasks.length; i++) { if (state.tasks[i].status === 'pending') n++; if (state.tasks[i].escalation) e++ }; setPendingCount(n); setEscCount(e); setIsOpen(state.open); setIsRoot(state.isRoot) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
438
474
|
if (!isRoot) return null // 子代理会话不显示看板入口
|
|
439
475
|
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)
|
|
@@ -448,9 +484,10 @@ function apply(ctx) {
|
|
|
448
484
|
var _q = useState(state.teamMode), teamMode = _q[0], setTeamMode = _q[1]; var _dr = useState(state.dateRange), setDR = _dr[1]
|
|
449
485
|
var _wmo = useState(state.workMode), workMode = _wmo[0], setWorkModeState = _wmo[1]
|
|
450
486
|
var _cro = useState(state.createOpen), createOpen = _cro[0], setCreateOpen = _cro[1]; var _crf = useState(state.createFlash), createFlash = _crf[0], setCreateFlash = _crf[1]
|
|
487
|
+
var _te = useState(state.tasksErr), tasksErr = _te[0], setTasksErr = _te[1] // 读路径错误条(无错不渲染)
|
|
451
488
|
var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
|
|
452
489
|
var _fbo = useState(state.feedbackEnabled), fbEnabled = _fbo[0], setFbEnabled = _fbo[1]
|
|
453
|
-
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); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
490
|
+
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); 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) } }, [])
|
|
454
491
|
if (!open) return null
|
|
455
492
|
if (state.isRoot === false) return null // 子代理会话不渲染看板面板
|
|
456
493
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
@@ -470,7 +507,7 @@ function apply(ctx) {
|
|
|
470
507
|
var ta = a.archivedAt || a.resolvedAt || a.createdAt || '', tb = b.archivedAt || b.resolvedAt || b.createdAt || ''
|
|
471
508
|
return state.archSort === 'time-asc' ? ta.localeCompare(tb) : tb.localeCompare(ta)
|
|
472
509
|
})
|
|
473
|
-
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 ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
|
|
510
|
+
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 } }, '「+ 新建任务」'), '创建第一张卡,或在对话里说一句 ', React.createElement('code', { style: { fontSize: 11, background: C.nested, padding: '0 4px', borderRadius: 3 } }, 'task_create')) : null, React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
|
|
474
511
|
}
|
|
475
512
|
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' } },
|
|
476
513
|
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 } },
|
|
@@ -484,7 +521,9 @@ function apply(ctx) {
|
|
|
484
521
|
React.createElement(WorkModeSwitch, { mode: workMode }),
|
|
485
522
|
React.createElement('button', { onClick: function () { fetchTasks(); fetchChildren() }, title: '刷新', style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 12, color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('refresh-cw', 13)),
|
|
486
523
|
React.createElement('button', { onClick: function () { state.open = false; state.detailId = null; notify() }, title: '关闭', style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 14, color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('x', 14)))),
|
|
487
|
-
React.createElement('div', { style: {
|
|
524
|
+
tasksErr ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '4px 12px', fontSize: 11, color: C.err, background: 'rgba(239,68,68,.08)', borderBottom: '1px solid ' + C.border, flexShrink: 0 }, title: '刷新失败不影响已显示数据,下次轮询成功自动恢复' }, ic('alert-triangle', 12), React.createElement('span', null, tasksErr)) : null,
|
|
525
|
+
// 空态统一(反馈 n-mut9rzoq3flu):空板不再整块替换为「🎉 暂无任务」单行,统一走 content 渲染六列骨架;引导 CTA 行在 content 内部按空板条件插入
|
|
526
|
+
React.createElement('div', { style: { flex: 1, overflowY: 'auto', padding: '10px 12px' } }, content),
|
|
488
527
|
createOpen ? React.createElement(CreateTaskForm, null) : null)
|
|
489
528
|
}
|
|
490
529
|
|
|
@@ -560,14 +599,62 @@ function apply(ctx) {
|
|
|
560
599
|
function isCardHover(id, kind) { return String(state.cardHover || '') === (id + '|' + kind) }
|
|
561
600
|
function setHover(id, kind) { var v = id ? (id + '|' + kind) : ''; if (state.cardHover !== v) { state.cardHover = v; notify() } }
|
|
562
601
|
|
|
563
|
-
|
|
602
|
+
// ===== 卡片「当前动作」行摘要化(client 侧提炼;host 仍下发原文,不改 host)=====
|
|
603
|
+
// host 的 activity 形如 `🔧 pwsh {"command": "Get-ChildItem …"}`(90 字符硬截,常断在 JSON 中段,
|
|
604
|
+
// 原样展示零信息量,反馈 n-mut914xana0t)或 `💬 自由文本`。这里提炼为「工具名 + 关键参数摘要」:
|
|
605
|
+
// pwsh → 命令体;read/write/edit → 文件 basename;browser_* → 动作名 + 目标(target/url/ref);
|
|
606
|
+
// 参数摘要 40 字符内、优先在空格/路径分隔符处断点;解析失败/自由文本回退为原文前 40 字符(头部截断)。
|
|
607
|
+
function actCut(s, n) { s = String(s || ''); if (s.length <= n) return s; var c = s.slice(0, n); var last = c.charCodeAt(c.length - 1); if (last >= 0xD800 && last <= 0xDBFF) c = c.slice(0, -1); var sp = Math.max(c.lastIndexOf(' '), c.lastIndexOf('/'), c.lastIndexOf('\\')); if (sp >= Math.floor(n / 2)) c = c.slice(0, sp); return c.replace(/[\s/\\]+$/, '') + '…' } // 不斩断代理对(emoji)
|
|
608
|
+
// 从不完整 JSON 文本里抢指定 key 的第一个字符串值(host 文本常被硬截,JSON.parse 多半失败,正则直达)
|
|
609
|
+
function actArg(rest, key) { var m = String(rest || '').match(new RegExp('"' + key + '"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)')); return m ? m[1] : '' }
|
|
610
|
+
function actBase(p) { p = String(p || ''); var i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\')); return i >= 0 ? p.slice(i + 1) : p }
|
|
611
|
+
function activitySummary(text) {
|
|
612
|
+
var s = String(text || '').trim()
|
|
613
|
+
if (!s) return ''
|
|
614
|
+
if (s.indexOf('💬') === 0) return actCut(s, 40) // 自由文本:保留 💬 前缀从头部截
|
|
615
|
+
var body = s.replace(/^🔧\s*/, '') // 去工具调用前缀(host 格式 🔧 <tool> <argsJSON>)
|
|
616
|
+
var m = body.match(/^([A-Za-z_][\w-]*)\s*([\s\S]*)$/)
|
|
617
|
+
if (!m) return actCut(s, 40)
|
|
618
|
+
var tool = m[1], rest = (m[2] || '').trim()
|
|
619
|
+
if (!rest) return actCut(s, 40)
|
|
620
|
+
var v = ''
|
|
621
|
+
if (tool === 'pwsh') v = actArg(rest, 'command')
|
|
622
|
+
else if (tool === 'read' || tool === 'write' || tool === 'edit') v = actBase(actArg(rest, 'file_path'))
|
|
623
|
+
if (v) return tool + ' ' + actCut(v, 40)
|
|
624
|
+
if (tool.indexOf('browser_') === 0) {
|
|
625
|
+
var act = actArg(rest, 'action'); var tgt = actArg(rest, 'target') || actArg(rest, 'url') || actArg(rest, 'ref')
|
|
626
|
+
var parts = []; if (act) parts.push(act); if (tgt) parts.push(tgt)
|
|
627
|
+
if (parts.length) return tool + ' ' + actCut(parts.join(' '), 40)
|
|
628
|
+
}
|
|
629
|
+
return actCut(s, 40) // 无法解析:原文前 40 字符(从头部截,不再断在 JSON 中段)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// 史诗/依赖/父子识别层(childStats 缺省兼容:host 未返回该字段时一律不渲染相关元素):
|
|
633
|
+
// 父卡 = 元信息行首位「📦 史诗 · resolved/total」徽章 + 3px 迷你进度条 + activeTitle 非空时「▸ 在跑」行;
|
|
634
|
+
// 子卡 = 标题下一行小字「↳ 父任务标题」(从 state.tasks 找父卡,找不到回退 shortId);
|
|
635
|
+
// 依赖未满足的待办卡 = 卡片底部灰字「⛓ 等待「第一个未满足依赖标题」」(多个时附「 等 N 个」)。
|
|
636
|
+
// ===== 调研门禁·UI:「⚠️ 无调研」徽章判定(纯 client 侧现算,host 不下发该字段)=====
|
|
637
|
+
// 口径:pipeline ∈ {full, work}(direct 主窗口自处理,无需调研注入)且 touches 非空(声明了要改文件)
|
|
638
|
+
// 且 context.files / context.notes 皆空。context 缺省兼容:老任务无 context 对象按空处理——
|
|
639
|
+
// 即老任务也会亮徽章,这是预期行为(立单质量可见即问责;draft/pending 等状态照常显示,与状态无关)。
|
|
640
|
+
function noResearch(t) {
|
|
641
|
+
var pl = t.pipeline || 'full'
|
|
642
|
+
if (pl !== 'full' && pl !== 'work') return false
|
|
643
|
+
if (!Array.isArray(t.touches) || t.touches.length === 0) return false
|
|
644
|
+
var cx = t.context || {}
|
|
645
|
+
var files = Array.isArray(cx.files) ? cx.files : []
|
|
646
|
+
var notes = typeof cx.notes === 'string' ? cx.notes.trim() : ''
|
|
647
|
+
return files.length === 0 && notes.length === 0
|
|
648
|
+
}
|
|
649
|
+
function Card(props) { var t = props.task; var pc = prioColor[t.priority] || prioColor.low; var dragging = state.dragTask === t.id; var preview = (t.deliverable && t.deliverable.summary) || t.resolution; var critGlow = t.priority === 'critical' && !t.escalation; var sel = !!state.selected[t.id]; var pm = pipeOf(t); var depBlock = t.status === 'pending' && depsBlocked(t); var delOk = !state.selectMode && canDelete(t); var cs = (state.childStats && state.childStats[t.id]) || null; var epic = !!(cs && cs.total > 0); var parentT = t.parentId ? getTask(t.parentId) : null; var depWaitTitle = ''; var depWaitN = 0; if (depBlock) { t.dependsOn.forEach(function (id) { var d = getTask(id); if (!d || (d.status !== 'resolved' && d.status !== 'archived')) { depWaitN++; if (!depWaitTitle) depWaitTitle = d && d.title ? d.title : id } }) } var durB = cardDur(t); return React.createElement('div', { draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onClick: function () { if (state.selectMode) { if (state.selected[t.id]) delete state.selected[t.id]; else state.selected[t.id] = true; notify() } else { state.detailId = t.id; notify() } }, style: { border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡死 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
|
|
650
|
+
epic ? React.createElement('span', { title: '史诗父卡:' + cs.resolved + '/' + cs.total + ' 个子任务已完成', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + cs.resolved + '/' + cs.total) : null,
|
|
564
651
|
t.frozen ? React.createElement('span', { title: '已冻结:不参与自动派发(详情页可「解除冻结」)', style: { color: C.brand, fontWeight: 600 } }, '❄ 冻结') : null,
|
|
565
652
|
(Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
|
|
566
653
|
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
567
654
|
(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,
|
|
568
655
|
t.status === 'in-progress' && t.claimedBy ? React.createElement('span', null, '⚡ ' + shortId(t.claimedBy)) : null,
|
|
569
656
|
t.assignee ? React.createElement('span', null, '👤→' + shortId(t.assignee)) : null,
|
|
570
|
-
|
|
657
|
+
durB ? React.createElement('span', { title: durB.tip }, durB.txt) : null,
|
|
571
658
|
// 删除入口(仅草稿/待办/阻塞,多选模式下隐藏以免误触):hover 才由透明转红,平时不抢视觉
|
|
572
659
|
delOk ? React.createElement('span', {
|
|
573
660
|
onClick: function (e) { e.stopPropagation(); deleteTask(t.id, t.title) }, // 阻止冒泡:不打开详情页
|
|
@@ -575,10 +662,14 @@ function apply(ctx) {
|
|
|
575
662
|
title: '删除任务(不可恢复;执行中请先终止,已落定请用归档)',
|
|
576
663
|
style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', cursor: 'pointer', color: C.err, opacity: isCardHover(t.id, 'del') ? 1 : 0, transition: 'opacity .15s' }
|
|
577
664
|
}, ic('trash-2', 11)) : null),
|
|
578
|
-
|
|
665
|
+
// 史诗迷你进度条(3px 高,resolved/total 比例,ok 色填充)+ 在跑子任务行(activeTitle 非空才渲染)
|
|
666
|
+
epic ? React.createElement('div', { style: { height: 3, borderRadius: 2, background: C.nested, marginTop: 4, overflow: 'hidden' }, title: '子任务进度 ' + cs.resolved + '/' + cs.total }, React.createElement('div', { style: { height: '100%', width: Math.max(0, Math.min(100, Math.round((cs.resolved / cs.total) * 100))) + '%', background: C.ok, borderRadius: 2, transition: 'width .3s' } })) : null,
|
|
667
|
+
epic && cs.activeTitle ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '在跑子任务:' + cs.activeTitle }, '▸ 在跑:' + cs.activeTitle) : null,
|
|
668
|
+
// 当前动作行:展示摘要(工具名+关键参数),悬停 title 仍是 host 原文
|
|
669
|
+
(t.status === 'in-progress' || t.status === 'verifying') && state.activity[t.id] ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: state.activity[t.id] }, '👁 ' + activitySummary(state.activity[t.id])) : null,
|
|
579
670
|
// 里程碑进展(Worker 主动上报的轻量进展,kind=progress):进行中的卡片显示最新一条 + 相对时间,无则不显示
|
|
580
671
|
t.status === 'in-progress' && t.lastProgress && t.lastProgress.text ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '最近进展(' + (t.lastProgress.by || '') + ' · ' + fmtTime(t.lastProgress.at) + '):' + t.lastProgress.text }, '📈 ' + t.lastProgress.text + ' · ' + ago(t.lastProgress.at)) : null,
|
|
581
|
-
t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null) }
|
|
672
|
+
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) }
|
|
582
673
|
|
|
583
674
|
function ArchiveView() {
|
|
584
675
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
@@ -639,6 +730,8 @@ function apply(ctx) {
|
|
|
639
730
|
var _dr = useState(wm === 'team'), asDraft = _dr[0], setAsDraft = _dr[1]
|
|
640
731
|
var _bs = useState(false), busy = _bs[0], setBusy = _bs[1]
|
|
641
732
|
var _er = useState(''), err = _er[0], setErr = _er[1]
|
|
733
|
+
var _ok = useState(''), okMsg = _ok[0], setOkMsg = _ok[1] // 成功行(仅「创建成功但响应带 warning」的不关窗路径展示)
|
|
734
|
+
var _wn = useState(''), warn = _wn[0], setWarn = _wn[1] // host create-task 成功路径附带的 warning 原文(可能多条合并,原样黄色展示)
|
|
642
735
|
// 依赖候选:当前看板 status 为 draft/pending 的任务
|
|
643
736
|
var cands = state.tasks.filter(function (t) { return t.status === 'draft' || t.status === 'pending' })
|
|
644
737
|
function close() { state.createOpen = false; notify() }
|
|
@@ -649,13 +742,17 @@ function apply(ctx) {
|
|
|
649
742
|
if (!t) { setErr('⚠️ 请先填写标题'); return }
|
|
650
743
|
var touches = touchesRaw.split(/[\n,,;;]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
|
|
651
744
|
var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
|
|
652
|
-
setBusy(true); setErr('')
|
|
745
|
+
setBusy(true); setErr(''); setOkMsg(''); setWarn('')
|
|
653
746
|
rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
|
|
654
747
|
setBusy(false)
|
|
655
748
|
if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
|
|
749
|
+
fetchTasks()
|
|
750
|
+
// 调研门禁·软警告:host 成功路径可附 warning 字符串(如空描述提醒,可能多条合并)——不拦截创建、
|
|
751
|
+
// 也不打断成功提示:表单保持打开,成功行 + ⚠️ 黄色 warning 原文行同屏展示,读完自行关闭;
|
|
752
|
+
// 清掉标题防「再点一次创建」连击出重复卡(空标题会被校验拦住)。无 warning 维持原关窗 + 头部 flash 行为。
|
|
753
|
+
if (r && r.warning) { setOkMsg(asDraft ? '✅ 已存草稿' : '✅ 已创建任务'); setWarn(String(r.warning)); setTitle(''); return }
|
|
656
754
|
state.createOpen = false
|
|
657
755
|
flashCreated(asDraft ? '✅ 已存草稿' : '✅ 已创建任务')
|
|
658
|
-
fetchTasks()
|
|
659
756
|
notify()
|
|
660
757
|
}).catch(function (e) { setBusy(false); setErr('⚠️ ' + String(e)) })
|
|
661
758
|
}
|
|
@@ -664,11 +761,16 @@ function apply(ctx) {
|
|
|
664
761
|
function field(label, node) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node) }
|
|
665
762
|
var btnGhost = { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 5, background: 'transparent', color: C.text2, cursor: 'pointer' }
|
|
666
763
|
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 }
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
764
|
+
// 视口级 overlay(修裁切,反馈 n-mut9rzl4mxe4):fixed 全屏遮罩脱离看板抽屉(maxHeight 60vh + overflow hidden)
|
|
765
|
+
// 的裁剪上下文(抽屉无 transform/filter,fixed 相对视口定位不被裁);弹窗限高 80vh、字段区内部滚动、
|
|
766
|
+
// 底部按钮区吸底常驻——标题与「创建并派发」始终同屏,无需盲滚。
|
|
767
|
+
return React.createElement('div', { style: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }, onClick: function (e) { if (e.target === e.currentTarget) close() } },
|
|
768
|
+
React.createElement('div', { style: { width: 440, maxWidth: '100%', maxHeight: '80vh', display: 'flex', flexDirection: 'column', background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 12px 32px rgba(0,0,0,0.28)', overflow: 'hidden' } },
|
|
769
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 12px', borderBottom: '1px solid ' + C.border, flexShrink: 0 } },
|
|
670
770
|
React.createElement('span', { style: { fontSize: 13, fontWeight: 600, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 5 } }, ic('plus', 14), '新建任务'),
|
|
671
771
|
React.createElement('button', { onClick: close, title: '关闭', style: { border: 'none', background: 'transparent', cursor: 'pointer', color: C.text2, display: 'inline-flex' } }, ic('x', 14))),
|
|
772
|
+
// 字段区:内容超出 80vh 时在此内部滚动
|
|
773
|
+
React.createElement('div', { style: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '10px 12px 4px' } },
|
|
672
774
|
field('标题 *', React.createElement('input', { value: title, onChange: function (e) { setTitle(e.target.value) }, placeholder: '一句话说清要做什么', style: inp })),
|
|
673
775
|
field('描述', React.createElement('textarea', { value: desc, onChange: function (e) { setDesc(e.target.value) }, rows: 3, placeholder: '背景 / 目标 / 约束(可空)', style: Object.assign({}, inp, { resize: 'vertical', minHeight: 46 }) })),
|
|
674
776
|
React.createElement('div', { style: { display: 'flex', gap: 8 } },
|
|
@@ -689,11 +791,15 @@ function apply(ctx) {
|
|
|
689
791
|
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: C.text, cursor: 'pointer', marginBottom: 6, flexWrap: 'wrap' } },
|
|
690
792
|
React.createElement('input', { type: 'checkbox', checked: asDraft, onChange: function (e) { setAsDraft(e.target.checked) } }),
|
|
691
793
|
'存为草稿',
|
|
692
|
-
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, wm === 'team' ? '(Team 托管默认草稿;取消勾选 = 立即派发)' : '(先补依赖/上下文,稍后统一发布)')),
|
|
693
|
-
|
|
694
|
-
React.createElement('div', { style: {
|
|
695
|
-
React.createElement('
|
|
696
|
-
React.createElement('
|
|
794
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, wm === 'team' ? '(Team 托管默认草稿;取消勾选 = 立即派发)' : '(先补依赖/上下文,稍后统一发布)'))),
|
|
795
|
+
// 底部:错误行 + 按钮区吸底常驻(flexShrink:0,不随字段区滚动)
|
|
796
|
+
React.createElement('div', { style: { flexShrink: 0, padding: '8px 12px 12px', borderTop: '1px solid ' + C.border, background: C.bg } },
|
|
797
|
+
okMsg ? React.createElement('div', { style: { fontSize: 11, color: C.ok, marginBottom: 6 } }, okMsg) : null,
|
|
798
|
+
warn ? React.createElement('div', { style: { fontSize: 11, color: C.warn, marginBottom: 6, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, '⚠️ ' + warn) : null,
|
|
799
|
+
err ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 6 } }, err) : null,
|
|
800
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
|
|
801
|
+
React.createElement('button', { onClick: close, style: btnGhost }, '取消'),
|
|
802
|
+
React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即进入派发池', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并派发'))))))
|
|
697
803
|
}
|
|
698
804
|
|
|
699
805
|
// ============================================================================
|
|
@@ -765,6 +871,32 @@ function apply(ctx) {
|
|
|
765
871
|
msg ? React.createElement('div', { style: { fontSize: 10, color: C.err, marginTop: 3 } }, msg) : null)
|
|
766
872
|
}
|
|
767
873
|
|
|
874
|
+
// 父子区块(与 DepsSection 并列):
|
|
875
|
+
// 子卡 = 父链面包屑「↳ 史诗:<父标题>」,点击回跳父卡详情;
|
|
876
|
+
// 父卡 = 非归档子任务清单(状态色点 + 标题 + 状态标签,点击直达子卡详情;标题行汇总 resolved/total)。
|
|
877
|
+
// 数据源是 state.tasks 按 parentId 现算(parentId 为老字段),不依赖 host 的 childStats,天然缺省兼容;
|
|
878
|
+
// 既无父也无子的普通卡整块不渲染。嵌套史诗(本身也是子卡的父卡)两段同区块上下排列。
|
|
879
|
+
function FamilySection(props) {
|
|
880
|
+
var task = props.task
|
|
881
|
+
var parentT = task.parentId ? getTask(task.parentId) : null
|
|
882
|
+
var kids = state.tasks.filter(function (x) { return x.parentId === task.id && x.status !== 'archived' })
|
|
883
|
+
if (!task.parentId && kids.length === 0) return null
|
|
884
|
+
var resolvedN = kids.filter(function (x) { return x.status === 'resolved' }).length
|
|
885
|
+
function jump(id) { state.detailId = id; notify() }
|
|
886
|
+
return React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
|
|
887
|
+
task.parentId ? React.createElement('div', { style: { fontSize: 11, marginBottom: kids.length ? 6 : 0 } },
|
|
888
|
+
React.createElement('span', { style: { color: C.text2, fontWeight: 600 } }, '↳ 史诗:'),
|
|
889
|
+
React.createElement('span', { onClick: function () { if (parentT) jump(task.parentId) }, title: parentT ? parentT.title + ' (' + task.parentId + ')' : task.parentId, style: { color: parentT ? C.brand : C.text2, cursor: parentT ? 'pointer' : 'default', fontWeight: 600 } }, parentT && parentT.title ? parentT.title : shortId(task.parentId))) : null,
|
|
890
|
+
kids.length > 0 ? React.createElement('div', null,
|
|
891
|
+
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '📦 子任务 · ' + resolvedN + '/' + kids.length + ' 已完成'),
|
|
892
|
+
kids.map(function (x) {
|
|
893
|
+
return React.createElement('div', { key: x.id, onClick: function () { jump(x.id) }, title: (statusLabels[x.status] || x.status) + ' · ' + x.id, style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3, fontSize: 10, cursor: 'pointer' } },
|
|
894
|
+
React.createElement('span', { style: { width: 7, height: 7, borderRadius: '50%', background: statusColors[x.status] || C.text2, flexShrink: 0 } }),
|
|
895
|
+
React.createElement('span', { style: { color: C.brand, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, x.title || x.id),
|
|
896
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0 } }, statusLabels[x.status] || x.status))
|
|
897
|
+
})) : null)
|
|
898
|
+
}
|
|
899
|
+
|
|
768
900
|
function DetailView() {
|
|
769
901
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
|
|
770
902
|
var _a = useState(task ? task.title : ''), editTitle = _a[0], setEditTitle = _a[1]; var _b = useState(task ? task.description || '' : ''), editDesc = _b[0], setEditDesc = _b[1]; var _c = useState(false), saving = _c[0], setSaving = _c[1]; var _d = useState(state.boardMode), mode = _d[0], setMode = _d[1]
|
|
@@ -800,6 +932,8 @@ function apply(ctx) {
|
|
|
800
932
|
return React.createElement('div', { style: { padding: '4px 2px' } },
|
|
801
933
|
React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
|
|
802
934
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 } }, React.createElement('span', { style: { fontSize: 10, padding: '1px 6px', borderRadius: 3, background: 'color-mix(in srgb, ' + (prioColor[task.priority] || prioColor.low) + ' 20%, transparent)', color: (prioColor[task.priority] || prioColor.low) } }, prioLabel[task.priority] || '中'), React.createElement('span', { style: { fontSize: 11, padding: '1px 8px', borderRadius: 3, background: C.nested, color: C.text } }, statusLabels[task.status] || task.status), React.createElement('select', { value: task.pipeline || 'full', onChange: function (e) { rpc('update-task', { taskId: task.id, pipeline: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '管线档位', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.text2 } }, React.createElement('option', { value: 'full' }, '全流程(执行+验证)'), React.createElement('option', { value: 'work' }, '免验证(只做不验)'), React.createElement('option', { value: 'direct' }, '主窗口处理')), task.pipelineAuto ? React.createElement('span', { style: { fontSize: 9, color: C.text2 }, title: '由规则自动分类,可手动覆盖' }, 'auto') : null, isManual ? React.createElement('span', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('user', 10), '手动派发') : null),
|
|
935
|
+
// 最近失败(host settle 失败路径写入 task.lastError;缺字段静默不渲染,≤2 行截断,悬停看全文)
|
|
936
|
+
task.lastError ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 8, padding: '4px 8px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)', display: '-webkit-box', WebkitBoxOrient: 'vertical', WebkitLineClamp: 2, overflow: 'hidden', wordBreak: 'break-all' }, title: String(task.lastError) }, '最近失败: ' + String(task.lastError)) : null,
|
|
803
937
|
task.frozen ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.brand, borderRadius: 6, background: 'color-mix(in srgb, ' + C.brand + ' 8%, transparent)', display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
804
938
|
React.createElement('span', { style: { fontWeight: 600 } }, '❄ 已冻结:不参与自动派发'),
|
|
805
939
|
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '裁决挂起' + (task.frozenAt ? '(' + ago(task.frozenAt) + ')' : '') + ',留出补 dependsOn/上下文的时间;解冻后重新入池'),
|
|
@@ -840,8 +974,25 @@ function apply(ctx) {
|
|
|
840
974
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 3 } }, '任务描述'),
|
|
841
975
|
React.createElement('textarea', { value: editDesc, onChange: function (e) { setEditDesc(e.target.value) }, rows: 3, style: { width: '100%', fontSize: 12, padding: '6px 8px', marginBottom: 6, border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' } }),
|
|
842
976
|
task.context && task.context.instructions ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '指引: ' + task.context.instructions) : null,
|
|
843
|
-
|
|
844
|
-
|
|
977
|
+
// ===== 调研注入清单(调研门禁·详情侧,与卡片「⚠️ 无调研」徽章同口径)=====
|
|
978
|
+
// 一行汇总派发时注入给 Worker/Verifier 的调研上下文:files 数量 + basename 清单(悬停看完整路径)、
|
|
979
|
+
// notes 字数统计(悬停看原文,超 500 字截断标注);两者皆空 → 明示「无调研注入」。
|
|
980
|
+
// context 缺省兼容:老任务无 context 对象按空处理——同样亮出空态,这是预期(立单质量问责)。
|
|
981
|
+
// 行风格对齐上方「最近失败」行(border + color-mix 淡底):空态 warn 黄调,有内容走中性 nested。
|
|
982
|
+
(function () {
|
|
983
|
+
var cx = task.context || {}
|
|
984
|
+
var files = Array.isArray(cx.files) ? cx.files : []
|
|
985
|
+
var notes = typeof cx.notes === 'string' ? cx.notes : ''
|
|
986
|
+
var empty = files.length === 0 && notes.trim().length === 0
|
|
987
|
+
var parts = []
|
|
988
|
+
if (files.length) parts.push(files.length + ' 个文件(' + files.map(function (f) { return actBase(f) }).join('、') + ')')
|
|
989
|
+
if (notes.trim()) parts.push('调研笔记 ' + notes.length + ' 字')
|
|
990
|
+
var tip = empty ? '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes'
|
|
991
|
+
: (files.length ? '完整路径:\n' + files.join('\n') : '') + (notes.trim() ? (files.length ? '\n\n' : '') + '调研笔记原文:\n' + (notes.length > 500 ? notes.slice(0, 500) + '…(共 ' + notes.length + ' 字)' : notes) : '')
|
|
992
|
+
return React.createElement('div', { style: { fontSize: 11, marginBottom: 8, padding: '4px 8px', border: '1px solid ' + (empty ? C.warn : C.border), borderRadius: 6, background: empty ? 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' : C.nested, color: empty ? C.warn : C.text2, wordBreak: 'break-word' }, title: tip },
|
|
993
|
+
'📎 调研注入: ' + (empty ? '无调研注入——Worker 需自行定位,建议补 contextFiles/contextNotes' : parts.join(' · ')))
|
|
994
|
+
})(),
|
|
995
|
+
React.createElement(FamilySection, { task: task }),
|
|
845
996
|
React.createElement(DepsSection, { task: task }),
|
|
846
997
|
task.acceptance ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', background: C.nested, borderRadius: 4, borderLeft: '2px solid ' + C.ok, fontFamily: 'monospace', display: 'flex', alignItems: 'center', gap: 4 } }, ic('flask-conical', 11), '硬性验收: ' + task.acceptance) : null,
|
|
847
998
|
task.resolution ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', background: C.nested, borderRadius: 4, borderLeft: '2px solid ' + C.warn, maxHeight: 120, overflowY: 'auto', whiteSpace: 'pre-wrap' } }, '📝 ' + task.resolution) : null,
|
|
@@ -900,19 +1051,21 @@ function apply(ctx) {
|
|
|
900
1051
|
// ============================================================================
|
|
901
1052
|
// dashboard —— 仪表盘域:统计(computeStats/TrendChart/StatCard/BarRow)· 范围筛选(RangeFilter)
|
|
902
1053
|
// · 全局总览(GlobalBoards)· 报告(buildReport/ReportButton)· Token 消耗(TokenUsage)
|
|
1054
|
+
// · 架构健康(HealthHints,架构自省 L1)
|
|
903
1055
|
// · 团队池(TeamView/PoolStatus)· 设置(WorkModeSwitch/PoolCfg/MinCfg/ModelCfg/PoolCfgPopover)
|
|
904
1056
|
// 本文件是 apply(ctx) 函数体片段,不可独立运行;由 scripts/build-client.cjs 按序拼接生成 lib/client.js。
|
|
905
1057
|
// ============================================================================
|
|
906
1058
|
|
|
907
1059
|
function computeStats(tasks) {
|
|
908
|
-
var total = tasks.length, byStatus = {}, byPriority = {}, byAgent = {},
|
|
1060
|
+
var total = tasks.length, byStatus = {}, byPriority = {}, byAgent = {}, queueTimes = [], execTimes = [], verifyTimes = [], todayDone = 0, todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); var recentActivity = []
|
|
909
1061
|
var dailyDone = [] // #15 近 7 天每日完成趋势
|
|
910
1062
|
for (var di = 6; di >= 0; di--) { var dayStart = new Date(todayStart); dayStart.setDate(dayStart.getDate() - di); var dayEnd = new Date(dayStart); dayEnd.setDate(dayEnd.getDate() + 1); dailyDone.push({ label: (dayStart.getMonth() + 1) + '/' + dayStart.getDate(), count: 0, start: dayStart, end: dayEnd }) }
|
|
911
|
-
tasks.forEach(function (t) { byStatus[t.status] = (byStatus[t.status] || 0) + 1; byPriority[t.priority || 'medium'] = (byPriority[t.priority || 'medium'] || 0) + 1; if (t.claimedBy && (t.status === 'in-progress' || t.status === 'verifying')) { if (!byAgent[t.claimedBy]) byAgent[t.claimedBy] = 0; byAgent[t.claimedBy]++ } if (t.
|
|
1063
|
+
tasks.forEach(function (t) { byStatus[t.status] = (byStatus[t.status] || 0) + 1; byPriority[t.priority || 'medium'] = (byPriority[t.priority || 'medium'] || 0) + 1; if (t.claimedBy && (t.status === 'in-progress' || t.status === 'verifying')) { if (!byAgent[t.claimedBy]) byAgent[t.claimedBy] = 0; byAgent[t.claimedBy]++ } if (t.createdAt && t.claimedAt) queueTimes.push(new Date(t.claimedAt) - new Date(t.createdAt)); if (t.claimedAt && t.resolvedAt) execTimes.push(new Date(t.resolvedAt) - new Date(t.claimedAt)); if (t.verifiedAt && t.resolvedAt) verifyTimes.push(new Date(t.verifiedAt) - new Date(t.resolvedAt)); if (t.resolvedAt) { var rd = new Date(t.resolvedAt); if (rd >= todayStart) todayDone++; for (var k = 0; k < dailyDone.length; k++) { if (rd >= dailyDone[k].start && rd < dailyDone[k].end) { dailyDone[k].count++; break } } } if (Array.isArray(t.history)) t.history.forEach(function (h) { recentActivity.push({ taskId: t.id, title: t.title, from: h.from, to: h.to, actor: h.actor, timestamp: h.timestamp, note: h.note }) }) })
|
|
912
1064
|
recentActivity.sort(function (a, b) { return (b.timestamp || '').localeCompare(a.timestamp || '') }); recentActivity = recentActivity.slice(0, 10)
|
|
913
1065
|
function avgMs(arr) { if (arr.length === 0) return null; var s = arr.reduce(function (a, b) { return a + b }, 0); return Math.round(s / arr.length) }
|
|
914
1066
|
function fmtMs(ms) { if (!ms) return '-'; var m = Math.floor(ms / 60000); if (m < 60) return m + ' 分钟'; var h = Math.floor(m / 60); if (h < 24) return h + ' 小时 ' + (m % 60) + ' 分'; return Math.floor(h / 24) + ' 天 ' + (h % 24) + ' 时' }
|
|
915
|
-
|
|
1067
|
+
// 耗时口径三分离(task-mutdnitw):avgQueue=平均排队(创建→被领取)、avgExec=平均执行(被领取→完成),均只统计领取过的卡;avgVerify 口径不动(完成→验收)
|
|
1068
|
+
return { total: total, byStatus: byStatus, byPriority: byPriority, byAgent: byAgent, avgQueue: fmtMs(avgMs(queueTimes)), avgExec: fmtMs(avgMs(execTimes)), avgVerify: fmtMs(avgMs(verifyTimes)), todayDone: todayDone, dailyDone: dailyDone, recentActivity: recentActivity }
|
|
916
1069
|
}
|
|
917
1070
|
|
|
918
1071
|
function TrendChart(props) {
|
|
@@ -932,6 +1085,9 @@ function apply(ctx) {
|
|
|
932
1085
|
|
|
933
1086
|
function StatCard(props) { return React.createElement('div', { style: { flex: '1 1 0', minWidth: 80, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, textAlign: 'center' } }, React.createElement('div', { style: { fontSize: 20, fontWeight: 700, color: props.color || C.text } }, String(props.value)), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 2 } }, props.label)) }
|
|
934
1087
|
|
|
1088
|
+
// 双行指标卡(耗时口径三分离:原「平均完成时间」一个展示位拆为 排队/执行 两行,不挤占其他指标位)
|
|
1089
|
+
function StatCardDual(props) { return React.createElement('div', { style: { flex: '1 1 0', minWidth: 80, padding: '6px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, textAlign: 'center' }, title: props.title }, React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: props.color || C.text } }, String(props.value1)), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 1 } }, props.label1), React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: props.color || C.text, marginTop: 3 } }, String(props.value2)), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 1 } }, props.label2)) }
|
|
1090
|
+
|
|
935
1091
|
function BarRow(props) { var pct = props.total > 0 ? (props.count / props.total * 100) : 0; return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 } }, React.createElement('span', { style: { width: 40, fontSize: 10, color: C.text2, textAlign: 'right', flexShrink: 0 } }, props.label), React.createElement('div', { style: { flex: 1, height: 8, background: C.nested, borderRadius: 4, overflow: 'hidden' } }, React.createElement('div', { style: { height: '100%', width: pct + '%', background: props.color || C.brand, borderRadius: 4, transition: 'width .3s' } })), React.createElement('span', { style: { width: 24, fontSize: 10, color: C.text2, flexShrink: 0 } }, String(props.count))) }
|
|
936
1092
|
|
|
937
1093
|
// ===== 仪表盘:日期范围筛选(共享给报告与全局总览)=====
|
|
@@ -1103,17 +1259,39 @@ function apply(ctx) {
|
|
|
1103
1259
|
}))))
|
|
1104
1260
|
}
|
|
1105
1261
|
|
|
1262
|
+
// ===== 架构健康提示区(架构自省 L1 · 数据源:state.healthHints)=====
|
|
1263
|
+
// healthHints 由 host lib/health.mjs 的 computeHealthHints(tasks) 每次请求现算(纯函数零存储零 IO):
|
|
1264
|
+
// [{ level: 'warn'|'info', text }]。kernel fetchTasks 已将其与 tasks 同源透传进 state.healthHints,
|
|
1265
|
+
// 本组件不再自持 rpc('get-tasks')(消灭仪表盘打开期间的 3s 双轮询),随面板 notify 重渲染同步刷新。
|
|
1266
|
+
// 缺省兼容:老 host 无此字段 / 非数组 / 空数组 → 返回 null,整块不渲染(零残留)。无按钮无状态。
|
|
1267
|
+
function HealthHints() {
|
|
1268
|
+
var hints = (Array.isArray(state.healthHints) ? state.healthHints : []).filter(function (h) { return h && h.text })
|
|
1269
|
+
if (!hints.length) return null
|
|
1270
|
+
// 两级配色:warn=⚠️ 琥珀(C.warn);info=ℹ️ 蓝灰(DSH 现有 business 信息色——色板无独立 info 档)
|
|
1271
|
+
var infoColor = 'var(--dsw-alias-state-business-primary)'
|
|
1272
|
+
return React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } },
|
|
1273
|
+
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('activity', 11), '架构健康'),
|
|
1274
|
+
hints.map(function (h, i) {
|
|
1275
|
+
var warn = h.level === 'warn'
|
|
1276
|
+
var col = warn ? C.warn : infoColor
|
|
1277
|
+
return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'baseline', gap: 5, fontSize: 10, lineHeight: 1.5, color: col, marginBottom: 3 } },
|
|
1278
|
+
React.createElement('span', { style: { flexShrink: 0 } }, warn ? '⚠️' : 'ℹ️'),
|
|
1279
|
+
React.createElement('span', null, String(h.text)))
|
|
1280
|
+
}))
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1106
1283
|
function Dashboard() {
|
|
1107
1284
|
var stats = computeStats(state.tasks); var statusOrder = ['pending', 'in-progress', 'verifying', 'resolved', 'blocked', 'archived']; var prioOrder = ['critical', 'high', 'medium', 'low']
|
|
1108
1285
|
return React.createElement('div', null,
|
|
1109
1286
|
React.createElement(RangeFilter),
|
|
1110
1287
|
React.createElement(GlobalBoards),
|
|
1111
1288
|
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 })),
|
|
1289
|
+
React.createElement(HealthHints),
|
|
1112
1290
|
React.createElement(TokenUsage, { usage: state.usageSummary }),
|
|
1113
1291
|
React.createElement('div', { style: { display: 'flex', gap: 12, marginBottom: 12, flexWrap: 'wrap' } },
|
|
1114
1292
|
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 }) })),
|
|
1115
1293
|
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 }) }))),
|
|
1116
|
-
React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' } }, React.createElement(
|
|
1294
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' } }, React.createElement(StatCardDual, { label1: '平均排队', value1: stats.avgQueue, label2: '平均执行', value2: stats.avgExec, title: '耗时口径三分离:⏳ 排队 = 创建 → 被领取(等了多久);⏱ 执行 = 被领取 → 完成(干了多久);均不含验收时长' }), React.createElement(StatCard, { label: '平均验证时间', value: stats.avgVerify }), React.createElement(StatCard, { label: '今日完成', value: stats.todayDone, color: C.ok })),
|
|
1117
1295
|
React.createElement(TrendChart, { data: stats.dailyDone }),
|
|
1118
1296
|
Object.keys(stats.byAgent).length > 0 ? React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, 'Agent 负载(进行中+验证中)'), Object.keys(stats.byAgent).map(function (aid) { return React.createElement('div', { key: aid, style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3, fontSize: 11 } }, React.createElement(ActorLink, { id: aid }), React.createElement('span', { style: { color: C.text2 } }, stats.byAgent[aid] + ' 个任务')) })) : null,
|
|
1119
1297
|
React.createElement('div', { style: { 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 } }, '最近活跃'), stats.recentActivity.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无记录') : stats.recentActivity.map(function (a, i) { return React.createElement('div', { key: i, style: { fontSize: 10, color: C.text2, marginBottom: 3, display: 'flex', gap: 4, alignItems: 'center' } }, React.createElement('span', { style: { color: statusColors[a.to] || C.brand, fontWeight: 600 } }, statusLabels[a.to] || a.to), React.createElement('span', { style: { color: C.text, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, a.title), React.createElement(ActorLink, { id: a.actor }), React.createElement('span', null, ago(a.timestamp))) })))
|
|
@@ -1174,8 +1352,9 @@ function apply(ctx) {
|
|
|
1174
1352
|
var vActive = (ps.verifiers || []).filter(function (v) { return v.busy }).length
|
|
1175
1353
|
var vTotal = (ps.verifiers || []).length
|
|
1176
1354
|
return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
|
|
1177
|
-
|
|
1178
|
-
React.createElement('span', { title: '
|
|
1355
|
+
// 池水位 title 标注(反馈 n-mut9rzoq3flu):裸数字新用户看不懂,⚡=Worker 在跑/上限,✓=Verifier 在跑/上限
|
|
1356
|
+
React.createElement('span', { title: 'Worker 在跑/上限', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), wActive + '/' + wTotal),
|
|
1357
|
+
React.createElement('span', { title: 'Verifier 在跑/上限', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), vActive + '/' + vTotal))
|
|
1179
1358
|
}
|
|
1180
1359
|
|
|
1181
1360
|
// #11 头部减负:池配置收纳进 ⚙️ 弹出层(含 #17 verifier 异构模型设置)
|