dsh-agent-board 1.7.1 → 1.7.2
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 +17 -9
- package/index.mjs +5 -3
- package/lib/client/board-list.js +37 -5
- package/lib/client/dashboard.js +111 -13
- package/lib/client/kernel.js +43 -10
- package/lib/client/task-detail.js +136 -9
- package/lib/client.js +327 -37
- package/lib/core.mjs +308 -20
- package/lib/dispatch.mjs +154 -12
- package/lib/health.mjs +43 -11
- package/lib/notify.mjs +42 -12
- package/lib/policy.mjs +17 -1
- package/lib/rpc.mjs +49 -11
- package/lib/session.mjs +7 -2
- package/lib/store.mjs +2 -1
- package/lib/usage.mjs +84 -3
- package/package.json +1 -1
- package/test/core.test.mjs +1544 -39
- package/test/health.test.mjs +86 -4
package/README.md
CHANGED
|
@@ -102,8 +102,12 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
102
102
|
|
|
103
103
|
### 仪表盘(Token 消耗)
|
|
104
104
|
|
|
105
|
-
- 仪表盘视图新增「Token
|
|
106
|
-
-
|
|
105
|
+
- 仪表盘视图新增「Token 消耗」区:本看板累计**有效 / 缓存读 / 合计**三分量 + 输入 / 输出(缓存写非零时一并展示)拆分、按模型分布条形图、任务消耗 **Top 8**(标题可点击直达该任务详情,title 附「有效 N / 缓存读 M」拆分);大数字为**今日有效消耗**(本地日口径,无日账则显示 0,旁附「含缓存读共 X」小字对照,有效值不可知时以总量近似并标 `~`)、旁附「累计(本看板)」三分量小字,下方「近 7 天」迷你条形按日展示近 7 天**有效消耗**(今天高亮,title 补总量对照,7 天全为 0 时不渲染);进行中的卡片右上角显示本任务已累计消耗(`⛁ 数字`);区底固定一行口径 caption:**仅看板派发的 Worker/Verifier run 消耗,不含主窗口对话**
|
|
106
|
+
- 口径要点:**有效消耗 = 输入 + 输出 + 缓存写(不含缓存读)**。实证本板累计 10.3M 里缓存读占 9.7M(94%)、有效消耗仅 605K——总量被缓存读撑高约 17 倍,因此大数字与「近 7 天」一律用有效值,缓存读单列可查(差额一眼可见),避免「仪表盘与真实消耗差太多」
|
|
107
|
+
- 数据来源:每次 Worker/Verifier run 结算时读该 run 的 v4 会话日志(`~/.dsh/sessions/*/<runId>/session.v4.jsonl.zstd`),把 `assistant/message` 事件的 `usage`(`inputTokens` / `outputTokens` / `cacheReadTokens` / `cacheWriteTokens` / `totalTokens`,字段形状以真实日志为准)按 zstd 帧逐帧累加到任务 `usage`(含按模型小计、`runs` 计数与 `byDay` 日账——本地日 `YYYY-MM-DD`,一次 run 整笔记在结算日;多轮重跑/驳回重做自动累加);**run 级留账**:本次用量同时原样写回 `t.runs` 对应条目的 `usage`(五分量俱全),因此按天 / 按模型 / 按阶段任何维度都能从 runs 精确重建,口径若要再调整不必回头猜
|
|
108
|
+
- `byDay` 为**双指标**形态 `{ t, e }`(`t` = 总量含缓存读,`e` = 有效消耗不含缓存读;一次 run 不跨日拆分)。聚合端兼容老数据:值是裸 number 的历史日账按 `{ t: n, e: null }` 处理——总量照常保留(「近 7 天」不会整段消失),有效值不可知就置 `null` 并由 UI 标 `~` 近似,**绝不把总量冒充有效值**
|
|
109
|
+
- `get-tasks` 再现算 board 级 `usageSummary`(总量 / 有效合计 / 按模型 / Top8(带有效与缓存读拆分)/ 日账 `byDay`,不落盘额外表;老任务无 `byDay` 时整笔近似归到 `updatedAt` 的本地日,无 `updatedAt` 则不计入任何日)——**只做展示、不做计费断言**,日志读不到或没有 usage 时一律显示「暂无数据」
|
|
110
|
+
- 统计口径边界:本区只统计**看板派发的 Worker/Verifier run**,**不含主窗口对话本身**(不越界统计,UI caption 已明示)
|
|
107
111
|
- 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 / 任务**执行**时长 p90 >45min(claimedAt→resolvedAt 纯干活口径,不含排队)/ 同路径驳回 ≥2 次),命中才在仪表盘渲染提示条(⚠️/ℹ️ 两级,最多 3 条)——让运行数据主动提示"该优化架构了"(如某文件反复成为锁热点=该拆),信号只建议不裁判
|
|
108
112
|
- 统计区耗时同口径拆分:「平均排队 / 平均执行」双行展示(平均验收单列不变)
|
|
109
113
|
- 统计区新增「调研 ROI」对比行:resolved/archived 卡按有无调研注入分组现算卡数 / 平均执行时长 / 平均 token(双组总样本 ≥4 才渲染,无调研组明显更慢时数字 warn 色提示)
|
|
@@ -129,17 +133,20 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
129
133
|
- **依赖调度**:`dependsOn` 声明依赖(DFS 环检测),依赖全部完成后才会被派发,串行链路自动编排
|
|
130
134
|
- **管线分档**:`full`(执行+验证)/ `work`(只做不验)/ `direct`(不进池,主窗口直接处理),创建时按规则自动分类、可手动覆盖
|
|
131
135
|
- **硬性验收**:`acceptance` 字段写验收脚本命令,Worker 必须实际运行、Verifier 必须独立复跑
|
|
132
|
-
- **文件级排他**:`touches` 声明本任务要改的文件/glob(如 `["src/**", "README.md"]
|
|
136
|
+
- **文件级排他**:`touches` 声明本任务要改的文件/glob(如 `["src/**", "README.md"]`);持有文件锁的任务(`in-progress`/`verifying`/`resolved`)与候选 touches 重叠就跳过本轮(卡片显示 `🔒 等文件释放`,详情页列出在等谁),**锁持到归档**:验收通过后继续持锁(护住「验收后-提交前」的提交窗口期),归档(含批量归档)才真释放,`cancelled` 立即放锁(放弃语义=不再产出)——避免并行 Worker 改同一批文件互踩。手动「派发」遇到冲突会列出冲突任务,确认后才以 `force` 越权派发
|
|
133
137
|
- **里程碑进展通道**:Worker 每完成一个可验证的里程碑,可调用 `board_report`(`kind: "progress"`,`question` 写一行进展摘要 ≤200 字符)上报——进行中的卡片显示「📈 最近进展 · 相对时间」(覆盖式只留最新一条),详情页消息流保留全部 progress 条目
|
|
134
138
|
- **防表演式汇报**:进展契约只写在 Worker prompt 里、且要求「有实际产物/结论才报」(禁止定时汇报);progress **静默不通知主窗口**(不进回执聚合),也不写 `history` 流转记录,避免刷屏
|
|
135
|
-
- **子任务**:父子层级 + 上下文继承 + 父任务自动流转 + 级联归档(僵尸态出清:`archive-task` 对「无活跃 run 的 in-progress」——典型如被 parentKick 推进后子任务已全部归档的史诗——直接放行,有活跃 run
|
|
139
|
+
- **子任务**:父子层级 + 上下文继承 + 父任务自动流转 + 级联归档(僵尸态出清:`archive-task` 对「无活跃 run 的 in-progress」——典型如被 parentKick 推进后子任务已全部归档的史诗——直接放行,有活跃 run 的仍拒);**归档子任务仍计入史诗进度并在详情留档可见**——进度分子口径 `settled = resolved | cancelled | archived`、分母也含归档,归档一张子卡不会再让史诗进度从 `0/10` 退化成 `0/9`(进度只增不减),全归档的父卡也照常显示徽章;详情子任务清单不排归档行(灰化 + 行尾「已归档」徽章 + 沉底排序,点击仍可进子卡看留档)
|
|
136
140
|
- **死会话零打扰**:15s 派发心跳对无活 root 的会话板直接跳过(不读盘/不写盘/零日志,会话重开后自动恢复派发);`create-task` 拒绝不完整 sessionId(防裸短 id 建出幻影板)
|
|
137
141
|
- **面板轮询渲染短路**:`get-tasks` 响应附 `tasksHash`,任务列表无变化时前端跳过重渲染(usage/池状态等轻量字段照常刷新)
|
|
138
|
-
|
|
139
|
-
-
|
|
142
|
+
- **看板抽屉稳定性(isRoot 蝶变防抖)**:生成开始/结束瞬间 agents 树重建会让 `isRoot` 出现单次瞬态 `false`——现在需**连续 3 次(≈9s)false** 才收起抽屉(会话生命周期内「曾确认 true」粘滞),看板不再在生成状态切换时突然消失;从未确认 root 的子代理会话仍即时隐藏入口,保护语义不变
|
|
143
|
+
- 卡片识别层:父卡显示「📦 史诗 · settled/total」徽章 + 3px 迷你进度条(`settled = resolved | cancelled | archived`,total 同口径含归档;有在跑子任务时附「▸ 在跑:标题」行);子卡标题下显示「↳ 父任务标题」;依赖未满足的待办卡底部灰字「⛓ 等待「依赖标题」」(childStats 由 host 现算,缺字段一律不渲染)
|
|
144
|
+
- 详情父子区块:父卡详情列子任务清单(状态色点 + 标题,**含已归档留档**——归档行灰化 + 行尾「已归档」徽章并沉底,点击直达子卡详情,标题行汇总 settled/total);子卡详情顶行「↳ 史诗:父标题」点击回跳父卡
|
|
145
|
+
- **可选 hooks(史诗可选点位:前置/后置各是一次真实 agent 运行)**:`epic.hooks = { pre: { enabled, prompt, state, runId }, post: 同构 }`——**前置**让子任务具备开跑条件(串行闸门:未完成前该史诗的子任务一张都不派),**后置**把这批活收口(全部子任务了结后跑一遍,完成后史诗才转验证中);prompt 只给薄框架契约,具体动作由 hook agent **自行决策**,**吃不准就走歧义上报**(失败即转阻塞等人裁决:重试/跳过/放弃,不自动重跑)。hook run 记在 `epic.runs[]`(role `hook-pre`/`hook-post`),卡片带相位徽章(`⏳ 前置准备中` / `🧪 收尾中` / `⚠️ hook 失败待裁决`,无 hooks 的老卡零渲染),详情页 hooks 区可编辑 enabled+prompt、查看 state/耗时并跳转 run 会话、失败一键跳裁决区;**hooks 仅主窗口可设**(工具与 RPC 双通道门禁),commit/push 不入任何默认形态
|
|
140
146
|
- **删除通道(真删,无 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` 留一行日志便于溯源
|
|
141
147
|
- **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team 模式提示词都写了这条契约
|
|
142
148
|
- **suggestSplit 软提示**:`task_create` / `create-task` 发现描述超 500 字符、或标题/描述命中「全量 / 整体 / 系统级 / 全面 / 重构 / 所有模块 / 整个」等史诗特征词时,返回体附带一行 `suggestSplit` 建议文案(**只提示,不阻断创建与派发**;未命中则不出现该字段,老调用方无感)
|
|
149
|
+
- **开关 `epicSplit`(史诗拆分总开关)**(⚙️ 入池配置弹层「**功能**」小节,默认**开**):关掉只停**引导**——Team 提示词第 6 条拆分条款整条不注入、`create-task`/`task_create` 响应不再附 `suggestSplit` 软提示;**机制不禁**(显式传 `parentId` 建子卡、史诗自动收口与 hooks 状态机照常工作,明确要拆时不受阻);老看板文件没有该字段 → 读路径自动补 `true`(与升级前行为一致)。工具描述里的粒度契约是静态工具定义(随 request header 快照),不在开关范围
|
|
143
150
|
|
|
144
151
|
### 一次性派发(v74 去池化)
|
|
145
152
|
|
|
@@ -152,6 +159,7 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
152
159
|
- 孤儿回收:子代理 run 结束/丢失超 2 分钟 → 任务自动回待办重派
|
|
153
160
|
- 看门狗:运行超时且事件流停滞 → 标记"疑似卡死"(不自动杀,裁决权交主窗口/用户)
|
|
154
161
|
- 歧义上报:Worker 遇到歧义不猜测,上报等主窗口裁决(任何模式下都通知);裁决后新 Worker 携带裁决答案接手
|
|
162
|
+
- 驳回详情全量带回:三条驳回路径(`board_verdict` 工具 / Verifier 文本结算 / 手动 `task_verify`·`verify-task`)统一往 `t.messages` 落一条 `kind: "rejection"` 完整驳回包(summary + checks 逐条核对证据;手动路径补写 `t.verification`),经 `buildMessages` 全量注入重派 Worker prompt——新 Worker 据此返工,不再只看到 300 字截断的 history 记录
|
|
155
163
|
- 手动派发:详情页「派发 / 派发验收」按钮可随时手动触发单任务派发(auto 模式补派、manual 模式主通道)
|
|
156
164
|
- 会话隔离:看板按会话分桶,多会话互不干扰
|
|
157
165
|
|
|
@@ -169,14 +177,14 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
169
177
|
|
|
170
178
|
- **歧义裁决**:Worker 遇歧义不猜测,一律上报;裁决后新 Worker 携带答案接手(Team 托管档附带 system prompt 派发引导 + 默认草稿护栏)
|
|
171
179
|
- **Verifier 验收**:`acceptance` 硬性验收脚本命令,Worker 必须实际运行、Verifier 必须独立复跑;跨档一致
|
|
172
|
-
- **touches 排他**:`touches`
|
|
180
|
+
- **touches 排他**:`touches` 文件级排他锁在活动任务间生效,冲突任务跳过本轮派发,**锁持到归档**(`in-progress`/`verifying`/`resolved` 持有,归档释放,`cancelled` 立即释放);跨档一致
|
|
173
181
|
- 孤儿回收、看门狗、级联归档、会话隔离同样三档一致
|
|
174
182
|
|
|
175
183
|
Team 托管档独有(调度员体验):
|
|
176
184
|
- 主窗口 system prompt 注入派发引导(提示词层面建议实质性改动走看板,不硬拦截);引导含**上下文书写提示**——子代理是全新会话、无会话记忆,description 写不够会自行调研跑偏
|
|
177
185
|
- **默认草稿护栏**:`task_create` / `create-task` 缺省建为草稿(草稿不派发),先把所有任务的 dependsOn、contextNotes/contextFiles 补齐,再逐个 `task_update publish=true` 统一发布;确实要立即派发的单个任务才显式传 `draft:false`
|
|
178
186
|
- **歧义通知 25s 去抖**:通知延迟 25s 投递,投递前重读看板——歧义已被裁决、或任务已 resolved/archived 就静默跳过(消除主窗口 turn 排队导致的过期回声);同一任务连续多次上报只投最新一条
|
|
179
|
-
- 任务完成/阻塞时主窗口收到**批量聚合回执**(45s 窗口或满 5
|
|
187
|
+
- 任务完成/阻塞时主窗口收到**批量聚合回执**(45s 窗口或满 5 条聚合,等主窗口空闲再发,不打断对话);⚙️ 设置区「通知」小节有两个回执开关——`notifyDispatch`(⚡ 派发回执:任务被 Worker/Verifier 领走时播报)与 `notifyDone`(✅ 完成回执:完成/阻塞时聚合播报),缺省均**开**(老看板文件缺字段自动补 `true`);**歧义裁决通知不受开关影响**(裁决通道不是回执,任务等人裁决必须提醒)
|
|
180
188
|
|
|
181
189
|
> 兼容:旧的 `set-board-mode` / `set-team-mode` 两个 RPC 原样保留(旧客户端与脚本不受影响),
|
|
182
190
|
> 内部仍以 `boardMode` + `teamMode` 两个字段落盘,老看板文件无损;`get-tasks` 额外返回派生字段 `workMode` 供 UI 单点读取。
|
|
@@ -220,7 +228,7 @@ v1.6.0 起 host 端从单体 index.mjs(1487 行)拆为薄壳 + 7 个领域
|
|
|
220
228
|
| 模块 | 域 | 内容 |
|
|
221
229
|
|---|---|---|
|
|
222
230
|
| `policy.mjs` | 策略层 | 粒度治理软闸门 + 学习飞轮候选教训(纯函数零状态) |
|
|
223
|
-
| `usage.mjs` | 统计 | v4 会话日志定位 / zstd 分帧 / token usage
|
|
231
|
+
| `usage.mjs` | 统计 | v4 会话日志定位 / zstd 分帧 / token usage 聚合(纯函数;有效消耗 `effectiveTokens` + `byDay` 双指标 `{t,e}` 聚合,兼容老 number 日账) |
|
|
224
232
|
| `session.mjs` | 会话 | root 解析缓存 / 会话 id 归一 / workMode 派生 / runsFor |
|
|
225
233
|
| `store.mjs` | 持久化 | boardPath / rt / wt 原子落盘 / 跨重启继承 / fileLocks 串行化 / mutateLocked |
|
|
226
234
|
| `notify.mjs` | 通知 | makeMsg / 歧义 25s 去抖 / 回执聚合 + 空闲门控 / 投递前过滤 |
|
package/index.mjs
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// 共享闭包状态全部收进 apply 内的显式 state 对象,逐模块经参数注入(禁止跨模块隐式引用)。
|
|
15
15
|
//
|
|
16
16
|
// 兼容 re-export:对外导出契约不变(单测与历史引用直接 import 自本文件)。
|
|
17
|
-
export { TASK_SIZE_CONTRACT, SUGGEST_SPLIT_TEXT, TEAM_SPLIT_RULE, suggestSplitOf, withSplitHint } from './lib/policy.mjs'
|
|
18
|
-
export { findRunLog, readRunUsage, aggregateUsageSummary } from './lib/usage.mjs'
|
|
17
|
+
export { TASK_SIZE_CONTRACT, SUGGEST_SPLIT_TEXT, TEAM_SPLIT_RULE, suggestSplitOf, withSplitHint, splitRuleOf } from './lib/policy.mjs'
|
|
18
|
+
export { findRunLog, readRunUsage, aggregateUsageSummary, effectiveTokens, taskEffectiveTokens } from './lib/usage.mjs'
|
|
19
19
|
import { createSession } from './lib/session.mjs'
|
|
20
20
|
import { createStore } from './lib/store.mjs'
|
|
21
21
|
import { createNotify } from './lib/notify.mjs'
|
|
@@ -32,6 +32,7 @@ export function apply(ctx) {
|
|
|
32
32
|
knownSessions: {}, // 心跳驱动的会话集合(>30 分钟无活跃淘汰)
|
|
33
33
|
teamModeCache: {}, // rt()/set-*-mode 同步,systemPrompt 引导段读取
|
|
34
34
|
feedbackCache: {}, // 学习飞轮开关缓存(rt()/set-board-config 同步)
|
|
35
|
+
epicSplitCache: {}, // 史诗拆分总开关缓存(rt()/set-board-config 同步,Team 引导段读取)
|
|
35
36
|
fileLocks: {}, // 每会话一条 promise 链,串行化所有 读-改-写
|
|
36
37
|
cyclePending: {}, // kickCycle 50ms 去抖
|
|
37
38
|
activeRuns: {}, // sid -> { taskId: { run, role, taskId, startedAt, model } }
|
|
@@ -54,8 +55,9 @@ export function apply(ctx) {
|
|
|
54
55
|
var dispatch = createDispatch(ctx, state, {
|
|
55
56
|
rt: store.rt, wt: store.wt, mutateLocked: store.mutateLocked, kickCycle: store.kickCycle,
|
|
56
57
|
rootForSession: session.rootForSession, sessionCwd: session.sessionCwd,
|
|
57
|
-
withTimeout: session.withTimeout, runsFor: session.runsFor, feedbackOn: session.feedbackOn,
|
|
58
|
+
withTimeout: session.withTimeout, runsFor: session.runsFor, feedbackOn: session.feedbackOn, epicSplitOn: session.epicSplitOn,
|
|
58
59
|
pushSysNote: notify.pushSysNote, maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone,
|
|
60
|
+
notifyDispatched: notify.notifyDispatched,
|
|
59
61
|
})
|
|
60
62
|
storeDeps.poolCycle = dispatch.poolCycle // 晚绑定收口
|
|
61
63
|
// 僵尸 epic 出清配套(反馈 n-mutma3mmwceq):archive-task 门禁的活性判定。
|
package/lib/client/board-list.js
CHANGED
|
@@ -103,7 +103,9 @@
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// 史诗/依赖/父子识别层(childStats 缺省兼容:host 未返回该字段时一律不渲染相关元素):
|
|
106
|
-
// 父卡 = 元信息行首位「📦 史诗 ·
|
|
106
|
+
// 父卡 = 元信息行首位「📦 史诗 · settled/total」徽章 + 3px 迷你进度条 + activeTitle 非空时「▸ 在跑」行;
|
|
107
|
+
// settled = resolved | cancelled | archived(含已归档子任务、total 也含),口径同宿主 aggregateChildStats;
|
|
108
|
+
// 老宿主只下发 resolved 时回退读它(缺省兼容)——所以归档子卡不再让徽章从 0/10 退化成 0/9;
|
|
107
109
|
// 子卡 = 标题下一行小字「↳ 父任务标题」(从 state.tasks 找父卡,找不到回退 shortId);
|
|
108
110
|
// 依赖未满足的待办卡 = 卡片底部灰字「⛓ 等待「第一个未满足依赖标题」」(多个时附「 等 N 个」)。
|
|
109
111
|
// ===== 调研门禁·UI:「⚠️ 无调研」徽章判定(纯 client 侧现算,host 不下发该字段)=====
|
|
@@ -119,13 +121,43 @@
|
|
|
119
121
|
var notes = typeof cx.notes === 'string' ? cx.notes.trim() : ''
|
|
120
122
|
return files.length === 0 && notes.length === 0
|
|
121
123
|
}
|
|
122
|
-
|
|
124
|
+
// ===== epic hook 相位徽章(hooks=agent run 的卡片侧可见性,task-muuw56yf)=====
|
|
125
|
+
// 与 host core.normalizeHooks 契约同源:epic.hooks = { pre: { enabled, prompt, state, runId, pending? }, post: 同构 },
|
|
126
|
+
// state ∈ idle | running | done | failed(host 状态机推进,UI 只读展示,不在这里改状态)。
|
|
127
|
+
// 缺省兼容硬约束:卡上无 hooks / 该点位未启用 → 返回 null,老卡渲染逐字不变(零额外 DOM)。
|
|
128
|
+
// 优先级与文案(同一时刻只可能有一个相位有话说):
|
|
129
|
+
// pre|post failed → 「⚠️ hook 失败待裁决」(err 色):史诗已被 host 转 blocked 挂歧义,过程态文案都让位;
|
|
130
|
+
// pre running → 「⏳ 前置准备中」:串行闸门期内,该 epic 的子任务一张都不派;
|
|
131
|
+
// post running → 「🧪 收尾中」:子任务已全部了结、epic 仍 in-progress,等收口 run 归还;
|
|
132
|
+
// idle / done → 不渲染(普通进行中维持「📦 史诗 · x/y」原样,稳定态不加噪音)。
|
|
133
|
+
function hookBadgeOf(t) {
|
|
134
|
+
var h = t && t.hooks
|
|
135
|
+
if (!h) return null
|
|
136
|
+
var pre = (h.pre && h.pre.enabled) ? h.pre : null
|
|
137
|
+
var post = (h.post && h.post.enabled) ? h.post : null
|
|
138
|
+
if ((pre && pre.state === 'failed') || (post && post.state === 'failed')) {
|
|
139
|
+
var ph = (pre && pre.state === 'failed') ? 'pre' : 'post'
|
|
140
|
+
return { txt: '⚠️ hook 失败待裁决', color: C.err, tip: 'hooks.' + ph + '(' + (ph === 'pre' ? '前置准备' : '收口') + ')运行失败:史诗已转阻塞等人裁决(重试/跳过/放弃),详情页可跳裁决区' }
|
|
141
|
+
}
|
|
142
|
+
if (pre && pre.state === 'running') return { txt: '⏳ 前置准备中', color: C.brand, tip: '前置 hook run 正在跑:完成前该史诗的子任务一张都不派(串行闸门)' }
|
|
143
|
+
if (post && post.state === 'running') return { txt: '🧪 收尾中', color: C.brand, tip: '收口 hook run 正在跑:子任务已全部了结,史诗仍 in-progress,收口完成后才转验证中' }
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
function hookPhaseBadge(t) {
|
|
147
|
+
var b = hookBadgeOf(t)
|
|
148
|
+
if (!b) return null
|
|
149
|
+
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)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
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);
|
|
123
153
|
// 无障碍(反馈 n-mut9rzpyc7p1):卡片根以 button 角色进 Tab 序(aria-label=标题+状态),
|
|
124
154
|
// Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
|
|
125
155
|
// focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
|
|
126
156
|
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
157
|
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: '史诗父卡:' +
|
|
158
|
+
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,
|
|
159
|
+
// hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
|
|
160
|
+
epic ? hookPhaseBadge(t) : null,
|
|
129
161
|
t.frozen ? React.createElement('span', { title: '已冻结:不参与自动派发(详情页可「解除冻结」)', style: { color: C.brand, fontWeight: 600 } }, '❄ 冻结') : null,
|
|
130
162
|
(Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
|
|
131
163
|
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
@@ -140,8 +172,8 @@
|
|
|
140
172
|
title: '删除任务(不可恢复;执行中请先终止,已落定请用归档)',
|
|
141
173
|
style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', cursor: 'pointer', color: C.err, opacity: isCardHover(t.id, 'del') ? 1 : 0, transition: 'opacity .15s' }
|
|
142
174
|
}, ic('trash-2', 11)) : null),
|
|
143
|
-
// 史诗迷你进度条(3px 高,
|
|
144
|
-
epic ? React.createElement('div', { style: { height: 3, borderRadius: 2, background: C.nested, marginTop: 4, overflow: 'hidden' }, title: '子任务进度 ' +
|
|
175
|
+
// 史诗迷你进度条(3px 高,settled/total 比例,ok 色填充)+ 在跑子任务行(activeTitle 非空才渲染)
|
|
176
|
+
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,
|
|
145
177
|
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
178
|
// 当前动作行:展示摘要(工具名+关键参数),悬停 title 仍是 host 原文
|
|
147
179
|
(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,
|
package/lib/client/dashboard.js
CHANGED
|
@@ -26,15 +26,20 @@
|
|
|
26
26
|
if (!(execMs >= 0)) return // 时间戳异常(NaN/负值)跳过
|
|
27
27
|
var cx = t.context || {}
|
|
28
28
|
var hasRes = (Array.isArray(cx.files) && cx.files.length > 0) || (typeof cx.notes === 'string' && cx.notes.trim().length > 0)
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// token 口径与「Token 消耗区」一致化(task-muupnnq5):取**有效消耗**(输入+输出+缓存写,不含缓存读),
|
|
30
|
+
// 不再拿含缓存读的 total 冒充——本板实测缓存读占总量 ~94%,用 total 会让 ROI 行的 token 均值虚高十几倍,
|
|
31
|
+
// 与 Token 区大数字口径互相打架。分量字段缺失的老/存量卡兜底退化为 total 并打 tokFallback 标记(title 标注),
|
|
32
|
+
// 绝不把总量伪装成有效值;无任何结算记录的卡 tok=null,不参与均值(不拉低样本)。
|
|
33
|
+
var tk = roiTokenOf(t.usage)
|
|
34
|
+
;(hasRes ? roiYes : roiNo).push({ execMs: execMs, tok: tk.tok, tokFallback: tk.fallback })
|
|
31
35
|
})
|
|
32
36
|
var roi = null
|
|
33
37
|
if (roiYes.length > 0 && roiNo.length > 0 && roiYes.length + roiNo.length >= 4) {
|
|
34
38
|
var roiAgg = function (arr) {
|
|
35
39
|
var execMs = Math.round(arr.reduce(function (a, b) { return a + b.execMs }, 0) / arr.length)
|
|
36
|
-
var toks = []
|
|
37
|
-
|
|
40
|
+
var toks = [], fb = 0
|
|
41
|
+
arr.forEach(function (x) { if (x.tok !== null) { toks.push(x.tok); if (x.tokFallback) fb++ } })
|
|
42
|
+
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 }
|
|
38
43
|
}
|
|
39
44
|
roi = { yes: roiAgg(roiYes), no: roiAgg(roiNo) }
|
|
40
45
|
}
|
|
@@ -42,6 +47,19 @@
|
|
|
42
47
|
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 }
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
// 单卡 token 口径(task-muupnnq5):ROI 行与「Token 消耗区」口径同源,避免同一张卡两处两个数。
|
|
51
|
+
// 有分量(input/output/cacheWrite 任一 >0)→ 有效消耗(不含缓存读);只有 total 的老/存量卡 → 兜底 total 并标 fallback;
|
|
52
|
+
// 两者皆无 → tok=null(无结算样本,跳过而不是记 0,避免拉低均值)。
|
|
53
|
+
// 注:客户端 bundle 只注入 React(无模块系统),故此处内联,host 侧同款判定在 lib/usage.mjs taskEffectiveTokens。
|
|
54
|
+
function roiTokenOf(u) {
|
|
55
|
+
if (!u) return { tok: null, fallback: false }
|
|
56
|
+
function pos(v) { var n = Number(v); return isFinite(n) && n > 0 ? n : 0 }
|
|
57
|
+
var inp = pos(u.input), outp = pos(u.output), cw = pos(u.cacheWrite), tot = pos(u.total)
|
|
58
|
+
if (inp > 0 || outp > 0 || cw > 0) return { tok: inp + outp + cw, fallback: false }
|
|
59
|
+
if (tot > 0) return { tok: tot, fallback: true }
|
|
60
|
+
return { tok: null, fallback: false }
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
function TrendChart(props) {
|
|
46
64
|
var data = props.data || []
|
|
47
65
|
var max = 1; data.forEach(function (d) { if (d.count > max) max = d.count })
|
|
@@ -65,17 +83,23 @@
|
|
|
65
83
|
// 调研 ROI 对比行(task-mutnjesa):让数据替道理说话——「有调研 vs 无调研」分组账单直接摆给主窗口看,
|
|
66
84
|
// 比任何引导文案都管用。数据源 computeStats 的 roi(null = 任一组为空或总样本 <4,整块不渲染);
|
|
67
85
|
// 无调研组明显更慢(平均执行 > 有调研组 1.2 倍,阈值写清避免把随机波动误读成结论)时其数字用 warn 色。
|
|
86
|
+
// token 口径(task-muupnnq5):**有效消耗**(输入+输出+缓存写,不含缓存读),与「Token 消耗区」大数字同源;
|
|
87
|
+
// 组内只要有一张卡走了 total 兜底(无分量字段的老/存量卡),该数字后加 '~' 并在 title 里写明张数。
|
|
68
88
|
function ResearchRoiRow(props) {
|
|
69
89
|
var roi = props.roi
|
|
70
90
|
if (!roi) return null
|
|
71
91
|
function fmtM(ms) { var m = Math.round(ms / 60000); if (m < 60) return m + 'm'; return (m / 60).toFixed(1) + 'h' }
|
|
72
92
|
var noSlower = roi.no.execMs > roi.yes.execMs * 1.2
|
|
73
93
|
function grp(icon, label, g, warnNums) {
|
|
74
|
-
|
|
94
|
+
var approx = (g.tokFallbackN || 0) > 0
|
|
95
|
+
var tip = g.tok === null ? label + ':无带 usage 结算记录的卡,token 均值不可计'
|
|
96
|
+
: g.n + ' 卡中 ' + g.tokN + ' 张有 usage 结算记录;均值口径 = 有效消耗(输入+输出+缓存写,不含缓存读)'
|
|
97
|
+
+ (approx ? ';其中 ' + g.tokFallbackN + ' 张无分量字段(老/存量结算),退化为含缓存读的合计值参与均值(故标 ~)' : '')
|
|
98
|
+
return React.createElement('span', { title: tip },
|
|
75
99
|
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'))
|
|
100
|
+
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'))
|
|
77
101
|
}
|
|
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
|
|
102
|
+
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
103
|
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('scale', 11), '调研 ROI'),
|
|
80
104
|
grp('📎', '有调研', roi.yes, false),
|
|
81
105
|
React.createElement('span', null, '·'),
|
|
@@ -206,6 +230,24 @@
|
|
|
206
230
|
// usageSummary 由 host 从各任务 t.usage 现算(t.usage 来自 Worker/Verifier 会话 v4 日志的
|
|
207
231
|
// assistant/message.usage 聚合)。这里只做展示、不做计费断言;日志读不到/还没有 run 结算时
|
|
208
232
|
// usageSummary.total 为 0,一律显示「暂无数据」。
|
|
233
|
+
// 日账(usageSummary.byDay:{'YYYY-MM-DD': {t,e}})用于「今日」大数字与「近 7 天」条形。
|
|
234
|
+
// 双指标口径:t = 总量(含缓存读)、e = 有效消耗(输入+输出+缓存写,不含缓存读)。
|
|
235
|
+
// e === null 表示该日只有老形态 number 日账 / 存量兜底(没有逐 run 拆分,有效值不可知)——
|
|
236
|
+
// 展示时退化为 t 并在文案/title 上标 ~ 近似,绝不把总量冒充有效值。
|
|
237
|
+
// dayKey 口径与 host 记账完全一致——本地 getters 拼,绝不用 toISOString()(UTC 会让
|
|
238
|
+
// 晚上 8 点后的消耗落到次日,今日消耗直接错位)。
|
|
239
|
+
function localDayKey(d) {
|
|
240
|
+
var x = d || new Date()
|
|
241
|
+
function p2(n) { return (n < 10 ? '0' : '') + n }
|
|
242
|
+
return x.getFullYear() + '-' + p2(x.getMonth() + 1) + '-' + p2(x.getDate())
|
|
243
|
+
}
|
|
244
|
+
// 近 n 天的本地日 key 序列(旧 → 新,最后一个是今天);本地日期构造天然跨月/跨年正确
|
|
245
|
+
function lastNDays(n) {
|
|
246
|
+
var out = []
|
|
247
|
+
var now = new Date()
|
|
248
|
+
for (var i = n - 1; i >= 0; i--) out.push(localDayKey(new Date(now.getFullYear(), now.getMonth(), now.getDate() - i)))
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
209
251
|
function fmtTokens(n) {
|
|
210
252
|
var v = Number(n) || 0
|
|
211
253
|
if (v >= 1000000000) return (v / 1000000000).toFixed(1) + 'B'
|
|
@@ -232,13 +274,49 @@
|
|
|
232
274
|
var maxM = models.length ? (models[0].total || 1) : 1
|
|
233
275
|
var top = u.topTasks || []
|
|
234
276
|
var maxT = top.length ? (top[0].total || 1) : 1
|
|
277
|
+
// 日账:今日数字取本地日 key,没有日账(byDay 缺字段/老 host)时退化为 0,不炸也不误报。
|
|
278
|
+
// 读侧兼容两种单元形态:老 number(只有总量,有效值不可知)→ { t: n, e: null }。
|
|
279
|
+
var byDay = (u.byDay && typeof u.byDay === 'object') ? u.byDay : {}
|
|
280
|
+
function dayOf(v) {
|
|
281
|
+
if (v && typeof v === 'object') return { t: Number(v.t) || 0, e: (v.e === 0 || v.e) ? Number(v.e) || 0 : null }
|
|
282
|
+
return { t: Number(v) || 0, e: null }
|
|
283
|
+
}
|
|
284
|
+
var todayKey = localDayKey()
|
|
285
|
+
var todayCell = dayOf(byDay[todayKey])
|
|
286
|
+
// 有效消耗缺失(老日账/存量兜底)→ 大数字退化为总量并打 approx 标 ~,口径不伪造
|
|
287
|
+
var todayApprox = todayCell.e === null
|
|
288
|
+
var todayEff = todayApprox ? todayCell.t : todayCell.e
|
|
289
|
+
var days = lastNDays(7)
|
|
290
|
+
var maxDay = 1
|
|
291
|
+
var hasDayData = false
|
|
292
|
+
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 }
|
|
235
293
|
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))) }
|
|
294
|
+
// 累计三分量:有效(真实成本)+ 缓存读(占了 total 的大头,必须单列才看得出虚高来源)+ 合计
|
|
295
|
+
var effTotal = (typeof u.effective === 'number') ? u.effective : ((u.input || 0) + (u.output || 0) + (u.cacheWrite || 0))
|
|
236
296
|
return React.createElement('div', { style: box },
|
|
237
297
|
head,
|
|
238
298
|
React.createElement('div', { style: { display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap', marginBottom: 8 } },
|
|
239
|
-
React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color: C.brand } }, fmtTokens(
|
|
240
|
-
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens
|
|
241
|
-
|
|
299
|
+
React.createElement('span', { style: { fontSize: 20, fontWeight: 700, color: C.brand }, title: '今日有效消耗(本地日 ' + todayKey + ')= 输入+输出+缓存写,不含缓存读;一次 run 的消耗整笔记在结算日' + (todayApprox ? '。本条日账来自老形态/存量兜底数据,有效值不可知,此处以总量近似(标 ~)' : '') }, (todayApprox ? '~' : '') + fmtTokens(todayEff)),
|
|
300
|
+
React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, 'tokens(今日有效' + (todayApprox ? ' · 近似' : '') + ')'),
|
|
301
|
+
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,
|
|
302
|
+
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))),
|
|
303
|
+
mini('输入', u.input), mini('输出', u.output), u.cacheWrite ? mini('缓存写', u.cacheWrite) : null),
|
|
304
|
+
// 近 7 天迷你条形:高按区间 max 归一(今天高亮 brand,其余浅底 + 边框),
|
|
305
|
+
// 柱高一律取**有效消耗**;e 不可知的日退化为总量(title 标 ~ 近似)。
|
|
306
|
+
// 7 天全为 0 时整块不渲染(零残留,不占版面)
|
|
307
|
+
hasDayData ? React.createElement('div', { style: { marginBottom: 8 } },
|
|
308
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '近 7 天(有效消耗)'),
|
|
309
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 4 } },
|
|
310
|
+
days.map(function (k) {
|
|
311
|
+
var c = dayOf(byDay[k])
|
|
312
|
+
var approx = c.e === null
|
|
313
|
+
var v = approx ? c.t : c.e
|
|
314
|
+
var isToday = k === todayKey
|
|
315
|
+
var h = v > 0 ? Math.max(3, Math.round(v / maxDay * 32)) : 3
|
|
316
|
+
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 } },
|
|
317
|
+
React.createElement('div', { style: { width: '100%', height: h, background: isToday ? C.brand : C.nested, border: '1px solid ' + (isToday ? C.brand : C.border), borderRadius: 2 } }),
|
|
318
|
+
React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2, whiteSpace: 'nowrap' } }, k.slice(5)))
|
|
319
|
+
}))) : null,
|
|
242
320
|
React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } },
|
|
243
321
|
React.createElement('div', { style: { flex: '1 1 240px', minWidth: 200 } },
|
|
244
322
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '按模型分布'),
|
|
@@ -249,8 +327,10 @@
|
|
|
249
327
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '任务消耗 Top 8'),
|
|
250
328
|
top.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无数据') : top.map(function (x) {
|
|
251
329
|
var t = getTask(x.id)
|
|
252
|
-
return React.createElement(UsageRow, { key: x.id, label: x.title || x.id, value: x.total, max: maxT, color: C.ok, labelColor: t ? C.brand : C.text2, title: x.title + '
|
|
253
|
-
})))
|
|
330
|
+
return React.createElement(UsageRow, { key: x.id, label: x.title || x.id, value: x.total, max: maxT, color: C.ok, labelColor: t ? C.brand : C.text2, title: x.title + '(合计 ' + String(x.total) + ' tokens · 有效 ' + String((typeof x.effective === 'number') ? x.effective : (x.total - (x.cacheRead || 0))) + ' / 缓存读 ' + String(x.cacheRead || 0) + ' · ' + (x.runs || 0) + ' 次 run)' + (t ? '——点击查看详情' : ''), onClick: t ? function () { state.detailId = x.id; notify() } : undefined })
|
|
331
|
+
}))),
|
|
332
|
+
// 口径边界:本区只统计看板派发的 Worker/Verifier run,主窗口对话自身不越界纳入
|
|
333
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:仅看板派发的 Worker/Verifier run 消耗,不含主窗口对话;大数字与「近 7 天」为有效消耗(输入+输出+缓存写,不含缓存读),缓存读单列'))
|
|
254
334
|
}
|
|
255
335
|
|
|
256
336
|
// ===== 架构健康提示区(架构自省 L1 · 数据源:state.healthHints)=====
|
|
@@ -415,7 +495,25 @@
|
|
|
415
495
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '学习反馈'),
|
|
416
496
|
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
|
|
417
497
|
React.createElement('input', { type: 'checkbox', checked: !!props.feedbackEnabled, onChange: function (e) { rpc('set-board-config', { key: 'feedbackEnabled', value: e.target.checked }).then(fetchTasks).catch(function () {}) } }),
|
|
418
|
-
React.createElement('span', null, '候选教训(Verifier 驳回/仲裁结论自动生成候选,Worker prompt 提示先检索历史教训)'))
|
|
498
|
+
React.createElement('span', null, '候选教训(Verifier 驳回/仲裁结论自动生成候选,Worker prompt 提示先检索历史教训)')),
|
|
499
|
+
// 回执开关(用户要求「回执可以做一个开关,放到设置里」):派发/完成两类回执各自可关,缺省都开。
|
|
500
|
+
// 只影响回执播报,不影响派发与状态机;歧义裁决通知不在此闸门内(见下行说明文案)。
|
|
501
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '通知'),
|
|
502
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
|
|
503
|
+
React.createElement('input', { type: 'checkbox', checked: props.notifyDispatch !== false, onChange: function (e) { rpc('set-board-config', { key: 'notifyDispatch', value: e.target.checked }).then(fetchTasks).catch(function () {}) } }),
|
|
504
|
+
React.createElement('span', null, '⚡ 派发回执(任务被 Worker/Verifier 领走时播报)')),
|
|
505
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260, marginTop: 3 } },
|
|
506
|
+
React.createElement('input', { type: 'checkbox', checked: props.notifyDone !== false, onChange: function (e) { rpc('set-board-config', { key: 'notifyDone', value: e.target.checked }).then(fetchTasks).catch(function () {}) } }),
|
|
507
|
+
React.createElement('span', null, '✅ 完成回执(任务完成或阻塞时聚合播报)')),
|
|
508
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '歧义裁决通知不受这两个开关影响(任务等人裁决必须提醒)'),
|
|
509
|
+
// 史诗拆分总开关(板级 epicSplit,缺省 true):**只关引导,不禁机制**——关掉后 Team 提示词不再
|
|
510
|
+
// 注入「大任务必须拆分」第 6 条、create-task 响应不再附 suggestSplit 软提示;显式传 parentId 建
|
|
511
|
+
// 子卡、史诗自动收口/hooks 状态机照常(用户/主窗口明确要拆时不受阻)。勾选态缺字段=开,与 host 同口径。
|
|
512
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '功能'),
|
|
513
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
|
|
514
|
+
React.createElement('input', { type: 'checkbox', checked: props.epicSplit !== false, onChange: function (e) { rpc('set-board-config', { key: 'epicSplit', value: e.target.checked }).then(fetchTasks).catch(function () {}) } }),
|
|
515
|
+
React.createElement('span', null, '🧩 史诗拆分:大任务引导拆为 epic + 子任务')),
|
|
516
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 260 } }, '关掉只停引导:显式 parentId 建子卡与史诗自动收口照常工作')) : null)
|
|
419
517
|
}
|
|
420
518
|
|
|
421
519
|
function TeamView() {
|
package/lib/client/kernel.js
CHANGED
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
|
|
220
220
|
var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
|
|
221
221
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
222
|
-
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: '' }
|
|
222
|
+
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: '' }
|
|
223
223
|
|
|
224
224
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
225
225
|
try {
|
|
@@ -258,6 +258,32 @@
|
|
|
258
258
|
function clearReadErr() { state.tasksErr = '' } // 仅 fetchTasks 成功路径调用(3s 轮询兜底,任何错误条都能随之消失)
|
|
259
259
|
function readErrText(e) { return String((e && e.message) || e || '网络异常') }
|
|
260
260
|
|
|
261
|
+
// ===== isRoot 蝶变防抖(反馈 n-muuerxv9ijxs / task-muupr8ld)=====
|
|
262
|
+
// 为什么防抖:host rpc.mjs L97 的 isRoot 是每次 get-tasks 现算的
|
|
263
|
+
// (agents.roots() 是否含本会话 id)。生成开始/结束瞬间 agents 树重建,roots() 存在一个
|
|
264
|
+
// 瞬态窗口返回不含本 sid → 单次 isRoot=false 就触发 L303 强收抽屉(state.open=false),
|
|
265
|
+
// 用户表现为「看板在生成状态切换时突然消失,每次都要重新打开」;按钮/面板同病闪烁。
|
|
266
|
+
// 防抖口径:已确认过 true 的会话(everTrue)需连续 IS_ROOT_FALSE_LIMIT 次 false
|
|
267
|
+
// (host 3s 心跳 ≈ 9s)才把 isRootStable 翻成 false——覆盖瞬态窗口,真正降级仍会收敛。
|
|
268
|
+
// 为什么子代理会话不防抖:everTrue=false 的会话(新打开的子代理)立即 isRootStable=false,
|
|
269
|
+
// 保护语义不削弱——子代理会话本就不该出现看板入口/抽屉,即时收起没有代价。
|
|
270
|
+
// 消费口径:强收(本函数)/按钮(BoardButton)/面板(TopPanel)/活动心跳省流(fetchActivity)
|
|
271
|
+
// 四处一律读 isRootStable,不读单次轮询的 state.isRoot 原始值。
|
|
272
|
+
var IS_ROOT_FALSE_LIMIT = 3
|
|
273
|
+
function applyIsRoot(rawIsRoot) {
|
|
274
|
+
state.isRoot = rawIsRoot
|
|
275
|
+
if (rawIsRoot) {
|
|
276
|
+
state.isRootEverTrue = true // 曾确认 true:此后 false 需连续累计(会话生命周期内粘滞,切换会话时重置)
|
|
277
|
+
state.isRootFalseN = 0
|
|
278
|
+
state.isRootStable = true
|
|
279
|
+
} else if (!state.isRootEverTrue) {
|
|
280
|
+
state.isRootStable = false // 从未 true(新打开的子代理会话):即时收起,不防抖
|
|
281
|
+
} else {
|
|
282
|
+
state.isRootFalseN++
|
|
283
|
+
if (state.isRootFalseN >= IS_ROOT_FALSE_LIMIT) state.isRootStable = false // 连续 3 次 false:真降级(子代理树已重建不含本 sid),收敛
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
261
287
|
function fetchTasks() {
|
|
262
288
|
if (!state.sessionId) return
|
|
263
289
|
var epoch = reqEpoch
|
|
@@ -299,8 +325,13 @@
|
|
|
299
325
|
state.childStats = (d && d.childStats) || {}
|
|
300
326
|
// 学习飞轮 v1 能力检测:老 host 不返回该字段 → 视为开启(默认开);只有显式 false 才关。
|
|
301
327
|
state.feedbackEnabled = !(d && d.feedbackEnabled === false)
|
|
302
|
-
|
|
303
|
-
|
|
328
|
+
// 回执开关(设置区「通知」):同口径——老 host 不返回 → 视为开,只有显式 false 才关
|
|
329
|
+
state.notifyDispatch = !(d && d.notifyDispatch === false)
|
|
330
|
+
state.notifyDone = !(d && d.notifyDone === false)
|
|
331
|
+
// 史诗拆分总开关(设置区「功能」):同上——老 host 不返回 = 开(引导照旧),只有显式 false 才关
|
|
332
|
+
state.epicSplit = !(d && d.epicSplit === false)
|
|
333
|
+
applyIsRoot(!d || d.isRoot !== false) // 原始值只喂给防抖器,消费点一律读 isRootStable
|
|
334
|
+
if (!state.isRootStable && state.open) { state.open = false; state.detailId = null } // 子代理会话(含连续 3 次 false 的真降级):强制收起看板
|
|
304
335
|
if (d && d.dispatchInfo && d.dispatchInfoAt && Date.now() - new Date(d.dispatchInfoAt).getTime() < 120000) { state.dispatchInfo = d.dispatchInfo } else { state.dispatchInfo = '' } // 瞬时通知 2min 内有效,过期强制清空(服务端写后不清曾致残留数天)
|
|
305
336
|
// escalation 一等公民:出现新的待裁决任务 → 面板自动弹开直达该任务详情
|
|
306
337
|
// (escalation.question 在 tasksHash 序列化口径内:新歧义必然改变 hash → 必然走变化分支,不会漏弹)
|
|
@@ -318,7 +349,7 @@
|
|
|
318
349
|
|
|
319
350
|
// 活动心跳:对进行中/验收中的任务轮询子代理最近动作(卡片与详情展示"现在跑到哪了")
|
|
320
351
|
function fetchActivity() {
|
|
321
|
-
if (!state.sessionId || !state.
|
|
352
|
+
if (!state.sessionId || !state.isRootStable || !state.open) return // 面板关闭时不轮询活动(省同步 I/O);isRootStable 口径见 isRoot 蝶变防抖
|
|
322
353
|
var running = state.tasks.filter(function (t) { return t.status === 'in-progress' || t.status === 'verifying' })
|
|
323
354
|
if (!running.length) { if (Object.keys(state.activity).length) { state.activity = {}; notify() } return }
|
|
324
355
|
var pending = running.length
|
|
@@ -469,9 +500,9 @@
|
|
|
469
500
|
function BoardButton(props) {
|
|
470
501
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
471
502
|
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]
|
|
472
|
-
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])
|
|
473
|
-
useEffect(function () { function update() { var n = 0, e = 0; for (var i = 0; i < state.tasks.length; i++) { if (state.tasks[i].status === 'pending') n++; if (state.tasks[i].escalation) e++ }; setPendingCount(n); setEscCount(e); setIsOpen(state.open); setIsRoot(state.
|
|
474
|
-
if (!isRoot) return null //
|
|
503
|
+
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"保守隐藏,避免切进子代理会话时入口闪现
|
|
504
|
+
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) } }, [])
|
|
505
|
+
if (!isRoot) return null // 子代理会话不显示看板入口(读 isRootStable:瞬态 false 不收,见 isRoot 蝶变防抖)
|
|
475
506
|
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)
|
|
476
507
|
}
|
|
477
508
|
|
|
@@ -487,9 +518,11 @@
|
|
|
487
518
|
var _te = useState(state.tasksErr), tasksErr = _te[0], setTasksErr = _te[1] // 读路径错误条(无错不渲染)
|
|
488
519
|
var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
|
|
489
520
|
var _fbo = useState(state.feedbackEnabled), fbEnabled = _fbo[0], setFbEnabled = _fbo[1]
|
|
490
|
-
|
|
521
|
+
var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
|
|
522
|
+
var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
|
|
523
|
+
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) } }, [])
|
|
491
524
|
if (!open) return null
|
|
492
|
-
if (state.
|
|
525
|
+
if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
|
|
493
526
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
494
527
|
var hasFilter = state.filterQ.trim() || state.filterPrio.length > 0 || state.filterTag
|
|
495
528
|
if (hasFilter) { active = active.filter(passFilter); archived = archived.filter(passFilter) }
|
|
@@ -513,7 +546,7 @@
|
|
|
513
546
|
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 } },
|
|
514
547
|
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)),
|
|
515
548
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
|
|
516
|
-
React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT, feedbackEnabled: fbEnabled }),
|
|
549
|
+
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 }),
|
|
517
550
|
dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
|
|
518
551
|
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), '新建任务'),
|
|
519
552
|
createFlash ? React.createElement('span', { style: { fontSize: 10, color: C.ok } }, createFlash) : null,
|