dsh-agent-board 1.8.3 → 1.9.0

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/lib/client.js CHANGED
@@ -175,6 +175,19 @@ function apply(ctx) {
175
175
  ['path', { d: 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z' }],
176
176
  ['circle', { cx: 12, cy: 12, r: 3 }]
177
177
  ],
178
+ // 模型路由弹层按钮图标(task-mvnyc8vo):芯片语义(lucide cpu)
179
+ 'cpu': [
180
+ ['rect', { x: 4, y: 4, width: 16, height: 16, rx: 2 }],
181
+ ['rect', { x: 9, y: 9, width: 6, height: 6 }],
182
+ ['path', { d: 'M15 2v2' }],
183
+ ['path', { d: 'M15 20v2' }],
184
+ ['path', { d: 'M2 15h2' }],
185
+ ['path', { d: 'M2 9h2' }],
186
+ ['path', { d: 'M20 15h2' }],
187
+ ['path', { d: 'M20 9h2' }],
188
+ ['path', { d: 'M9 2v2' }],
189
+ ['path', { d: 'M9 20v2' }]
190
+ ],
178
191
  'rocket': [
179
192
  ['path', { d: 'M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z' }],
180
193
  ['path', { d: 'm12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z' }],
@@ -255,7 +268,12 @@ function apply(ctx) {
255
268
  var UI_ZOOM_KEY = 'dsh-agent-board-ui-zoom'
256
269
  var UI_ZOOM_LEVELS = [0.9, 1, 1.1, 1.25, 1.5] // 90% / 100%(默认)/ 110% / 125% / 150%
257
270
  function readUiZoom() { try { var v = parseFloat(localStorage.getItem(UI_ZOOM_KEY)); return UI_ZOOM_LEVELS.indexOf(v) >= 0 ? v : 1 } catch (_) { return 1 } }
258
- var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, cleanupRetentionDays: 7, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, verifyUserGuide: true, uiZoom: readUiZoom(), lessonPushed: {}, isRoot: true, isRootEverTrue: false, isRootFalseN: 0, isRootStable: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', filterReview: false, selectMode: false, selected: {}, undoSnapshot: null, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', mpRole: '', mpSize: '', usageSummary: null, childStats: {}, tasksErr: '', tasksHash: '' }
271
+ // ===== 首用引导横幅 dismissal(反馈 n-muygxqc7045b 最低成本方案,task-mvmttnux)=====
272
+ // 纯客户端偏好(同 uiZoom 模式,不走 host set-board-config):localStorage 键 dsh-agent-board-onboard-dismissed,
273
+ // '1'=用户已 ✕ 关闭、不再渲染;读取异常一律视为未关闭(横幅照常渲染,下次 ✕ 写入成功即持久)。
274
+ var ONBOARD_DISMISS_KEY = 'dsh-agent-board-onboard-dismissed'
275
+ function readOnboardDismissed() { try { return localStorage.getItem(ONBOARD_DISMISS_KEY) === '1' } catch (_) { return false } }
276
+ var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', laneModels: null, fallbackModels: null, softTimeoutMin: 30, hardTimeoutMin: 120, cleanupRetentionDays: 7, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, verifyUserGuide: true, uiZoom: readUiZoom(), onboardDismissed: readOnboardDismissed(), lessonPushed: {}, isRoot: true, isRootEverTrue: false, isRootFalseN: 0, isRootStable: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', filterReview: false, selectMode: false, selected: {}, undoSnapshot: null, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', mpRole: '', mpSize: '', usageSummary: null, childStats: {}, tasksErr: '', tasksHash: '' }
259
277
 
260
278
  // #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
261
279
  try {
@@ -325,7 +343,7 @@ function apply(ctx) {
325
343
  // 「缺字段/脏值=开,只有显式 false 才关」,与 host core.cfg 同口径——单点定义,避免纠偏把脏值当真值。
326
344
  function cfgKnobOf(src, key) { return !(src && src[key] === false) }
327
345
  // 变更检测:与 notify 同类的轻量浅比较(只看这几个布尔开关;settings 无嵌套对象)。
328
- function cfgKnobsChanged(a, b) { return a.feedbackEnabled !== b.feedbackEnabled || a.notifyDispatch !== b.notifyDispatch || a.notifyDone !== b.notifyDone || a.epicSplit !== b.epicSplit || a.verifyUserGuide !== b.verifyUserGuide }
346
+ function cfgKnobsChanged(a, b) { return a.feedbackEnabled !== b.feedbackEnabled || a.notifyDispatch !== b.notifyDispatch || a.notifyDone !== b.notifyDone || a.epicSplit !== b.epicSplit || a.verifyUserGuide !== b.verifyUserGuide || JSON.stringify(a.laneModels || null) !== JSON.stringify(b.laneModels || null) || JSON.stringify(a.fallbackModels || null) !== JSON.stringify(b.fallbackModels || null) }
329
347
 
330
348
  function fetchTasks() {
331
349
  if (!state.sessionId) return
@@ -365,7 +383,10 @@ function apply(ctx) {
365
383
  // → 勾选框必须等下一次任意 notify 才翻面(安静板卡数秒)。这里在赋值前快照、赋值后比对,
366
384
  // 有变化就补一次 notify:乐观更新(PoolCfgPopover)已让点击瞬时翻面,本兜底是服务端权威值纠偏
367
385
  // (乐观值与服务端不一致时以服务端为准,失败回滚亦由此收敛)。
368
- var cfgKnobs = { feedbackEnabled: state.feedbackEnabled, notifyDispatch: state.notifyDispatch, notifyDone: state.notifyDone, epicSplit: state.epicSplit, verifyUserGuide: state.verifyUserGuide }
386
+ // 快照 laneModels 存**原值**(非 JSON 串):比较器 cfgKnobsChanged 已对双侧各做一次 stringify,
387
+ // 快照侧若先 stringify 一次则类型不对称(串 vs 对象/null 恒不等 → null→null 也判变化),
388
+ // 3s 轮询每轮补 notify、tasksHash 渲染节约当场作废(验收驳回回归,task-mvneq4mw)。
389
+ var cfgKnobs = { feedbackEnabled: state.feedbackEnabled, notifyDispatch: state.notifyDispatch, notifyDone: state.notifyDone, epicSplit: state.epicSplit, verifyUserGuide: state.verifyUserGuide, laneModels: state.laneModels || null, fallbackModels: state.fallbackModels || null }
369
390
  if (tasksChanged) {
370
391
  state.tasksHash = newHash
371
392
  state.tasks = (d && d.tasks) || []
@@ -381,6 +402,13 @@ function apply(ctx) {
381
402
  state.maxVerifiers = (d && d.maxVerifiers) || 2
382
403
  state.verifierModel = (d && d.verifierModel) || ''
383
404
  state.workerModel = (d && d.workerModel) || ''
405
+ // 力度路由表(task-mvneq4mw GUI):cfg 归一形态 {light/standard/heavy:{worker,verifier}} 透传进 state,
406
+ // 池配置弹层「力度路由」小节 6 槽下拉读它;老 host 不返回 → null(弹层按全空槽渲染,与板级模型行同语义)
407
+ state.laneModels = (d && d.laneModels) || null
408
+ // 备用模型链(task-mvnyc8vo GUI 透传 / task-mvnqxlpn 数据面):cfg 归一后的有序数组透传进 state,
409
+ // 模型路由弹层「备用模型链」文本框读它;老 host 不返回 → null(按空链渲染)。写通道=既有
410
+ // set-board-config key='fallbackModels'(host 逐项合法清单校验,非法整体拒写)。
411
+ state.fallbackModels = (d && d.fallbackModels) || null
384
412
  state.softTimeoutMin = (d && d.softTimeoutMin) || 30
385
413
  state.hardTimeoutMin = (d && d.hardTimeoutMin) || 120
386
414
  state.poolStatus = (d && d.poolStatus) || null
@@ -628,6 +656,8 @@ function apply(ctx) {
628
656
  var _m = useState(state.minWorkers), minW = _m[0], setMinW = _m[1]; var _n = useState(state.maxWorkers), maxW = _n[0], setMaxW = _n[1]; var _o = useState(state.minVerifiers), minV = _o[0], setMinV = _o[1]; var _p = useState(state.maxVerifiers), maxV = _p[0], setMaxV = _p[1]; var _wm = useState(state.workerModel), workerModel = _wm[0], setWorkerModel = _wm[1]; var _vm = useState(state.verifierModel), verifierModel = _vm[0], setVerifierModel = _vm[1]
629
657
  var _q = useState(state.teamMode), teamMode = _q[0], setTeamMode = _q[1]; var _dr = useState(state.dateRange), setDR = _dr[1]
630
658
  var _wmo = useState(state.workMode), workMode = _wmo[0], setWorkModeState = _wmo[1]
659
+ var _lms = useState(state.laneModels), laneMs = _lms[0], setLaneMs = _lms[1] // 力度路由表(模型路由弹层「力度路由」小节 6 槽下拉数据源)
660
+ var _fbm = useState(state.fallbackModels), fbMs = _fbm[0], setFbMs = _fbm[1] // 备用模型链(模型路由弹层「备用模型链」文本框数据源)
631
661
  var _cro = useState(state.createOpen), createOpen = _cro[0], setCreateOpen = _cro[1]; var _crf = useState(state.createFlash), createFlash = _crf[0], setCreateFlash = _crf[1]
632
662
  var _te = useState(state.tasksErr), tasksErr = _te[0], setTasksErr = _te[1] // 读路径错误条(无错不渲染)
633
663
  var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
@@ -637,8 +667,9 @@ function apply(ctx) {
637
667
  var _vug = useState(state.verifyUserGuide), vugOn = _vug[0], setVugOn = _vug[1] // 自测指南开关勾选态(设置区「验收」)
638
668
  var _crd = useState(state.cleanupRetentionDays), crd = _crd[0], setCrd = _crd[1] // 清理保留天数(设置区「存储清理」)
639
669
  var _uzm = useState(state.uiZoom), uiZoom = _uzm[0], setUiZoom = _uzm[1] // 界面缩放档位(设置区「界面」,zoom 挂面板根容器)
670
+ var _obd = useState(state.onboardDismissed), obd = _obd[0], setObd = _obd[1] // 首用引导横幅关闭态(✕ 写 localStorage 持久化,仅 board 视图 content 内渲染)
640
671
  var _usg = useState(state.usageSummary), setUsage = _usg[1] // Token 区聚合订阅(task-mv10lvud 根修):usageSummary 换新对象即触发渲染
641
- useEffect(function () { function update() { setOpen(state.open); setTasksState(state.tasks); setModeState(state.boardMode); setDetailId(state.detailId); setDragOver(state.dragOver); setDispatchInfo(state.dispatchInfo); setViewState(state.view); setLayL(state.layoutLeft); setLayR(state.layoutRight); setMinW(state.minWorkers); setMaxW(state.maxWorkers); setMinV(state.minVerifiers); setMaxV(state.maxVerifiers); setWorkerModel(state.workerModel); setVerifierModel(state.verifierModel); setTeamMode(state.teamMode); setWorkModeState(state.workMode); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin); setFbEnabled(state.feedbackEnabled); setNdOn(state.notifyDispatch); setNnOn(state.notifyDone); setEsOn(state.epicSplit); setVugOn(state.verifyUserGuide); setCrd(state.cleanupRetentionDays); setUiZoom(state.uiZoom); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash); setTasksErr(state.tasksErr); setUsage(state.usageSummary) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
672
+ useEffect(function () { function update() { setOpen(state.open); setTasksState(state.tasks); setModeState(state.boardMode); setDetailId(state.detailId); setDragOver(state.dragOver); setDispatchInfo(state.dispatchInfo); setViewState(state.view); setLayL(state.layoutLeft); setLayR(state.layoutRight); setMinW(state.minWorkers); setMaxW(state.maxWorkers); setMinV(state.minVerifiers); setMaxV(state.maxVerifiers); setWorkerModel(state.workerModel); setVerifierModel(state.verifierModel); setLaneMs(state.laneModels); setFbMs(state.fallbackModels); setTeamMode(state.teamMode); setWorkModeState(state.workMode); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin); setFbEnabled(state.feedbackEnabled); setNdOn(state.notifyDispatch); setNnOn(state.notifyDone); setEsOn(state.epicSplit); setVugOn(state.verifyUserGuide); setCrd(state.cleanupRetentionDays); setUiZoom(state.uiZoom); setObd(state.onboardDismissed); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash); setTasksErr(state.tasksErr); setUsage(state.usageSummary) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
642
673
  if (!open) return null
643
674
  if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
644
675
  var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
@@ -658,7 +689,9 @@ function apply(ctx) {
658
689
  var ta = a.archivedAt || a.resolvedAt || a.createdAt || '', tb = b.archivedAt || b.resolvedAt || b.createdAt || ''
659
690
  return state.archSort === 'time-asc' ? ta.localeCompare(tb) : tb.localeCompare(ta)
660
691
  })
661
- content = React.createElement('div', null, React.createElement(FilterBar, null), hasFilter && active.length === 0 && archived.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 16, color: C.text2, fontSize: 11 } }, '无匹配任务') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, state.selectMode ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '16px 8px 12px', color: C.text2, fontSize: 12 } }, '点击右上角', React.createElement('span', { style: { color: C.brand, fontWeight: 600 } }, '「+ 新建任务」'), '创建第一张卡,或直接在对话里说:帮我建一张任务卡') : null, React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
692
+ // 首用引导横幅(task-mvmttnux,反馈 n-muygxqc7045b 最低成本方案):仅未 dismiss 时渲染(localStorage 持久化),
693
+ // 「?」链 README + ✕ 关闭;独立于下方空态 CTA 行,不动空态统一决策(六列骨架照常)。
694
+ content = React.createElement('div', null, React.createElement(FilterBar, null), hasFilter && active.length === 0 && archived.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 16, color: C.text2, fontSize: 11 } }, '无匹配任务') : null, obd ? null : React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', background: C.nested, borderRadius: 6 } }, React.createElement('span', null, '看板 = 后台 Worker 执行、Verifier 独立验收;拆解官拆大任务,轻/标/重三档力度路由'), React.createElement('a', { href: 'https://github.com/PPawnsir/task-board-plugin#readme', target: '_blank', rel: 'noopener', title: '查看 README 使用文档', style: { color: C.brand, textDecoration: 'none', fontWeight: 600, flexShrink: 0 } }, '?'), React.createElement('button', { onClick: function () { try { localStorage.setItem(ONBOARD_DISMISS_KEY, '1') } catch (_) {} state.onboardDismissed = true; notify() }, title: '关闭引导(可在 ⚙️ 设置-界面重新开启)', style: { marginLeft: 'auto', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 10, color: C.text2, padding: '0 2px', lineHeight: 1, flexShrink: 0 } }, '✕')), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, state.selectMode ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '16px 8px 12px', color: C.text2, fontSize: 12 } }, '点击右上角', React.createElement('span', { style: { color: C.brand, fontWeight: 600 } }, '「+ 新建任务」'), '创建第一张卡,或直接在对话里说:帮我建一张任务卡') : null, React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
662
695
  }
663
696
  // 面板根容器(顶层 div):zoom 挂这里——等比缩放整个面板子树(字体/图标/间距),只影响本面板
664
697
  // 不影响宿主页面;uiZoom 为订阅值(设置弹层选档 → state.uiZoom + notify → 本组件重渲染即时生效)。
@@ -667,7 +700,9 @@ function apply(ctx) {
667
700
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 12px', borderBottom: '1px solid ' + C.border, flexShrink: 0, flexWrap: 'wrap', gap: 4 } },
668
701
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } }, React.createElement('span', { style: { fontWeight: 600, fontSize: 13, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 5 } }, ic('clipboard-list', 15), '智能看板'), React.createElement(ViewTab, null), React.createElement(PoolStatus, null)),
669
702
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
670
- React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT, feedbackEnabled: fbEnabled, notifyDispatch: ndOn, notifyDone: nnOn, epicSplit: esOn, verifyUserGuide: vugOn, retentionDays: crd }),
703
+ React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, softT: softT, hardT: hardT, feedbackEnabled: fbEnabled, notifyDispatch: ndOn, notifyDone: nnOn, epicSplit: esOn, verifyUserGuide: vugOn, retentionDays: crd }),
704
+ // 模型路由独立弹层(task-mvnyc8vo):板级模型 + 力度路由六槽 + 备用链,从池配置弹层搬走
705
+ React.createElement(ModelRoutePopover, { workerModel: workerModel, verifierModel: verifierModel, laneModels: laneMs, fbModels: fbMs }),
671
706
  dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
