dsh-agent-board 1.9.0 → 1.9.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 CHANGED
@@ -41,7 +41,7 @@
41
41
  | **0.1.7 ~ 0.1.7-x**(含 rc) | **≥ 1.2.2(必须)** | 0.1.7 会话日志升级为 format v4:插件消息 `source.kind` 必须是生产者自有 kind。1.2.1 及更早会在**任务完成/阻塞回执**落盘时抛 `SessionFormatError: format v4 message requires a producer-owned source kind`,并连带使主窗口当前轮次失败(表现为「本轮运行失败」);同时「跳转会话」因宿主移除 `sessions.open` 而失效(控制台 `sessionsSvc.open is not a function`),卡片活动心跳读不到 v4 日志(`session.v4.jsonl.zstd`) |
42
42
  | 0.1.5-rc.1 ~ 0.1.6 | ≤ 1.2.1 | 1.2.2 起按 0.1.7 协议编写(v4 source kind、`uiWorkspace.openSession` 跳转),旧宿主未做回归验证,建议停留 1.2.1 |
43
43
 
44
- > 当前版本 **v1.9.0** 已在宿主 **0.2.1-rc.3** 全量实测:640 断言全绿 + 拆解官/力度路由/健康嗅探真机 E2E 全链。
44
+ > 当前版本 **v1.9.1** 已在宿主 **0.2.1-rc.3** 全量实测:723 断言全绿 + 拆解官/力度路由/健康嗅探/帧活性挂死检测真机 E2E 全链。
45
45
 
