dsh-subagent-profile 0.4.0 → 0.5.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
@@ -133,6 +133,25 @@ const CSS = [
133
133
  '.sap-adviceFoot{align-items:center;gap:10px;flex-wrap:wrap;display:flex;border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px;margin-top:2px}',
134
134
  '.sap-adviceFootNotes{flex-direction:column;gap:2px;display:flex;min-width:0}',
135
135
  '.sap-adviceFootNote{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0}',
136
+ '.sap-adviceStatusLine{align-items:center;gap:8px;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary);margin:10px 0 0;display:flex}',
137
+ '.sap-adviceStatusLineWarn{color:var(--dsw-alias-state-error-primary);font-weight:600}',
138
+ '.sap-adviceStatusDim{color:var(--dsw-alias-label-tertiary)}',
139
+ '.sap-candidateState{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);border-radius:3px;padding:1px 6px;white-space:nowrap;flex:none}',
140
+ '.sap-adviceTimelineLine{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}',
141
+ '.sap-adviceTimelineStep{color:var(--dsw-alias-label-secondary)}',
142
+ '.sap-adviceTimelineStepDim{color:var(--dsw-alias-label-dimmed)}',
143
+ '.sap-rollbackBox{border:1px solid var(--dsw-alias-state-error-secondary);background:var(--dsw-alias-interactive-bg-hover-danger);border-radius:6px;padding:10px 12px;flex-direction:column;gap:4px;display:flex;margin-top:12px}',
144
+ '.sap-rollbackBoxLine{color:var(--dsw-alias-label-secondary);font-size:12.5px;line-height:18px;margin:0}',
145
+ '.sap-rollbackBoxKeep{color:var(--dsw-alias-label-tertiary);font-size:12.5px;line-height:18px;margin:0}',
146
+ '.sap-rollbackBoxActions{display:flex;gap:8px;margin-top:6px}',
147
+ '.sap-rollbackDanger{border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:0 0}',
148
+ '.sap-applyModal{background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:10px;padding:12px 14px;margin-top:12px;display:flex}',
149
+ '.sap-applyModalHead{align-items:baseline;gap:8px;display:flex}',
150
+ '.sap-applyModalTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}',
151
+ '.sap-applyModalLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;margin:0}',
152
+ '.sap-applyModalLine{color:var(--dsw-alias-label-primary);font-size:12.5px;line-height:18px;margin:0}',
153
+ '.sap-applyModalLineDim{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}',
154
+ '.sap-applyModalActions{justify-content:flex-end;gap:8px;display:flex}',
136
155
  '.sap-candidateSettingsRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
137
156
  '.sap-candidateSettingsRow b{font-weight:600;color:var(--dsw-alias-label-primary)}',
138
157
  '.sap-candidateSettingsEdit{border:0;background:0 0;padding:0;margin:0;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-decoration:underline;text-underline-offset:3px}',
@@ -514,7 +533,7 @@ const ZH = {
514
533
  ledgerCostCardScope: '本会话口径',
515
534
  ledgerCostCardCounter: '若继承父会话',
516
535
  ledgerCostCardSave: '省',
517
- ledgerEvidenceToggle: '查看完整因果链 ▾',
536
+ ledgerEvidenceToggle: '查看完整因果链 ▸',
518
537
  ledgerEvidenceClose: '收起因果链 ▴',
519
538
  ledgerVsTitle: '请求 vs 生效',
520
539
  ledgerVsDiff: '{n} 项与请求不同(琥珀底行)',
@@ -548,14 +567,14 @@ const ZH = {
548
567
  ledgerExecLiveNote: '后台执行中:过程事件随活动更新',
549
568
  ledgerEventStart: '开始执行',
550
569
  ledgerEventCall: '第 {n} 次工具调用',
551
- ledgerEventStall: '⚠ 停滞 {s}s 无活动(超过阈值 {t}s)',
570
+ ledgerEventStall: '⚠ 停滞 {s}s 无活动(超过 {t}s 上限)',
552
571
  ledgerEventStalling: '⚠ 停滞中(已 {s}s 无活动)',
553
572
  ledgerEventIntervene: '主 agent 干预',
554
573
  ledgerEventResume: '恢复执行',
555
574
  ledgerEventDone: '完成',
556
575
  ledgerEventRunning: '执行中…',
557
576
  ledgerEventMoreCalls: '…其间还有 {n} 次调用',
558
- ledgerStallThresholdNote: '停滞阈值 30s(待实测)',
577
+ ledgerStallThresholdNote: '停滞上限 30s(待实测)',
559
578
  ledgerSettleOutput: '输出摘要',
560
579
  ledgerSettleCostBreakdown: '成本分解',
561
580
  ledgerSettleInheritNote: '未指定模型,按继承父会话估算',
@@ -592,9 +611,9 @@ const ZH = {
592
611
  adviceSectionDetail: '派发明细',
593
612
  adviceSwitchOn: '开(只提示,不自动改配置)',
594
613
  adviceSwitchOff: '关(只提示,不自动改配置)',
595
- advicePanelEmpty: '暂无建议(需累计 ≥3 次派发且加权成功分低于阈值)',
614
+ advicePanelEmpty: '暂无建议(近期派发表现不佳时才会生成)',
596
615
  advicePanelGlobal: '全局',
597
- advicePanelReadonly: '(仅提示:统计与建议仅供参考,不改变派发行为)',
616
+ advicePanelReadonly: '(仅提示:统计与建议不改变派发行为)',
598
617
  adviceDirUp: '可上调预算',
599
618
  adviceDirDown: '建议降级',
600
619
  adviceDirFlat: '维持现状',
@@ -633,8 +652,8 @@ const ZH = {
633
652
  candidateTitle: '候选方案',
634
653
  candidateNotApplied: '(未应用)',
635
654
  candidateCountSuffix: ' 个 · 单选',
636
- candidateCountNote: ' · 仅参考,不会自动应用',
637
- candidateIntro: '系统基于近期表现生成的改进方案,仅参考、不自动应用。',
655
+ candidateCountNote: ' · 应用前需你确认',
656
+ candidateIntro: '系统基于近期表现生成的改进方案;应用前需要你确认,并进入观察期。',
638
657
  candidateRegenerate: '重新生成候选',
639
658
  candidateSafetyPending: '安全检查:正在核对…',
640
659
  candidateGroupChanges: '改动',
@@ -643,6 +662,40 @@ const ZH = {
643
662
  candidateSafetyPass: '全部通过 ✓',
644
663
  candidateSafetyFail: '存在拒绝项',
645
664
  candidateNotApplicable: '应用功能在后续版本开放(届时会先经你确认)',
665
+ // 观察期/撤销建议/确认流(用户可见面零内部词:观察中/表现变差/撤销/调整前)
666
+ adviceStatusObservingSuffix: '——表现下降会提醒你撤销',
667
+ adviceApplyConfirm: '确认调整方案',
668
+ adviceApplyCancel: '取消',
669
+ adviceApplyModalSummary: '候选摘要',
670
+ adviceApplyModalImpact: '成本与能力影响',
671
+ adviceApplyImpactDown: '能力配置降级,工具面不扩大,成本通常更低',
672
+ adviceApplyImpactUp: '预算上调,成本可能上升',
673
+ adviceApplyImpactFlat: '能力配置不变,成本不受影响',
674
+ adviceApplySafetyNote: '安全检查:',
675
+ adviceApplyDefaultNote: '本建议改的是方案默认值;派发时显式参数仍会覆盖它',
676
+ adviceApplyObserveNote: '应用后进入观察期(5 次派发),表现下降时会提醒你撤销,不会自动修改',
677
+ adviceApplySuccess: '已应用,进入观察期',
678
+ adviceApplyObservingTitle: '观察中,暂不支持再次调整',
679
+ adviceApplySelectTitle: '请先选择一个候选方案',
680
+ adviceRollbackBoxAdvice: '建议撤销这次调整,恢复调整前的配置。',
681
+ adviceRollbackRestorePrefix: '恢复为:',
682
+ adviceRollbackEvidence: '对比:调整前表现分 {before} · 调整后 {after}(近 {n} 次派发)',
683
+ adviceRollbackKeepDone: '已选择:继续使用当前配置(已留痕)',
684
+ adviceRollbackBtn: '撤销上次调整',
685
+ adviceRollbackKeepBtn: '继续用当前方案',
686
+ adviceRollbackSuccess: '已恢复调整前配置,观察期终止',
687
+ adviceKeepSuccess: '已选择保留当前配置(已留痕)',
688
+ adviceRollbackModalTitle: '撤销这次调整?',
689
+ adviceRollbackModalRestore: '恢复为',
690
+ adviceRollbackModalChange: '会改变',
691
+ adviceRollbackModalNote: '若方案已被手动修改,将无法一键撤销;本次撤销会记入审计',
692
+ adviceRollbackConfirm: '确认撤销',
693
+ adviceRollbackChangeSuffix: '调整前自定义项一并恢复',
694
+ adviceTimelineTitle: '进化轨迹',
695
+ adviceTimelinePending: '待你处理',
696
+ adviceTimelineSep: ' → ',
697
+ adviceRestoreFallback: '调整前的完整配置',
698
+ adviceReminderView: '查看建议',
646
699
  candidateValidityForever: '⏱ 无期限',
647
700
  candidateValidityPrefix: '⏱ 至 ',
648
701
  candidateGeneratedPrefix: '生成于 ',
@@ -650,14 +703,14 @@ const ZH = {
650
703
  candidateSamplesSuffix: ' 次派发',
651
704
  candidatePerfPrefix: '近期表现 ',
652
705
  candidatePerfSuffix: '(满分 100)',
653
- candidateNoSample: '暂无近期派发样本,仅供参考',
706
+ candidateNoSample: '暂无近期派发样本',
654
707
  // 候选卡底部:无对应方案存来源配置为新方案;有对应方案调整按钮灰置 + 定位。
655
708
  adviceSaveProfile: '保存为方案',
656
709
  adviceApplyDisabled: '按建议调整方案',
657
710
  adviceSavedNotice: '已保存为新方案:{id}',
658
711
  adviceFootNoteSave: '仅保存预设、模型与提供方;自定义人格与工具过滤无法还原(隐私保护不落盘),如需完整配置请手动编辑方案。',
659
712
  adviceFootNoteMatchedPrefix: '对应方案:',
660
- // 建议摘要行(v8:对象/建议/置信度/加权成功分;展开后见理由与明细)
713
+ // 建议摘要行(v8:对象/建议/置信度;展开后见理由与明细)
661
714
  adviceNoProfileTag: '无方案',
662
715
  // 安全页签:安全保证卡 + 审计健康卡(只读明细表)
663
716
  safetyCardTitle: '安全保证',
@@ -1410,7 +1463,7 @@ function buildSubagentBanner(el, subagent, parentPreset, modelState, bannerMode)
1410
1463
  // (标记已读/采纳/忽略/拒绝/关闭)经 POST 回写 host 并刷新——一条提醒 = 一条审计记录。
1411
1464
  // sessions 服务供条目主/子会话标题查询与跳转;跳转失败静默(设置页不被导航异常打断)。
1412
1465
  function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1413
- function ReminderCenter() {
1466
+ function ReminderCenter(props) {
1414
1467
  const [reminders, setReminders] = useState([])
1415
1468
  const [error, setError] = useState('')
1416
1469
  const refresh = () => {
@@ -1422,7 +1475,8 @@ function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1422
1475
  const request = action === 'ack' ? api('/reminders/ack', { ids: [id] }) : api('/reminders/action', { id, action })
1423
1476
  request.then(refresh).catch((err) => setError(String((err && err.message) || err)))
1424
1477
  }
1425
- return buildReminderCenter(el, reminders, error, act, getSessions(), openChild)
1478
+ const jumpToAdvice = props !== null && typeof props === 'object' && typeof props.jumpToAdvice === 'function' ? props.jumpToAdvice : null
1479
+ return buildReminderCenter(el, reminders, error, act, getSessions(), openChild, jumpToAdvice)
1426
1480
  }
1427
1481
  return ReminderCenter
1428
1482
  }
@@ -1431,17 +1485,17 @@ function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1431
1485
  // 可展开区(原生 details,展开后仍按现有行渲染并保留已处理标注,可追溯)。审计留痕
1432
1486
  // 由 host 侧动作回写,已处理条目不再堆在默认列表里。全空才显示空态文案;仅剩已处理时
1433
1487
  // 只显示折叠区,不显示空态。
1434
- function buildReminderCenter(el, reminders, error, act, sessions, openChild) {
1488
+ function buildReminderCenter(el, reminders, error, act, sessions, openChild, jumpToAdvice) {
1435
1489
  const handled = reminders.filter((r) => typeof r.action === 'string' && r.action !== '')
1436
1490
  const pending = reminders.filter((r) => !(typeof r.action === 'string' && r.action !== ''))
1437
1491
  const pendingList = pending.length > 0
1438
- ? el('ul', { className: 'sap-rows' }, pending.map((r) => buildReminderRow(el, r, act, sessions, openChild)))
1492
+ ? el('ul', { className: 'sap-rows' }, pending.map((r) => buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice)))
1439
1493
  : null
1440
1494
  const handledList = handled.length > 0
1441
1495
  ? el('details', { className: 'sap-customized sap-customizedArrowEnd sap-reminderHandled' },
1442
1496
  el('summary', { className: 'sap-customizedSummary' }, '已处理(' + handled.length + ')', el('span', { className: 'sap-customizedEndArrow' }, '▸')),
1443
1497
  el('div', { className: 'sap-customizedBody' },
1444
- el('ul', { className: 'sap-rows' }, handled.map((r) => buildReminderRow(el, r, act, sessions, openChild)))
1498
+ el('ul', { className: 'sap-rows' }, handled.map((r) => buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice)))
1445
1499
  )
1446
1500
  )
1447
1501
  : null
@@ -1575,10 +1629,38 @@ function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1575
1629
  )
1576
1630
  }
1577
1631
 
1632
+ // 回滚建议提醒行:标题/说明只读 + 「查看建议」跳转到自进化与建议页签(只读跳转,
1633
+ // 无采纳/忽略按钮);与通知类提醒同款「标记已读/关闭」。
1634
+ function buildRollbackReminderRow(el, r, act, jumpToAdvice) {
1635
+ const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1636
+ const severityText = r.severity === 'P0' ? '紧急' : '注意'
1637
+ const jump = () => { if (typeof jumpToAdvice === 'function') { try { jumpToAdvice() } catch { /* 页签不可用:静默 */ } } }
1638
+ return el('li', { key: String(r.id), className: 'sap-rowCard' + (r.unread === true ? ' sap-reminderUnread' : '') },
1639
+ el('div', { className: 'sap-reminderHead' },
1640
+ el('span', { className: 'sap-chip ' + severityCls }, severityText),
1641
+ el('span', { className: 'sap-reminderTitle', title: r.title }, r.title)
1642
+ ),
1643
+ el('p', { className: 'sap-desc' }, r.detail),
1644
+ typeof r.action === 'string' && r.action !== ''
1645
+ ? null
1646
+ : el('div', { className: 'sap-reminderActions' }, [
1647
+ typeof jumpToAdvice === 'function' ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: jump }, ZH.adviceReminderView) : null,
1648
+ r.unread === true ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1649
+ r.severity !== 'P0' || r.unread !== true
1650
+ ? el('button', { type: 'button', className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1651
+ : null
1652
+ ]),
1653
+ typeof r.action === 'string' && r.action !== ''
1654
+ ? el('p', { className: 'sap-metaValueDefault' }, ZH.reminderActed.replace('{action}', ZH.reminderActionZh[r.action] || r.action))
1655
+ : null
1656
+ )
1657
+ }
1658
+
1578
1659
  // 通知类提醒行:P0 未读只保留「标记已读」(常驻至已读,不可跳过未读);已读后出现
1579
1660
  // 「关闭」(与 P1/P2 同);P1/P2 恒保留「标记已读 / 关闭」。已终态条目隐藏动作按钮,
1580
1661
  // 只显示已处理灰字标注(折叠区可追溯、防对终态条目误点产生重复审计)。
1581
- function buildReminderRow(el, r, act, sessions, openChild) {
1662
+ function buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice) {
1663
+ if (r.kind === 'suggestion-rollback') return buildRollbackReminderRow(el, r, act, jumpToAdvice)
1582
1664
  if (r.kind === 'adoption-false') return buildUnadoptedReminderRow(el, r, act, sessions, openChild)
1583
1665
  const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1584
1666
  const severityText = r.severity === 'P0' ? '紧急' : '注意'
@@ -2657,6 +2739,45 @@ function makeCandidateSettingsActions(api, s, refresh, applyWrite) {
2657
2739
  }
2658
2740
  }
2659
2741
 
2742
+ // 应用确认流动作:按建议调整方案(POST /evolution/apply,humanConfirmed 由确认按钮
2743
+ // 触发)、撤销上次调整(action=rollback)、继续用当前方案(action=keep)。成功即刷新
2744
+ // 并置顶部成功提示;失败原因回显在各自弹窗内(不静默)。
2745
+ function makeApplyFlowActions(api, s, refresh) {
2746
+ const closeApply = () => s.setApplyModal((prev) => ({ ...(prev ?? {}), open: false, error: '' }))
2747
+ const closeRollback = () => s.setRollbackModal((prev) => ({ ...(prev ?? {}), open: false, error: '' }))
2748
+ const applyCandidate = () => {
2749
+ const modal = s.applyModal ?? {}
2750
+ if (modal.candidateId === '') return
2751
+ s.setError('')
2752
+ api('/evolution/apply', { assetId: modal.candidateId, human_confirmed: true })
2753
+ .then(() => { closeApply(); s.setSavedNotice(ZH.adviceApplySuccess); refresh() })
2754
+ .catch((err) => s.setApplyModal((prev) => ({ ...(prev ?? {}), error: String((err && err.message) || err) })))
2755
+ }
2756
+ const rollbackAsset = () => {
2757
+ const modal = s.rollbackModal ?? {}
2758
+ if (modal.assetId === '') return
2759
+ s.setError('')
2760
+ api('/evolution/rollback', { assetId: modal.assetId, human_confirmed: true, action: 'rollback' })
2761
+ .then(() => { closeRollback(); s.setSavedNotice(ZH.adviceRollbackSuccess); refresh() })
2762
+ .catch((err) => s.setRollbackModal((prev) => ({ ...(prev ?? {}), error: String((err && err.message) || err) })))
2763
+ }
2764
+ const keepAsset = (assetId) => () => {
2765
+ s.setError('')
2766
+ api('/evolution/rollback', { assetId, human_confirmed: true, action: 'keep' })
2767
+ .then(() => { s.setSavedNotice(ZH.adviceKeepSuccess); refresh() })
2768
+ .catch((err) => s.setError(String((err && err.message) || err)))
2769
+ }
2770
+ return {
2771
+ openApplyModal: (profileKey, candidateId) => () => s.setApplyModal({ profileKey, candidateId, open: true, error: '' }),
2772
+ closeApplyModal: closeApply,
2773
+ applyCandidate,
2774
+ openRollbackModal: (assetId) => () => s.setRollbackModal({ assetId, open: true, error: '' }),
2775
+ closeRollbackModal: closeRollback,
2776
+ rollbackAsset,
2777
+ keepAsset,
2778
+ }
2779
+ }
2780
+
2660
2781
  function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools) {
2661
2782
  return {
2662
2783
  ...makeFormActions(s, loadEfforts, loadTools),
@@ -2666,6 +2787,7 @@ function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools
2666
2787
  ...makeEscapeActions(api, s),
2667
2788
  ...makeCandidateActions(api, s),
2668
2789
  ...makeCandidateSettingsActions(api, s, refresh, applyWrite),
2790
+ ...makeApplyFlowActions(api, s, refresh),
2669
2791
  ...makeTabActions(s),
2670
2792
  locateProfile: makeLocateProfile(s),
2671
2793
  }
@@ -2798,7 +2920,7 @@ function buildProfileRows(el, chip, s, actions) {
2798
2920
  ? el('p', { className: 'sap-costLine' },
2799
2921
  [
2800
2922
  typeof profile.avgCost === 'number' ? `平均 ≈${profile.avgCost} 元/次` : '',
2801
- typeof profile.successRate === 'number' ? `成功率 ${profile.successRate}${typeof profile.n === 'number' ? `(N=${profile.n})` : ''}` : ''
2923
+ typeof profile.successRate === 'number' ? `近期表现 ${Math.round(profile.successRate * 100)}/100${typeof profile.n === 'number' ? `(基于最近 ${profile.n} 次派发)` : ''}` : ''
2802
2924
  ].filter(Boolean).join(' · '))
2803
2925
  : null
2804
2926
  )
@@ -2907,14 +3029,20 @@ function candidateValidityText(expiry) {
2907
3029
  return ZH.candidateValidityPrefix + formatMonthDayTime(expiry)
2908
3030
  }
2909
3031
 
2910
- // 指标行:把「平均耗时 <ms>ms」换算成短时(40.4s 等),其余指标原样(信息不删)。
3032
+ // 指标行:把「平均耗时 <ms>ms」换算成短时(40.4s 等);第 1 个指标恒为成功分行,
3033
+ // 历史台账里的旧标签「加权成功分」在此显示层映射为「近期表现分」(值保持 0~1
3034
+ // 原样不改口径,0~100 视图在候选生成信息行)。其余指标原样(信息不删)。
2911
3035
  function adviceMetricsOf(performanceText) {
2912
- return adviceEvidenceOf(performanceText).metrics.map((metric) => {
3036
+ return adviceEvidenceOf(performanceText).metrics.map((metric, index) => {
2913
3037
  const hit = /^平均耗时 (.+)$/.exec(metric)
2914
3038
  if (hit !== null) {
2915
3039
  const ms = Number(hit[1])
2916
3040
  return Number.isFinite(ms) ? '平均耗时 ' + formatDurationMs(ms) : metric
2917
3041
  }
3042
+ if (index === 0) {
3043
+ const space = metric.indexOf(' ')
3044
+ return space > 0 ? '近期表现分 ' + metric.slice(space + 1) : metric
3045
+ }
2918
3046
  return metric
2919
3047
  })
2920
3048
  }
@@ -2968,7 +3096,7 @@ function dispatchModeZh(mode) {
2968
3096
  return mode
2969
3097
  }
2970
3098
 
2971
- // 表现摘要拆两层:计数(完成/失败/终止)与指标(加权成功分/平均耗时/平均输出),
3099
+ // 表现摘要拆两层:计数(完成/失败/终止)与指标(近期表现分/平均耗时/平均输出),
2972
3100
  // 各渲染一遍;解析不出的部分原样保留(信息不删)。
2973
3101
  function adviceEvidenceOf(performanceText) {
2974
3102
  const text = typeof performanceText === 'string' ? performanceText : ''
@@ -3058,13 +3186,19 @@ function adviceSectionOf(el, title, children) {
3058
3186
  }
3059
3187
 
3060
3188
  // 单张建议卡(v10):摘要行 = 对象/建议/置信度/▸(结论三件套,无加权分);
3061
- // 展开区 = 小节序列 理由 → 近期表现 → 派发明细 → 候选方案(未应用)→ 操作区。
3189
+ // 展开区 = 观察期状态行 → 小节序列(理由/近期表现/派发明细/候选方案)→ 撤销建议区
3190
+ // (regress)→ 进化轨迹 → 操作区;确认/撤销弹窗就地渲染在卡内。
3062
3191
  function buildAdviceCard(el, s, actions, a, candidates, sessions) {
3063
3192
  const profile = matchingProfileOf(s.profiles, a.profileKey)
3064
3193
  const metrics = adviceMetricsOf(a.performanceText)
3065
3194
  const rows = s.adviceDetails && typeof s.adviceDetails === 'object' && Array.isArray(s.adviceDetails[a.profileKey]) ? s.adviceDetails[a.profileKey] : []
3066
3195
  const openMap = s.adviceOpen !== null && typeof s.adviceOpen === 'object' ? s.adviceOpen : {}
3067
3196
  const open = openMap[a.profileKey] === true
3197
+ const assetView = adviceAssetViewOf(s, a.profileKey)
3198
+ const selectedId = candidateSelectedIdOf(s, a.profileKey, candidates)
3199
+ const selected = candidates.find((c) => c !== null && typeof c === 'object' && c.id === selectedId)
3200
+ const applyModal = s.applyModal ?? {}
3201
+ const rollbackModal = s.rollbackModal ?? {}
3068
3202
  const summary = el('button', { type: 'button', className: 'sap-adviceSummary', 'aria-expanded': open, onClick: actions.toggleAdviceOpen(a.profileKey) },
3069
3203
  el('span', { className: 'sap-adviceObjLine' },
3070
3204
  el('span', { className: 'sap-adviceObj' }, adviceSummaryObjectText(profile, a.profileKey)),
@@ -3075,11 +3209,16 @@ function buildAdviceCard(el, s, actions, a, candidates, sessions) {
3075
3209
  el('span', { className: 'sap-adviceConf' }, ZH.adviceConfidence + confidenceZhOf(a.confidence)),
3076
3210
  el('span', { className: 'sap-adviceSummaryArrow' }, '▸')))
3077
3211
  const detail = open !== true ? null : el('div', { className: 'sap-adviceDetail' },
3212
+ buildAdviceStatusLine(el, assetView),
3078
3213
  typeof a.reasonText === 'string' && a.reasonText !== '' ? adviceSectionOf(el, ZH.adviceSectionReason, el('p', { className: 'sap-adviceSectionBody' }, a.reasonText)) : null,
3079
3214
  metrics.length > 0 ? adviceSectionOf(el, ZH.adviceSectionPerf, el('p', { className: 'sap-adviceSectionBody' }, metrics.join(' · '))) : null,
3080
3215
  adviceSectionOf(el, ZH.adviceSectionDetail, buildAdviceDetails(el, s, actions, a.profileKey, rows, sessions)),
3081
3216
  candidates.length > 0 ? buildAdviceCandidates(el, s, actions, a, candidates) : null,
3082
- buildAdviceFoot(el, s, actions, a, profile, candidates))
3217
+ assetView !== null ? buildRollbackSuggestion(el, s, actions, assetView) : null,
3218
+ buildAdviceTimeline(el, assetView),
3219
+ buildAdviceFoot(el, s, actions, a, profile, candidates, assetView),
3220
+ applyModal.open === true && applyModal.profileKey === a.profileKey && selected !== undefined ? buildApplyModal(el, s, actions, a, profile, selected) : null,
3221
+ rollbackModal.open === true && assetView !== null && rollbackModal.assetId === assetView.id ? buildRollbackModal(el, s, actions, assetView) : null)
3083
3222
  return el('li', { key: String(a.profileKey), className: 'sap-adviceItem' + (open === true ? ' sap-adviceItemOpen' : '') }, summary, detail)
3084
3223
  }
3085
3224
 
@@ -3113,6 +3252,173 @@ function buildAdviceDetails(el, s, actions, profileKey, rows, sessions) {
3113
3252
  )
3114
3253
  }
3115
3254
 
3255
+ // ── 观察期/撤销建议/进化轨迹(数据源 = /evolution/assets 派生视图)──────────
3256
+
3257
+ // 建议卡对应资产视图:同键资产取最近一条(观察期/已应用优先于纯草稿);无资产 → null。
3258
+ function adviceAssetViewOf(s, profileKey) {
3259
+ const assets = Array.isArray(s.evolutionAssets) ? s.evolutionAssets : []
3260
+ const owned = assets.filter((a) => a && typeof a === 'object' && a.profile_key === profileKey)
3261
+ if (owned.length === 0) return null
3262
+ const rank = (a) => (a && a.canary && typeof a.canary === 'object' && a.canary.state !== 'pendingApply' ? 1 : 0)
3263
+ return owned.sort((a, b) => rank(b) - rank(a) || (Number.isFinite(b.updatedAt) ? b.updatedAt : 0) - (Number.isFinite(a.updatedAt) ? a.updatedAt : 0))[0]
3264
+ }
3265
+
3266
+ // 候选行徽标对应资产(按候选 id = 资产 id)。
3267
+ function candidateAssetByIdOf(s, id) {
3268
+ const assets = Array.isArray(s.evolutionAssets) ? s.evolutionAssets : []
3269
+ return assets.find((a) => a && a.id === id) ?? null
3270
+ }
3271
+
3272
+ // 观察期状态行:regress 警示强调(结论先行);其余次级色;pendingApply 不显示。
3273
+ function buildAdviceStatusLine(el, view) {
3274
+ const canary = view !== null && typeof view === 'object' ? view.canary : null
3275
+ if (canary === null || typeof canary !== 'object' || canary.state === 'pendingApply') return null
3276
+ const warn = canary.state === 'regress'
3277
+ const suffix = canary.state === 'observing' ? ZH.adviceStatusObservingSuffix : ''
3278
+ return el('div', { className: 'sap-adviceStatusLine' + (warn ? ' sap-adviceStatusLineWarn' : '') },
3279
+ el('span', null, String(canary.label ?? '') + suffix))
3280
+ }
3281
+
3282
+ // 恢复目标名:fromSnapshot 方案名列表;空快照回退通用文案(回滚 = 移除应用方案)。
3283
+ function rollbackRestoreNameOf(asset) {
3284
+ const snapshot = asset !== null && typeof asset === 'object' ? asset.fromSnapshot : null
3285
+ const profiles = snapshot !== null && typeof snapshot === 'object' && Array.isArray(snapshot.profiles) ? snapshot.profiles : []
3286
+ const names = profiles.map((p) => (p && p.config && typeof p.config.name === 'string' && p.config.name !== '' ? p.config.name : (p && p.config && p.config.id))).filter((n) => typeof n === 'string' && n !== '')
3287
+ return names.length > 0 ? names.join('、') : ZH.adviceRestoreFallback
3288
+ }
3289
+
3290
+ // 表现分 → 百分制整数(证据对照行口径,与「满分 100」一致)。
3291
+ function adviceScore100(score) {
3292
+ return Number.isFinite(score) ? Math.round(score * 100) : '—'
3293
+ }
3294
+
3295
+ function rollbackEvidenceTextOf(asset) {
3296
+ const canary = asset !== null && typeof asset === 'object' && asset.canary !== null && typeof asset.canary === 'object' ? asset.canary : {}
3297
+ const post = canary.post !== null && typeof canary.post === 'object' ? canary.post : null
3298
+ const base = canary.baseline !== null && typeof canary.baseline === 'object' ? canary.baseline : null
3299
+ const n = post !== null && Number.isFinite(post.sampleN) ? post.sampleN : 0
3300
+ return ZH.adviceRollbackEvidence.replace('{before}', adviceScore100(base === null ? null : base.weighted_success)).replace('{after}', adviceScore100(post === null ? null : post.weighted_success)).replace('{n}', n)
3301
+ }
3302
+
3303
+ function tierZhOf(tier) {
3304
+ return tier === 'cheap' ? '低' : tier === 'premium' ? '高' : '中'
3305
+ }
3306
+
3307
+ // 撤销确认弹窗「会改变」行:模型/预算档位/推理强度变化 + 自定义项一并恢复。
3308
+ function rollbackChangeTextOf(asset) {
3309
+ const parts = []
3310
+ const applied = asset !== null && typeof asset === 'object' && asset.applied_config !== null && typeof asset.applied_config === 'object' ? asset.applied_config : {}
3311
+ const snapshot = asset !== null && typeof asset === 'object' && asset.fromSnapshot !== null && typeof asset.fromSnapshot === 'object' && Array.isArray(asset.fromSnapshot.profiles) ? asset.fromSnapshot.profiles : []
3312
+ const before = snapshot.length > 0 && snapshot[0].config !== null && typeof snapshot[0].config === 'object' ? snapshot[0].config : {}
3313
+ if (before.model !== undefined || applied.model !== undefined) parts.push('模型 ' + (before.model || '未指定') + ' → ' + (applied.model || '未指定'))
3314
+ if (before.tokenTier !== undefined && applied.tokenTier !== undefined && before.tokenTier !== applied.tokenTier) {
3315
+ parts.push('预算档位 ' + tierZhOf(before.tokenTier) + ' → ' + tierZhOf(applied.tokenTier) + ',成本可能回升')
3316
+ } else if (before.reasoningEffort !== undefined && applied.reasoningEffort !== undefined && before.reasoningEffort !== applied.reasoningEffort) {
3317
+ parts.push('推理强度已上调,成本可能回升')
3318
+ }
3319
+ parts.push(ZH.adviceRollbackChangeSuffix)
3320
+ return parts.join(';')
3321
+ }
3322
+
3323
+ // 撤销建议区(regress):结论先行在状态行,本区 = 建议/恢复为/证据 + 双按钮;
3324
+ // 用户已选择保留 → 只读「已选择」行;无表态前常驻只读(不自动执行任何动作)。
3325
+ function buildRollbackSuggestion(el, s, actions, asset) {
3326
+ const canary = asset !== null && typeof asset === 'object' && asset.canary !== null && typeof asset.canary === 'object' ? asset.canary : {}
3327
+ if (canary.state !== 'regress') return null
3328
+ if (canary.kept === true) {
3329
+ return el('div', { className: 'sap-rollbackBox' }, el('p', { className: 'sap-rollbackBoxKeep' }, ZH.adviceRollbackKeepDone))
3330
+ }
3331
+ return el('div', { className: 'sap-rollbackBox' },
3332
+ el('p', { className: 'sap-applyModalLine' }, ZH.adviceRollbackBoxAdvice),
3333
+ el('p', { className: 'sap-rollbackBoxLine' }, ZH.adviceRollbackRestorePrefix, el('b', null, rollbackRestoreNameOf(asset))),
3334
+ el('p', { className: 'sap-rollbackBoxLine' }, rollbackEvidenceTextOf(asset)),
3335
+ el('div', { className: 'sap-rollbackBoxActions' },
3336
+ el('button', { type: 'button', className: 'sap-secondaryButton sap-rollbackDanger', style: smallBtn, onClick: actions.openRollbackModal(asset.id) }, ZH.adviceRollbackBtn),
3337
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.keepAsset(asset.id) }, ZH.adviceRollbackKeepBtn)))
3338
+ }
3339
+
3340
+ // 成本与能力影响行(确认弹窗):降级/上调方向 + 预算档位变化。
3341
+ function applyImpactTextOf(profile, candidate, suggestion) {
3342
+ const cfg = candidate !== null && typeof candidate === 'object' && candidate.config !== null && typeof candidate.config === 'object' ? candidate.config : {}
3343
+ const parts = [suggestion === '降' ? ZH.adviceApplyImpactDown : suggestion === '升' ? ZH.adviceApplyImpactUp : ZH.adviceApplyImpactFlat]
3344
+ if (profile !== null && cfg.tokenTier !== undefined && profile.tokenTier !== undefined && cfg.tokenTier !== profile.tokenTier) {
3345
+ parts.push('预算档位 ' + tierZhOf(profile.tokenTier) + ' → ' + tierZhOf(cfg.tokenTier))
3346
+ }
3347
+ return parts.join(';')
3348
+ }
3349
+
3350
+ // 应用确认弹窗:候选摘要(名称/方向/安全检查)/成本与能力影响/两条说明行;
3351
+ // 主按钮「确认调整方案」即执行(humanConfirmed 由点击触发,无强制勾选),失败红字回显。
3352
+ function buildApplyModal(el, s, actions, a, profile, candidate) {
3353
+ const modal = s.applyModal ?? {}
3354
+ const preview = s.draftPreviews !== null && typeof s.draftPreviews === 'object' ? s.draftPreviews[candidate.id] : undefined
3355
+ const checks = preview !== undefined && preview.checks !== null && typeof preview.checks === 'object' && Array.isArray(preview.checks) ? preview.checks : []
3356
+ const safety = preview === undefined ? ZH.candidateSafetyPending
3357
+ : preview.ok === true ? ZH.adviceApplySafetyNote + ZH.candidateSafetyPass.replace(' ✓', '') + ' ' + checks.length + '/' + checks.length
3358
+ : ZH.adviceApplySafetyNote + ZH.candidateSafetyFail
3359
+ return el('div', { className: 'sap-applyModal' },
3360
+ el('div', { className: 'sap-applyModalHead' }, el('span', { className: 'sap-applyModalTitle' }, ZH.adviceApplyConfirm)),
3361
+ el('div', null,
3362
+ el('p', { className: 'sap-applyModalLabel' }, ZH.adviceApplyModalSummary),
3363
+ el('p', { className: 'sap-applyModalLine' }, candidate.name + ' · ' + directionZh(a.suggestion)),
3364
+ el('p', { className: 'sap-applyModalLineDim' }, safety)),
3365
+ el('div', null,
3366
+ el('p', { className: 'sap-applyModalLabel' }, ZH.adviceApplyModalImpact),
3367
+ el('p', { className: 'sap-applyModalLine' }, applyImpactTextOf(profile, candidate, a.suggestion))),
3368
+ el('p', { className: 'sap-applyModalLineDim' }, ZH.adviceApplyDefaultNote),
3369
+ el('p', { className: 'sap-applyModalLineDim' }, ZH.adviceApplyObserveNote),
3370
+ modal.error !== '' ? el('p', { className: 'sap-notice' }, modal.error) : null,
3371
+ el('div', { className: 'sap-applyModalActions' },
3372
+ el('button', { type: 'button', className: 'sap-secondaryButton', onClick: actions.closeApplyModal }, ZH.adviceApplyCancel),
3373
+ el('button', { type: 'button', className: 'sap-primaryButton', onClick: actions.applyCandidate }, ZH.adviceApplyConfirm)))
3374
+ }
3375
+
3376
+ // 撤销确认弹窗:恢复为/会改变/说明 + 确认撤销(朱砂描边)/取消;失败红字回显。
3377
+ function buildRollbackModal(el, s, actions, asset) {
3378
+ const modal = s.rollbackModal ?? {}
3379
+ return el('div', { className: 'sap-applyModal' },
3380
+ el('div', { className: 'sap-applyModalHead' }, el('span', { className: 'sap-applyModalTitle' }, ZH.adviceRollbackModalTitle)),
3381
+ el('div', null,
3382
+ el('p', { className: 'sap-applyModalLabel' }, ZH.adviceRollbackModalRestore),
3383
+ el('p', { className: 'sap-applyModalLine' }, rollbackRestoreNameOf(asset))),
3384
+ el('div', null,
3385
+ el('p', { className: 'sap-applyModalLabel' }, ZH.adviceRollbackModalChange),
3386
+ el('p', { className: 'sap-applyModalLine' }, rollbackChangeTextOf(asset))),
3387
+ el('p', { className: 'sap-applyModalLineDim' }, ZH.adviceRollbackModalNote),
3388
+ modal.error !== '' ? el('p', { className: 'sap-notice' }, modal.error) : null,
3389
+ el('div', { className: 'sap-applyModalActions' },
3390
+ el('button', { type: 'button', className: 'sap-secondaryButton', onClick: actions.closeRollbackModal }, ZH.adviceApplyCancel),
3391
+ el('button', { type: 'button', className: 'sap-secondaryButton sap-rollbackDanger', onClick: actions.rollbackAsset }, ZH.adviceRollbackConfirm)))
3392
+ }
3393
+
3394
+ // 轨迹单事件文案:生成(MM-DD)/应用(MM-DD · 人工确认)/观察(k/kTotal)/
3395
+ // 判定:…/撤销·保留:…(未发生步骤灰字由 buildAdviceTimeline 补)。
3396
+ function timelineEventTextOf(event) {
3397
+ if (event.event === 'generated') return '生成(' + formatMonthDayTime(event.ts) + ')'
3398
+ if (event.event === 'applied') return '应用(' + formatMonthDayTime(event.ts) + (typeof event.meta === 'string' && event.meta !== '' ? ' · ' + event.meta : '') + ')'
3399
+ if (event.event === 'observed') return '观察(' + (typeof event.meta === 'string' ? event.meta : '') + ')'
3400
+ if (event.event === 'judged') return '判定:' + (typeof event.meta === 'string' && event.meta !== '' ? event.meta : ZH.adviceTimelinePending)
3401
+ if (event.event === 'resolved') return '撤销/保留:' + (typeof event.meta === 'string' && event.meta !== '' ? event.meta : ZH.adviceTimelinePending)
3402
+ return event.event
3403
+ }
3404
+
3405
+ // 进化轨迹小节:assets lifecycle 行式时间线;未应用(无 applied 事件)或缺生命周期
3406
+ // 数据 → 隐藏小节。未发生步骤(判定/处理)灰字补位。
3407
+ function buildAdviceTimeline(el, view) {
3408
+ const lifecycle = view !== null && typeof view === 'object' && Array.isArray(view.lifecycle) ? view.lifecycle : []
3409
+ if (lifecycle.length === 0 || !lifecycle.some((e) => e.event === 'applied')) return null
3410
+ const chain = []
3411
+ const push = (text, dim) => chain.push(el('span', { key: String(chain.length), className: dim === true ? 'sap-adviceTimelineStepDim' : 'sap-adviceTimelineStep' }, text))
3412
+ for (const event of lifecycle) {
3413
+ if (chain.length > 0) chain.push(el('span', { key: 'sep' + chain.length }, ZH.adviceTimelineSep))
3414
+ push(timelineEventTextOf(event), false)
3415
+ }
3416
+ const has = (kind) => lifecycle.some((e) => e.event === kind)
3417
+ if (!has('judged')) { chain.push(el('span', { key: 'sepd' }, ZH.adviceTimelineSep)); push('判定:' + ZH.adviceTimelinePending, true) }
3418
+ if (!has('resolved')) { chain.push(el('span', { key: 'sepr' }, ZH.adviceTimelineSep)); push('撤销/保留:' + ZH.adviceTimelinePending, true) }
3419
+ return adviceSectionOf(el, ZH.adviceTimelineTitle, el('p', { className: 'sap-adviceTimelineLine' }, chain))
3420
+ }
3421
+
3116
3422
  // 候选管理设置(顶部一行 + 就地浮层):更新模式与有效期用户可配;
3117
3423
  // 自定义按天数输入(1~365),提交时换算小时数。
3118
3424
  function candidateModeTextOf(mode) {
@@ -3207,10 +3513,13 @@ function buildAdviceCandidateRow(el, s, actions, a, candidate, index, selected)
3207
3513
  if (event && typeof event.stopPropagation === 'function') event.stopPropagation()
3208
3514
  actions.selectCandidate(a.profileKey, candidate.id)()
3209
3515
  }
3516
+ const asset = candidateAssetByIdOf(s, candidate.id)
3517
+ const stateLabel = asset !== null && typeof asset.stateLabel === 'string' && asset.stateLabel !== '' ? asset.stateLabel : null
3210
3518
  return el('div', { key: String(candidate.id), className: 'sap-candidate' + (selected === candidate.id ? ' sap-candidateSelected' : '') },
3211
3519
  el('button', { type: 'button', className: 'sap-candidateRow', 'aria-expanded': open, onClick: actions.toggleCandidateOpen(candidate) },
3212
3520
  el('span', { role: 'radio', 'aria-checked': selected === candidate.id, className: 'sap-candidateRadio', onClick: onRadio }),
3213
3521
  el('span', { className: 'sap-candidateName' }, '候选 ' + (index + 1) + ' · ' + candidate.name),
3522
+ stateLabel !== null ? el('span', { className: 'sap-candidateState' }, stateLabel) : null,
3214
3523
  el('span', { className: 'sap-candidateValidity' }, candidateValidityText(candidate.expiry)),
3215
3524
  el('span', { className: 'sap-candidateArrow' }, '▸')),
3216
3525
  open === true ? el('div', { className: 'sap-candidateBody' },
@@ -3233,7 +3542,7 @@ function buildAdviceCandidateRow(el, s, actions, a, candidate, index, selected)
3233
3542
  }
3234
3543
 
3235
3544
  // 生成信息行(最浅灰,两分支模板):有样本 = 生成于 MM-DD · 基于最近 N 次派发 ·
3236
- // 近期表现 X(满分 100,X=round(加权分×100));无样本 = 生成于 MM-DD · 暂无近期派发
3545
+ // 近期表现 X(满分 100,X=round(表现分×100));无样本 = 生成于 MM-DD · 暂无近期派发
3237
3546
  // 样本,仅供参考(不显示 0 分);无生成时间省去「生成于」段。
3238
3547
  function candidateGeneratedLine(el, candidate) {
3239
3548
  const basis = candidate.basis !== null && typeof candidate.basis === 'object' ? candidate.basis : {}
@@ -3251,24 +3560,36 @@ function candidateGeneratedLine(el, candidate) {
3251
3560
  return el('p', { className: 'sap-candidateHint' }, parts.join(' · '))
3252
3561
  }
3253
3562
 
3254
- // 卡底部:无对应方案 = [保存为方案](统一主按钮)+ 备注两行;有对应方案 =
3255
- // 灰置[按建议调整方案](统一次级按钮)+ 定位;说明与「生成于…」拆两行不连排。
3256
- function buildAdviceFoot(el, s, actions, a, profile, candidates) {
3563
+ // 卡底部:无对应方案 = [保存为方案](统一主按钮)+ 备注;有对应方案 = [按建议调整
3564
+ // 方案] + 定位。按钮启用条件 = applyEnabled && 已选候选;禁用 title = 禁用原因
3565
+ // (冷启动 = reasons.join(';') 并附卡脚一行灰字;观察中 = 暂不支持再次调整)。
3566
+ function buildAdviceFoot(el, s, actions, a, profile, candidates, assetView) {
3257
3567
  const basis = (Array.isArray(candidates) && candidates.length > 0 && candidates[0] !== null && typeof candidates[0] === 'object' && candidates[0].basis !== null && typeof candidates[0].basis === 'object') ? candidates[0].basis : {}
3258
3568
  const generatedAt = formatMonthDayTime(basis.generatedAt)
3259
3569
  const generatedText = generatedAt !== '' ? ZH.candidateGeneratedPrefix + generatedAt : ''
3260
- const noteLines = (line) => el('div', { className: 'sap-adviceFootNotes' },
3261
- el('p', { className: 'sap-adviceFootNote' }, line),
3262
- generatedText !== '' ? el('p', { className: 'sap-adviceFootNote' }, generatedText) : null)
3263
- if (profile !== null) {
3570
+ const noteLines = (lines) => el('div', { className: 'sap-adviceFootNotes' }, lines.map((line) => el('p', { key: line, className: 'sap-adviceFootNote' }, line)))
3571
+ if (profile === null) {
3264
3572
  return el('div', { className: 'sap-adviceFoot' },
3265
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, disabled: true, title: ZH.candidateNotApplicable }, ZH.adviceApplyDisabled),
3266
- el('button', { type: 'button', className: 'sap-detailToggle', onClick: actions.locateProfile(profile.id) }, ZH.adviceRowLocate),
3267
- noteLines(ZH.adviceFootNoteMatchedPrefix + (profile.name || profile.id) + '(' + profile.id + ')'))
3268
- }
3573
+ el('button', { type: 'button', className: 'sap-primaryButton', style: smallBtn, onClick: actions.saveAdviceProfile(a.profileKey) }, ZH.adviceSaveProfile),
3574
+ noteLines([ZH.adviceFootNoteSave]))
3575
+ }
3576
+ const status = s.evolutionStatus ?? {}
3577
+ const applyEnabled = status.applyEnabled === true
3578
+ const selectedId = candidateSelectedIdOf(s, a.profileKey, candidates)
3579
+ const canaryState = assetView !== null && assetView.canary !== null && typeof assetView.canary === 'object' ? assetView.canary.state : 'pendingApply'
3580
+ const observing = canaryState === 'observing' || canaryState === 'stalled' || canaryState === 'inconclusive' || canaryState === 'regress' || canaryState === 'aborted'
3581
+ let disabledTitle = ''
3582
+ if (observing) disabledTitle = ZH.adviceApplyObservingTitle
3583
+ else if (!applyEnabled) disabledTitle = Array.isArray(status.reasons) && status.reasons.length > 0 ? status.reasons.join(';') : ZH.candidateNotApplicable
3584
+ else if (selectedId === '') disabledTitle = ZH.adviceApplySelectTitle
3585
+ const disabled = disabledTitle !== ''
3586
+ const notes = [ZH.adviceFootNoteMatchedPrefix + (profile.name || profile.id) + '(' + profile.id + ')']
3587
+ if (generatedText !== '') notes.push(generatedText)
3588
+ if (!applyEnabled && !observing) notes.push(disabledTitle)
3269
3589
  return el('div', { className: 'sap-adviceFoot' },
3270
- el('button', { type: 'button', className: 'sap-primaryButton', style: smallBtn, onClick: actions.saveAdviceProfile(a.profileKey) }, ZH.adviceSaveProfile),
3271
- noteLines(ZH.adviceFootNoteSave))
3590
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, disabled, title: disabledTitle, onClick: disabled ? undefined : actions.openApplyModal(a.profileKey, selectedId) }, ZH.adviceApplyDisabled),
3591
+ el('button', { type: 'button', className: 'sap-detailToggle', onClick: actions.locateProfile(profile.id) }, ZH.adviceRowLocate),
3592
+ noteLines(notes))
3272
3593
  }
3273
3594
 
3274
3595
  // 逃生舱卡片:开关 + (打开时)显式警告文案 + 放行集增删(输入框 + 添加按钮,
@@ -3562,7 +3883,7 @@ function buildSectionReturn(el, s, actions, rows, addCard, reminderCenter, sessi
3562
3883
  ], true)),