672
707
  React.createElement('button', { onClick: function () { state.createOpen = true; notify() }, title: '新建任务(可存为草稿)', style: { fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border, borderRadius: 6, cursor: 'pointer', background: 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('plus', 11), '新建任务'),
673
708
  createFlash ? React.createElement('span', { style: { fontSize: 10, color: C.ok } }, createFlash) : null,
@@ -843,16 +878,51 @@ function apply(ctx) {
843
878
  return React.createElement('span', { title: b.tip, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + b.color + ' 14%, transparent)', color: b.color, fontWeight: 600, whiteSpace: 'nowrap' } }, b.txt)
844
879
  }
845
880
 
881
+ // ===== 拆解官状态徽标(task-mvomad6c ②,源卡列表行「🧩 拆解中 / 已提案 ×N」)=====
882
+ // 纯函数现算(不入 state、不落库、不引用 C/ago——自包含,可直接被单测提取执行),
883
+ // 返回 { txt, tone('brand'|'warn'), tip },色板映射留给渲染层 decomposePhaseBadge。
884
+ // 数据源 = t.runs 留档(role='decomposer')+ 同板子卡提案批:
885
+ // 在跑 → 末条 decomposer 留档 outcome='running' 且无 endedAt;**超龄守卫**:at 距今
886
+ // ≥ hardMin(留档自带,缺省 120)分钟视为 host 重启残留(sweepZombieRuns 只扫
887
+ // 落定卡,draft/pending 源卡的 running 留档不会被关账)——不亮「拆解中」,继续下判;
888
+ // 已提案 → 同板存在 proposalOf===本卡 id 且 status='draft' 的子卡(拆解官物化的待确认提案批;
889
+ // 逐张 publish 后 draft 消失、徽标同步熄灭——「待确认」语义精准跟随);
890
+ // 其余(从未拆解 / 判定不拆 / 失败 / 提案已全发布)→ null 不渲染,老卡零 DOM 变化。
891
+ function decomposeBadgeOf(t, allTasks) {
892
+ if (!t || !Array.isArray(t.runs)) return null
893
+ var last = null
894
+ for (var i = t.runs.length - 1; i >= 0; i--) { if (t.runs[i] && t.runs[i].role === 'decomposer') { last = t.runs[i]; break } }
895
+ if (last && last.outcome === 'running' && !last.endedAt) {
896
+ var ageMs = last.at ? (Date.now() - new Date(last.at).getTime()) : Infinity
897
+ if (isFinite(ageMs) && ageMs < (last.hardMin || 120) * 60000) {
898
+ return { txt: '🧩 拆解中', tone: 'brand', tip: '拆解官正在读本卡并产出子任务提案(run ' + last.id + ');拆解期间本卡不派 Worker,落定后转「🧩 已提案」' }
899
+ }
900
+ }
901
+ var kidsN = 0
902
+ var list = Array.isArray(allTasks) ? allTasks : []
903
+ for (var k = 0; k < list.length; k++) { var x = list[k]; if (x && x.proposalOf === t.id && x.status === 'draft') kidsN++ }
904
+ if (kidsN) return { txt: '🧩 已提案 ×' + kidsN, tone: 'warn', tip: '拆解官已产出 ' + kidsN + ' 张草稿子卡:确认后逐张发布(详情页「发布」或 task_update publish=true),全发布后本徽标熄灭' }
905
+ return null
906
+ }
907
+ function decomposePhaseBadge(t) {
908
+ var b = decomposeBadgeOf(t, state.tasks)
909
+ if (!b) return null
910
+ var color = b.tone === 'warn' ? C.warn : C.brand
911
+ return React.createElement('span', { title: b.tip, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + color + ' 14%, transparent)', color: color, fontWeight: 600, whiteSpace: 'nowrap' } }, b.txt)
912
+ }
913
+
846
914
  function Card(props) { var t = props.task; var pc = prioColor[t.priority] || prioColor.low; var dragging = state.dragTask === t.id; var preview = (t.deliverable && t.deliverable.summary) || t.resolution; var critGlow = t.priority === 'critical' && !t.escalation; var sel = !!state.selected[t.id]; var pm = pipeOf(t); var depBlock = t.status === 'pending' && depsBlocked(t); var delOk = !state.selectMode && canDelete(t); var cs = (state.childStats && state.childStats[t.id]) || null; var csDone = cs ? (typeof cs.settled === 'number' ? cs.settled : cs.resolved) : 0; var epic = !!(cs && cs.total > 0); var parentT = t.parentId ? getTask(t.parentId) : null; var depWaitTitle = ''; var depWaitN = 0; if (depBlock) { t.dependsOn.forEach(function (id) { var d = getTask(id); if (!d || (d.status !== 'resolved' && d.status !== 'archived')) { depWaitN++; if (!depWaitTitle) depWaitTitle = d && d.title ? d.title : id } }) } var durB = cardDur(t);
847
915
  // 无障碍(反馈 n-mut9rzpyc7p1):卡片根以 button 角色进 Tab 序(aria-label=标题+状态),
848
916
  // Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
849
917
  // focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
850
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() } }
851
- 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] || '中'), 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', { 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' } },
852
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,
853
921
  // hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
