dsh-agent-board 1.7.5 → 1.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/index.mjs +1 -1
- package/lib/client/board-list.js +37 -26
- package/lib/client/dashboard.js +75 -22
- package/lib/client/kernel.js +14 -4
- package/lib/client/task-detail.js +7 -1
- package/lib/client.js +133 -53
- package/lib/core.mjs +100 -0
- package/lib/dispatch.mjs +16 -4
- package/lib/rpc.mjs +35 -9
- package/lib/usage.mjs +77 -1
- package/package.json +1 -1
- package/test/core.test.mjs +297 -6
- package/test/integration.test.mjs +37 -0
- package/test/quality-alert-digest.test.mjs +128 -0
- package/test/scorecard-clusters.test.mjs +2 -2
package/README.md
CHANGED
|
@@ -114,7 +114,8 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
114
114
|
- **「模型表现」区**(记分卡卡3,`task-muxhtgi2`):Token 区下方新增**模型 × 场景七指标对比表**——每行一个 模型 × 角色(worker/verifier)× 规模段(小 <1M / 中 1~10M / 大 >10M,按任务有效 token 分桶)桶;七列口径一句话版:**有效均值** = 桶内 run 有效消耗(输入+输出+缓存写,不含缓存读)的均值、**耗时中位** = run 耗时(endedAt−at)中位数(P90 在行 title)、**驳回率** = 归因到本桶的验收驳回 / 桶内 runs、**一次通过率** = 桶内 resolved 任务中零驳回占比、**超时率** = 超时落定 run / 桶内 runs、**续跑率** = resume 续跑记录 / 桶内 runs、**缓存命中率** = cacheRead/(input+cacheRead)(⚠️ 跨 provider 对比先核 input 是否含缓存读);表头列名均带 title 口径说明;表头下两个**本地筛选器**(角色 全部/worker/verifier + 规模段 全部/小/中/大)只在已返回的 scoreboard 上过滤、**不重拉**;桶内 runs<5 整行**灰显** + title「样本 <5,仅供参考」(不参与任何「最优」强调);无数据显「暂无足够 run 数据」;Token 区 caption 新增「**→ 模型表现**」锚链接一键滚动到位。数据源 = `get-tasks` 响应里的 `usageSummary.scoreboard`(卡2 host 聚合底座 `buildScoreboard`,与 Token 区同一统计范围口径裁剪)
|
|
115
115
|
- **「质量趋势」区**(记分卡卡4,`task-muxhtgil`):模型表现区下方新增全局趋势四块(P1 质量 KPI 并入)——**一次通过率**(近 14 天柱形:当日落定 resolved 任务中零驳回占比,今日柱高亮 + 块头标今日值,无落定日画灰基线不假装 0%)、**卡时长分布**(四桶横条 <10m / 10-30m / 30-60m / >60m,创建→落定墙钟含排队/验收全程)、**超时率走势**(近 14 天迷你柱行:当日超时落定 run / 当日 runs)、**续跑成功率**(resume 续跑记录中 completed 占比,范围总量口径、host 出参无逐日故不伪造 byDay);全部读 `usageSummary.scoreboard.trends`(卡2 已按统计范围裁剪,client 只渲染不重拉),范围激活时区头带「范围内: …」徽章;近 14 天为固定窗口(范围落在窗口外时对应日柱为空,caption 写明);整区/单块空态均灰显不炸
|
|
116
116
|
- **「驳回聚类」区**(记分卡卡5,`task-muxhu1zv`):质量趋势区下方新增**驳回原因 Top3**——host 对验收驳回文本做规则法关键词聚类(不上 embedding:驳回文本结构模板化;首命中归类,占比分母=驳回总数,未匹配进「其他」兜底桶;按驳回消息自身本地日落点随统计范围裁剪),每行 TopN 类目 + 占比 + 代表原文(行 title 悬浮,各截 80 字)+ **「建议沉淀为约定」结构化文本**(形如「驳回 Top1『验收脚本未实跑』占 42%——建议在派发约定/Worker prompt 补一条:…」,一键复制按钮,沉淀动作人工);挂 `usageSummary.scoreboard.rejectionClusters`,老 host 缺字段整块不渲染,无驳回显空态。同一通道的**「按表现荐模型」hint**(卡5③):某模型在中小卡 worker 桶合并后 runs≥10 且一次通过率≥90% 且有效均值较其他每个合格模型严格低 >30% → 「架构健康」区亮黄条「模型 X 在中小卡表现最优(通过率 92% · 有效均值 0.8M,较其他合格模型低 35%),建议设为默认 workerModel」;无足够数据/无对比对象不亮
|
|
117
|
-
- 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 / 任务**执行**时长 p90 >45min(claimedAt→resolvedAt 纯干活口径,不含排队)/ 同路径驳回 ≥2 次),命中才在仪表盘渲染提示条(⚠️/ℹ️ 两级,最多 3 条)——让运行数据主动提示"该优化架构了"(如某文件反复成为锁热点=该拆),信号只建议不裁判;**运行时健康自检**(`task-muxhrkbg`)复用同一 hint 通道——host 侧在 poolCycle 成功轮 / settle 结算 / 幽灵回收三处记内存心跳(`state.poolHealth`,不落盘):有可派卡+有空位却 >5min 无成功派发轮 → 亮「🔴 派发循环疑似冻结」,有在跑卡却 >30min 无任何结算 → 黄级「结算通道长时间无活动」,幽灵表项回收 >0 时留一条一次性 info 记录(90s 保鲜、读到即灭)——运行事故不再只靠 e2e
|
|
117
|
+
- 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 / 任务**执行**时长 p90 >45min(claimedAt→resolvedAt 纯干活口径,不含排队)/ 同路径驳回 ≥2 次),命中才在仪表盘渲染提示条(⚠️/ℹ️ 两级,最多 3 条)——让运行数据主动提示"该优化架构了"(如某文件反复成为锁热点=该拆),信号只建议不裁判;**运行时健康自检**(`task-muxhrkbg`)复用同一 hint 通道——host 侧在 poolCycle 成功轮 / settle 结算 / 幽灵回收三处记内存心跳(`state.poolHealth`,不落盘):有可派卡+有空位却 >5min 无成功派发轮 → 亮「🔴 派发循环疑似冻结」,有在跑卡却 >30min 无任何结算 → 黄级「结算通道长时间无活动」,幽灵表项回收 >0 时留一条一次性 info 记录(90s 保鲜、读到即灭)——运行事故不再只靠 e2e 翻车与肉眼发现;**质量异动告警**(异常驱动审视②):`usageSummary.scoreboard.trends` 环比——近 7 天一次通过率较再前 7 天跌 >10 个百分点(两侧样本各 ≥5 防小样本误报)→ 黄条「质量异动:一次通过率本周 X%(上周 Y%),建议抽查近期验收」,超时率 / 驳回率突变各加一条同构规则(阈值常量置顶,恰好 10pp 不亮、10.1pp 亮)
|
|
118
|
+
- 报告导出新增「## 批次摘要(近 24 小时)」段(异常驱动审视③):`buildReport` 顶部一页纸——完成 N 张 / 驳回 M 次 / Token 总量(约 X,附有效 Y)/ 异常事件计数(超时 + 幽灵回收,读现有字段与 healthHints)/ 一句话质量趋势;纯聚合现有字段、不新建状态,无数据时优雅空态(一句话「近 24 小时无活动记录」)
|
|
118
119
|
- 统计区耗时同口径拆分:「平均排队 / 平均执行」双行展示(平均验收单列不变)
|
|
119
120
|
- 统计区新增「调研 ROI」对比行:resolved/archived 卡按有无调研注入分组现算卡数 / 平均执行时长 / 平均 token(双组总样本 ≥4 才渲染,无调研组明显更慢时数字 warn 色提示)
|
|
120
121
|
|
|
@@ -139,6 +140,7 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
139
140
|
- **依赖调度**:`dependsOn` 声明依赖(DFS 环检测),依赖全部完成后才会被派发,串行链路自动编排
|
|
140
141
|
- **管线分档**:`full`(执行+验证)/ `work`(只做不验)/ `direct`(不进池,主窗口直接处理),创建时按规则自动分类、可手动覆盖
|
|
141
142
|
- **硬性验收**:`acceptance` 字段写验收脚本命令,Worker 必须实际运行、Verifier 必须独立复跑
|
|
143
|
+
- **git 纪律红线**:Worker prompt 写死禁止还原命令(`git checkout` / `git restore` / `git reset --hard` / `git clean` 等——会冲掉并行 Worker 与你自己的未提交在途编辑);只改自己写的文件、别人改了别的文件与你无关,确实需要干净基线时走歧义上报(`board_report` kind=escalate)由主窗口裁决
|
|
142
144
|
- **文件级排他**:`touches` 声明本任务要改的文件/glob(如 `["src/**", "README.md"]`);持有文件锁的任务(`in-progress`/`verifying`)与候选 touches 重叠就跳过本轮(卡片显示 `🔒 等文件释放`,详情页列出在等谁),**锁随工作态**:状态流转到已完成(`resolved`)即放锁,`cancelled`/归档同样不再持锁(归档回归纯收纳动作、不再是释放点)——锁只护「正在写」的阶段,「验收后-提交前」的窗口期由主窗口「回执到即提交」纪律 + 史诗 post-hook 承接,不用长持锁把整批串行化。避免并行 Worker 改同一批文件互踩;手动「派发」遇到冲突会列出冲突任务,确认后才以 `force` 越权派发
|
|
143
145
|
- **里程碑进展通道**:Worker 每完成一个可验证的里程碑,可调用 `board_report`(`kind: "progress"`,`question` 写一行进展摘要 ≤200 字符)上报——进行中的卡片显示「📈 最近进展 · 相对时间」(覆盖式只留最新一条),详情页消息流保留全部 progress 条目
|
|
144
146
|
- **防表演式汇报**:进展契约只写在 Worker prompt 里、且要求「有实际产物/结论才报」(禁止定时汇报);progress **静默不通知主窗口**(不进回执聚合),也不写 `history` 流转记录,避免刷屏
|
|
@@ -160,7 +162,7 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
160
162
|
- **预研上下文注入(contextFiles/contextNotes,瘦身分离形态)**:主窗口调研时读过的文件与笔记随子代理的**首条 prompt 一次性注入**——调研笔记全文(notes,≤8000 字符)+ **文件清单**(每行「`路径:L起-L止` — 一句用途」);**文件内容本体不进 prompt**,由子代理用 `read` 工具按行号范围按需自取(执行时盘面更新鲜;旧形态「host 读盘取正文注入」既受单文件 8KB/总包 40KB 截断,又随 runtime 快照每轮刷新重发——自治 run 实测 6×48.8K 字符≈白烧 75–100K token);UI 侧调研门禁——full/work 且声明了 touches 却未附调研的卡片亮「⚠️ 无调研」徽章,详情页「调研注入」区列 files 清单 + notes 字数(无则明示)
|
|
161
163
|
- **锚点行段与用途**:`contextFiles` 条目写法「`path:L2350-L2420` / `path:L2350`」+ 可选「` — 一句用途`」(em dash 两侧空格分隔;缺省只给路径行号)——锚点只认尾部 `:L<行号>`(兼容 Windows 盘符),清单里原样带上行号供子代理直接按行段 read;锚点写错(`:L0` / `:L5-L2`)自动剥掉,不误导子代理去读空段
|
|
162
164
|
- **按需自取代替 host 预切段**:派发侧零读盘(不再切行段、不再附结构索引块)——子代理自己 `read(path, offset, limit)` 取需要的那段,清单里给出的行号就是起点;`task_preview_context` / `preview-context` 返回的也是这份瘦身清单(不是文件正文)
|
|
163
|
-
- **派发调研门禁(warning 族,软提示不阻断)**:`task_create`/`create-task` 响应附 `warning` 字段——①描述为空「Worker 只能凭标题猜需求」②full/work + touches 非空而未附调研上下文 ③touches 含整树 glob 建议精确到文件级(可多条合并);GUI 表单内黄色展示不关窗。epic 发布(publish)时自动轻量预检全部子任务注入情况,缺材料则 pushSysNote
|
|
165
|
+
- **派发调研门禁(warning 族,软提示不阻断)**:`task_create`/`create-task` 响应附 `warning` 字段——①描述为空「Worker 只能凭标题猜需求」②full/work + touches 非空而未附调研上下文 ③touches 含整树 glob 建议精确到文件级(可多条合并);GUI 表单内黄色展示不关窗。epic 发布(publish)时自动轻量预检全部子任务注入情况,缺材料则 pushSysNote 汇总提醒主窗口(全有不打扰);派发时清单组装失败落任务「最近失败」行,不再静默。另存 `lintWarnings[]` 起草 lint(四规则:touches 整树 glob「粒度过粗,几乎锁整仓」/ 相对路径双仓库歧义「建议加仓库前缀」/ full 无验收「建议带验收命令」/ 标题缺动词或描述空),落卡透出、详情页 ⚠️ 行展示,不阻断创建
|
|
164
166
|
- **Worker/Verifier 均可配置异构模型**(⚙️ 弹出层下拉选择,空 = 继承父级),避免同源盲点;模型故障自动熔断回退父级模型
|
|
165
167
|
- 孤儿回收:子代理 run 结束/丢失超 2 分钟 → 任务自动回待办重派
|
|
166
168
|
- 看门狗:运行超时且事件流停滞 → 标记"疑似卡死"(不自动杀,裁决权交主窗口/用户)
|
|
@@ -168,6 +170,7 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
168
170
|
- 驳回详情全量带回:三条驳回路径(`board_verdict` 工具 / Verifier 文本结算 / 手动 `task_verify`·`verify-task`)统一往 `t.messages` 落一条 `kind: "rejection"` 完整驳回包(summary + checks 逐条核对证据;手动路径补写 `t.verification`),经 `buildMessages` 全量注入重派 Worker prompt——新 Worker 据此返工,不再只看到 300 字截断的 history 记录
|
|
169
171
|
- **Verifier 自测指南(userTest)**:Verifier prompt 末尾追加「## 自测指南」段契约——四字段 `gist`(一句人话说改了什么)/ `steps[]`(用户操作步骤,每条一步)/ `expect`(预期看到什么)/ `tier`(`ui`=界面可操作 | `metric`=看指标变化 | `internal`=纯内部无用户可感知面;`internal` 时 steps 可空、expect 写「验证靠测试套件」);**诚实护栏写死在 prompt**:只给亲自验过/从 diff 可推导的步骤,不许编没验过的操作,UI 特性给具体路径(哪个区哪个按钮),host-only 改动如实标 `internal`。双模落账 `t.verification.userTest`(工具通道 `board_verdict` 的 `userTest` 参数 / 文本通道收「## 自测指南」段;tier 非法/缺省一律归 `internal` 保守档,缺段/字段全空不挂字段);详情页验证记录区渲染「📋 自测指南」块(gist + 编号步骤 + 预期 + tier 徽章:ui 绿 / metric 蓝 / internal 灰),报告导出聚合「## 本版自测清单」段(按验收通过时间倒序近 10 张已验收卡,`internal` 收末尾并标注「无用户可感知面」)
|
|
170
172
|
- **开关 `verifyUserGuide`**(⚙️ 设置区「验收」小节——「通知」旁,默认**开**):关掉后 Verifier prompt 不拼指南段(省 token)、验收落账不挂 `userTest`、详情页自测指南块与报告清单段整块不渲染;老看板文件没有该字段 → 读路径自动补 `true`(与升级前行为一致)
|
|
173
|
+
- **Verifier 验收员加餐**(固定人设 + 文件纪律 + 工具收窄):verifier spawn 挂固定 `persona`(宿主 scoped persona-prefix 影子段)——独立判断不轻信 Worker 汇报、以验收脚本实证为准、诚实分级、结论写给主窗口与用户双读者;prompt 内写死**文件纪律**(可写临时/测试文件,工程代码与文档只读,验收结束清理,事后 touches 审计兜底);`toolFilter` 保守收窄只砍联网检索(`web_fetch`/`web_search`),spawn 失败自动剥外挂参数裸请求重试(Worker spawn 路径不受影响)
|
|
171
174
|
- 手动派发:详情页「派发 / 派发验收」按钮可随时手动触发单任务派发(auto 模式补派、manual 模式主通道)
|
|
172
175
|
- 会话隔离:看板按会话分桶,多会话互不干扰
|
|
173
176
|
|
|
@@ -185,6 +188,7 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
185
188
|
- **结算双通道 + 池韧性(2026-10-06 事故修复)**:continuable 结算有事件通道(`agent/status` 的 running→idle,身份取 `agent.session.id`)与上报通道(`board_report` 落定即收尾)两条入口,任一到达即关账(结局/usage/超时臂三件套,幂等);派发周期自带**幽灵活跃表项 GC**(卡面证据核对回收残留 rec,防残留把派发容量顶到 0 拖死全池)+ 整轮 try/catch 与逐卡隔离(单点异常只作废该卡该轮)+ 去抖 latch 时间戳兜底复位
|
|
186
189
|
- **续跑指令优先级**:断点续跑指令会带上卡上 messages 原文(仲裁/干预/驳回理由),并声明**最新裁决/干预优先于历史原始契约**(冲突以最新为准)——冷复活子会话的历史里没有仲裁答案,不带原文它无从知晓
|
|
187
190
|
- **高优干预实时送达 continuable Worker**:`task_intervene` 对 continuable rec 走宿主投递通道(保留插件 source,下一个 step 边界消费),会话不可用降级 `sendMessage` 冷复活投递,再不行回退「记录注入随重派送达」并在 history 注明
|
|
191
|
+
- **运行区子会话 id 归属标注(防误杀)**:卡详情「运行」区显示当前活跃 run 的子会话 id(childId)并标注「对应子代理列表同名条目」,终止 Worker 前先到子代理列表核对该 id 的同名条目,避免误杀别的合法 Worker
|
|
188
192
|
|
|
189
193
|
### 工作模式(三档)
|
|
190
194
|
|
|
@@ -201,6 +205,7 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
201
205
|
- **歧义裁决**:Worker 遇歧义不猜测,一律上报;裁决后新 Worker 携带答案接手(Team 托管档附带 system prompt 派发引导 + 默认草稿护栏)
|
|
202
206
|
- **Verifier 验收**:`acceptance` 硬性验收脚本命令,Worker 必须实际运行、Verifier 必须独立复跑;跨档一致
|
|
203
207
|
- **touches 排他**:`touches` 文件级排他锁在活动任务间生效,冲突任务跳过本轮派发,**锁随工作态**(`in-progress`/`verifying` 持有;状态流转到 `resolved` 即释放,`cancelled`/归档同样释放——归档不再承担解锁职责);跨档一致
|
|
208
|
+
- **起草 lint**:`task_create`/`create-task` 与更新入口现算四规则软警告(touches 整树 glob「粒度过粗,几乎锁整仓」/ 相对路径双仓库歧义「建议加仓库前缀」/ full 无验收「建议带验收命令」/ 标题缺动词或描述空),存 `lintWarnings[]` 落卡透出、详情页 ⚠️ 行展示,不阻断;跨档一致
|
|
204
209
|
- 孤儿回收、看门狗、级联归档、会话隔离同样三档一致
|
|
205
210
|
|
|
206
211
|
Team 托管档独有(调度员体验):
|
package/index.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// 兼容 re-export:对外导出契约不变(单测与历史引用直接 import 自本文件)。
|
|
17
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, readMainWindowUsage, sizeBucketOf, runRoleOf, auditRunEntry, auditRunsCompleteness, attributeRejection, buildScoreboard, SCOREBOARD_MIN_SAMPLE, SIZE_BUCKET_SMALL_MAX, SIZE_BUCKET_MEDIUM_MAX, clusterRejections, REJECTION_CLUSTER_RULES, modelPerfHint, MODEL_HINT_MIN_RUNS, MODEL_HINT_MIN_PASS, MODEL_HINT_EFF_GAP } from './lib/usage.mjs'
|
|
18
|
+
export { findRunLog, readRunUsage, aggregateUsageSummary, effectiveTokens, taskEffectiveTokens, readMainWindowUsage, sizeBucketOf, runRoleOf, auditRunEntry, auditRunsCompleteness, attributeRejection, buildScoreboard, SCOREBOARD_MIN_SAMPLE, SIZE_BUCKET_SMALL_MAX, SIZE_BUCKET_MEDIUM_MAX, clusterRejections, REJECTION_CLUSTER_RULES, modelPerfHint, MODEL_HINT_MIN_RUNS, MODEL_HINT_MIN_PASS, MODEL_HINT_EFF_GAP, qualityChangeHints, QUALITY_CHANGE_PP, QUALITY_MIN_SAMPLE, QUALITY_WINDOW_DAYS } 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'
|
package/lib/client/board-list.js
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
// #14 批量操作条(#16 带一步撤销:快照操作前的 priority/status;delete 是真删,明确不支持撤销)
|
|
8
8
|
function BatchBar() {
|
|
9
9
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
10
|
-
var _a = useState(0), cnt = _a[0], setCnt = _a[1]; var _b = useState(false), on = _b[0], setOn = _b[1]; var _c = useState(''), msg = _c[0], setMsg = _c[1]; var _d = useState(null), undo = _d[0], setUndo = _d[1]
|
|
10
|
+
var _a = useState(0), cnt = _a[0], setCnt = _a[1]; var _b = useState(false), on = _b[0], setOn = _b[1]; var _c = useState(''), msg = _c[0], setMsg = _c[1]; var _d = useState(null), undo = _d[0], setUndo = _d[1]; var _e = useState([]), reasons = _e[0], setReasons = _e[1]
|
|
11
11
|
useEffect(function () { function update() { setCnt(Object.keys(state.selected).length); setOn(state.selectMode); setUndo(state.undoSnapshot) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
12
12
|
if (!on) return null
|
|
13
13
|
var ids = Object.keys(state.selected)
|
|
14
14
|
function snapshot(op) { return { op: op, at: Date.now(), items: ids.map(function (id) { var t = getTask(id); return t ? { id: id, priority: t.priority, status: t.status } : null }).filter(Boolean) } }
|
|
15
|
-
//
|
|
16
|
-
|
|
15
|
+
// 跳过原因逐条渲染:host batch-op 返回 reasons 对象(id → 原因),转成「标题:原因」列表
|
|
16
|
+
// (巡检 n-muyj5p755l3c:批量归档跳过此前只显示「跳过 N」无原因)
|
|
17
|
+
function reasonRows(r) { var m = (r && r.reasons) || {}; return Object.keys(m).map(function (id) { var t = getTask(id); return { title: t ? t.title : shortId(id), reason: String(m[id]) } }) }
|
|
17
18
|
function run(op, value) {
|
|
18
19
|
// 只有可撤销的 op 才需要快照(delete 是真删,快照里的 status/priority 复活不回整张卡)
|
|
19
20
|
var snap = op === 'delete' ? null : snapshot(op)
|
|
@@ -22,8 +23,8 @@
|
|
|
22
23
|
state.selected = {}
|
|
23
24
|
// 删除无 undo:done>0 也不留撤销快照(否则「↩️ 撤销」点了恢复不了,纯粹误导);其他 op 维持一步撤销
|
|
24
25
|
state.undoSnapshot = (r && r.done > 0 && op !== 'delete') ? snap : null
|
|
25
|
-
|
|
26
|
-
setMsg('✅ 已处理 ' + (r && r.done || 0) + ' 个' + (r && r.skipped && r.skipped.length ? ',跳过 ' + r.skipped.length +
|
|
26
|
+
setReasons(reasonRows(r))
|
|
27
|
+
setMsg('✅ 已处理 ' + (r && r.done || 0) + ' 个' + (r && r.skipped && r.skipped.length ? ',跳过 ' + r.skipped.length + ' 个' : ''))
|
|
27
28
|
fetchTasks()
|
|
28
29
|
}).catch(function (e) { setMsg('⚠️ ' + String(e)) })
|
|
29
30
|
}
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
}
|
|
36
37
|
function doUndo() { if (!undo) return; rpc('batch-undo', { snapshot: undo }).then(function (r) { setMsg('↩️ 已撤销 ' + (r && r.done || 0) + ' 个'); state.undoSnapshot = null; fetchTasks() }).catch(function (e) { setMsg('⚠️ ' + String(e)) }) }
|
|
37
38
|
var btn = { fontSize: 10, padding: '3px 10px', borderRadius: 4, border: 'none', cursor: 'pointer', fontWeight: 600 }
|
|
38
|
-
return React.createElement('div', { style: { position: 'sticky', bottom: 0, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 8px', marginTop: 8, background: C.nested, border: '1px solid ' + C.border, borderRadius: 6 } },
|
|
39
|
+
return React.createElement('div', { style: { position: 'sticky', bottom: 0, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 8px', marginTop: 8, background: C.nested, border: '1px solid ' + C.border, borderRadius: 6, flexWrap: 'wrap' } },
|
|
39
40
|
React.createElement('span', { style: { fontSize: 11, color: C.text, fontWeight: 600 } }, '已选 ' + cnt + ' 项'),
|
|
40
41
|
React.createElement('button', { onClick: function () { run('archive') }, disabled: cnt === 0, style: Object.assign({}, btn, { background: C.text2, color: C_INV, display: 'inline-flex', alignItems: 'center', gap: 3 }) }, ic('archive', 11), '批量归档'),
|
|
41
42
|
['critical', 'high', 'medium', 'low'].map(function (p) { return React.createElement('button', { key: p, onClick: function () { run('set-priority', p) }, disabled: cnt === 0, style: Object.assign({}, btn, { background: prioColor[p], color: C_INV }) }, prioLabel[p]) }),
|
|
@@ -43,21 +44,26 @@
|
|
|
43
44
|
React.createElement('button', { onClick: batchDelete, disabled: cnt === 0, title: '删除选中任务(不可恢复;执行中/验证中的需先终止,已落定请用归档)', style: Object.assign({}, btn, { background: C.err, color: C_INV, display: 'inline-flex', alignItems: 'center', gap: 3 }) }, ic('trash-2', 11), '批量删除'),
|
|
44
45
|
undo ? React.createElement('button', { onClick: doUndo, title: '撤销最近一次批量操作(删除不可撤销)', style: Object.assign({}, btn, { background: C.brand, color: C_INV }) }, '↩️ 撤销') : null,
|
|
45
46
|
msg ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, msg) : null,
|
|
46
|
-
React.createElement('button', { onClick: function () { state.selected = {}; notify() }, style: { marginLeft: 'auto', fontSize: 10, padding: '3px 8px', border: 'none', background: 'transparent', color: C.text2, cursor: 'pointer' } }, '清除选择')
|
|
47
|
+
React.createElement('button', { onClick: function () { state.selected = {}; notify() }, style: { marginLeft: 'auto', fontSize: 10, padding: '3px 8px', border: 'none', background: 'transparent', color: C.text2, cursor: 'pointer' } }, '清除选择'),
|
|
48
|
+
reasons.length > 0 ? React.createElement('div', { style: { flexBasis: '100%', marginTop: 4, padding: '4px 6px', background: C.card, border: '1px solid ' + C.border, borderRadius: 4, maxHeight: 96, overflowY: 'auto' } },
|
|
49
|
+
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2 } }, '跳过原因'),
|
|
50
|
+
reasons.map(function (it, i) { return React.createElement('div', { key: i, style: { fontSize: 10, color: C.text2, marginBottom: 1, wordBreak: 'break-word' } }, it.title + ':' + it.reason) })) : null)
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
function FilterBar() {
|
|
50
54
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
// 优先级筛选单一事实源:chip 高亮与「清除」显隐统一读全局 state.filterPrio(消灭本地 fp 副本),
|
|
56
|
+
// 曾因本地态与全局 filterPrio 不同帧更新出现「清除按钮消失但高亮残留」的矛盾画面(巡检 n-muyimk1hpdxv)
|
|
57
|
+
var _a = useState(state.filterQ), q = _a[0], setQ = _a[1]; var _c = useState(state.filterTag), ft = _c[0], setFt = _c[1]
|
|
58
|
+
useEffect(function () { function update() { setQ(state.filterQ); setFt(state.filterTag) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
53
59
|
function setQ2(v) { state.filterQ = v; notify() }
|
|
54
60
|
function togglePrio(p) { var i = state.filterPrio.indexOf(p); if (i >= 0) state.filterPrio.splice(i, 1); else state.filterPrio.push(p); notify() }
|
|
55
61
|
function clearAll() { state.filterQ = ''; state.filterPrio = []; state.filterTag = ''; notify() }
|
|
56
|
-
var hasFilter = q.trim() ||
|
|
62
|
+
var hasFilter = q.trim() || state.filterPrio.length > 0 || ft
|
|
57
63
|
var chipBase = { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: '1px solid ' + C.border, cursor: 'pointer', background: 'transparent', color: C.text2, transition: 'all .15s' }
|
|
58
64
|
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
|
|
59
65
|
React.createElement('input', { value: q, onChange: function (e) { setQ2(e.target.value) }, placeholder: '🔍 搜索标题/描述/ID…', style: { flex: '0 1 200px', fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text } }),
|
|
60
|
-
['critical', 'high', 'medium', 'low'].map(function (p) { var on =
|
|
66
|
+
['critical', 'high', 'medium', 'low'].map(function (p) { var on = state.filterPrio.indexOf(p) >= 0; return React.createElement('button', { key: p, onClick: function () { togglePrio(p) }, style: Object.assign({}, chipBase, on ? { background: prioColor[p], color: C_INV, borderColor: prioColor[p] } : {}) }, prioLabel[p]) }),
|
|
61
67
|
allTags().length > 0 ? React.createElement('select', { value: ft, onChange: function (e) { state.filterTag = e.target.value; notify() }, style: { fontSize: 10, padding: '2px 4px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text } }, React.createElement('option', { value: '' }, '🏷 全部标签'), allTags().map(function (g) { return React.createElement('option', { key: g, value: g }, g) })) : null,
|
|
62
68
|
hasFilter ? React.createElement('button', { onClick: clearAll, style: { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: 'none', background: C.nested, color: C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('x', 9), '清除') : null)
|
|
63
69
|
}
|
|
@@ -162,8 +168,8 @@
|
|
|
162
168
|
(Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
|
|
163
169
|
React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
|
|
164
170
|
(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,
|
|
165
|
-
t.status === 'in-progress' && t.claimedBy ? React.createElement('span',
|
|
166
|
-
t.assignee ? React.createElement('span',
|
|
171
|
+
t.status === 'in-progress' && t.claimedBy ? React.createElement('span', { title: '执行该任务的子代理会话:' + t.claimedBy }, '⚡ ' + String(t.claimedBy).slice(0, 8) + '…') : null,
|
|
172
|
+
t.assignee ? React.createElement('span', { title: '指派给:' + t.assignee }, '👤→' + String(t.assignee).slice(0, 8) + '…') : null,
|
|
167
173
|
durB ? React.createElement('span', { title: durB.tip }, durB.txt) : null,
|
|
168
174
|
// 删除入口(仅草稿/待办/阻塞,多选模式下隐藏以免误触):hover 才由透明转红,平时不抢视觉
|
|
169
175
|
delOk ? React.createElement('span', {
|
|
@@ -182,22 +188,27 @@
|
|
|
182
188
|
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) }
|
|
183
189
|
|
|
184
190
|
function ArchiveView() {
|
|
185
|
-
var _R = React; var useState = _R.useState
|
|
191
|
+
var _R = React; var useState = _R.useState
|
|
186
192
|
var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
|
|
187
|
-
|
|
188
|
-
|
|
193
|
+
// 归档数据源口径对齐仪表盘(反馈 n-muyg3e0m9z7i):直接读 state.tasks(get-tasks 全量含归档),
|
|
194
|
+
// 不再依赖独立 state.archived/fetchArchived——旧路径异步填库却未接入 TopPanel 的 notify 更新项,
|
|
195
|
+
// 归档 tab 打开后停在「暂无归档任务」空态,而仪表盘「已归档」计数来自 state.tasks(全量),二者对不上。
|
|
196
|
+
// 全量 + 按归档时间倒序(archivedAt 优先,回退 resolvedAt/createdAt)+ 顶部注明条数。
|
|
197
|
+
var archived = state.tasks.filter(function (t) { return t.status === 'archived' })
|
|
198
|
+
var list = archived.filter(function (t) {
|
|
189
199
|
if (!q) return true
|
|
190
200
|
var qq = q.toLowerCase()
|
|
191
201
|
return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
|
|
192
202
|
}).sort(function (a, b) {
|
|
193
|
-
//
|
|
194
|
-
function
|
|
195
|
-
return (
|
|
203
|
+
// 归档时间倒序:archivedAt 优先,回退 resolvedAt/createdAt(老卡可能缺 archivedAt)
|
|
204
|
+
function archTs(t) { return t.archivedAt || t.resolvedAt || t.createdAt || '' }
|
|
205
|
+
return (archTs(b) || '').localeCompare(archTs(a) || '')
|
|
196
206
|
})
|
|
197
|
-
function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () {
|
|
207
|
+
function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchTasks() }).catch(function () {}) }
|
|
198
208
|
return React.createElement('div', null,
|
|
199
209
|
React.createElement('input', { value: q, onChange: function (e) { setQ(e.target.value); state.archQ = e.target.value }, placeholder: '检索标题 / ID / 标签…', style: { width: '100%', padding: '5px 8px', fontSize: 11, border: '1px solid ' + C.border2, borderRadius: 5, background: C.card, color: C.text, marginBottom: 8 } }),
|
|
200
|
-
|
|
210
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '共 ' + archived.length + ' 条归档(全量,与仪表盘「已归档」同口径)'),
|
|
211
|
+
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
|
|
201
212
|
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
202
213
|
list.map(function (t) {
|
|
203
214
|
return React.createElement('div', { key: t.id, style: { padding: '6px 8px', marginBottom: 5, background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } },
|
|
@@ -268,7 +279,7 @@
|
|
|
268
279
|
}
|
|
269
280
|
var inp = { width: '100%', boxSizing: 'border-box', fontSize: 11, padding: '4px 6px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit' }
|
|
270
281
|
var lblStyle = { fontSize: 10, color: C.text2, marginBottom: 3 }
|
|
271
|
-
function field(label, node) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node) }
|
|
282
|
+
function field(label, node, hint) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node, hint ? React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.4 } }, hint) : null) }
|
|
272
283
|
var btnGhost = { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 5, background: 'transparent', color: C.text2, cursor: 'pointer' }
|
|
273
284
|
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 }
|
|
274
285
|
// 视口级 overlay(修裁切,反馈 n-mut9rzl4mxe4):fixed 全屏遮罩脱离看板抽屉(maxHeight 60vh + overflow hidden)
|
|
@@ -288,8 +299,8 @@
|
|
|
288
299
|
React.createElement('div', { style: { flex: '1 1 0', minWidth: 0 } }, field('管线', React.createElement('select', { value: pipe, onChange: function (e) { setPipe(e.target.value) }, style: inp },
|
|
289
300
|
React.createElement('option', { value: 'full' }, '全流程(执行+验证)'),
|
|
290
301
|
React.createElement('option', { value: 'work' }, '免验证(只做不验)'),
|
|
291
|
-
React.createElement('option', { value: 'direct' }, '主窗口处理'))))),
|
|
292
|
-
field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp })),
|
|
302
|
+
React.createElement('option', { value: 'direct' }, '主窗口处理')), '管线 = 任务走哪种流程:全流程 = Worker 做 + Verifier 验;免验证 = 只做不验;主窗口处理 = 你在对话里直接办'))),
|
|
303
|
+
field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp }), 'touches = 本卡要改动的文件(glob 匹配);声明后并行任务间防互踩(文件锁)'),
|
|
293
304
|
field('依赖(全部完成后才派发,可空)', cands.length === 0
|
|
294
305
|
? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '当前无草稿/待办任务可选')
|
|
295
306
|
: React.createElement('div', { style: { maxHeight: 96, overflowY: 'auto', border: '1px solid ' + C.border, borderRadius: 4, padding: '4px 6px', background: C.card } }, cands.map(function (x) {
|
|
@@ -297,7 +308,7 @@
|
|
|
297
308
|
React.createElement('input', { type: 'checkbox', checked: !!depSel[x.id], onChange: function () { toggleDep(x.id) } }),
|
|
298
309
|
React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, (statusLabels[x.status] || x.status) + ' · ' + (x.title || x.id)))
|
|
299
310
|
}))),
|
|
300
|
-
field('验收脚本(命令,可空)', React.createElement('input', { value: acc, onChange: function (e) { setAcc(e.target.value) }, placeholder: '如 node --test test/x.test.js', style: Object.assign({}, inp, { fontFamily: 'monospace' }) })),
|
|
311
|
+
field('验收脚本(命令,可空)', React.createElement('input', { value: acc, onChange: function (e) { setAcc(e.target.value) }, placeholder: '如 node --test test/x.test.js', style: Object.assign({}, inp, { fontFamily: 'monospace' }) }), '验收脚本 = 硬性验收命令:Worker 必须真实运行,Verifier 独立复跑'),
|
|
301
312
|
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: C.text, cursor: 'pointer', marginBottom: 6, flexWrap: 'wrap' } },
|
|
302
313
|
React.createElement('input', { type: 'checkbox', checked: asDraft, onChange: function (e) { setAsDraft(e.target.checked) } }),
|
|
303
314
|
'存为草稿',
|
|
@@ -309,5 +320,5 @@
|
|
|
309
320
|
err ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 6 } }, err) : null,
|
|
310
321
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
|
|
311
322
|
React.createElement('button', { onClick: close, style: btnGhost }, '取消'),
|
|
312
|
-
React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '
|
|
323
|
+
React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即交给 Worker 执行', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并交给 Worker'))))))
|
|
313
324
|
}
|
package/lib/client/dashboard.js
CHANGED
|
@@ -202,8 +202,59 @@
|
|
|
202
202
|
})) : null)
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// ===== 每日批次摘要(近 24h,异常驱动审视③):报告导出「批次一页纸」段的数据源 =====
|
|
206
|
+
// 纯聚合现有字段(不新建状态、不重拉):完成/驳回/token/超时按 run 与消息的 at 滚 24h 窗口现算,
|
|
207
|
+
// 幽灵回收读当前 healthHints 里的一次性 info(「本轮回收 N 个幽灵活跃表项」,无则 0);
|
|
208
|
+
// 一句话质量趋势按近 24h 落定的 resolved 任务一次通过率(rejectCount=0 占比)。
|
|
209
|
+
// 范围无关:报告顶部的批次摘要永远反映「最近 24h」实况,不受「统计范围」筛选影响。
|
|
210
|
+
function batchDigest() {
|
|
211
|
+
var now = Date.now(), since = now - 86400000
|
|
212
|
+
var all = (Array.isArray(state.tasks) ? state.tasks : []).concat(Array.isArray(state.archived) ? state.archived : [])
|
|
213
|
+
var resolved = 0, rejected = 0, tokTot = 0, tokEff = 0, timeouts = 0, trendResolved = 0, trendFirstPass = 0
|
|
214
|
+
function in24(iso) { var d = Date.parse(iso); return isFinite(d) && d >= since && d <= now }
|
|
215
|
+
function pos(v) { var n = Number(v); return isFinite(n) && n > 0 ? n : 0 }
|
|
216
|
+
all.forEach(function (t) {
|
|
217
|
+
if (!t) return
|
|
218
|
+
var doneAt = t.resolvedAt || t.verifiedAt || ''
|
|
219
|
+
if (doneAt && in24(doneAt)) {
|
|
220
|
+
resolved++
|
|
221
|
+
if (t.status === 'resolved') { trendResolved++; if (!(t.rejectCount > 0)) trendFirstPass++ }
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(t.messages)) t.messages.forEach(function (m) { if (m && m.kind === 'rejection' && m.at && in24(m.at)) rejected++ })
|
|
224
|
+
if (Array.isArray(t.runs)) t.runs.forEach(function (r) {
|
|
225
|
+
if (!r || !r.at || !in24(r.at)) return
|
|
226
|
+
var u = (r.usage && typeof r.usage === 'object') ? r.usage : {}
|
|
227
|
+
var inp = pos(u.input), outp = pos(u.output), cr = pos(u.cacheRead), cw = pos(u.cacheWrite)
|
|
228
|
+
var tot = pos(u.total) || (inp + outp + cr + cw)
|
|
229
|
+
tokTot += tot; tokEff += inp + outp + cw
|
|
230
|
+
if (r.outcome === 'timeout/error' || r.outcome === 'timeout') timeouts++
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
var ghosts = 0
|
|
234
|
+
var hints = Array.isArray(state.healthHints) ? state.healthHints : []
|
|
235
|
+
hints.forEach(function (h) { var m = h && h.text ? String(h.text).match(/本轮回收 (\d+) 个幽灵活跃表项/) : null; if (m) ghosts += pos(m[1]) })
|
|
236
|
+
var trend = trendResolved > 0 ? ('一次通过率 ' + Math.round(trendFirstPass / trendResolved * 100) + '%(近 24h 完成 ' + trendResolved + ' 张中 ' + trendFirstPass + ' 张零驳回)') : '近 24h 无完成记录,暂无质量趋势'
|
|
237
|
+
var has = resolved > 0 || rejected > 0 || tokTot > 0 || timeouts > 0 || ghosts > 0 || trendResolved > 0
|
|
238
|
+
return { has: has, resolved: resolved, rejected: rejected, tokTot: tokTot, tokEff: tokEff, timeouts: timeouts, ghosts: ghosts, trend: trend }
|
|
239
|
+
}
|
|
240
|
+
|
|
205
241
|
function buildReport() {
|
|
206
242
|
var lines = ['# 任务看板报告', '', '生成时间: ' + new Date().toLocaleString(), '工作模式: ' + (workModeNames[state.workMode || (state.teamMode ? 'team' : (state.boardMode === 'auto' ? 'auto' : 'list'))] || '自动派发'), '', '统计范围: ' + rangeLabel()]
|
|
243
|
+
// ===== 每日批次摘要(异常驱动审视③):报告顶部一页纸,近 24h 五字段 + 一句话质量趋势 =====
|
|
244
|
+
var dg = batchDigest()
|
|
245
|
+
if (dg.has) {
|
|
246
|
+
lines.push('## 批次摘要(近 24 小时)')
|
|
247
|
+
lines.push('- 完成:' + dg.resolved + ' 张')
|
|
248
|
+
lines.push('- 驳回:' + dg.rejected + ' 次')
|
|
249
|
+
lines.push('- Token:约 ' + fmtTokens(dg.tokTot) + (dg.tokEff > 0 ? '(其中有效 ' + fmtTokens(dg.tokEff) + ')' : ''))
|
|
250
|
+
lines.push('- 异常事件:超时 ' + dg.timeouts + ' 次 · 幽灵回收 ' + dg.ghosts + ' 次')
|
|
251
|
+
lines.push('- 质量趋势:' + dg.trend)
|
|
252
|
+
lines.push('')
|
|
253
|
+
} else {
|
|
254
|
+
lines.push('## 批次摘要(近 24 小时)')
|
|
255
|
+
lines.push('近 24 小时无活动记录(无完成 / 驳回 / 消耗 / 异常事件)')
|
|
256
|
+
lines.push('')
|
|
257
|
+
}
|
|
207
258
|
var groups = { inProgress: [], verifying: [], pending: [], blocked: [], resolved: [] }
|
|
208
259
|
var rg = activeRange()
|
|
209
260
|
state.tasks.forEach(function (t) {
|
|
@@ -414,7 +465,7 @@
|
|
|
414
465
|
// 范围说明(task-muwc7hjd):用户常把 RangeFilter 当成「整页过滤」,但今日大数字与近 7 天柱子
|
|
415
466
|
// 是自身固定口径(今日=本地今天、近 7 天=最近 7 个本地日)——不随范围变,必须在文案里讲清,
|
|
416
467
|
// 否则「选了范围数字没变」看起来像 bug。模型分布/Top8/累计才是范围生效的三处。
|
|
417
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:累计与 Top 8 仅看板派发的 Worker/Verifier run
|
|
468
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:累计与 Top 8 仅看板派发的 Worker/Verifier run(= 一次执行)消耗;主窗口行=本会话对话消耗,与看板派发口径并列不混入(单列一行 + 模型分布尾部一条,不进累计 / Top 8);主数字(今日 / 近 7 天 / 模型分布 / Top 8 / 主窗口行)均为总量口径(含缓存读);有效消耗(input+output+cacheWrite,不含缓存读)见悬浮 title'),
|
|
418
469
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。'),
|
|
419
470
|
// 「→ 模型表现」滚动锚链接(task-muxhtgi2 卡3):一键滚到下方模型×场景七指标表
|
|
420
471
|
// (与详情页 goArbitration 同一 scrollIntoView 原语;目标区空态也挂锚 id,链接始终可达)
|
|
@@ -565,8 +616,8 @@
|
|
|
565
616
|
var days14 = lastNDays(14)
|
|
566
617
|
var todayKey = localDayKey()
|
|
567
618
|
// 块头:左块名 + 右侧今日/汇总值(fontWeight 400 小字)
|
|
568
|
-
function blockHead(label, rightEl) {
|
|
569
|
-
return React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'baseline', gap: 6 } }, label,
|
|
619
|
+
function blockHead(label, rightEl, tip) {
|
|
620
|
+
return React.createElement('div', { title: tip, style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'baseline', gap: 6 } }, label,
|
|
570
621
|
rightEl ? React.createElement('span', { style: { marginLeft: 'auto', fontWeight: 400, fontSize: 9 } }, rightEl) : null)
|
|
571
622
|
}
|
|
572
623
|
// ===== ① 一次通过率趋势(近 14 天):当日率 = 落定 resolved 任务中零驳回(rejectCount=0)占比 =====
|
|
@@ -579,14 +630,14 @@
|
|
|
579
630
|
var fpHas = false; fpCells.forEach(function (c) { if (c.rate !== null) fpHas = true })
|
|
580
631
|
var fpToday = fpCells[fpCells.length - 1]
|
|
581
632
|
var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
582
|
-
blockHead('一次通过率(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: fpToday.rate !== null ? C.brand : C.text2, fontWeight: 600 } }, fpToday.rate !== null ? mpPct(fpToday.rate) : '—'))),
|
|
633
|
+
blockHead('一次通过率(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: fpToday.rate !== null ? C.brand : C.text2, fontWeight: 600 } }, fpToday.rate !== null ? mpPct(fpToday.rate) : '—')), '一次通过率 = 当天落定「已完成」任务里零驳回(rejectCount=0)的占比;驳回 = Verifier 验收判 reject'),
|
|
583
634
|
fpHas ? qtDayBars(fpCells, C.ok, todayKey)
|
|
584
635
|
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 resolved 任务落定'))
|
|
585
636
|
// ===== ② 卡时长分布(四桶横条):创建 → 落定(verifiedAt||resolvedAt)墙钟,含排队/执行/验收全程 =====
|
|
586
637
|
var durMax = 1
|
|
587
638
|
if (durB) QT_DUR_BUCKETS.forEach(function (b) { var v = Number(durB[b.k]) || 0; if (v > durMax) durMax = v })
|
|
588
639
|
var block2 = React.createElement('div', { style: { flex: '1 1 180px', minWidth: 170 } },
|
|
589
|
-
blockHead('卡时长分布', durSum > 0 ? React.createElement('span', null, React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, String(durSum)), ' 张落定卡') : null),
|
|
640
|
+
blockHead('卡时长分布', durSum > 0 ? React.createElement('span', null, React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, String(durSum)), ' 张落定卡') : null, '卡时长 = 创建 → 落定的墙钟(含排队/执行/验收全程)'),
|
|
590
641
|
durSum === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无时长样本')
|
|
591
642
|
: React.createElement('div', null, QT_DUR_BUCKETS.map(function (b) {
|
|
592
643
|
var v = Number(durB[b.k]) || 0
|
|
@@ -607,13 +658,13 @@
|
|
|
607
658
|
var toHas = false; toCells.forEach(function (c) { if (c.rate !== null) toHas = true })
|
|
608
659
|
var toToday = toCells[toCells.length - 1]
|
|
609
660
|
var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
610
|
-
blockHead('超时率走势(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: toToday.rate !== null ? C.warn : C.text2, fontWeight: 600 } }, toToday.rate !== null ? mpPct(toToday.rate) : '—'))),
|
|
661
|
+
blockHead('超时率走势(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: toToday.rate !== null ? C.warn : C.text2, fontWeight: 600 } }, toToday.rate !== null ? mpPct(toToday.rate) : '—')), '超时率 = 超时落定(timeout/error)的 run 占当天 run 的比例'),
|
|
611
662
|
toHas ? qtDayBars(toCells, C.warn, todayKey)
|
|
612
663
|
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 run 落定'))
|
|
613
664
|
// ===== ④ 续跑成功率(范围总量口径,卡2出参无 byDay):resume:true 续跑记录中 completed 占比 =====
|
|
614
665
|
var rsmRate = (rsm && typeof rsm.rate === 'number' && isFinite(rsm.rate)) ? rsm.rate : null
|
|
615
666
|
var block4 = React.createElement('div', { style: { flex: '1 1 180px', minWidth: 170 } },
|
|
616
|
-
blockHead('续跑成功率', rsmRuns > 0 ? React.createElement('span', null, React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, String((rsm && rsm.completed) || 0) + '/' + rsmRuns), ' 续跑成功') : null),
|
|
667
|
+
blockHead('续跑成功率', rsmRuns > 0 ? React.createElement('span', null, React.createElement('span', { style: { color: C.text, fontWeight: 600 } }, String((rsm && rsm.completed) || 0) + '/' + rsmRuns), ' 续跑成功') : null, '续跑成功率 = resume 续跑记录中 outcome=completed 的占比;续跑 = 前次超时/失败后继续跑'),
|
|
617
668
|
rsmRuns === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无续跑记录')
|
|
618
669
|
: React.createElement('div', { title: '续跑成功率 = resume:true 续跑记录中 outcome=completed 的占比(范围总量口径,host 出参无逐日);续跑 = 前次 run 超时/失败后续跑' },
|
|
619
670
|
React.createElement('div', { style: { fontSize: 16, fontWeight: 700, color: rsmRate !== null ? C.ok : C.text2 } }, rsmRate !== null ? mpPct(rsmRate) : '—'),
|
|
@@ -687,6 +738,8 @@
|
|
|
687
738
|
var infoColor = 'var(--dsw-alias-state-business-primary)'
|
|
688
739
|
return React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } },
|
|
689
740
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('activity', 11), '架构健康'),
|
|
741
|
+
// 术语白话(巡检 n-muyg5m7o5r00):settle/touches/p90 等黑话第一屏无解,补一行术语说明
|
|
742
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 5, lineHeight: 1.5 } }, '运行时自检告警,多为自动恢复的瞬时异常。术语:settle = 任务结算通道;touches = 任务声明的改动文件(文件锁);p90 = 执行时长第 90 百分位'),
|
|
690
743
|
hints.map(function (h, i) {
|
|
691
744
|
var warn = h.level === 'warn'
|
|
692
745
|
var col = warn ? C.warn : infoColor
|
|
@@ -727,12 +780,12 @@
|
|
|
727
780
|
function pick(m) { if (m === cur) return; rpc('set-work-mode', { mode: m }).then(fetchTasks).catch(function () {}) }
|
|
728
781
|
// 档位文案:名称 + 一句说明(说明常驻显示在按钮内,选中态高亮不可用 title 才看得到)
|
|
729
782
|
var opts = [
|
|
730
|
-
{ k: 'list', name: '清单模式', desc: '
|
|
731
|
-
{ k: 'auto', name: '自动派发', desc: '
|
|
732
|
-
{ k: 'team', name: 'Team 托管', desc: '
|
|
783
|
+
{ k: 'list', name: '清单模式', desc: '只读清单,不自动派发', icon: 'clipboard-list' },
|
|
784
|
+
{ k: 'auto', name: '自动派发', desc: '建卡即派给后台子代理', icon: 'zap' },
|
|
785
|
+
{ k: 'team', name: 'Team 托管', desc: '草稿起步,你裁决全流程', icon: 'users' }
|
|
733
786
|
]
|
|
734
787
|
var btnBase = { border: 'none', cursor: 'pointer', fontWeight: 500, textAlign: 'center', display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 1, padding: '3px 9px', transition: 'all .15s', lineHeight: 1.25 }
|
|
735
|
-
return React.createElement('div', { title: '工作模式(清单 / 自动派发 / Team 托管)——歧义裁决、Verifier
|
|
788
|
+
return React.createElement('div', { title: '工作模式(清单 / 自动派发 / Team 托管)——歧义裁决、Verifier 验收、文件锁(touches)排他在三档下全部生效', style: { display: 'inline-flex', borderRadius: 8, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, opts.map(function (o) {
|
|
736
789
|
var on = cur === o.k
|
|
737
790
|
return React.createElement('button', {
|
|
738
791
|
key: o.k, onClick: function () { pick(o.k) }, title: o.name + ':' + o.desc,
|
|
@@ -771,13 +824,13 @@
|
|
|
771
824
|
var ps = state.poolStatus
|
|
772
825
|
if (!ps) return null
|
|
773
826
|
var wActive = (ps.workers || []).filter(function (w) { return w.busy }).length
|
|
774
|
-
var wTotal =
|
|
827
|
+
var wTotal = state.maxWorkers // 分母=并发上限而非当前池实例数,空闲也显示 0/上限
|
|
775
828
|
var vActive = (ps.verifiers || []).filter(function (v) { return v.busy }).length
|
|
776
|
-
var vTotal =
|
|
829
|
+
var vTotal = state.maxVerifiers
|
|
777
830
|
return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
|
|
778
|
-
//
|
|
779
|
-
React.createElement('span', { title: 'Worker
|
|
780
|
-
React.createElement('span', { title: 'Verifier
|
|
831
|
+
// 池水位标注(反馈 n-mut9rzoq3flu / n-muyg4wl9208c):裸数字第一屏无解,补可见文字标签 + title
|
|
832
|
+
React.createElement('span', { title: '活跃 Worker 数/上限(⚡ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), 'Worker ' + wActive + '/' + wTotal),
|
|
833
|
+
React.createElement('span', { title: '活跃 Verifier 数/上限(✓ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), 'Verifier ' + vActive + '/' + vTotal))
|
|
781
834
|
}
|
|
782
835
|
|
|
783
836
|
// #11 头部减负:池配置收纳进 ⚙️ 弹出层(含 #17 verifier 异构模型设置)
|
|
@@ -787,7 +840,7 @@
|
|
|
787
840
|
var _b = useState(false), dirty = _b[0], setDirty = _b[1]
|
|
788
841
|
var _m = useState(null), models = _m[0], setModels = _m[1]
|
|
789
842
|
var cfgKey = props.cfgKey || 'verifierModel'
|
|
790
|
-
var label = props.label || '
|
|
843
|
+
var label = props.label || 'Verifier 模型'
|
|
791
844
|
// 下拉列出当前网关可用模型(host list-models RPC);枚举失败降级为文本输入
|
|
792
845
|
useEffect(function () {
|
|
793
846
|
var cancelled = false
|
|
@@ -840,13 +893,13 @@
|
|
|
840
893
|
open && pos ? React.createElement('div', { style: { position: 'fixed', top: pos.top + 'px', right: pos.right + 'px', padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 4px 16px rgba(0,0,0,0.15)', zIndex: 1000, whiteSpace: 'nowrap', maxHeight: 'calc(100vh - ' + (pos.top + 12) + 'px)', overflowY: 'auto' } },
|
|
841
894
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
|
|
842
895
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
|
|
843
|
-
React.createElement(PoolCfg, { label: '
|
|
844
|
-
React.createElement(PoolCfg, { label: '
|
|
896
|
+
React.createElement(PoolCfg, { label: 'Worker 并发', cfgKey: 'maxWorkers', value: props.maxW }),
|
|
897
|
+
React.createElement(PoolCfg, { label: 'Verifier 并发', cfgKey: 'maxVerifiers', value: props.maxV })),
|
|
845
898
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
|
|
846
|
-
React.createElement(ModelCfg, { label: '
|
|
899
|
+
React.createElement(ModelCfg, { label: 'Worker 模型', cfgKey: 'workerModel', value: props.workerModel }),
|
|
847
900
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Worker 执行模型)')),
|
|
848
901
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
849
|
-
React.createElement(ModelCfg, { label: '
|
|
902
|
+
React.createElement(ModelCfg, { label: 'Verifier 模型', cfgKey: 'verifierModel', value: props.verifierModel }),
|
|
850
903
|
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)')),
|
|
851
904
|
React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '两级超时(软超时只提醒主窗口,由你决定继续等待或终止)'),
|
|
852
905
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
@@ -897,7 +950,7 @@
|
|
|
897
950
|
React.createElement('span', { style: { marginLeft: 'auto', fontSize: 9, padding: '1px 6px', borderRadius: 3, background: C.brand, color: C_INV } }, '忙碌')),
|
|
898
951
|
m.model ? React.createElement('div', { style: { fontSize: 9, color: C.warn, marginBottom: 3 }, title: '异构模型审查' }, '🧬 ' + m.model) : null,
|
|
899
952
|
curTask ? React.createElement('div', { onClick: function () { state.detailId = curTask.id; state.view = 'board'; notify() }, style: { fontSize: 10, color: C.brand, cursor: 'pointer', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: curTask.title }, '→ ' + curTask.title) : React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '→ ' + m.taskId),
|
|
900
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '
|
|
953
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '一次性子代理会话:' + (m.runId || '') }, '会话 ' + String(m.runId || '').slice(0, 8) + '…(用完即销毁)'))
|
|
901
954
|
}
|
|
902
955
|
var ws = ps.workers || [], vs = ps.verifiers || []
|
|
903
956
|
var _R2 = React; var _st = _R2.useState(''), editMsg = _st[0], setEditMsg = _st[1]
|
package/lib/client/kernel.js
CHANGED
|
@@ -218,6 +218,16 @@
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
|
|
221
|
+
// 六列列头 title + 空列引导(巡检 n-muyg4fl8tm6r 六列无说明 / n-muygwnxpwcsq 空列零引导):
|
|
222
|
+
// 黑话→人话(title 悬浮补一句),空列给下一步动作指引(灰字)。
|
|
223
|
+
var COLUMN_META = {
|
|
224
|
+
draft: { title: '草稿:已保存未发布(补完依赖/上下文后统一发布)', empty: '暂无草稿——「+ 新建任务」勾选存为草稿后会出现在这里' },
|
|
225
|
+
pending: { title: '待办:已发布待派发(等 Worker 领取,或等依赖/文件锁释放)', empty: '暂无待办——已发布待派发的任务会出现在这里' },
|
|
226
|
+
'in-progress': { title: '进行中:Worker 子代理正在执行', empty: '暂无执行中——Worker 领取任务后会出现在这里' },
|
|
227
|
+
verifying: { title: '验证中:Worker 已完成,独立 Verifier 复跑验收中', empty: '暂无卡片——Worker 提交验收后会出现在这里' },
|
|
228
|
+
resolved: { title: '已完成:Verifier 验收通过', empty: '暂无已完成——验收通过后会出现在这里' },
|
|
229
|
+
blocked: { title: '阻塞:卡住待处理(等依赖/等主窗口裁决)', empty: '暂无阻塞——任务卡住待裁决时会出现在这里' }
|
|
230
|
+
}
|
|
221
231
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
222
232
|
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, verifyUserGuide: true, lessonPushed: {}, isRoot: true, isRootEverTrue: false, isRootFalseN: 0, isRootStable: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', selectMode: false, selected: {}, undoSnapshot: null, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', mpRole: '', mpSize: '', usageSummary: null, childStats: {}, tasksErr: '', tasksHash: '' }
|
|
223
233
|
|
|
@@ -506,7 +516,7 @@
|
|
|
506
516
|
// #18 依赖未满足判断(依赖不存在视为阻塞——创建时已校验,防御性兜底)
|
|
507
517
|
function depsBlocked(t) { if (!Array.isArray(t.dependsOn) || t.dependsOn.length === 0) return false; return t.dependsOn.some(function (id) { var d = getTask(id); return !d || (d.status !== 'resolved' && d.status !== 'archived') }) }
|
|
508
518
|
// #19 管线档位元数据
|
|
509
|
-
var pipeMeta = { full: { icon: 'flask-conical', label: '
|
|
519
|
+
var pipeMeta = { full: { icon: 'flask-conical', label: '全流程:Worker 执行 + Verifier 独立验收', short: '全流程' }, work: { icon: 'file-text', label: '免验证:Worker 只做,不验收', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直办:不派 Worker,由当前对话直接处理', short: '直办' } }
|
|
510
520
|
// ===== 耗时口径三分离(task-mutdnitw):排队 ≠ 执行 ≠ 验收,不再把排队算进耗时 =====
|
|
511
521
|
// waitOf:⏳ 排队时长 = createdAt → claimedAt(未领取则统计到 now)——"等了多久"
|
|
512
522
|
// execOf:⏱ 执行时长 = claimedAt → resolvedAt/archivedAt(未定则统计到 now)——"干了多久"
|
|
@@ -546,7 +556,7 @@
|
|
|
546
556
|
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)
|
|
547
557
|
}
|
|
548
558
|
|
|
549
|
-
function ViewTab() { var btnBase = { fontSize: 11, padding: '3px 10px', border: 'none', cursor: 'pointer', fontWeight: 500, borderRadius: 5, transition: 'all .15s' }; function goArch() { state.view = 'archive'; notify(); fetchArchived() } return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 6, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { state.view = 'board'; notify() }, style: Object.assign({}, btnBase, state.view === 'board' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('clipboard-list', 12), '看板')), React.createElement('button', { onClick: function () { state.view = 'team'; notify() }, style: Object.assign({}, btnBase, state.view === 'team' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('users', 12, '
|
|
559
|
+
function ViewTab() { var btnBase = { fontSize: 11, padding: '3px 10px', border: 'none', cursor: 'pointer', fontWeight: 500, borderRadius: 5, transition: 'all .15s' }; function goArch() { state.view = 'archive'; notify(); fetchArchived() } return React.createElement('div', { style: { display: 'inline-flex', borderRadius: 6, border: '1px solid ' + C.border, overflow: 'hidden', background: C.card } }, React.createElement('button', { onClick: function () { state.view = 'board'; notify() }, style: Object.assign({}, btnBase, state.view === 'board' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('clipboard-list', 12), '看板')), React.createElement('button', { onClick: function () { state.view = 'team'; notify() }, title: 'Worker / Verifier 执行池(一次性子代理,随任务结算销毁)', style: Object.assign({}, btnBase, state.view === 'team' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('users', 12, '执行池')), React.createElement('button', { onClick: function () { state.view = 'dashboard'; notify() }, style: Object.assign({}, btnBase, state.view === 'dashboard' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('bar-chart-3', 12, '仪表盘')), React.createElement('button', { onClick: goArch, title: '已归档任务(可检索、可恢复)', style: Object.assign({}, btnBase, state.view === 'archive' ? { background: C.brand, color: C_INV } : { background: 'transparent', color: C.text2 }) }, icText('archive', 12, '归档'))) }
|
|
550
560
|
|
|
551
561
|
function TopPanel() {
|
|
552
562
|
var _R = React; var useState = _R.useState, useEffect = _R.useEffect
|
|
@@ -574,14 +584,14 @@
|
|
|
574
584
|
else if (detailId) { content = React.createElement(DetailView) }
|
|
575
585
|
else {
|
|
576
586
|
var prioRank = { critical: 4, high: 3, medium: 2, low: 1 }
|
|
577
|
-
var cols = COLUMNS.map(function (st) { var colTasks = active.filter(function (t) { return t.status === st }); colTasks.sort(function (a, b) { var e = (b.escalation ? 1 : 0) - (a.escalation ? 1 : 0); if (e) return e; var sk = (b.stuckSince ? 1 : 0) - (a.stuckSince ? 1 : 0); if (sk) return sk; var db = (depsBlocked(a) ? 1 : 0) - (depsBlocked(b) ? 1 : 0); if (db) return db; var p = (prioRank[b.priority] || 2) - (prioRank[a.priority] || 2); if (p) return p; return (a.createdAt || '').localeCompare(b.createdAt || '') }); var isOver = dragOver === st; return React.createElement('div', { key: st, onDragOver: function (e) { onColDragOver(e, st) }, onDragLeave: function () { onColDragLeave(st) }, onDrop: function (e) { onColDrop(e, st) }, style: { flex: '1 1 0', minWidth: 150, borderRadius: 8, padding: 6, background: isOver ? C.nested : 'transparent', border: isOver ? '1px dashed ' + C.brand : '1px dashed transparent', transition: 'background .15s, border .15s' } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4, padding: '0 2px' } }, (statusLabels[st] || st) + ' (' + colTasks.length + ')'), colTasks.map(function (t) { return React.createElement(Card, { key: t.id, task: t }) })) })
|
|
587
|
+
var cols = COLUMNS.map(function (st) { var colTasks = active.filter(function (t) { return t.status === st }); colTasks.sort(function (a, b) { var e = (b.escalation ? 1 : 0) - (a.escalation ? 1 : 0); if (e) return e; var sk = (b.stuckSince ? 1 : 0) - (a.stuckSince ? 1 : 0); if (sk) return sk; var db = (depsBlocked(a) ? 1 : 0) - (depsBlocked(b) ? 1 : 0); if (db) return db; var p = (prioRank[b.priority] || 2) - (prioRank[a.priority] || 2); if (p) return p; return (a.createdAt || '').localeCompare(b.createdAt || '') }); var isOver = dragOver === st; return React.createElement('div', { key: st, onDragOver: function (e) { onColDragOver(e, st) }, onDragLeave: function () { onColDragLeave(st) }, onDrop: function (e) { onColDrop(e, st) }, style: { flex: '1 1 0', minWidth: 150, borderRadius: 8, padding: 6, background: isOver ? C.nested : 'transparent', border: isOver ? '1px dashed ' + C.brand : '1px dashed transparent', transition: 'background .15s, border .15s' } }, React.createElement('div', { title: (COLUMN_META[st] || {}).title, style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4, padding: '0 2px', cursor: 'help' } }, (statusLabels[st] || st) + ' (' + colTasks.length + ')'), colTasks.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2, opacity: 0.7, padding: '2px 6px 8px', textAlign: 'center' } }, (COLUMN_META[st] || {}).empty) : colTasks.map(function (t) { return React.createElement(Card, { key: t.id, task: t }) })) })
|
|
578
588
|
// 归档区:排序选择器(默认归档时间降序)+ 纵向滚动列表(不再横向拉条)
|
|
579
589
|
var archSorted = archived.slice().sort(function (a, b) {
|
|
580
590
|
if (state.archSort === 'title') return (a.title || '').localeCompare(b.title || '')
|
|
581
591
|
var ta = a.archivedAt || a.resolvedAt || a.createdAt || '', tb = b.archivedAt || b.resolvedAt || b.createdAt || ''
|
|
582
592
|
return state.archSort === 'time-asc' ? ta.localeCompare(tb) : tb.localeCompare(ta)
|
|
583
593
|
})
|
|
584
|
-
content = React.createElement('div', null, React.createElement(FilterBar, null), hasFilter && active.length === 0 && archived.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 16, color: C.text2, fontSize: 11 } }, '无匹配任务') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, state.selectMode ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '16px 8px 12px', color: C.text2, fontSize: 12 } }, '点击右上角', React.createElement('span', { style: { color: C.brand, fontWeight: 600 } }, '「+ 新建任务」'), '
|
|
594
|
+
content = React.createElement('div', null, React.createElement(FilterBar, null), hasFilter && active.length === 0 && archived.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 16, color: C.text2, fontSize: 11 } }, '无匹配任务') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, state.selectMode ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '16px 8px 12px', color: C.text2, fontSize: 12 } }, '点击右上角', React.createElement('span', { style: { color: C.brand, fontWeight: 600 } }, '「+ 新建任务」'), '创建第一张卡,或直接在对话里说:帮我建一张任务卡') : null, React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
|
|
585
595
|
}
|
|
586
596
|
return React.createElement('div', { style: { position: 'fixed', top: 44, left: (layL + 8) + 'px', right: (layR + 8) + 'px', maxHeight: '60vh', zIndex: 900, background: C.bg, border: '1px solid ' + C.border, borderRadius: '0 0 10px 10px', boxShadow: '0 8px 24px rgba(0,0,0,0.12)', display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
587
597
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 12px', borderBottom: '1px solid ' + C.border, flexShrink: 0, flexWrap: 'wrap', gap: 4 } },
|
|
@@ -290,11 +290,16 @@
|
|
|
290
290
|
React.createElement('div', { style: { fontSize: 10, display: 'flex', flexWrap: 'wrap', gap: 5 } }, task.waitingForTouches.map(function (id) { var t2 = getTask(id); return React.createElement('span', { key: id, onClick: function () { if (t2) { state.detailId = id; notify() } }, title: t2 ? (t2.title + ' (' + id + ')') : id, style: { padding: '1px 5px', borderRadius: 3, background: C.nested, border: '1px solid ' + C.border, color: t2 ? C.brand : C.text2, cursor: t2 ? 'pointer' : 'default' } }, (t2 ? t2.title.slice(0, 20) : shortId(id)) + ' · ' + shortId(id)) })),
|
|
291
291
|
Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 4 } }, '本任务 touches: ' + task.touches.join('、')) : null) : null,
|
|
292
292
|
Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '🔒 文件级排他(touches): ' + task.touches.join('、')) : null,
|
|
293
|
+
// 起草 lint(task.lintWarnings[],host 建卡/更新卡现算落盘,get-tasks 透出):
|
|
294
|
+
// 四规则软警告(粒度过粗/双仓库歧义/缺验收/标题缺动词或描述空)随卡展示,缺字段/空数组整块不渲染(老卡零影响)。
|
|
295
|
+
Array.isArray(task.lintWarnings) && task.lintWarnings.length ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
|
|
296
|
+
task.lintWarnings.map(function (w, i) { return React.createElement('div', { key: i, style: { fontSize: 10, color: C.text2, lineHeight: 1.6, display: 'flex', alignItems: 'flex-start', gap: 4 } }, React.createElement('span', { style: { flexShrink: 0 } }, '⚠️'), React.createElement('span', null, w)) })) : null,
|
|
293
297
|
(task.status === 'in-progress' || task.status === 'verifying') && state.activity[task.id] ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginBottom: 8, padding: '4px 8px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'center', gap: 4, animation: 'tskb-pulse 2s ease-in-out infinite' } }, ic('activity', 10), React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '当前动作: ' + state.activity[task.id])) : null,
|
|
294
298
|
task.status === 'in-progress' && task.claimedBy ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 8, padding: '5px 8px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
295
299
|
ic('activity', 10),
|
|
296
300
|
React.createElement('span', null, '已运行 ' + elapsedSince(task.claimedAt) + '(软超时 ' + state.softTimeoutMin + ' 分提醒 · 硬超时 ' + state.hardTimeoutMin + ' 分自动终止)'),
|
|
297
301
|
elapsedMin(task.claimedAt) > state.softTimeoutMin ? React.createElement('span', { style: { color: C.warn, fontWeight: 600 } }, '⏱ 已超软超时') : null,
|
|
302
|
+
React.createElement('span', { title: '当前活跃 run 的子会话 id(continuable 即 childId);终止前先到子代理列表核对该 id 的同名条目,避免误杀别的合法 Worker', style: { fontSize: 10, color: C.text2, fontFamily: 'monospace', wordBreak: 'break-all', flexBasis: '100%' } }, '子会话 id: ' + task.claimedBy + '(对应子代理列表同名条目)'),
|
|
298
303
|
React.createElement('button', { onClick: doTerminate, title: '终止该执行 Agent,任务回 pending 重新排队', style: { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.err, borderRadius: 3, background: 'transparent', color: C.err, cursor: 'pointer', marginLeft: 'auto' } }, '⏹ 立即终止')) : null,
|
|
299
304
|
task.escalation ? React.createElement('div', { id: 'tskb-escalation', style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)' } },
|
|
300
305
|
React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), 'Worker 上报歧义 — 等待主窗口裁决'),
|
|
@@ -369,7 +374,8 @@
|
|
|
369
374
|
React.createElement(MsgThread, { messages: task.messages, taskId: task.id }),
|
|
370
375
|
Array.isArray(task.history) && task.history.length > 0 ? React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 3 } }, '流转轨迹'), React.createElement('div', { style: { fontSize: 10, color: C.text2, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, task.history.map(function (h, i) { return React.createElement('div', { key: i, style: { marginBottom: 2 } }, React.createElement('span', { style: { color: C.brand } }, statusLabels[h.to] || h.to), ' · ' + ago(h.timestamp) + ' · ', React.createElement(ActorLink, { id: h.actor }), h.note ? ' · ' + h.note : '') }))) : null,
|
|
371
376
|
React.createElement('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap', marginTop: 6 } },
|
|
372
|
-
|
|
377
|
+
// 草稿态「保存并重置」实为 resetToPending 静默发布:按钮文案改「保存并发布」+ title 发布预警(巡检 n-muyi5yfec8m0)
|
|
378
|
+
React.createElement('button', { onClick: saveEdit, disabled: saving, title: task.status === 'draft' ? '发布后按当前工作模式可能被立即派发' : '保存并重置为待办', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, saving ? '保存中…' : [ic('save', 10), task.status === 'draft' ? ' 保存并发布' : ' 保存并重置']),
|
|
373
379
|
task.status === 'draft' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('update-task', { taskId: task.id, publish: true }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('rocket', 10), '发布(进入派发池)') : null,
|
|
374
380
|
(task.status === 'pending' || task.status === 'blocked') ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('claim-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: 'pointer' } }, '领取') : null,
|
|
375
381
|
task.status === 'pending' ? React.createElement('button', { onClick: function () { doAction(function () { return doManualDispatch('worker') }) }, title: '手动派发给一次性 Worker 子代理', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.warn, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('zap', 10), '派发') : null,
|