3563
3884
  buildTabZone(el, s, 'advice', buildAdvicePanel(el, s, actions, sessions)),
3564
3885
  buildTabZone(el, s, 'safety', buildSafetySection(el, s, actions, s.auditReminders)),
3565
- buildTabZone(el, s, 'reminders', reminderCenter)
3886
+ buildTabZone(el, s, 'reminders', el(reminderCenter, { jumpToAdvice: () => s.setActiveTab('advice') }))
3566
3887
  )
3567
3888
  }
3568
3889
 
@@ -3604,18 +3925,17 @@ function attachDraftActions(api, s, refresh, applyWrite, actions) {
3604
3925
  function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter, getSessions) {
3605
3926
  function ProfilesSection() {
3606
3927
  const [profiles, setProfiles] = useState([]); const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] }); const [enabled, setEnabled] = useState(true)
3607
- const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [error, setError] = useState('')
3608
- const [savedNotice, setSavedNotice] = useState(''); const [persistWarning, setPersistWarning] = useState(''); const [form, setForm] = useState(EMPTY_FORM)
3609
- const [tools, setTools] = useState([]); const [versionWarnings, setVersionWarnings] = useState([]); const [pluginVersion, setPluginVersion] = useState(''); const [audit, setAudit] = useState(AUDIT_DEFAULTS)
3610
- const [escapeEnabled, setEscapeEnabled] = useState(false); const [escapePresets, setEscapePresets] = useState([]); const [escapeInput, setEscapeInput] = useState('')
3928
+ const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [error, setError] = useState(''); const [savedNotice, setSavedNotice] = useState('')
3929
+ const [persistWarning, setPersistWarning] = useState(''); const [form, setForm] = useState(EMPTY_FORM); const [tools, setTools] = useState([]); const [versionWarnings, setVersionWarnings] = useState([])
3930
+ const [pluginVersion, setPluginVersion] = useState(''); const [audit, setAudit] = useState(AUDIT_DEFAULTS); const [escapeEnabled, setEscapeEnabled] = useState(false); const [escapePresets, setEscapePresets] = useState([]); const [escapeInput, setEscapeInput] = useState('')
3611
3931
  const [evolutionAdvice, setEvolutionAdvice] = useState(false); const [drafts, setDrafts] = useState([]); const [advice, setAdvice] = useState([]); const [evolutionCandidates, setEvolutionCandidates] = useState([])