854
922
  epic ? hookPhaseBadge(t) : null,
855
923
  t.frozen ? React.createElement('span', { title: '已冻结:不参与自动派发(详情页可「解除冻结」)', style: { color: C.brand, fontWeight: 600 } }, '❄ 冻结') : null,
924
+ // 拆解官徽标(task-mvomad6c ②):源卡「🧩 拆解中 / 已提案 ×N」,非拆解卡零渲染
925
+ decomposePhaseBadge(t),
856
926
  (Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
857
927
  React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
858
928
  (t.retryCount || 0) + (t.rejectCount || 0) > 0 ? React.createElement('span', { title: '重试 ' + (t.retryCount || 0) + ' 次 / 驳回 ' + (t.rejectCount || 0) + ' 次', style: { color: C.warn, fontWeight: 600 } }, '⟳' + ((t.retryCount || 0) + (t.rejectCount || 0))) : null,
@@ -924,15 +994,18 @@ function apply(ctx) {
924
994
  (function () {
925
995
  var runs = historyRuns(t)
926
996
  if (!runs.length) return React.createElement('span', { style: { fontSize: 9, color: C.text2, opacity: 0.6, padding: '2px 0' } }, '无历史会话')
927
- var wN = 0, vN = 0
997
+ // 拆解官可见性同族修正(task-mvomad6c ③):runs 行此前 W/V 二元标签,decomposer 被误标 W——
998
+ // 加 🧩 标签与独立序数(W/V 序号口径逐字不变)
999
+ var wN = 0, vN = 0, dN = 0
928
1000
  return React.createElement('span', { style: { display: 'inline-flex', gap: 5, flexWrap: 'wrap' } }, runs.map(function (r, i) {
929
- var seq = r.role === 'verifier' ? (++vN) : (++wN)
1001
+ var isD = r.role === 'decomposer'
1002
+ var seq = r.role === 'verifier' ? (++vN) : (isD ? (++dN) : (++wN))
930
1003
  var isV = r.role === 'verifier'
931
1004
  return React.createElement('button', {
932
1005
  key: i, onClick: function (e) { e.stopPropagation(); if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
933
- title: (isV ? 'Verifier' : 'Worker') + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
1006
+ title: (isV ? 'Verifier' : (isD ? '拆解官' : 'Worker')) + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
934
1007
  style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + (isV ? C.warn : C.brand), borderRadius: 3, background: 'transparent', color: isV ? C.warn : C.brand, cursor: 'pointer' }
935
- }, '→ ' + (isV ? 'V' : 'W') + '#' + seq)
1008
+ }, '→ ' + (isV ? 'V' : (isD ? '🧩' : 'W')) + '#' + seq)
936
1009
  }))
937
1010
  })()))
938
1011
  }))
@@ -947,6 +1020,8 @@ function apply(ctx) {
947
1020
  var _d = useState(''), desc = _d[0], setDesc = _d[1]
948
1021
  var _p = useState('medium'), prio = _p[0], setPrio = _p[1]
949
1022
  var _pl = useState('full'), pipe = _pl[0], setPipe = _pl[1]
1023
+ // 档位(task-mvomad2c size 普适化):Team 板缺省标档(无继承项,host 硬闸必填);非 Team 缺省「继承」=不落 size 字段(标档语义)
1024
+ var _sz = useState(wm === 'team' ? 'standard' : ''), sizeSel = _sz[0], setSizeSel = _sz[1]
950
1025
  var _tc = useState(''), touchesRaw = _tc[0], setTouchesRaw = _tc[1]
951
1026
  var _dp = useState({}), depSel = _dp[0], setDepSel = _dp[1]
952
1027
  var _ac = useState(''), acc = _ac[0], setAcc = _ac[1]
@@ -968,7 +1043,9 @@ function apply(ctx) {
968
1043
  var tags = tagsRaw.split(/[,,]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
969
1044
  var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
970
1045
  setBusy(true); setErr(''); setOkMsg(''); setWarn('')
971
- rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, tags: tags, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
1046
+ var payload = { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, tags: tags, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }
1047
+ if (sizeSel) payload.size = sizeSel // 档位:继承(空值)不落字段;Team 板缺省标档显式写(host 硬闸缺档拒建)
1048
+ rpc('create-task', payload).then(function (r) {
972
1049
  setBusy(false)
973
1050
  if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
974
1051
  fetchTasks()
@@ -1003,7 +1080,9 @@ function apply(ctx) {
1003
1080
  React.createElement('div', { style: { flex: '1 1 0', minWidth: 0 } }, field('管线', React.createElement('select', { value: pipe, onChange: function (e) { setPipe(e.target.value) }, style: inp },
1004
1081
  React.createElement('option', { value: 'full' }, '全流程(执行+验证)'),
1005
1082
  React.createElement('option', { value: 'work' }, '免验证(只做不验)'),
1006
- React.createElement('option', { value: 'direct' }, '主窗口处理')), '管线 = 任务走哪种流程:全流程 = Worker 做 + Verifier 验;免验证 = 只做不验;主窗口处理 = 你在对话里直接办'))),
1083
+ React.createElement('option', { value: 'direct' }, '主窗口处理')), '管线 = 任务走哪种流程:全流程 = Worker 做 + Verifier 验;免验证 = 只做不验;主窗口处理 = 你在对话里直接办')),
1084
+ React.createElement('div', { style: { flex: '1 1 0', minWidth: 0 } }, field('档位', React.createElement('select', { value: sizeSel, onChange: function (e) { setSizeSel(e.target.value) }, style: inp },
1085
+ (wm === 'team' ? [] : [React.createElement('option', { key: '', value: '' }, '继承(标档)')]).concat([['light', '轻(light)'], ['standard', '标(standard)'], ['heavy', '重(heavy)']].map(function (s) { return React.createElement('option', { key: s[0], value: s[0] }, s[1]) }))), wm === 'team' ? '力度档位(Team 板必填硬闸):轻=单点小改,标=常规改动,重=跨模块/高风险' : '力度档位 = 路由板级 laneModels 三档表定模;继承 = 不显式设档(标档语义)'))),
1007
1086
  field('标签(逗号分隔,可空)', React.createElement('input', { value: tagsRaw, onChange: function (e) { setTagsRaw(e.target.value) }, placeholder: '如 bug, epic, 待验收', style: inp }), '标签 = 用于筛选的逗号分隔关键字(自动 trim + 去空)'),
1008
1087
  field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp }), 'touches = 本卡要改动的文件(glob 匹配);声明后并行任务间防互踩(文件锁)'),
