dsh-agent-board 1.8.4 → 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.
Files changed (42) hide show
  1. package/README.md +35 -6
  2. package/index.mjs +6 -1
  3. package/lib/client/board-list.js +51 -7
  4. package/lib/client/dashboard.js +316 -26
  5. package/lib/client/kernel.js +41 -6
  6. package/lib/client/task-detail.js +16 -9
  7. package/lib/client.js +424 -48
  8. package/lib/core.mjs +303 -21
  9. package/lib/dispatch.mjs +1045 -25
  10. package/lib/notify.mjs +37 -4
  11. package/lib/policy.mjs +187 -7
  12. package/lib/rpc.mjs +374 -24
  13. package/lib/store.mjs +37 -0
  14. package/lib/usage.mjs +34 -4
  15. package/package.json +1 -1
  16. package/test/core.test.mjs +681 -44
  17. package/test/decompose.test.mjs +648 -0
  18. package/test/fault-e2e-circuit.test.mjs +187 -0
  19. package/test/fault-e2e-fallback-chain.test.mjs +249 -0
  20. package/test/fault-e2e-hung.test.mjs +504 -0
  21. package/test/fault-e2e-recovery.test.mjs +255 -0
  22. package/test/fault-e2e-softgate.test.mjs +206 -0
  23. package/test/fault-e2e-stale-dir.test.mjs +226 -0
  24. package/test/fault-injection-docs.test.mjs +114 -0
  25. package/test/fault-provider.test.mjs +458 -0
  26. package/test/helpers/fault-provider.mjs +475 -0
  27. package/test/helpers/mock-ctx.mjs +15 -1
  28. package/test/hung-frame-signal.test.mjs +630 -0
  29. package/test/integration.test.mjs +557 -0
  30. package/test/lane-gui-bysize.test.mjs +835 -0
  31. package/test/lane-models.test.mjs +728 -0
  32. package/test/onboarding-banner.test.mjs +88 -0
  33. package/test/ops-log.test.mjs +138 -0
  34. package/test/popover-clamp.test.mjs +92 -0
  35. package/test/rate-limit-cooldown.test.mjs +308 -0
  36. package/test/readme-decompose-docs.test.mjs +112 -0
  37. package/test/settle-stop-interrupt.test.mjs +510 -0
  38. package/test/size-hint.test.mjs +192 -0
  39. package/test/spawn-activation.test.mjs +211 -0
  40. package/test/store-data-safety.test.mjs +102 -0
  41. package/test/ui-zoom-client.test.mjs +9 -4
  42. package/test/verify-timing.test.mjs +94 -0
@@ -2,7 +2,7 @@
2
2
  // dashboard —— 仪表盘域:统计(computeStats/TrendChart/StatCard/BarRow/ResearchRoiRow)· 范围筛选(RangeFilter)
3
3
  // · 全局总览(GlobalBoards)· 报告(buildReport/ReportButton)· Token 消耗(TokenUsage)
4
4
  // · 架构健康(HealthHints,架构自省 L1)
5
- // · 团队池(TeamView/PoolStatus)· 设置(WorkModeSwitch/PoolCfg/MinCfg/ModelCfg/PoolCfgPopover)
5
+ // · 团队池(TeamView/PoolStatus)· 设置(WorkModeSwitch/PoolCfg/MinCfg/ModelCfg/OrderedModelsEditor/PoolCfgPopover/ModelRoutePopover)
6
6
  // 本文件是 apply(ctx) 函数体片段,不可独立运行;由 scripts/build-client.cjs 按序拼接生成 lib/client.js。
7
7
  // ============================================================================
8
8
 
@@ -508,6 +508,44 @@
508
508
  { k: 'resumeRate', label: '续跑率', title: '续跑率 = resume:true 续跑记录 / 桶内 runs', fmt: function (b) { return mpPct(b.resumeRate) } },
509
509
  { k: 'cacheHitRate', label: '缓存命中率', title: '缓存命中率 = cacheRead / (input + cacheRead);⚠️ 跨 provider 口径可能不一(input 是否含缓存读各家不同),对比前先核口径;无 usage 样本 → —', fmt: function (b) { return mpPct(b.cacheHitRate) } }
510
510
  ]
