dsh-agent-board 1.6.1 → 1.7.1
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 +15 -5
- package/index.mjs +6 -0
- package/lib/client/board-list.js +91 -14
- package/lib/client/dashboard.js +62 -22
- package/lib/client/kernel.js +65 -13
- package/lib/client/task-detail.js +72 -2
- package/lib/client.js +290 -51
- package/lib/core.mjs +185 -0
- package/lib/dispatch.mjs +30 -144
- package/lib/health.mjs +8 -7
- package/lib/rpc.mjs +47 -9
- package/lib/session.mjs +18 -2
- package/package.json +1 -1
- package/test/core.test.mjs +663 -1
- package/test/health.test.mjs +19 -8
package/README.md
CHANGED
|
@@ -91,8 +91,11 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
91
91
|
|
|
92
92
|
- 会话标题栏「智能看板」按钮 → 顶部抽屉面板(看板 / 团队 / 仪表盘三视图)
|
|
93
93
|
- 面板右上角「+ 新建任务」:表单弹层填标题/描述/优先级/管线/touches/依赖多选/验收脚本,Team 托管默认存为草稿(提交走 create-task RPC,成功即刷新、失败表单内联报错)
|
|
94
|
-
-
|
|
95
|
-
-
|
|
94
|
+
- 新建任务弹层为视口级 overlay(fixed 全屏遮罩 + 限高 80vh + 字段区内部滚动 + 按钮区吸底常驻,不再被看板抽屉裁切);卡片「当前动作」行摘要化为「工具名 + 关键参数」(40 字符内有意义断点截断,悬停 title 看 host 原文),不再展示裸 JSON
|
|
95
|
+
- 六列状态流:草稿 → 待办 → 进行中 → 验证中 → 已完成 → 阻塞(空板也统一渲染六列骨架 + 引导 CTA:右上角「+ 新建任务」或对话一句 task_create 建第一张卡)
|
|
96
|
+
- 读路径 error 防线:get-tasks 返回 {error} 或网络抖动时保留现有卡片与设置(不再静默清空全板),面板头部下方出非阻断红色错误条,下次刷新成功自动消失
|
|
97
|
+
- 耗时口径三分离:待办/草稿卡显「⏳ 等待 N」(创建→被领取),执行后显「⏱ 执行 N」(被领取→完成,tooltip 写清口径)——排队时间不再混进"耗时"
|
|
98
|
+
- 拖拽流转、多选批量操作(带一步撤销)、文本/优先级/标签筛选;卡片键盘可达(Tab 聚焦 + Enter/Space 开详情),详情页附「流转到」按钮组(状态迁移的键盘替代通道,失败内联回显)
|
|
96
99
|
- 归档区:时间倒序 + 排序选择器 + 纵向滚动
|
|
97
100
|
- Esc 逐级关闭(详情 → 看板 → 面板)
|
|
98
101
|
- 全部结构性图标为 Lucide 线性 SVG(`currentColor` 跟随主题,浅深色自适应)
|
|
@@ -101,7 +104,9 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
101
104
|
|
|
102
105
|
- 仪表盘视图新增「Token 消耗」区:本看板累计总量 + 输入 / 输出 / 缓存读(缓存写非零时一并展示)拆分、按模型分布条形图、任务消耗 **Top 8**(标题可点击直达该任务详情);进行中的卡片右上角显示本任务已累计消耗(`⛁ 数字`)
|
|
103
106
|
- 数据来源:每次 Worker/Verifier run 结算时读该 run 的 v4 会话日志(`~/.dsh/sessions/*/<runId>/session.v4.jsonl.zstd`),把 `assistant/message` 事件的 `usage`(`inputTokens` / `outputTokens` / `cacheReadTokens` / `cacheWriteTokens` / `totalTokens`,字段形状以真实日志为准)按 zstd 帧逐帧累加到任务 `usage`(含按模型小计与 `runs` 计数,多轮重跑/驳回重做自动累加),`get-tasks` 再现算 board 级 `usageSummary`(总量 / 按模型 / Top8,不落盘额外表)——**只做展示、不做计费断言**,日志读不到或没有 usage 时一律显示「暂无数据」
|
|
104
|
-
- 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 /
|
|
107
|
+
- 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 / 任务**执行**时长 p90 >45min(claimedAt→resolvedAt 纯干活口径,不含排队)/ 同路径驳回 ≥2 次),命中才在仪表盘渲染提示条(⚠️/ℹ️ 两级,最多 3 条)——让运行数据主动提示"该优化架构了"(如某文件反复成为锁热点=该拆),信号只建议不裁判
|
|
108
|
+
- 统计区耗时同口径拆分:「平均排队 / 平均执行」双行展示(平均验收单列不变)
|
|
109
|
+
- 统计区新增「调研 ROI」对比行:resolved/archived 卡按有无调研注入分组现算卡数 / 平均执行时长 / 平均 token(双组总样本 ≥4 才渲染,无调研组明显更慢时数字 warn 色提示)
|
|
105
110
|
|
|
106
111
|
### 学习反馈(候选教训信号 → 主窗口沉淀)
|
|
107
112
|
|
|
@@ -127,7 +132,11 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
127
132
|
- **文件级排他**:`touches` 声明本任务要改的文件/glob(如 `["src/**", "README.md"]`);进行中的任务持有文件锁,派发器发现候选与活动任务 touches 重叠就跳过本轮(卡片显示 `🔒 等文件释放`,详情页列出在等谁),锁在提交验收/完成后自动释放——避免并行 Worker 改同一批文件互踩。手动「派发」遇到冲突会列出冲突任务,确认后才以 `force` 越权派发
|
|
128
133
|
- **里程碑进展通道**:Worker 每完成一个可验证的里程碑,可调用 `board_report`(`kind: "progress"`,`question` 写一行进展摘要 ≤200 字符)上报——进行中的卡片显示「📈 最近进展 · 相对时间」(覆盖式只留最新一条),详情页消息流保留全部 progress 条目
|
|
129
134
|
- **防表演式汇报**:进展契约只写在 Worker prompt 里、且要求「有实际产物/结论才报」(禁止定时汇报);progress **静默不通知主窗口**(不进回执聚合),也不写 `history` 流转记录,避免刷屏
|
|
130
|
-
- **子任务**:父子层级 + 上下文继承 + 父任务自动流转 +
|
|
135
|
+
- **子任务**:父子层级 + 上下文继承 + 父任务自动流转 + 级联归档(僵尸态出清:`archive-task` 对「无活跃 run 的 in-progress」——典型如被 parentKick 推进后子任务已全部归档的史诗——直接放行,有活跃 run 的仍拒)
|
|
136
|
+
- **死会话零打扰**:15s 派发心跳对无活 root 的会话板直接跳过(不读盘/不写盘/零日志,会话重开后自动恢复派发);`create-task` 拒绝不完整 sessionId(防裸短 id 建出幻影板)
|
|
137
|
+
- **面板轮询渲染短路**:`get-tasks` 响应附 `tasksHash`,任务列表无变化时前端跳过重渲染(usage/池状态等轻量字段照常刷新)
|
|
138
|
+
- 卡片识别层:父卡显示「📦 史诗 · resolved/total」徽章 + 3px 迷你进度条(有在跑子任务时附「▸ 在跑:标题」行);子卡标题下显示「↳ 父任务标题」;依赖未满足的待办卡底部灰字「⛓ 等待「依赖标题」」(childStats 由 host 现算,缺字段一律不渲染)
|
|
139
|
+
- 详情父子区块:父卡详情列子任务清单(状态色点 + 标题,点击直达子卡详情,标题行汇总 resolved/total);子卡详情顶行「↳ 史诗:父标题」点击回跳父卡
|
|
131
140
|
- **删除通道(真删,无 undo)**:`delete-task` RPC(卡片 hover 垃圾桶按钮 / 详情页「删除」按钮,均先 `confirm('删除不可恢复,确认删除「标题」?')`)+ `batch-op op='delete'`(多选模式底部「批量删除」,同样 confirm)。状态门禁:**草稿/待办/阻塞可删**;进行中/验证中拒绝并提示先用 `terminate-agent` 终止(避免在跑的 run 变孤儿);已完成/取消引导改用归档(`archive-task`,留档可检索);有**未归档子任务**时拒删(防 `parentId` 悬空破坏父任务自动流转);已归档任务幂等返回 ok。是真删(从 `tasks` 数组移除),因此**不产生 `batch-undo` 撤销快照**(批量条对 delete 不显示「↩️ 撤销」),删除操作在 host 端 `console.error` 留一行日志便于溯源
|
|
132
141
|
- **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team 模式提示词都写了这条契约
|
|
133
142
|
- **suggestSplit 软提示**:`task_create` / `create-task` 发现描述超 500 字符、或标题/描述命中「全量 / 整体 / 系统级 / 全面 / 重构 / 所有模块 / 整个」等史诗特征词时,返回体附带一行 `suggestSplit` 建议文案(**只提示,不阻断创建与派发**;未命中则不出现该字段,老调用方无感)
|
|
@@ -135,9 +144,10 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
135
144
|
### 一次性派发(v74 去池化)
|
|
136
145
|
|
|
137
146
|
- 每个任务 spawn 一个**一次性子代理**(Worker/Verifier),上下文全量注入 prompt,做完即销毁——无常驻池、无池化状态残留
|
|
138
|
-
- **预研上下文注入(contextFiles/contextNotes)**:主窗口调研时读过的文件与笔记,由 host 在派发时读盘取最新内容,经「上下文注入」区块提供给 Worker/Verifier(不混进 user prompt);预算口径单文件 8KB、总包 40KB
|
|
147
|
+
- **预研上下文注入(contextFiles/contextNotes)**:主窗口调研时读过的文件与笔记,由 host 在派发时读盘取最新内容,经「上下文注入」区块提供给 Worker/Verifier(不混进 user prompt);预算口径单文件 8KB、总包 40KB;UI 侧调研门禁——full/work 且声明了 touches 却未附调研的卡片亮「⚠️ 无调研」徽章,详情页「调研注入」区列 files 清单 + notes 字数(无则明示)
|
|
139
148
|
- **锚点行段**:`contextFiles` 支持 `path:L2350-L2420` / `path:L2350` 行段语法(只认尾部 `:L<行号>`,兼容 Windows 盘符),只注入该段(段长上限 400 行,超出截断并标注);锚点无效(越界/写法错)自动回退头部注入并标注「锚点无效,已回退头部」
|
|
140
149
|
- **截断结构索引**:头部注入被预算截断时,标注升级为「截断:共 N 行,已注入 1–M 行」,并附结构索引块(JS/TS 顶层函数/类/箭头赋值、Markdown 标题及行号,上限 40 条)——Worker 照索引用锚点语法补读目标段即可,不用全文盘点
|
|
150
|
+
- **派发调研门禁(warning 族,软提示不阻断)**:`task_create`/`create-task` 响应附 `warning` 字段——①描述为空「Worker 只能凭标题猜需求」②full/work + touches 非空而未附调研上下文 ③touches 含整树 glob 建议精确到文件级(可多条合并);GUI 表单内黄色展示不关窗。epic 发布(publish)时自动轻量预检全部子任务注入情况,缺材料则 pushSysNote 汇总提醒主窗口(全有不打扰);派发时读包失败落任务「最近失败」行,不再静默
|
|
141
151
|
- **Worker/Verifier 均可配置异构模型**(⚙️ 弹出层下拉选择,空 = 继承父级),避免同源盲点;模型故障自动熔断回退父级模型
|
|
142
152
|
- 孤儿回收:子代理 run 结束/丢失超 2 分钟 → 任务自动回待办重派
|
|
143
153
|
- 看门狗:运行超时且事件流停滞 → 标记"疑似卡死"(不自动杀,裁决权交主窗口/用户)
|
package/index.mjs
CHANGED
|
@@ -58,12 +58,18 @@ export function apply(ctx) {
|
|
|
58
58
|
pushSysNote: notify.pushSysNote, maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone,
|
|
59
59
|
})
|
|
60
60
|
storeDeps.poolCycle = dispatch.poolCycle // 晚绑定收口
|
|
61
|
+
// 僵尸 epic 出清配套(反馈 n-mutma3mmwceq):archive-task 门禁的活性判定。
|
|
62
|
+
// 口径:runsFor 表内有记录且未 settled 即活跃。settleRun/手动终止都会摘除表项;
|
|
63
|
+
// settled=true 是 finish→settleRun 的结算过渡窗口(Worker 已结束),视为不活跃放行。
|
|
64
|
+
function hasActiveRun(sid, taskId) { var rec = session.runsFor(sid)[taskId]; return !!(rec && !rec.settled) }
|
|
61
65
|
createRpc(ctx, state, {
|
|
62
66
|
getActorId: session.getActorId, resolveRoot: session.resolveRoot, toolSessionId: session.toolSessionId, rpcSessionId: session.rpcSessionId,
|
|
63
67
|
rootForSession: session.rootForSession, deriveWorkMode: session.deriveWorkMode, runsFor: session.runsFor,
|
|
64
68
|
rt: store.rt, mutateLocked: store.mutateLocked,
|
|
65
69
|
maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone,
|
|
70
|
+
pushSysNote: notify.pushSysNote, sessionCwd: session.sessionCwd, // 调研门禁③:epic 发布预检汇总投递 + 预研路径相对解析根
|
|
66
71
|
spawnOneShot: dispatch.spawnOneShot, accumulateRunUsage: dispatch.accumulateRunUsage, readContextPack: dispatch.readContextPack,
|
|
72
|
+
hasActiveRun: hasActiveRun,
|
|
67
73
|
})
|
|
68
74
|
|
|
69
75
|
console.log('[task-board] v74 loaded (pool removed: one-shot dispatch, context injected per task, dispose on settle)')
|
package/lib/client/board-list.js
CHANGED
|
@@ -67,14 +67,72 @@
|
|
|
67
67
|
function isCardHover(id, kind) { return String(state.cardHover || '') === (id + '|' + kind) }
|
|
68
68
|
function setHover(id, kind) { var v = id ? (id + '|' + kind) : ''; if (state.cardHover !== v) { state.cardHover = v; notify() } }
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
// 卡片键盘焦点态(无障碍,反馈 n-mut9rzpyc7p1):内联样式表达不了 :focus 伪类,
|
|
71
|
+
// 用 focus/blur 事件 + state 现算主题色 outline——样式由 state 推导,轮询重渲染也不会丢焦点框。
|
|
72
|
+
function isCardFocus(id) { return String(state.cardFocus || '') === id }
|
|
73
|
+
function setCardFocus(id) { var v = id || ''; if (state.cardFocus !== v) { state.cardFocus = v; notify() } }
|
|
74
|
+
|
|
75
|
+
// ===== 卡片「当前动作」行摘要化(client 侧提炼;host 仍下发原文,不改 host)=====
|
|
76
|
+
// host 的 activity 形如 `🔧 pwsh {"command": "Get-ChildItem …"}`(90 字符硬截,常断在 JSON 中段,
|
|
77
|
+
// 原样展示零信息量,反馈 n-mut914xana0t)或 `💬 自由文本`。这里提炼为「工具名 + 关键参数摘要」:
|
|
78
|
+
// pwsh → 命令体;read/write/edit → 文件 basename;browser_* → 动作名 + 目标(target/url/ref);
|
|
79
|
+
// 参数摘要 40 字符内、优先在空格/路径分隔符处断点;解析失败/自由文本回退为原文前 40 字符(头部截断)。
|
|
80
|
+
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)
|
|
81
|
+
// 从不完整 JSON 文本里抢指定 key 的第一个字符串值(host 文本常被硬截,JSON.parse 多半失败,正则直达)
|
|
82
|
+
function actArg(rest, key) { var m = String(rest || '').match(new RegExp('"' + key + '"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)')); return m ? m[1] : '' }
|
|
83
|
+
function actBase(p) { p = String(p || ''); var i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\')); return i >= 0 ? p.slice(i + 1) : p }
|
|
84
|
+
function activitySummary(text) {
|
|
85
|
+
var s = String(text || '').trim()
|
|
86
|
+
if (!s) return ''
|
|
87
|
+
if (s.indexOf('💬') === 0) return actCut(s, 40) // 自由文本:保留 💬 前缀从头部截
|
|
88
|
+
var body = s.replace(/^🔧\s*/, '') // 去工具调用前缀(host 格式 🔧 <tool> <argsJSON>)
|
|
89
|
+
var m = body.match(/^([A-Za-z_][\w-]*)\s*([\s\S]*)$/)
|
|
90
|
+
if (!m) return actCut(s, 40)
|
|
91
|
+
var tool = m[1], rest = (m[2] || '').trim()
|
|
92
|
+
if (!rest) return actCut(s, 40)
|
|
93
|
+
var v = ''
|
|
94
|
+
if (tool === 'pwsh') v = actArg(rest, 'command')
|
|
95
|
+
else if (tool === 'read' || tool === 'write' || tool === 'edit') v = actBase(actArg(rest, 'file_path'))
|
|
96
|
+
if (v) return tool + ' ' + actCut(v, 40)
|
|
97
|
+
if (tool.indexOf('browser_') === 0) {
|
|
98
|
+
var act = actArg(rest, 'action'); var tgt = actArg(rest, 'target') || actArg(rest, 'url') || actArg(rest, 'ref')
|
|
99
|
+
var parts = []; if (act) parts.push(act); if (tgt) parts.push(tgt)
|
|
100
|
+
if (parts.length) return tool + ' ' + actCut(parts.join(' '), 40)
|
|
101
|
+
}
|
|
102
|
+
return actCut(s, 40) // 无法解析:原文前 40 字符(从头部截,不再断在 JSON 中段)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 史诗/依赖/父子识别层(childStats 缺省兼容:host 未返回该字段时一律不渲染相关元素):
|
|
106
|
+
// 父卡 = 元信息行首位「📦 史诗 · resolved/total」徽章 + 3px 迷你进度条 + activeTitle 非空时「▸ 在跑」行;
|
|
107
|
+
// 子卡 = 标题下一行小字「↳ 父任务标题」(从 state.tasks 找父卡,找不到回退 shortId);
|
|
108
|
+
// 依赖未满足的待办卡 = 卡片底部灰字「⛓ 等待「第一个未满足依赖标题」」(多个时附「 等 N 个」)。
|
|
109
|
+
// ===== 调研门禁·UI:「⚠️ 无调研」徽章判定(纯 client 侧现算,host 不下发该字段)=====
|
|
110
|
+
// 口径:pipeline ∈ {full, work}(direct 主窗口自处理,无需调研注入)且 touches 非空(声明了要改文件)
|
|
111
|
+
// 且 context.files / context.notes 皆空。context 缺省兼容:老任务无 context 对象按空处理——
|
|
112
|
+
// 即老任务也会亮徽章,这是预期行为(立单质量可见即问责;draft/pending 等状态照常显示,与状态无关)。
|
|
113
|
+
function noResearch(t) {
|
|
114
|
+
var pl = t.pipeline || 'full'
|
|
115
|
+
if (pl !== 'full' && pl !== 'work') return false
|
|
116
|
+
if (!Array.isArray(t.touches) || t.touches.length === 0) return false
|
|
117
|
+
var cx = t.context || {}
|
|
118
|
+
var files = Array.isArray(cx.files) ? cx.files : []
|
|
119
|
+
var notes = typeof cx.notes === 'string' ? cx.notes.trim() : ''
|
|
120
|
+
return files.length === 0 && notes.length === 0
|
|
121
|
+
}
|
|
122
|
+
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);
|
|
123
|
+
// 无障碍(反馈 n-mut9rzpyc7p1):卡片根以 button 角色进 Tab 序(aria-label=标题+状态),
|
|
124
|
+
// Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
|
|
125
|
+
// focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
|
|
126
|
+
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() } }
|
|
127
|
+
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' } },
|
|
128
|
+
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,
|
|
71
129
|
t.frozen ? React.createElement('span', { title: '已冻结:不参与自动派发(详情页可「解除冻结」)', style: { color: C.brand, fontWeight: 600 } }, '❄ 冻结') : null,
|
|
72
130
|
(Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
|
|
73
131
|
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
74
132
|
(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,
|
|
75
133
|
t.status === 'in-progress' && t.claimedBy ? React.createElement('span', null, '⚡ ' + shortId(t.claimedBy)) : null,
|
|
76
134
|
t.assignee ? React.createElement('span', null, '👤→' + shortId(t.assignee)) : null,
|
|
77
|
-
|
|
135
|
+
durB ? React.createElement('span', { title: durB.tip }, durB.txt) : null,
|
|
78
136
|
// 删除入口(仅草稿/待办/阻塞,多选模式下隐藏以免误触):hover 才由透明转红,平时不抢视觉
|
|
79
137
|
delOk ? React.createElement('span', {
|
|
80
138
|
onClick: function (e) { e.stopPropagation(); deleteTask(t.id, t.title) }, // 阻止冒泡:不打开详情页
|
|
@@ -82,10 +140,14 @@
|
|
|
82
140
|
title: '删除任务(不可恢复;执行中请先终止,已落定请用归档)',
|
|
83
141
|
style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', cursor: 'pointer', color: C.err, opacity: isCardHover(t.id, 'del') ? 1 : 0, transition: 'opacity .15s' }
|
|
84
142
|
}, ic('trash-2', 11)) : null),
|
|
85
|
-
|
|
143
|
+
// 史诗迷你进度条(3px 高,resolved/total 比例,ok 色填充)+ 在跑子任务行(activeTitle 非空才渲染)
|
|
144
|
+
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,
|
|
145
|
+
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,
|
|
146
|
+
// 当前动作行:展示摘要(工具名+关键参数),悬停 title 仍是 host 原文
|
|
147
|
+
(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,
|
|
86
148
|
// 里程碑进展(Worker 主动上报的轻量进展,kind=progress):进行中的卡片显示最新一条 + 相对时间,无则不显示
|
|
87
149
|
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,
|
|
88
|
-
t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null) }
|
|
150
|
+
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) }
|
|
89
151
|
|
|
90
152
|
function ArchiveView() {
|
|
91
153
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
@@ -146,6 +208,8 @@
|
|
|
146
208
|
var _dr = useState(wm === 'team'), asDraft = _dr[0], setAsDraft = _dr[1]
|
|
147
209
|
var _bs = useState(false), busy = _bs[0], setBusy = _bs[1]
|
|
148
210
|
var _er = useState(''), err = _er[0], setErr = _er[1]
|
|
211
|
+
var _ok = useState(''), okMsg = _ok[0], setOkMsg = _ok[1] // 成功行(仅「创建成功但响应带 warning」的不关窗路径展示)
|
|
212
|
+
var _wn = useState(''), warn = _wn[0], setWarn = _wn[1] // host create-task 成功路径附带的 warning 原文(可能多条合并,原样黄色展示)
|
|
149
213
|
// 依赖候选:当前看板 status 为 draft/pending 的任务
|
|
150
214
|
var cands = state.tasks.filter(function (t) { return t.status === 'draft' || t.status === 'pending' })
|
|
151
215
|
function close() { state.createOpen = false; notify() }
|
|
@@ -156,13 +220,17 @@
|
|
|
156
220
|
if (!t) { setErr('⚠️ 请先填写标题'); return }
|
|
157
221
|
var touches = touchesRaw.split(/[\n,,;;]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
|
|
158
222
|
var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
|
|
159
|
-
setBusy(true); setErr('')
|
|
223
|
+
setBusy(true); setErr(''); setOkMsg(''); setWarn('')
|
|
160
224
|
rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
|
|
161
225
|
setBusy(false)
|
|
162
226
|
if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
|
|
227
|
+
fetchTasks()
|
|
228
|
+
// 调研门禁·软警告:host 成功路径可附 warning 字符串(如空描述提醒,可能多条合并)——不拦截创建、
|
|
229
|
+
// 也不打断成功提示:表单保持打开,成功行 + ⚠️ 黄色 warning 原文行同屏展示,读完自行关闭;
|
|
230
|
+
// 清掉标题防「再点一次创建」连击出重复卡(空标题会被校验拦住)。无 warning 维持原关窗 + 头部 flash 行为。
|
|
231
|
+
if (r && r.warning) { setOkMsg(asDraft ? '✅ 已存草稿' : '✅ 已创建任务'); setWarn(String(r.warning)); setTitle(''); return }
|
|
163
232
|
state.createOpen = false
|
|
164
233
|
flashCreated(asDraft ? '✅ 已存草稿' : '✅ 已创建任务')
|
|
165
|
-
fetchTasks()
|
|
166
234
|
notify()
|
|
167
235
|
}).catch(function (e) { setBusy(false); setErr('⚠️ ' + String(e)) })
|
|
168
236
|
}
|
|
@@ -171,11 +239,16 @@
|
|
|
171
239
|
function field(label, node) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node) }
|
|
172
240
|
var btnGhost = { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 5, background: 'transparent', color: C.text2, cursor: 'pointer' }
|
|
173
241
|
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 }
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
242
|
+
// 视口级 overlay(修裁切,反馈 n-mut9rzl4mxe4):fixed 全屏遮罩脱离看板抽屉(maxHeight 60vh + overflow hidden)
|
|
243
|
+
// 的裁剪上下文(抽屉无 transform/filter,fixed 相对视口定位不被裁);弹窗限高 80vh、字段区内部滚动、
|
|
244
|
+
// 底部按钮区吸底常驻——标题与「创建并派发」始终同屏,无需盲滚。
|
|
245
|
+
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() } },
|
|
246
|
+
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' } },
|
|
247
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 12px', borderBottom: '1px solid ' + C.border, flexShrink: 0 } },
|
|
177
248
|
React.createElement('span', { style: { fontSize: 13, fontWeight: 600, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 5 } }, ic('plus', 14), '新建任务'),
|
|
178
249
|
React.createElement('button', { onClick: close, title: '关闭', style: { border: 'none', background: 'transparent', cursor: 'pointer', color: C.text2, display: 'inline-flex' } }, ic('x', 14))),
|
|
250
|
+
// 字段区:内容超出 80vh 时在此内部滚动
|
|
251
|
+
React.createElement('div', { style: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '10px 12px 4px' } },
|
|
179
252
|
field('标题 *', React.createElement('input', { value: title, onChange: function (e) { setTitle(e.target.value) }, placeholder: '一句话说清要做什么', style: inp })),
|
|
180
253
|
field('描述', React.createElement('textarea', { value: desc, onChange: function (e) { setDesc(e.target.value) }, rows: 3, placeholder: '背景 / 目标 / 约束(可空)', style: Object.assign({}, inp, { resize: 'vertical', minHeight: 46 }) })),
|
|
181
254
|
React.createElement('div', { style: { display: 'flex', gap: 8 } },
|
|
@@ -196,9 +269,13 @@
|
|
|
196
269
|
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: C.text, cursor: 'pointer', marginBottom: 6, flexWrap: 'wrap' } },
|
|
197
270
|
React.createElement('input', { type: 'checkbox', checked: asDraft, onChange: function (e) { setAsDraft(e.target.checked) } }),
|
|
198
271
|
'存为草稿',
|
|
199
|
-
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, wm === 'team' ? '(Team 托管默认草稿;取消勾选 = 立即派发)' : '(先补依赖/上下文,稍后统一发布)')),
|
|
200
|
-
|
|
201
|
-
React.createElement('div', { style: {
|
|
202
|
-
React.createElement('
|
|
203
|
-
React.createElement('
|
|
272
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, wm === 'team' ? '(Team 托管默认草稿;取消勾选 = 立即派发)' : '(先补依赖/上下文,稍后统一发布)'))),
|
|
273
|
+
// 底部:错误行 + 按钮区吸底常驻(flexShrink:0,不随字段区滚动)
|
|
274
|
+
React.createElement('div', { style: { flexShrink: 0, padding: '8px 12px 12px', borderTop: '1px solid ' + C.border, background: C.bg } },
|
|
275
|
+
okMsg ? React.createElement('div', { style: { fontSize: 11, color: C.ok, marginBottom: 6 } }, okMsg) : null,
|
|
276
|
+
warn ? React.createElement('div', { style: { fontSize: 11, color: C.warn, marginBottom: 6, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, '⚠️ ' + warn) : null,
|
|
277
|
+
err ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 6 } }, err) : null,
|
|
278
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
|
|
279
|
+
React.createElement('button', { onClick: close, style: btnGhost }, '取消'),
|
|
280
|
+
React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即进入派发池', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并派发'))))))
|
|
204
281
|
}
|
package/lib/client/dashboard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
|
-
// dashboard —— 仪表盘域:统计(computeStats/TrendChart/StatCard/BarRow)· 范围筛选(RangeFilter)
|
|
2
|
+
// dashboard —— 仪表盘域:统计(computeStats/TrendChart/StatCard/BarRow/ResearchRoiRow)· 范围筛选(RangeFilter)
|
|
3
3
|
// · 全局总览(GlobalBoards)· 报告(buildReport/ReportButton)· Token 消耗(TokenUsage)
|
|
4
4
|
// · 架构健康(HealthHints,架构自省 L1)
|
|
5
5
|
// · 团队池(TeamView/PoolStatus)· 设置(WorkModeSwitch/PoolCfg/MinCfg/ModelCfg/PoolCfgPopover)
|
|
@@ -7,14 +7,39 @@
|
|
|
7
7
|
// ============================================================================
|
|
8
8
|
|
|
9
9
|
function computeStats(tasks) {
|
|
10
|
-
var total = tasks.length, byStatus = {}, byPriority = {}, byAgent = {},
|
|
10
|
+
var total = tasks.length, byStatus = {}, byPriority = {}, byAgent = {}, queueTimes = [], execTimes = [], verifyTimes = [], todayDone = 0, todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); var recentActivity = []
|
|
11
11
|
var dailyDone = [] // #15 近 7 天每日完成趋势
|
|
12
12
|
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 }) }
|
|
13
|
-
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.
|
|
13
|
+
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 }) }) })
|
|
14
14
|
recentActivity.sort(function (a, b) { return (b.timestamp || '').localeCompare(a.timestamp || '') }); recentActivity = recentActivity.slice(0, 10)
|
|
15
15
|
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) }
|
|
16
16
|
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) + ' 时' }
|
|
17
|
-
|
|
17
|
+
// 调研 ROI 分组(task-mutnjesa):resolved/archived 且有执行数据(claimedAt→resolvedAt 可算,
|
|
18
|
+
// 无 claimedAt 的手工/direct 卡跳过)的卡按「有无调研注入」分两组——有调研 = context.files /
|
|
19
|
+
// context.notes 任一非空(context 缺省按无调研,与详情页「调研注入」行同口径)。
|
|
20
|
+
// 与 usageSummary 同哲学:纯现算零存储。任一组为空或总样本 <4 时 roi=null(样本太少没说服力,不渲染)。
|
|
21
|
+
var roiYes = [], roiNo = []
|
|
22
|
+
tasks.forEach(function (t) {
|
|
23
|
+
if (t.status !== 'resolved' && t.status !== 'archived') return
|
|
24
|
+
if (!t.claimedAt || !t.resolvedAt) return
|
|
25
|
+
var execMs = new Date(t.resolvedAt) - new Date(t.claimedAt)
|
|
26
|
+
if (!(execMs >= 0)) return // 时间戳异常(NaN/负值)跳过
|
|
27
|
+
var cx = t.context || {}
|
|
28
|
+
var hasRes = (Array.isArray(cx.files) && cx.files.length > 0) || (typeof cx.notes === 'string' && cx.notes.trim().length > 0)
|
|
29
|
+
var tok = (t.usage && typeof t.usage.total === 'number' && t.usage.total > 0) ? t.usage.total : null // 无 usage 记录的卡不拉低 token 均值(只计有结算样本)
|
|
30
|
+
;(hasRes ? roiYes : roiNo).push({ execMs: execMs, tok: tok })
|
|
31
|
+
})
|
|
32
|
+
var roi = null
|
|
33
|
+
if (roiYes.length > 0 && roiNo.length > 0 && roiYes.length + roiNo.length >= 4) {
|
|
34
|
+
var roiAgg = function (arr) {
|
|
35
|
+
var execMs = Math.round(arr.reduce(function (a, b) { return a + b.execMs }, 0) / arr.length)
|
|
36
|
+
var toks = []; arr.forEach(function (x) { if (x.tok !== null) toks.push(x.tok) })
|
|
37
|
+
return { n: arr.length, execMs: execMs, tok: toks.length ? Math.round(toks.reduce(function (a, b) { return a + b }, 0) / toks.length) : null }
|
|
38
|
+
}
|
|
39
|
+
roi = { yes: roiAgg(roiYes), no: roiAgg(roiNo) }
|
|
40
|
+
}
|
|
41
|
+
// 耗时口径三分离(task-mutdnitw):avgQueue=平均排队(创建→被领取)、avgExec=平均执行(被领取→完成),均只统计领取过的卡;avgVerify 口径不动(完成→验收)
|
|
42
|
+
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 }
|
|
18
43
|
}
|
|
19
44
|
|
|
20
45
|
function TrendChart(props) {
|
|
@@ -34,6 +59,29 @@
|
|
|
34
59
|
|
|
35
60
|
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)) }
|
|
36
61
|
|
|
62
|
+
// 双行指标卡(耗时口径三分离:原「平均完成时间」一个展示位拆为 排队/执行 两行,不挤占其他指标位)
|
|
63
|
+
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)) }
|
|
64
|
+
|
|
65
|
+
// 调研 ROI 对比行(task-mutnjesa):让数据替道理说话——「有调研 vs 无调研」分组账单直接摆给主窗口看,
|
|
66
|
+
// 比任何引导文案都管用。数据源 computeStats 的 roi(null = 任一组为空或总样本 <4,整块不渲染);
|
|
67
|
+
// 无调研组明显更慢(平均执行 > 有调研组 1.2 倍,阈值写清避免把随机波动误读成结论)时其数字用 warn 色。
|
|
68
|
+
function ResearchRoiRow(props) {
|
|
69
|
+
var roi = props.roi
|
|
70
|
+
if (!roi) return null
|
|
71
|
+
function fmtM(ms) { var m = Math.round(ms / 60000); if (m < 60) return m + 'm'; return (m / 60).toFixed(1) + 'h' }
|
|
72
|
+
var noSlower = roi.no.execMs > roi.yes.execMs * 1.2
|
|
73
|
+
function grp(icon, label, g, warnNums) {
|
|
74
|
+
return React.createElement('span', null,
|
|
75
|
+
icon + ' ' + label + ' ',
|
|
76
|
+
React.createElement('span', { style: { fontWeight: 600, color: warnNums ? C.warn : C.text } }, g.n + ' 卡 平均 ' + fmtM(g.execMs) + '/约 ' + (g.tok === null ? '-' : fmtTokens(g.tok)) + ' tok'))
|
|
77
|
+
}
|
|
78
|
+
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 结算记录的卡)' },
|
|
79
|
+
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('scale', 11), '调研 ROI'),
|
|
80
|
+
grp('📎', '有调研', roi.yes, false),
|
|
81
|
+
React.createElement('span', null, '·'),
|
|
82
|
+
grp('⚠️', '无调研', roi.no, noSlower))
|
|
83
|
+
}
|
|
84
|
+
|
|
37
85
|
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))) }
|
|
38
86
|
|
|
39
87
|
// ===== 仪表盘:日期范围筛选(共享给报告与全局总览)=====
|
|
@@ -205,23 +253,13 @@
|
|
|
205
253
|
}))))
|
|
206
254
|
}
|
|
207
255
|
|
|
208
|
-
// ===== 架构健康提示区(架构自省 L1 · 数据源:
|
|
256
|
+
// ===== 架构健康提示区(架构自省 L1 · 数据源:state.healthHints)=====
|
|
209
257
|
// healthHints 由 host lib/health.mjs 的 computeHealthHints(tasks) 每次请求现算(纯函数零存储零 IO):
|
|
210
|
-
// [{ level: 'warn'|'info', text }]
|
|
211
|
-
//
|
|
212
|
-
//
|
|
258
|
+
// [{ level: 'warn'|'info', text }]。kernel fetchTasks 已将其与 tasks 同源透传进 state.healthHints,
|
|
259
|
+
// 本组件不再自持 rpc('get-tasks')(消灭仪表盘打开期间的 3s 双轮询),随面板 notify 重渲染同步刷新。
|
|
260
|
+
// 缺省兼容:老 host 无此字段 / 非数组 / 空数组 → 返回 null,整块不渲染(零残留)。无按钮无状态。
|
|
213
261
|
function HealthHints() {
|
|
214
|
-
var
|
|
215
|
-
var _a = useState([]), hints = _a[0], setHints = _a[1]
|
|
216
|
-
useEffect(function () {
|
|
217
|
-
var dead = false
|
|
218
|
-
rpc('get-tasks').then(function (d) {
|
|
219
|
-
if (dead) return
|
|
220
|
-
var arr = (d && Array.isArray(d.healthHints)) ? d.healthHints : []
|
|
221
|
-
setHints(arr.filter(function (h) { return h && h.text }))
|
|
222
|
-
}).catch(function () {})
|
|
223
|
-
return function () { dead = true }
|
|
224
|
-
}, [state.tasks])
|
|
262
|
+
var hints = (Array.isArray(state.healthHints) ? state.healthHints : []).filter(function (h) { return h && h.text })
|
|
225
263
|
if (!hints.length) return null
|
|
226
264
|
// 两级配色:warn=⚠️ 琥珀(C.warn);info=ℹ️ 蓝灰(DSH 现有 business 信息色——色板无独立 info 档)
|
|
227
265
|
var infoColor = 'var(--dsw-alias-state-business-primary)'
|
|
@@ -247,7 +285,8 @@
|
|
|
247
285
|
React.createElement('div', { style: { display: 'flex', gap: 12, marginBottom: 12, flexWrap: 'wrap' } },
|
|
248
286
|
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 }) })),
|
|
249
287
|
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 }) }))),
|
|
250
|
-
React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' } }, React.createElement(
|
|
288
|
+
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 })),
|
|
289
|
+
React.createElement(ResearchRoiRow, { roi: stats.roi }),
|
|
251
290
|
React.createElement(TrendChart, { data: stats.dailyDone }),
|
|
252
291
|
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,
|
|
253
292
|
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))) })))
|
|
@@ -308,8 +347,9 @@
|
|
|
308
347
|
var vActive = (ps.verifiers || []).filter(function (v) { return v.busy }).length
|
|
309
348
|
var vTotal = (ps.verifiers || []).length
|
|
310
349
|
return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
|
|
311
|
-
|
|
312
|
-
React.createElement('span', { title: '
|
|
350
|
+
// 池水位 title 标注(反馈 n-mut9rzoq3flu):裸数字新用户看不懂,⚡=Worker 在跑/上限,✓=Verifier 在跑/上限
|
|
351
|
+
React.createElement('span', { title: 'Worker 在跑/上限', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), wActive + '/' + wTotal),
|
|
352
|
+
React.createElement('span', { title: 'Verifier 在跑/上限', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), vActive + '/' + vTotal))
|
|
313
353
|
}
|
|
314
354
|
|
|
315
355
|
// #11 头部减负:池配置收纳进 ⚙️ 弹出层(含 #17 verifier 异构模型设置)
|