1009
1088
  field('依赖(全部完成后才派发,可空)', cands.length === 0
@@ -1375,7 +1454,7 @@ function apply(ctx) {
1375
1454
  React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer' } }, '← 返回看板'),
1376
1455
  React.createElement('button', { onClick: function () { state.detailId = null; notify() }, title: '关闭详情面板(返回看板)', style: { fontSize: 11, padding: '1px 6px', border: '1px solid ' + C.border, borderRadius: 3, background: 'transparent', color: C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', lineHeight: 1.4 } }, ic('x', 11))),
1377
1456
  React.createElement(ReviewSummary, { task: task }),
1378
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 } }, React.createElement('select', { value: task.priority || 'medium', onChange: function (e) { rpc('update-task', { taskId: task.id, priority: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '优先级(四档:紧急/高/中/低)', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: (prioColor[task.priority] || prioColor.low), fontWeight: 600 } }, ['critical', 'high', 'medium', 'low'].map(function (p) { return React.createElement('option', { key: p, value: p }, prioLabel[p] + '(' + p + ')') })), React.createElement('span', { style: { fontSize: 11, padding: '1px 8px', borderRadius: 3, background: C.nested, color: C.text } }, statusLabels[task.status] || task.status), React.createElement('select', { value: task.pipeline || 'full', onChange: function (e) { rpc('update-task', { taskId: task.id, pipeline: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '管线档位', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.text2 } }, React.createElement('option', { value: 'full' }, '全流程(执行+验证)'), React.createElement('option', { value: 'work' }, '免验证(只做不验)'), React.createElement('option', { value: 'direct' }, '主窗口处理')), task.pipelineAuto ? React.createElement('span', { style: { fontSize: 9, color: C.text2 }, title: '由规则自动分类,可手动覆盖' }, 'auto') : null, isManual ? React.createElement('span', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('user', 10), '手动派发') : null),
1457
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 } }, React.createElement('select', { value: task.priority || 'medium', onChange: function (e) { rpc('update-task', { taskId: task.id, priority: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '优先级(四档:紧急/高/中/低)', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: (prioColor[task.priority] || prioColor.low), fontWeight: 600 } }, ['critical', 'high', 'medium', 'low'].map(function (p) { return React.createElement('option', { key: p, value: p }, prioLabel[p] + '(' + p + ')') })), React.createElement('span', { style: { fontSize: 11, padding: '1px 8px', borderRadius: 3, background: C.nested, color: C.text } }, statusLabels[task.status] || task.status), React.createElement('select', { value: task.size || (state.teamMode ? 'standard' : ''), onChange: function (e) { rpc('update-task', { taskId: task.id, size: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '力度档位(路由板级 laneModels 三档表定模):轻=单点小改/文档/配置,标=常规模块改动,重=跨模块/高风险' + (state.teamMode ? ';Team 板硬闸必填,不可调回继承' : ';继承=清除 size 回标档语义'), style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.brand, fontWeight: 600 } }, (state.teamMode ? [] : [React.createElement('option', { key: '', value: '' }, '继承(标)')]).concat([['light', '轻(light)'], ['standard', '标(standard)'], ['heavy', '重(heavy)']].map(function (s) { return React.createElement('option', { key: s[0], value: s[0] }, s[1]) }))), task.model ? React.createElement('span', { style: { fontSize: 9, padding: '1px 6px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 700, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '卡级模型特例 model=' + task.model + '(取模链最高优先;只读展示——改/删走对话 task_update,v1 不做 GUI 编辑)' }, '🎯 ' + task.model) : null, React.createElement('select', { value: task.pipeline || 'full', onChange: function (e) { rpc('update-task', { taskId: task.id, pipeline: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '管线档位', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.text2 } }, React.createElement('option', { value: 'full' }, '全流程(执行+验证)'), React.createElement('option', { value: 'work' }, '免验证(只做不验)'), React.createElement('option', { value: 'direct' }, '主窗口处理')), task.pipelineAuto ? React.createElement('span', { style: { fontSize: 9, color: C.text2 }, title: '由规则自动分类,可手动覆盖' }, 'auto') : null, isManual ? React.createElement('span', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('user', 10), '手动派发') : null),
1379
1458
  React.createElement(TagsEditor, { key: task.id, task: task }),
1380
1459
  // 流转到按钮组(键盘可达的状态迁移入口,替代拖拽;无合法迁移的状态(draft/resolved 等)整块不渲染)
1381
1460
  flowBtns.length > 0 ? React.createElement('div', { role: 'group', 'aria-label': '状态流转', style: { display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap', marginBottom: 8, padding: '5px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
@@ -1504,26 +1583,33 @@ function apply(ctx) {
1504
1583
  canCancel(task) ? React.createElement('button', { onClick: function () { cancelTask(task.id, task.title, setActionMsg) }, title: '取消任务(取消后不再派发,可在归档恢复待办)', style: { fontSize: 10, padding: '3px 8px', border: '1px solid ' + C.warn, borderRadius: 3, background: 'transparent', color: C.warn, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('ban', 10), '取消任务') : null,
1505
1584
  canDelete(task) ? React.createElement('button', { onClick: function () { deleteTask(task.id, task.title, setActionMsg) }, title: '删除任务(不可恢复;执行中请先终止,已落定请用归档)', style: { fontSize: 10, padding: '3px 8px', border: '1px solid ' + C.err, borderRadius: 3, background: 'transparent', color: C.err, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('trash-2', 10), '删除') : null) : null,
1506
1585
  (function () {
1507
- // 历史会话:无论任务处于哪个阶段都完整列出(Worker 各次 + Verifier 各次)
1586
+ // 历史会话:无论任务处于哪个阶段都完整列出(Worker 各次 + Verifier 各次 + 拆解官各次)
1508
1587
  var runs = historyRuns(task)
1509
1588
  if (!runs.length) return null
1510
- var roleMeta = { worker: { label: 'W', color: C.brand, tip: 'Worker' }, verifier: { label: 'V', color: C.warn, tip: 'Verifier' } }
1511
- var wN = 0, vN = 0
1589
+ // 拆解官可见性(task-mvomad6c ③):decomposer 角色标签 🧩(此前 fallback 进 worker 桶被误标 W)。
1590
+ var roleMeta = { worker: { label: 'W', color: C.brand, tip: 'Worker' }, verifier: { label: 'V', color: C.warn, tip: 'Verifier' }, decomposer: { label: '🧩', color: C.brand, tip: '拆解官' } }
1591
+ // 序数按角色分桶:W/V 序号与旧口径逐字一致(未知角色如 hook 仍计 Worker 桶,与 fallback 标签同桶);
1592
+ // decomposer 独立编号(🧩 #1 #2…重拆次数一眼可读)。
1593
+ var seqN = {}
1512
1594
  return React.createElement('div', { style: { marginTop: 8, padding: '6px 8px', background: C.nested, borderRadius: 6 } },
1513
1595
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('users', 10), '历史会话(点击跳转查看,含全部重试)'),
1514
1596
  React.createElement('div', { style: { display: 'flex', gap: 5, flexWrap: 'wrap' } },
1515
1597
  runs.map(function (r, i) {
1516
1598
  var rm = roleMeta[r.role] || roleMeta.worker
1517
- var seq = r.role === 'verifier' ? (++vN) : (++wN)
1518
- var isCur = (r.role === 'worker' && task.claimedBy === r.id) || (r.role === 'verifier' && task.verifierRun === r.id)
1599
+ var seqKey = roleMeta[r.role] ? r.role : 'worker'
1600
+ seqN[seqKey] = (seqN[seqKey] || 0) + 1; var seq = seqN[seqKey]
1601
+ // 拆解官在跑高亮:源卡不动卡面状态(无 claimedBy/verifierRun 可比对),以留档 running 未关账为在跑口径
1602
+ var isCur = (r.role === 'worker' && task.claimedBy === r.id) || (r.role === 'verifier' && task.verifierRun === r.id) || (r.role === 'decomposer' && r.outcome === 'running' && !r.endedAt)
1519
1603
  var okMark = r.outcome === 'completed' ? ' ✅' : (r.outcome === 'running' ? ' ⏳' : (r.outcome ? ' ⚠' : ''))
1520
1604
  // 续跑标记(卡2 落的 runs[i].resume 字段;卡3 Step3 在此显式标注):同一条会话的第二次及以后
1521
1605
  // 派发是「冷复活续跑」而不是新开 Worker,不标出来会让人把「第 2/3 次」误读成又开了一个新会话。
1522
1606
  // 只读 r.resume,不猜(老留档没有该字段 → 零渲染,与改造前逐字一致)。
1523
1607
  var rsMark = r.resume === true ? ' ↻' : ''
1608
+ // 耗时展示(task-mvomad6c ③顺带补齐全角色):落定 run 显 at→endedAt 净耗时,在跑显已跑时长
1609
+ var durTxt = r.at ? (r.endedAt ? ' · 耗时 ' + fmtDur(new Date(r.at).getTime(), new Date(r.endedAt).getTime()) : ' · 已跑 ' + elapsedSince(r.at)) : ''
1524
1610
  return React.createElement('button', {
1525
1611
  key: i, onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
1526
- title: rm.tip + ' 第 ' + seq + ' 次' + (r.resume === true ? '(↻ 冷复活续跑:沿用上一次的子会话,非新开)' : '') + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + (r.outcome ? ' · ' + r.outcome : '') + '(' + r.id + ')',
1612
+ title: rm.tip + ' 第 ' + seq + ' 次' + (r.resume === true ? '(↻ 冷复活续跑:沿用上一次的子会话,非新开)' : '') + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + (r.outcome ? ' · ' + r.outcome : '') + durTxt + '(' + r.id + ')',
1527
1613
  style: { fontSize: 10, padding: '2px 8px', border: '1px solid ' + (isCur ? rm.color : C.border), borderRadius: 3, background: isCur ? C.card : 'transparent', color: rm.color, cursor: 'pointer', fontWeight: isCur ? 600 : 400 }
1528
1614
  }, '→ ' + rm.tip + ' #' + seq + (r.at ? ' · ' + ago(r.at) : '') + okMark + rsMark)
1529
1615
  })))
@@ -1539,7 +1625,7 @@ function apply(ctx) {
1539
1625
  // dashboard —— 仪表盘域:统计(computeStats/TrendChart/StatCard/BarRow/ResearchRoiRow)· 范围筛选(RangeFilter)
1540
1626
  // · 全局总览(GlobalBoards)· 报告(buildReport/ReportButton)· Token 消耗(TokenUsage)
1541
1627
  // · 架构健康(HealthHints,架构自省 L1)
1542
- // · 团队池(TeamView/PoolStatus)· 设置(WorkModeSwitch/PoolCfg/MinCfg/ModelCfg/PoolCfgPopover)
1628
+ // · 团队池(TeamView/PoolStatus)· 设置(WorkModeSwitch/PoolCfg/MinCfg/ModelCfg/OrderedModelsEditor/PoolCfgPopover/ModelRoutePopover)
1543
1629
  // 本文件是 apply(ctx) 函数体片段,不可独立运行;由 scripts/build-client.cjs 按序拼接生成 lib/client.js。
1544
1630
  // ============================================================================
1545
1631
 
@@ -2045,6 +2131,44 @@ function apply(ctx) {
2045
2131
  { k: 'resumeRate', label: '续跑率', title: '续跑率 = resume:true 续跑记录 / 桶内 runs', fmt: function (b) { return mpPct(b.resumeRate) } },
2046
2132
  { k: 'cacheHitRate', label: '缓存命中率', title: '缓存命中率 = cacheRead / (input + cacheRead);⚠️ 跨 provider 口径可能不一(input 是否含缓存读各家不同),对比前先核口径;无 usage 样本 → —', fmt: function (b) { return mpPct(b.cacheHitRate) } }
2047
2133
  ]
2134
+ // ===== 按档位小表(task-mvneq4mw):行=力度三档(轻/标/重),列=runs/驳回/异常/token =====
2135
+ // 数据源 sb.bySize(host 装配层 assembleScoreboard 按 run.size 留档归桶;无 size 老 run 归标档,零迁移)。
2136
+ // 口径:驳回 = 归因到该档 worker run 的验收驳回;异常 = 超时落定 + 跑崩落定(行 title 拆分);
2137
+ // token = 有效口径(输入+输出+缓存写,与本区八指标同),行 title 附总量(含缓存读);随统计范围裁剪。
2138
+ // 只认 worker/verifier run(hook 恒父模型、不进档位统计);全部档位零 runs 时整块不渲染(不画空表)。
2139
+ var LANE_LABEL = { light: '轻档', standard: '标档', heavy: '重档' }
2140
+ function LaneTable(props) {
2141
+ var bySize = props.bySize
2142
+ if (!bySize) return null
2143
+ var lanes = ['light', 'standard', 'heavy']
2144
+ var totalRuns = 0
2145
+ for (var li = 0; li < lanes.length; li++) totalRuns += (bySize[lanes[li]] && bySize[lanes[li]].runs) || 0
2146
+ if (!totalRuns) return null
2147
+ var thL2 = { fontSize: 9, color: C.text2, fontWeight: 600, textAlign: 'left', padding: '2px 4px', borderBottom: '1px solid ' + C.border, whiteSpace: 'nowrap' }
2148
+ var thR2 = { fontSize: 9, color: C.text2, fontWeight: 600, textAlign: 'right', padding: '2px 4px', borderBottom: '1px solid ' + C.border, whiteSpace: 'nowrap', cursor: 'help' }
2149
+ var tdL2 = { fontSize: 10, color: C.text, textAlign: 'left', padding: '3px 4px', borderBottom: '1px solid ' + C.nested, whiteSpace: 'nowrap' }
2150
+ var tdR2 = { fontSize: 10, color: C.text, textAlign: 'right', padding: '3px 4px', borderBottom: '1px solid ' + C.nested, whiteSpace: 'nowrap' }
2151
+ return React.createElement('div', { style: { marginTop: 8 } },
2152
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '按档位'),
2153
+ React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse' } },
2154
+ React.createElement('thead', null, React.createElement('tr', null,
2155
+ React.createElement('th', { style: thL2 }, '档位'),
2156
+ React.createElement('th', { style: thR2, title: '该档已落定 Worker/Verifier run 数(hook 不计入)' }, 'runs'),
2157
+ React.createElement('th', { style: thR2, title: '驳回 = 归因到该档 worker run 的验收驳回次数' }, '驳回'),
2158
+ React.createElement('th', { style: thR2, title: '异常 = 超时落定(timeout/error)+ 跑崩落定(incomplete/error/failed)的 run 数;terminated 人工终止不计入' }, '异常'),
2159
+ React.createElement('th', { style: thR2, title: 'token = 有效口径合计(输入+输出+缓存写,不含缓存读);行悬浮见总量(含缓存读)' }, 'token'))),
2160
+ React.createElement('tbody', null, lanes.map(function (ln) {
2161
+ var b = bySize[ln] || { runs: 0, rejected: 0, failed: 0, timeouts: 0, tokSum: 0, effSum: 0 }
2162
+ 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 归标档'
2163
+ return React.createElement('tr', { key: ln, title: tip },
2164
+ React.createElement('td', { style: tdL2 }, LANE_LABEL[ln]),
2165
+ React.createElement('td', { style: tdR2 }, String(b.runs)),
2166
+ React.createElement('td', { style: tdR2 }, String(b.rejected)),
2167
+ React.createElement('td', { style: tdR2 }, String(b.failed + b.timeouts)),
2168
+ React.createElement('td', { style: tdR2 }, fmtTokens(b.effSum)))
2169
+ }))),
2170
+ 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 = 有效口径(悬浮行看总量);随统计范围裁剪'))
2171
+ }
2048
2172
  function ModelPerf(props) {
2049
2173
  var _R = React; var useState = _R.useState
2050
2174
  var _a = useState(state.mpRole || ''), roleF = _a[0], setRoleF = _a[1]
@@ -2059,8 +2183,9 @@ function apply(ctx) {
2059
2183
  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)
2060
2184
  var sb = props.sb
2061
2185
  var buckets = (sb && Array.isArray(sb.buckets)) ? sb.buckets : []
2062
- // 空态:无 scoreboard(老 host)/ 无入桶 run → 一句话空态(锚 id 照挂,Token 区链接始终可达)
2063
- 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 留账)'))
2186
+ // 空态:无 scoreboard(老 host)/ 无入桶 run → 一句话空态(锚 id 照挂,Token 区链接始终可达);
2187
+ // 按档位小表仍渲染(它不要求学模型名——无模型老 run 也烧了档位 token,档位视图可能恰好还有数据)
2188
+ 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 }))
2064
2189
  // 本地筛选:角色 × 规模段,纯数组 pass 不重拉
2065
2190
  var shown = buckets.filter(function (b) { return (!roleF || b.role === roleF) && (!sizeF || b.size === sizeF) })
2066
2191
  function chip(cur, setter, v, label) {
@@ -2098,7 +2223,9 @@ function apply(ctx) {
2098
2223
  React.createElement('td', { style: tdR }, String(b.runs)),
2099
2224
  MP_COLS.map(function (c) { return React.createElement('td', { key: c.k, style: tdR }, c.fmt(b)) }))
2100
2225
  })))),
2101
- React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 6, lineHeight: 1.5 } }, '口径:每行 = 模型 × 角色 × 规模段桶(规模按任务有效 token:小 <1M / 中 1~10M / 大 >10M);率类分母为桶内 runs,一次通过率分母为桶内 resolved 任务;失败率 = 跑崩落定(outcome 为 incomplete/error/failed)的 run / 桶内 runs,terminated 人工终止不计入;runs<5 行灰显 = 样本不足仅供参考;随统计范围裁剪(与 Token 区同一 range)'))
2226
+ 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)'),
2227
+ // 按档位小表(task-mvneq4mw):三档 × runs/驳回/异常/token,挂在八指标表下方
2228
+ React.createElement(LaneTable, { bySize: sb && sb.bySize }))
2102
2229
  }
2103
2230
 
2104
2231
  // ===== 质量趋势区(记分卡卡4,task-muxhtgil):一次通过率趋势 / 卡时长分布 / 超时率走势 / 续跑成功率 =====
@@ -2419,7 +2546,7 @@ function apply(ctx) {
2419
2546
  function commit(v) {
2420
2547
  var n = parseInt(v, 10)
2421
2548
  if (isNaN(n) || n < props.min || n > props.max) { setVal(String(props.value)); setDirty(false); return }
2422
- setVal(String(n)); setDirty(false); rpc('set-board-config', { key: props.cfgKey, value: n }).then(fetchTasks).catch(function () {})
2549
+ 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 () {})
2423
2550
  }
2424
2551
  return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } },
2425
2552
  props.label,
@@ -2458,20 +2585,48 @@ function apply(ctx) {
2458
2585
  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)
2459
2586
  }
2460
2587
 
2588
+ // ===== 取模来源角标(task-mvnyc8vo,用户「能看到子 agent 当前在用什么模型跑」)=====
2589
+ // board/parent=常规继承路径不标(绝大多数,标了反成噪音);其余路径给小角标,一眼看出本次取模走了哪条路:
2590
+ // card=卡级特例「卡」/ lane=档位路由「档」/ fallback=备用模型链「备」/ cooldown-downgrade=冷却降档「降」。
2591
+ var MODEL_SRC_BADGE = { card: '卡', lane: '档', fallback: '备', 'cooldown-downgrade': '降' }
2592
+ var MODEL_SRC_LABEL = { card: '卡级特例', lane: '档位路由', board: '板级模型', parent: '继承父模型', fallback: '备用模型链', 'cooldown-downgrade': '冷却降档' }
2593
+ function modelSourceBadge(src) {
2594
+ if (!src || src === 'board' || src === 'parent') return null
2595
+ 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)
2596
+ }
2597
+
2461
2598
  function PoolStatus() {
2462
2599
  var ps = state.poolStatus
2463
2600
  if (!ps) return null
2464
- var wActive = (ps.workers || []).filter(function (w) { return w.busy }).length
2601
+ var wBusy = (ps.workers || []).filter(function (w) { return w.busy })
2602
+ var vBusy = (ps.verifiers || []).filter(function (v) { return v.busy })
2603
+ var wActive = wBusy.length
2465
2604
  var wTotal = state.maxWorkers // 分母=并发上限而非当前池实例数,空闲也显示 0/上限
2466
- var vActive = (ps.verifiers || []).filter(function (v) { return v.busy }).length
2605
+ var vActive = vBusy.length
2467
2606
  var vTotal = state.maxVerifiers
2607
+ // 拆解官可见性(task-mvomad6c ①):decomposer 与 hook 同归 verifiers 桶,靠条目 role 择出——
2608
+ // 在跑时于 Verifier 计数旁独立亮「🧩 拆解中 ×N」。Verifier x/y 计数口径不变(并发位占用语义:
2609
+ // decomposer 计 verifier 位,剔掉会让水位失真);缺 role 字段的老快照条目零渲染(能力检测)。
2610
+ var decBusy = vBusy.filter(function (v) { return v.role === 'decomposer' })
2611
+ // 在跑子代理模型可见(task-mvnyc8vo):每个 busy run 渲染 model 小芯片 + 取模来源角标,
2612
+ // 与执行池成员卡同口径;缺 model 字段的老数据条目不渲染芯片。
2613
+ var chips = wBusy.concat(vBusy).filter(function (m) { return !!m.model })
2468
2614
  return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
2469
2615
  // 池水位标注(反馈 n-mut9rzoq3flu / n-muyg4wl9208c):裸数字第一屏无解,补可见文字标签 + title
2470
2616
  React.createElement('span', { title: '活跃 Worker 数/上限(⚡ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), 'Worker ' + wActive + '/' + wTotal),
2471
- React.createElement('span', { title: '活跃 Verifier 数/上限(✓ = 在跑)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), 'Verifier ' + vActive + '/' + vTotal))
2617
+ React.createElement('span', { title: '活跃 Verifier 数/上限(✓ = 在跑;拆解官在跑也占 Verifier 并发位,见右侧 🧩 计数)', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), 'Verifier ' + vActive + '/' + vTotal),
2618
+ 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,
2619
+ chips.map(function (m) {
2620
+ 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 } },
2621
+ React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, m.model),
2622
+ modelSourceBadge(m.modelSource))
2623
+ }))
2472
2624
  }