511
+ // ===== 按档位小表(task-mvneq4mw):行=力度三档(轻/标/重),列=runs/驳回/异常/token =====
512
+ // 数据源 sb.bySize(host 装配层 assembleScoreboard 按 run.size 留档归桶;无 size 老 run 归标档,零迁移)。
513
+ // 口径:驳回 = 归因到该档 worker run 的验收驳回;异常 = 超时落定 + 跑崩落定(行 title 拆分);
514
+ // token = 有效口径(输入+输出+缓存写,与本区八指标同),行 title 附总量(含缓存读);随统计范围裁剪。
515
+ // 只认 worker/verifier run(hook 恒父模型、不进档位统计);全部档位零 runs 时整块不渲染(不画空表)。
516
+ var LANE_LABEL = { light: '轻档', standard: '标档', heavy: '重档' }
517
+ function LaneTable(props) {
518
+ var bySize = props.bySize
519
+ if (!bySize) return null
520
+ var lanes = ['light', 'standard', 'heavy']
521
+ var totalRuns = 0
522
+ for (var li = 0; li < lanes.length; li++) totalRuns += (bySize[lanes[li]] && bySize[lanes[li]].runs) || 0
523
+ if (!totalRuns) return null
524
+ var thL2 = { fontSize: 9, color: C.text2, fontWeight: 600, textAlign: 'left', padding: '2px 4px', borderBottom: '1px solid ' + C.border, whiteSpace: 'nowrap' }
525
+ var thR2 = { fontSize: 9, color: C.text2, fontWeight: 600, textAlign: 'right', padding: '2px 4px', borderBottom: '1px solid ' + C.border, whiteSpace: 'nowrap', cursor: 'help' }
526
+ var tdL2 = { fontSize: 10, color: C.text, textAlign: 'left', padding: '3px 4px', borderBottom: '1px solid ' + C.nested, whiteSpace: 'nowrap' }
527
+ var tdR2 = { fontSize: 10, color: C.text, textAlign: 'right', padding: '3px 4px', borderBottom: '1px solid ' + C.nested, whiteSpace: 'nowrap' }
528
+ return React.createElement('div', { style: { marginTop: 8 } },
529
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '按档位'),
530
+ React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse' } },
531
+ React.createElement('thead', null, React.createElement('tr', null,
532
+ React.createElement('th', { style: thL2 }, '档位'),
533
+ React.createElement('th', { style: thR2, title: '该档已落定 Worker/Verifier run 数(hook 不计入)' }, 'runs'),
534
+ React.createElement('th', { style: thR2, title: '驳回 = 归因到该档 worker run 的验收驳回次数' }, '驳回'),
535
+ React.createElement('th', { style: thR2, title: '异常 = 超时落定(timeout/error)+ 跑崩落定(incomplete/error/failed)的 run 数;terminated 人工终止不计入' }, '异常'),
536
+ React.createElement('th', { style: thR2, title: 'token = 有效口径合计(输入+输出+缓存写,不含缓存读);行悬浮见总量(含缓存读)' }, 'token'))),
537
+ React.createElement('tbody', null, lanes.map(function (ln) {
538
+ var b = bySize[ln] || { runs: 0, rejected: 0, failed: 0, timeouts: 0, tokSum: 0, effSum: 0 }
539
+ var tip = LANE_LABEL[ln] + ':runs=' + b.runs + ';驳回 ' + b.rejected + ';异常 ' + (b.failed + b.timeouts) + '(超时 ' + b.timeouts + ' + 跑崩 ' + b.failed + ');token 有效 ' + fmtTokens(b.effSum) + ' · 总量 ' + fmtTokens(b.tokSum) + '(含缓存读);老 run 无 size 归标档'
540
+ return React.createElement('tr', { key: ln, title: tip },
541
+ React.createElement('td', { style: tdL2 }, LANE_LABEL[ln]),
542
+ React.createElement('td', { style: tdR2 }, String(b.runs)),
543
+ React.createElement('td', { style: tdR2 }, String(b.rejected)),
544
+ React.createElement('td', { style: tdR2 }, String(b.failed + b.timeouts)),
545
+ React.createElement('td', { style: tdR2 }, fmtTokens(b.effSum)))
546
+ }))),
547
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 4, lineHeight: 1.5 } }, '口径:行 = 力度档位(run.size 派发留档,老 run 无 size 归标档);只计 Worker/Verifier run(hook 恒父模型不计入);驳回 = 归因到该档 worker run 的验收驳回;异常 = 超时 + 跑崩;token = 有效口径(悬浮行看总量);随统计范围裁剪'))
548
+ }
511
549
  function ModelPerf(props) {
512
550
  var _R = React; var useState = _R.useState
513
551
  var _a = useState(state.mpRole || ''), roleF = _a[0], setRoleF = _a[1]
@@ -522,8 +560,9 @@
522
560
  tgOn ? React.createElement('span', { style: { fontSize: 9, fontWeight: 400, color: C.brand, border: '1px solid ' + C.brand, borderRadius: 8, padding: '0 6px' }, title: '本区桶数据已按统计范围过滤(与 Token 区同一 range 口径):' + rangeLabel() }, '范围内: ' + rangeLabel()) : null)
523
561
  var sb = props.sb
524
562
  var buckets = (sb && Array.isArray(sb.buckets)) ? sb.buckets : []
525
- // 空态:无 scoreboard(老 host)/ 无入桶 run → 一句话空态(锚 id 照挂,Token 区链接始终可达)
526
- if (!buckets.length) return React.createElement('div', { id: 'tskb-model-perf', style: box }, head, React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无足够 run 数据(还没有可入桶的已落定 Worker/Verifier run 留账)'))
563
+ // 空态:无 scoreboard(老 host)/ 无入桶 run → 一句话空态(锚 id 照挂,Token 区链接始终可达);
564
+ // 按档位小表仍渲染(它不要求学模型名——无模型老 run 也烧了档位 token,档位视图可能恰好还有数据)
565
+ if (!buckets.length) return React.createElement('div', { id: 'tskb-model-perf', style: box }, head, React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无足够 run 数据(还没有可入桶的已落定 Worker/Verifier run 留账)'), React.createElement(LaneTable, { bySize: sb && sb.bySize }))
527
566
  // 本地筛选:角色 × 规模段,纯数组 pass 不重拉
528
567
  var shown = buckets.filter(function (b) { return (!roleF || b.role === roleF) && (!sizeF || b.size === sizeF) })
529
568
  function chip(cur, setter, v, label) {
@@ -561,7 +600,9 @@
561
600
  React.createElement('td', { style: tdR }, String(b.runs)),
562
601
  MP_COLS.map(function (c) { return React.createElement('td', { key: c.k, style: tdR }, c.fmt(b)) }))
563
602
  })))),
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)'))
603
+ 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)'),
604
+ // 按档位小表(task-mvneq4mw):三档 × runs/驳回/异常/token,挂在八指标表下方
605
+ React.createElement(LaneTable, { bySize: sb && sb.bySize }))
565
606
  }
566
607
 
567
608
  // ===== 质量趋势区(记分卡卡4,task-muxhtgil):一次通过率趋势 / 卡时长分布 / 超时率走势 / 续跑成功率 =====
@@ -882,7 +923,7 @@
882
923
  function commit(v) {
883
924
  var n = parseInt(v, 10)
884
925
  if (isNaN(n) || n < props.min || n > props.max) { setVal(String(props.value)); setDirty(false); return }
885
- setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(fetchTasks).catch(function () {})
926
+ setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(function (r) { if (typeof props.onSaved === 'function') props.onSaved(r); return fetchTasks() }).catch(function () {})
886
927
  }