3612
- const [adviceGlobal, setAdviceGlobal] = useState(null); const [costSummary, setCostSummary] = useState({ global: null, byModel: [] }); const [draftPreviews, setDraftPreviews] = useState({})
3613
- const [candidateOpen, setCandidateOpen] = useState({}); const [candidateSelected, setCandidateSelected] = useState({}); const [candidateSafetyOpen, setCandidateSafetyOpen] = useState({})
3614
- const [candidateSettings, setCandidateSettings] = useState({ candidateMode: 'auto', candidateTtlH: 24 }); const [candidateSettingsOpen, setCandidateSettingsOpen] = useState(false)
3932
+ const [adviceGlobal, setAdviceGlobal] = useState(null); const [costSummary, setCostSummary] = useState({ global: null, byModel: [] }); const [draftPreviews, setDraftPreviews] = useState({}); const [candidateOpen, setCandidateOpen] = useState({})
3933
+ const [candidateSelected, setCandidateSelected] = useState({}); const [candidateSafetyOpen, setCandidateSafetyOpen] = useState({}); const [candidateSettings, setCandidateSettings] = useState({ candidateMode: 'auto', candidateTtlH: 24 }); const [candidateSettingsOpen, setCandidateSettingsOpen] = useState(false)
3615
3934
  const [candidateSettingsForm, setCandidateSettingsForm] = useState({ candidateMode: 'auto', candidateTtlChoice: '24', customDays: '', error: '' }); const [adviceDetails, setAdviceDetails] = useState({}); const [detailOpen, setDetailOpen] = useState({})