2473
2625
 
2474
- // #11 头部减负:池配置收纳进 ⚙️ 弹出层(含 #17 verifier 异构模型设置)
2626
+ // #11 头部减负:池配置收纳进 ⚙️ 弹出层(并发/超时/开关);模型区(含 #17 verifier 异构模型设置、
2627
+ // 力度路由六槽、备用链)2026-10-25 起搬至「模型路由」独立弹层 ModelRoutePopover(task-mvnyc8vo)。
2628
+ // 六槽/备用链 2026-10-26 起升级为 OrderedModelsEditor 有序链编辑器(task-mvokikl3:下拉添加+芯片 ×/↑ 排序,
2629
+ // 槽位读写整个有序数组)——原「GUI 只编首选」的 laneFirst 单选形态退役。
2475
2630
  function ModelCfg(props) {
2476
2631
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect
2477
2632
  var _a = useState(props.value || ''), val = _a[0], setVal = _a[1]
@@ -2479,6 +2634,10 @@ function apply(ctx) {
2479
2634
  var _m = useState(null), models = _m[0], setModels = _m[1]
2480
2635
  var cfgKey = props.cfgKey || 'verifierModel'
2481
2636
  var label = props.label || 'Verifier 模型'
2637
+ // 只服务板级 Worker/Verifier 模型行(两段式单选,空=「继承父级」)。力度路由档位槽曾复用本组件
2638
+ // (task-mvneq4mw lane 分支),2026-10-26 起随链式编辑器升级迁往 OrderedModelsEditor(task-mvokikl3)——
2639
+ // 档位槽走 laneModels 部分合并写通道的职责随编辑器搬走,本组件不再承担。
2640
+ var inhLabel = props.inheritLabel || '继承父级'
2482
2641
  // 下拉列出当前网关可用模型(host list-models RPC);枚举失败降级为文本输入
2483
2642
  useEffect(function () {
2484
2643
  var cancelled = false
@@ -2486,21 +2645,24 @@ function apply(ctx) {
2486
2645
  return function () { cancelled = true }
2487
2646
  }, [])
2488
2647
  useEffect(function () { setVal(props.value || ''); setDirty(false) }, [props.value])
2489
- function save() { setDirty(false); rpc('set-board-config', { key: cfgKey, value: (val || '').trim() }).then(fetchTasks).catch(function () {}) }
2648
+ function save() {
2649
+ setDirty(false)
2650
+ rpc('set-board-config', { key: cfgKey, value: (val || '').trim() }).then(fetchTasks).catch(function () {})
2651
+ }
2490
2652
  // 下拉两段式(task-mv9yuq2l,实证 n-mv8ki92qnhdq:巡检戳即点即写 → verifierModel 神隐回退无从审计):
2491
2653
  // 选中只改本地草稿态 + 亮 dirty(对比服务端权威值 props.value),点旁边 ✓ 保存键才走 save() 提交 rpc——
2492
2654
  // 与下方降级文本输入同款两段式语义,巡检路过戳一下不再直接写配置。
2493
2655
  function pick(v) { setVal(v); setDirty(v !== (props.value || '')) }
2494
2656
  if (models) {
2495
2657
  return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
2496
- React.createElement('select', { value: val, onChange: function (e) { pick(e.target.value) }, title: label + '(空=继承父级;选中后点 ✓ 才保存)', style: { width: 150, padding: '0px 4px', fontSize: 9, border: '1px solid ' + (dirty ? C.brand : C.border), borderRadius: 2, background: C.card, color: C.text } },
2497
- React.createElement('option', { value: '' }, '继承父级'),
2658
+ 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 } },
2659
+ React.createElement('option', { value: '' }, inhLabel),
2498
2660
  models.map(function (m) { return React.createElement('option', { key: m.id, value: m.id }, m.name + ' (' + m.provider + ')') })),
2499
2661
  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)
2500
2662
  }