887
928
  return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } },
888
929
  props.label,
@@ -921,20 +962,48 @@
921
962
  dirty ? React.createElement('button', { onClick: function () { commit(val) }, title: '应用', 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)
922
963
  }
923
964
 
965
+ // ===== 取模来源角标(task-mvnyc8vo,用户「能看到子 agent 当前在用什么模型跑」)=====
966
+ // board/parent=常规继承路径不标(绝大多数,标了反成噪音);其余路径给小角标,一眼看出本次取模走了哪条路:
967
+ // card=卡级特例「卡」/ lane=档位路由「档」/ fallback=备用模型链「备」/ cooldown-downgrade=冷却降档「降」。
968
+ var MODEL_SRC_BADGE = { card: '卡', lane: '档', fallback: '备', 'cooldown-downgrade': '降' }
969
+ var MODEL_SRC_LABEL = { card: '卡级特例', lane: '档位路由', board: '板级模型', parent: '继承父模型', fallback: '备用模型链', 'cooldown-downgrade': '冷却降档' }
970
+ function modelSourceBadge(src) {
971
+ if (!src || src === 'board' || src === 'parent') return null
972
+ return React.createElement('span', { title: '取模来源:' + (MODEL_SRC_LABEL[src] || src), style: { fontSize: 8, padding: '0 3px', borderRadius: 3, border: '1px solid ' + C.warn, color: C.warn, lineHeight: '11px', flexShrink: 0 } }, MODEL_SRC_BADGE[src] || src)
973
+ }
974
+
924
975
  function PoolStatus() {
925
976
  var ps = state.poolStatus
926
977
  if (!ps) return null
927
- var wActive = (ps.workers || []).filter(function (w) { return w.busy }).length
978
+ var wBusy = (ps.workers || []).filter(function (w) { return w.busy })
979
+ var vBusy = (ps.verifiers || []).filter(function (v) { return v.busy })
980
+ var wActive = wBusy.length
928
981
  var wTotal = state.maxWorkers // 分母=并发上限而非当前池实例数,空闲也显示 0/上限
929
- var vActive = (ps.verifiers || []).filter(function (v) { return v.busy }).length
982
+ var vActive = vBusy.length
930
983
  var vTotal = state.maxVerifiers
931
- return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
984
+ // 拆解官可见性(task-mvomad6c ①):decomposer 与 hook 同归 verifiers 桶,靠条目 role 择出——
985
+ // 在跑时于 Verifier 计数旁独立亮「🧩 拆解中 ×N」。Verifier x/y 计数口径不变(并发位占用语义:
986
+ // decomposer 计 verifier 位,剔掉会让水位失真);缺 role 字段的老快照条目零渲染(能力检测)。
987
+ var decBusy = vBusy.filter(function (v) { return v.role === 'decomposer' })
988
+ // 在跑子代理模型可见(task-mvnyc8vo):每个 busy run 渲染 model 小芯片 + 取模来源角标,
989
+ // 与执行池成员卡同口径;缺 model 字段的老数据条目不渲染芯片。
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', flexWrap: 'wrap' } },
932
992
  // 池水位标注(反馈 n-mut9rzoq3flu / n-muyg4wl9208c):裸数字第一屏无解,补可见文字标签 + title
933
993
  React.createElement('span', { title: '活跃 Worker 数/上限(⚡ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), 'Worker ' + wActive + '/' + wTotal),
934
- React.createElement('span', { title: '活跃 Verifier 数/上限(✓ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), 'Verifier ' + vActive + '/' + vTotal))
994
+ React.createElement('span', { title: '活跃 Verifier 数/上限(✓ = 在跑;拆解官在跑也占 Verifier 并发位,见右侧 🧩 计数)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), 'Verifier ' + vActive + '/' + vTotal),
995
+ decBusy.length ? React.createElement('span', { title: '拆解官运行中(占 Verifier 并发位,源卡:' + decBusy.map(function (m) { return m.taskId || '-' }).join('、') + ')——读码产出子任务提案,落定后源卡亮「🧩 已提案」徽标', style: { display: 'inline-flex', alignItems: 'center', gap: 2, color: C.brand, fontWeight: 600 } }, '🧩 拆解中 ×' + decBusy.length) : null,
996
+ chips.map(function (m) {
997
+ return React.createElement('span', { key: m.id, title: '在跑模型:' + m.model + '(任务 ' + (m.taskId || '-') + (m.modelSource ? ',取模来源:' + (MODEL_SRC_LABEL[m.modelSource] || m.modelSource) : '') + ')', style: { display: 'inline-flex', alignItems: 'center', gap: 2, padding: '0 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, maxWidth: 150 } },
998
+ React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, m.model),
999
+ modelSourceBadge(m.modelSource))
1000
+ }))
935
1001
  }
936
1002
 
937
- // #11 头部减负:池配置收纳进 ⚙️ 弹出层(含 #17 verifier 异构模型设置)
1003
+ // #11 头部减负:池配置收纳进 ⚙️ 弹出层(并发/超时/开关);模型区(含 #17 verifier 异构模型设置、
1004
+ // 力度路由六槽、备用链)2026-10-25 起搬至「模型路由」独立弹层 ModelRoutePopover(task-mvnyc8vo)。
1005
+ // 六槽/备用链 2026-10-26 起升级为 OrderedModelsEditor 有序链编辑器(task-mvokikl3:下拉添加+芯片 ×/↑ 排序,
1006
+ // 槽位读写整个有序数组)——原「GUI 只编首选」的 laneFirst 单选形态退役。
938
1007
  function ModelCfg(props) {
939
1008
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect
940
1009
  var _a = useState(props.value || ''), val = _a[0], setVal = _a[1]
@@ -942,6 +1011,10 @@
942
1011
  var _m = useState(null), models = _m[0], setModels = _m[1]
943
1012
  var cfgKey = props.cfgKey || 'verifierModel'
944
1013
  var label = props.label || 'Verifier 模型'
1014
+ // 只服务板级 Worker/Verifier 模型行(两段式单选,空=「继承父级」)。力度路由档位槽曾复用本组件
1015
+ // (task-mvneq4mw lane 分支),2026-10-26 起随链式编辑器升级迁往 OrderedModelsEditor(task-mvokikl3)——
1016
+ // 档位槽走 laneModels 部分合并写通道的职责随编辑器搬走,本组件不再承担。
1017
+ var inhLabel = props.inheritLabel || '继承父级'
945
1018
  // 下拉列出当前网关可用模型(host list-models RPC);枚举失败降级为文本输入
946
1019
  useEffect(function () {
947
1020
  var cancelled = false
@@ -949,21 +1022,24 @@
949
1022
  return function () { cancelled = true }
950
1023
  }, [])
951
1024
  useEffect(function () { setVal(props.value || ''); setDirty(false) }, [props.value])
952
- function save() { setDirty(false); rpc('set-board-config', { key: cfgKey, value: (val || '').trim() }).then(fetchTasks).catch(function () {}) }
1025
+ function save() {
1026
+ setDirty(false)
1027
+ rpc('set-board-config', { key: cfgKey, value: (val || '').trim() }).then(fetchTasks).catch(function () {})
1028
+ }
953
1029
  // 下拉两段式(task-mv9yuq2l,实证 n-mv8ki92qnhdq:巡检戳即点即写 → verifierModel 神隐回退无从审计):
954
1030
  // 选中只改本地草稿态 + 亮 dirty(对比服务端权威值 props.value),点旁边 ✓ 保存键才走 save() 提交 rpc——
955
1031
  // 与下方降级文本输入同款两段式语义,巡检路过戳一下不再直接写配置。
956
1032
  function pick(v) { setVal(v); setDirty(v !== (props.value || '')) }
957
1033
  if (models) {
958
1034
  return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, 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 } },
960
- React.createElement('option', { value: '' }, '继承父级'),
1035
+ React.createElement('select', { value: val, onChange: function (e) { pick(e.target.value) }, title: label + '(空=' + inhLabel + ';选中后点 ✓ 才保存)', style: { width: 150, padding: '0px 4px', fontSize: 9, border: '1px solid ' + (dirty ? C.brand : C.border), borderRadius: 2, background: C.card, color: C.text } },
1036
+ React.createElement('option', { value: '' }, inhLabel),
961
1037
  models.map(function (m) { return React.createElement('option', { key: m.id, value: m.id }, m.name + ' (' + m.provider + ')') })),
962
1038
  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)
963
1039
  }
964
1040
  // 降级:文本输入(模型枚举不可用时)
965
1041
  return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
966
- React.createElement('input', { value: val, onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== (props.value || '')) }, onBlur: function () { if (dirty) save() }, onKeyDown: function (e) { if (e.key === 'Enter') save() }, placeholder: '空=同父级', style: { width: 110, padding: '0px 4px', fontSize: 9, border: '1px solid ' + (dirty ? C.brand : C.border2), borderRadius: 2, background: C.card, color: C.text } }),
1042
+ React.createElement('input', { value: val, onChange: function (e) { setVal(e.target.value); setDirty(e.target.value !== (props.value || '')) }, onBlur: function () { if (dirty) save() }, onKeyDown: function (e) { if (e.key === 'Enter') save() }, placeholder: '空=' + inhLabel, style: { width: 110, padding: '0px 4px', fontSize: 9, border: '1px solid ' + (dirty ? C.brand : C.border2), borderRadius: 2, background: C.card, color: C.text } }),
967
1043
  dirty ? React.createElement('button', { onClick: save, title: '应用', 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)
968
1044
  }
969
1045
 
@@ -984,6 +1060,215 @@
984
1060
  UI_ZOOM_LEVELS.map(function (z) { return React.createElement('option', { key: String(z), value: String(z) }, Math.round(z * 100) + '%' + (z === 1 ? '(默认)' : '')) })))