46
46
  ```sh
47
47
  dsh plugin --profile web add dsh-agent-board@latest # 0.1.7+/0.2.x 宿主(推荐)
@@ -119,7 +119,7 @@ dsh plugin --profile web remove dsh-agent-board
119
119
 
120
120
  - **看板**:六列状态流、拖拽 / 多选批量 / 文本·优先级·标签筛选、归档留档、会话隔离
121
121
  - **派发**:一次性 Worker / 独立 Verifier、依赖调度(DFS 环检测)、touches 文件级排他、可续跑 Worker、Team 托管、**spawn 落闸锁内重验**(spawnGateOk:claim 占位与落闸决策读同一把锁的同一份快照,terminate/dependsOn 后补 落在 spawn 在途窗口即弃权——竞态与后补依赖双双根治)、**高优干预分态**(intervene 无活跃 run 时区分「续跑窗口,干预随续跑注入」与「真无 run」,返回体带 state 字段)、**建卡派发预警**(将自动派发时 lintWarnings 瞬态提示「约 1s 内被领取,想占位请 draft:true」)、**宿主 spawn API 双兼容**(v0.2.1 宿主把 `subagents.start/startContinuable` 统一重构为 `startActivation(spec)`——能力探测自动选路:新契约按 `delivery` 语义分发(continuable=`parent` 结算留言 / 一次性=`caller` 只回 result),Activation 适配旧 run 形状下游零改动;老宿主逐字走旧双路径)+ **continuable 结算兜底臂**(`activation.result` 在子会话被宿主拆除时落定即接失败结算——rate-limit/模型崩溃的死子不再挂到硬超时)
122
- - **智能调度** `☆`:**拆解官工作流**(自然语言需求卡 → `task_decompose` 拆解官先调研后提案(outputSchema 强箍,子卡必带档位/touches/验收命令)→ 主窗口呈现、你确认调档 → 逐张 publish;拆解官在池条「🧩 拆解中」与源卡徽标可见)+ **力度路由**(轻/标/重三档 × Worker/Verifier 六条有序链,四级取模链:卡级特例→档位链→板级→父模型,每跳过健康闸)+ **备用模型链** `fallbackModels`(额度到顶/故障先跨供应商同档切换,链尽才降档)+ **冷却与熔断**(限流带重置时间解析、连接/网关故障 10 分钟短窗自恢复)+ **模型健康嗅探**(`llm.stream` 极简探测定时扫已配置模型,统一 modelHealth 视图——冷却/熔断/近 5 次 run 聚合,弹层链芯片带 🟢🟡🔴⚪ 健康点,事前调度可见)
122
+ - **智能调度** `☆`:**拆解官工作流**(自然语言需求卡 → `task_decompose` 拆解官先调研后提案(outputSchema 强箍,子卡必带档位/touches/验收命令 + **dependsOn 序号接线**:提案内以 1 起序号互相引用,物化时映射真实任务 id,前排约束天然无环、坏序号丢弃留痕)→ 主窗口呈现、你确认调档 → 逐张 publish;拆解官在池条「🧩 拆解中」与源卡徽标可见;**落定走正式回执**:「🧩 已拆解 N 个(待确认发布)」聚合分区——split 列子卡清单 / keep 列理由,源卡停 draft 也必送达(投递过滤豁免,archived 才丢),失败仍走 ⚠️ 系统异常)+ **力度路由**(轻/标/重三档 × Worker/Verifier 六条有序链,四级取模链:卡级特例→档位链→板级→父模型,每跳过健康闸)+ **备用模型链** `fallbackModels`(额度到顶/故障先跨供应商同档切换,链尽才降档)+ **冷却与熔断**(限流带重置时间解析、连接/网关故障 10 分钟短窗自恢复)+ **模型健康嗅探**(`llm.stream` 极简探测定时扫已配置模型,统一 modelHealth 视图——冷却/熔断/近 5 次 run 聚合,弹层链芯片带 🟢🟡🔴⚪ 健康点,事前调度可见;**探测错误码分治**:额度>凭据>连接信号才判失败,纯空响应(供应商怪癖)判 🟡 degraded 存活——不喂冷却、路由仍可选用;**探测预算独立**:每次尝试各 20s(慢模型重试不再被共享窗口饿死),重试预算耗尽沿用首探判定不判死、超时不覆盖既有观测)
123
123
  - **执行纪律面** `☆`:**计划闸口**(Worker 动笔前报「计划改动」清单,越界+撞他卡锁双查警示,点名持锁卡)+ **仲裁扩权入锁**(`task_arbitrate` 的 `expandTouches` 把放行文件并入有效锁集+冲突预检+hold 撞锁自动等锁复工)+ **续跑健康闸**(病模型续跑自动改 fresh spawn、宿主秒拆喂熔断、死会话哨兵 5s 即重派)+ **sizeHint 动态调档**(Worker 执行中自报档位修正,下次 spawn 生效)+ **opsLog 人工留痕**(GUI/脚本写集落盘截尾 50 条 + Team 注入「最近人工操作」摘要)
124
124
  - **验收治理** `★`:硬性验收脚本独立复跑、驳回全量带回重派、Verifier 自测指南(userTest)、风险队列「待你过目」、验收时序防护(Verifier 在场时主窗口抢批需显式 `force:true`;有活跃 run 的卡禁止归档;Verifier 迟到结论落 `lateVerdict` 留痕+直投告警)、**迟到 verdict 三保险**(①入参校验:`verdict` 非 approved/rejected 拒收并返回重调指引——漏带字段不再被当驳回落账;②通知分态:approved 迟到=「✅ 迟到复核·全过追认」notice 级,🚨「迟到驳回」只留给真 rejected;③指纹去重:同 taskId+by+verdict 只投一次,重复落账零重复通知)
125
125
  - **度量** `★`:Token 消耗(总量 / 有效 / 缓存读,按模型·按任务·按日)+ 记分卡(模型表现 / 质量趋势 / 驳回聚类)
@@ -132,7 +132,7 @@ dsh plugin --profile web remove dsh-agent-board
132
132
 
133
133
  - 会话标题栏「智能看板」按钮 → 顶部抽屉面板(看板 / 团队 / 仪表盘三视图)
134
134
  - 面板右上角「+ 新建任务」:表单弹层填标题/描述/优先级/管线/touches/依赖多选/验收脚本,Team 托管默认存为草稿(提交走 create-task RPC,成功即刷新、失败表单内联报错)
135
- - 新建任务弹层为视口级 overlay(fixed 全屏遮罩 + 限高 80vh + 字段区内部滚动 + 按钮区吸底常驻,不再被看板抽屉裁切);卡片「当前动作」行摘要化为「工具名 + 关键参数」(40 字符内有意义断点截断,悬停 title 看 host 原文),不再展示裸 JSON
135
+ - 新建任务弹层为视口级 overlay(fixed 全屏遮罩 + 限高 80vh + 字段区内部滚动 + 按钮区吸底常驻,不再被看板抽屉裁切);**设置/模型路由弹层带边界钳制**(左探出抽屉自动改左锚、右溢钳回,界面缩放档位下坐标自动还原);执行池条窄抽屉下自动折行;卡片「当前动作」行摘要化为「工具名 + 关键参数」(40 字符内有意义断点截断,悬停 title 看 host 原文),不再展示裸 JSON
136
136
  - 六列状态流:草稿 → 待办 → 进行中 → 验证中 → 已完成 → 阻塞(空板也统一渲染六列骨架 + 引导 CTA:右上角「+ 新建任务」或对话一句 task_create 建第一张卡)
137
137
  - 读路径 error 防线:get-tasks 返回 {error} 或网络抖动时保留现有卡片与设置(不再静默清空全板),面板头部下方出非阻断红色错误条,下次刷新成功自动消失
138
138
  - 耗时口径三分离:待办/草稿卡显「⏳ 等待 N」(创建→被领取),执行后显「⏱ 执行 N」(被领取→完成,tooltip 写清口径)——排队时间不再混进"耗时"
@@ -221,7 +221,7 @@ draft → pending → in-progress → verifying → resolved → archived
221
221
  - **Worker/Verifier 均可配置异构模型**(⚙️ 弹出层下拉选择,空 = 继承父级),避免同源盲点;模型故障自动熔断回退父级模型
222
222
  - **力度路由(三档 × 双角色模型表,`task-mvnemvf5` 地基 + `task-mvneq4mw` GUI)**:卡级 `size`(light/standard/heavy,对话 `task_update` 调档,缺省=标档语义、存量卡零迁移)查板级 `laneModels` 路由表定模——取模链四级:**卡级 `model` 特例(最高)→ 档位 lane 槽 → 板级 workerModel/verifierModel 行 → 父模型**(每跳照常过熔断,命中熔断降下一跳;实际命中跳随 run 落 `modelSource` 留档审计,档位随 run 落 `size` 留档供记分卡按档位聚合);「模型路由」弹层(工具栏 cpu 图标)「**力度路由**」小节:三档 × Worker/Verifier 共 **6 条有序链编辑器**(下拉添加+芯片 × 移除/↑ 前移,改动即写),空链 =「继承板级」(继承上方 Worker/Verifier 模型行);写通道 = `set-board-config key='laneModels'` **部分合并**(只合显式给出的槽位,非法模型整体拒写不落半截 + configLog 审计行);**档位槽支持有序数组=同档优先序**(`task-mvnxckhq`:槽位 `string[]` 第一位=首选,按序取首个健康模型——首选冷却/熔断则次选赢,槽内全病照旧 fallback→降档→父模型;GUI 为链式编辑器——下拉添加+芯片 ×/↑ 排序,读写整个有序数组,`task-mvokikl3`;老单字符串槽位读入自动归一单元素数组,零迁移);卡面 size/卡级 model 徽章只读展示(GUI 不编辑,调档走对话 `task_update`)
223
223
  - 孤儿回收:子代理 run 结束/丢失超 2 分钟 → 任务自动回待办重派
224
- - 看门狗:运行超时且事件流停滞 → 标记"疑似卡死"(不自动杀,裁决权交主窗口/用户)
224
+ - 看门狗:**帧活性双层判定**——运行中 assistant 帧静默 ≥120s → 标「🟡 疑似卡顿」(仅标卡、不杀,裁决权交主窗口/用户);帧静默 ≥600s → 判死按模型面故障结算重派并计入熔断连击;无事件订阅能力/零帧观测的宿主退回日志零增长兜底(连续 40 轮零增长 × ≥600s 墙钟)
225
225
  - 歧义上报:Worker 遇到歧义不猜测,上报等主窗口裁决(任何模式下都通知);裁决后新 Worker 携带裁决答案接手
226
226
  - 驳回详情全量带回:三条驳回路径(`board_verdict` 工具 / Verifier 文本结算 / 手动 `task_verify`·`verify-task`)统一往 `t.messages` 落一条 `kind: "rejection"` 完整驳回包(summary + checks 逐条核对证据;手动路径补写 `t.verification`),经 `buildMessages` 全量注入重派 Worker prompt——新 Worker 据此返工,不再只看到 300 字截断的 history 记录
227
227
  - **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` 收末尾并标注「无用户可感知面」)
@@ -244,7 +244,7 @@ draft → pending → in-progress → verifying → resolved → archived
244
244
  适用场景:用户用自然语言丢来一个大需求(跨模块、边界不清、一口气干不完)——先建 **draft 需求卡**保留需求原文,再用 `task_decompose` 派**一次性拆解官**出实现侧子卡提案;确认后逐张发布,Worker 拿到的就是 10~30 分钟可独立验收的小卡。Team 模式第 9 条需求卡工作流(`TEAM_DECOMPOSE_FLOW`)即这条链路;`task_decompose` 工具常驻,**不受 `epicSplit` 开关影响**。
245
245
 
246
246
  - **门禁**:源卡须 `draft`/`pending`(`in-progress`/`verifying`/已完成卡拒拆:「源卡须为 draft/pending」);同一源卡已有活跃拆解 run → 拒并返回 `runId`(等落定再重拆);仅主窗口可用(子代理调用被拒)。