2501
2663
  // 降级:文本输入(模型枚举不可用时)
2502
2664
  return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, label,
2503
- 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 } }),
2665
+ 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 } }),
2504
2666
  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)
2505
2667
  }
2506
2668
 
@@ -2521,6 +2683,172 @@ function apply(ctx) {
2521
2683
  UI_ZOOM_LEVELS.map(function (z) { return React.createElement('option', { key: String(z), value: String(z) }, Math.round(z * 100) + '%' + (z === 1 ? '(默认)' : '')) })))
2522
2684
  }
2523
2685
 
2686
+ // 首用引导横幅重开入口(task-mvnnkv7x-2):✕ 关闭后此前无任何重开通道(kernel.js 仅 localStorage.setItem
2687
+ // 写键,title 写死「不再显示」)。照 ZoomCfg 同族纯客户端偏好范式:localStorage.removeItem(ONBOARD_DISMISS_KEY)
2688
+ // + state.onboardDismissed=false + notify——TopPanel update 链 setObd 订阅即时重渲染出横幅,无需刷新。
2689
+ // 仅关闭态(state.onboardDismissed=true)有意义:组件内自守 + 「界面」小节挂载点双闸,未关闭时不渲染。
2690
+ function OnboardReset() {
2691
+ if (!state.onboardDismissed) return null
2692
+ function reopen() {
2693
+ try { localStorage.removeItem(ONBOARD_DISMISS_KEY) } catch (_) {}
2694
+ state.onboardDismissed = false; notify()
2695
+ }
2696
+ return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: C.text2 } }, '引导横幅',
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
+ }
2699
+
2700
+ // ===== 模型健康点(task-mvoulhm7):🟢健康 / 🟡冷却中至HH:mm / 🔴熔断 / ⚪未知 =====
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 } }
2703
+ function healthHhmm(ts) { var d = new Date(Number(ts)); return isNaN(d.getTime()) ? '' : ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2) }
2704
+ function healthTimeText(v) { if (!v) return ''; var d = new Date(v); return isNaN(d.getTime()) ? String(v) : d.toLocaleString() }
2705
+ function ModelHealthDot(props) {
2706
+ var h = (props.health && props.health[props.model]) || { state: 'unknown', note: '尚无健康信号' }
2707
+ var meta = MODEL_HEALTH_META[h.state] || MODEL_HEALTH_META.unknown
2708
+ var untilText = h.state === 'cooldown' && h.until ? healthHhmm(h.until) : ''
2709
+ var label = meta.label + (untilText ? '至 ' + untilText : '')
2710
+ var lines = [props.model + ':' + label]
2711
+ if (h.note) lines.push(String(h.note))
2712
+ if (h.lastProbe) lines.push('最近探测:' + healthTimeText(h.lastProbe))
2713
+ if (h.lastSeen) lines.push('最近信号:' + healthTimeText(h.lastSeen))
2714
+ if (typeof h.latencyMs === 'number') lines.push('最近延迟:' + h.latencyMs + 'ms')
2715
+ if (Array.isArray(h.recentRuns) && h.recentRuns.length) lines.push('近 ' + h.recentRuns.length + ' 次 run:' + h.recentRuns.filter(function (r) { return r.ok }).length + ' 次成功')
2716
+ 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)
2717
+ }
2718
+
2719
+ // ===== 有序模型链编辑器(task-mvokikl3 链式编辑器:下拉添加 + 芯片排序,替代首选单选与文本框)=====
2720
+ // 弹层内复用 7 处(力度路由六槽 + 备用模型链):当前链渲染为模型芯片行(每片 × 移除 + ↑ 前移一位),
2721
+ // 尾部下拉(合法目录全量、过滤已在链中的)+「添加」追加到链尾;空链显示 props.emptyHint 占位语义
2722
+ // (六槽=「继承板级」、备用链=「无(不启用)」)。改动即写:props.onSave(nextArr) 返回 rpc Promise——
2723
+ // 复用既有 set-board-config 通道(laneModels 槽位写**整个有序数组** / fallbackModels 写**整个数组**,
2724
+ // 零 RPC 改动),host 逐项合法清单校验、非法整体拒写(ok:false 亮 ⚠️ 不静默吞);写成功后 fetchTasks 回读权威值。
2725
+ // props.models=null(目录枚举失败/老宿主)时降级为文本输入 + 添加,不丢功能。
2726
+ function OrderedModelsEditor(props) {
2727
+ var _R = React; var useState = _R.useState
2728
+ // 读侧容错:老 host 单字符串形态归一为单元素数组;非字符串/空串项滤掉(host 归一后恒为干净数组,此处只是防御)
2729
+ var list = Array.isArray(props.value) ? props.value.filter(function (x) { return typeof x === 'string' && x }) : (typeof props.value === 'string' && props.value ? [props.value] : [])
2730
+ var _p = useState(''), pickVal = _p[0], setPickVal = _p[1]
2731
+ var _m = useState(''), msg = _m[0], setMsg = _m[1]
2732
+ var models = Array.isArray(props.models) ? props.models : null
2733
+ // 下拉过滤已在链中的(链内唯一,重复添加无意义)
2734
+ var avail = models ? models.filter(function (m) { return list.indexOf(m.id) < 0 }) : null
2735
+ function commit(next) {
2736
+ setMsg('')
2737
+ Promise.resolve(props.onSave(next)).then(function (r) {
2738
+ if (r && r.ok === false) { setMsg('⚠️ ' + (r.error || '保存失败')); setTimeout(function () { setMsg('') }, 5000); return }
2739
+ fetchTasks()
2740
+ }).catch(function () { setMsg('⚠️ 保存失败'); setTimeout(function () { setMsg('') }, 3000) })
2741
+ }
2742
+ function removeAt(i) { var next = list.slice(); next.splice(i, 1); commit(next) }
2743
+ 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) }
2744
+ function add() { var v = (pickVal || '').trim(); if (!v || list.indexOf(v) >= 0) return; setPickVal(''); commit(list.concat([v])) }
2745
+ var chipBtn = { fontSize: 9, padding: '0px 2px', border: 'none', background: 'transparent', color: C.text2, cursor: 'pointer', lineHeight: '12px' }
2746
+ return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'flex-start', gap: 4, fontSize: 9, color: C.text2, whiteSpace: 'normal' } },
2747
+ props.label ? React.createElement('span', { style: { paddingTop: 1, minWidth: 62 } }, props.label) : null,
2748
+ React.createElement('span', { style: { display: 'inline-flex', flexWrap: 'wrap', gap: 3, alignItems: 'center', maxWidth: 250 } },
2749
+ list.length === 0 ? React.createElement('span', { style: { color: C.text2, fontStyle: 'italic', paddingTop: 1 } }, props.emptyHint || '无') : null,
2750
+ list.map(function (m, i) {
2751
+ 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' } },
2752
+ React.createElement(ModelHealthDot, { model: m, health: props.health }),
2753
+ React.createElement('span', null, m),
2754
+ i > 0 ? React.createElement('button', { onClick: function () { upAt(i) }, title: '前移一位(↑)', style: chipBtn }, '↑') : null,
2755
+ React.createElement('button', { onClick: function () { removeAt(i) }, title: '移除(×)', style: chipBtn }, '×'))
2756
+ })),
2757
+ // 尾部:下拉(过滤已选)/ 目录缺失降级文本输入 +「添加」追加链尾
2758
+ 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 } },
2759
+ React.createElement('option', { value: '' }, '选择模型…'),
2760
+ avail.map(function (m) { return React.createElement('option', { key: m.id, value: m.id }, m.name + ' (' + m.provider + ')') }))
2761
+ : 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 } }),
2762
+ 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 } }, '添加'),
2763
+ msg ? React.createElement('span', { style: { color: C.err, maxWidth: 140 } }, msg) : null)
2764
+ }
2765
+
2766
+ // ===== 模型路由独立弹层(task-mvnyc8vo,用户 2026-10-25「模型配置独立弹出卡片」)=====
2767
+ // 模型相关全部配置从池配置弹层**搬走**(不是复制)到本弹层:①板级 Worker/Verifier 模型(ModelCfg
2768
+ // 复用)②力度路由六槽 ③fallbackModels 备用链——②③ 2026-10-26 起统一为 OrderedModelsEditor
2769
+ // 有序链编辑器(task-mvokikl3:下拉添加+芯片 ×/↑ 排序,替代首选单选与文本框);池配置弹层只留并发/超时/开关。
2770
+ // 弹层样式复用 PoolCfgPopover 的 fixed 定位模式(脱离面板 overflow 裁剪 + uiZoom 档位补偿)。
2771
+ function ModelRoutePopover(props) {
2772
+ var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
2773
+ var _a = useState(false), open = _a[0], setOpen = _a[1]
2774
+ var _p = useState(null), pos = _p[0], setPos = _p[1]
2775
+ var ref = useRef(null); var btnRef = useRef(null)
2776
+ // 链编辑器共用模型目录:弹层取一次 list-models 下发 7 处编辑器(六槽+备用链),免每编辑器各发一次 RPC;
2777
+ // 目录不可用(老宿主/枚举失败)时 routeModels 恒 null,编辑器降级为文本输入 + 添加,不丢功能。
2778
+ var _rm = useState(null), routeModels = _rm[0], setRouteModels = _rm[1]
2779
+ // 健康面局部读取(不改 kernel 全局状态):弹层打开即取 + 15s 刷新;关闭时令 epoch 失效,迟到响应丢弃。
2780
+ var _mh = useState({}), modelHealth = _mh[0], setModelHealth = _mh[1]
2781
+ var _pe = useState(true), probeEnabled = _pe[0], setProbeEnabled = _pe[1]
2782
+ var _pi = useState(15), probeIntervalMin = _pi[0], setProbeIntervalMin = _pi[1]
2783
+ var healthEpoch = useRef(0)
2784
+ function refreshModelHealth() {
2785
+ var epoch = healthEpoch.current, args = state.tasksHash ? { ifHash: state.tasksHash } : {}
2786
+ return rpc('get-tasks', args).then(function (d) {
2787
+ if (!d || d.error || epoch !== healthEpoch.current) return d
2788
+ setModelHealth(d.modelHealth || {})
2789
+ setProbeEnabled(d.probeEnabled !== false)
2790
+ setProbeIntervalMin(Number(d.probeIntervalMin) > 0 ? Number(d.probeIntervalMin) : 15)
2791
+ return d
2792
+ }).catch(function () { return null })
2793
+ }
2794
+ useEffect(function () { if (!open) return; refreshModelHealth(); var timer = setInterval(refreshModelHealth, 15000); return function () { clearInterval(timer); healthEpoch.current++ } }, [open])
2795
+ useEffect(function () {
2796
+ var cancelled = false
2797
+ rpc('list-models').then(function (r) { if (!cancelled && r && r.ok && Array.isArray(r.models) && r.models.length) setRouteModels(r.models) }).catch(function () {})
2798
+ return function () { cancelled = true }
2799
+ }, [])
2800
+ // 改动即写(复用既有 set-board-config 通道,零 RPC 改动):六槽=laneModels 部分合并、槽位写**整个有序数组**
2801
+ // (非仅首选——task-mvokikl3 起 GUI 可编完整链);备用链=fallbackModels 整个数组。host 逐项合法清单校验、
2802
+ // 任一非法整体拒写(ok:false 由编辑器亮 ⚠️ 不静默吞);写成功后编辑器侧 fetchTasks 回读权威值。
2803
+ function saveLaneSlot(lane, role, nextArr) { var patch = {}; patch[lane] = {}; patch[lane][role] = nextArr; return rpc('set-board-config', { key: 'laneModels', value: patch }) }
2804
+ function saveFallbackChain(nextArr) { return rpc('set-board-config', { key: 'fallbackModels', value: nextArr }) }
2805
+ function saveProbeEnabled(next) { setProbeEnabled(next); return rpc('set-board-config', { key: 'probeEnabled', value: next }).then(refreshModelHealth).catch(refreshModelHealth) }
2806
+ 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])
2807
+ function toggle() {
2808
+ // 与 PoolCfgPopover 同口径:fixed + 视口坐标(面板 overflow:hidden 会裁 absolute 弹层);
2809
+ // 界面缩放补偿——zoom 子树内 getBoundingClientRect 返回缩放后坐标,各项除以档位还原 CSS px。
2810
+ 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) }) }
2812
+ setOpen(!open)
2813
+ }
2814
+ return React.createElement('span', { ref: ref, style: { position: 'relative', display: 'inline-flex' } },
2815
+ 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' } },
2817
+ // ① 板级模型(从池配置弹层搬入,组件与写通道逐字不变)
2818
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 5 } }, '板级模型'),
2819
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
2820
+ React.createElement(ModelCfg, { label: 'Worker 模型', cfgKey: 'workerModel', value: props.workerModel }),
2821
+ props.workerModel ? React.createElement(ModelHealthDot, { model: props.workerModel, health: modelHealth }) : null,
2822
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Worker 执行模型)')),
2823
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
2824
+ React.createElement(ModelCfg, { label: 'Verifier 模型', cfgKey: 'verifierModel', value: props.verifierModel }),
2825
+ props.verifierModel ? React.createElement(ModelHealthDot, { model: props.verifierModel, health: modelHealth }) : null,
2826
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)')),
2827
+ // 主动健康嗅探配置:只打本页已配置模型;关掉后冷却/熔断/run 被动健康面仍然保留。
2828
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '健康嗅探(事前调度)'),
2829
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', whiteSpace: 'normal', maxWidth: 360 } },
2830
+ React.createElement('label', { style: { display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 9, color: C.text, cursor: 'pointer' }, title: '每间隔对板级/档位/备用链中已配置模型发一次 maxTokens=1 的极短 ping;不创建子代理' },
2831
+ React.createElement('input', { type: 'checkbox', checked: !!probeEnabled, onChange: function (e) { saveProbeEnabled(e.target.checked) } }), '主动探测'),
2832
+ React.createElement(MinCfg, { label: '间隔', cfgKey: 'probeIntervalMin', value: probeIntervalMin, min: 1, max: 1440, tip: '主动健康探测间隔(1~1440 分钟,默认 15)', onSaved: refreshModelHealth }),
2833
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '仅已配置模型 · 关闭仍保留被动健康')),
2834
+ // ② 力度路由六槽(链编辑器,task-mvokikl3):三档 × Worker/Verifier 各一条有序模型链——
2835
+ // 读写整个槽位数组(非仅首选);空链=继承上方板级模型行。
2836
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '力度路由'),
2837
+ [['light', '轻档'], ['standard', '标档'], ['heavy', '重档']].map(function (ln) {
2838
+ var laneObj = (props.laneModels && props.laneModels[ln[0]]) || {}
2839
+ return React.createElement('div', { key: ln[0], style: { marginBottom: 4 } },
2840
+ React.createElement('div', { style: { marginBottom: 2 } },
2841
+ React.createElement(OrderedModelsEditor, { label: ln[1] + ' Worker', value: laneObj.worker, models: routeModels, emptyHint: '继承板级', health: modelHealth, onSave: function (next) { return saveLaneSlot(ln[0], 'worker', next) } })),
2842
+ React.createElement('div', null,
2843
+ React.createElement(OrderedModelsEditor, { label: ln[1] + ' Verifier', value: laneObj.verifier, models: routeModels, emptyHint: '继承板级', health: modelHealth, onSave: function (next) { return saveLaneSlot(ln[0], 'verifier', next) } })))
2844
+ }),
2845
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 1, whiteSpace: 'normal', maxWidth: 360 } }, '每槽=有序模型链:按序取第一个健康模型(首选冷却/熔断则次选递补,槽内全病走备用链→降档→父模型);空链 = 继承上方 Worker/Verifier 模型行;卡档位走对话 task_update size=light/standard/heavy'),
2846
+ // ③ 备用模型链:同款链编辑器(task-mvokikl3,替代逗号/换行文本框)
2847
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '备用模型链'),
2848
+ React.createElement(OrderedModelsEditor, { label: '备用链', value: props.fbModels, models: routeModels, emptyHint: '无(不启用)', health: modelHealth, onSave: saveFallbackChain }),
2849
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, whiteSpace: 'normal', maxWidth: 360 } }, '取模口径:卡级特例 → 档位路由 → 板级模型;备用链=档位链全不可用时的跨档逃生索——候选冷却/熔断时按链顺序找首个健康替代,链尽再走降档/父模型兜底;空链=不启用')) : null)
2850
+ }
2851
+
2524
2852
  function PoolCfgPopover(props) {
2525
2853
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect, useRef = _R.useRef
2526
2854
  var _a = useState(false), open = _a[0], setOpen = _a[1]
@@ -2558,12 +2886,8 @@ function apply(ctx) {
2558
2886
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
2559
2887
  React.createElement(PoolCfg, { label: 'Worker 并发', cfgKey: 'maxWorkers', value: props.maxW }),
2560
2888
  React.createElement(PoolCfg, { label: 'Verifier 并发', cfgKey: 'maxVerifiers', value: props.maxV })),
2561
- React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 5 } },
2562
- React.createElement(ModelCfg, { label: 'Worker 模型', cfgKey: 'workerModel', value: props.workerModel }),
2563
- React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Worker 执行模型)')),
2564
- React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
2565
- React.createElement(ModelCfg, { label: 'Verifier 模型', cfgKey: 'verifierModel', value: props.verifierModel }),
2566
- React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(Verifier 异构审查)')),
2889
+ // 模型相关配置(板级 Worker/Verifier 模型行 + 力度路由六槽)已搬至「模型路由」独立弹层
2890
+ //(task-mvnyc8vo,ModelRoutePopover)——本弹层只留并发/超时/开关。
2567
2891
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '两级超时(软超时只提醒主窗口,由你决定继续等待或终止)'),
2568
2892
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
2569
2893
  React.createElement(MinCfg, { label: '软超时', cfgKey: 'softTimeoutMin', value: props.softT, min: 1, max: 480, tip: '超过后在看板提醒主窗口,不终止 run(1~480 分钟)' }),
