dsh-subagent-profile 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +16 -57
  2. package/README.zh.md +16 -57
  3. package/index.mjs +97 -122
  4. package/lib/client.js +631 -88
  5. package/lib/core/cleanup.mjs +33 -0
  6. package/lib/core/data-maintenance.mjs +139 -0
  7. package/lib/core/decision-trace.mjs +20 -1
  8. package/lib/core/delegation.mjs +16 -0
  9. package/lib/core/dispatch-gates.mjs +122 -9
  10. package/lib/core/dispatch-schema.mjs +45 -7
  11. package/lib/core/dispatch-tool.mjs +30 -46
  12. package/lib/core/evolution-advice.mjs +31 -10
  13. package/lib/core/evolution-assets.mjs +13 -0
  14. package/lib/core/evolution-canary.mjs +427 -0
  15. package/lib/core/evolution-draft.mjs +21 -0
  16. package/lib/core/evolution-engine.mjs +176 -31
  17. package/lib/core/evolution-generate.mjs +7 -23
  18. package/lib/core/evolution-lag.mjs +78 -0
  19. package/lib/core/evolution-ledger.mjs +4 -1
  20. package/lib/core/evolution-routes.mjs +157 -16
  21. package/lib/core/http-routes.mjs +22 -7
  22. package/lib/core/model-policy.mjs +104 -0
  23. package/lib/core/presets-sync.mjs +24 -2
  24. package/lib/core/profile-directory.mjs +17 -0
  25. package/lib/core/profile-provider.mjs +7 -2
  26. package/lib/core/profiles-query.mjs +178 -0
  27. package/lib/core/profiles-store.mjs +40 -6
  28. package/lib/core/pure.mjs +1 -1
  29. package/lib/core/reminder-store.mjs +37 -3
  30. package/lib/core/snapshots.mjs +148 -0
  31. package/package.json +2 -2
  32. package/presets/orchestrator-v2/NOTICE +7 -0
  33. package/presets/{orchestrator → orchestrator-v2}/agent.cordis.yml +78 -7
  34. package/presets/orchestrator-v2/custom-bash.mjs +213 -0
  35. package/presets/orchestrator-v2/preset.yml +2 -0
  36. package/presets/orchestrator-v2/tool-bootstrap.mjs +620 -0
  37. package/presets/orchestrator/preset.yml +0 -2
package/lib/client.js CHANGED
@@ -23,6 +23,7 @@
23
23
 