247
- - **提案契约**:拆解官**零建卡工具**(权限口子根本不用开),唯一交付物是 `outputSchema` 强箍的 `{verdict, children[], rationale}`——`verdict` 只 `split`/`keep`;每个 child 必填 `title`/`description`/`size`/`touches`/`acceptance`,可选 `modelHint`/`dependsOnIdx`;`children` 上限 **8 张**(`DECOMPOSE_MAX_CHILDREN`,超出截断);`dependsOnIdx` 只许引用本批**前排**下标,物化时翻译成真实 id,按构造天然无环。拆解纪律写死在 prompt:先 `read` 调研清单里的文件、按文件/模块边界从实现侧拆,每张卡 touches 精确到文件、acceptance 是可执行命令。
247
+ - **提案契约**:拆解官**零建卡工具**(权限口子根本不用开),唯一交付物是 `outputSchema` 强箍的 `{verdict, children[], rationale}`——`verdict` 只 `split`/`keep`;每个 child 必填 `title`/`description`/`size`/`touches`/`acceptance`,可选 `modelHint`/`dependsOn`;`children` 上限 **8 张**(`DECOMPOSE_MAX_CHILDREN`,超出截断);`dependsOn` 以 1 起序号引用本批**前排**子卡(如第 2 张依赖第 1 张填 `["1"]`),物化时翻译成真实任务 id,按构造天然无环(坏序号丢弃+卡面留痕,task-mvr44rm3)。拆解纪律写死在 prompt:先 `read` 调研清单里的文件、按文件/模块边界从实现侧拆,每张卡 touches 精确到文件、acceptance 是可执行命令。
248
248
  - **物化**:run 落定后 host 读 `result.structured` 自动物化为 **draft 子卡**(`parentId`=源卡、`proposalOf`=源卡 id、size/model/touches/acceptance/dependsOn 全部落位,id 派生 `源卡id-N`),源卡追加「🧩 拆解提案已就位」消息并通知主窗口;**源卡状态不变**(仍 draft/pending),确认或调档后逐张 `task_update publish=true` 发布(也可 `batch-op publish` 批量)。`verdict=keep` = 判定不用拆:rationale 落源卡消息、零子卡,源卡按原样推进。
249
249
  - **幂等重拆**:新提案落地时清掉上次**未发布**的提案子卡(`proposalOf` 指向该源卡且仍 `draft` 的整批移除);已 publish/已落定的子卡绝不碰;提案畸形或 run 失败时旧批**不清**(失败不毁已有资产),失败原因落源卡消息,可原样重试。
250
250
  - **并发位**:拆解官 run 计入 **Verifier 并发位**(`activeV`),**不占 Worker 位**;活跃拆解期间源卡**跳过派发**(不让 Worker 抢在拆解结论前整坨执行);宿主不支持 `outputSchema` 时自动剥外挂参数重试,仍拿不到 `structured` 走失败臂(源卡零改动)。
package/index.mjs CHANGED
@@ -75,6 +75,7 @@ export function apply(ctx) {
75
75
  withTimeout: session.withTimeout, runsFor: session.runsFor, feedbackOn: session.feedbackOn, epicSplitOn: session.epicSplitOn,
76
76
  pushSysNote: notify.pushSysNote, maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone,
77
77
  notifyDispatched: notify.notifyDispatched, notifyLateReject: notify.notifyLateReject,
78
+ notifyDecomposed: notify.notifyDecomposed, // 拆解官回执(task-mvrqiu6x):「🧩 已拆解」聚合分区接线
78
79
  notifyLateApprove: notify.notifyLateApprove, // 迟到复核分态通知(三连根修②,task-mvf7nve8)
79
80
  })
80
81
  storeDeps.poolCycle = dispatch.poolCycle // 晚绑定收口
@@ -195,7 +195,7 @@
195
195
  // Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
196
196
  // focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
197
197
  function activate() { if (state.selectMode) { if (state.selected[t.id]) delete state.selected[t.id]; else state.selected[t.id] = true; notify() } else { state.detailId = t.id; notify() } }