985
1061
  }
986
1062
 
1063
+ // 首用引导横幅重开入口(task-mvnnkv7x-2):✕ 关闭后此前无任何重开通道(kernel.js 仅 localStorage.setItem
1064
+ // 写键,title 写死「不再显示」)。照 ZoomCfg 同族纯客户端偏好范式:localStorage.removeItem(ONBOARD_DISMISS_KEY)
1065
+ // + state.onboardDismissed=false + notify——TopPanel update 链 setObd 订阅即时重渲染出横幅,无需刷新。
1066
+ // 仅关闭态(state.onboardDismissed=true)有意义:组件内自守 + 「界面」小节挂载点双闸,未关闭时不渲染。
1067
+ function OnboardReset() {
1068
+ if (!state.onboardDismissed) return null
1069
+ function reopen() {
1070
+ try { localStorage.removeItem(ONBOARD_DISMISS_KEY) } catch (_) {}
1071
+ state.onboardDismissed = false; notify()
1072
+ }
1073
+ return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, '引导横幅',
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
+ }
1076
+
1077
+ // ===== 模型健康点(task-mvoulhm7):🟢健康 / 🟡降级(链路存活,task-mvrqdw4k 空响应分治) / 🟡冷却中至HH:mm / 🔴熔断 / ⚪未知 =====
1078
+ // 数据只来自 get-tasks.modelHealth;title 悬浮展示探测/run 时间、最近延迟与近 5 次 run 聚合。
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 } }
1081
+ function healthHhmm(ts) { var d = new Date(Number(ts)); return isNaN(d.getTime()) ? '' : ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2) }
1082
+ function healthTimeText(v) { if (!v) return ''; var d = new Date(v); return isNaN(d.getTime()) ? String(v) : d.toLocaleString() }
1083
+ function ModelHealthDot(props) {
1084
+ var h = (props.health && props.health[props.model]) || { state: 'unknown', note: '尚无健康信号' }
1085
+ var meta = MODEL_HEALTH_META[h.state] || MODEL_HEALTH_META.unknown
1086
+ var untilText = h.state === 'cooldown' && h.until ? healthHhmm(h.until) : ''
1087
+ var label = meta.label + (untilText ? '至 ' + untilText : '')
1088
+ var lines = [props.model + ':' + label]
1089
+ if (h.note) lines.push(String(h.note))
1090
+ if (h.lastProbe) lines.push('最近探测:' + healthTimeText(h.lastProbe))
1091
+ if (h.lastSeen) lines.push('最近信号:' + healthTimeText(h.lastSeen))
1092
+ if (typeof h.latencyMs === 'number') lines.push('最近延迟:' + h.latencyMs + 'ms')
1093
+ if (Array.isArray(h.recentRuns) && h.recentRuns.length) lines.push('近 ' + h.recentRuns.length + ' 次 run:' + h.recentRuns.filter(function (r) { return r.ok }).length + ' 次成功')
1094
+ return React.createElement('span', { role: 'img', 'aria-label': props.model + ' ' + label, title: lines.join('\n'), style: { color: meta.color, cursor: 'help', flexShrink: 0, fontSize: 9, lineHeight: '12px' } }, meta.icon)
1095
+ }
1096
+
1097
+ // ===== 有序模型链编辑器(task-mvokikl3 链式编辑器:下拉添加 + 芯片排序,替代首选单选与文本框)=====
1098
+ // 弹层内复用 7 处(力度路由六槽 + 备用模型链):当前链渲染为模型芯片行(每片 × 移除 + ↑ 前移一位),
1099
+ // 尾部下拉(合法目录全量、过滤已在链中的)+「添加」追加到链尾;空链显示 props.emptyHint 占位语义
1100
+ // (六槽=「继承板级」、备用链=「无(不启用)」)。改动即写:props.onSave(nextArr) 返回 rpc Promise——
1101
+ // 复用既有 set-board-config 通道(laneModels 槽位写**整个有序数组** / fallbackModels 写**整个数组**,
1102
+ // 零 RPC 改动),host 逐项合法清单校验、非法整体拒写(ok:false 亮 ⚠️ 不静默吞);写成功后 fetchTasks 回读权威值。
1103
+ // props.models=null(目录枚举失败/老宿主)时降级为文本输入 + 添加,不丢功能。
1104
+ function OrderedModelsEditor(props) {
1105
+ var _R = React; var useState = _R.useState
1106
+ // 读侧容错:老 host 单字符串形态归一为单元素数组;非字符串/空串项滤掉(host 归一后恒为干净数组,此处只是防御)
1107
+ var list = Array.isArray(props.value) ? props.value.filter(function (x) { return typeof x === 'string' && x }) : (typeof props.value === 'string' && props.value ? [props.value] : [])
1108
+ var _p = useState(''), pickVal = _p[0], setPickVal = _p[1]
1109
+ var _m = useState(''), msg = _m[0], setMsg = _m[1]
1110
+ var models = Array.isArray(props.models) ? props.models : null
1111
+ // 下拉过滤已在链中的(链内唯一,重复添加无意义)
1112
+ var avail = models ? models.filter(function (m) { return list.indexOf(m.id) < 0 }) : null
1113
+ function commit(next) {
1114
+ setMsg('')
1115
+ Promise.resolve(props.onSave(next)).then(function (r) {
1116
+ if (r && r.ok === false) { setMsg('⚠️ ' + (r.error || '保存失败')); setTimeout(function () { setMsg('') }, 5000); return }
1117
+ fetchTasks()
1118
+ }).catch(function () { setMsg('⚠️ 保存失败'); setTimeout(function () { setMsg('') }, 3000) })
1119
+ }
1120
+ function removeAt(i) { var next = list.slice(); next.splice(i, 1); commit(next) }
1121
+ function upAt(i) { if (i <= 0) return; var next = list.slice(); var t = next[i - 1]; next[i - 1] = next[i]; next[i] = t; commit(next) }
1122
+ function add() { var v = (pickVal || '').trim(); if (!v || list.indexOf(v) >= 0) return; setPickVal(''); commit(list.concat([v])) }
1123
+ var chipBtn = { fontSize: 9, padding: '0px 2px', border: 'none', background: 'transparent', color: C.text2, cursor: 'pointer', lineHeight: '12px' }
1124
+ return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'flex-start', gap: 4, fontSize: 9, color: C.text2, whiteSpace: 'normal' } },
1125
+ props.label ? React.createElement('span', { style: { paddingTop: 1, minWidth: 62 } }, props.label) : null,
1126
+ React.createElement('span', { style: { display: 'inline-flex', flexWrap: 'wrap', gap: 3, alignItems: 'center', maxWidth: 250 } },
1127
+ list.length === 0 ? React.createElement('span', { style: { color: C.text2, fontStyle: 'italic', paddingTop: 1 } }, props.emptyHint || '无') : null,
1128
+ list.map(function (m, i) {
1129
+ return React.createElement('span', { key: m + '@' + i, title: (i === 0 ? '首选' : '第 ' + (i + 1) + ' 顺位') + ' · 按序取第一个健康模型', style: { display: 'inline-flex', alignItems: 'center', gap: 1, padding: '0px 2px 0px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.nested, color: C.text, whiteSpace: 'nowrap' } },
1130
+ React.createElement(ModelHealthDot, { model: m, health: props.health }),
1131
+ React.createElement('span', null, m),
1132
+ i > 0 ? React.createElement('button', { onClick: function () { upAt(i) }, title: '前移一位(↑)', style: chipBtn }, '↑') : null,
1133
+ React.createElement('button', { onClick: function () { removeAt(i) }, title: '移除(×)', style: chipBtn }, '×'))
1134
+ })),
1135
+ // 尾部:下拉(过滤已选)/ 目录缺失降级文本输入 +「添加」追加链尾
1136
+ avail ? React.createElement('select', { value: pickVal, onChange: function (e) { setPickVal(e.target.value) }, title: '选择要追加到链尾的模型(已在链中的已过滤)', style: { width: 150, padding: '0px 2px', fontSize: 9, border: '1px solid ' + C.border, borderRadius: 2, background: C.card, color: C.text } },
1137
+ React.createElement('option', { value: '' }, '选择模型…'),
1138
+ avail.map(function (m) { return React.createElement('option', { key: m.id, value: m.id }, m.name + ' (' + m.provider + ')') }))
1139
+ : React.createElement('input', { value: pickVal, onChange: function (e) { setPickVal(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') add() }, placeholder: 'provider/model', title: '模型目录不可用,手输 provider/model 后点添加', style: { width: 110, padding: '0px 4px', fontSize: 9, border: '1px solid ' + C.border2, borderRadius: 2, background: C.card, color: C.text } }),
1140
+ React.createElement('button', { onClick: add, disabled: !pickVal, title: '追加到链尾', style: { fontSize: 9, padding: '0px 5px', border: '1px solid ' + C.border, borderRadius: 2, background: C.card, color: pickVal ? C.brand : C.text2, cursor: pickVal ? 'pointer' : 'default', lineHeight: '14px', fontWeight: 600 } }, '添加'),
1141
+ msg ? React.createElement('span', { style: { color: C.err, maxWidth: 140 } }, msg) : null)
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
+
1185
+ // ===== 模型路由独立弹层(task-mvnyc8vo,用户 2026-10-25「模型配置独立弹出卡片」)=====
1186
+ // 模型相关全部配置从池配置弹层**搬走**(不是复制)到本弹层:①板级 Worker/Verifier 模型(ModelCfg
1187
+ // 复用)②力度路由六槽 ③fallbackModels 备用链——②③ 2026-10-26 起统一为 OrderedModelsEditor
1188
+ // 有序链编辑器(task-mvokikl3:下拉添加+芯片 ×/↑ 排序,替代首选单选与文本框);池配置弹层只留并发/超时/开关。
1189
+ // 弹层样式复用 PoolCfgPopover 的 fixed 定位模式(脱离面板 overflow 裁剪 + uiZoom 档位补偿)。
1190
+ function ModelRoutePopover(props) {
1191
+ var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
1192
+ var _a = useState(false), open = _a[0], setOpen = _a[1]
1193
+ var _p = useState(null), pos = _p[0], setPos = _p[1]
1194
+ var ref = useRef(null); var btnRef = useRef(null)
1195
+ // 链编辑器共用模型目录:弹层取一次 list-models 下发 7 处编辑器(六槽+备用链),免每编辑器各发一次 RPC;
1196
+ // 目录不可用(老宿主/枚举失败)时 routeModels 恒 null,编辑器降级为文本输入 + 添加,不丢功能。
1197
+ var _rm = useState(null), routeModels = _rm[0], setRouteModels = _rm[1]
1198
+ // 健康面局部读取(不改 kernel 全局状态):弹层打开即取 + 15s 刷新;关闭时令 epoch 失效,迟到响应丢弃。
1199
+ var _mh = useState({}), modelHealth = _mh[0], setModelHealth = _mh[1]
1200
+ var _pe = useState(true), probeEnabled = _pe[0], setProbeEnabled = _pe[1]
1201
+ var _pi = useState(15), probeIntervalMin = _pi[0], setProbeIntervalMin = _pi[1]
1202
+ var healthEpoch = useRef(0)
1203
+ function refreshModelHealth() {
1204
+ var epoch = healthEpoch.current, args = state.tasksHash ? { ifHash: state.tasksHash } : {}
1205
+ return rpc('get-tasks', args).then(function (d) {
1206
+ if (!d || d.error || epoch !== healthEpoch.current) return d
1207
+ setModelHealth(d.modelHealth || {})
1208
+ setProbeEnabled(d.probeEnabled !== false)
1209
+ setProbeIntervalMin(Number(d.probeIntervalMin) > 0 ? Number(d.probeIntervalMin) : 15)
1210
+ return d
1211
+ }).catch(function () { return null })
1212
+ }
1213
+ useEffect(function () { if (!open) return; refreshModelHealth(); var timer = setInterval(refreshModelHealth, 15000); return function () { clearInterval(timer); healthEpoch.current++ } }, [open])
1214
+ useEffect(function () {
1215
+ var cancelled = false
1216
+ rpc('list-models').then(function (r) { if (!cancelled && r && r.ok && Array.isArray(r.models) && r.models.length) setRouteModels(r.models) }).catch(function () {})
1217
+ return function () { cancelled = true }
1218
+ }, [])
1219
+ // 改动即写(复用既有 set-board-config 通道,零 RPC 改动):六槽=laneModels 部分合并、槽位写**整个有序数组**
1220
+ // (非仅首选——task-mvokikl3 起 GUI 可编完整链);备用链=fallbackModels 整个数组。host 逐项合法清单校验、
1221
+ // 任一非法整体拒写(ok:false 由编辑器亮 ⚠️ 不静默吞);写成功后编辑器侧 fetchTasks 回读权威值。
1222
+ function saveLaneSlot(lane, role, nextArr) { var patch = {}; patch[lane] = {}; patch[lane][role] = nextArr; return rpc('set-board-config', { key: 'laneModels', value: patch }) }
1223
+ function saveFallbackChain(nextArr) { return rpc('set-board-config', { key: 'fallbackModels', value: nextArr }) }
1224
+ function saveProbeEnabled(next) { setProbeEnabled(next); return rpc('set-board-config', { key: 'probeEnabled', value: next }).then(refreshModelHealth).catch(refreshModelHealth) }
1225
+ useEffect(function () { if (!open) return; function onDown(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }; document.addEventListener('mousedown', onDown); return function () { document.removeEventListener('mousedown', onDown) } }, [open])
1226
+ function toggle() {
1227
+ // 与 PoolCfgPopover 同口径:fixed + 视口坐标(面板 overflow:hidden 会裁 absolute 弹层);
1228
+ // 界面缩放补偿——zoom 子树内 getBoundingClientRect 返回缩放后坐标,各项除以档位还原 CSS px。
1229
+ // 锚点统一走 clampPopoverPos(task-mvt05utl):左溢改左锚,修按钮贴抽屉左缘时弹层左探被裁。
1230
+ var z = state.uiZoom || 1
1231
+ if (!open && btnRef.current) { var r = btnRef.current.getBoundingClientRect(); setPos(clampPopoverPos(r, ROUTE_POPOVER_W, z, popoverBounds(btnRef.current))) }
1232
+ setOpen(!open)
1233
+ }
1234
+ return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
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)),
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' } },
1237
+ // ① 板级模型(从池配置弹层搬入,组件与写通道逐字不变)
1238
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '板级模型'),
1239
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
1240
+ React.createElement(ModelCfg, { label: 'Worker 模型', cfgKey: 'workerModel', value: props.workerModel }),
1241
+ props.workerModel ? React.createElement(ModelHealthDot, { model: props.workerModel, health: modelHealth }) : null,
1242
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Worker 执行模型)')),
1243
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
1244
+ React.createElement(ModelCfg, { label: 'Verifier 模型', cfgKey: 'verifierModel', value: props.verifierModel }),
1245
+ props.verifierModel ? React.createElement(ModelHealthDot, { model: props.verifierModel, health: modelHealth }) : null,
1246
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)')),
1247
+ // 主动健康嗅探配置:只打本页已配置模型;关掉后冷却/熔断/run 被动健康面仍然保留。
1248
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '健康嗅探(事前调度)'),
1249
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', whiteSpace: 'normal', maxWidth: 360 } },
1250
+ React.createElement('label', { style: { display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 9, color: C.text, cursor: 'pointer' }, title: '每间隔对板级/档位/备用链中已配置模型发一次 maxTokens=1 的极短 ping;不创建子代理' },
1251
+ React.createElement('input', { type: 'checkbox', checked: !!probeEnabled, onChange: function (e) { saveProbeEnabled(e.target.checked) } }), '主动探测'),
1252
+ React.createElement(MinCfg, { label: '间隔', cfgKey: 'probeIntervalMin', value: probeIntervalMin, min: 1, max: 1440, tip: '主动健康探测间隔(1~1440 分钟,默认 15)', onSaved: refreshModelHealth }),
1253
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '仅已配置模型 · 关闭仍保留被动健康')),
1254
+ // ② 力度路由六槽(链编辑器,task-mvokikl3):三档 × Worker/Verifier 各一条有序模型链——
1255
+ // 读写整个槽位数组(非仅首选);空链=继承上方板级模型行。
1256
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '力度路由'),
1257
+ [['light', '轻档'], ['standard', '标档'], ['heavy', '重档']].map(function (ln) {
1258
+ var laneObj = (props.laneModels && props.laneModels[ln[0]]) || {}
1259
+ return React.createElement('div', { key: ln[0], style: { marginBottom: 4 } },
1260
+ React.createElement('div', { style: { marginBottom: 2 } },
1261
+ React.createElement(OrderedModelsEditor, { label: ln[1] + ' Worker', value: laneObj.worker, models: routeModels, emptyHint: '继承板级', health: modelHealth, onSave: function (next) { return saveLaneSlot(ln[0], 'worker', next) } })),
1262
+ React.createElement('div', null,
1263
+ React.createElement(OrderedModelsEditor, { label: ln[1] + ' Verifier', value: laneObj.verifier, models: routeModels, emptyHint: '继承板级', health: modelHealth, onSave: function (next) { return saveLaneSlot(ln[0], 'verifier', next) } })))
1264
+ }),
1265
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 1, whiteSpace: 'normal', maxWidth: 360 } }, '每槽=有序模型链:按序取第一个健康模型(首选冷却/熔断则次选递补,槽内全病走备用链→降档→父模型);空链 = 继承上方 Worker/Verifier 模型行;卡档位走对话 task_update size=light/standard/heavy'),
1266
+ // ③ 备用模型链:同款链编辑器(task-mvokikl3,替代逗号/换行文本框)
1267
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '备用模型链'),
1268
+ React.createElement(OrderedModelsEditor, { label: '备用链', value: props.fbModels, models: routeModels, emptyHint: '无(不启用)', health: modelHealth, onSave: saveFallbackChain }),
1269
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 360 } }, '取模口径:卡级特例 → 档位路由 → 板级模型;备用链=档位链全不可用时的跨档逃生索——候选冷却/熔断时按链顺序找首个健康替代,链尽再走降档/父模型兜底;空链=不启用')) : null)
1270
+ }
1271
+
987
1272
  function PoolCfgPopover(props) {
988
1273
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
989
1274
  var _a = useState(false), open = _a[0], setOpen = _a[1]
@@ -1010,23 +1295,21 @@
1010
1295
  // 界面缩放补偿(task-mv58e01j):根容器挂了 CSS zoom 后,getBoundingClientRect 返回缩放后的
1011
1296
  // 可视坐标,而 fixed 弹层在 zoom 子树内 top/right/maxHeight 会再被缩一次 → 各项除以档位还原成
1012
1297
  // CSS px,弹层仍贴着按钮且不溢出视口底(zoom=1 时与旧行为逐字一致)。
1298
+ // 锚点统一走 clampPopoverPos(task-mvt05utl):左溢改左锚——修按钮贴抽屉左缘(窄窗工具栏折行后
1299
+ // ⚙ 落到第二行左端)时弹层左半探出抽屉/视口被裁(2026-10-29 截图病灶本体)。
1013
1300
  var z = state.uiZoom || 1
1014
- 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))) }
1015
1302
  setOpen(!open)