24
24
  const CSS = [
25
25
  '.sap-section{width:100%;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}',
26
+ '.sap-zoneStack{flex-direction:column;gap:12px;display:flex}',
26
27
  '.sap-statusBar{position:sticky;top:0;z-index:1;background:transparent;border:0;border-bottom:1px solid var(--dsw-alias-border-l2);flex-wrap:wrap;gap:4px 14px;padding:4px 0 8px;display:flex;font-size:12px;line-height:18px}',
27
28
  '.sap-statusDim{color:var(--dsw-alias-label-tertiary)}',
28
29
  '.sap-rowHighlight{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}',
@@ -133,6 +134,25 @@ const CSS = [
133
134
  '.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
135
  '.sap-adviceFootNotes{flex-direction:column;gap:2px;display:flex;min-width:0}',
135
136
  '.sap-adviceFootNote{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0}',
137
+ '.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}',
138
+ '.sap-adviceStatusLineWarn{color:var(--dsw-alias-state-error-primary);font-weight:600}',
139
+ '.sap-adviceStatusDim{color:var(--dsw-alias-label-tertiary)}',
140
+ '.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}',
141
+ '.sap-adviceTimelineLine{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}',
142
+ '.sap-adviceTimelineStep{color:var(--dsw-alias-label-secondary)}',
143
+ '.sap-adviceTimelineStepDim{color:var(--dsw-alias-label-dimmed)}',
144
+ '.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}',
145
+ '.sap-rollbackBoxLine{color:var(--dsw-alias-label-secondary);font-size:12.5px;line-height:18px;margin:0}',
146
+ '.sap-rollbackBoxKeep{color:var(--dsw-alias-label-tertiary);font-size:12.5px;line-height:18px;margin:0}',
147
+ '.sap-rollbackBoxActions{display:flex;gap:8px;margin-top:6px}',
148
+ '.sap-rollbackDanger{border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:0 0}',
149
+ '.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}',
150
+ '.sap-applyModalHead{align-items:baseline;gap:8px;display:flex}',
151
+ '.sap-applyModalTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}',
152
+ '.sap-applyModalLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;margin:0}',
153
+ '.sap-applyModalLine{color:var(--dsw-alias-label-primary);font-size:12.5px;line-height:18px;margin:0}',
154
+ '.sap-applyModalLineDim{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}',
155
+ '.sap-applyModalActions{justify-content:flex-end;gap:8px;display:flex}',
136
156
  '.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
157
  '.sap-candidateSettingsRow b{font-weight:600;color:var(--dsw-alias-label-primary)}',
138
158
  '.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}',
@@ -145,6 +165,8 @@ const CSS = [
145
165
  '.sap-candidateSettingsCustom .sap-input{max-width:120px}',
146
166
  '.sap-candidateSettingsActions{align-items:center;gap:10px;display:flex}',
147
167
  '.sap-desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
168
+ '.sap-descMinor{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:16px}',
169
+ '.sap-maintenanceSwitchDesc{flex-direction:column;gap:2px;display:flex}',
148
170
  '.sap-warn{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}',
149
171
  '.sap-versionWarn{background:var(--dsw-alias-state-warn-tertiary);border:1px solid var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);border-radius:8px;flex-direction:column;gap:4px;padding:10px 12px;font-size:12px;line-height:18px;margin:0;display:flex}',
150
172
  '.sap-chips{flex-wrap:wrap;gap:6px;display:flex}',
@@ -286,6 +308,7 @@ const CSS = [
286
308
  '.sap-ledgerRowHead:hover{background:var(--dsw-alias-interactive-bg-hover)}',
287
309
  '.sap-ledgerTime{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-family:monospace;flex:none}',
288
310
  '.sap-ledgerMeta{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;min-width:0;word-break:break-word}',
311
+ '.sap-ledgerImageText{color:var(--dsw-alias-label-tertiary);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:none}',
289
312
  '.sap-ledgerToggle{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;transition:transform .12s;margin-left:auto;flex:none;display:inline-flex}',
290
313
  '.sap-ledgerRowOpen .sap-ledgerToggle{transform:rotate(90deg)}',
291
314
  '.sap-ledgerExpand{flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2);padding:10px 14px 12px;display:flex}',
@@ -419,7 +442,7 @@ const ZH = {
419
442
  // 提醒系统
420
443
  reminderCenterTitle: '提醒中心',
421
444
  reminderCenterDesc: '需要你关注的事件汇总,所有动作均有审计记录;处理过的条目折叠保存',
422
- reminderEmpty: '暂无提醒(治理事件会在这里留痕)',
445
+ reminderEmpty: '暂无提醒(历史操作与事件会在这里留痕)',
423
446
  reminderAck: '标记已读',
424
447
  reminderAdopt: '采纳',
425
448
  reminderIgnore: '忽略',
@@ -438,6 +461,27 @@ const ZH = {
438
461
  reminderNoteLabel: '说明',
439
462
  reminderNoSettlement: '未结算',
440
463
  reminderNoExcerpt: '(未记录)',
464
+ reminderArchivedEmpty: '暂无已归档条目(数据维护区执行归档后在此分页查看)',
465
+ reminderArchivedMore: '加载更早',
466
+ // 数据维护(设置页 → 方案管理尾部)
467
+ maintenanceCardTitle: '数据维护',
468
+ maintenanceCardDesc: '候选、快照、提醒与统计数据的清理入口;每次清理都会留下记录',
469
+ snapshotSwitchDesc: '派发时在本机自动保存完整配置(含自定义人格),供「保存为方案」与对比使用。\n数据仅存您本机(~/.dsh/subagent-evolution/snapshots/),插件更新不影响,可随时清理。\n关闭开关即停止新快照(已存保留)。',
470
+ snapshotCount: '已存快照:{n} / {cap} 条',
471
+ snapshotCapHint: '快照已达护栏上限,停止新快照(可清理后继续)',
472
+ snapshotNone: '暂无快照(开启开关后每次派发保存一条)',
473
+ snapshotView: '查看',
474
+ snapshotViewClose: '收起',
475
+ snapshotMeta: '快照 {id} · 于 {ts}',
476
+ snapshotPersonaFold: '人格({n} 字符,点击展开)',
477
+ snapshotEmptyConfig: '(无配置字段)',
478
+ cleanCandidates: '清理候选/建议',
479
+ cleanSnapshots: '清理快照',
480
+ cleanReminders: '归档已处理提醒',
481
+ cleanStats: '重置统计',
482
+ maintenanceConfirm: '确认要执行「{zone}」吗?清理操作会留下记录。',
483
+ maintenanceDone: '已执行 {zone}:{count} 条',
484
+ maintenanceCleanErr: '清理失败:',
441
485
  toolRunning: '运行中',
442
486
  tierZh: { cheap: '省 token(cheap)', balanced: '均衡(balanced)', premium: '高成本(premium)' },
443
487
  stopReasonZh: { completed: '完成', 'max-tokens': '超出预算', aborted: '已中止', refusal: '已拒绝', error: '出错' },
@@ -514,7 +558,7 @@ const ZH = {
514
558
  ledgerCostCardScope: '本会话口径',
515
559
  ledgerCostCardCounter: '若继承父会话',
516
560
  ledgerCostCardSave: '省',
517
- ledgerEvidenceToggle: '查看完整因果链 ▾',
561
+ ledgerEvidenceToggle: '查看完整因果链 ▸',
518
562
  ledgerEvidenceClose: '收起因果链 ▴',
519
563
  ledgerVsTitle: '请求 vs 生效',
520
564
  ledgerVsDiff: '{n} 项与请求不同(琥珀底行)',
@@ -548,14 +592,14 @@ const ZH = {
548
592
  ledgerExecLiveNote: '后台执行中:过程事件随活动更新',
549
593
  ledgerEventStart: '开始执行',
550
594
  ledgerEventCall: '第 {n} 次工具调用',
551
- ledgerEventStall: '⚠ 停滞 {s}s 无活动(超过阈值 {t}s)',
595
+ ledgerEventStall: '⚠ 停滞 {s}s 无活动(超过 {t}s 上限)',
552
596
  ledgerEventStalling: '⚠ 停滞中(已 {s}s 无活动)',
553
597
  ledgerEventIntervene: '主 agent 干预',
554
598
  ledgerEventResume: '恢复执行',
555
599
  ledgerEventDone: '完成',
556
600
  ledgerEventRunning: '执行中…',
557
601
  ledgerEventMoreCalls: '…其间还有 {n} 次调用',
558
- ledgerStallThresholdNote: '停滞阈值 30s(待实测)',
602
+ ledgerStallThresholdNote: '停滞上限 30s(待实测)',
559
603
  ledgerSettleOutput: '输出摘要',
560
604
  ledgerSettleCostBreakdown: '成本分解',
561
605
  ledgerSettleInheritNote: '未指定模型,按继承父会话估算',
@@ -592,9 +636,9 @@ const ZH = {
592
636
  adviceSectionDetail: '派发明细',
593
637
  adviceSwitchOn: '开(只提示,不自动改配置)',
594
638
  adviceSwitchOff: '关(只提示,不自动改配置)',
595
- advicePanelEmpty: '暂无建议(需累计 ≥3 次派发且加权成功分低于阈值)',
639
+ advicePanelEmpty: '暂无建议(近期派发表现不佳时才会生成)',
596
640
  advicePanelGlobal: '全局',
597
- advicePanelReadonly: '(仅提示:统计与建议仅供参考,不改变派发行为)',
641
+ advicePanelReadonly: '(仅提示:统计与建议不改变派发行为)',
598
642
  adviceDirUp: '可上调预算',
599
643
  adviceDirDown: '建议降级',
600
644
  adviceDirFlat: '维持现状',
@@ -633,8 +677,8 @@ const ZH = {
633
677
  candidateTitle: '候选方案',
634
678
  candidateNotApplied: '(未应用)',
635
679
  candidateCountSuffix: ' 个 · 单选',
636
- candidateCountNote: ' · 仅参考,不会自动应用',
637
- candidateIntro: '系统基于近期表现生成的改进方案,仅参考、不自动应用。',
680
+ candidateCountNote: ' · 应用前需你确认',
681
+ candidateIntro: '系统基于近期表现生成的改进方案;应用前需要你确认,并进入观察期。',
638
682
  candidateRegenerate: '重新生成候选',
639
683
  candidateSafetyPending: '安全检查:正在核对…',
640
684
  candidateGroupChanges: '改动',
@@ -643,6 +687,40 @@ const ZH = {
643
687
  candidateSafetyPass: '全部通过 ✓',
644
688
  candidateSafetyFail: '存在拒绝项',
645
689
  candidateNotApplicable: '应用功能在后续版本开放(届时会先经你确认)',
690
+ // 观察期/撤销建议/确认流(用户可见面零内部词:观察中/表现变差/撤销/调整前)
691
+ adviceStatusObservingSuffix: '——表现下降会提醒你撤销',
692
+ adviceApplyConfirm: '确认调整方案',
693
+ adviceApplyCancel: '取消',
694
+ adviceApplyModalSummary: '候选摘要',
695
+ adviceApplyModalImpact: '成本与能力影响',
696
+ adviceApplyImpactDown: '能力配置降级,工具面不扩大,成本通常更低',
697
+ adviceApplyImpactUp: '预算上调,成本可能上升',
698
+ adviceApplyImpactFlat: '能力配置不变,成本不受影响',
699
+ adviceApplySafetyNote: '安全检查:',
700
+ adviceApplyDefaultNote: '本建议改的是方案默认值;派发时显式参数仍会覆盖它',
701
+ adviceApplyObserveNote: '应用后进入观察期(5 次派发),表现下降时会提醒你撤销,不会自动修改',
702
+ adviceApplySuccess: '已应用,进入观察期',
703
+ adviceApplyObservingTitle: '观察中,暂不支持再次调整',
704
+ adviceApplySelectTitle: '请先选择一个候选方案',
705
+ adviceRollbackBoxAdvice: '建议撤销这次调整,恢复调整前的配置。',
706
+ adviceRollbackRestorePrefix: '恢复为:',
707
+ adviceRollbackEvidence: '对比:调整前表现分 {before} · 调整后 {after}(近 {n} 次派发)',
708
+ adviceRollbackKeepDone: '已选择:继续使用当前配置(已留痕)',
709
+ adviceRollbackBtn: '撤销上次调整',
710
+ adviceRollbackKeepBtn: '继续用当前方案',
711
+ adviceRollbackSuccess: '已恢复调整前配置,观察期终止',
712
+ adviceKeepSuccess: '已选择保留当前配置(已留痕)',
713
+ adviceRollbackModalTitle: '撤销这次调整?',
714
+ adviceRollbackModalRestore: '恢复为',
715
+ adviceRollbackModalChange: '会改变',
716
+ adviceRollbackModalNote: '若方案已被手动修改,将无法一键撤销;本次撤销会记入审计',
717
+ adviceRollbackConfirm: '确认撤销',
718
+ adviceRollbackChangeSuffix: '调整前自定义项一并恢复',
719
+ adviceTimelineTitle: '进化轨迹',
720
+ adviceTimelinePending: '待你处理',
721
+ adviceTimelineSep: ' → ',
722
+ adviceRestoreFallback: '调整前的完整配置',
723
+ adviceReminderView: '查看建议',
646
724
  candidateValidityForever: '⏱ 无期限',
647
725
  candidateValidityPrefix: '⏱ 至 ',
648
726
  candidateGeneratedPrefix: '生成于 ',
@@ -650,14 +728,14 @@ const ZH = {
650
728
  candidateSamplesSuffix: ' 次派发',
651
729
  candidatePerfPrefix: '近期表现 ',
652
730
  candidatePerfSuffix: '(满分 100)',
653
- candidateNoSample: '暂无近期派发样本,仅供参考',
731
+ candidateNoSample: '暂无近期派发样本',
654
732
  // 候选卡底部:无对应方案存来源配置为新方案;有对应方案调整按钮灰置 + 定位。
655
733
  adviceSaveProfile: '保存为方案',
656
734
  adviceApplyDisabled: '按建议调整方案',
657
735
  adviceSavedNotice: '已保存为新方案:{id}',
658
736
  adviceFootNoteSave: '仅保存预设、模型与提供方;自定义人格与工具过滤无法还原(隐私保护不落盘),如需完整配置请手动编辑方案。',
659
737
  adviceFootNoteMatchedPrefix: '对应方案:',
660
- // 建议摘要行(v8:对象/建议/置信度/加权成功分;展开后见理由与明细)
738
+ // 建议摘要行(v8:对象/建议/置信度;展开后见理由与明细)
661
739
  adviceNoProfileTag: '无方案',
662
740
  // 安全页签:安全保证卡 + 审计健康卡(只读明细表)
663
741
  safetyCardTitle: '安全保证',
@@ -666,7 +744,7 @@ const ZH = {
666
744
  auditCardTitle: '审计健康',
667
745
  auditCardDesc: '治理审计状态与留痕,异常红字提示',
668
746
  auditDetailToggle: '查看审计明细(点击展开)',
669
- auditDetailEmpty: '暂无审计明细(治理事件会在这里留痕)',
747
+ auditDetailEmpty: '暂无审计明细(历史操作与事件会在这里留痕)',
670
748
  auditDetailScope: '只读展示,暂不提供导出与过滤',
671
749
  auditColTime: '时间',
672
750
  auditColEvent: '事件',
@@ -1047,6 +1125,27 @@ function readDispatchResult(block) {
1047
1125
  }
1048
1126
 
1049
1127
 
1128
+ // 生效画像一行(派发卡片/台账行展示):优先 B3 交付的 meta.imageText(后端同源
1129
+ // 格式,;分隔转 UI 的 · 分隔、路由段加「模型」前缀);旧结果无 imageText 时按
1130
+ // 生效字段同格式合成。未绑定方案 → 「继承父方案 · …」;两者都不可得返回空串
1131
+ // (不渲染该行)。fail-soft:坏输入绝不 throw。
1132
+ function imageTextOfBlock(meta, eff) {
1133
+ const e = eff && typeof eff === 'object' ? eff : {}
1134
+ const m = meta && typeof meta === 'object' ? meta : {}
1135
+ let base = typeof m.imageText === 'string' && m.imageText !== '' ? m.imageText : ''
1136
+ if (base === '') {
1137
+ const profile = typeof e.profile === 'string' && e.profile !== '' && e.profile !== '(inline)' ? e.profile : ''
1138
+ const provider = typeof e.provider === 'string' && e.provider !== '' && e.provider !== '(parent)' ? e.provider : ''
1139
+ const model = typeof e.model === 'string' && e.model !== '' && e.model !== '(parent)' ? e.model : ''
1140
+ const route = provider !== '' && model !== '' ? provider + '/' + model : '继承父'
1141
+ const tier = typeof m.tokenTier === 'string' && m.tokenTier !== '' ? m.tokenTier : 'balanced'
1142
+ base = profile !== '' ? `方案 ${profile};${route};档位 ${tier}` : `继承父方案;${route};档位 ${tier}`
1143
+ }
1144
+ const parts = base.split(';').map((p) => p.trim()).filter((p) => p !== '')
1145
+ if (parts.length >= 2) parts[1] = '模型 ' + parts[1]
1146
+ return parts.join(' · ')
1147
+ }
1148
+
1050
1149
  // 结算数字提取:tokens 数与缓存命中分别取,供「≈N tokens(缓存命中 M)」
1051
1150
  // 单括号合并展示。tokens 不可得返回 null。
1052
1151
  function summaryNumbersOf(result, block) {
@@ -1409,10 +1508,14 @@ function buildSubagentBanner(el, subagent, parentPreset, modelState, bannerMode)
1409
1508
  // 通知中心(设置页通知与提醒页签):读 /reminders(与审计同源 host store),全动作
1410
1509
  // (标记已读/采纳/忽略/拒绝/关闭)经 POST 回写 host 并刷新——一条提醒 = 一条审计记录。
1411
1510
  // sessions 服务供条目主/子会话标题查询与跳转;跳转失败静默(设置页不被导航异常打断)。
1511
+ // 归档视图每页条数(防大量已处理条目一次性渲染卡 UI)。
1512
+ const ARCHIVED_PAGE_SIZE = 20
1513
+
1412
1514
  function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1413
- function ReminderCenter() {
1515
+ function ReminderCenter(props) {
1414
1516
  const [reminders, setReminders] = useState([])
1415
1517
  const [error, setError] = useState('')
1518
+ const [archived, setArchived] = useState({ open: false, list: [], total: 0, offset: 0, loading: false })
1416
1519
  const refresh = () => {
1417
1520
  api('/reminders').then((data) => setReminders(Array.isArray(data.reminders) ? data.reminders : [])).catch(() => {})
1418
1521
  }
@@ -1422,29 +1525,66 @@ function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1422
1525
  const request = action === 'ack' ? api('/reminders/ack', { ids: [id] }) : api('/reminders/action', { id, action })
1423
1526
  request.then(refresh).catch((err) => setError(String((err && err.message) || err)))
1424
1527
  }
1425
- return buildReminderCenter(el, reminders, error, act, getSessions(), openChild)
1528
+ // 归档视图分页加载:打开时取第一页,「加载更早」按 offset 追加(存储只读)。
1529
+ const loadArchivedPage = (offset) => {
1530
+ setArchived((prev) => ({ ...prev, loading: true }))
1531
+ api('/reminders/archived?offset=' + offset + '&limit=' + ARCHIVED_PAGE_SIZE)
1532
+ .then((data) => {
1533
+ const rows = Array.isArray(data.reminders) ? data.reminders : []
1534
+ setArchived((prev) => ({ ...prev, loading: false, total: typeof data.total === 'number' ? data.total : prev.total, offset, list: offset === 0 ? rows : [...prev.list, ...rows] }))
1535
+ })
1536
+ .catch(() => setArchived((prev) => ({ ...prev, loading: false })))
1537
+ }
1538
+ const toggleArchived = () => {
1539
+ const nextOpen = !archived.open
1540
+ setArchived((prev) => ({ ...prev, open: nextOpen }))
1541
+ if (nextOpen && archived.list.length === 0) loadArchivedPage(0)
1542
+ }
1543
+ const jumpToAdvice = props !== null && typeof props === 'object' && typeof props.jumpToAdvice === 'function' ? props.jumpToAdvice : null
1544
+ return buildReminderCenter(el, reminders, error, act, getSessions(), openChild, jumpToAdvice, archived, toggleArchived, loadArchivedPage)
1426
1545
  }
1427
1546
  return ReminderCenter
1428
1547
  }
1429
1548
 
1549
+ // 归档视图区:折叠开关(总条数)+ 只读行列表(已处理条目复渲染)+ 加载更早。
1550
+ function buildArchivedReminderSection(el, archived, act, sessions, openChild, jumpToAdvice, toggleArchived, loadArchivedPage) {
1551
+ const rows = Array.isArray(archived.list) ? archived.list : []
1552
+ const hasMore = archived.list.length < archived.total
1553
+ return el('details', { className: 'sap-customized sap-customizedArrowEnd sap-reminderHandled', open: archived.open === true },
1554
+ el('summary', { className: 'sap-customizedSummary', onClick: toggleArchived }, '已归档(' + archived.total + ')', el('span', { className: 'sap-customizedEndArrow' }, '▸')),
1555
+ el('div', { className: 'sap-customizedBody' },
1556
+ rows.length === 0
1557
+ ? el('p', { className: 'sap-desc' }, ZH.reminderArchivedEmpty)
1558
+ : [
1559
+ el('ul', { className: 'sap-rows' }, rows.map((r) => buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice))),
1560
+ hasMore
1561
+ ? el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, disabled: archived.loading === true, onClick: () => loadArchivedPage(archived.offset + ARCHIVED_PAGE_SIZE) }, ZH.reminderArchivedMore)
1562
+ : null
1563
+ ]
1564
+ )
1565
+ )
1566
+ }
1567
+
1430
1568
  // 提醒中心分层:未终态条目(action 为空)进默认列表;已终态条目折叠为「已处理(N)」
1431
- // 可展开区(原生 details,展开后仍按现有行渲染并保留已处理标注,可追溯)。审计留痕
1432
- // 由 host 侧动作回写,已处理条目不再堆在默认列表里。全空才显示空态文案;仅剩已处理时
1433
- // 只显示折叠区,不显示空态。
1434
- function buildReminderCenter(el, reminders, error, act, sessions, openChild) {
1569
+ // 可展开区(原生 details,展开后仍按现有行渲染并保留已处理标注,可追溯);归档后的
1570
+ // 条目进入「已归档(N)」只读分页视图(存储与审计保留)。审计留痕由 host 侧动作回写。
1571
+ function buildReminderCenter(el, reminders, error, act, sessions, openChild, jumpToAdvice, archived, toggleArchived, loadArchivedPage) {
1435
1572
  const handled = reminders.filter((r) => typeof r.action === 'string' && r.action !== '')
1436
1573
  const pending = reminders.filter((r) => !(typeof r.action === 'string' && r.action !== ''))
1437
1574
  const pendingList = pending.length > 0
1438
- ? el('ul', { className: 'sap-rows' }, pending.map((r) => buildReminderRow(el, r, act, sessions, openChild)))
1575
+ ? el('ul', { className: 'sap-rows' }, pending.map((r) => buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice)))
1439
1576
  : null
1440
1577
  const handledList = handled.length > 0
1441
1578
  ? el('details', { className: 'sap-customized sap-customizedArrowEnd sap-reminderHandled' },
1442
1579
  el('summary', { className: 'sap-customizedSummary' }, '已处理(' + handled.length + ')', el('span', { className: 'sap-customizedEndArrow' }, '▸')),
1443
1580
  el('div', { className: 'sap-customizedBody' },
1444
- el('ul', { className: 'sap-rows' }, handled.map((r) => buildReminderRow(el, r, act, sessions, openChild)))
1581
+ el('ul', { className: 'sap-rows' }, handled.map((r) => buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice)))
1445
1582
  )
1446
1583
  )
1447
1584
  : null
1585
+ const archivedSection = archived !== null && typeof archived === 'object'
1586
+ ? buildArchivedReminderSection(el, archived, act, sessions, openChild, jumpToAdvice, toggleArchived, loadArchivedPage)
1587
+ : null
1448
1588
  return el('div', { className: 'sap-card sap-cardStatic' },
1449
1589
  el('div', { className: 'sap-cardHead' },
1450
1590
  el('div', { className: 'sap-cardTitleBox' },
@@ -1456,7 +1596,8 @@ function buildReminderCenter(el, reminders, error, act, sessions, openChild) {
1456
1596
  error !== '' ? el('p', { className: 'sap-notice' }, error) : null,
1457
1597
  pendingList === null && handledList === null
1458
1598
  ? el('p', { className: 'sap-desc' }, ZH.reminderEmpty)
1459
- : [pendingList, handledList]
1599
+ : [pendingList, handledList],
1600
+ archivedSection
1460
1601
  )
1461
1602
  )
1462
1603
  }
@@ -1523,10 +1664,20 @@ function reminderJumpButton(el, jump, disabled, onClick) {
1523
1664
  )
1524
1665
  }
1525
1666
 
1667
+ // 会话存在性探测(孤儿灰置):跳转引用指向已删会话(binding 拿不到)时灰置跳转
1668
+ // 按钮——回溯安全,不点进不可达会话。空 id/异常一律视为不可用(fail-soft)。
1669
+ function sessionExists(sessions, id) {
1670
+ if (typeof id !== 'string' || id === '') return false
1671
+ try { return sessions.binding(id) !== undefined } catch { return false }
1672
+ }
1673
+
1526
1674
  // 未采纳条目五字段区:主会话/子会话可点击跳转,任务/结果/说明只读展示。
1527
1675
  function buildUnadoptedFields(el, r, sessions, openChild) {
1676
+ const childId = r.childSessionId !== null && r.childSessionId !== undefined && r.childSessionId !== '' ? r.childSessionId : r.childId
1528
1677
  const parentJump = reminderJumpText(sessions, r.sessionId, ZH.reminderParentLabel)
1529
- const childJump = reminderJumpText(sessions, r.childSessionId !== null && r.childSessionId !== undefined && r.childSessionId !== '' ? r.childSessionId : r.childId, ZH.reminderChildLabel)
1678
+ const childJump = reminderJumpText(sessions, childId, ZH.reminderChildLabel)
1679
+ const parentGone = !sessionExists(sessions, r.sessionId)
1680
+ const childGone = !sessionExists(sessions, childId)
1530
1681
  const openParent = () => { try { sessions.open(r.sessionId) } catch { /* 会话不可用:静默 */ } }
1531
1682
  const childAddress = typeof r.sessionId === 'string' && r.sessionId !== '' && typeof r.childSessionId === 'string' && r.childSessionId !== ''
1532
1683
  ? { parentSessionId: r.sessionId, childSessionId: r.childSessionId, mode: reminderChildMode(r.childMode) }
@@ -1534,8 +1685,8 @@ function buildUnadoptedFields(el, r, sessions, openChild) {
1534
1685
  const jumpChild = () => { if (childAddress !== null) { try { openChild(childAddress) } catch { /* 子会话不可用:静默 */ } } }
1535
1686
  const excerpt = typeof r.promptExcerpt === 'string' && r.promptExcerpt !== '' ? '「' + r.promptExcerpt + '」' : ZH.reminderNoExcerpt
1536
1687
  return el('div', { className: 'sap-reminderFields' },
1537
- reminderJumpButton(el, parentJump, typeof r.sessionId !== 'string' || r.sessionId === '', openParent),
1538
- reminderJumpButton(el, childJump, childAddress === null, jumpChild),
1688
+ reminderJumpButton(el, parentJump, parentGone, openParent),
1689
+ reminderJumpButton(el, childJump, childAddress === null || childGone, jumpChild),
1539
1690
  reminderFieldLine(el, ZH.reminderTaskLabel, excerpt),
1540
1691
  reminderFieldLine(el, ZH.reminderResultLabel, reminderResultText(r)),
1541
1692
  reminderFieldLine(el, ZH.reminderNoteLabel, r.detail)
@@ -1545,10 +1696,11 @@ function buildUnadoptedFields(el, r, sessions, openChild) {
1545
1696
  // 未采纳条目按钮三件套:查看子会话(跳转,恒保留)/ 标记已读(未读显示)/ 关闭。
1546
1697
  // 已终态条目隐藏标记已读与关闭(只留只读跳转),防对终态条目重复处置产生重复审计;
1547
1698
  // 提醒上的处置只改变提醒自身状态(已读/终态),不改变采纳判定——采纳与否由事件流自动汇总。
1548
- function buildUnadoptedActions(el, r, act, childAddress, jumpChild) {
1699
+ // 子会话已删(孤儿)时查看按钮灰置(回溯安全)。
1700
+ function buildUnadoptedActions(el, r, act, childAddress, jumpChild, childGone) {
1549
1701
  const terminated = typeof r.action === 'string' && r.action !== ''
1550
1702
  return el('div', { className: 'sap-reminderActions' }, [
1551
- el('button', { type: 'button', className: 'sap-detailToggle', disabled: childAddress === null, onClick: jumpChild }, ZH.reminderViewChild),
1703
+ el('button', { type: 'button', className: 'sap-detailToggle', disabled: childAddress === null || childGone === true, onClick: jumpChild }, ZH.reminderViewChild),
1552
1704
  terminated ? null : r.unread === true ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1553
1705
  terminated ? null : el('button', { type: 'button', className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1554
1706
  ])
@@ -1558,9 +1710,11 @@ function buildUnadoptedActions(el, r, act, childAddress, jumpChild) {
1558
1710
  function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1559
1711
  const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1560
1712
  const severityText = r.severity === 'P0' ? '紧急' : '注意'
1713
+ const childId = r.childSessionId !== null && r.childSessionId !== undefined && r.childSessionId !== '' ? r.childSessionId : r.childId
1561
1714
  const childAddress = typeof r.sessionId === 'string' && r.sessionId !== '' && typeof r.childSessionId === 'string' && r.childSessionId !== ''
1562
1715
  ? { parentSessionId: r.sessionId, childSessionId: r.childSessionId, mode: reminderChildMode(r.childMode) }
1563
1716
  : null
1717
+ const childGone = !sessionExists(sessions, childId)
1564
1718
  const jumpChild = () => { if (childAddress !== null) { try { openChild(childAddress) } catch { /* 子会话不可用:静默 */ } } }
1565
1719
  return el('li', { key: String(r.id), className: 'sap-rowCard' + (r.unread === true ? ' sap-reminderUnread' : '') },
1566
1720
  el('div', { className: 'sap-reminderHead' },
@@ -1568,7 +1722,34 @@ function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1568
1722
  el('span', { className: 'sap-reminderTitle', title: r.title }, r.title)
1569
1723
  ),
1570
1724
  buildUnadoptedFields(el, r, sessions, openChild),
1571
- buildUnadoptedActions(el, r, act, childAddress, jumpChild),
1725
+ buildUnadoptedActions(el, r, act, childAddress, jumpChild, childGone),
1726
+ typeof r.action === 'string' && r.action !== ''
1727
+ ? el('p', { className: 'sap-metaValueDefault' }, ZH.reminderActed.replace('{action}', ZH.reminderActionZh[r.action] || r.action))
1728
+ : null
1729
+ )
1730
+ }
1731
+
1732
+ // 回滚建议提醒行:标题/说明只读 + 「查看建议」跳转到自进化与建议页签(只读跳转,
1733
+ // 无采纳/忽略按钮);与通知类提醒同款「标记已读/关闭」。
1734
+ function buildRollbackReminderRow(el, r, act, jumpToAdvice) {
1735
+ const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1736
+ const severityText = r.severity === 'P0' ? '紧急' : '注意'
1737
+ const jump = () => { if (typeof jumpToAdvice === 'function') { try { jumpToAdvice() } catch { /* 页签不可用:静默 */ } } }
1738
+ return el('li', { key: String(r.id), className: 'sap-rowCard' + (r.unread === true ? ' sap-reminderUnread' : '') },
1739
+ el('div', { className: 'sap-reminderHead' },
1740
+ el('span', { className: 'sap-chip ' + severityCls }, severityText),
1741
+ el('span', { className: 'sap-reminderTitle', title: r.title }, r.title)
1742
+ ),
1743
+ el('p', { className: 'sap-desc' }, r.detail),
1744
+ typeof r.action === 'string' && r.action !== ''
1745
+ ? null
1746
+ : el('div', { className: 'sap-reminderActions' }, [
1747
+ typeof jumpToAdvice === 'function' ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: jump }, ZH.adviceReminderView) : null,
1748
+ r.unread === true ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1749
+ r.severity !== 'P0' || r.unread !== true
1750
+ ? el('button', { type: 'button', className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1751
+ : null
1752
+ ]),
1572
1753
  typeof r.action === 'string' && r.action !== ''
1573
1754
  ? el('p', { className: 'sap-metaValueDefault' }, ZH.reminderActed.replace('{action}', ZH.reminderActionZh[r.action] || r.action))
1574
1755
  : null
@@ -1578,7 +1759,8 @@ function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1578
1759
  // 通知类提醒行:P0 未读只保留「标记已读」(常驻至已读,不可跳过未读);已读后出现
1579
1760
  // 「关闭」(与 P1/P2 同);P1/P2 恒保留「标记已读 / 关闭」。已终态条目隐藏动作按钮,
1580
1761
  // 只显示已处理灰字标注(折叠区可追溯、防对终态条目误点产生重复审计)。
1581
- function buildReminderRow(el, r, act, sessions, openChild) {
1762
+ function buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice) {
1763
+ if (r.kind === 'suggestion-rollback') return buildRollbackReminderRow(el, r, act, jumpToAdvice)
1582
1764
  if (r.kind === 'adoption-false') return buildUnadoptedReminderRow(el, r, act, sessions, openChild)
1583
1765
  const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1584
1766
  const severityText = r.severity === 'P0' ? '紧急' : '注意'
@@ -1712,7 +1894,8 @@ function collectDispatchRecords(nodes) {
1712
1894
  request: readDispatchRequest(node),
1713
1895
  result: readDispatchResult(node),
1714
1896
  argsSummary: summarizeArgsRaw(node.call.argsRaw),
1715
- contentFirstLine: firstContentLine(node.content)
1897
+ contentFirstLine: firstContentLine(node.content),
1898
+ imageText: node.meta !== null && typeof node.meta === 'object' && typeof node.meta.imageText === 'string' ? node.meta.imageText : ''
1716
1899
  })
1717
1900
  }
1718
1901
  return records
@@ -2222,17 +2405,17 @@ function useBackgroundJobPoll(useState, useEffect, api, sessionId, callId, settl
2222
2405
  function makeDispatchToolview(el, chip, useState, useEffect) {
2223
2406
  function DispatchToolview(props) {
2224
2407
  const block = props && props.block; const request = readDispatchRequest(block); const result = readDispatchResult(block)
2225
- const settled = !!(block && typeof block === 'object' && 'kind' in block)
2226
- const trace = block && block.meta ? normalizeDecisionTrace(block.meta) : null
2408
+ const settled = !!(block && typeof block === 'object' && 'kind' in block); const trace = block && block.meta ? normalizeDecisionTrace(block.meta) : null
2227
2409
  const { eff, ignored, kindLabel } = resolveDispatchState(request, result, settled)
2228
2410
  const chips = buildDispatchChips(el, chip, eff, result, block, ignored, request)
2229
- const safetyLine = safetyLineText(trace); const toolLine = toolLineText(request, trace)
2230
- const metaLine = (safetyLine !== '' || toolLine !== '') ? el('div', { className: 'sap-toolMeta' },
2411
+ // 生效画像行并入 meta 行(B3 imageText 回读;旧结果按生效字段合成,空则不出现)。
2412
+ const safetyLine = safetyLineText(trace); const toolLine = toolLineText(request, trace); const imageText = imageTextOfBlock(block && typeof block === 'object' ? block.meta : null, eff)
2413
+ const metaLine = (safetyLine !== '' || toolLine !== '' || imageText !== '') ? el('div', { className: 'sap-toolMeta' },
2231
2414
  safetyLine !== '' ? el('span', { className: 'sap-toolMetaItem' }, safetyLine) : null,
2232
- toolLine !== '' ? el('span', { className: 'sap-toolMetaItem' }, toolLine) : null
2415
+ toolLine !== '' ? el('span', { className: 'sap-toolMetaItem' }, toolLine) : null,
2416
+ imageText !== '' ? el('span', { className: 'sap-toolMetaItem' }, imageText) : null
2233
2417
  ) : null
2234
2418
  const usage = usageFrom(result, block)
2235
- // 后台运行中按 callId 轮询 /ledger/jobs 配对结算(前台/continuable 静态终态)。
2236
2419
  const jobSettled = useBackgroundJobPoll(useState, useEffect, api, props.sessionId, props.callId, settled, request)
2237
2420
  const [open, setOpen] = useState(false); const suggestion = draftSuggestionFromDispatch(request, trace, result); const draft = useDraftCapture(useState, suggestion)
2238
2421
  // 摘要行只承载动态结果(成本/耗时/结束原因/缓存命中);静态配置(方案/预设/
@@ -2657,6 +2840,45 @@ function makeCandidateSettingsActions(api, s, refresh, applyWrite) {
2657
2840
  }
2658
2841
  }
2659
2842
 
2843
+ // 应用确认流动作:按建议调整方案(POST /evolution/apply,humanConfirmed 由确认按钮
2844
+ // 触发)、撤销上次调整(action=rollback)、继续用当前方案(action=keep)。成功即刷新
2845
+ // 并置顶部成功提示;失败原因回显在各自弹窗内(不静默)。
2846
+ function makeApplyFlowActions(api, s, refresh) {
2847
+ const closeApply = () => s.setApplyModal((prev) => ({ ...(prev ?? {}), open: false, error: '' }))
2848
+ const closeRollback = () => s.setRollbackModal((prev) => ({ ...(prev ?? {}), open: false, error: '' }))
2849
+ const applyCandidate = () => {
2850
+ const modal = s.applyModal ?? {}
2851
+ if (modal.candidateId === '') return
2852
+ s.setError('')
2853
+ api('/evolution/apply', { assetId: modal.candidateId, human_confirmed: true })
2854
+ .then(() => { closeApply(); s.setSavedNotice(ZH.adviceApplySuccess); refresh() })
2855
+ .catch((err) => s.setApplyModal((prev) => ({ ...(prev ?? {}), error: String((err && err.message) || err) })))
2856
+ }
2857
+ const rollbackAsset = () => {
2858
+ const modal = s.rollbackModal ?? {}
2859
+ if (modal.assetId === '') return
2860
+ s.setError('')
2861
+ api('/evolution/rollback', { assetId: modal.assetId, human_confirmed: true, action: 'rollback' })
2862
+ .then(() => { closeRollback(); s.setSavedNotice(ZH.adviceRollbackSuccess); refresh() })
2863
+ .catch((err) => s.setRollbackModal((prev) => ({ ...(prev ?? {}), error: String((err && err.message) || err) })))
2864
+ }
2865
+ const keepAsset = (assetId) => () => {
2866
+ s.setError('')
2867
+ api('/evolution/rollback', { assetId, human_confirmed: true, action: 'keep' })
2868
+ .then(() => { s.setSavedNotice(ZH.adviceKeepSuccess); refresh() })
2869
+ .catch((err) => s.setError(String((err && err.message) || err)))
2870
+ }
2871
+ return {
2872
+ openApplyModal: (profileKey, candidateId) => () => s.setApplyModal({ profileKey, candidateId, open: true, error: '' }),
2873
+ closeApplyModal: closeApply,
2874
+ applyCandidate,
2875
+ openRollbackModal: (assetId) => () => s.setRollbackModal({ assetId, open: true, error: '' }),
2876
+ closeRollbackModal: closeRollback,
2877
+ rollbackAsset,
2878
+ keepAsset,
2879
+ }
2880
+ }
2881
+
2660
2882
  function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools) {
2661
2883
  return {
2662
2884
  ...makeFormActions(s, loadEfforts, loadTools),
@@ -2666,6 +2888,7 @@ function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools
2666
2888
  ...makeEscapeActions(api, s),
2667
2889
  ...makeCandidateActions(api, s),
2668
2890
  ...makeCandidateSettingsActions(api, s, refresh, applyWrite),
2891
+ ...makeApplyFlowActions(api, s, refresh),
2669
2892
  ...makeTabActions(s),
2670
2893
  locateProfile: makeLocateProfile(s),
2671
2894
  }
@@ -2798,7 +3021,7 @@ function buildProfileRows(el, chip, s, actions) {
2798
3021
  ? el('p', { className: 'sap-costLine' },
2799
3022
  [
2800
3023
  typeof profile.avgCost === 'number' ? `平均 ≈${profile.avgCost} 元/次` : '',
2801
- typeof profile.successRate === 'number' ? `成功率 ${profile.successRate}${typeof profile.n === 'number' ? `(N=${profile.n})` : ''}` : ''
3024
+ typeof profile.successRate === 'number' ? `近期表现 ${Math.round(profile.successRate * 100)}/100${typeof profile.n === 'number' ? `(基于最近 ${profile.n} 次派发)` : ''}` : ''
2802
3025
  ].filter(Boolean).join(' · '))
2803
3026
  : null
2804
3027
  )
@@ -2907,14 +3130,20 @@ function candidateValidityText(expiry) {
2907
3130
  return ZH.candidateValidityPrefix + formatMonthDayTime(expiry)
2908
3131
  }
2909
3132
 
2910
- // 指标行:把「平均耗时 <ms>ms」换算成短时(40.4s 等),其余指标原样(信息不删)。
3133
+ // 指标行:把「平均耗时 <ms>ms」换算成短时(40.4s 等);第 1 个指标恒为成功分行,
3134
+ // 历史台账里的旧标签「加权成功分」在此显示层映射为「近期表现分」(值保持 0~1
3135
+ // 原样不改口径,0~100 视图在候选生成信息行)。其余指标原样(信息不删)。
2911
3136
  function adviceMetricsOf(performanceText) {
2912
- return adviceEvidenceOf(performanceText).metrics.map((metric) => {
3137
+ return adviceEvidenceOf(performanceText).metrics.map((metric, index) => {
2913
3138
  const hit = /^平均耗时 (.+)$/.exec(metric)
2914
3139
  if (hit !== null) {
2915
3140
  const ms = Number(hit[1])
2916
3141
  return Number.isFinite(ms) ? '平均耗时 ' + formatDurationMs(ms) : metric
2917
3142
  }
3143
+ if (index === 0) {
3144
+ const space = metric.indexOf(' ')
3145
+ return space > 0 ? '近期表现分 ' + metric.slice(space + 1) : metric
3146
+ }
2918
3147
  return metric
2919
3148
  })
2920
3149
  }
@@ -2968,7 +3197,7 @@ function dispatchModeZh(mode) {
2968
3197
  return mode
2969
3198
  }
2970
3199
 
2971
- // 表现摘要拆两层:计数(完成/失败/终止)与指标(加权成功分/平均耗时/平均输出),
3200
+ // 表现摘要拆两层:计数(完成/失败/终止)与指标(近期表现分/平均耗时/平均输出),
2972
3201
  // 各渲染一遍;解析不出的部分原样保留(信息不删)。
2973
3202
  function adviceEvidenceOf(performanceText) {
2974
3203
  const text = typeof performanceText === 'string' ? performanceText : ''
@@ -3058,13 +3287,19 @@ function adviceSectionOf(el, title, children) {
3058
3287
  }
3059
3288
 
3060
3289
  // 单张建议卡(v10):摘要行 = 对象/建议/置信度/▸(结论三件套,无加权分);
3061
- // 展开区 = 小节序列 理由 → 近期表现 → 派发明细 → 候选方案(未应用)→ 操作区。
3290
+ // 展开区 = 观察期状态行 → 小节序列(理由/近期表现/派发明细/候选方案)→ 撤销建议区
3291
+ // (regress)→ 进化轨迹 → 操作区;确认/撤销弹窗就地渲染在卡内。
3062
3292
  function buildAdviceCard(el, s, actions, a, candidates, sessions) {
3063
3293
  const profile = matchingProfileOf(s.profiles, a.profileKey)
3064
3294
  const metrics = adviceMetricsOf(a.performanceText)
3065
3295
  const rows = s.adviceDetails && typeof s.adviceDetails === 'object' && Array.isArray(s.adviceDetails[a.profileKey]) ? s.adviceDetails[a.profileKey] : []
3066
3296
  const openMap = s.adviceOpen !== null && typeof s.adviceOpen === 'object' ? s.adviceOpen : {}
3067
3297
  const open = openMap[a.profileKey] === true
3298
+ const assetView = adviceAssetViewOf(s, a.profileKey)
3299
+ const selectedId = candidateSelectedIdOf(s, a.profileKey, candidates)
3300
+ const selected = candidates.find((c) => c !== null && typeof c === 'object' && c.id === selectedId)
3301
+ const applyModal = s.applyModal ?? {}
3302
+ const rollbackModal = s.rollbackModal ?? {}
3068
3303
  const summary = el('button', { type: 'button', className: 'sap-adviceSummary', 'aria-expanded': open, onClick: actions.toggleAdviceOpen(a.profileKey) },
3069
3304
  el('span', { className: 'sap-adviceObjLine' },
3070
3305
  el('span', { className: 'sap-adviceObj' }, adviceSummaryObjectText(profile, a.profileKey)),
@@ -3075,11 +3310,16 @@ function buildAdviceCard(el, s, actions, a, candidates, sessions) {
3075
3310
  el('span', { className: 'sap-adviceConf' }, ZH.adviceConfidence + confidenceZhOf(a.confidence)),
3076
3311
  el('span', { className: 'sap-adviceSummaryArrow' }, '▸')))
3077
3312
  const detail = open !== true ? null : el('div', { className: 'sap-adviceDetail' },
3313
+ buildAdviceStatusLine(el, assetView),
3078
3314
  typeof a.reasonText === 'string' && a.reasonText !== '' ? adviceSectionOf(el, ZH.adviceSectionReason, el('p', { className: 'sap-adviceSectionBody' }, a.reasonText)) : null,
3079
3315
  metrics.length > 0 ? adviceSectionOf(el, ZH.adviceSectionPerf, el('p', { className: 'sap-adviceSectionBody' }, metrics.join(' · '))) : null,
3080
3316
  adviceSectionOf(el, ZH.adviceSectionDetail, buildAdviceDetails(el, s, actions, a.profileKey, rows, sessions)),
3081
3317
  candidates.length > 0 ? buildAdviceCandidates(el, s, actions, a, candidates) : null,
3082
- buildAdviceFoot(el, s, actions, a, profile, candidates))
3318
+ assetView !== null ? buildRollbackSuggestion(el, s, actions, assetView) : null,
3319
+ buildAdviceTimeline(el, assetView),
3320
+ buildAdviceFoot(el, s, actions, a, profile, candidates, assetView),
3321
+ applyModal.open === true && applyModal.profileKey === a.profileKey && selected !== undefined ? buildApplyModal(el, s, actions, a, profile, selected) : null,
3322
+ rollbackModal.open === true && assetView !== null && rollbackModal.assetId === assetView.id ? buildRollbackModal(el, s, actions, assetView) : null)
3083
3323
  return el('li', { key: String(a.profileKey), className: 'sap-adviceItem' + (open === true ? ' sap-adviceItemOpen' : '') }, summary, detail)
3084
3324
  }
3085
3325
 
@@ -3113,6 +3353,173 @@ function buildAdviceDetails(el, s, actions, profileKey, rows, sessions) {
3113
3353
  )
3114
3354
  }
3115
3355
 
3356
+ // ── 观察期/撤销建议/进化轨迹(数据源 = /evolution/assets 派生视图)──────────
3357
+
3358
+ // 建议卡对应资产视图:同键资产取最近一条(观察期/已应用优先于纯草稿);无资产 → null。
3359
+ function adviceAssetViewOf(s, profileKey) {
3360
+ const assets = Array.isArray(s.evolutionAssets) ? s.evolutionAssets : []
3361
+ const owned = assets.filter((a) => a && typeof a === 'object' && a.profile_key === profileKey)
3362
+ if (owned.length === 0) return null
3363
+ const rank = (a) => (a && a.canary && typeof a.canary === 'object' && a.canary.state !== 'pendingApply' ? 1 : 0)
3364
+ return owned.sort((a, b) => rank(b) - rank(a) || (Number.isFinite(b.updatedAt) ? b.updatedAt : 0) - (Number.isFinite(a.updatedAt) ? a.updatedAt : 0))[0]
3365
+ }
3366
+
3367
+ // 候选行徽标对应资产(按候选 id = 资产 id)。
3368
+ function candidateAssetByIdOf(s, id) {
3369
+ const assets = Array.isArray(s.evolutionAssets) ? s.evolutionAssets : []
3370
+ return assets.find((a) => a && a.id === id) ?? null
3371
+ }
3372
+
3373
+ // 观察期状态行:regress 警示强调(结论先行);其余次级色;pendingApply 不显示。
3374
+ function buildAdviceStatusLine(el, view) {
3375
+ const canary = view !== null && typeof view === 'object' ? view.canary : null
3376
+ if (canary === null || typeof canary !== 'object' || canary.state === 'pendingApply') return null
3377
+ const warn = canary.state === 'regress'
3378
+ const suffix = canary.state === 'observing' ? ZH.adviceStatusObservingSuffix : ''
3379
+ return el('div', { className: 'sap-adviceStatusLine' + (warn ? ' sap-adviceStatusLineWarn' : '') },
3380
+ el('span', null, String(canary.label ?? '') + suffix))
3381
+ }
3382
+
3383
+ // 恢复目标名:fromSnapshot 方案名列表;空快照回退通用文案(回滚 = 移除应用方案)。
3384
+ function rollbackRestoreNameOf(asset) {
3385
+ const snapshot = asset !== null && typeof asset === 'object' ? asset.fromSnapshot : null
3386
+ const profiles = snapshot !== null && typeof snapshot === 'object' && Array.isArray(snapshot.profiles) ? snapshot.profiles : []
3387
+ 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 !== '')
3388
+ return names.length > 0 ? names.join('、') : ZH.adviceRestoreFallback
3389
+ }
3390
+
3391
+ // 表现分 → 百分制整数(证据对照行口径,与「满分 100」一致)。
3392
+ function adviceScore100(score) {
3393
+ return Number.isFinite(score) ? Math.round(score * 100) : '—'
3394
+ }
3395
+
3396
+ function rollbackEvidenceTextOf(asset) {
3397
+ const canary = asset !== null && typeof asset === 'object' && asset.canary !== null && typeof asset.canary === 'object' ? asset.canary : {}
3398
+ const post = canary.post !== null && typeof canary.post === 'object' ? canary.post : null
3399
+ const base = canary.baseline !== null && typeof canary.baseline === 'object' ? canary.baseline : null
3400
+ const n = post !== null && Number.isFinite(post.sampleN) ? post.sampleN : 0
3401
+ return ZH.adviceRollbackEvidence.replace('{before}', adviceScore100(base === null ? null : base.weighted_success)).replace('{after}', adviceScore100(post === null ? null : post.weighted_success)).replace('{n}', n)
3402
+ }
3403
+
3404
+ function tierZhOf(tier) {
3405
+ return tier === 'cheap' ? '低' : tier === 'premium' ? '高' : '中'
3406
+ }
3407
+
3408
+ // 撤销确认弹窗「会改变」行:模型/预算档位/推理强度变化 + 自定义项一并恢复。
3409
+ function rollbackChangeTextOf(asset) {
3410
+ const parts = []
3411
+ const applied = asset !== null && typeof asset === 'object' && asset.applied_config !== null && typeof asset.applied_config === 'object' ? asset.applied_config : {}
3412
+ const snapshot = asset !== null && typeof asset === 'object' && asset.fromSnapshot !== null && typeof asset.fromSnapshot === 'object' && Array.isArray(asset.fromSnapshot.profiles) ? asset.fromSnapshot.profiles : []
3413
+ const before = snapshot.length > 0 && snapshot[0].config !== null && typeof snapshot[0].config === 'object' ? snapshot[0].config : {}
3414
+ if (before.model !== undefined || applied.model !== undefined) parts.push('模型 ' + (before.model || '未指定') + ' → ' + (applied.model || '未指定'))
3415
+ if (before.tokenTier !== undefined && applied.tokenTier !== undefined && before.tokenTier !== applied.tokenTier) {
3416
+ parts.push('预算档位 ' + tierZhOf(before.tokenTier) + ' → ' + tierZhOf(applied.tokenTier) + ',成本可能回升')
3417
+ } else if (before.reasoningEffort !== undefined && applied.reasoningEffort !== undefined && before.reasoningEffort !== applied.reasoningEffort) {
3418
+ parts.push('推理强度已上调,成本可能回升')
3419
+ }
3420
+ parts.push(ZH.adviceRollbackChangeSuffix)
3421
+ return parts.join(';')
3422
+ }
3423
+
3424
+ // 撤销建议区(regress):结论先行在状态行,本区 = 建议/恢复为/证据 + 双按钮;
3425
+ // 用户已选择保留 → 只读「已选择」行;无表态前常驻只读(不自动执行任何动作)。
3426
+ function buildRollbackSuggestion(el, s, actions, asset) {
3427
+ const canary = asset !== null && typeof asset === 'object' && asset.canary !== null && typeof asset.canary === 'object' ? asset.canary : {}
3428
+ if (canary.state !== 'regress') return null
3429
+ if (canary.kept === true) {
3430
+ return el('div', { className: 'sap-rollbackBox' }, el('p', { className: 'sap-rollbackBoxKeep' }, ZH.adviceRollbackKeepDone))
3431
+ }
3432
+ return el('div', { className: 'sap-rollbackBox' },
3433
+ el('p', { className: 'sap-applyModalLine' }, ZH.adviceRollbackBoxAdvice),
3434
+ el('p', { className: 'sap-rollbackBoxLine' }, ZH.adviceRollbackRestorePrefix, el('b', null, rollbackRestoreNameOf(asset))),
3435
+ el('p', { className: 'sap-rollbackBoxLine' }, rollbackEvidenceTextOf(asset)),
3436
+ el('div', { className: 'sap-rollbackBoxActions' },
3437
+ el('button', { type: 'button', className: 'sap-secondaryButton sap-rollbackDanger', style: smallBtn, onClick: actions.openRollbackModal(asset.id) }, ZH.adviceRollbackBtn),
3438
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.keepAsset(asset.id) }, ZH.adviceRollbackKeepBtn)))
3439
+ }
3440
+
3441
+ // 成本与能力影响行(确认弹窗):降级/上调方向 + 预算档位变化。
3442
+ function applyImpactTextOf(profile, candidate, suggestion) {
3443
+ const cfg = candidate !== null && typeof candidate === 'object' && candidate.config !== null && typeof candidate.config === 'object' ? candidate.config : {}
3444
+ const parts = [suggestion === '降' ? ZH.adviceApplyImpactDown : suggestion === '升' ? ZH.adviceApplyImpactUp : ZH.adviceApplyImpactFlat]
3445
+ if (profile !== null && cfg.tokenTier !== undefined && profile.tokenTier !== undefined && cfg.tokenTier !== profile.tokenTier) {
3446
+ parts.push('预算档位 ' + tierZhOf(profile.tokenTier) + ' → ' + tierZhOf(cfg.tokenTier))
3447
+ }
3448
+ return parts.join(';')
3449
+ }
3450
+
3451
+ // 应用确认弹窗:候选摘要(名称/方向/安全检查)/成本与能力影响/两条说明行;
3452
+ // 主按钮「确认调整方案」即执行(humanConfirmed 由点击触发,无强制勾选),失败红字回显。
3453
+ function buildApplyModal(el, s, actions, a, profile, candidate) {
3454
+ const modal = s.applyModal ?? {}
3455
+ const preview = s.draftPreviews !== null && typeof s.draftPreviews === 'object' ? s.draftPreviews[candidate.id] : undefined
3456
+ const checks = preview !== undefined && preview.checks !== null && typeof preview.checks === 'object' && Array.isArray(preview.checks) ? preview.checks : []
3457
+ const safety = preview === undefined ? ZH.candidateSafetyPending
3458
+ : preview.ok === true ? ZH.adviceApplySafetyNote + ZH.candidateSafetyPass.replace(' ✓', '') + ' ' + checks.length + '/' + checks.length
3459
+ : ZH.adviceApplySafetyNote + ZH.candidateSafetyFail
3460
+ return el('div', { className: 'sap-applyModal' },
3461
+ el('div', { className: 'sap-applyModalHead' }, el('span', { className: 'sap-applyModalTitle' }, ZH.adviceApplyConfirm)),
3462
+ el('div', null,
3463
+ el('p', { className: 'sap-applyModalLabel' }, ZH.adviceApplyModalSummary),
3464
+ el('p', { className: 'sap-applyModalLine' }, candidate.name + ' · ' + directionZh(a.suggestion)),
3465
+ el('p', { className: 'sap-applyModalLineDim' }, safety)),
3466
+ el('div', null,
3467
+ el('p', { className: 'sap-applyModalLabel' }, ZH.adviceApplyModalImpact),
3468
+ el('p', { className: 'sap-applyModalLine' }, applyImpactTextOf(profile, candidate, a.suggestion))),
3469
+ el('p', { className: 'sap-applyModalLineDim' }, ZH.adviceApplyDefaultNote),
3470
+ el('p', { className: 'sap-applyModalLineDim' }, ZH.adviceApplyObserveNote),
3471
+ modal.error !== '' ? el('p', { className: 'sap-notice' }, modal.error) : null,
3472
+ el('div', { className: 'sap-applyModalActions' },
3473
+ el('button', { type: 'button', className: 'sap-secondaryButton', onClick: actions.closeApplyModal }, ZH.adviceApplyCancel),
3474
+ el('button', { type: 'button', className: 'sap-primaryButton', onClick: actions.applyCandidate }, ZH.adviceApplyConfirm)))
3475
+ }
3476
+
3477
+ // 撤销确认弹窗:恢复为/会改变/说明 + 确认撤销(朱砂描边)/取消;失败红字回显。
3478
+ function buildRollbackModal(el, s, actions, asset) {
3479
+ const modal = s.rollbackModal ?? {}
3480
+ return el('div', { className: 'sap-applyModal' },
3481
+ el('div', { className: 'sap-applyModalHead' }, el('span', { className: 'sap-applyModalTitle' }, ZH.adviceRollbackModalTitle)),
3482
+ el('div', null,
3483
+ el('p', { className: 'sap-applyModalLabel' }, ZH.adviceRollbackModalRestore),
3484
+ el('p', { className: 'sap-applyModalLine' }, rollbackRestoreNameOf(asset))),
3485
+ el('div', null,
3486
+ el('p', { className: 'sap-applyModalLabel' }, ZH.adviceRollbackModalChange),
3487
+ el('p', { className: 'sap-applyModalLine' }, rollbackChangeTextOf(asset))),
3488
+ el('p', { className: 'sap-applyModalLineDim' }, ZH.adviceRollbackModalNote),
3489
+ modal.error !== '' ? el('p', { className: 'sap-notice' }, modal.error) : null,
3490
+ el('div', { className: 'sap-applyModalActions' },
3491
+ el('button', { type: 'button', className: 'sap-secondaryButton', onClick: actions.closeRollbackModal }, ZH.adviceApplyCancel),
3492
+ el('button', { type: 'button', className: 'sap-secondaryButton sap-rollbackDanger', onClick: actions.rollbackAsset }, ZH.adviceRollbackConfirm)))
3493
+ }
3494
+
3495
+ // 轨迹单事件文案:生成(MM-DD)/应用(MM-DD · 人工确认)/观察(k/kTotal)/
3496
+ // 判定:…/撤销·保留:…(未发生步骤灰字由 buildAdviceTimeline 补)。
3497
+ function timelineEventTextOf(event) {
3498
+ if (event.event === 'generated') return '生成(' + formatMonthDayTime(event.ts) + ')'
3499
+ if (event.event === 'applied') return '应用(' + formatMonthDayTime(event.ts) + (typeof event.meta === 'string' && event.meta !== '' ? ' · ' + event.meta : '') + ')'
3500
+ if (event.event === 'observed') return '观察(' + (typeof event.meta === 'string' ? event.meta : '') + ')'
3501
+ if (event.event === 'judged') return '判定:' + (typeof event.meta === 'string' && event.meta !== '' ? event.meta : ZH.adviceTimelinePending)
3502
+ if (event.event === 'resolved') return '撤销/保留:' + (typeof event.meta === 'string' && event.meta !== '' ? event.meta : ZH.adviceTimelinePending)
3503
+ return event.event
3504
+ }
3505
+
3506
+ // 进化轨迹小节:assets lifecycle 行式时间线;未应用(无 applied 事件)或缺生命周期
3507
+ // 数据 → 隐藏小节。未发生步骤(判定/处理)灰字补位。
3508
+ function buildAdviceTimeline(el, view) {
3509
+ const lifecycle = view !== null && typeof view === 'object' && Array.isArray(view.lifecycle) ? view.lifecycle : []
3510
+ if (lifecycle.length === 0 || !lifecycle.some((e) => e.event === 'applied')) return null
3511
+ const chain = []
3512
+ const push = (text, dim) => chain.push(el('span', { key: String(chain.length), className: dim === true ? 'sap-adviceTimelineStepDim' : 'sap-adviceTimelineStep' }, text))
3513
+ for (const event of lifecycle) {
3514
+ if (chain.length > 0) chain.push(el('span', { key: 'sep' + chain.length }, ZH.adviceTimelineSep))
3515
+ push(timelineEventTextOf(event), false)
3516
+ }
3517
+ const has = (kind) => lifecycle.some((e) => e.event === kind)
3518
+ if (!has('judged')) { chain.push(el('span', { key: 'sepd' }, ZH.adviceTimelineSep)); push('判定:' + ZH.adviceTimelinePending, true) }
3519
+ if (!has('resolved')) { chain.push(el('span', { key: 'sepr' }, ZH.adviceTimelineSep)); push('撤销/保留:' + ZH.adviceTimelinePending, true) }
3520
+ return adviceSectionOf(el, ZH.adviceTimelineTitle, el('p', { className: 'sap-adviceTimelineLine' }, chain))
3521
+ }
3522
+
3116
3523
  // 候选管理设置(顶部一行 + 就地浮层):更新模式与有效期用户可配;
3117
3524
  // 自定义按天数输入(1~365),提交时换算小时数。
3118
3525
  function candidateModeTextOf(mode) {
@@ -3207,10 +3614,13 @@ function buildAdviceCandidateRow(el, s, actions, a, candidate, index, selected)
3207
3614
  if (event && typeof event.stopPropagation === 'function') event.stopPropagation()
3208
3615
  actions.selectCandidate(a.profileKey, candidate.id)()
3209
3616
  }
3617
+ const asset = candidateAssetByIdOf(s, candidate.id)
3618
+ const stateLabel = asset !== null && typeof asset.stateLabel === 'string' && asset.stateLabel !== '' ? asset.stateLabel : null
3210
3619
  return el('div', { key: String(candidate.id), className: 'sap-candidate' + (selected === candidate.id ? ' sap-candidateSelected' : '') },
3211
3620
  el('button', { type: 'button', className: 'sap-candidateRow', 'aria-expanded': open, onClick: actions.toggleCandidateOpen(candidate) },
3212
3621
  el('span', { role: 'radio', 'aria-checked': selected === candidate.id, className: 'sap-candidateRadio', onClick: onRadio }),
3213
3622
  el('span', { className: 'sap-candidateName' }, '候选 ' + (index + 1) + ' · ' + candidate.name),
3623
+ stateLabel !== null ? el('span', { className: 'sap-candidateState' }, stateLabel) : null,
3214
3624
  el('span', { className: 'sap-candidateValidity' }, candidateValidityText(candidate.expiry)),
3215
3625
  el('span', { className: 'sap-candidateArrow' }, '▸')),
3216
3626
  open === true ? el('div', { className: 'sap-candidateBody' },
@@ -3233,7 +3643,7 @@ function buildAdviceCandidateRow(el, s, actions, a, candidate, index, selected)
3233
3643
  }
3234
3644
 
3235
3645
  // 生成信息行(最浅灰,两分支模板):有样本 = 生成于 MM-DD · 基于最近 N 次派发 ·
3236
- // 近期表现 X(满分 100,X=round(加权分×100));无样本 = 生成于 MM-DD · 暂无近期派发
3646
+ // 近期表现 X(满分 100,X=round(表现分×100));无样本 = 生成于 MM-DD · 暂无近期派发
3237
3647
  // 样本,仅供参考(不显示 0 分);无生成时间省去「生成于」段。
3238
3648
  function candidateGeneratedLine(el, candidate) {
3239
3649
  const basis = candidate.basis !== null && typeof candidate.basis === 'object' ? candidate.basis : {}
@@ -3251,24 +3661,36 @@ function candidateGeneratedLine(el, candidate) {
3251
3661
  return el('p', { className: 'sap-candidateHint' }, parts.join(' · '))
3252
3662
  }
3253
3663
 
3254
- // 卡底部:无对应方案 = [保存为方案](统一主按钮)+ 备注两行;有对应方案 =
3255
- // 灰置[按建议调整方案](统一次级按钮)+ 定位;说明与「生成于…」拆两行不连排。
3256
- function buildAdviceFoot(el, s, actions, a, profile, candidates) {
3664
+ // 卡底部:无对应方案 = [保存为方案](统一主按钮)+ 备注;有对应方案 = [按建议调整
3665
+ // 方案] + 定位。按钮启用条件 = applyEnabled && 已选候选;禁用 title = 禁用原因
3666
+ // (冷启动 = reasons.join(';') 并附卡脚一行灰字;观察中 = 暂不支持再次调整)。
3667
+ function buildAdviceFoot(el, s, actions, a, profile, candidates, assetView) {
3257
3668
  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
3669
  const generatedAt = formatMonthDayTime(basis.generatedAt)
3259
3670
  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) {
3671
+ const noteLines = (lines) => el('div', { className: 'sap-adviceFootNotes' }, lines.map((line) => el('p', { key: line, className: 'sap-adviceFootNote' }, line)))
3672
+ if (profile === null) {
3264
3673
  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
- }
3674
+ el('button', { type: 'button', className: 'sap-primaryButton', style: smallBtn, onClick: actions.saveAdviceProfile(a.profileKey) }, ZH.adviceSaveProfile),
3675
+ noteLines([ZH.adviceFootNoteSave]))
3676
+ }
3677
+ const status = s.evolutionStatus ?? {}
3678
+ const applyEnabled = status.applyEnabled === true
3679
+ const selectedId = candidateSelectedIdOf(s, a.profileKey, candidates)
3680
+ const canaryState = assetView !== null && assetView.canary !== null && typeof assetView.canary === 'object' ? assetView.canary.state : 'pendingApply'
3681
+ const observing = canaryState === 'observing' || canaryState === 'stalled' || canaryState === 'inconclusive' || canaryState === 'regress' || canaryState === 'aborted'
3682
+ let disabledTitle = ''
3683
+ if (observing) disabledTitle = ZH.adviceApplyObservingTitle
3684
+ else if (!applyEnabled) disabledTitle = Array.isArray(status.reasons) && status.reasons.length > 0 ? status.reasons.join(';') : ZH.candidateNotApplicable
3685
+ else if (selectedId === '') disabledTitle = ZH.adviceApplySelectTitle
3686
+ const disabled = disabledTitle !== ''
3687
+ const notes = [ZH.adviceFootNoteMatchedPrefix + (profile.name || profile.id) + '(' + profile.id + ')']
3688
+ if (generatedText !== '') notes.push(generatedText)
3689
+ if (!applyEnabled && !observing) notes.push(disabledTitle)
3269
3690
  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))
3691
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, disabled, title: disabledTitle, onClick: disabled ? undefined : actions.openApplyModal(a.profileKey, selectedId) }, ZH.adviceApplyDisabled),
3692
+ el('button', { type: 'button', className: 'sap-detailToggle', onClick: actions.locateProfile(profile.id) }, ZH.adviceRowLocate),
3693
+ noteLines(notes))
3272
3694
  }
3273
3695
 
3274
3696
  // 逃生舱卡片:开关 + (打开时)显式警告文案 + 放行集增删(输入框 + 添加按钮,
@@ -3543,26 +3965,29 @@ function buildSectionReturn(el, s, actions, rows, addCard, reminderCenter, sessi
3543
3965
  ...notices,
3544
3966
  buildTabBar(el, s),
3545
3967
  buildTabZone(el, s, 'profiles',
3546
- buildSettingsCard(el, 1, '方案管理', '管理派发可用的方案:启用、编辑、删除,含成本与成功率', true, () => {}, [
3547
- el('p', { className: 'sap-intro' }, ZH.profilesIntro),
3548
- el('ul', { className: 'sap-rows' }, rows),
3549
- buildDraftRows(el, s, actions),
3550
- el('div', { className: 'sap-addBlock' },
3551
- s.adding ? addCard : el('div', { className: 'sap-addActions' },
3552
- el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
3968
+ el('div', { className: 'sap-zoneStack' },
3969
+ buildSettingsCard(el, 1, '方案管理', '管理派发可用的方案:启用、编辑、删除,含成本与成功率', true, () => {}, [
3970
+ el('p', { className: 'sap-intro' }, ZH.profilesIntro),
3971
+ el('ul', { className: 'sap-rows' }, rows),
3972
+ buildDraftRows(el, s, actions),
3973
+ el('div', { className: 'sap-addBlock' },
3974
+ s.adding ? addCard : el('div', { className: 'sap-addActions' },
3975
+ el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
3976
+ )
3977
+ ),
3978
+ el('div', { className: 'sap-addActions', style: { marginTop: '4px' } },
3979
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.resetAll }, ZH.resetAllBuiltins)
3553
3980
  )
3554
- ),
3555
- el('div', { className: 'sap-addActions', style: { marginTop: '4px' } },
3556
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.resetAll }, ZH.resetAllBuiltins)
3557
- )
3558
- ], true)),
3981
+ ], true),
3982
+ buildMaintenanceCard(el, s, actions)
3983
+ )),
3559
3984
  buildTabZone(el, s, 'cost',
3560
3985
  buildSettingsCard(el, 2, '全局成本', '跨全部会话的派发成本汇总,按模型与方案拆分', true, () => {}, [
3561
3986
  buildCostObservabilitySection(el, s, actions)
3562
3987
  ], true)),
3563
3988
  buildTabZone(el, s, 'advice', buildAdvicePanel(el, s, actions, sessions)),
3564
3989
  buildTabZone(el, s, 'safety', buildSafetySection(el, s, actions, s.auditReminders)),
3565
- buildTabZone(el, s, 'reminders', reminderCenter)
3990
+ buildTabZone(el, s, 'reminders', el(reminderCenter, { jumpToAdvice: () => s.setActiveTab('advice') }))
3566
3991
  )
3567
3992
  }
3568
3993
 
@@ -3601,21 +4026,138 @@ function attachDraftActions(api, s, refresh, applyWrite, actions) {
3601
4026
  }
3602
4027
  }
3603
4028
 
4029
+ // ── 设置页:数据维护区(快照开关/单条查看 + 四区清理按钮组)───────────────────
4030
+
4031
+ // 快照区数据装载 hook:挂载拉一次,开关切换与清理后回读(单条查看状态同驻)。
4032
+ function useSnapshotData(useState, useEffect) {
4033
+ const [snapshots, setSnapshots] = useState({ snapshotEnabled: false, count: 0, cap: 200, entries: [] })
4034
+ const [snapshotView, setSnapshotView] = useState({})
4035
+ const [maintenanceNotice, setMaintenanceNotice] = useState('')
4036
+ const refreshSnapshots = () => {
4037
+ api('/snapshots').then((data) => {
4038
+ setSnapshots({
4039
+ snapshotEnabled: data !== null && typeof data === 'object' && data.snapshotEnabled === true,
4040
+ count: data !== null && typeof data === 'object' && typeof data.count === 'number' ? data.count : 0,
4041
+ cap: data !== null && typeof data === 'object' && typeof data.cap === 'number' ? data.cap : 200,
4042
+ entries: data !== null && typeof data === 'object' && Array.isArray(data.entries) ? data.entries : []
4043
+ })
4044
+ }).catch(() => {})
4045
+ }
4046
+ useEffect(() => { refreshSnapshots() }, [])
4047
+ return { snapshots, refreshSnapshots, snapshotView, setSnapshotView, maintenanceNotice, setMaintenanceNotice }
4048
+ }
4049
+
4050
+ // 数据维护动作:快照开关、单条查看(展开/收起)、四区清理(确认一次、结果 echo)。
4051
+ function attachMaintenanceActions(api, s, refresh, actions) {
4052
+ actions.toggleSnapshots = () => {
4053
+ api('/snapshots/set-enabled', { snapshotEnabled: !s.snapshots.snapshotEnabled })
4054
+ .then((data) => {
4055
+ if (data !== null && typeof data === 'object' && data.persisted === false) s.setPersistWarning(ZH.persistFail)
4056
+ else s.setPersistWarning('')
4057
+ s.refreshSnapshots()
4058
+ })
4059
+ .catch((err) => s.setError(String((err && err.message) || err)))
4060
+ }
4061
+ actions.viewSnapshot = (id) => () => {
4062
+ const next = { ...s.snapshotView }
4063
+ if (next[id] !== undefined) { delete next[id]; s.setSnapshotView(next); return }
4064
+ api('/snapshot?id=' + encodeURIComponent(id))
4065
+ .then((data) => {
4066
+ if (data !== null && typeof data === 'object' && data.ok === true && data.snapshot !== undefined) {
4067
+ s.setSnapshotView({ ...s.snapshotView, [id]: data.snapshot })
4068
+ }
4069
+ })
4070
+ .catch(() => {})
4071
+ }
4072
+ actions.cleanZone = (zone, label) => () => {
4073
+ if (!window.confirm(ZH.maintenanceConfirm.replace('{zone}', label))) return
4074
+ api('/maintenance/clean', { zone })
4075
+ .then((data) => {
4076
+ const count = data !== null && typeof data === 'object' && typeof data.count === 'number' ? data.count : 0
4077
+ s.setMaintenanceNotice(ZH.maintenanceDone.replace('{zone}', label).replace('{count}', String(count)))
4078
+ s.refreshSnapshots()
4079
+ refresh()
4080
+ })
4081
+ .catch((err) => s.setError(ZH.maintenanceCleanErr + String((err && err.message) || err)))
4082
+ }
4083
+ }
4084
+
4085
+ // 单条快照只读体:persona 默认折叠(本地只读允许看原文,UI 不默认展开);其余
4086
+ // 字段等宽 JSON 展示。
4087
+ function snapshotViewBody(el, payload) {
4088
+ const cfg = payload !== null && typeof payload === 'object' && payload.config !== null && typeof payload.config === 'object' ? payload.config : {}
4089
+ const keys = Object.keys(cfg)
4090
+ const persona = typeof cfg.persona === 'string' ? cfg.persona : ''
4091
+ return el('div', { className: 'sap-reminderFields' },
4092
+ keys.length === 0 ? el('p', { className: 'sap-desc' }, ZH.snapshotEmptyConfig) : null,
4093
+ keys.map((key) => {
4094
+ if (key === 'persona') {
4095
+ return el('details', { key: 'snap-persona', className: 'sap-customized' },
4096
+ el('summary', { className: 'sap-customizedSummary' }, ZH.snapshotPersonaFold.replace('{n}', String(persona.length))),
4097
+ el('pre', { className: 'sap-metaValueDefault' }, persona)
4098
+ )
4099
+ }
4100
+ return reminderFieldLine(el, key, formatGateValue(cfg[key]))
4101
+ })
4102
+ )
4103
+ }
4104
+
4105
+ // 快照开关说明三行分层:首行主说明,后两行小字次要色(数据落点与关闭语义)。
4106
+ function snapshotSwitchDescRows(el) {
4107
+ return ZH.snapshotSwitchDesc.split('\n').filter((line) => line !== '').map((line, index) => el('p', { key: 'snap-desc-' + index, className: index === 0 ? 'sap-desc' : 'sap-descMinor' }, line))
4108
+ }
4109
+
4110
+ // 数据维护卡(方案管理尾部):快照开关 + B7 文案 + 数量/护栏提示 + 单条查看 +
4111
+ // 维护按钮组(四区各自独立、一次确认、结果 echo、DOM 一行)。
4112
+ function buildMaintenanceCard(el, s, actions) {
4113
+ const snap = s.snapshots
4114
+ const atCap = snap.count >= snap.cap && snap.count > 0
4115
+ const entries = Array.isArray(snap.entries) ? snap.entries : []
4116
+ const zoneButtons = [
4117
+ { zone: 'candidates', label: ZH.cleanCandidates },
4118
+ { zone: 'snapshots', label: ZH.cleanSnapshots },
4119
+ { zone: 'reminders', label: ZH.cleanReminders },
4120
+ { zone: 'stats', label: ZH.cleanStats }
4121
+ ].map((z) => el('button', { key: 'clean-' + z.zone, type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.cleanZone(z.zone, z.label) }, z.label))
4122
+ return buildSettingsCard(el, 11, ZH.maintenanceCardTitle, ZH.maintenanceCardDesc, true, () => {}, [
4123
+ el('label', { className: 'sap-switch' },
4124
+ el('input', { type: 'checkbox', checked: snap.snapshotEnabled === true, onChange: actions.toggleSnapshots }),
4125
+ el('span', null, '配置快照')
4126
+ ),
4127
+ el('div', { className: 'sap-maintenanceSwitchDesc' }, snapshotSwitchDescRows(el)),
4128
+ el('p', { className: 'sap-desc' },
4129
+ snap.count > 0 ? ZH.snapshotCount.replace('{n}', String(snap.count)).replace('{cap}', String(snap.cap)) : ZH.snapshotNone
4130
+ ),
4131
+ atCap ? el('p', { className: 'sap-notice' }, ZH.snapshotCapHint) : null,
4132
+ entries.map((entry) => {
4133
+ const payload = s.snapshotView[entry.id]
4134
+ return el('div', { key: 'snap-' + entry.id, className: 'sap-candidateSettingsPanel' },
4135
+ el('div', { className: 'sap-candidateSettingsRow' },
4136
+ el('b', null, ZH.snapshotMeta.replace('{id}', entry.id).replace('{ts}', formatMonthDayTime(entry.ts))),
4137
+ el('button', { type: 'button', className: 'sap-candidateSettingsEdit', onClick: actions.viewSnapshot(entry.id) }, payload !== undefined ? ZH.snapshotViewClose : ZH.snapshotView)
4138
+ ),
4139
+ payload !== undefined ? snapshotViewBody(el, payload) : null
4140
+ )
4141
+ }),
4142
+ s.maintenanceNotice !== '' ? el('p', { className: 'sap-notice' }, s.maintenanceNotice) : null,
4143
+ el('div', { className: 'sap-candidateSettingsActions' }, zoneButtons)
4144
+ ], true)
4145
+ }
4146
+
3604
4147
  function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter, getSessions) {
3605
4148
  function ProfilesSection() {
3606
4149
  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('')
4150
+ const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [error, setError] = useState(''); const [savedNotice, setSavedNotice] = useState('')
4151
+ const [persistWarning, setPersistWarning] = useState(''); const [form, setForm] = useState(EMPTY_FORM); const [tools, setTools] = useState([]); const [versionWarnings, setVersionWarnings] = useState([])
4152
+ const [pluginVersion, setPluginVersion] = useState(''); const [audit, setAudit] = useState(AUDIT_DEFAULTS); const [escapeEnabled, setEscapeEnabled] = useState(false); const [escapePresets, setEscapePresets] = useState([]); const [escapeInput, setEscapeInput] = useState('')
3611
4153
  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)
4154
+ const [adviceGlobal, setAdviceGlobal] = useState(null); const [costSummary, setCostSummary] = useState({ global: null, byModel: [] }); const [draftPreviews, setDraftPreviews] = useState({}); const [candidateOpen, setCandidateOpen] = useState({})
4155
+ const [candidateSelected, setCandidateSelected] = useState({}); const [candidateSafetyOpen, setCandidateSafetyOpen] = useState({}); const [candidateSettings, setCandidateSettings] = useState({ candidateMode: 'auto', candidateTtlH: 24 }); const [candidateSettingsOpen, setCandidateSettingsOpen] = useState(false)
3615
4156
  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 }
4157
+ const [activeTab, setActiveTab] = useState('profiles'); const [highlightId, setHighlightId] = useState(null); const [adviceOpen, setAdviceOpen] = useState({}); const [auditDetailOpen, setAuditDetailOpen] = useState(false); const [auditReminders, setAuditReminders] = useState([])
4158
+ const [evolutionStatus, setEvolutionStatus] = useState({ applyEnabled: false, canaryEnabled: false, reasons: [] }); const [evolutionAssets, setEvolutionAssets] = useState([])
4159
+ const [applyModal, setApplyModal] = useState({ profileKey: '', candidateId: '', open: false, error: '' }); const [rollbackModal, setRollbackModal] = useState({ assetId: '', open: false, error: '' }); const snapshotCtl = useSnapshotData(useState, useEffect)
4160
+ 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, snapshots: snapshotCtl.snapshots, snapshotView: snapshotCtl.snapshotView, maintenanceNotice: snapshotCtl.maintenanceNotice, refreshSnapshots: snapshotCtl.refreshSnapshots, setSnapshotView: snapshotCtl.setSnapshotView, setMaintenanceNotice: snapshotCtl.setMaintenanceNotice, 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
4161
  const refresh = useCallback(() => {
3620
4162
  api('/list')
3621
4163
  .then((data) => {
@@ -3625,11 +4167,12 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, Re
3625
4167
  setAdviceGlobal(data.adviceGlobal !== null && typeof data.adviceGlobal === 'object' ? data.adviceGlobal : null)
3626
4168
  setAdviceDetails(data.adviceDetails !== null && typeof data.adviceDetails === 'object' ? data.adviceDetails : {})
3627
4169
  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: [] })
4170
+ const status = data.evolutionStatus !== null && typeof data.evolutionStatus === 'object' ? data.evolutionStatus : {}
4171
+ setEvolutionStatus({ applyEnabled: status.applyEnabled === true, canaryEnabled: status.canaryEnabled === true, reasons: Array.isArray(status.reasons) ? status.reasons : [] })
3628
4172
  })
3629
4173
  .catch((err) => setError(String((err && err.message) || err)))
3630
- api('/drafts')
3631
- .then((data) => setDrafts(Array.isArray(data.drafts) ? data.drafts : []))
3632
- .catch(() => {})
4174
+ api('/evolution/assets').then((data) => setEvolutionAssets(Array.isArray(data.assets) ? data.assets : [])).catch(() => {})
4175
+ api('/drafts').then((data) => setDrafts(Array.isArray(data.drafts) ? data.drafts : [])).catch(() => {})
3633
4176
  api('/reminders').then((data) => setAuditReminders(Array.isArray(data.reminders) ? data.reminders : [])).catch(() => {})
3634
4177
  }, [])
3635
4178
  const loadOptions = useCallback(() => {
@@ -3639,15 +4182,11 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, Re
3639
4182
  }, [])
3640
4183
  const loadEfforts = makeEffortsLoader(api, setOptions); const loadTools = makeToolsLoader(api, setTools)
3641
4184
  const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings, setPluginVersion)(), []) // 只挂载时拉一次:稳定引用防 effect 依赖恒变
3642
- useEffect(() => {
3643
- refresh()
3644
- loadOptions()
3645
- loadVersions()
3646
- }, [refresh, loadOptions, loadVersions])
4185
+ useEffect(() => { refresh(); loadOptions(); loadVersions() }, [refresh, loadOptions, loadVersions])
3647
4186
  const applyWrite = makeApplyWrite(s); const actions = makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools)
3648
- attachDraftActions(api, s, refresh, applyWrite, actions)
4187
+ attachDraftActions(api, s, refresh, applyWrite, actions); attachMaintenanceActions(api, s, refresh, actions)
3649
4188
  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))
4189
+ return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection), ReminderCenter, sessionsSnapshot(getSessions))
3651
4190
  }