198
- return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.size ? 'color-mix(in srgb, ' + C.brand + ' 18%, transparent)' : C.nested, color: t.size ? C.brand : C.text2, fontWeight: 600, flexShrink: 0, marginTop: 1 }, title: t.size ? '力度档位 size=' + t.size + '(路由板级 laneModels 三档表定模;详情页/对话 task_update 可调)' : '力度档位未显式设置——缺省=标档语义(路由 laneModels.standard 槽)' }, ({ light: '轻', standard: '标', heavy: '重' })[t.size] || '标'), reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0, marginTop: 1 } }, '👁 建议过目') : null, noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡死 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
198
+ return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.size ? 'color-mix(in srgb, ' + C.brand + ' 18%, transparent)' : C.nested, color: t.size ? C.brand : C.text2, fontWeight: 600, flexShrink: 0, marginTop: 1 }, title: t.size ? '力度档位 size=' + t.size + '(路由板级 laneModels 三档表定模;详情页/对话 task_update 可调)' : '力度档位未显式设置——缺省=标档语义(路由 laneModels.standard 槽)' }, ({ light: '轻', standard: '标', heavy: '重' })[t.size] || '标'), reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0, marginTop: 1 } }, '👁 建议过目') : null, noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { title: 'assistant 帧静默 ≥2 分钟(120s):仅标卡不终止,裁决权交主窗口/用户;静默 ≥10 分钟(600s)由看门狗判死并按模型面故障结算重派', style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡顿 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
199
199
  epic ? React.createElement('span', { title: '史诗父卡:' + csDone + '/' + cs.total + ' 个子任务已了结(完成/取消/归档)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + csDone + '/' + cs.total) : null,
200
200
  // hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
201
201
  epic ? hookPhaseBadge(t) : null,
@@ -988,7 +988,7 @@
988
988
  // 在跑子代理模型可见(task-mvnyc8vo):每个 busy run 渲染 model 小芯片 + 取模来源角标,
989
989
  // 与执行池成员卡同口径;缺 model 字段的老数据条目不渲染芯片。
990
990
  var chips = wBusy.concat(vBusy).filter(function (m) { return !!m.model })
991
- return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
991
+ return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' } },
992
992
  // 池水位标注(反馈 n-mut9rzoq3flu / n-muyg4wl9208c):裸数字第一屏无解,补可见文字标签 + title
993
993
  React.createElement('span', { title: '活跃 Worker 数/上限(⚡ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), 'Worker ' + wActive + '/' + wTotal),
994
994
  React.createElement('span', { title: '活跃 Verifier 数/上限(✓ = 在跑;拆解官在跑也占 Verifier 并发位,见右侧 🧩 计数)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), 'Verifier ' + vActive + '/' + vTotal),
@@ -1074,9 +1074,10 @@
1074
1074
  React.createElement('button', { onClick: reopen, title: '重新显示首用引导横幅(board 视图顶部定位行)', style: { padding: '0px 5px', fontSize: 9, border: '1px solid ' + C.border, borderRadius: 2, background: C.card, color: C.text, cursor: 'pointer', lineHeight: '14px' } }, '重新显示'))
1075
1075
  }
1076
1076
 
1077
- // ===== 模型健康点(task-mvoulhm7):🟢健康 / 🟡冷却中至HH:mm / 🔴熔断 / ⚪未知 =====
1077
+ // ===== 模型健康点(task-mvoulhm7):🟢健康 / 🟡降级(链路存活,task-mvrqdw4k 空响应分治) / 🟡冷却中至HH:mm / 🔴熔断 / ⚪未知 =====
1078
1078
  // 数据只来自 get-tasks.modelHealth;title 悬浮展示探测/run 时间、最近延迟与近 5 次 run 聚合。
1079
- var MODEL_HEALTH_META = { healthy: { icon: '🟢', label: '健康', color: C.ok }, cooldown: { icon: '🟡', label: '冷却中', color: C.warn }, circuit: { icon: '🔴', label: '熔断', color: C.err }, unknown: { icon: '⚪', label: '未知', color: C.text2 } }
1079
+ // degraded(task-mvrqdw4k):纯 EMPTY_RESPONSE=供应商怪癖(链路通但极简请求零内容),与 cooldown 同图标不同文案。
1080
+ var MODEL_HEALTH_META = { healthy: { icon: '🟢', label: '健康', color: C.ok }, degraded: { icon: '🟡', label: '降级', color: C.warn }, cooldown: { icon: '🟡', label: '冷却中', color: C.warn }, circuit: { icon: '🔴', label: '熔断', color: C.err }, unknown: { icon: '⚪', label: '未知', color: C.text2 } }
1080
1081
  function healthHhmm(ts) { var d = new Date(Number(ts)); return isNaN(d.getTime()) ? '' : ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2) }
1081
1082
  function healthTimeText(v) { if (!v) return ''; var d = new Date(v); return isNaN(d.getTime()) ? String(v) : d.toLocaleString() }
1082
1083
  function ModelHealthDot(props) {
@@ -1140,6 +1141,47 @@
1140
1141
  msg ? React.createElement('span', { style: { color: C.err, maxWidth: 140 } }, msg) : null)
1141
1142
  }
1142
1143
 
1144
+ // ===== 弹层定位共用钳制(task-mvt05utl,用户 2026-10-29「排版有点乱」①主修:弹层左探被裁)=====
1145
+ // 病灶:两弹层(PoolCfgPopover/ModelRoutePopover)fixed 定位统一右锚定(right=视口右缘-按钮右缘,
1146
+ // 内容向左延伸),按钮贴抽屉左缘(窄窗下工具栏折行,⚙/路由按钮落到第二行左端)时弹层左半
1147
+ // 探出抽屉/视口被裁。修法:clampPopoverPos 纯函数统一预算锚点(两弹层共用)——
1148
+ // · 默认右锚定:右缘贴按钮右缘(zoom=1 且不越界时与旧式输出逐字一致)
1149
+ // · 左溢:按钮右缘-预算宽 < 抽屉左缘+8 → 改左锚定(左缘=抽屉左缘+8,right 收起不双锚拉宽)
1150
+ // · 右溢:左锚后右缘仍越过抽屉右缘-8 → 钳回右锚(右缘=抽屉右缘-8;抽屉比弹层还窄的极端兜底)
1151
+ // · 缩放:各项除以档位还原(fixed 弹层在 zoom 子树内 inset 再被缩一次,沿用既有 /z 口径)
1152
+ // popW=弹层预算宽(whiteSpace:nowrap,宽随文案,常量估算:PoolCfg≈360 / ModelRoute≈500);
1153
+ // bounds=抽屉可视边界(popoverBounds 实测面板根;缺省退化视口两缘,钳制退化为纯视口口径不炸)。
1154
+ function clampPopoverPos(r, popW, z, bounds) {
1155
+ var zz = z || 1
1156
+ var b = bounds || { left: 0, right: window.innerWidth }
1157
+ var m = 8
1158
+ var top = (r.bottom + 4) / zz
1159
+ var maxH = Math.max(140, (window.innerHeight - r.bottom - 16) / zz)
1160
+ var right = Math.max(m, window.innerWidth - r.right) / zz
1161
+ var left = null
1162
+ if (r.right - popW < b.left + m) {
1163
+ left = (b.left + m) / zz
1164
+ if ((b.left + m) + popW > b.right - m) { left = null; right = Math.max(m, window.innerWidth - (b.right - m)) / zz }
1165
+ }
1166
+ return { top: top, left: left, right: left != null ? null : right, maxH: maxH }
1167
+ }
1168
+ // 抽屉可视边界实测(钳制输入):沿按钮向上找首个 position:fixed 祖先——面板根容器(kernel TopPanel
1169
+ // 顶层 fixed div,overflow:hidden 即抽屉);getBoundingClientRect 返回可视坐标(zoom 后真实边界,
1170
+ // 免 zoom 换算、不依赖自身 inset 是否被缩的口径)。找不到(结构变化/异常)退化视口两缘不炸。
1171
+ function popoverBounds(fromEl) {
1172
+ try {
1173
+ var el = fromEl && fromEl.parentElement
1174
+ while (el && el !== document.body) {
1175
+ if (getComputedStyle(el).position === 'fixed') { var rb = el.getBoundingClientRect(); return { left: rb.left, right: rb.right } }
1176
+ el = el.parentElement
1177
+ }
1178
+ } catch (_) {}
1179
+ return { left: 0, right: window.innerWidth }
1180
+ }
1181
+ // 弹层预算宽(whiteSpace:nowrap 内容宽随文案,常量估算;偏大偏向左锚=修被裁病灶的安全侧)
1182
+ var POOL_POPOVER_W = 360
1183
+ var ROUTE_POPOVER_W = 500
1184
+
1143
1185
  // ===== 模型路由独立弹层(task-mvnyc8vo,用户 2026-10-25「模型配置独立弹出卡片」)=====
1144
1186
  // 模型相关全部配置从池配置弹层**搬走**(不是复制)到本弹层:①板级 Worker/Verifier 模型(ModelCfg
1145
1187
  // 复用)②力度路由六槽 ③fallbackModels 备用链——②③ 2026-10-26 起统一为 OrderedModelsEditor
@@ -1184,13 +1226,14 @@
1184
1226
  function toggle() {
1185
1227
  // 与 PoolCfgPopover 同口径:fixed + 视口坐标(面板 overflow:hidden 会裁 absolute 弹层);
1186
1228
  // 界面缩放补偿——zoom 子树内 getBoundingClientRect 返回缩放后坐标,各项除以档位还原 CSS px。
1229
+ // 锚点统一走 clampPopoverPos(task-mvt05utl):左溢改左锚,修按钮贴抽屉左缘时弹层左探被裁。
1187
1230
  var z = state.uiZoom || 1
1188
- if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos({ top: (r.bottom + 4) / z, right: Math.max(8, window.innerWidth - r.right) / z, maxH: Math.max(140, (window.innerHeight - r.bottom - 16) / z) }) }
1231
+ if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos(clampPopoverPos(r, ROUTE_POPOVER_W, z, popoverBounds(btnRef.current))) }
1189
1232
  setOpen(!open)
1190
1233
  }
1191
1234
  return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
1192
1235
  React.createElement('button', { ref: btnRef, onClick: toggle, title: '模型路由(板级模型 / 力度路由六槽 / 备用模型链)', style: { fontSize: 12, padding: '3px 8px', border: '1px solid ' + (open ? C.brand : C.border), borderRadius: 6, cursor: 'pointer', background: open ? C.nested : 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('cpu', 13)),
1193
- 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: (pos.maxH || 300) + 'px', overflowY: 'auto' } },
1236
+ open && pos ? React.createElement('div', { style: { position: 'fixed', top: pos.top + 'px', left: pos.left != null ? pos.left + 'px' : null, right: pos.right != null ? pos.right + 'px' : null, 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: (pos.maxH || 300) + 'px', overflowY: 'auto' } },
1194
1237
  // ① 板级模型(从池配置弹层搬入,组件与写通道逐字不变)
1195
1238
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '板级模型'),
1196
1239
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
@@ -1252,13 +1295,15 @@
1252
1295
  // 界面缩放补偿(task-mv58e01j):根容器挂了 CSS zoom 后,getBoundingClientRect 返回缩放后的
1253
1296
  // 可视坐标,而 fixed 弹层在 zoom 子树内 top/right/maxHeight 会再被缩一次 → 各项除以档位还原成
1254
1297
  // CSS px,弹层仍贴着按钮且不溢出视口底(zoom=1 时与旧行为逐字一致)。
1298
+ // 锚点统一走 clampPopoverPos(task-mvt05utl):左溢改左锚——修按钮贴抽屉左缘(窄窗工具栏折行后
1299
+ // ⚙ 落到第二行左端)时弹层左半探出抽屉/视口被裁(2026-10-29 截图病灶本体)。
1255
1300
  var z = state.uiZoom || 1
1256
- if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos({ top: (r.bottom + 4) / z, right: Math.max(8, window.innerWidth - r.right) / z, maxH: Math.max(140, (window.innerHeight - r.bottom - 16) / z) }) }
1301
+ if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos(clampPopoverPos(r, POOL_POPOVER_W, z, popoverBounds(btnRef.current))) }
1257
1302
  setOpen(!open)