@@ -2591,7 +2915,7 @@ function apply(ctx) {
2591
2915
  React.createElement('input', { type: 'checkbox', checked: props.verifyUserGuide !== false, onChange: function (e) { setCfg('verifyUserGuide', e.target.checked) } }),
2592
2916
  React.createElement('span', null, '📋 自测指南(验收结论附用户自测步骤,详情页与报告展示)')),
2593
2917
  // 史诗拆分总开关(板级 epicSplit,缺省 true):**只关引导,不禁机制**——关掉后 Team 提示词不再
2594
- // 注入「大任务必须拆分」第 6 条、create-task 响应不再附 suggestSplit 软提示;显式传 parentId 建
2918
+ // 注入「大任务必须拆分」第 6~8 条拆分 doctrine、create-task 响应不再附 suggestSplit 软提示;显式传 parentId 建
2595
2919
  // 子卡、史诗自动收口/hooks 状态机照常(用户/主窗口明确要拆时不受阻)。勾选态缺字段=开,与 host 同口径。
2596
2920
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '功能'),
2597
2921
  React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 5, fontSize: 10, color: C.text, cursor: 'pointer', whiteSpace: 'normal', maxWidth: 260 } },
@@ -2609,7 +2933,11 @@ function apply(ctx) {
2609
2933
  React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, margin: '7px 0 5px' } }, '界面'),