1016
1303
  }
1017
1304
  return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
1018
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)),
1019
- 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' } },
1020
1307
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '并发上限(一次性派发,用完即销毁)'),
1021
1308
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
1022
1309
  React.createElement(PoolCfg, { label: 'Worker 并发', cfgKey: 'maxWorkers', value: props.maxW }),
1023
1310
  React.createElement(PoolCfg, { label: 'Verifier 并发', cfgKey: 'maxVerifiers', value: props.maxV })),
1024
- React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
1025
- React.createElement(ModelCfg, { label: 'Worker 模型', cfgKey: 'workerModel', value: props.workerModel }),
1026
- React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Worker 执行模型)')),
1027
- React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
1028
- React.createElement(ModelCfg, { label: 'Verifier 模型', cfgKey: 'verifierModel', value: props.verifierModel }),
1029
- React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)')),
1311
+ // 模型相关配置(板级 Worker/Verifier 模型行 + 力度路由六槽)已搬至「模型路由」独立弹层
1312
+ //(task-mvnyc8vo,ModelRoutePopover)——本弹层只留并发/超时/开关。
1030
1313
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '两级超时(软超时只提醒主窗口,由你决定继续等待或终止)'),
1031
1314
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
1032
1315
  React.createElement(MinCfg, { label: '软超时', cfgKey: 'softTimeoutMin', value: props.softT, min: 1, max: 480, tip: '超过后在看板提醒主窗口,不终止 run(1~480 分钟)' }),