1258
1303
  }
1259
1304
  return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
1260
1305
  React.createElement('button', { ref: btnRef, onClick: toggle, title: '池配置', style: { fontSize: 12, padding: '3px 8px', border: '1px solid ' + (open ? C.brand : C.border), borderRadius: 6, cursor: 'pointer', background: open ? C.nested : 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('settings', 13)),
1261
- 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: (pos.maxH || 300) + 'px', overflowY: 'auto' } },
1306
+ open && pos ? React.createElement('div', { style: { position: 'fixed', top: pos.top + 'px', left: pos.left != null ? pos.left + 'px' : null, right: pos.right != null ? pos.right + 'px' : null, 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: (pos.maxH || 300) + 'px', overflowY: 'auto' } },
1262
1307
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
1263
1308
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
1264
1309
  React.createElement(PoolCfg, { label: 'Worker 并发', cfgKey: 'maxWorkers', value: props.maxW }),
@@ -387,8 +387,8 @@
387
387
  arbAction === 'hold' ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '冻结后不参与自动派发,补完上下文可在详情页「解除冻结」放行') : null)) : null,
388
388
  actionMsg ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6 } }, actionMsg) : null,
389
389
  task.stuckSince ? React.createElement('div', { style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
390
- React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), '执行 Agent 疑似卡死'),
391
- React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6 } }, '标记于 ' + ago(task.stuckSince) + '(运行超 5 分钟且事件流停滞超 1 分钟)。你可以查看其会话后决定:'),
390
+ React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), '执行 Agent 疑似卡顿'),
391
+ React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6 } }, '标记于 ' + ago(task.stuckSince) + '(assistant 帧静默 ≥2 分钟(120s),仅标卡不终止,裁决权交主窗口/用户;静默 ≥10 分钟(600s)将由看门狗判死并按模型面故障结算重派)。你可以查看其会话后决定:'),
392
392
  React.createElement('div', { style: { display: 'flex', gap: 6 } },
393
393
  React.createElement('button', { onClick: doTerminate, style: { fontSize: 11, padding: '4px 12px', border: 'none', borderRadius: 4, background: C.err, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('stop-circle', 12), '终止任务(重新排队)'),
394
394
  React.createElement('button', { onClick: doDismiss, style: { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 4, background: 'transparent', color: C.text2, cursor: 'pointer' } }, '继续观察'),
package/lib/client.js CHANGED
@@ -916,7 +916,7 @@ function apply(ctx) {
916
916
  // Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
917
917
  // focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
918
918
  function activate() { if (state.selectMode) { if (state.selected[t.id]) delete state.selected[t.id]; else state.selected[t.id] = true; notify() } else { state.detailId = t.id; notify() } }
919
- return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.size ? 'color-mix(in srgb, ' + C.brand + ' 18%, transparent)' : C.nested, color: t.size ? C.brand : C.text2, fontWeight: 600, flexShrink: 0, marginTop: 1 }, title: t.size ? '力度档位 size=' + t.size + '(路由板级 laneModels 三档表定模;详情页/对话 task_update 可调)' : '力度档位未显式设置——缺省=标档语义(路由 laneModels.standard 槽)' }, ({ light: '轻', standard: '标', heavy: '重' })[t.size] || '标'), reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0, marginTop: 1 } }, '👁 建议过目') : null, noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡死 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
919
+ return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.size ? 'color-mix(in srgb, ' + C.brand + ' 18%, transparent)' : C.nested, color: t.size ? C.brand : C.text2, fontWeight: 600, flexShrink: 0, marginTop: 1 }, title: t.size ? '力度档位 size=' + t.size + '(路由板级 laneModels 三档表定模;详情页/对话 task_update 可调)' : '力度档位未显式设置——缺省=标档语义(路由 laneModels.standard 槽)' }, ({ light: '轻', standard: '标', heavy: '重' })[t.size] || '标'), reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0, marginTop: 1 } }, '👁 建议过目') : null, noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { title: 'assistant 帧静默 ≥2 分钟(120s):仅标卡不终止,裁决权交主窗口/用户;静默 ≥10 分钟(600s)由看门狗判死并按模型面故障结算重派', style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡顿 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
920
920
  epic ? React.createElement('span', { title: '史诗父卡:' + csDone + '/' + cs.total + ' 个子任务已了结(完成/取消/归档)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + csDone + '/' + cs.total) : null,
921
921
  // hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
922
922
  epic ? hookPhaseBadge(t) : null,
@@ -1496,8 +1496,8 @@ function apply(ctx) {
1496
1496
  arbAction === 'hold' ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '冻结后不参与自动派发,补完上下文可在详情页「解除冻结」放行') : null)) : null,
1497
1497
  actionMsg ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6 } }, actionMsg) : null,