2610
2934
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
2611
2935
  React.createElement(ZoomCfg, null),
2612
- React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(等比缩放面板字体/图标/间距,仅本面板生效并本地记忆)'))) : null)
2936
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(等比缩放面板字体/图标/间距,仅本面板生效并本地记忆)')),
2937
+ // 引导横幅重开行(task-mvnnkv7x-2):仅关闭态(obd=true)渲染——横幅挂着时无需重开入口
2938
+ state.onboardDismissed ? React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 3 } },
2939
+ React.createElement(OnboardReset, null),
2940
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, '(首用定位行已 ✕ 关闭,点此重新显示)')) : null) : null)
2613
2941
  }
2614
2942
 
2615
2943
  function TeamView() {
@@ -2617,13 +2945,16 @@ function apply(ctx) {
2617
2945
  // v74 一次性模型:每个活跃卡片 = 一个在跑的 run(随任务结算销毁),没有常驻成员
2618
2946
  function memberCard(m, role) {
2619
2947
  var isW = role === 'worker'
2948
+ // 拆解官可见性(task-mvomad6c ①同族):verifiers 桶里的 decomposer 条目不再冒充「Verifier · 执行中」
2949
+ // (缺 role 的老快照条目 isDec=false,维持原标签——零回归)
2950
+ var isDec = !isW && m.role === 'decomposer'
2620
2951
  var curTask = m.taskId ? getTask(m.taskId) : null
2621
2952
  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) } },
2622
2953
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 } },
2623
2954
  React.createElement('span', { style: { display: 'inline-flex', color: isW ? C.brand : C.warn } }, ic(isW ? 'zap' : 'clipboard-check', 13)),
2624
- React.createElement('span', { style: { fontSize: 12, fontWeight: 700, color: C.text } }, (isW ? 'Worker' : 'Verifier') + ' · 执行中'),
2955
+ React.createElement('span', { style: { fontSize: 12, fontWeight: 700, color: C.text } }, (isW ? 'Worker' : (isDec ? '🧩 拆解官' : 'Verifier')) + ' · 执行中'),
2625
2956
  React.createElement('span', { style: { marginLeft: 'auto', fontSize: 9, padding: '1px 6px', borderRadius: 3, background: C.brand, color: C_INV } }, '忙碌')),
2626
- m.model ? React.createElement('div', { style: { fontSize: 9, color: C.warn, marginBottom: 3 }, title: '异构模型审查' }, '🧬 ' + m.model) : null,
2957
+ 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,
2627
2958
  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),
2628
2959
  React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3 }, title: '一次性子代理会话:' + (m.runId || '') }, '会话 ' + String(m.runId || '').slice(0, 8) + '…(用完即销毁)'))
2629
2960
  }