@@ -1054,7 +1337,7 @@
1054
1337
  React.createElement('input', { type: 'checkbox', checked: props.verifyUserGuide !== false, onChange: function (e) { setCfg('verifyUserGuide', e.target.checked) } }),
1055
1338
  React.createElement('span', null, '📋 自测指南(验收结论附用户自测步骤,详情页与报告展示)')),
1056
1339
  // 史诗拆分总开关(板级 epicSplit,缺省 true):**只关引导,不禁机制**——关掉后 Team 提示词不再
1057
- // 注入「大任务必须拆分」第 6 条、create-task 响应不再附 suggestSplit 软提示;显式传 parentId 建
1340
+ // 注入「大任务必须拆分」第 6~8 条拆分 doctrine、create-task 响应不再附 suggestSplit 软提示;显式传 parentId 建
1058
1341
  // 子卡、史诗自动收口/hooks 状态机照常(用户/主窗口明确要拆时不受阻)。勾选态缺字段=开,与 host 同口径。
1059
1342
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '功能'),
1060
1343
  React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
@@ -1072,7 +1355,11 @@
1072
1355
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '界面'),
1073
1356
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
1074
1357
  React.createElement(ZoomCfg, null),
1075
- React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(等比缩放面板字体/图标/间距,仅本面板生效并本地记忆)'))) : null)
1358
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(等比缩放面板字体/图标/间距,仅本面板生效并本地记忆)')),
1359
+ // 引导横幅重开行(task-mvnnkv7x-2):仅关闭态(obd=true)渲染——横幅挂着时无需重开入口
1360
+ state.onboardDismissed ? React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 3 } },
1361
+ React.createElement(OnboardReset, null),
1362
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(首用定位行已 ✕ 关闭,点此重新显示)')) : null) : null)
1076
1363
  }