1498
1498
  task.stuckSince ? React.createElement('div', { style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
1499
- React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), '执行 Agent 疑似卡死'),
1500
- React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6 } }, '标记于 ' + ago(task.stuckSince) + '(运行超 5 分钟且事件流停滞超 1 分钟)。你可以查看其会话后决定:'),
1499
+ React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.warn, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), '执行 Agent 疑似卡顿'),
1500
+ React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6 } }, '标记于 ' + ago(task.stuckSince) + '(assistant 帧静默 ≥2 分钟(120s),仅标卡不终止,裁决权交主窗口/用户;静默 ≥10 分钟(600s)将由看门狗判死并按模型面故障结算重派)。你可以查看其会话后决定:'),
1501
1501
  React.createElement('div', { style: { display: 'flex', gap: 6 } },
1502
1502
  React.createElement('button', { onClick: doTerminate, style: { fontSize: 11, padding: '4px 12px', border: 'none', borderRadius: 4, background: C.err, color: C_INV, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 } }, ic('stop-circle', 12), '终止任务(重新排队)'),
1503
1503
  React.createElement('button', { onClick: doDismiss, style: { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 4, background: 'transparent', color: C.text2, cursor: 'pointer' } }, '继续观察'),
@@ -2611,7 +2611,7 @@ function apply(ctx) {
2611
2611
  // 在跑子代理模型可见(task-mvnyc8vo):每个 busy run 渲染 model 小芯片 + 取模来源角标,
2612
2612
  // 与执行池成员卡同口径;缺 model 字段的老数据条目不渲染芯片。
2613
2613
  var chips = wBusy.concat(vBusy).filter(function (m) { return !!m.model })
2614
- return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
2614
+ return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' } },
2615
2615
  // 池水位标注(反馈 n-mut9rzoq3flu / n-muyg4wl9208c):裸数字第一屏无解,补可见文字标签 + title
2616
2616
  React.createElement('span', { title: '活跃 Worker 数/上限(⚡ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), 'Worker ' + wActive + '/' + wTotal),
2617
2617
  React.createElement('span', { title: '活跃 Verifier 数/上限(✓ = 在跑;拆解官在跑也占 Verifier 并发位,见右侧 🧩 计数)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), 'Verifier ' + vActive + '/' + vTotal),
@@ -2697,9 +2697,10 @@ function apply(ctx) {
2697
2697
  React.createElement('button', { onClick: reopen, title: '重新显示首用引导横幅(board 视图顶部定位行)', style: { padding: '0px 5px', fontSize: 9, border: '1px solid ' + C.border, borderRadius: 2, background: C.card, color: C.text, cursor: 'pointer', lineHeight: '14px' } }, '重新显示'))
2698
2698
  }
2699
2699
 
2700
- // ===== 模型健康点(task-mvoulhm7):🟢健康 / 🟡冷却中至HH:mm / 🔴熔断 / ⚪未知 =====
2700
+ // ===== 模型健康点(task-mvoulhm7):🟢健康 / 🟡降级(链路存活,task-mvrqdw4k 空响应分治) / 🟡冷却中至HH:mm / 🔴熔断 / ⚪未知 =====
2701
2701
  // 数据只来自 get-tasks.modelHealth;title 悬浮展示探测/run 时间、最近延迟与近 5 次 run 聚合。
2702
- var MODEL_HEALTH_META = { healthy: { icon: '🟢', label: '健康', color: C.ok }, cooldown: { icon: '🟡', label: '冷却中', color: C.warn }, circuit: { icon: '🔴', label: '熔断', color: C.err }, unknown: { icon: '⚪', label: '未知', color: C.text2 } }
2702
+ // degraded(task-mvrqdw4k):纯 EMPTY_RESPONSE=供应商怪癖(链路通但极简请求零内容),与 cooldown 同图标不同文案。
2703
+ var MODEL_HEALTH_META = { healthy: { icon: '🟢', label: '健康', color: C.ok }, degraded: { icon: '🟡', label: '降级', color: C.warn }, cooldown: { icon: '🟡', label: '冷却中', color: C.warn }, circuit: { icon: '🔴', label: '熔断', color: C.err }, unknown: { icon: '⚪', label: '未知', color: C.text2 } }
2703
2704
  function healthHhmm(ts) { var d = new Date(Number(ts)); return isNaN(d.getTime()) ? '' : ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2) }
2704
2705
  function healthTimeText(v) { if (!v) return ''; var d = new Date(v); return isNaN(d.getTime()) ? String(v) : d.toLocaleString() }
2705
2706
  function ModelHealthDot(props) {
@@ -2763,6 +2764,47 @@ function apply(ctx) {
2763
2764
  msg ? React.createElement('span', { style: { color: C.err, maxWidth: 140 } }, msg) : null)
2764
2765
  }
2765
2766
 
2767
+ // ===== 弹层定位共用钳制(task-mvt05utl,用户 2026-10-29「排版有点乱」①主修:弹层左探被裁)=====
2768
+ // 病灶:两弹层(PoolCfgPopover/ModelRoutePopover)fixed 定位统一右锚定(right=视口右缘-按钮右缘,
2769
+ // 内容向左延伸),按钮贴抽屉左缘(窄窗下工具栏折行,⚙/路由按钮落到第二行左端)时弹层左半
2770
+ // 探出抽屉/视口被裁。修法:clampPopoverPos 纯函数统一预算锚点(两弹层共用)——
2771
+ // · 默认右锚定:右缘贴按钮右缘(zoom=1 且不越界时与旧式输出逐字一致)
2772
+ // · 左溢:按钮右缘-预算宽 < 抽屉左缘+8 → 改左锚定(左缘=抽屉左缘+8,right 收起不双锚拉宽)
2773
+ // · 右溢:左锚后右缘仍越过抽屉右缘-8 → 钳回右锚(右缘=抽屉右缘-8;抽屉比弹层还窄的极端兜底)
2774
+ // · 缩放:各项除以档位还原(fixed 弹层在 zoom 子树内 inset 再被缩一次,沿用既有 /z 口径)
2775
+ // popW=弹层预算宽(whiteSpace:nowrap,宽随文案,常量估算:PoolCfg≈360 / ModelRoute≈500);
2776
+ // bounds=抽屉可视边界(popoverBounds 实测面板根;缺省退化视口两缘,钳制退化为纯视口口径不炸)。
2777
+ function clampPopoverPos(r, popW, z, bounds) {
2778
+ var zz = z || 1
2779
+ var b = bounds || { left: 0, right: window.innerWidth }
2780
+ var m = 8
2781
+ var top = (r.bottom + 4) / zz
2782
+ var maxH = Math.max(140, (window.innerHeight - r.bottom - 16) / zz)
2783
+ var right = Math.max(m, window.innerWidth - r.right) / zz
2784
+ var left = null
2785
+ if (r.right - popW < b.left + m) {
2786
+ left = (b.left + m) / zz
2787
+ if ((b.left + m) + popW > b.right - m) { left = null; right = Math.max(m, window.innerWidth - (b.right - m)) / zz }
2788
+ }
2789
+ return { top: top, left: left, right: left != null ? null : right, maxH: maxH }
2790
+ }
2791
+ // 抽屉可视边界实测(钳制输入):沿按钮向上找首个 position:fixed 祖先——面板根容器(kernel TopPanel
2792
+ // 顶层 fixed div,overflow:hidden 即抽屉);getBoundingClientRect 返回可视坐标(zoom 后真实边界,
2793
+ // 免 zoom 换算、不依赖自身 inset 是否被缩的口径)。找不到(结构变化/异常)退化视口两缘不炸。
2794
+ function popoverBounds(fromEl) {
2795
+ try {
2796
+ var el = fromEl && fromEl.parentElement
2797
+ while (el && el !== document.body) {
2798
+ if (getComputedStyle(el).position === 'fixed') { var rb = el.getBoundingClientRect(); return { left: rb.left, right: rb.right } }
2799
+ el = el.parentElement
2800
+ }
2801
+ } catch (_) {}
2802
+ return { left: 0, right: window.innerWidth }
2803
+ }
2804
+ // 弹层预算宽(whiteSpace:nowrap 内容宽随文案,常量估算;偏大偏向左锚=修被裁病灶的安全侧)
2805
+ var POOL_POPOVER_W = 360
2806
+ var ROUTE_POPOVER_W = 500
2807
+
2766
2808
  // ===== 模型路由独立弹层(task-mvnyc8vo,用户 2026-10-25「模型配置独立弹出卡片」)=====
2767
2809
  // 模型相关全部配置从池配置弹层**搬走**(不是复制)到本弹层:①板级 Worker/Verifier 模型(ModelCfg
2768
2810
  // 复用)②力度路由六槽 ③fallbackModels 备用链——②③ 2026-10-26 起统一为 OrderedModelsEditor
@@ -2807,13 +2849,14 @@ function apply(ctx) {
2807
2849
  function toggle() {
2808
2850
  // 与 PoolCfgPopover 同口径:fixed + 视口坐标(面板 overflow:hidden 会裁 absolute 弹层);
2809
2851
  // 界面缩放补偿——zoom 子树内 getBoundingClientRect 返回缩放后坐标,各项除以档位还原 CSS px。
2852
+ // 锚点统一走 clampPopoverPos(task-mvt05utl):左溢改左锚,修按钮贴抽屉左缘时弹层左探被裁。
2810
2853
  var z = state.uiZoom || 1
2811
- if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos({ top: (r.bottom + 4) / z, right: Math.max(8, window.innerWidth - r.right) / z, maxH: Math.max(140, (window.innerHeight - r.bottom - 16) / z) }) }
2854
+ if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos(clampPopoverPos(r, ROUTE_POPOVER_W, z, popoverBounds(btnRef.current))) }
2812
2855
  setOpen(!open)
2813
2856
  }
2814
2857
  return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
2815
2858
  React.createElement('button', { ref: btnRef, onClick: toggle, title: '模型路由(板级模型 / 力度路由六槽 / 备用模型链)', style: { fontSize: 12, padding: '3px 8px', border: '1px solid ' + (open ? C.brand : C.border), borderRadius: 6, cursor: 'pointer', background: open ? C.nested : 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('cpu', 13)),
2816
- 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: (pos.maxH || 300) + 'px', overflowY: 'auto' } },
2859
+ open && pos ? React.createElement('div', { style: { position: 'fixed', top: pos.top + 'px', left: pos.left != null ? pos.left + 'px' : null, right: pos.right != null ? pos.right + 'px' : null, 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: (pos.maxH || 300) + 'px', overflowY: 'auto' } },
2817
2860
  // ① 板级模型(从池配置弹层搬入,组件与写通道逐字不变)
2818
2861
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '板级模型'),
2819
2862
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
@@ -2875,13 +2918,15 @@ function apply(ctx) {
2875
2918
  // 界面缩放补偿(task-mv58e01j):根容器挂了 CSS zoom 后,getBoundingClientRect 返回缩放后的
2876
2919
  // 可视坐标,而 fixed 弹层在 zoom 子树内 top/right/maxHeight 会再被缩一次 → 各项除以档位还原成
2877
2920
  // CSS px,弹层仍贴着按钮且不溢出视口底(zoom=1 时与旧行为逐字一致)。
2921
+ // 锚点统一走 clampPopoverPos(task-mvt05utl):左溢改左锚——修按钮贴抽屉左缘(窄窗工具栏折行后
2922
+ // ⚙ 落到第二行左端)时弹层左半探出抽屉/视口被裁(2026-10-29 截图病灶本体)。
2878
2923
  var z = state.uiZoom || 1
2879
- if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos({ top: (r.bottom + 4) / z, right: Math.max(8, window.innerWidth - r.right) / z, maxH: Math.max(140, (window.innerHeight - r.bottom - 16) / z) }) }
2924
+ if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos(clampPopoverPos(r, POOL_POPOVER_W, z, popoverBounds(btnRef.current))) }
2880
2925
  setOpen(!open)
2881
2926
  }
2882
2927
  return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
2883
2928
  React.createElement('button', { ref: btnRef, onClick: toggle, title: '池配置', style: { fontSize: 12, padding: '3px 8px', border: '1px solid ' + (open ? C.brand : C.border), borderRadius: 6, cursor: 'pointer', background: open ? C.nested : 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('settings', 13)),
2884
- 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: (pos.maxH || 300) + 'px', overflowY: 'auto' } },
2929
+ open && pos ? React.createElement('div', { style: { position: 'fixed', top: pos.top + 'px', left: pos.left != null ? pos.left + 'px' : null, right: pos.right != null ? pos.right + 'px' : null, 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: (pos.maxH || 300) + 'px', overflowY: 'auto' } },
2885
2930
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
2886
2931
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
2887
2932
  React.createElement(PoolCfg, { label: 'Worker 并发', cfgKey: 'maxWorkers', value: props.maxW }),
package/lib/core.mjs CHANGED
@@ -74,12 +74,19 @@ export function overlapsTouches(mine, theirs) {
74
74
  //(卡 messages + 主窗口通知 + 工具返回体 warning 三通道,接线在 rpc.mjs board_report progress 分支)。
75
75
  export var PLAN_GATE_PREFIX = '计划改动:'
76
76
  // 「计划改动:」前缀识别 + 路径清单解析:换行/逗号/分号/顿号分隔(中英文逗号都认),兼容 markdown
77
- // 列表符(- / * / •)与行内代码反引号包裹;每条先剥行尾注释再过 normTouch 归一(Windows 反斜杠、./ 前缀、尾斜杠),
78
- // 去重保序。全角/半角冒号都识别。
79
- // 行尾注释剥离(task-mvnrtpxo,task-mvnnkv7x-2 误报实证):Worker 自然写法「path(L663 语义注释)」
80
- // 整行被当路径 → 与 touches 不匹配 → 假越界警示。每条目截掉首个全角/半角括号起始的尾巴(/[((].*$/),
81
- // 顺带兼容行内「 — 」说明尾巴;剥光后空串条目丢弃。边界:括号内含路径分隔符的极端形态不处理
82
- //(Windows 路径带括号罕见,接受)。
77
+ // 列表符(- / * / • / ·)与行内代码反引号包裹;每条先取「行首路径前缀」(见下)、过像路径判定
78
+ //(纯散文行丢弃,task-mvqfl7in)再过 normTouch 归一(Windows 反斜杠、./ 前缀、尾斜杠),去重保序。
79
+ // 全角/半角冒号都识别。
80
+ // 路径前缀截断(task-mvnrtpxo/task-mvnnkv7x-2 误报实证 → task-mvr44s0y 收紧):Worker 自然写法
81
+ // 「path(L663 语义注释)」「path — 说明」「path 说明文字」一律截到首个空白/全角标点前的路径前缀
82
+ // ——路径本体不含空格与全角标点(,;、:()「」等),唯一例外是反引号包裹的路径(取首对反引号
83
+ // 内容,可含空格);半角括号沿用行尾注释语义(文件名带括号罕见,接受);半角冒号不在截断类内
84
+ //(Windows 盘符 C:\ 合法)。截断类含括号与空白,是旧「括号/破折号尾巴剥离」的严格超集。
85
+ // 像路径收紧(task-mvr44s0y,n-mvqfiofr0kt9 追加样本两类残留):①全角标点拆段——中文逗号/顿号
86
+ // 分隔的长说明段(散文在前、路径碎片在后)截完是散文首词,过不了像路径判定 → 整条不进清单;
87
+ // ②含路径碎片的散文段(如「故障层复用 test/helpers/fault-provider.mjs 可变目录」)→ 整条丢弃
88
+ //(「取路径前缀或丢弃」二选一取丢弃,断言锁死)。两类都靠「前缀必须像路径」收口,条目全空走
89
+ // rpc.mjs 既有「未解析到路径 → 重报提示」自纠正;路径在前的混合行正常截出前缀参与比对。
83
90
  // 返回 null = 非计划闸口上报(普通进展);返回数组 = 计划清单(空数组 = 有前缀但没解析到路径)。
84
91
  export function parsePlanFiles(question) {
85
92
  var q = String(question == null ? '' : question).trim()
@@ -88,11 +95,27 @@ export function parsePlanFiles(question) {
88
95
  var parts = q.slice(m[0].length).split(/[\n,,;;、]+/)
89
96
  var out = []
90
97
  for (var i = 0; i < parts.length; i++) {
91
- // 顺序:列表符 → 行尾注释 → 反引号 → trim → 归一(注释须先于反引号剥,否则「`path`(注释)」形态残留尾反引号)
92
- var v = parts[i].trim().replace(/^[-*•]\s+/, '')
93
- v = v.replace(/[((].*$/, '').replace(/\s+—\s+.*$/, '').trim() // 剥行尾注释再归一
94
- v = normTouch(v.replace(/^`+|`+$/g, '').trim())
95
- if (v && out.indexOf(v) < 0) out.push(v)
98
+ // 顺序:列表符 → 引号对提取/首空白全角标点截断 → 残留反引号 → trim → 像路径判定 → 归一
99
+ var v = parts[i].trim().replace(/^[-*•·]\s+/, '')
100
+ // 引号包裹路径(「除非引号包裹」例外分支,行内代码反引号):取首对反引号内容为条目、可含
101
+ // 空格,配对后尾巴(说明/注释)一并忽略;无配对落空走下方截断兜底(残留端点反引号仍剥)
102
+ var bt = /^`+([^`]+)`+/.exec(v)
103
+ if (bt) { v = bt[1] } else {
104
+ // 截到首个空白/全角标点前的路径前缀(task-mvr44s0y):截完是「行首前缀」——路径在前的
105
+ // 混合行(「path(注释)」「path — 说明」「path 说明文字」)截出路径本体;散文段(说明在
106
+ // 前、路径碎片在后)截完是散文首词,过不了像路径判定 → 整条丢弃(选定语义,断言锁死)
107
+ var cut = v.search(/[\s,;、:()()「」『』《》〈〉【】〔〕“”‘’…—·~!?]/)
108
+ if (cut >= 0) v = v.slice(0, cut)
109
+ }
110
+ v = v.replace(/^`+|`+$/g, '').trim()
111
+ // 像路径判定(task-mvqfl7in,n-mvqfiofr0kt9 误报实证):无路径特征的纯散文行(如「调研文件
112
+ // 清单」「只动解析不动闸口比对」及截断后的散文首词)不入选;含路径分隔符('/' 或 Windows
113
+ // 反斜杠——normTouch 同源等价)或以代码扩展名结尾(宽正则,.mjs/.md/.json/.py/.yml 等)才
114
+ // 入选;条目全被丢光走既有「空清单」语义(rpc.mjs 有前缀但零路径 → 重报提示自纠正)。
115
+ // 判定在归一前做:`lib/`、`D:\dir` 的分隔符证据会被 normTouch 剥尾斜杠/改写,先记再归一。
116
+ var likePath = /[\/\\]/.test(v) || /\.[a-z0-9]{1,6}$/i.test(v)
117
+ v = normTouch(v)
118
+ if (v && likePath && out.indexOf(v) < 0) out.push(v)
96
119
  }
97
120
  return out
98
121
  }
@@ -313,7 +336,9 @@ export function modelHealthView(d, bucket, opts) {
313
336
  var sid = String(opts.sid || ''), now = Number(opts.now) || Date.now(), out = {}
314
337
  for (var i = 0; i < ids.length; i++) {
315
338
  var id = ids[i], raw = health[id] || {}
316
- var state = raw.observed === 'healthy' ? 'healthy' : 'unknown'
339
+ // task-mvrqdw4k:degraded 派生(公开视图与内部 syncModelHealth 同口径;circuit/cooldown 覆盖在下方,优先级不变)——
340
+ // get-tasks 是 UI 健康点唯一数据源,不透出 degraded 则 GUI 永远回退 ⚪未知、降级态不可见。
341
+ var state = raw.observed === 'healthy' ? 'healthy' : (raw.observed === 'degraded' ? 'degraded' : 'unknown')
317
342
  var note = raw.observedNote || raw.note || '尚无健康信号'
318
343
  var until = Number(cooldown[id]) || 0, cooling = until > now
319
344
  if (cooling) { state = 'cooldown'; note = raw.cooldownNote || '模型冷却中' }