3616
- const [activeTab, setActiveTab] = useState('profiles'); const [highlightId, setHighlightId] = useState(null)
3617
- const [adviceOpen, setAdviceOpen] = useState({}); const [auditDetailOpen, setAuditDetailOpen] = useState(false); const [auditReminders, setAuditReminders] = useState([])
3618
- const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, pluginVersion, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, drafts, advice, evolutionCandidates, adviceGlobal, costSummary, draftPreviews, candidateOpen, candidateSelected, candidateSafetyOpen, candidateSettings, candidateSettingsOpen, candidateSettingsForm, adviceDetails, detailOpen, activeTab, highlightId, adviceOpen, auditDetailOpen, auditReminders, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setPluginVersion, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice, setDrafts, setAdvice, setEvolutionCandidates, setAdviceGlobal, setCostSummary, setDraftPreviews, setCandidateOpen, setCandidateSelected, setCandidateSafetyOpen, setCandidateSettings, setCandidateSettingsOpen, setCandidateSettingsForm, setAdviceDetails, setDetailOpen, setActiveTab, setHighlightId, setAdviceOpen, setAuditDetailOpen, setAuditReminders }
3935
+ const [activeTab, setActiveTab] = useState('profiles'); const [highlightId, setHighlightId] = useState(null); const [adviceOpen, setAdviceOpen] = useState({}); const [auditDetailOpen, setAuditDetailOpen] = useState(false); const [auditReminders, setAuditReminders] = useState([])
3936
+ const [evolutionStatus, setEvolutionStatus] = useState({ applyEnabled: false, canaryEnabled: false, reasons: [] }); const [evolutionAssets, setEvolutionAssets] = useState([])
3937
+ const [applyModal, setApplyModal] = useState({ profileKey: '', candidateId: '', open: false, error: '' }); const [rollbackModal, setRollbackModal] = useState({ assetId: '', open: false, error: '' })
3938
+ const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, pluginVersion, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, drafts, advice, evolutionCandidates, adviceGlobal, costSummary, draftPreviews, candidateOpen, candidateSelected, candidateSafetyOpen, candidateSettings, candidateSettingsOpen, candidateSettingsForm, adviceDetails, detailOpen, activeTab, highlightId, adviceOpen, auditDetailOpen, auditReminders, evolutionStatus, evolutionAssets, applyModal, rollbackModal, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setPluginVersion, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice, setDrafts, setAdvice, setEvolutionCandidates, setAdviceGlobal, setCostSummary, setDraftPreviews, setCandidateOpen, setCandidateSelected, setCandidateSafetyOpen, setCandidateSettings, setCandidateSettingsOpen, setCandidateSettingsForm, setAdviceDetails, setDetailOpen, setActiveTab, setHighlightId, setAdviceOpen, setAuditDetailOpen, setAuditReminders, setEvolutionStatus, setEvolutionAssets, setApplyModal, setRollbackModal }
3619
3939
  const refresh = useCallback(() => {
3620
3940
  api('/list')
3621
3941
  .then((data) => {
@@ -3625,11 +3945,12 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, Re
3625
3945
  setAdviceGlobal(data.adviceGlobal !== null && typeof data.adviceGlobal === 'object' ? data.adviceGlobal : null)
3626
3946
  setAdviceDetails(data.adviceDetails !== null && typeof data.adviceDetails === 'object' ? data.adviceDetails : {})
3627
3947
  setCostSummary(data && typeof data.costSummary === 'object' && data.costSummary !== null && typeof data.costSummary.global === 'object' ? { global: data.costSummary.global, byModel: Array.isArray(data.costSummary.byModel) ? data.costSummary.byModel : [] } : { global: null, byModel: [] })
3948
+ const status = data.evolutionStatus !== null && typeof data.evolutionStatus === 'object' ? data.evolutionStatus : {}
3949
+ setEvolutionStatus({ applyEnabled: status.applyEnabled === true, canaryEnabled: status.canaryEnabled === true, reasons: Array.isArray(status.reasons) ? status.reasons : [] })
3628
3950
  })
3629
3951
  .catch((err) => setError(String((err && err.message) || err)))
3630
- api('/drafts')
3631
- .then((data) => setDrafts(Array.isArray(data.drafts) ? data.drafts : []))
3632
- .catch(() => {})
3952
+ api('/evolution/assets').then((data) => setEvolutionAssets(Array.isArray(data.assets) ? data.assets : [])).catch(() => {})
3953
+ api('/drafts').then((data) => setDrafts(Array.isArray(data.drafts) ? data.drafts : [])).catch(() => {})
3633
3954
  api('/reminders').then((data) => setAuditReminders(Array.isArray(data.reminders) ? data.reminders : [])).catch(() => {})
3634
3955
  }, [])