1077
1364
 
1078
1365
  function TeamView() {
@@ -1080,13 +1367,16 @@
1080
1367
  // v74 一次性模型:每个活跃卡片 = 一个在跑的 run(随任务结算销毁),没有常驻成员
1081
1368
  function memberCard(m, role) {
1082
1369
  var isW = role === 'worker'
1370
+ // 拆解官可见性(task-mvomad6c ①同族):verifiers 桶里的 decomposer 条目不再冒充「Verifier · 执行中」
1371
+ // (缺 role 的老快照条目 isDec=false,维持原标签——零回归)
1372
+ var isDec = !isW && m.role === 'decomposer'
1083
1373
  var curTask = m.taskId ? getTask(m.taskId) : null
1084
1374
  return React.createElement('div', { key: m.id, style: { minWidth: 170, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 8, borderTop: '3px solid ' + (isW ? C.brand : C.warn) } },
1085
1375
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 } },
1086
1376
  React.createElement('span', { style: { display: 'inline-flex', color: isW ? C.brand : C.warn } }, ic(isW ? 'zap' : 'clipboard-check', 13)),
1087
- React.createElement('span', { style: { fontSize: 12, fontWeight: 700, color: C.text } }, (isW ? 'Worker' : 'Verifier') + ' · 执行中'),
1377
+ React.createElement('span', { style: { fontSize: 12, fontWeight: 700, color: C.text } }, (isW ? 'Worker' : (isDec ? '🧩 拆解官' : 'Verifier')) + ' · 执行中'),
1088
1378
  React.createElement('span', { style: { marginLeft: 'auto', fontSize: 9, padding: '1px 6px', borderRadius: 3, background: C.brand, color: C_INV } }, '忙碌')),
1089
- m.model ? React.createElement('div', { style: { fontSize: 9, color: C.warn, marginBottom: 3 }, title: '异构模型审查' }, '🧬 ' + m.model) : null,
1379
+ m.model ? React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 3, display: 'flex', alignItems: 'center', gap: 3 }, title: '本次取模:' + m.model + (m.modelSource ? '(' + (MODEL_SRC_LABEL[m.modelSource] || m.modelSource) + ')' : '') }, React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '🧬 ' + m.model), modelSourceBadge(m.modelSource)) : null,
1090
1380
  curTask ? React.createElement('div', { onClick: function () { state.detailId = curTask.id; state.view = 'board'; notify() }, style: { fontSize: 10, color: C.brand, cursor: 'pointer', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: curTask.title }, '→ ' + curTask.title) : React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '→ ' + m.taskId),
1091
1381
  React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '一次性子代理会话:' + (m.runId || '') }, '会话 ' + String(m.runId || '').slice(0, 8) + '…(用完即销毁)'))
1092
1382
  }