3652
4191
  return ProfilesSection
3653
4192
  }
@@ -3961,7 +4500,7 @@ function buildJsonBody(el, kv) {
3961
4500
 
3962
4501
  // ── 三层信息架构(结论层 / 证据层)纯渲染 ─────────────────────────
3963
4502
 
3964
- // 停滞检测阈值(待实测:measured-params 回填惯例,展示层同步标注「待实测」)。
4503
+ // 停滞检测上限(待实测:measured-params 回填惯例,展示层同步标注「待实测」)。
3965
4504
  const STALL_THRESHOLD_MS = 30000
3966
4505
 
3967
4506
  // 折叠态状态 chip:拒绝(红)/执行中(琥珀脉冲)/异常(琥珀)/成功(绿)。
@@ -4631,6 +5170,7 @@ function buildLedgerRowHead(el, record, profileNames, proc, open, onToggle) {
4631
5170
  const costText = rejected ? '—' : (!settled ? ZH.ledgerCostRunning : (costInfo !== null ? '≈' + costInfo.actual + ' 元' : ''))
4632
5171
  const profileId = recordProfile(record)
4633
5172
  const profileText = profileId !== '' ? profileDisplay(profileId, profileNames) : ZH.inlineProfile
5173
+ const imageText = imageTextOfBlock({ imageText: record.imageText }, record.trace !== null && typeof record.trace === 'object' ? record.trace.effective : null)
4634
5174
  const toolsText = ledgerToolsCountOf(record, proc)
4635
5175
  const interveneText = proc !== null && proc.interventions > 0 ? ZH.ledgerInterventionBadge.replace('{n}', String(proc.interventions)) : ''
4636
5176
  return el('button', {
@@ -4641,6 +5181,9 @@ function buildLedgerRowHead(el, record, profileNames, proc, open, onToggle) {
4641
5181
  },
4642
5182
  el('span', { className: 'sap-ledgerTime' }, formatClockTime(record.time)),
4643
5183
  el('span', { className: 'sap-ledgerMeta' }, profileText),
5184
+ imageText !== ''
5185
+ ? el('span', { className: 'sap-ledgerMeta sap-ledgerImageText' }, imageText)
5186
+ : null,
4644
5187
  el('span', { className: 'sap-ledgerStatus ' + status.cls }, status.label),
4645
5188
  costText !== '' ? el('span', { className: 'sap-ledgerCostText' }, costText) : null,
4646
5189
  toolsText !== '' ? el('span', { className: 'sap-ledgerToolsText' }, toolsText) : null,