3635
3956
  const loadOptions = useCallback(() => {
@@ -3647,7 +3968,7 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, Re
3647
3968
  const applyWrite = makeApplyWrite(s); const actions = makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools)
3648
3969
  attachDraftActions(api, s, refresh, applyWrite, actions)
3649
3970
  const rows = buildProfileRows(el, chip, s, actions); const toolSection = buildToolSection(el, s, actions)
3650
- return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection), el(ReminderCenter), sessionsSnapshot(getSessions))
3971
+ return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection), ReminderCenter, sessionsSnapshot(getSessions))
3651
3972
  }
3652
3973
  return ProfilesSection
3653
3974
  }
@@ -3961,7 +4282,7 @@ function buildJsonBody(el, kv) {
3961
4282
 
3962
4283
  // ── 三层信息架构(结论层 / 证据层)纯渲染 ─────────────────────────
3963
4284
 
3964
- // 停滞检测阈值(待实测:measured-params 回填惯例,展示层同步标注「待实测」)。
4285
+ // 停滞检测上限(待实测:measured-params 回填惯例,展示层同步标注「待实测」)。
3965
4286
  const STALL_THRESHOLD_MS = 30000
3966
4287
 
3967
4288
  // 折叠态状态 chip:拒绝(红)/执行中(琥珀脉冲)/异常(琥珀)/成功(绿)。