dsh-agent-board 1.7.1 → 1.7.3
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 +40 -15
- package/index.mjs +15 -7
- package/lib/client/board-list.js +37 -5
- package/lib/client/dashboard.js +164 -25
- package/lib/client/kernel.js +84 -13
- package/lib/client/task-detail.js +142 -11
- package/lib/client.js +427 -54
- package/lib/core.mjs +355 -35
- package/lib/dispatch.mjs +1032 -196
- package/lib/health.mjs +43 -11
- package/lib/notify.mjs +42 -12
- package/lib/policy.mjs +17 -1
- package/lib/rpc.mjs +159 -25
- package/lib/session.mjs +7 -2
- package/lib/store.mjs +28 -2
- package/lib/usage.mjs +238 -17
- package/package.json +1 -1
- package/test/core.test.mjs +3469 -88
- package/test/health.test.mjs +86 -4
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, childStats: {}, tasksErr: '', tasksHash: '' }
|
|
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: '' }
|
|
236
236
|
|
|
237
237
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
238
238
|
try {
|
|
@@ -271,10 +271,54 @@ function apply(ctx) {
|
|
|
271
271
|
function clearReadErr() { state.tasksErr = '' } // 仅 fetchTasks 成功路径调用(3s 轮询兜底,任何错误条都能随之消失)
|
|
272
272
|
function readErrText(e) { return String((e && e.message) || e || '网络异常') }
|
|
273
273
|
|
|
274
|
+
// ===== isRoot 蝶变防抖(反馈 n-muuerxv9ijxs / task-muupr8ld)=====
|
|
275
|
+
// 为什么防抖:host rpc.mjs L97 的 isRoot 是每次 get-tasks 现算的
|
|
276
|
+
// (agents.roots() 是否含本会话 id)。生成开始/结束瞬间 agents 树重建,roots() 存在一个
|
|
277
|
+
// 瞬态窗口返回不含本 sid → 单次 isRoot=false 就触发 L303 强收抽屉(state.open=false),
|
|
278
|
+
// 用户表现为「看板在生成状态切换时突然消失,每次都要重新打开」;按钮/面板同病闪烁。
|
|
279
|
+
// 防抖口径:已确认过 true 的会话(everTrue)需连续 IS_ROOT_FALSE_LIMIT 次 false
|
|
280
|
+
// (host 3s 心跳 ≈ 9s)才把 isRootStable 翻成 false——覆盖瞬态窗口,真正降级仍会收敛。
|
|
281
|
+
// 为什么子代理会话不防抖:everTrue=false 的会话(新打开的子代理)立即 isRootStable=false,
|
|
282
|
+
// 保护语义不削弱——子代理会话本就不该出现看板入口/抽屉,即时收起没有代价。
|
|
283
|
+
// 消费口径:强收(本函数)/按钮(BoardButton)/面板(TopPanel)/活动心跳省流(fetchActivity)
|
|
284
|
+
// 四处一律读 isRootStable,不读单次轮询的 state.isRoot 原始值。
|
|
285
|
+
var IS_ROOT_FALSE_LIMIT = 3
|
|
286
|
+
function applyIsRoot(rawIsRoot) {
|
|
287
|
+
state.isRoot = rawIsRoot
|
|
288
|
+
if (rawIsRoot) {
|
|
289
|
+
state.isRootEverTrue = true // 曾确认 true:此后 false 需连续累计(会话生命周期内粘滞,切换会话时重置)
|
|
290
|
+
state.isRootFalseN = 0
|
|
291
|
+
state.isRootStable = true
|
|
292
|
+
} else if (!state.isRootEverTrue) {
|
|
293
|
+
state.isRootStable = false // 从未 true(新打开的子代理会话):即时收起,不防抖
|
|
294
|
+
} else {
|
|
295
|
+
state.isRootFalseN++
|
|
296
|
+
if (state.isRootFalseN >= IS_ROOT_FALSE_LIMIT) state.isRootStable = false // 连续 3 次 false:真降级(子代理树已重建不含本 sid),收敛
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ===== 设置开关权威纠偏(反馈:勾选几秒才同步,task-muw5uudk)=====
|
|
301
|
+
// 开关字段(学习反馈/派发回执/完成回执/史诗拆分)在 get-tasks 里**无条件**照常赋值,但取值口径是
|
|
302
|
+
// 「缺字段/脏值=开,只有显式 false 才关」,与 host core.cfg 同口径——单点定义,避免纠偏把脏值当真值。
|
|
303
|
+
function cfgKnobOf(src, key) { return !(src && src[key] === false) }
|
|
304
|
+
// 变更检测:与 notify 同类的轻量浅比较(只看这几个布尔开关;settings 无嵌套对象)。
|
|
305
|
+
function cfgKnobsChanged(a, b) { return a.feedbackEnabled !== b.feedbackEnabled || a.notifyDispatch !== b.notifyDispatch || a.notifyDone !== b.notifyDone || a.epicSplit !== b.epicSplit }
|
|
306
|
+
|
|
274
307
|
function fetchTasks() {
|
|
275
308
|
if (!state.sessionId) return
|
|
276
309
|
var epoch = reqEpoch
|
|
277
|
-
|
|
310
|
+
// ===== 统计范围随轮询带给 host(Token 区的模型分布/Top8/累计要按范围重算)=====
|
|
311
|
+
// 为什么过滤在 host 做:run 级数据(runs[i].usage + runs[i].at)只在 host,客户端只拿聚合,
|
|
312
|
+
// 想按范围裁只能重拉一次 host 聚合——所以范围变化时由 RangeFilter 的 setRange 触发本函数(见 dashboard.js)。
|
|
313
|
+
// 空范围(两端皆空)不传 field:host 收到 undefined 即全量聚合,与老 host 的调用形态完全一致;
|
|
314
|
+
// 绝不用空字符串占位(host 侧 `from`/`to` 空串虽也判为无范围,但少传一个字段就少一处口径分叉)。
|
|
315
|
+
var rgSend = activeRange()
|
|
316
|
+
var rpcArgs = (rgSend.from || rgSend.to) ? { range: rgSend } : undefined
|
|
317
|
+
// 范围守卫:范围已在本轮响应回来前被改掉 → 这份响应属于旧范围,丢弃 usageSummary 赋值
|
|
318
|
+
// (否则旧范围的旧聚合会覆盖新范围的新聚合,用户看到「切了范围数字没变」直到下一次轮询)。
|
|
319
|
+
// 注意 tasksHash 短路只管 tasks:usageSummary 照常赋值(现状已对,保持)。
|
|
320
|
+
var rgKey = rgSend.from + '~' + rgSend.to
|
|
321
|
+
rpc('get-tasks', rpcArgs).then(function (d) {
|
|
278
322
|
if (epoch !== reqEpoch) return // 会话已切换,丢弃过期响应
|
|
279
323
|
if (d && d.error) { reportReadErr('看板数据刷新失败:' + d.error); return } // error 分支:保留旧 tasks/设置,绝不当空板渲染
|
|
280
324
|
clearReadErr()
|
|
@@ -285,6 +329,11 @@ function apply(ctx) {
|
|
|
285
329
|
// 老 host 不返回 tasksHash → 恒视为变化,行为与旧版完全一致。
|
|
286
330
|
var newHash = (d && d.tasksHash) || ''
|
|
287
331
|
var tasksChanged = !newHash || newHash !== state.tasksHash
|
|
332
|
+
// 设置开关的权威纠偏(task-muw5uudk):开关不走 tasksHash,hash 不变分支里照常赋值也没有 notify
|
|
333
|
+
// → 勾选框必须等下一次任意 notify 才翻面(安静板卡数秒)。这里在赋值前快照、赋值后比对,
|
|
334
|
+
// 有变化就补一次 notify:乐观更新(PoolCfgPopover)已让点击瞬时翻面,本兜底是服务端权威值纠偏
|
|
335
|
+
// (乐观值与服务端不一致时以服务端为准,失败回滚亦由此收敛)。
|
|
336
|
+
var cfgKnobs = { feedbackEnabled: state.feedbackEnabled, notifyDispatch: state.notifyDispatch, notifyDone: state.notifyDone, epicSplit: state.epicSplit }
|
|
288
337
|
if (tasksChanged) {
|
|
289
338
|
state.tasksHash = newHash
|
|
290
339
|
state.tasks = (d && d.tasks) || []
|
|
@@ -303,7 +352,18 @@ function apply(ctx) {
|
|
|
303
352
|
state.softTimeoutMin = (d && d.softTimeoutMin) || 30
|
|
304
353
|
state.hardTimeoutMin = (d && d.hardTimeoutMin) || 120
|
|
305
354
|
state.poolStatus = (d && d.poolStatus) || null
|
|
306
|
-
|
|
355
|
+
// token 消耗聚合(board 级,host 端现算):范围守卫——响应回来时范围若已变,保留旧值不覆盖
|
|
356
|
+
// (新范围的那次请求会带着新聚合回来;老 host 不返回该字段 → null,零渲染)
|
|
357
|
+
// ===== usageSummary 变化检测(task-muwq9u04:选范围不重渲染)=====
|
|
358
|
+
// 病根:范围切换只让 state.usageSummary 换对象,tasksHash 与四个 cfg 开关都不变 → 无 notify
|
|
359
|
+
// → Token 区冻在旧数字上,要等下一次任意 notify(改任务/切开关)才翻新。
|
|
360
|
+
// 为什么用「上一次的 JSON 串」比对而不是对象引用:host 每次 get-tasks 都现算聚合、**必然返回新对象**
|
|
361
|
+
// (引用比较恒真)→ 3s 轮询每轮都 notify,tasksHash 的渲染节约当场作废。JSON 串只在这份聚合
|
|
362
|
+
// 真变了时才不等(KB 级体积、3s 一次,代价可忽略);赋值前先快照,赋值后比对。
|
|
363
|
+
var usageJsonPrev = JSON.stringify(state.usageSummary || null)
|
|
364
|
+
var rgNow = activeRange()
|
|
365
|
+
if (rgKey === (rgNow.from + '~' + rgNow.to)) state.usageSummary = (d && d.usageSummary) || null
|
|
366
|
+
var usageDelta = !tasksChanged && JSON.stringify(state.usageSummary || null) !== usageJsonPrev
|
|
307
367
|
// 架构健康提示(架构自省 L1):与 tasks 同源透传,HealthHints 组件直接读 state.healthHints,
|
|
308
368
|
// 不再自持 rpc('get-tasks')——消灭仪表盘打开期间的双轮询。老 host 无此字段 → 空数组零渲染
|
|
309
369
|
state.healthHints = (d && Array.isArray(d.healthHints)) ? d.healthHints : []
|
|
@@ -311,9 +371,18 @@ function apply(ctx) {
|
|
|
311
371
|
// 缺省兼容——老 host 不返回该字段时置空对象,卡片/详情层遇空一律不渲染相关元素
|
|
312
372
|
state.childStats = (d && d.childStats) || {}
|
|
313
373
|
// 学习飞轮 v1 能力检测:老 host 不返回该字段 → 视为开启(默认开);只有显式 false 才关。
|
|
314
|
-
state.feedbackEnabled =
|
|
315
|
-
|
|
316
|
-
|
|
374
|
+
state.feedbackEnabled = cfgKnobOf(d, 'feedbackEnabled')
|
|
375
|
+
// 回执开关(设置区「通知」):同口径——老 host 不返回 → 视为开,只有显式 false 才关
|
|
376
|
+
state.notifyDispatch = cfgKnobOf(d, 'notifyDispatch')
|
|
377
|
+
state.notifyDone = cfgKnobOf(d, 'notifyDone')
|
|
378
|
+
// 史诗拆分总开关(设置区「功能」):同上——老 host 不返回 = 开(引导照旧),只有显式 false 才关
|
|
379
|
+
state.epicSplit = cfgKnobOf(d, 'epicSplit')
|
|
380
|
+
// 开关有变 / Token 区聚合有变 → 补一次 notify(tasksChanged 分支已在上面 notify 过,
|
|
381
|
+
// 这里只管 hash 不变时被跳过的那两次:开关乐观更新纠偏 + 统计范围切换后的新聚合)
|
|
382
|
+
var cfgDelta = !tasksChanged && cfgKnobsChanged(cfgKnobs, state)
|
|
383
|
+
if (cfgDelta || usageDelta) notify()
|
|
384
|
+
applyIsRoot(!d || d.isRoot !== false) // 原始值只喂给防抖器,消费点一律读 isRootStable
|
|
385
|
+
if (!state.isRootStable && state.open) { state.open = false; state.detailId = null } // 子代理会话(含连续 3 次 false 的真降级):强制收起看板
|
|
317
386
|
if (d && d.dispatchInfo && d.dispatchInfoAt && Date.now() - new Date(d.dispatchInfoAt).getTime() < 120000) { state.dispatchInfo = d.dispatchInfo } else { state.dispatchInfo = '' } // 瞬时通知 2min 内有效,过期强制清空(服务端写后不清曾致残留数天)
|
|
318
387
|
// escalation 一等公民:出现新的待裁决任务 → 面板自动弹开直达该任务详情
|
|
319
388
|
// (escalation.question 在 tasksHash 序列化口径内:新歧义必然改变 hash → 必然走变化分支,不会漏弹)
|
|
@@ -331,7 +400,7 @@ function apply(ctx) {
|
|
|
331
400
|
|
|
332
401
|
// 活动心跳:对进行中/验收中的任务轮询子代理最近动作(卡片与详情展示"现在跑到哪了")
|
|
333
402
|
function fetchActivity() {
|
|
334
|
-
if (!state.sessionId || !state.
|
|
403
|
+
if (!state.sessionId || !state.isRootStable || !state.open) return // 面板关闭时不轮询活动(省同步 I/O);isRootStable 口径见 isRoot 蝶变防抖
|
|
335
404
|
var running = state.tasks.filter(function (t) { return t.status === 'in-progress' || t.status === 'verifying' })
|
|
336
405
|
if (!running.length) { if (Object.keys(state.activity).length) { state.activity = {}; notify() } return }
|
|
337
406
|
var pending = running.length
|
|
@@ -482,9 +551,9 @@ function apply(ctx) {
|
|
|
482
551
|
function BoardButton(props) {
|
|
483
552
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
484
553
|
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]
|
|
485
|
-
useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; reqEpoch++; state.tasks = []; state.tasksHash = ''; state.children = []; state.activity = {}; state.archived = []; state.detailId = null; state.childStats = {}; state.tasksErr = ''; notify(); fetchTasks(); fetchChildren() } } }, [props && props.sessionId])
|
|
486
|
-
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.
|
|
487
|
-
if (!isRoot) return null //
|
|
554
|
+
useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; reqEpoch++; state.tasks = []; state.tasksHash = ''; state.children = []; state.activity = {}; state.archived = []; state.detailId = null; state.childStats = {}; state.tasksErr = ''; state.isRootEverTrue = false; state.isRootFalseN = 0; state.isRootStable = false; notify(); fetchTasks(); fetchChildren() } } }, [props && props.sessionId]) // 会话切换重置 isRoot 防抖态(everTrue/计数/stable 都按会话生命周期——旧会话的"曾确认 true"不得漂到新会话);reset 后首轮 get-tasks 判定前按"未确认 root"保守隐藏,避免切进子代理会话时入口闪现
|
|
555
|
+
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.isRootStable) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
556
|
+
if (!isRoot) return null // 子代理会话不显示看板入口(读 isRootStable:瞬态 false 不收,见 isRoot 蝶变防抖)
|
|
488
557
|
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)
|
|
489
558
|
}
|
|
490
559
|
|
|
@@ -500,9 +569,11 @@ function apply(ctx) {
|
|
|
500
569
|
var _te = useState(state.tasksErr), tasksErr = _te[0], setTasksErr = _te[1] // 读路径错误条(无错不渲染)
|
|
501
570
|
var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
|
|
502
571
|
var _fbo = useState(state.feedbackEnabled), fbEnabled = _fbo[0], setFbEnabled = _fbo[1]
|
|
503
|
-
|
|
572
|
+
var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
|
|
573
|
+
var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
|
|
574
|
+
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); 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) } }, [])
|
|
504
575
|
if (!open) return null
|
|
505
|
-
if (state.
|
|
576
|
+
if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
|
|
506
577
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
507
578
|
var hasFilter = state.filterQ.trim() || state.filterPrio.length > 0 || state.filterTag
|
|
508
579
|
if (hasFilter) { active = active.filter(passFilter); archived = archived.filter(passFilter) }
|
|
@@ -526,7 +597,7 @@ function apply(ctx) {
|
|
|
526
597
|
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 } },
|
|
527
598
|
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)),
|
|
528
599
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
529
|
-
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT, feedbackEnabled: fbEnabled }),
|
|
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 }),
|
|
530
601
|
dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
|
|
531
602
|
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), '新建任务'),
|
|
532
603
|
createFlash ? React.createElement('span', { style: { fontSize: 10, color: C.ok } }, createFlash) : null,
|
|
@@ -648,7 +719,9 @@ function apply(ctx) {
|
|
|
648
719
|
}
|
|
649
720
|
|
|
650
721
|
// 史诗/依赖/父子识别层(childStats 缺省兼容:host 未返回该字段时一律不渲染相关元素):
|
|
651
|
-
// 父卡 = 元信息行首位「📦 史诗 ·
|
|
722
|
+
// 父卡 = 元信息行首位「📦 史诗 · settled/total」徽章 + 3px 迷你进度条 + activeTitle 非空时「▸ 在跑」行;
|
|
723
|
+
// settled = resolved | cancelled | archived(含已归档子任务、total 也含),口径同宿主 aggregateChildStats;
|
|
724
|
+
// 老宿主只下发 resolved 时回退读它(缺省兼容)——所以归档子卡不再让徽章从 0/10 退化成 0/9;
|
|
652
725
|
// 子卡 = 标题下一行小字「↳ 父任务标题」(从 state.tasks 找父卡,找不到回退 shortId);
|
|
653
726
|
// 依赖未满足的待办卡 = 卡片底部灰字「⛓ 等待「第一个未满足依赖标题」」(多个时附「 等 N 个」)。
|
|
654
727
|
// ===== 调研门禁·UI:「⚠️ 无调研」徽章判定(纯 client 侧现算,host 不下发该字段)=====
|
|
@@ -664,13 +737,43 @@ function apply(ctx) {
|
|
|
664
737
|
var notes = typeof cx.notes === 'string' ? cx.notes.trim() : ''
|
|
665
738
|
return files.length === 0 && notes.length === 0
|
|
666
739
|
}
|
|
667
|
-
|
|
740
|
+
// ===== epic hook 相位徽章(hooks=agent run 的卡片侧可见性,task-muuw56yf)=====
|
|
741
|
+
// 与 host core.normalizeHooks 契约同源:epic.hooks = { pre: { enabled, prompt, state, runId, pending? }, post: 同构 },
|
|
742
|
+
// state ∈ idle | running | done | failed(host 状态机推进,UI 只读展示,不在这里改状态)。
|
|
743
|
+
// 缺省兼容硬约束:卡上无 hooks / 该点位未启用 → 返回 null,老卡渲染逐字不变(零额外 DOM)。
|
|
744
|
+
// 优先级与文案(同一时刻只可能有一个相位有话说):
|
|
745
|
+
// pre|post failed → 「⚠️ hook 失败待裁决」(err 色):史诗已被 host 转 blocked 挂歧义,过程态文案都让位;
|
|
746
|
+
// pre running → 「⏳ 前置准备中」:串行闸门期内,该 epic 的子任务一张都不派;
|
|
747
|
+
// post running → 「🧪 收尾中」:子任务已全部了结、epic 仍 in-progress,等收口 run 归还;
|
|
748
|
+
// idle / done → 不渲染(普通进行中维持「📦 史诗 · x/y」原样,稳定态不加噪音)。
|
|
749
|
+
function hookBadgeOf(t) {
|
|
750
|
+
var h = t && t.hooks
|
|
751
|
+
if (!h) return null
|
|
752
|
+
var pre = (h.pre && h.pre.enabled) ? h.pre : null
|
|
753
|
+
var post = (h.post && h.post.enabled) ? h.post : null
|
|
754
|
+
if ((pre && pre.state === 'failed') || (post && post.state === 'failed')) {
|
|
755
|
+
var ph = (pre && pre.state === 'failed') ? 'pre' : 'post'
|
|
756
|
+
return { txt: '⚠️ hook 失败待裁决', color: C.err, tip: 'hooks.' + ph + '(' + (ph === 'pre' ? '前置准备' : '收口') + ')运行失败:史诗已转阻塞等人裁决(重试/跳过/放弃),详情页可跳裁决区' }
|
|
757
|
+
}
|
|
758
|
+
if (pre && pre.state === 'running') return { txt: '⏳ 前置准备中', color: C.brand, tip: '前置 hook run 正在跑:完成前该史诗的子任务一张都不派(串行闸门)' }
|
|
759
|
+
if (post && post.state === 'running') return { txt: '🧪 收尾中', color: C.brand, tip: '收口 hook run 正在跑:子任务已全部了结,史诗仍 in-progress,收口完成后才转验证中' }
|
|
760
|
+
return null
|
|
761
|
+
}
|
|
762
|
+
function hookPhaseBadge(t) {
|
|
763
|
+
var b = hookBadgeOf(t)
|
|
764
|
+
if (!b) return null
|
|
765
|
+
return React.createElement('span', { title: b.tip, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + b.color + ' 14%, transparent)', color: b.color, fontWeight: 600, whiteSpace: 'nowrap' } }, b.txt)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
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 csDone = cs ? (typeof cs.settled === 'number' ? cs.settled : cs.resolved) : 0; 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);
|
|
668
769
|
// 无障碍(反馈 n-mut9rzpyc7p1):卡片根以 button 角色进 Tab 序(aria-label=标题+状态),
|
|
669
770
|
// Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
|
|
670
771
|
// focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
|
|
671
772
|
function activate() { 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() } }
|
|
672
773
|
return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, 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' } },
|
|
673
|
-
epic ? React.createElement('span', { title: '史诗父卡:' +
|
|
774
|
+
epic ? React.createElement('span', { title: '史诗父卡:' + csDone + '/' + cs.total + ' 个子任务已了结(完成/取消/归档)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + csDone + '/' + cs.total) : null,
|
|
775
|
+
// hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
|
|
776
|
+
epic ? hookPhaseBadge(t) : null,
|
|
674
777
|
t.frozen ? React.createElement('span', { title: '已冻结:不参与自动派发(详情页可「解除冻结」)', style: { color: C.brand, fontWeight: 600 } }, '❄ 冻结') : null,
|
|
675
778
|
(Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
|
|
676
779
|
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
@@ -685,8 +788,8 @@ function apply(ctx) {
|
|
|
685
788
|
title: '删除任务(不可恢复;执行中请先终止,已落定请用归档)',
|
|
686
789
|
style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', cursor: 'pointer', color: C.err, opacity: isCardHover(t.id, 'del') ? 1 : 0, transition: 'opacity .15s' }
|
|
687
790
|
}, ic('trash-2', 11)) : null),
|
|
688
|
-
// 史诗迷你进度条(3px 高,
|
|
689
|
-
epic ? React.createElement('div', { style: { height: 3, borderRadius: 2, background: C.nested, marginTop: 4, overflow: 'hidden' }, title: '子任务进度 ' +
|
|
791
|
+
// 史诗迷你进度条(3px 高,settled/total 比例,ok 色填充)+ 在跑子任务行(activeTitle 非空才渲染)
|
|
792
|
+
epic ? React.createElement('div', { style: { height: 3, borderRadius: 2, background: C.nested, marginTop: 4, overflow: 'hidden' }, title: '子任务进度 ' + csDone + '/' + cs.total }, React.createElement('div', { style: { height: '100%', width: Math.max(0, Math.min(100, Math.round((csDone / cs.total) * 100))) + '%', background: C.ok, borderRadius: 2, transition: 'width .3s' } })) : null,
|
|
690
793
|
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,
|
|
691
794
|
// 当前动作行:展示摘要(工具名+关键参数),悬停 title 仍是 host 原文
|
|
692
795
|
(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,
|
|
@@ -896,30 +999,155 @@ function apply(ctx) {
|
|
|
896
999
|
|
|
897
1000
|
// 父子区块(与 DepsSection 并列):
|
|
898
1001
|
// 子卡 = 父链面包屑「↳ 史诗:<父标题>」,点击回跳父卡详情;
|
|
899
|
-
// 父卡 =
|
|
1002
|
+
// 父卡 = 子任务清单(状态色点 + 标题 + 状态标签,点击直达子卡详情;标题行汇总 settled/total)。
|
|
1003
|
+
// 归档留档(反馈 task-muupgfot):清单**含已归档子任务**——归档后子任务不该从详情消失,
|
|
1004
|
+
// 否则史诗拆分过程整段丢失、只剩一个分母缩水的数字。归档行灰化(降透明度 + 次要文字色)
|
|
1005
|
+
// + 行尾「已归档」徽章,并沉底排在未归档之后(稳定排序:未归档保持 state.tasks 既有顺序);
|
|
1006
|
+
// 仅降视觉权重不改交互——点击照旧进子卡详情看留档。
|
|
1007
|
+
// 口径与卡片 📦 徽章同源:settled = resolved | cancelled | archived(宿主 aggregateChildStats 的
|
|
1008
|
+
// settled/resolved 字段),total 含归档,所以详情与卡片永远显示同一个 N/M。
|
|
900
1009
|
// 数据源是 state.tasks 按 parentId 现算(parentId 为老字段),不依赖 host 的 childStats,天然缺省兼容;
|
|
901
1010
|
// 既无父也无子的普通卡整块不渲染。嵌套史诗(本身也是子卡的父卡)两段同区块上下排列。
|
|
902
1011
|
function FamilySection(props) {
|
|
903
1012
|
var task = props.task
|
|
904
1013
|
var parentT = task.parentId ? getTask(task.parentId) : null
|
|
905
|
-
|
|
1014
|
+
// 不按状态过滤(归档也要留在清单里);仅按「是否归档」稳定排序把归档沉底(V8 sort 稳定,未归档保序)
|
|
1015
|
+
var kids = state.tasks.filter(function (x) { return x.parentId === task.id })
|
|
1016
|
+
.sort(function (a, b) { return (a.status === 'archived' ? 1 : 0) - (b.status === 'archived' ? 1 : 0) })
|
|
906
1017
|
if (!task.parentId && kids.length === 0) return null
|
|
907
|
-
var
|
|
1018
|
+
var settledN = kids.filter(function (x) { return x.status === 'resolved' || x.status === 'cancelled' || x.status === 'archived' }).length
|
|
908
1019
|
function jump(id) { state.detailId = id; notify() }
|
|
909
1020
|
return React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
|
|
910
1021
|
task.parentId ? React.createElement('div', { style: { fontSize: 11, marginBottom: kids.length ? 6 : 0 } },
|
|
911
1022
|
React.createElement('span', { style: { color: C.text2, fontWeight: 600 } }, '↳ 史诗:'),
|
|
912
1023
|
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,
|
|
913
1024
|
kids.length > 0 ? React.createElement('div', null,
|
|
914
|
-
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '📦 子任务 · ' +
|
|
1025
|
+
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '📦 子任务 · ' + settledN + '/' + kids.length + ' 已了结'),
|
|
915
1026
|
kids.map(function (x) {
|
|
916
|
-
|
|
1027
|
+
var arch = x.status === 'archived'
|
|
1028
|
+
return React.createElement('div', { key: x.id, onClick: function () { jump(x.id) }, title: (statusLabels[x.status] || x.status) + ' · ' + x.id + (arch ? ' · 已归档留档(点击查看)' : ''), style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3, fontSize: 10, cursor: 'pointer', opacity: arch ? 0.55 : 1 } },
|
|
917
1029
|
React.createElement('span', { style: { width: 7, height: 7, borderRadius: '50%', background: statusColors[x.status] || C.text2, flexShrink: 0 } }),
|
|
918
|
-
React.createElement('span', { style: { color: C.brand, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, x.title || x.id),
|
|
919
|
-
|
|
1030
|
+
React.createElement('span', { style: { color: arch ? C.text2 : C.brand, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, x.title || x.id),
|
|
1031
|
+
arch
|
|
1032
|
+
? React.createElement('span', { style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, color: C.text2, border: '1px solid ' + C.border, flexShrink: 0 } }, '已归档')
|
|
1033
|
+
: React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0 } }, statusLabels[x.status] || x.status))
|
|
920
1034
|
})) : null)
|
|
921
1035
|
}
|
|
922
1036
|
|
|
1037
|
+
// ===== Hooks(可选点位)· 史诗前置/后置 agent run 的编辑与状态面板(task-muuw56yf)=====
|
|
1038
|
+
// 契约以 host core.normalizeHooks / mergeHooks 为准(勿凭记忆改字段名):
|
|
1039
|
+
// epic.hooks = { pre: { enabled, prompt, state, runId, pending? }, post: 同构 },state ∈ idle|running|done|failed;
|
|
1040
|
+
// hook run 记在 epic.runs[](role 'hook-pre'/'hook-post',字段 { role, id, at, endedAt, outcome, model })。
|
|
1041
|
+
// 写入通道 = update-task RPC 的 hooks 字段(浅合并:未提交的点位保留原值,传 null 撤点位)。
|
|
1042
|
+
// 三条 UI 口径:
|
|
1043
|
+
// ① 老卡零渲染:既无子任务、也未声明 hooks 的普通卡整块不出现(与卡片相位徽章同一门禁);
|
|
1044
|
+
// ② 权限不在 UI 判断——「hooks 仅主窗口可设」由 host 双重门禁裁决,失败原因原样 ⚠️ 回显;
|
|
1045
|
+
// 客户端预判角色只会把真实错误变成灰按钮,反而更难查;
|
|
1046
|
+
// ③ state 只读展示(host 状态机推进,重启可恢复);重试=把 state 置回 idle,走裁决通道,
|
|
1047
|
+
// 这里只负责给 failed 一个「前往裁决」锚点,不另造裁决入口。
|
|
1048
|
+
function hookRunOf(task, phase, runId) {
|
|
1049
|
+
var role = 'hook-' + phase
|
|
1050
|
+
// host 侧 run id 落位不对称:pre 在 spawn 成功时回填 hooks.pre.runId,post 复用 verifierRun 位
|
|
1051
|
+
// (claim 占位换真 id);且两端结算都把 runId 归零——所以取不到时按角色回退「最后一条」。
|
|
1052
|
+
var rid = runId || (phase === 'post' ? task.verifierRun : null)
|
|
1053
|
+
var runs = Array.isArray(task.runs) ? task.runs : []
|
|
1054
|
+
var hit = null
|
|
1055
|
+
for (var i = 0; i < runs.length; i++) {
|
|
1056
|
+
if (!runs[i] || runs[i].role !== role) continue
|
|
1057
|
+
if (rid && runs[i].id !== rid) continue
|
|
1058
|
+
hit = runs[i] // 顺序遍历取最后一条匹配:重试后的新 run 覆盖旧展示
|
|
1059
|
+
}
|
|
1060
|
+
return hit
|
|
1061
|
+
}
|
|
1062
|
+
function hookStateMeta(task, phase) {
|
|
1063
|
+
var h = (task.hooks && task.hooks[phase]) || null
|
|
1064
|
+
var st = (h && h.state) || 'idle'
|
|
1065
|
+
var off = !h || !h.enabled
|
|
1066
|
+
var run = hookRunOf(task, phase, h && h.runId)
|
|
1067
|
+
var meta = { idle: { label: off ? '未启用' : '待运行', color: C.text2 }, running: { label: '运行中', color: C.brand }, done: { label: '已完成', color: C.ok }, failed: { label: '失败', color: C.err } }[st] || { label: String(st), color: C.text2 }
|
|
1068
|
+
// 耗时:运行中 = 距 spawn 的已跑时长;已落定 = spawn → endedAt 净耗时(老 run 无 endedAt 则不显示)
|
|
1069
|
+
var dur = ''
|
|
1070
|
+
if (run && run.at) {
|
|
1071
|
+
if (st === 'running') dur = '已跑 ' + elapsedSince(run.at)
|
|
1072
|
+
else if (run.endedAt) dur = '耗时 ' + fmtDur(new Date(run.at).getTime(), new Date(run.endedAt).getTime())
|
|
1073
|
+
}
|
|
1074
|
+
return { state: st, off: off, label: meta.label, color: meta.color, run: run, dur: dur }
|
|
1075
|
+
}
|
|
1076
|
+
function HooksSection(props) {
|
|
1077
|
+
var _R = React; var useState = _R.useState // 与全库组件同例:bundler 只注入 React 本体,hook 须自行解构(漏了就是运行期 ReferenceError,shell 容错层直接卸载整个 overlay——2026-10-05 详情页崩板事故)
|
|
1078
|
+
var task = props.task
|
|
1079
|
+
var kHooks = task.hooks || null
|
|
1080
|
+
var _a = useState(''), msg = _a[0], setMsg = _a[1]
|
|
1081
|
+
var _b = useState(false), saving = _b[0], setSaving = _b[1]
|
|
1082
|
+
// 编辑缓冲以「卡上现值」为初值;父层以 key=task.id 挂载 → 切换任务自动重挂,缓冲不会串卡
|
|
1083
|
+
var _c = useState({ pre: { enabled: !!(kHooks && kHooks.pre && kHooks.pre.enabled), prompt: (kHooks && kHooks.pre && kHooks.pre.prompt) || '' }, post: { enabled: !!(kHooks && kHooks.post && kHooks.post.enabled), prompt: (kHooks && kHooks.post && kHooks.post.prompt) || '' } }), buf = _c[0], setBuf = _c[1]
|
|
1084
|
+
var kids = state.tasks.filter(function (x) { return x.parentId === task.id })
|
|
1085
|
+
var declared = !!(kHooks && (kHooks.pre || kHooks.post))
|
|
1086
|
+
if (kids.length === 0 && !declared) return null
|
|
1087
|
+
function setPhase(ph, patch) { var nb = { pre: buf.pre, post: buf.post }; nb[ph] = Object.assign({}, nb[ph], patch); setBuf(nb) }
|
|
1088
|
+
// 提交体组装(三条口径缺一都踩坑):
|
|
1089
|
+
// a) prompt 清空 = 撤掉该点位:host normalizeHooks 明确拒绝空 prompt,撤点位用 null 表达才在契约内
|
|
1090
|
+
// (不该把「我不想要这个 hook 了」变成一条报错);**例外**——该点位 state='running' 时不清空:
|
|
1091
|
+
// run 还在飞,撤掉声明会让它的结算落到空点位(表现为白跑一轮),删除意图原样保留并回一行提示;
|
|
1092
|
+
// b) 已声明点位的 state/runId/pending 原样回传:normalizeHooks 对缺省 state 归零成 idle,
|
|
1093
|
+
// 不透传就等于「改一下 prompt 把在跑/已落定的 hook 打回待运行」;
|
|
1094
|
+
// c) 凑不出 prompt 的点位一律提交 null(幂等删除,不凭空造点位)。
|
|
1095
|
+
function payloadFor(ph) {
|
|
1096
|
+
var cur = (task.hooks && task.hooks[ph]) || null
|
|
1097
|
+
var b = buf[ph] || { enabled: false, prompt: '' }
|
|
1098
|
+
function keep(h) { var it = { enabled: !!h.enabled, prompt: String(h.prompt || '') }; if (h.state) it.state = h.state; if (h.runId) it.runId = h.runId; if (h.pending) it.pending = true; return it }
|
|
1099
|
+
if (!String(b.prompt || '').trim()) {
|
|
1100
|
+
if (cur && cur.state === 'running') return { item: keep(cur), warn: ph + ' 运行中:本次不清空该点位(run 还在跑),等它跑完再撤' }
|
|
1101
|
+
return { item: null }
|
|
1102
|
+
}
|
|
1103
|
+
var item = { enabled: !!b.enabled, prompt: String(b.prompt).slice(0, 4000) }
|
|
1104
|
+
if (cur) {
|
|
1105
|
+
if (cur.state) item.state = cur.state
|
|
1106
|
+
if (cur.runId) item.runId = cur.runId
|
|
1107
|
+
if (cur.pending) item.pending = true
|
|
1108
|
+
}
|
|
1109
|
+
return { item: item }
|
|
1110
|
+
}
|
|
1111
|
+
function save() {
|
|
1112
|
+
if (saving) return
|
|
1113
|
+
var pre = payloadFor('pre'), post = payloadFor('post')
|
|
1114
|
+
var warns = [pre.warn, post.warn].filter(Boolean)
|
|
1115
|
+
setSaving(true); setMsg('')
|
|
1116
|
+
rpc('update-task', { taskId: task.id, hooks: { pre: pre.item, post: post.item } }).then(function (r) {
|
|
1117
|
+
setSaving(false)
|
|
1118
|
+
if (r && r.ok === false) { setMsg('⚠️ ' + (r.error || '保存失败')); return }
|
|
1119
|
+
setMsg(warns.length ? '⚠️ ' + warns.join(';') : '✅ 已保存 hooks 配置')
|
|
1120
|
+
fetchTasks()
|
|
1121
|
+
}).catch(function (e) { setSaving(false); setMsg('⚠️ ' + String(e)) })
|
|
1122
|
+
}
|
|
1123
|
+
// 「前往裁决」= 同页锚点跳转(裁决区 id 固定 tskb-escalation,由 DetailView 渲染);
|
|
1124
|
+
// hook 失败时 host 已把 epic 转 blocked 并挂好 escalation,这里只把视线送过去。
|
|
1125
|
+
function goArbitration() { var el = document.getElementById('tskb-escalation'); if (el && el.scrollIntoView) el.scrollIntoView({ block: 'center' }) }
|
|
1126
|
+
function phaseRow(ph, title, hint) {
|
|
1127
|
+
var m = hookStateMeta(task, ph)
|
|
1128
|
+
var b = buf[ph] || { enabled: false, prompt: '' }
|
|
1129
|
+
return React.createElement('div', { key: ph, style: { marginBottom: 6, padding: '5px 7px', border: '1px solid ' + C.border, borderRadius: 5, background: C.nested } },
|
|
1130
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', marginBottom: 4 } },
|
|
1131
|
+
React.createElement('label', { style: { fontSize: 11, fontWeight: 600, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'pointer' } },
|
|
1132
|
+
React.createElement('input', { type: 'checkbox', checked: !!b.enabled, onChange: function (e) { setPhase(ph, { enabled: e.target.checked }) }, title: '启用该点位(停用后 host 全链路跳过它)' }),
|
|
1133
|
+
title),
|
|
1134
|
+
React.createElement('span', { title: 'state 由 host 状态机推进(写在卡上,重启可恢复);失败后重试=把 state 置回 idle,走裁决通道', style: { fontSize: 9, padding: '0 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + m.color + ' 16%, transparent)', color: m.color, fontWeight: 600 } }, m.label),
|
|
1135
|
+
m.dur ? React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, m.dur) : null,
|
|
1136
|
+
m.run && m.run.id ? React.createElement('button', { onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(m.run.id) }, title: '查看该 hook run 的会话(' + m.run.id + (m.run.model ? ' · ' + m.run.model : '') + ')', style: { fontSize: 9, padding: '1px 6px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '→ 查看会话') : null,
|
|
1137
|
+
m.state === 'failed' ? React.createElement('button', { onClick: goArbitration, title: '跳到本页裁决区:重试(置回 idle)/ 跳过(置 done 放行)/ 放弃(终止史诗)', style: { fontSize: 9, padding: '1px 6px', border: 'none', borderRadius: 3, background: C.err, color: C_INV, cursor: 'pointer', fontWeight: 600 } }, '前往裁决') : null),
|
|
1138
|
+
React.createElement('textarea', { value: b.prompt, onChange: function (e) { setPhase(ph, { prompt: e.target.value }) }, rows: 2, placeholder: '补充指令(可选)——前置=让子任务具备开跑条件;后置=把这批活收口。具体动作由 hook agent 自行决策', style: { width: '100%', fontSize: 11, padding: '4px 6px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' } }),
|
|
1139
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, lineHeight: 1.6 } }, hint))
|
|
1140
|
+
}
|
|
1141
|
+
return React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
|
|
1142
|
+
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('zap', 11), 'Hooks(可选点位)'),
|
|
1143
|
+
phaseRow('pre', '⏳ 前置(pre)', '让子任务具备开跑条件:未完成前该史诗的子任务一张都不派(串行闸门)。'),
|
|
1144
|
+
phaseRow('post', '🧪 收尾(post)', '把这批活收口:全部子任务了结后跑一遍,完成后史诗才转验证中。'),
|
|
1145
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
1146
|
+
React.createElement('button', { onClick: save, disabled: saving, title: '保存到卡上(update-task RPC,hooks 字段浅合并)', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: saving ? 'default' : 'pointer', opacity: saving ? 0.6 : 1, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, saving ? '保存中…' : [ic('save', 10), ' 保存 hooks']),
|
|
1147
|
+
msg ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, msg) : null),
|
|
1148
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 4, lineHeight: 1.6 } }, '每个点位是一次真实 agent 运行(不是声明式命令):prompt 只给薄框架契约与上下文,动作由 hook agent 自行决策,吃不准会歧义上报;失败即转阻塞等人裁决(重试/跳过/放弃),不会自动重跑。清空 prompt 保存 = 撤掉该点位。hooks 仅主窗口可设(host 门禁,失败原因原样回显)。'))
|
|
1149
|
+
}
|
|
1150
|
+
|
|
923
1151
|
function DetailView() {
|
|
924
1152
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
|
|
925
1153
|
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]
|
|
@@ -942,7 +1170,7 @@ function apply(ctx) {
|
|
|
942
1170
|
if (r && r.ok === false && r.error === 'touches-conflict') {
|
|
943
1171
|
var ids = Array.isArray(r.conflicts) ? r.conflicts : []
|
|
944
1172
|
var names = ids.map(function (id) { var t2 = getTask(id); return (t2 ? t2.title : id) + ' (' + id + ')' }).join('、')
|
|
945
|
-
if (window.confirm('⚠️
|
|
1173
|
+
if (window.confirm('⚠️ 文件锁冲突:以下任务正持有同一批文件(touches 重叠;in-progress/verifying 卡持锁,状态流转到 resolved/cancelled/归档 即自动放行):\n\n' + (names || ids.join('、')) + '\n\n强行并行可能互相覆盖改动/diff 冲突。仍要越权派发吗?')) return send(true)
|
|
946
1174
|
setActionMsg('⛔ 已取消派发(等文件锁释放,或调整 touches 声明)')
|
|
947
1175
|
return { ok: false, cancelled: true }
|
|
948
1176
|
}
|
|
@@ -998,7 +1226,7 @@ function apply(ctx) {
|
|
|
998
1226
|
React.createElement('span', null, '已运行 ' + elapsedSince(task.claimedAt) + '(软超时 ' + state.softTimeoutMin + ' 分提醒 · 硬超时 ' + state.hardTimeoutMin + ' 分自动终止)'),
|
|
999
1227
|
elapsedMin(task.claimedAt) > state.softTimeoutMin ? React.createElement('span', { style: { color: C.warn, fontWeight: 600 } }, '⏱ 已超软超时') : null,
|
|
1000
1228
|
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,
|
|
1001
|
-
task.escalation ? React.createElement('div', { style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)' } },
|
|
1229
|
+
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)' } },
|
|
1002
1230
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), 'Worker 上报歧义 — 等待主窗口裁决'),
|
|
1003
1231
|
React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, whiteSpace: 'pre-wrap', maxHeight: 160, overflowY: 'auto' } }, task.escalation.question),
|
|
1004
1232
|
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '上报于 ' + ago(task.escalation.at) + ' · ' + (task.escalation.by || '')),
|
|
@@ -1041,6 +1269,8 @@ function apply(ctx) {
|
|
|
1041
1269
|
'📎 调研注入: ' + (empty ? '无调研注入——Worker 需自行定位,建议补 contextFiles/contextNotes' : parts.join(' · ')))
|
|
1042
1270
|
})(),
|
|
1043
1271
|
React.createElement(FamilySection, { task: task }),
|
|
1272
|
+
// Hooks 区(史诗可选点位):key=task.id 保证切换任务时重挂,编辑缓冲不串卡
|
|
1273
|
+
React.createElement(HooksSection, { key: task.id, task: task }),
|
|
1044
1274
|
React.createElement(DepsSection, { task: task }),
|
|
1045
1275
|
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,
|
|
1046
1276
|
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,
|
|
@@ -1082,11 +1312,15 @@ function apply(ctx) {
|
|
|
1082
1312
|
var seq = r.role === 'verifier' ? (++vN) : (++wN)
|
|
1083
1313
|
var isCur = (r.role === 'worker' && task.claimedBy === r.id) || (r.role === 'verifier' && task.verifierRun === r.id)
|
|
1084
1314
|
var okMark = r.outcome === 'completed' ? ' ✅' : (r.outcome === 'running' ? ' ⏳' : (r.outcome ? ' ⚠' : ''))
|
|
1315
|
+
// 续跑标记(卡2 落的 runs[i].resume 字段;卡3 Step3 在此显式标注):同一条会话的第二次及以后
|
|
1316
|
+
// 派发是「冷复活续跑」而不是新开 Worker,不标出来会让人把「第 2/3 次」误读成又开了一个新会话。
|
|
1317
|
+
// 只读 r.resume,不猜(老留档没有该字段 → 零渲染,与改造前逐字一致)。
|
|
1318
|
+
var rsMark = r.resume === true ? ' ↻' : ''
|
|
1085
1319
|
return React.createElement('button', {
|
|
1086
1320
|
key: i, onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
|
|
1087
|
-
title: rm.tip + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + (r.outcome ? ' · ' + r.outcome : '') + '(' + r.id + ')',
|
|
1321
|
+
title: rm.tip + ' 第 ' + seq + ' 次' + (r.resume === true ? '(↻ 冷复活续跑:沿用上一次的子会话,非新开)' : '') + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + (r.outcome ? ' · ' + r.outcome : '') + '(' + r.id + ')',
|
|
1088
1322
|
style: { fontSize: 10, padding: '2px 8px', border: '1px solid ' + (isCur ? rm.color : C.border), borderRadius: 3, background: isCur ? C.card : 'transparent', color: rm.color, cursor: 'pointer', fontWeight: isCur ? 600 : 400 }
|
|
1089
|
-
}, '→ ' + rm.tip + ' #' + seq + (r.at ? ' · ' + ago(r.at) : '') + okMark)
|
|
1323
|
+
}, '→ ' + rm.tip + ' #' + seq + (r.at ? ' · ' + ago(r.at) : '') + okMark + rsMark)
|
|
1090
1324
|
})))
|
|
1091
1325
|
})(),
|
|
1092
1326
|
canIntervene ? React.createElement('div', { style: { marginTop: 8, padding: '6px 8px', border: '1px dashed ' + C.warn, borderRadius: 6 } },
|
|
@@ -1124,15 +1358,20 @@ function apply(ctx) {
|
|
|
1124
1358
|
if (!(execMs >= 0)) return // 时间戳异常(NaN/负值)跳过
|
|
1125
1359
|
var cx = t.context || {}
|
|
1126
1360
|
var hasRes = (Array.isArray(cx.files) && cx.files.length > 0) || (typeof cx.notes === 'string' && cx.notes.trim().length > 0)
|
|
1127
|
-
|
|
1128
|
-
|
|
1361
|
+
// token 口径与「Token 消耗区」一致化(task-muupnnq5):取**有效消耗**(输入+输出+缓存写,不含缓存读),
|
|
1362
|
+
// 不再拿含缓存读的 total 冒充——本板实测缓存读占总量 ~94%,用 total 会让 ROI 行的 token 均值虚高十几倍,
|
|
1363
|
+
// 与 Token 区大数字口径互相打架。分量字段缺失的老/存量卡兜底退化为 total 并打 tokFallback 标记(title 标注),
|
|
1364
|
+
// 绝不把总量伪装成有效值;无任何结算记录的卡 tok=null,不参与均值(不拉低样本)。
|
|
1365
|
+
var tk = roiTokenOf(t.usage)
|
|
1366
|
+
;(hasRes ? roiYes : roiNo).push({ execMs: execMs, tok: tk.tok, tokFallback: tk.fallback })
|
|
1129
1367
|
})
|
|
1130
1368
|
var roi = null
|
|
1131
1369
|
if (roiYes.length > 0 && roiNo.length > 0 && roiYes.length + roiNo.length >= 4) {
|
|
1132
1370
|
var roiAgg = function (arr) {
|
|
1133
1371
|
var execMs = Math.round(arr.reduce(function (a, b) { return a + b.execMs }, 0) / arr.length)
|
|
1134
|
-
var toks = []
|
|
1135
|
-
|
|
1372
|
+
var toks = [], fb = 0
|
|
1373
|
+
arr.forEach(function (x) { if (x.tok !== null) { toks.push(x.tok); if (x.tokFallback) fb++ } })
|
|
1374
|
+
return { n: arr.length, execMs: execMs, tok: toks.length ? Math.round(toks.reduce(function (a, b) { return a + b }, 0) / toks.length) : null, tokN: toks.length, tokFallbackN: fb }
|
|
1136
1375
|
}
|
|
1137
1376
|
roi = { yes: roiAgg(roiYes), no: roiAgg(roiNo) }
|
|
1138
1377
|
}
|
|
@@ -1140,6 +1379,19 @@ function apply(ctx) {
|
|
|
1140
1379
|
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, roi: roi }
|
|
1141
1380
|
}
|
|
1142
1381
|
|
|
1382
|
+
// 单卡 token 口径(task-muupnnq5):ROI 行与「Token 消耗区」口径同源,避免同一张卡两处两个数。
|
|
1383
|
+
// 有分量(input/output/cacheWrite 任一 >0)→ 有效消耗(不含缓存读);只有 total 的老/存量卡 → 兜底 total 并标 fallback;
|
|
1384
|
+
// 两者皆无 → tok=null(无结算样本,跳过而不是记 0,避免拉低均值)。
|
|
1385
|
+
// 注:客户端 bundle 只注入 React(无模块系统),故此处内联,host 侧同款判定在 lib/usage.mjs taskEffectiveTokens。
|
|
1386
|
+
function roiTokenOf(u) {
|
|
1387
|
+
if (!u) return { tok: null, fallback: false }
|
|
1388
|
+
function pos(v) { var n = Number(v); return isFinite(n) && n > 0 ? n : 0 }
|
|
1389
|
+
var inp = pos(u.input), outp = pos(u.output), cw = pos(u.cacheWrite), tot = pos(u.total)
|
|
1390
|
+
if (inp > 0 || outp > 0 || cw > 0) return { tok: inp + outp + cw, fallback: false }
|
|
1391
|
+
if (tot > 0) return { tok: tot, fallback: true }
|
|
1392
|
+
return { tok: null, fallback: false }
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1143
1395
|
function TrendChart(props) {
|
|
1144
1396
|
var data = props.data || []
|
|
1145
1397
|
var max = 1; data.forEach(function (d) { if (d.count > max) max = d.count })
|
|
@@ -1163,17 +1415,23 @@ function apply(ctx) {
|
|
|
1163
1415
|
// 调研 ROI 对比行(task-mutnjesa):让数据替道理说话——「有调研 vs 无调研」分组账单直接摆给主窗口看,
|
|
1164
1416
|
// 比任何引导文案都管用。数据源 computeStats 的 roi(null = 任一组为空或总样本 <4,整块不渲染);
|
|
1165
1417
|
// 无调研组明显更慢(平均执行 > 有调研组 1.2 倍,阈值写清避免把随机波动误读成结论)时其数字用 warn 色。
|
|
1418
|
+
// token 口径(task-muupnnq5):**有效消耗**(输入+输出+缓存写,不含缓存读),与「Token 消耗区」大数字同源;
|
|
1419
|
+
// 组内只要有一张卡走了 total 兜底(无分量字段的老/存量卡),该数字后加 '~' 并在 title 里写明张数。
|
|
1166
1420
|
function ResearchRoiRow(props) {
|
|
1167
1421
|
var roi = props.roi
|
|
1168
1422
|
if (!roi) return null
|
|
1169
1423
|
function fmtM(ms) { var m = Math.round(ms / 60000); if (m < 60) return m + 'm'; return (m / 60).toFixed(1) + 'h' }
|
|
1170
1424
|
var noSlower = roi.no.execMs > roi.yes.execMs * 1.2
|
|
1171
1425
|
function grp(icon, label, g, warnNums) {
|
|
1172
|
-
|
|
1426
|
+
var approx = (g.tokFallbackN || 0) > 0
|
|
1427
|
+
var tip = g.tok === null ? label + ':无带 usage 结算记录的卡,token 均值不可计'
|
|
1428
|
+
: g.n + ' 卡中 ' + g.tokN + ' 张有 usage 结算记录;均值口径 = 有效消耗(输入+输出+缓存写,不含缓存读)'
|
|
1429
|
+
+ (approx ? ';其中 ' + g.tokFallbackN + ' 张无分量字段(老/存量结算),退化为含缓存读的合计值参与均值(故标 ~)' : '')
|
|
1430
|
+
return React.createElement('span', { title: tip },
|
|
1173
1431
|
icon + ' ' + label + ' ',
|
|
1174
|
-
React.createElement('span', { style: { fontWeight: 600, color: warnNums ? C.warn : C.text } }, g.n + ' 卡 平均 ' + fmtM(g.execMs) + '/约 ' + (g.tok === null ? '-' : fmtTokens(g.tok)) + ' tok'))
|
|
1432
|
+
React.createElement('span', { style: { fontWeight: 600, color: warnNums ? C.warn : C.text } }, g.n + ' 卡 平均 ' + fmtM(g.execMs) + '/约 ' + (g.tok === null ? '-' : (approx ? '~' : '') + fmtTokens(g.tok)) + ' tok'))
|
|
1175
1433
|
}
|
|
1176
|
-
return React.createElement('div', { style: { padding: '6px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12, fontSize: 10, color: C.text2, display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }, title: 'resolved/archived 且领取过的卡按有无调研注入分组现算(执行 = 被领取→完成;token 均值只计有 usage
|
|
1434
|
+
return React.createElement('div', { style: { padding: '6px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12, fontSize: 10, color: C.text2, display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }, title: 'resolved/archived 且领取过的卡按有无调研注入分组现算(执行 = 被领取→完成;token 均值只计有 usage 结算记录的卡,口径 = 有效消耗:输入+输出+缓存写,不含缓存读;老卡无分量字段时退化为合计并标 ~)' },
|
|
1177
1435
|
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('scale', 11), '调研 ROI'),
|
|
1178
1436
|
grp('📎', '有调研', roi.yes, false),
|
|
1179
1437
|
React.createElement('span', null, '·'),
|
|
@@ -1205,7 +1463,14 @@ function apply(ctx) {
|
|
|
1205
1463
|
var _R = React; var useState = _R.useState
|
|
1206
1464
|
var _a = useState(state.rfOpen || false), open = _a[0], setOpen = _a[1]
|
|
1207
1465
|
var rg = activeRange()
|
|
1208
|
-
function setRange(from, to) {
|
|
1466
|
+
function setRange(from, to) {
|
|
1467
|
+
state.dateRange = { from: from, to: to }
|
|
1468
|
+
// 报告/总览是本地现算,notify 即可;Token 区的模型分布/Top8/累计要按范围重算,
|
|
1469
|
+
// 而过滤在 host(run 级数据只在 host)→ 必须重拉一次 get-tasks(fetchTasks 会带上新范围)。
|
|
1470
|
+
// fetchTasks 在 kernel 域定义,同处 apply 函数体 → 函数声明提升,此处可用。
|
|
1471
|
+
if (typeof fetchTasks === 'function') fetchTasks()
|
|
1472
|
+
notify()
|
|
1473
|
+
}
|
|
1209
1474
|
function preset(days) {
|
|
1210
1475
|
if (days === 0) { setRange('', ''); return }
|
|
1211
1476
|
var to = new Date(); var from = new Date(Date.now() - (days - 1) * 86400000)
|
|
@@ -1233,8 +1498,8 @@ function apply(ctx) {
|
|
|
1233
1498
|
React.createElement('input', { type: 'date', value: rg.from, onChange: function (e) { setRange(e.target.value, rg.to) }, style: dateInput })),
|
|
1234
1499
|
React.createElement('label', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 4 } }, '截至',
|
|
1235
1500
|
React.createElement('input', { type: 'date', value: rg.to, onChange: function (e) { setRange(rg.from, e.target.value) }, style: dateInput })),
|
|
1236
|
-
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '
|
|
1237
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 5 } }, '
|
|
1501
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '作用于报告、全局总览与 Token 区(模型分布/Top8/累计)')),
|
|
1502
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 5 } }, '报告统计「时间范围内有活动」的任务;总览只显示范围内有活跃的会话;Token 区按 run 的本地日落点过滤(今日与近 7 天为固定口径,不受范围影响)。')) : null)
|
|
1238
1503
|
}
|
|
1239
1504
|
|
|
1240
1505
|
function GlobalBoards() {
|
|
@@ -1304,6 +1569,24 @@ function apply(ctx) {
|
|
|
1304
1569
|
// usageSummary 由 host 从各任务 t.usage 现算(t.usage 来自 Worker/Verifier 会话 v4 日志的
|
|
1305
1570
|
// assistant/message.usage 聚合)。这里只做展示、不做计费断言;日志读不到/还没有 run 结算时
|
|
1306
1571
|
// usageSummary.total 为 0,一律显示「暂无数据」。
|
|
1572
|
+
// 日账(usageSummary.byDay:{'YYYY-MM-DD': {t,e}})用于「今日」大数字与「近 7 天」条形。
|
|
1573
|
+
// 双指标口径:t = 总量(含缓存读)、e = 有效消耗(输入+输出+缓存写,不含缓存读)。
|
|
1574
|
+
// e === null 表示该日只有老形态 number 日账 / 存量兜底(没有逐 run 拆分,有效值不可知)——
|
|
1575
|
+
// 展示时退化为 t 并在文案/title 上标 ~ 近似,绝不把总量冒充有效值。
|
|
1576
|
+
// dayKey 口径与 host 记账完全一致——本地 getters 拼,绝不用 toISOString()(UTC 会让
|
|
1577
|
+
// 晚上 8 点后的消耗落到次日,今日消耗直接错位)。
|
|
1578
|
+
function localDayKey(d) {
|
|
1579
|
+
var x = d || new Date()
|
|
1580
|
+
function p2(n) { return (n < 10 ? '0' : '') + n }
|
|
1581
|
+
return x.getFullYear() + '-' + p2(x.getMonth() + 1) + '-' + p2(x.getDate())
|
|
1582
|
+
}
|
|
1583
|
+
// 近 n 天的本地日 key 序列(旧 → 新,最后一个是今天);本地日期构造天然跨月/跨年正确
|
|
1584
|
+
function lastNDays(n) {
|
|
1585
|
+
var out = []
|
|
1586
|
+
var now = new Date()
|
|
1587
|
+
for (var i = n - 1; i >= 0; i--) out.push(localDayKey(new Date(now.getFullYear(), now.getMonth(), now.getDate() - i)))
|
|
1588
|
+
return out
|
|
1589
|
+
}
|
|
1307
1590
|
function fmtTokens(n) {
|
|
1308
1591
|
var v = Number(n) || 0
|
|
1309
1592
|
if (v >= 1000000000) return (v / 1000000000).toFixed(1) + 'B'
|
|
@@ -1324,31 +1607,90 @@ function apply(ctx) {
|
|
|
1324
1607
|
function TokenUsage(props) {
|
|
1325
1608
|
var u = props.usage
|
|
1326
1609
|
var box = { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 }
|
|
1327
|
-
|
|
1610
|
+
// 范围激活标记:Token 区的模型分布/Top8/累计已按「统计范围」过滤(host 侧重算),
|
|
1611
|
+
// 不加标记的话用户没法判断看到的数字是全量还是范围内——标记与 RangeFilter 同源(activeRange)。
|
|
1612
|
+
var tg = activeRange()
|
|
1613
|
+
var tgOn = !!(tg.from || tg.to)
|
|
1614
|
+
var head = React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('bar-chart-3', 11), 'Token 消耗',
|
|
1615
|
+
tgOn ? React.createElement('span', { style: { fontSize: 9, fontWeight: 400, color: C.brand, border: '1px solid ' + C.brand, borderRadius: 8, padding: '0 6px' }, title: '模型分布 / 任务消耗 Top 8 / 累计已按统计范围过滤:' + rangeLabel() }, '范围内: ' + rangeLabel()) : null)
|
|
1328
1616
|
if (!u || !u.total) return React.createElement('div', { style: box }, head, React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据(Worker/Verifier 会话日志里还没有 usage 记录)'))
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1617
|
+
// ===== 主数字口径统一(task-muwq9u04):模型分布行与 Top8 行的主数字一律显示**有效消耗**(e),
|
|
1618
|
+
// 含缓存读的合计(t)退到 title 悬浮;否则同一块里「今日有效 3.7M」与「单任务 21M」并排自相矛盾
|
|
1619
|
+
// (用户就是这么判成 bug 的)。数据源:host 的 byModelEff(模型有效分摊)与 topTasks[].effective;
|
|
1620
|
+
// 老 host 缺 byModelEff / 老卡无五分量留账 → 退化用合计值并在 title 标 ~(口径不伪造)。
|
|
1621
|
+
// 排序按**显示口径**(有效)降序:条形长度与行序一致,否则首行不是最长的条。
|
|
1622
|
+
var models = Object.keys(u.byModel || {}).map(function (m) {
|
|
1623
|
+
var tt = u.byModel[m] || 0
|
|
1624
|
+
var ee = (u.byModelEff && typeof u.byModelEff[m] === 'number') ? u.byModelEff[m] : null
|
|
1625
|
+
return { model: m, total: tt, eff: (ee === null ? tt : ee), approx: ee === null }
|
|
1626
|
+
}).sort(function (a, b) { return b.eff - a.eff })
|
|
1627
|
+
var maxM = models.length ? (models[0].eff || 1) : 1
|
|
1628
|
+
var top = (u.topTasks || []).map(function (x) {
|
|
1629
|
+
var ee = (typeof x.effective === 'number') ? x.effective : null
|
|
1630
|
+
return { id: x.id, title: x.title, total: x.total, cacheRead: x.cacheRead, runs: x.runs, eff: (ee === null ? x.total : ee), approx: ee === null }
|
|
1631
|
+
}).sort(function (a, b) { return b.eff - a.eff })
|
|
1632
|
+
var maxT = top.length ? (top[0].eff || 1) : 1
|
|
1633
|
+
// 日账:今日数字取本地日 key,没有日账(byDay 缺字段/老 host)时退化为 0,不炸也不误报。
|
|
1634
|
+
// 读侧兼容两种单元形态:老 number(只有总量,有效值不可知)→ { t: n, e: null }。
|
|
1635
|
+
var byDay = (u.byDay && typeof u.byDay === 'object') ? u.byDay : {}
|
|
1636
|
+
function dayOf(v) {
|
|
1637
|
+
if (v && typeof v === 'object') return { t: Number(v.t) || 0, e: (v.e === 0 || v.e) ? Number(v.e) || 0 : null }
|
|
1638
|
+
return { t: Number(v) || 0, e: null }
|
|
1639
|
+
}
|
|
1640
|
+
var todayKey = localDayKey()
|
|
1641
|
+
var todayCell = dayOf(byDay[todayKey])
|
|
1642
|
+
// 有效消耗缺失(老日账/存量兜底)→ 大数字退化为总量并打 approx 标 ~,口径不伪造
|
|
1643
|
+
var todayApprox = todayCell.e === null
|
|
1644
|
+
var todayEff = todayApprox ? todayCell.t : todayCell.e
|
|
1645
|
+
var days = lastNDays(7)
|
|
1646
|
+
var maxDay = 1
|
|
1647
|
+
var hasDayData = false
|
|
1648
|
+
for (var di = 0; di < days.length; di++) { var dc = dayOf(byDay[days[di]]); var dv = (dc.e === null ? dc.t : dc.e); if (dv > maxDay) maxDay = dv; if (dc.t > 0 || dc.e > 0) hasDayData = true }
|
|
1333
1649
|
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))) }
|
|
1650
|
+
// 累计三分量:有效(真实成本)+ 缓存读(占了 total 的大头,必须单列才看得出虚高来源)+ 合计
|
|
1651
|
+
var effTotal = (typeof u.effective === 'number') ? u.effective : ((u.input || 0) + (u.output || 0) + (u.cacheWrite || 0))
|
|
1334
1652
|
return React.createElement('div', { style: box },
|
|
1335
1653
|
head,
|
|
1336
1654
|
React.createElement('div', { style: { display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap', marginBottom: 8 } },
|
|
1337
|
-
React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color: C.brand } }, fmtTokens(
|
|
1338
|
-
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens
|
|
1339
|
-
|
|
1655
|
+
React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color: C.brand }, title: '今日有效消耗(本地日 ' + todayKey + ')= 输入+输出+缓存写,不含缓存读;一次 run 的消耗整笔记在结算日' + (todayApprox ? '。本条日账来自老形态/存量兜底数据,有效值不可知,此处以总量近似(标 ~)' : '') }, (todayApprox ? '~' : '') + fmtTokens(todayEff)),
|
|
1656
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens(今日有效' + (todayApprox ? ' · 近似' : '') + ')'),
|
|
1657
|
+
todayCell.t > todayEff ? React.createElement('span', { style: { fontSize: 10, color: C.text2 }, title: '今日总量(含缓存读)——与有效消耗的差额就是缓存读' }, '含缓存读共 ', React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, fmtTokens(todayCell.t))) : null,
|
|
1658
|
+
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))),
|
|
1659
|
+
mini('输入', u.input), mini('输出', u.output), u.cacheWrite ? mini('缓存写', u.cacheWrite) : null),
|
|
1660
|
+
// 近 7 天迷你条形:高按区间 max 归一(今天高亮 brand,其余浅底 + 边框),
|
|
1661
|
+
// 柱高一律取**有效消耗**;e 不可知的日退化为总量(title 标 ~ 近似)。
|
|
1662
|
+
// 7 天全为 0 时整块不渲染(零残留,不占版面)
|
|
1663
|
+
hasDayData ? React.createElement('div', { style: { marginBottom: 8 } },
|
|
1664
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '近 7 天(有效消耗)'),
|
|
1665
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 4 } },
|
|
1666
|
+
days.map(function (k) {
|
|
1667
|
+
var c = dayOf(byDay[k])
|
|
1668
|
+
var approx = c.e === null
|
|
1669
|
+
var v = approx ? c.t : c.e
|
|
1670
|
+
var isToday = k === todayKey
|
|
1671
|
+
var h = v > 0 ? Math.max(3, Math.round(v / maxDay * 32)) : 3
|
|
1672
|
+
return React.createElement('div', { key: k, title: k.slice(5) + ':有效 ' + String(v) + (approx ? '(近似:老日账只有总量)' : '') + ' tok' + (c.t > v ? ' / 含缓存读共 ' + String(c.t) + ' tok' : ''), style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 } },
|
|
1673
|
+
React.createElement('div', { style: { width: '100%', height: h, background: isToday ? C.brand : C.nested, border: '1px solid ' + (isToday ? C.brand : C.border), borderRadius: 2 } }),
|
|
1674
|
+
React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2, whiteSpace: 'nowrap' } }, k.slice(5)))
|
|
1675
|
+
}))) : null,
|
|
1340
1676
|
React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } },
|
|
1341
1677
|
React.createElement('div', { style: { flex: '1 1 240px', minWidth: 200 } },
|
|
1342
|
-
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '
|
|
1678
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '按模型分布(有效消耗)'),
|
|
1343
1679
|
models.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : models.map(function (m) {
|
|
1344
|
-
return React.createElement(UsageRow, { key: m.model, label: m.model, value: m.
|
|
1680
|
+
return React.createElement(UsageRow, { key: m.model, label: m.model, value: m.eff, max: maxM, color: C.brand, title: m.model + ':有效 ' + (m.approx ? '~' : '') + String(m.eff) + ' tokens(不含缓存读)' + (m.approx ? '——本模型无有效分量留账,以合计近似' : '') + ' · 含缓存读合计 ' + String(m.total) + ' tokens' })
|
|
1345
1681
|
})),
|
|
1346
1682
|
React.createElement('div', { style: { flex: '1 1 240px', minWidth: 200 } },
|
|
1347
|
-
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '任务消耗 Top 8'),
|
|
1683
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '任务消耗 Top 8(有效消耗)'),
|
|
1348
1684
|
top.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : top.map(function (x) {
|
|
1349
1685
|
var t = getTask(x.id)
|
|
1350
|
-
return React.createElement(UsageRow, { key: x.id, label: x.title || x.id, value: x.
|
|
1351
|
-
})))
|
|
1686
|
+
return React.createElement(UsageRow, { key: x.id, label: x.title || x.id, value: x.eff, max: maxT, color: C.ok, labelColor: t ? C.brand : C.text2, title: x.title + '(有效 ' + (x.approx ? '~' : '') + String(x.eff) + ' tokens(不含缓存读) · 含缓存读合计 ' + String(x.total) + ' · 其中缓存读 ' + String(x.cacheRead || 0) + ' · ' + (x.runs || 0) + ' 次 run)' + (x.approx ? '(老卡无五分量留账,有效值以合计近似)' : '') + (t ? '——点击查看详情' : ''), onClick: t ? function () { state.detailId = x.id; notify() } : undefined })
|
|
1687
|
+
}))),
|
|
1688
|
+
// 口径边界:本区只统计看板派发的 Worker/Verifier run,主窗口对话自身不越界纳入
|
|
1689
|
+
// 范围说明(task-muwc7hjd):用户常把 RangeFilter 当成「整页过滤」,但今日大数字与近 7 天柱子
|
|
1690
|
+
// 是自身固定口径(今日=本地今天、近 7 天=最近 7 个本地日)——不随范围变,必须在文案里讲清,
|
|
1691
|
+
// 否则「选了范围数字没变」看起来像 bug。模型分布/Top8/累计才是范围生效的三处。
|
|
1692
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:仅看板派发的 Worker/Verifier run 消耗,不含主窗口对话;主数字(模型分布 / Top 8)与「今日」「近 7 天」均为有效消耗口径(输入+输出+缓存写,不含缓存读),含缓存读的合计在悬浮 title 里单列对照'),
|
|
1693
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。'))
|
|
1352
1694
|
}
|
|
1353
1695
|
|
|
1354
1696
|
// ===== 架构健康提示区(架构自省 L1 · 数据源:state.healthHints)=====
|
|
@@ -1484,6 +1826,19 @@ function apply(ctx) {
|
|
|
1484
1826
|
var _a = useState(false), open = _a[0], setOpen = _a[1]
|
|
1485
1827
|
var _p = useState(null), pos = _p[0], setPos = _p[1]
|
|
1486
1828
|
var ref = useRef(null); var btnRef = useRef(null)
|
|
1829
|
+
// 开关点击即时反馈(反馈:勾选几秒才同步,task-muw5uudk):
|
|
1830
|
+
// checked 是受控值(读 Props→state,由 3s 轮询通知才更新),此前 onChange 只发 rpc →
|
|
1831
|
+
// 点下去要等 set-board-config + fetchTasks 双往返(安静板卡上 tasksHash 不变还会被渲染节约吃掉)才翻面。
|
|
1832
|
+
// 口径:**先写 state + notify(点击瞬时翻面)→ 再 rpc('set-board-config') → 失败回滚 state + notify
|
|
1833
|
+
// 并复用错误条 reportReadErr(沿用「读路径错误防线」:绝不让开关停在未落盘的值上)**。
|
|
1834
|
+
// 成功路径的权威纠偏在 kernel.fetchTasks(配置变化检测),此处不重复 fetchTasks:3s 轮询自会带回真值。
|
|
1835
|
+
function cfgRollback(key, prev) { state[key] = prev; notify() }
|
|
1836
|
+
function setCfg(key, next) {
|
|
1837
|
+
var prev = !!state[key] // 乐观更新前的值,失败回滚用
|
|
1838
|
+
state[key] = next === true // 归一成布尔:默认开口径由服务端权威值(fetchTasks)纠偏
|
|
1839
|
+
notify() // 立即翻面,不等 rpc
|
|
1840
|
+
rpc('set-board-config', { key: key, value: next === true }).catch(function (e) { cfgRollback(key, prev); reportReadErr('设置保存失败:' + readErrText(e)) })
|
|
1841
|
+
}
|
|
1487
1842
|
useEffect(function () { if (!open) return; function onDown(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }; document.addEventListener('mousedown', onDown); return function () { document.removeEventListener('mousedown', onDown) } }, [open])
|
|
1488
1843
|
function toggle() {
|
|
1489
1844
|
// position:fixed + 视口坐标:面板容器是 overflow:hidden + maxHeight:60vh,
|
|
@@ -1512,8 +1867,26 @@ function apply(ctx) {
|
|
|
1512
1867
|
// 学习飞轮 v1 总开关:关掉后不生成候选教训、prompt 不提软召回、详情页不渲染「沉淀」按钮
|
|
1513
1868
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '学习反馈'),
|
|
1514
1869
|
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
|
|
1515
|
-
React.createElement('input', { type: 'checkbox', checked: !!props.feedbackEnabled, onChange: function (e) {
|
|
1516
|
-
React.createElement('span', null, '候选教训(Verifier 驳回/仲裁结论自动生成候选,Worker prompt 提示先检索历史教训)'))
|
|
1870
|
+
React.createElement('input', { type: 'checkbox', checked: !!props.feedbackEnabled, onChange: function (e) { setCfg('feedbackEnabled', e.target.checked) } }),
|
|
1871
|
+
React.createElement('span', null, '候选教训(Verifier 驳回/仲裁结论自动生成候选,Worker prompt 提示先检索历史教训)')),
|
|
1872
|
+
// 回执开关(用户要求「回执可以做一个开关,放到设置里」):派发/完成两类回执各自可关,缺省都开。
|
|
1873
|
+
// 只影响回执播报,不影响派发与状态机;歧义裁决通知不在此闸门内(见下行说明文案)。
|
|
1874
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '通知'),
|
|
1875
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
|
|
1876
|
+
React.createElement('input', { type: 'checkbox', checked: props.notifyDispatch !== false, onChange: function (e) { setCfg('notifyDispatch', e.target.checked) } }),
|
|
1877
|
+
React.createElement('span', null, '⚡ 派发回执(任务被 Worker/Verifier 领走时播报)')),
|
|
1878
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260, marginTop: 3 } },
|
|
1879
|
+
React.createElement('input', { type: 'checkbox', checked: props.notifyDone !== false, onChange: function (e) { setCfg('notifyDone', e.target.checked) } }),
|
|
1880
|
+
React.createElement('span', null, '✅ 完成回执(任务完成或阻塞时聚合播报)')),
|
|
1881
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '歧义裁决通知不受这两个开关影响(任务等人裁决必须提醒)'),
|
|
1882
|
+
// 史诗拆分总开关(板级 epicSplit,缺省 true):**只关引导,不禁机制**——关掉后 Team 提示词不再
|
|
1883
|
+
// 注入「大任务必须拆分」第 6 条、create-task 响应不再附 suggestSplit 软提示;显式传 parentId 建
|
|
1884
|
+
// 子卡、史诗自动收口/hooks 状态机照常(用户/主窗口明确要拆时不受阻)。勾选态缺字段=开,与 host 同口径。
|
|
1885
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '功能'),
|
|
1886
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
|
|
1887
|
+
React.createElement('input', { type: 'checkbox', checked: props.epicSplit !== false, onChange: function (e) { setCfg('epicSplit', e.target.checked) } }),
|
|
1888
|
+
React.createElement('span', null, '🧩 史诗拆分:大任务引导拆为 epic + 子任务')),
|
|
1889
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '关掉只停引导:显式 parentId 建子卡与史诗自动收口照常工作')) : null)
|
|
1517
1890
|
}
|
|
1518
1891
|
|
|
1519
1892
|
function TeamView() {
|