dsh-agent-board 1.7.9 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/index.mjs +1 -0
- package/lib/client/dashboard.js +18 -11
- package/lib/client.js +18 -11
- package/lib/core.mjs +60 -3
- package/lib/dispatch.mjs +251 -9
- package/lib/health.mjs +9 -0
- package/lib/rpc.mjs +206 -23
- package/lib/usage.mjs +13 -4
- package/package.json +1 -1
- package/test/core.test.mjs +478 -6
- package/test/integration.test.mjs +258 -0
- package/test/runtime-health.test.mjs +22 -0
- package/test/scoreboard-client.test.mjs +15 -9
- package/test/scoreboard.test.mjs +44 -4
- package/test/settle-stop-interrupt.test.mjs +282 -0
- package/test/verify-timing.test.mjs +127 -2
package/README.md
CHANGED
|
@@ -113,13 +113,13 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
113
113
|
|
|
114
114
|
## 功能总览
|
|
115
115
|
|
|
116
|
-
**核心能力一页速览**(`★` 为 v1.7.5–1.
|
|
116
|
+
**核心能力一页速览**(`★` 为 v1.7.5–1.8.1 新栈):
|
|
117
117
|
|
|
118
118
|
- **看板**:六列状态流、拖拽 / 多选批量 / 文本·优先级·标签筛选、归档留档、会话隔离
|
|
119
|
-
- **派发**:一次性 Worker / 独立 Verifier、依赖调度(DFS 环检测)、touches 文件级排他、可续跑 Worker、Team
|
|
119
|
+
- **派发**:一次性 Worker / 独立 Verifier、依赖调度(DFS 环检测)、touches 文件级排他、可续跑 Worker、Team 托管、**spawn 落闸锁内重验**(spawnGateOk:claim 占位与落闸决策读同一把锁的同一份快照,terminate/dependsOn 后补 落在 spawn 在途窗口即弃权——竞态与后补依赖双双根治)、**高优干预分态**(intervene 无活跃 run 时区分「续跑窗口,干预随续跑注入」与「真无 run」,返回体带 state 字段)、**建卡派发预警**(将自动派发时 lintWarnings 瞬态提示「约 1s 内被领取,想占位请 draft:true」)
|
|
120
120
|
- **验收治理** `★`:硬性验收脚本独立复跑、驳回全量带回重派、Verifier 自测指南(userTest)、风险队列「待你过目」、验收时序防护(Verifier 在场时主窗口抢批需显式 `force:true`;有活跃 run 的卡禁止归档;Verifier 迟到结论落 `lateVerdict` 留痕+迟到驳回直投告警,不再静默吞掉)
|
|
121
121
|
- **度量** `★`:Token 消耗(总量 / 有效 / 缓存读,按模型·按任务·按日)+ 记分卡(模型表现 / 质量趋势 / 驳回聚类)
|
|
122
|
-
- **可运维性** `★`:运行时健康自检(派发冻结 / 结算停滞告警)、异常驱动审视(风险队列 / 质量异动告警 / 批次摘要)、起草 lint、数据安全(原子写盘 / 腐坏隔离 / 自动 salvage)、**僵尸 run 留档清扫**(host 重启后「落定卡上的 running 无 endedAt 留档」在派发周期自动关账 `stale-closed` + 清认领位,归档门禁不再被幻影 run
|
|
122
|
+
- **可运维性** `★`:运行时健康自检(派发冻结 / 结算停滞告警)、异常驱动审视(风险队列 / 质量异动告警 / 批次摘要)、起草 lint、数据安全(原子写盘 / 腐坏隔离 / 自动 salvage)、**僵尸 run 留档清扫**(host 重启后「落定卡上的 running 无 endedAt 留档」在派发周期自动关账 `stale-closed` + 清认领位,归档门禁不再被幻影 run 永久拦截)、**子会话生命周期收口**(一次性 run 落定/迟到 verdict 落账后主动 interrupt 子会话——「结算≠停止」空转根治;continuable 收紧为「续跑位真在本 child 手上」才豁免,残影 Worker 无处藏身)、**terminate 三保险**(no-rec 也按认领位盲杀补刀 + interrupt 失败 err 级可见化 + `hold:true` 原子「终止+转手动」)、**配置审计**(set-board-config 成功写入落板级 configLog {key,from,to,actor,at} 截尾 50 条;模型下拉两段式选中≠保存)
|
|
123
123
|
- **学习反馈**:候选教训信号 → 主窗口沉淀(零耦合,不碰任何笔记 / 记忆工具 API)
|
|
124
124
|
|
|
125
125
|
以下各节是完整规格。
|
|
@@ -151,7 +151,7 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
151
151
|
- `get-tasks` 再现算 board 级 `usageSummary`(总量 / 有效合计 / 按模型(合计 `byModel` + 有效分摊 `byModelEff`,两者同源累加)/ Top8(带有效与缓存读拆分)/ 日账双份(范围内 `byDay` + 未过滤全量 `byDayFull`,后者供「今日」「近 7 天」固定口径读取),不落盘额外表;老任务无 `byDay` 时整笔近似归到 `updatedAt` 的本地日,无 `updatedAt` 则不计入任何日)——**只做展示、不做计费断言**,日志读不到或没有 usage 时一律显示「暂无数据」;第二参 `range`(可选 `{ from, to }` 本地日)给定即只聚合该范围内的 run(口径见上条)
|
|
152
152
|
- **主窗口(本会话对话)消耗单列**(`task-muwsol23`):累计行下方新增一行「**主窗口(本会话):今日 X · 累计 Y**」(**总量口径含缓存读**——`task-muxnqunk` 翻转后缓存读不再单列、已含在 total 里,有效值见悬浮 title),按模型分布尾部追加一条「**主窗口(对话)**」(琥珀色与 run 模型区分,总量为 0 不渲染,固定尾部不参与排序);数据源是该看板所属主会话自己的 v4 日志,host 侧**增量尾读**聚合(缓存键=会话 id:文件不变零读、变大只读新增字节段、变小/轮换全量重读一次,尾部半帧不结算下轮补读——3s 轮询稳态零全量重读),五分量与有效口径(e=输入+输出+缓存写)同 Worker 完全一致;**隔离红线:不进 Top 8、不进「本看板累计」、不进架构健康/学习飞轮基数**,只并列展示;随统计范围裁剪(按 `assistant/message` 事件的本地日落点);日志不存在/读取失败/无 usage 一律**静默降级**(不渲染该行,不炸轮询)
|
|
153
153
|
- 统计口径边界:本区累计 / Top 8 / 架构健康与学习飞轮基数只统计**看板派发的 Worker/Verifier run**,**不含主窗口对话**;主窗口对话消耗按上条单列一行 + 模型分布尾部一条并列展示(不越界混入,UI caption 已明示)
|
|
154
|
-
- **「模型表现」区**(记分卡卡3,`task-muxhtgi2`):Token 区下方新增**模型 ×
|
|
154
|
+
- **「模型表现」区**(记分卡卡3,`task-muxhtgi2`):Token 区下方新增**模型 × 场景八指标对比表**——每行一个 模型 × 角色(worker/verifier)× 规模段(小 <1M / 中 1~10M / 大 >10M,按任务有效 token 分桶)桶;八列口径一句话版:**有效均值** = 桶内 run 有效消耗(输入+输出+缓存写,不含缓存读)的均值、**耗时中位** = run 耗时(endedAt−at)中位数(P90 在行 title)、**驳回率** = 归因到本桶的验收驳回 / 桶内 runs、**一次通过率** = 桶内 resolved 任务中零驳回占比、**超时率** = 超时落定 run / 桶内 runs、**失败率** = 跑崩落定(incomplete/error/failed)run / 桶内 runs(terminated 人工终止不计入,`task-mv73ym6i`)、**续跑率** = 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 区同一统计范围口径裁剪)
|
|
155
155
|
- **「质量趋势」区**(记分卡卡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 写明);整区/单块空态均灰显不炸
|
|
156
156
|
- **「驳回聚类」区**(记分卡卡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」;无足够数据/无对比对象不亮
|
|
157
157
|
- 「架构健康」区(架构自省 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 亮)
|
|
@@ -187,7 +187,9 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
187
187
|
- **依赖调度**:`dependsOn` 声明依赖(DFS 环检测),依赖全部完成后才会被派发,串行链路自动编排
|
|
188
188
|
- **管线分档**:`full`(执行+验证)/ `work`(只做不验)/ `direct`(不进池,主窗口直接处理),创建时按规则自动分类、可手动覆盖
|
|
189
189
|
- **硬性验收**:`acceptance` 字段写验收脚本命令,Worker 必须实际运行、Verifier 必须独立复跑
|
|
190
|
-
- **git 纪律红线**:Worker prompt 写死禁止还原命令(`git checkout` / `git restore` / `git reset --hard` / `git clean` 等——会冲掉并行 Worker 与你自己的未提交在途编辑);只改自己写的文件、别人改了别的文件与你无关,确实需要干净基线时走歧义上报(`board_report` kind=escalate
|
|
190
|
+
- **git 纪律红线**:Worker prompt 写死禁止还原命令(`git checkout` / `git restore` / `git reset --hard` / `git clean` 等——会冲掉并行 Worker 与你自己的未提交在途编辑);只改自己写的文件、别人改了别的文件与你无关,确实需要干净基线时走歧义上报(`board_report` kind=escalate)由主窗口裁决;**提交只允许精确路径 `git add`**(本卡 `touches` 声明的文件,或逐一列出明确改过的文件路径),禁止 `git add -A` / `git add .` / `git add 目录`——并行 Worker 共享暂存区,宽泛 add 会把别卡在途编辑卷进 commit(实证 n-mv6xntg1p6i5:三起暂存吞并)
|
|
191
|
+
- **大文件交付纪律**:Worker prompt 写死分块写入指引——单文件交付预估 >8KB(或 spec 含全量文案/长代码)时禁止单次 `write` 写入全量(单次超长输出会撞模型输出上限、上下文耗尽零产出),先 `write` 落主体骨架、再用 `edit` 追加续块(每块 ≤4KB)逐块落地
|
|
192
|
+
- **前轮半成品提示**:卡片 runs[] 含已落定 worker run 时,Worker prompt 追加「先 `git status` 看工作区有无未提交半成品,能续作就续作,别盲目从零」——重发/重派场景的旧产物先盘点再决定
|
|
191
193
|
- **文件级排他**:`touches` 声明本任务要改的文件/glob(如 `["src/**", "README.md"]`);持有文件锁的任务(`in-progress`/`verifying`)与候选 touches 重叠就跳过本轮(卡片显示 `🔒 等文件释放`,详情页列出在等谁),**锁随工作态**:状态流转到已完成(`resolved`)即放锁,`cancelled`/归档同样不再持锁(归档回归纯收纳动作、不再是释放点)——锁只护「正在写」的阶段,「验收后-提交前」的窗口期由主窗口「回执到即提交」纪律 + 史诗 post-hook 承接,不用长持锁把整批串行化。避免并行 Worker 改同一批文件互踩;手动「派发」遇到冲突会列出冲突任务,确认后才以 `force` 越权派发
|
|
192
194
|
- **里程碑进展通道**:Worker 每完成一个可验证的里程碑,可调用 `board_report`(`kind: "progress"`,`question` 写一行进展摘要 ≤200 字符)上报——进行中的卡片显示「📈 最近进展 · 相对时间」(覆盖式只留最新一条),详情页消息流保留全部 progress 条目
|
|
193
195
|
- **防表演式汇报**:进展契约只写在 Worker prompt 里、且要求「有实际产物/结论才报」(禁止定时汇报);progress **静默不通知主窗口**(不进回执聚合),也不写 `history` 流转记录,避免刷屏
|
|
@@ -199,9 +201,10 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
199
201
|
- 详情父子区块:父卡详情列子任务清单(状态色点 + 标题,**含已归档留档**——归档行灰化 + 行尾「已归档」徽章并沉底,点击直达子卡详情,标题行汇总 settled/total);子卡详情顶行「↳ 史诗:父标题」点击回跳父卡
|
|
200
202
|
- **可选 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 不入任何默认形态
|
|
201
203
|
- **删除通道(真删,无 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` 留一行日志便于溯源
|
|
202
|
-
- **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team
|
|
204
|
+
- **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team 模式提示词都写了这条契约;**容器卡结构性不派发**:凡有子任务的卡(epic)永不进派发候选(与 `pipeline` 字段无关,不再只靠 `direct` 口头约定——手滑建 `full` 档 epic 也不会被派出占 worker 槽),子任务全了结转 verifying 后派 Verifier 验收不受影响
|
|
203
205
|
- **suggestSplit 软提示**:`task_create` / `create-task` 发现描述超 500 字符、或标题/描述命中「全量 / 整体 / 系统级 / 全面 / 重构 / 所有模块 / 整个」等史诗特征词时,返回体附带一行 `suggestSplit` 建议文案(**只提示,不阻断创建与派发**;未命中则不出现该字段,老调用方无感)
|
|
204
206
|
- **开关 `epicSplit`(史诗拆分总开关)**(⚙️ 入池配置弹层「**功能**」小节,默认**开**):关掉只停**引导**——Team 提示词第 6 条拆分条款整条不注入、`create-task`/`task_create` 响应不再附 `suggestSplit` 软提示;**机制不禁**(显式传 `parentId` 建子卡、史诗自动收口与 hooks 状态机照常工作,明确要拆时不受阻);老看板文件没有该字段 → 读路径自动补 `true`(与升级前行为一致)。工具描述里的粒度契约是静态工具定义(随 request header 快照),不在开关范围
|
|
207
|
+
- **模型字段写保护**:`set-board-config` 对 `workerModel`/`verifierModel` 的非空值校验当前网关可用清单(`list-models` 同款枚举)——手写非法 id(如把熔断通知里的显示名抄成 id)直接拒写并返回合法清单前 8 个,验收 spawn 不再 UNKNOWN_MODEL 秒死;空串=清除放行;宿主无 llm 服务时降级跳过校验不炸。配套熔断通知分态:`UNKNOWN_MODEL` 类失败报「配置非法(不在可用清单)」,与「验收连续失败」区分——mistake 归 mistake,quality 归 quality
|
|
205
208
|
|
|
206
209
|
### 一次性派发(v74 去池化)
|
|
207
210
|
|
|
@@ -325,7 +328,7 @@ dsh web 的 client 运行时不具备模块解析能力(entry 被整体读成
|
|
|
325
328
|
| `kernel.js` | 底座(用户不可感知) | 渲染原语(ic/icText/ActorLink)、RPC 封装与轮询族、共享状态(state/listeners/notify)、图标(ICONS)、共享任务工具,以及面板骨架入口(BoardButton/ViewTab/TopPanel/slots.inject) |
|
|
326
329
|
| `board-list.js` | 看板列表 | 看板列与卡片(Card)、筛选条、多选批量操作条、建卡表单、归档列表 |
|
|
327
330
|
| `task-detail.js` | 任务详情 | 详情抽屉(编辑/流转/冻结/touches/历史会话/高优介入)、歧义裁决对话与候选教训沉淀、依赖区块 |
|
|
328
|
-
| `dashboard.js` | 仪表盘与设置 | 统计图表、Token
|
|
331
|
+
| `dashboard.js` | 仪表盘与设置 | 统计图表、Token 消耗、模型表现(八指标×场景筛选)、质量趋势(通过率/时长分布/超时/续跑)、全局总览、报告生成、团队池视图、池/模型/超时设置与工作模式开关 |
|
|
329
332
|
|
|
330
333
|
四个模块拼进同一个 `apply(ctx)` 函数作用域(`var`/`function` 声明提升使跨模块引用与拼接顺序无关;
|
|
331
334
|
所有同步执行代码——DOM 监听、轮询注册、布局同步、slots.inject——都在 kernel 域内保持原相对顺序)。
|
package/index.mjs
CHANGED
|
@@ -85,6 +85,7 @@ export function apply(ctx) {
|
|
|
85
85
|
pushSysNote: notify.pushSysNote, sessionCwd: session.sessionCwd, // 调研门禁③:epic 发布预检汇总投递 + 预研路径相对解析根
|
|
86
86
|
spawnOneShot: dispatch.spawnOneShot, accumulateRunUsage: dispatch.accumulateRunUsage, readContextPack: dispatch.readContextPack,
|
|
87
87
|
closeRunHistory: dispatch.closeRunHistory, // 手动终止 run 关账(doTerminate 状态无关清理,task-mv1pqy27)
|
|
88
|
+
resumeTarget: dispatch.resumeTarget, // doIntervene 无活跃 rec 时细分续跑窗口/真无 run(task-mv6c7cbo)
|
|
88
89
|
// 上报通道结算入口(task-muwkhqf8):board_report/board_verdict 把任务推进到落定态时,
|
|
89
90
|
// 由 rpc 侧顺手对「该任务当前的 continuable Worker rec」补做收尾三件套
|
|
90
91
|
// (关 run 结局 / usage 落账 / 摘超时臂)——治「工具上报完成后 idle 事件没到 → run 永远 running」。
|
package/lib/client/dashboard.js
CHANGED
|
@@ -471,21 +471,21 @@
|
|
|
471
471
|
// 否则「选了范围数字没变」看起来像 bug。模型分布/Top8/累计才是范围生效的三处。
|
|
472
472
|
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'),
|
|
473
473
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。'),
|
|
474
|
-
// 「→ 模型表现」滚动锚链接(task-muxhtgi2 卡3
|
|
474
|
+
// 「→ 模型表现」滚动锚链接(task-muxhtgi2 卡3):一键滚到下方模型×场景八指标表
|
|
475
475
|
// (与详情页 goArbitration 同一 scrollIntoView 原语;目标区空态也挂锚 id,链接始终可达)
|
|
476
476
|
React.createElement('div', { style: { fontSize: 9, marginTop: 4 } },
|
|
477
|
-
React.createElement('span', { onClick: function () { var el = document.getElementById('tskb-model-perf'); if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' }) }, style: { color: C.brand, cursor: 'pointer' }, title: '滚动到下方「模型表现」区:模型 × 场景(角色 ×
|
|
477
|
+
React.createElement('span', { onClick: function () { var el = document.getElementById('tskb-model-perf'); if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' }) }, style: { color: C.brand, cursor: 'pointer' }, title: '滚动到下方「模型表现」区:模型 × 场景(角色 × 规模段)八指标对比表' }, '→ 模型表现')))
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
// ===== 模型表现区(记分卡卡3,task-muxhtgi2
|
|
480
|
+
// ===== 模型表现区(记分卡卡3,task-muxhtgi2):八指标表格 × 场景筛选器(本地过滤不重拉)=====
|
|
481
481
|
// 数据源:state.usageSummary.scoreboard——卡2 host 聚合底座(buildScoreboard)挂进 get-tasks 响应,
|
|
482
482
|
// 与 Token 区同通道透传、同一 range 口径(范围切换重拉后随 notify 同步刷新,本区零自持请求)。
|
|
483
483
|
// 每桶 = 模型 × 角色(worker/verifier)× 规模段(small/medium/large,按任务有效 token 分桶)。
|
|
484
484
|
// 本区三件事全部本地现算:
|
|
485
485
|
// ① 场景筛选器:角色(全部/worker/verifier)+ 规模段(全部/小/中/大)——scoreboard 已在响应里,
|
|
486
486
|
// 筛选只是 buckets 数组 pass,绝不重拉(组件内无 rpc/fetchTasks);
|
|
487
|
-
// ②
|
|
488
|
-
//
|
|
487
|
+
// ② 八指标表格:每 模型×场景桶 一行,八列——有效均值/耗时中位/驳回率/一次通过率/超时率/失败率/
|
|
488
|
+
// 续跑率/缓存命中率;表头列名带 title 口径说明(与卡2 host 注释同源);
|
|
489
489
|
// ③ 样本护栏:insufficient(runs<5)行整行灰显 + title 前缀「样本 <5,仅供参考」——
|
|
490
490
|
// 仅供参考,不参与任何「最优」视觉强调(本区 v1 不做任何高亮排名,从源头规避)。
|
|
491
491
|
// 空态:scoreboard 缺字段 / buckets 空 → 「暂无足够 run 数据」(锚 id 照挂,Token 区链接始终可达);
|
|
@@ -495,13 +495,16 @@
|
|
|
495
495
|
// 率类/耗时格式化:null = 无样本(不假装 0,与 host 同哲学)
|
|
496
496
|
function mpPct(r) { return (r === null || r === undefined || !isFinite(r)) ? '—' : Math.round(r * 100) + '%' }
|
|
497
497
|
function mpDur(ms) { if (ms === null || ms === undefined || !isFinite(ms)) return '—'; var m = Math.round(ms / 60000); if (m < 1) return '<1m'; if (m < 60) return m + 'm'; return (m / 60).toFixed(1) + 'h' }
|
|
498
|
-
//
|
|
498
|
+
// 八列定义:label + title 口径说明 + 取值格式化(口径一句话版与 README 仪表盘段同步维护)
|
|
499
499
|
var MP_COLS = [
|
|
500
500
|
{ k: 'effAvg', label: '有效均值', title: '有效 token 均值 = 桶内 run 有效消耗(输入+输出+缓存写,不含缓存读)的均值;无 usage 留账的 run 不进均值(不伪造 0)', fmt: function (b) { return b.effAvg === null ? '—' : fmtTokens(b.effAvg) } },
|
|
501
501
|
{ k: 'durMedianMs', label: '耗时中位', title: 'run 耗时(endedAt - at)中位数;P90 见行 title;无合法时刻的 run 不进分布', fmt: function (b) { return mpDur(b.durMedianMs) } },
|
|
502
502
|
{ k: 'rejectRate', label: '驳回率', title: '驳回率 = 归因到本桶 run 的验收驳回次数 / 桶内 runs(归因 = 驳回前最近 worker run)', fmt: function (b) { return mpPct(b.rejectRate) } },
|
|
503
503
|
{ k: 'firstPassRate', label: '一次通过率', title: '一次通过率 = 桶内 resolved 任务中 rejectCount=0(零驳回)的占比;桶内无 resolved 任务 → —', fmt: function (b) { return mpPct(b.firstPassRate) } },
|
|
504
504
|
{ k: 'timeoutRate', label: '超时率', title: '超时率 = outcome 超时落定(timeout/error)的 run / 桶内 runs', fmt: function (b) { return mpPct(b.timeoutRate) } },
|
|
505
|
+
// 失败率列(task-mv73ym6i):跑崩的 run 不再隐形——incomplete(猝死/未产出结算)/error/failed
|
|
506
|
+
// 计入;terminated 人工终止不计入(人工动作非模型表现);超时落定走「超时率」列不重复计。
|
|
507
|
+
{ k: 'failRate', label: '失败率', title: '失败率 = 跑崩落定(outcome 为 incomplete/error/failed)的 run / 桶内 runs;terminated 人工终止不计入(人工动作非模型表现);超时落定走「超时率」列,不重复计', fmt: function (b) { return mpPct(b.failRate) } },
|
|
505
508
|
{ k: 'resumeRate', label: '续跑率', title: '续跑率 = resume:true 续跑记录 / 桶内 runs', fmt: function (b) { return mpPct(b.resumeRate) } },
|
|
506
509
|
{ k: 'cacheHitRate', label: '缓存命中率', title: '缓存命中率 = cacheRead / (input + cacheRead);⚠️ 跨 provider 口径可能不一(input 是否含缓存读各家不同),对比前先核口径;无 usage 样本 → —', fmt: function (b) { return mpPct(b.cacheHitRate) } }
|
|
507
510
|
]
|
|
@@ -551,14 +554,14 @@
|
|
|
551
554
|
MP_COLS.map(function (c) { return React.createElement('th', { key: c.k, style: thR, title: c.title }, c.label) }))),
|
|
552
555
|
React.createElement('tbody', null, shown.map(function (b) {
|
|
553
556
|
// 样本护栏:insufficient 行整行灰显 + title 前缀「样本 <5,仅供参考」,不进任何最优强调
|
|
554
|
-
var tip = (b.insufficient ? '样本 <5,仅供参考——' : '') + b.model + ' × ' + b.role + ' × ' + (MP_SIZE_LABEL[b.size] || b.size) + ':runs=' + b.runs + ';有效总额 ' + fmtTokens(b.effSum) + ' tok;耗时 P90 ' + mpDur(b.durP90Ms) + '
|
|
557
|
+
var tip = (b.insufficient ? '样本 <5,仅供参考——' : '') + b.model + ' × ' + b.role + ' × ' + (MP_SIZE_LABEL[b.size] || b.size) + ':runs=' + b.runs + ';有效总额 ' + fmtTokens(b.effSum) + ' tok;耗时 P90 ' + mpDur(b.durP90Ms) + '(八指标口径见表头 title)'
|
|
555
558
|
return React.createElement('tr', { key: b.model + '|' + b.role + '|' + b.size, title: tip, style: { opacity: b.insufficient ? 0.45 : 1 } },
|
|
556
559
|
React.createElement('td', { style: tdL }, b.model),
|
|
557
560
|
React.createElement('td', { style: tdL }, b.role + '·' + (MP_SIZE_LABEL[b.size] || b.size)),
|
|
558
561
|
React.createElement('td', { style: tdR }, String(b.runs)),
|
|
559
562
|
MP_COLS.map(function (c) { return React.createElement('td', { key: c.k, style: tdR }, c.fmt(b)) }))
|
|
560
563
|
})))),
|
|
561
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:每行 = 模型 × 角色 × 规模段桶(规模按任务有效 token:小 <1M / 中 1~10M / 大 >10M);率类分母为桶内 runs,一次通过率分母为桶内 resolved
|
|
564
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:每行 = 模型 × 角色 × 规模段桶(规模按任务有效 token:小 <1M / 中 1~10M / 大 >10M);率类分母为桶内 runs,一次通过率分母为桶内 resolved 任务;失败率 = 跑崩落定(outcome 为 incomplete/error/failed)的 run / 桶内 runs,terminated 人工终止不计入;runs<5 行灰显 = 样本不足仅供参考;随统计范围裁剪(与 Token 区同一 range)'))
|
|
562
565
|
}
|
|
563
566
|
|
|
564
567
|
// ===== 质量趋势区(记分卡卡4,task-muxhtgil):一次通过率趋势 / 卡时长分布 / 超时率走势 / 续跑成功率 =====
|
|
@@ -947,12 +950,16 @@
|
|
|
947
950
|
}, [])
|
|
948
951
|
useEffect(function () { setVal(props.value || ''); setDirty(false) }, [props.value])
|
|
949
952
|
function save() { setDirty(false); rpc('set-board-config', { key: cfgKey, value: (val || '').trim() }).then(fetchTasks).catch(function () {}) }
|
|
950
|
-
|
|
953
|
+
// 下拉两段式(task-mv9yuq2l,实证 n-mv8ki92qnhdq:巡检戳即点即写 → verifierModel 神隐回退无从审计):
|
|
954
|
+
// 选中只改本地草稿态 + 亮 dirty(对比服务端权威值 props.value),点旁边 ✓ 保存键才走 save() 提交 rpc——
|
|
955
|
+
// 与下方降级文本输入同款两段式语义,巡检路过戳一下不再直接写配置。
|
|
956
|
+
function pick(v) { setVal(v); setDirty(v !== (props.value || '')) }
|
|
951
957
|
if (models) {
|
|
952
958
|
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
|
|
953
|
-
React.createElement('select', { value: val, onChange: function (e) { pick(e.target.value) }, title: label + '
|
|
959
|
+
React.createElement('select', { value: val, onChange: function (e) { pick(e.target.value) }, title: label + '(空=继承父级;选中后点 ✓ 才保存)', style: { width: 150, padding: '0px 4px', fontSize: 9, border: '1px solid ' + (dirty ? C.brand : C.border), borderRadius: 2, background: C.card, color: C.text } },
|
|
954
960
|
React.createElement('option', { value: '' }, '继承父级'),
|
|
955
|
-
models.map(function (m) { return React.createElement('option', { key: m.id, value: m.id }, m.name + ' (' + m.provider + ')') }))
|
|
961
|
+
models.map(function (m) { return React.createElement('option', { key: m.id, value: m.id }, m.name + ' (' + m.provider + ')') })),
|
|
962
|
+
dirty ? React.createElement('button', { onClick: save, title: '保存 ' + label, style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null)
|
|
956
963
|
}
|
|
957
964
|
// 降级:文本输入(模型枚举不可用时)
|
|
958
965
|
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
|
package/lib/client.js
CHANGED
|
@@ -2008,21 +2008,21 @@ function apply(ctx) {
|
|
|
2008
2008
|
// 否则「选了范围数字没变」看起来像 bug。模型分布/Top8/累计才是范围生效的三处。
|
|
2009
2009
|
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'),
|
|
2010
2010
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2, lineHeight: 1.5 } }, '统计范围作用于按模型分布 / 任务消耗 Top 8 / 累计三分量(按 run 的本地日落点过滤);今日与「近 7 天」为固定口径,不随范围变化。'),
|
|
2011
|
-
// 「→ 模型表现」滚动锚链接(task-muxhtgi2 卡3
|
|
2011
|
+
// 「→ 模型表现」滚动锚链接(task-muxhtgi2 卡3):一键滚到下方模型×场景八指标表
|
|
2012
2012
|
// (与详情页 goArbitration 同一 scrollIntoView 原语;目标区空态也挂锚 id,链接始终可达)
|
|
2013
2013
|
React.createElement('div', { style: { fontSize: 9, marginTop: 4 } },
|
|
2014
|
-
React.createElement('span', { onClick: function () { var el = document.getElementById('tskb-model-perf'); if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' }) }, style: { color: C.brand, cursor: 'pointer' }, title: '滚动到下方「模型表现」区:模型 × 场景(角色 ×
|
|
2014
|
+
React.createElement('span', { onClick: function () { var el = document.getElementById('tskb-model-perf'); if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' }) }, style: { color: C.brand, cursor: 'pointer' }, title: '滚动到下方「模型表现」区:模型 × 场景(角色 × 规模段)八指标对比表' }, '→ 模型表现')))
|
|
2015
2015
|
}
|
|
2016
2016
|
|
|
2017
|
-
// ===== 模型表现区(记分卡卡3,task-muxhtgi2
|
|
2017
|
+
// ===== 模型表现区(记分卡卡3,task-muxhtgi2):八指标表格 × 场景筛选器(本地过滤不重拉)=====
|
|
2018
2018
|
// 数据源:state.usageSummary.scoreboard——卡2 host 聚合底座(buildScoreboard)挂进 get-tasks 响应,
|
|
2019
2019
|
// 与 Token 区同通道透传、同一 range 口径(范围切换重拉后随 notify 同步刷新,本区零自持请求)。
|
|
2020
2020
|
// 每桶 = 模型 × 角色(worker/verifier)× 规模段(small/medium/large,按任务有效 token 分桶)。
|
|
2021
2021
|
// 本区三件事全部本地现算:
|
|
2022
2022
|
// ① 场景筛选器:角色(全部/worker/verifier)+ 规模段(全部/小/中/大)——scoreboard 已在响应里,
|
|
2023
2023
|
// 筛选只是 buckets 数组 pass,绝不重拉(组件内无 rpc/fetchTasks);
|
|
2024
|
-
// ②
|
|
2025
|
-
//
|
|
2024
|
+
// ② 八指标表格:每 模型×场景桶 一行,八列——有效均值/耗时中位/驳回率/一次通过率/超时率/失败率/
|
|
2025
|
+
// 续跑率/缓存命中率;表头列名带 title 口径说明(与卡2 host 注释同源);
|
|
2026
2026
|
// ③ 样本护栏:insufficient(runs<5)行整行灰显 + title 前缀「样本 <5,仅供参考」——
|
|
2027
2027
|
// 仅供参考,不参与任何「最优」视觉强调(本区 v1 不做任何高亮排名,从源头规避)。
|
|
2028
2028
|
// 空态:scoreboard 缺字段 / buckets 空 → 「暂无足够 run 数据」(锚 id 照挂,Token 区链接始终可达);
|
|
@@ -2032,13 +2032,16 @@ function apply(ctx) {
|
|
|
2032
2032
|
// 率类/耗时格式化:null = 无样本(不假装 0,与 host 同哲学)
|
|
2033
2033
|
function mpPct(r) { return (r === null || r === undefined || !isFinite(r)) ? '—' : Math.round(r * 100) + '%' }
|
|
2034
2034
|
function mpDur(ms) { if (ms === null || ms === undefined || !isFinite(ms)) return '—'; var m = Math.round(ms / 60000); if (m < 1) return '<1m'; if (m < 60) return m + 'm'; return (m / 60).toFixed(1) + 'h' }
|
|
2035
|
-
//
|
|
2035
|
+
// 八列定义:label + title 口径说明 + 取值格式化(口径一句话版与 README 仪表盘段同步维护)
|
|
2036
2036
|
var MP_COLS = [
|
|
2037
2037
|
{ k: 'effAvg', label: '有效均值', title: '有效 token 均值 = 桶内 run 有效消耗(输入+输出+缓存写,不含缓存读)的均值;无 usage 留账的 run 不进均值(不伪造 0)', fmt: function (b) { return b.effAvg === null ? '—' : fmtTokens(b.effAvg) } },
|
|
2038
2038
|
{ k: 'durMedianMs', label: '耗时中位', title: 'run 耗时(endedAt - at)中位数;P90 见行 title;无合法时刻的 run 不进分布', fmt: function (b) { return mpDur(b.durMedianMs) } },
|
|
2039
2039
|
{ k: 'rejectRate', label: '驳回率', title: '驳回率 = 归因到本桶 run 的验收驳回次数 / 桶内 runs(归因 = 驳回前最近 worker run)', fmt: function (b) { return mpPct(b.rejectRate) } },
|
|
2040
2040
|
{ k: 'firstPassRate', label: '一次通过率', title: '一次通过率 = 桶内 resolved 任务中 rejectCount=0(零驳回)的占比;桶内无 resolved 任务 → —', fmt: function (b) { return mpPct(b.firstPassRate) } },
|
|
2041
2041
|
{ k: 'timeoutRate', label: '超时率', title: '超时率 = outcome 超时落定(timeout/error)的 run / 桶内 runs', fmt: function (b) { return mpPct(b.timeoutRate) } },
|
|
2042
|
+
// 失败率列(task-mv73ym6i):跑崩的 run 不再隐形——incomplete(猝死/未产出结算)/error/failed
|
|
2043
|
+
// 计入;terminated 人工终止不计入(人工动作非模型表现);超时落定走「超时率」列不重复计。
|
|
2044
|
+
{ k: 'failRate', label: '失败率', title: '失败率 = 跑崩落定(outcome 为 incomplete/error/failed)的 run / 桶内 runs;terminated 人工终止不计入(人工动作非模型表现);超时落定走「超时率」列,不重复计', fmt: function (b) { return mpPct(b.failRate) } },
|
|
2042
2045
|
{ k: 'resumeRate', label: '续跑率', title: '续跑率 = resume:true 续跑记录 / 桶内 runs', fmt: function (b) { return mpPct(b.resumeRate) } },
|
|
2043
2046
|
{ k: 'cacheHitRate', label: '缓存命中率', title: '缓存命中率 = cacheRead / (input + cacheRead);⚠️ 跨 provider 口径可能不一(input 是否含缓存读各家不同),对比前先核口径;无 usage 样本 → —', fmt: function (b) { return mpPct(b.cacheHitRate) } }
|
|
2044
2047
|
]
|
|
@@ -2088,14 +2091,14 @@ function apply(ctx) {
|
|
|
2088
2091
|
MP_COLS.map(function (c) { return React.createElement('th', { key: c.k, style: thR, title: c.title }, c.label) }))),
|
|
2089
2092
|
React.createElement('tbody', null, shown.map(function (b) {
|
|
2090
2093
|
// 样本护栏:insufficient 行整行灰显 + title 前缀「样本 <5,仅供参考」,不进任何最优强调
|
|
2091
|
-
var tip = (b.insufficient ? '样本 <5,仅供参考——' : '') + b.model + ' × ' + b.role + ' × ' + (MP_SIZE_LABEL[b.size] || b.size) + ':runs=' + b.runs + ';有效总额 ' + fmtTokens(b.effSum) + ' tok;耗时 P90 ' + mpDur(b.durP90Ms) + '
|
|
2094
|
+
var tip = (b.insufficient ? '样本 <5,仅供参考——' : '') + b.model + ' × ' + b.role + ' × ' + (MP_SIZE_LABEL[b.size] || b.size) + ':runs=' + b.runs + ';有效总额 ' + fmtTokens(b.effSum) + ' tok;耗时 P90 ' + mpDur(b.durP90Ms) + '(八指标口径见表头 title)'
|
|
2092
2095
|
return React.createElement('tr', { key: b.model + '|' + b.role + '|' + b.size, title: tip, style: { opacity: b.insufficient ? 0.45 : 1 } },
|
|
2093
2096
|
React.createElement('td', { style: tdL }, b.model),
|
|
2094
2097
|
React.createElement('td', { style: tdL }, b.role + '·' + (MP_SIZE_LABEL[b.size] || b.size)),
|
|
2095
2098
|
React.createElement('td', { style: tdR }, String(b.runs)),
|
|
2096
2099
|
MP_COLS.map(function (c) { return React.createElement('td', { key: c.k, style: tdR }, c.fmt(b)) }))
|
|
2097
2100
|
})))),
|
|
2098
|
-
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:每行 = 模型 × 角色 × 规模段桶(规模按任务有效 token:小 <1M / 中 1~10M / 大 >10M);率类分母为桶内 runs,一次通过率分母为桶内 resolved
|
|
2101
|
+
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:每行 = 模型 × 角色 × 规模段桶(规模按任务有效 token:小 <1M / 中 1~10M / 大 >10M);率类分母为桶内 runs,一次通过率分母为桶内 resolved 任务;失败率 = 跑崩落定(outcome 为 incomplete/error/failed)的 run / 桶内 runs,terminated 人工终止不计入;runs<5 行灰显 = 样本不足仅供参考;随统计范围裁剪(与 Token 区同一 range)'))
|
|
2099
2102
|
}
|
|
2100
2103
|
|
|
2101
2104
|
// ===== 质量趋势区(记分卡卡4,task-muxhtgil):一次通过率趋势 / 卡时长分布 / 超时率走势 / 续跑成功率 =====
|
|
@@ -2484,12 +2487,16 @@ function apply(ctx) {
|
|
|
2484
2487
|
}, [])
|
|
2485
2488
|
useEffect(function () { setVal(props.value || ''); setDirty(false) }, [props.value])
|
|
2486
2489
|
function save() { setDirty(false); rpc('set-board-config', { key: cfgKey, value: (val || '').trim() }).then(fetchTasks).catch(function () {}) }
|
|
2487
|
-
|
|
2490
|
+
// 下拉两段式(task-mv9yuq2l,实证 n-mv8ki92qnhdq:巡检戳即点即写 → verifierModel 神隐回退无从审计):
|
|
2491
|
+
// 选中只改本地草稿态 + 亮 dirty(对比服务端权威值 props.value),点旁边 ✓ 保存键才走 save() 提交 rpc——
|
|
2492
|
+
// 与下方降级文本输入同款两段式语义,巡检路过戳一下不再直接写配置。
|
|
2493
|
+
function pick(v) { setVal(v); setDirty(v !== (props.value || '')) }
|
|
2488
2494
|
if (models) {
|
|
2489
2495
|
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
|
|
2490
|
-
React.createElement('select', { value: val, onChange: function (e) { pick(e.target.value) }, title: label + '
|
|
2496
|
+
React.createElement('select', { value: val, onChange: function (e) { pick(e.target.value) }, title: label + '(空=继承父级;选中后点 ✓ 才保存)', style: { width: 150, padding: '0px 4px', fontSize: 9, border: '1px solid ' + (dirty ? C.brand : C.border), borderRadius: 2, background: C.card, color: C.text } },
|
|
2491
2497
|
React.createElement('option', { value: '' }, '继承父级'),
|
|
2492
|
-
models.map(function (m) { return React.createElement('option', { key: m.id, value: m.id }, m.name + ' (' + m.provider + ')') }))
|
|
2498
|
+
models.map(function (m) { return React.createElement('option', { key: m.id, value: m.id }, m.name + ' (' + m.provider + ')') })),
|
|
2499
|
+
dirty ? React.createElement('button', { onClick: save, title: '保存 ' + label, style: { fontSize: 9, padding: '0px 5px', border: 'none', borderRadius: 2, background: C.brand, color: C_INV, cursor: 'pointer', lineHeight: '14px', fontWeight: 600, display: 'inline-flex', alignItems: 'center' } }, ic('check', 9)) : null)
|
|
2493
2500
|
}
|
|
2494
2501
|
// 降级:文本输入(模型枚举不可用时)
|
|
2495
2502
|
return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
|
package/lib/core.mjs
CHANGED
|
@@ -160,7 +160,7 @@ export function boardHome() { return path.join(boardDirName(), '.dsh') }
|
|
|
160
160
|
// ownerCwd(跨重启继承):创建该看板的会话工作区路径,继承判定全靠它——取不到就省略字段
|
|
161
161
|
// (绝不落空串,否则「路径读不到的多个会话」会被误判成同一工作区)。
|
|
162
162
|
export function seed(sid, ownerCwd) {
|
|
163
|
-
var d = { version: 12, ownerSession: sid, boardMode: 'auto', teamMode: false, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, workerContinuable: true, verifyUserGuide: true, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, cleanupRetentionDays: 7, poolStatus: { workers: [], verifiers: [] }, tasks: [] }
|
|
163
|
+
var d = { version: 12, ownerSession: sid, boardMode: 'auto', teamMode: false, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, workerContinuable: true, verifyUserGuide: true, minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, cleanupRetentionDays: 7, poolStatus: { workers: [], verifiers: [] }, tasks: [], configLog: [] }
|
|
164
164
|
if (typeof ownerCwd === 'string' && ownerCwd) d.ownerCwd = ownerCwd
|
|
165
165
|
return d
|
|
166
166
|
}
|
|
@@ -182,6 +182,9 @@ export function normalizeBoard(d) {
|
|
|
182
182
|
if (typeof d.epicSplit !== 'boolean') d.epicSplit = true
|
|
183
183
|
if (typeof d.workerContinuable !== 'boolean') d.workerContinuable = true
|
|
184
184
|
if (typeof d.verifyUserGuide !== 'boolean') d.verifyUserGuide = true
|
|
185
|
+
// 配置审计行(实证 n-mv8ki92qnhdq:verifierModel 神隐回退零留痕无从审计):老看板缺字段/脏值 → 补空数组;
|
|
186
|
+
// 写入由 set-board-config 成功时追加 {key, from, to, actor, at}(截尾 50 条),此处只兜底形态。
|
|
187
|
+
if (!Array.isArray(d.configLog)) d.configLog = []
|
|
185
188
|
// 清理保留天数(卡龄主闸):缺字段/脏值(非数或 <1)→ 7;合法值四舍五入并钳 1-90。
|
|
186
189
|
if (typeof d.cleanupRetentionDays !== 'number' || !(d.cleanupRetentionDays >= 1)) d.cleanupRetentionDays = 7
|
|
187
190
|
else d.cleanupRetentionDays = Math.max(1, Math.min(90, Math.round(d.cleanupRetentionDays)))
|
|
@@ -753,6 +756,15 @@ export function buildFileOutline(content) {
|
|
|
753
756
|
// 已经建好的单张卡(要拆也轮不到它拆,真觉得大应走歧义上报)。故 epicSplit=false 时 Worker prompt 字面
|
|
754
757
|
// 与 true 时逐字相同(单测锁定 parity,防止将来有人往这里塞拆分条款而漏接门禁);buildWorkerPrompt
|
|
755
758
|
// 因此不引入 epicSplit 形参——没有条款可跳过,加个无用参数只会是死代码。
|
|
759
|
+
// 前轮半成品提示的命中判定(task-mvbeallq,残影治理②):卡 runs[] 里有**已落定**的 worker run
|
|
760
|
+
// (outcome 已写且非 running——running 是僵尸/在跑留档,不构成「有半成品可续作」的依据)。
|
|
761
|
+
// 「非本条 run」天然成立:本条 run 的留档在 spawn 成功后才由 recordRunHistory 追加,buildWorkerPrompt
|
|
762
|
+
// 跑在 spawn 之前,此刻 runs[] 里全是前轮;续跑路径不走本 prompt(tryResumeWorker 自带断点续跑指令)。
|
|
763
|
+
function hasSettledWorkerRun(t) {
|
|
764
|
+
if (!t || !Array.isArray(t.runs)) return false
|
|
765
|
+
for (var i = 0; i < t.runs.length; i++) { var r = t.runs[i]; if (r && r.role === 'worker' && r.outcome && r.outcome !== 'running') return true }
|
|
766
|
+
return false
|
|
767
|
+
}
|
|
756
768
|
export function buildWorkerPrompt(t, pack, feedbackEnabled) {
|
|
757
769
|
var notes = histNotes(t)
|
|
758
770
|
var msgs = buildMessages(t)
|
|
@@ -761,13 +773,24 @@ export function buildWorkerPrompt(t, pack, feedbackEnabled) {
|
|
|
761
773
|
if (t.acceptance) p += '\n硬性验收脚本: ' + t.acceptance + '\n(必须实际运行该命令并在自测情况中粘贴真实输出;未通过不得上报完成)'
|
|
762
774
|
if (notes) p += '\n\n该任务的过程记录(歧义上报/主窗口裁决/驳回/干预,请务必遵循最新裁决方向):\n' + notes
|
|
763
775
|
if (msgs) p += '\n\n该任务的详细消息(裁决答案/干预指令/歧义原文等,请务必遵循):\n' + msgs
|
|
776
|
+
// 前轮半成品提示(task-mvbeallq):有前轮已落定 worker run → 工作区可能躺着前轮未提交的半成品
|
|
777
|
+
// (残影被补停/池故障重派/驳回重做等场景),先 git status 盘点、能续作就续作,别盲目从零。
|
|
778
|
+
if (hasSettledWorkerRun(t)) p += '\n\n本卡有前轮 run 的痕迹——先 git status 看工作区有无未提交的半成品改动,能续作就在其基础上续作,别盲目从零推倒重来。'
|
|
764
779
|
if (pack) p += '\n\n' + pack
|
|
765
780
|
// 学习飞轮 v1 软召回(feedbackEnabled 关闭时不出现):只提示"先查历史教训",看板绝不代查——
|
|
766
781
|
// 环境里有没有笔记/记忆类工具、教训库长什么样,都是 Worker 自己判断的事(零耦合)。
|
|
767
782
|
if (feedbackEnabled !== false) p += '\n\n' + LESSON_RECALL_HINT
|
|
768
783
|
// git 纪律红线(n-muyaaoo7cqnd 实证事故:并行 Worker 跑 git checkout HEAD -- package.json
|
|
769
784
|
// 把另一 Worker 的在途编辑冲掉):禁止还原命令,只改自己写的文件,要干净基线走 escalate 上报。
|
|
770
|
-
|
|
785
|
+
// 精确路径 add 条款(n-mv6xntg1p6i5 实证:并行 Worker 共享 worktree+暂存区,宽泛 git add
|
|
786
|
+
// 三起吞并他卡在途编辑——p4-rdf 带走 p4-apply-ddl-core 的 4 处在途编辑混入自己 commit):
|
|
787
|
+
// 提交只允许精确路径 add(本卡 touches 或逐一列明改过的文件),禁 git add -A / . / 目录。
|
|
788
|
+
p += '\n\ngit 纪律红线:禁止 git checkout / git restore / git reset --hard / git clean 等还原命令(会冲掉并行 Worker 与你自己的未提交在途编辑);你只需管自己写的文件,别人改了别的文件与你无关;确实需要干净基线时用 board_report(kind=escalate)上报,由主窗口裁决。提交时只允许精确路径 git add <本卡 touches 声明的文件>(或明确你改过的文件路径逐一列出);禁止 git add -A / git add . / git add 目录——并行 Worker 共享暂存区,宽泛 add 会把别卡在途编辑卷进你的 commit。'
|
|
789
|
+
// 大文件交付纪律(实证 n-mv65bhk0mg5w:gs-gov-policy task-mv63fo9y 池 Worker 连续 3 个 run 单次输出
|
|
790
|
+
// 64k tok 撞上限、烧 330k token 零文件产出——都在试图一次性 write 整个 22.5KB HTML;主窗口改用
|
|
791
|
+
// write×1 + edit×2 分块落地同一 spec 一次过):固定段写死「预估 >8KB 禁止单次 write 全量」。
|
|
792
|
+
// 纯 prompt 层指引段,零逻辑风险;只进 Worker prompt,不进 verifier/hook(与 git 纪律段同例)。
|
|
793
|
+
p += '\n\n大文件交付纪律:单文件交付预估 >8KB(或 spec 含全量文案/长代码)时,禁止单次 write 写入全量——单次超长输出会撞模型输出上限、上下文耗尽零产出。先 write 落主体骨架,再用 edit 追加续块(每块 ≤4KB)逐块落地,直至写完。'
|
|
771
794
|
p += '\n\n完成契约(双模,工具优先):\n1. 完成时:优先调用 board_report 工具(kind=complete, taskId=' + t.id + ',summary=开发描述/changes=改动清单/selfTest=自测情况/diffStat=变更概要);工具不可用则按分段格式输出(## 开发描述 / ## 改动清单 / ## 自测情况 / ## diff 概要)。\n diffStat 要求:若本次改动发生在 git 仓库内,运行 git diff --stat(含 git status --short),把输出贴进 diffStat(≤1500 字符);关键逻辑变更可附 ≤20 行核心片段。非代码任务/无 git 仓库可省略。\n **board_report 调用成功即任务终点:立即结束输出,不要再修改/验证任何文件**。上报后任务即刻进入验收,你继续改动会让代码在验收口径之外漂移、且阻塞 Verifier 派发(实测有 Worker 上报后又自测 16 分钟);上报后发现新问题的,写进 selfTest 备注交由 Verifier/主窗口裁决。\n2. 歧义/信息不足/需用户决策时:优先调用 board_report(kind=escalate, taskId=' + t.id + ', question=疑问);工具不可用则输出以 [ESCALATE] 开头的说明。不要猜测。上报歧义后直接结束本轮——裁决后会有新 Worker 带着裁决答案接手。\n3. 进展汇报(较大任务):按里程碑推进,每完成一个可验证的里程碑调用一次 board_report(kind="progress", taskId=' + t.id + ', question=一行进展摘要,≤200 字符)。只在有实际产物/结论时报;禁止定时汇报或表演式汇报。'
|
|
772
795
|
return p
|
|
773
796
|
}
|
|
@@ -911,6 +934,11 @@ export function touchesConflict(t, holds) {
|
|
|
911
934
|
// pre hook 串行闸门(hooks=agent run 接线①):候选所属 epic 的 hooks.pre 未 done 时整批不派
|
|
912
935
|
// (见 preHookGate),由 poolCycle 改 spawn hook-pre run——同一 epic 的子任务在准备完成前
|
|
913
936
|
// 一张都不派,这是「pre 闸门」的串行语义;未声明 hooks 的 epic 逐字不受影响。
|
|
937
|
+
// 容器卡结构性排除(epic 占 worker 槽根修):凡有子任务的卡(epic)永不进派发候选——
|
|
938
|
+
// 与 pipeline 字段无关,不再只靠「pipeline=direct 口头约定」(task_create 缺省 full,
|
|
939
|
+
// 手滑忘传 direct 的 epic 进池被派出占槽、已就绪任务空等,用户实证)。verifs 分支不动:
|
|
940
|
+
// 子任务全了结后 epic 转 verifying 派 Verifier 验收是既有正确行为,不受此排除影响。
|
|
941
|
+
// 子任务口径与 childStats 同源:含已归档(parentId 仍指向即算有子任务)。
|
|
914
942
|
export function pickDispatch(d, capW, capV, busyTaskIds) {
|
|
915
943
|
var blockedTouches = []
|
|
916
944
|
var pendings = []
|
|
@@ -918,7 +946,7 @@ export function pickDispatch(d, capW, capV, busyTaskIds) {
|
|
|
918
946
|
// 防止同一轮 cycle 派出的两个任务声明重叠 touches。
|
|
919
947
|
var holds = holdsFiles(d)
|
|
920
948
|
if (capW > 0) {
|
|
921
|
-
var cands = d.tasks.filter(function (t) { return t.status === 'pending' && !t.claimedBy && !t.frozen && t.assignMode !== 'manual' && t.pipeline !== 'direct' && depsSatisfied(d, t) && !t.escalation && preHookGate(gpt(t, d.tasks)).pass })
|
|
949
|
+
var cands = d.tasks.filter(function (t) { return t.status === 'pending' && !t.claimedBy && !t.frozen && t.assignMode !== 'manual' && t.pipeline !== 'direct' && !d.tasks.some(function (x) { return x.parentId === t.id }) && depsSatisfied(d, t) && !t.escalation && preHookGate(gpt(t, d.tasks)).pass })
|
|
922
950
|
.sort(function (a, b) { var p = (PRIO_RANK[b.priority] || 2) - (PRIO_RANK[a.priority] || 2); return p !== 0 ? p : (a.createdAt || '').localeCompare(b.createdAt || '') })
|
|
923
951
|
for (var i = 0; i < cands.length && pendings.length < capW; i++) {
|
|
924
952
|
var conflicts = touchesConflict(cands[i], holds)
|
|
@@ -1047,6 +1075,35 @@ export function draftLint(t, io) {
|
|
|
1047
1075
|
return out
|
|
1048
1076
|
}
|
|
1049
1077
|
|
|
1078
|
+
// ===== 建卡派发预警(task-mvbealtf / 巡检反馈 n-mvba2neatqoa)=====
|
|
1079
|
+
// 巡检实证:agent 用 task_create 建「纯占位/测试」卡(非 Team 缺省 draft=false)0.145s 即被
|
|
1080
|
+
// 派发、烧 219K token 才终止——建卡响应只有验收/调研类 warning,无任何「将立即派发」提示。
|
|
1081
|
+
// 此预警只挂建卡返回体(瞬态提示,不落 task.lintWarnings 持久字段:落盘会在详情页长期残留
|
|
1082
|
+
// 过期文案,且 update-task 走 draftLint 重算会把它冲掉、两种口径自相矛盾)。
|
|
1083
|
+
// 判定只覆盖「建卡即入池」形态(pending + auto + 非 direct + 依赖满足);touches 锁/池暂停
|
|
1084
|
+
// 只可能延迟领取、不取消入池事实,故文案说「将进入派发池」而非「已被领取」。
|
|
1085
|
+
export var DISPATCH_NOW_LINT = '此卡将立即进入派发池(约 1s 内被 Worker 领取);想先占位请 draft:true 建草稿'
|
|
1086
|
+
|
|
1087
|
+
// 将自动派发形态判定:pending(非 draft)且 assignMode=auto 且 pipeline≠direct 且依赖已满足。
|
|
1088
|
+
// 纯函数零 IO(复用同文件 depsSatisfied);t 为刚建好的任务对象。
|
|
1089
|
+
export function willAutoDispatch(d, t) {
|
|
1090
|
+
if (!d || !t || typeof t !== 'object') return false
|
|
1091
|
+
if (t.status !== 'pending') return false
|
|
1092
|
+
if ((t.assignMode || 'auto') !== 'auto') return false
|
|
1093
|
+
if ((t.pipeline || 'full') === 'direct') return false
|
|
1094
|
+
return depsSatisfied(d, t)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// 建卡返回体追加派发预警(task_create 工具 / create-task RPC 双通道同口径):
|
|
1098
|
+
// 命中自动派发形态时,把响应里的 task 换成「lintWarnings 追加 DISPATCH_NOW_LINT」的浅拷贝——
|
|
1099
|
+
// 持久 task 本体不动(响应瞬态提示,见上方注释);未命中/异常入参原样返回(返回体形态不变)。
|
|
1100
|
+
export function appendDispatchWarning(out, d, t) {
|
|
1101
|
+
if (!out || !out.ok || !out.task || !willAutoDispatch(d, t)) return out
|
|
1102
|
+
var base = Array.isArray(t.lintWarnings) ? t.lintWarnings : []
|
|
1103
|
+
out.task = Object.assign({}, t, { lintWarnings: base.concat([DISPATCH_NOW_LINT]) })
|
|
1104
|
+
return out
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1050
1107
|
// ===== touches → suggestedContextFiles 自动桥接(调研遵循·host 三件套 ①)=====
|
|
1051
1108
|
// 让遵守成为最省力路径:建卡触发「无调研上下文」warning 时,直接把 touches 里的具体文件
|
|
1052
1109
|
// 提炼成可一键采纳的 contextFiles 建议(响应挂 suggestedContextFiles 字段)。
|