dsh-subagent-profile 0.3.4 → 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.
Files changed (38) hide show
  1. package/README.md +13 -8
  2. package/README.zh.md +13 -8
  3. package/index.mjs +99 -95
  4. package/lib/client.js +1288 -189
  5. package/lib/core/decision-trace.mjs +20 -1
  6. package/lib/core/delegation.mjs +3 -2
  7. package/lib/core/dispatch-gates.mjs +122 -9
  8. package/lib/core/dispatch-schema.mjs +45 -7
  9. package/lib/core/dispatch-tool.mjs +24 -25
  10. package/lib/core/evolution-advice.mjs +154 -34
  11. package/lib/core/evolution-assets.mjs +167 -0
  12. package/lib/core/evolution-canary.mjs +427 -0
  13. package/lib/core/evolution-draft.mjs +333 -0
  14. package/lib/core/evolution-engine.mjs +413 -0
  15. package/lib/core/evolution-generate.mjs +180 -0
  16. package/lib/core/evolution-lag.mjs +78 -0
  17. package/lib/core/evolution-ledger.mjs +135 -27
  18. package/lib/core/evolution-persistence.mjs +213 -0
  19. package/lib/core/evolution-renewal.mjs +64 -0
  20. package/lib/core/evolution-routes.mjs +234 -0
  21. package/lib/core/http-helpers.mjs +35 -0
  22. package/lib/core/http-routes.mjs +35 -39
  23. package/lib/core/model-policy.mjs +77 -0
  24. package/lib/core/presets-sync.mjs +24 -2
  25. package/lib/core/profile-directory.mjs +17 -0
  26. package/lib/core/profile-provider.mjs +9 -3
  27. package/lib/core/profiles-query.mjs +178 -0
  28. package/lib/core/profiles-store.mjs +56 -8
  29. package/lib/core/pure.mjs +1 -1
  30. package/lib/core/session-read.mjs +17 -0
  31. package/lib/core/shims.mjs +2 -1
  32. package/package.json +2 -2
  33. package/presets/orchestrator-v2/NOTICE +7 -0
  34. package/presets/{orchestrator → orchestrator-v2}/agent.cordis.yml +78 -7
  35. package/presets/orchestrator-v2/custom-bash.mjs +213 -0
  36. package/presets/orchestrator-v2/preset.yml +2 -0
  37. package/presets/orchestrator-v2/tool-bootstrap.mjs +620 -0
  38. package/presets/orchestrator/preset.yml +0 -2
package/lib/client.js CHANGED
@@ -22,7 +22,7 @@
22
22
  // ── 纯数据:样式(tokens 对齐 shipped「模型」设置节)────────────────────────
23
23
 
24
24
  const CSS = [
25
- '.sap-section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}',
25
+ '.sap-section{width:100%;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}',
26
26
  '.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
27
  '.sap-statusDim{color:var(--dsw-alias-label-tertiary)}',
28
28
  '.sap-rowHighlight{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}',
@@ -35,7 +35,7 @@ const CSS = [
35
35
  '.sap-phaseStep-running{color:var(--dsw-alias-state-warn-label);font-weight:500}',
36
36
  '.sap-reminderP0{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-secondary);font-weight:600}',
37
37
  '.sap-reminderP1{color:var(--dsw-alias-state-warn-label);font-weight:600}',
38
- '.sap-reminderUnread{border-color:var(--dsw-alias-brand-primary)}',
38
+ '.sap-reminderUnread{border-color:var(--dsw-alias-label-primary)}',
39
39
  '.sap-reminderHead{display:flex;align-items:center;gap:6px;min-width:0}',
40
40
  '.sap-reminderTitle{font-size:14px;font-weight:500;line-height:22px;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;flex:1}',
41
41
  '.sap-reminderMeta{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-family:monospace;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
@@ -70,11 +70,6 @@ const CSS = [
70
70
  '.sap-cardStatic .sap-cardHead{cursor:default}',
71
71
  '.sap-cardStatic .sap-cardHead:hover{background:0 0}',
72
72
  '.sap-cardStatic .sap-cardBody{display:block}',
73
- '.sap-subgroup{flex-direction:column;gap:6px;padding:2px 0;display:flex}',
74
- '.sap-subgroup + .sap-subgroup{margin-top:20px}',
75
- '.sap-subgroupTitle{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;font-weight:600}',
76
- '.sap-subgroupHint{color:var(--dsw-alias-label-tertiary);font-size:12.5px;line-height:18px;margin:0}',
77
- '.sap-subgroup .sap-desc,.sap-subgroup .sap-costLine{color:var(--dsw-alias-label-tertiary);font-size:12.5px;line-height:18px}',
78
73
  '.sap-costLine{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
79
74
  '.sap-title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}',
80
75
  '.sap-intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}',
@@ -88,6 +83,86 @@ const CSS = [
88
83
  '.sap-rowId{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-family:monospace}',
89
84
  '.sap-rowTag{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}',
90
85
  '.sap-rowActions{align-items:center;gap:4px;margin-left:auto;display:inline-flex}',
86
+ '.sap-adviceManageCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 12px;flex-direction:column;gap:6px;display:flex}',
87
+ '.sap-adviceCardHead{flex-direction:column;align-items:stretch;gap:2px}',
88
+ '.sap-adviceHeadTitleRow{display:flex;align-items:center;gap:8px}',
89
+ '.sap-adviceGlobalLine{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}',
90
+ '.sap-adviceSection{flex-direction:column;display:flex}',
91
+ '.sap-adviceSection + .sap-adviceSection{margin-top:10px}',
92
+ '.sap-adviceSectionTitle{color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;font-weight:700;margin:0 0 2px}',
93
+ '.sap-adviceSectionBody{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;margin:0}',
94
+ '.sap-candidateHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:16px}',
95
+ '.sap-candidateBody{border-top:1px dashed var(--dsw-alias-border-l2);margin-top:4px;padding-top:8px;padding-left:16px;flex-direction:column;gap:6px;display:flex}',
96
+ '.sap-candidateChanges{flex-direction:column;gap:4px;margin:0;padding:0;list-style:none;display:flex}',
97
+ '.sap-candidateChange{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}',
98
+ '.sap-adviceDetailBlock{flex-direction:column;gap:4px;display:flex}',
99
+ '.sap-adviceDetailToggle{border:0;background:0 0;padding:0;margin:0;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;line-height:18px;display:inline-flex;align-self:flex-start}',
100
+ '.sap-adviceDetailToggle:hover{color:var(--dsw-alias-label-primary)}',
101
+ '.sap-adviceDetailArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
102
+ '.sap-adviceDetailToggle[aria-expanded="true"] .sap-adviceDetailArrow{transform:rotate(90deg)}',
103
+ '.sap-adviceDetailTable{width:100%;border-collapse:collapse;font-size:12px;line-height:18px}',
104
+ '.sap-adviceDetailTable th{text-align:left;font-weight:500;color:var(--dsw-alias-label-tertiary);padding:3px 8px 3px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}',
105
+ '.sap-adviceDetailTable td{text-align:left;padding:4px 8px 4px 0;border-bottom:1px dotted var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);vertical-align:top}',
106
+ '.sap-adviceDetailTime{color:var(--dsw-alias-label-primary);font-weight:500;white-space:nowrap}',
107
+ '.sap-adviceDetailSession{color:var(--dsw-alias-label-primary)}',
108
+ '.sap-adviceDetailSessionId{color:var(--dsw-alias-label-tertiary);font-size:11px;margin-left:4px}',
109
+ '.sap-adviceDetailChild{font-family:ui-monospace,Consolas,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary)}',
110
+ '.sap-warn-x{color:var(--dsw-alias-state-error-primary)}',
111
+ '.sap-candidates{flex-direction:column;gap:6px;margin-top:14px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);display:flex}',
112
+ '.sap-candidatesTitle{color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;font-weight:700;margin:0}',
113
+ '.sap-candidatesTitle .sap-candidatesNotApplied{color:var(--dsw-alias-label-tertiary);font-weight:400}',
114
+ '.sap-candidatesCount{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;margin:2px 0 0}',
115
+ '.sap-candidate{border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1)}',
116
+ '.sap-candidateSelected{border-color:var(--dsw-alias-label-primary)}',
117
+ '.sap-candidateRow{box-sizing:border-box;width:100%;border:0;background:0 0;padding:8px 10px;margin:0;font:inherit;text-align:left;cursor:pointer;align-items:center;gap:8px;display:flex}',
118
+ '.sap-candidateRadio{box-sizing:border-box;width:14px;height:14px;border:1.5px solid var(--dsw-alias-border-l2);border-radius:7px;flex:none}',
119
+ '.sap-candidateSelected .sap-candidateRadio{border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-label-primary);box-shadow:inset 0 0 0 3px var(--dsw-alias-bg-layer-1)}',
120
+ '.sap-candidateName{font-weight:600;flex:1;min-width:0;padding-right:8px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;overflow-wrap:anywhere}',
121
+ '.sap-candidateValidity{margin-left:auto;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}',
122
+ '.sap-candidateArrow{margin-left:6px;color:var(--dsw-alias-label-tertiary);font-size:9px;line-height:1;flex:none;transition:transform .15s}',
123
+ '.sap-candidateOpen .sap-candidateArrow{transform:rotate(90deg)}',
124
+ '.sap-candidateGroup{flex-direction:column;gap:4px;display:flex}',
125
+ '.sap-candidateGroup + .sap-candidateGroup{margin-top:6px}',
126
+ '.sap-candidateGroupTitle{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;font-weight:700;margin:0}',
127
+ '.sap-candidateSafetyList{margin:0 0 0 16px;padding:0;list-style:none;flex-direction:column;gap:2px;display:flex}',
128
+ '.sap-candidateSafetyItem{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}',
129
+ '.sap-candidateSafetyToggle{border:0;background:0 0;padding:0;margin:0;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);align-items:center;gap:4px;font-size:12px;line-height:18px;display:inline-flex;align-self:flex-start;font-weight:600}',
130
+ '.sap-candidateSafetyToggle:hover{color:var(--dsw-alias-label-primary)}',
131
+ '.sap-candidateSafetyArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
132
+ '.sap-candidateSafetyToggleOpen .sap-candidateSafetyArrow{transform:rotate(90deg)}',
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
+ '.sap-adviceFootNotes{flex-direction:column;gap:2px;display:flex;min-width:0}',
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}',
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)}',
156
+ '.sap-candidateSettingsRow b{font-weight:600;color:var(--dsw-alias-label-primary)}',
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}',
158
+ '.sap-candidateSettingsEdit:hover{color:var(--dsw-alias-label-primary)}',
159
+ '.sap-candidateSettingsPanel{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:12px 14px;margin-top:8px;flex-direction:column;gap:10px;display:flex;background:var(--dsw-alias-bg-layer-1)}',
160
+ '.sap-candidateSettingsField{flex-direction:column;gap:6px;display:flex}',
161
+ '.sap-candidateSettingsOption{align-items:center;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);display:inline-flex;cursor:pointer}',
162
+ '.sap-candidateSettingsOptions{flex-wrap:wrap;gap:4px 14px;display:flex}',
163
+ '.sap-candidateSettingsCustom{align-items:center;gap:8px;display:flex}',
164
+ '.sap-candidateSettingsCustom .sap-input{max-width:120px}',
165
+ '.sap-candidateSettingsActions{align-items:center;gap:10px;display:flex}',
91
166
  '.sap-desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
92
167
  '.sap-warn{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}',
93
168
  '.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}',
@@ -121,6 +196,10 @@ const CSS = [
121
196
  '.sap-customized[open]>.sap-customizedSummary:before{transform:rotate(45deg)translate(-1px,-1px)}',
122
197
  '.sap-customizedSummary:hover{color:var(--dsw-alias-label-primary)}',
123
198
  '.sap-customizedBody{flex-direction:column;gap:12px;padding-top:12px;display:flex}',
199
+ '.sap-customizedArrowEnd .sap-customizedSummary:before{display:none}',
200
+ '.sap-customizedEndArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
201
+ '.sap-customizedArrowEnd[open] .sap-customizedEndArrow{transform:rotate(90deg)}',
202
+ '.sap-reminderHandled{border-top:none;padding-top:8px}',
124
203
  '.sap-editorActions{justify-content:flex-end;gap:8px;display:flex}',
125
204
  '.sap-primaryButton,.sap-secondaryButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border:none;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}',
126
205
  '.sap-primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}',
@@ -284,6 +363,37 @@ const CSS = [
284
363
  '.sap-escapeAdd{align-items:center;gap:8px;display:flex}',
285
364
  '.sap-chipRemove{box-sizing:border-box;color:var(--dsw-alias-state-error-primary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:4px;padding:0 4px;font-size:11px;line-height:16px}',
286
365
  '.sap-chipRemove:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}',
366
+ // 六页签:横向 tab(下划线激活+加粗)+ 内容区(非激活隐藏,全部挂载保状态)
367
+ '.sap-tabs{display:flex;flex-wrap:wrap;gap:2px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}',
368
+ '.sap-tab{box-sizing:border-box;border:0;background:0 0;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;line-height:20px;padding:9px 14px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap;user-select:none}',
369
+ '.sap-tab:hover{color:var(--dsw-alias-label-primary)}',
370
+ '.sap-tabActive{color:var(--dsw-alias-label-primary);font-weight:700;border-bottom-color:var(--dsw-alias-label-primary)}',
371
+ '.sap-zone{display:none}',
372
+ '.sap-zoneActive{display:block}',
373
+ // 建议摘要行 + 展开区(v8:卡片边界 .advice-item,无二级标题)
374
+ '.sap-adviceItem{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex-direction:column;display:flex}',
375
+ '.sap-adviceSummary{box-sizing:border-box;width:100%;border:0;background:0 0;padding:10px 14px;margin:0;font:inherit;text-align:left;cursor:pointer;flex-direction:column;align-items:stretch;gap:2px;display:flex}',
376
+ '.sap-adviceObjLine{align-items:center;gap:6px;flex-wrap:wrap;min-width:0;display:flex}',
377
+ '.sap-adviceSummaryLine{align-items:center;gap:8px;flex-wrap:wrap;min-width:0;display:flex}',
378
+ '.sap-adviceSummary:hover{background:var(--dsw-alias-interactive-bg-hover)}',
379
+ '.sap-adviceObj{color:var(--dsw-alias-label-primary);font-weight:600;font-size:13px;line-height:20px}',
380
+ '.sap-adviceSummaryText{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}',
381
+ '.sap-adviceConf{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:nowrap}',
382
+ '.sap-adviceSummaryArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
383
+ '.sap-adviceItemOpen .sap-adviceSummaryArrow{transform:rotate(90deg)}',
384
+ '.sap-adviceNoProfileTag{font-size:11px;line-height:16px;color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:3px;padding:0 5px;flex:none}',
385
+ '.sap-adviceDetail{border-top:1px dashed var(--dsw-alias-border-l2);padding:10px 14px 12px;flex-direction:column;gap:8px;display:flex}',
386
+ // 审计明细折叠表(只读展示)
387
+ '.sap-auditCollapse{border:0;background:0 0;padding:0;margin:0;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;line-height:18px;display:inline-flex;align-self:flex-start}',
388
+ '.sap-auditCollapse:hover{color:var(--dsw-alias-label-primary)}',
389
+ '.sap-auditArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
390
+ '.sap-auditCollapse[aria-expanded="true"] .sap-auditArrow{transform:rotate(90deg)}',
391
+ '.sap-auditTable{width:100%;border-collapse:collapse;font-size:12px;line-height:18px;margin-top:4px}',
392
+ '.sap-auditTable th{text-align:left;font-weight:500;color:var(--dsw-alias-label-tertiary);padding:3px 8px 3px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}',
393
+ '.sap-auditTable td{text-align:left;padding:4px 8px 4px 0;border-bottom:1px dotted var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);vertical-align:top}',
394
+ '.sap-auditTime{color:var(--dsw-alias-label-primary);font-weight:500;white-space:nowrap}',
395
+ '.sap-auditEvent{color:var(--dsw-alias-label-secondary);white-space:nowrap}',
396
+ '.sap-auditObject{color:var(--dsw-alias-label-primary)}',
287
397
  ].join('\n')
288
398
 
289
399
  // ── 纯数据:dispatch 卡片面向用户固定文案 ──────────────────────────────────
@@ -299,7 +409,7 @@ const ZH = {
299
409
  chipProvider: '提供方',
300
410
  chipModel: '模型',
301
411
  chipEffort: '推理强度',
302
- chipTier: '档位',
412
+ chipTier: '档位(估算口径)',
303
413
  chipEnabled: '已启用',
304
414
  chipDisabled: '已禁用',
305
415
  ignoredMark: '已忽略',
@@ -327,6 +437,7 @@ const ZH = {
327
437
  phaseLegacy: '已结算(旧版)',
328
438
  // 提醒系统
329
439
  reminderCenterTitle: '提醒中心',
440
+ reminderCenterDesc: '需要你关注的事件汇总,所有动作均有审计记录;处理过的条目折叠保存',
330
441
  reminderEmpty: '暂无提醒(治理事件会在这里留痕)',
331
442
  reminderAck: '标记已读',
332
443
  reminderAdopt: '采纳',
@@ -375,7 +486,7 @@ const ZH = {
375
486
  ledgerGateRejectDist: '安全检查拒绝分布',
376
487
  ledgerByProfile: '按方案',
377
488
  ledgerByModel: '按模型',
378
- ledgerByTier: '按档位',
489
+ ledgerByTier: '按档位(估算口径)',
379
490
  ledgerEmpty: '本会话暂无派发记录',
380
491
  ledgerEmptyHint: '在此会话中派发一次子 Agent 后,即可查看完整的决策轨迹。',
381
492
  ledgerLoadOlder: '加载更早',
@@ -422,7 +533,7 @@ const ZH = {
422
533
  ledgerCostCardScope: '本会话口径',
423
534
  ledgerCostCardCounter: '若继承父会话',
424
535
  ledgerCostCardSave: '省',
425
- ledgerEvidenceToggle: '查看完整因果链 ',
536
+ ledgerEvidenceToggle: '查看完整因果链 ',
426
537
  ledgerEvidenceClose: '收起因果链 ▴',
427
538
  ledgerVsTitle: '请求 vs 生效',
428
539
  ledgerVsDiff: '{n} 项与请求不同(琥珀底行)',
@@ -456,14 +567,14 @@ const ZH = {
456
567
  ledgerExecLiveNote: '后台执行中:过程事件随活动更新',
457
568
  ledgerEventStart: '开始执行',
458
569
  ledgerEventCall: '第 {n} 次工具调用',
459
- ledgerEventStall: '⚠ 停滞 {s}s 无活动(超过阈值 {t}s',
570
+ ledgerEventStall: '⚠ 停滞 {s}s 无活动(超过 {t}s 上限)',
460
571
  ledgerEventStalling: '⚠ 停滞中(已 {s}s 无活动)',
461
572
  ledgerEventIntervene: '主 agent 干预',
462
573
  ledgerEventResume: '恢复执行',
463
574
  ledgerEventDone: '完成',
464
575
  ledgerEventRunning: '执行中…',
465
576
  ledgerEventMoreCalls: '…其间还有 {n} 次调用',
466
- ledgerStallThresholdNote: '停滞阈值 30s(待实测)',
577
+ ledgerStallThresholdNote: '停滞上限 30s(待实测)',
467
578
  ledgerSettleOutput: '输出摘要',
468
579
  ledgerSettleCostBreakdown: '成本分解',
469
580
  ledgerSettleInheritNote: '未指定模型,按继承父会话估算',
@@ -484,6 +595,7 @@ const ZH = {
484
595
  ignoredChipTitleEmpty: '请求值已忽略',
485
596
  // 逃生舱(放行非官方预设)
486
597
  escapeTitle: '逃生舱(放行非官方预设)',
598
+ escapeCardDesc: '放行非官方预设的高风险通道,默认关闭',
487
599
  escapeWarn: '逃生舱已开启:允许派发非官方预设。风险自负——其余安全检查仍全量生效,每个放行都会留审计记录。',
488
600
  escapeEmpty: '暂无放行预设',
489
601
  escapeAdd: '添加',
@@ -492,23 +604,36 @@ const ZH = {
492
604
  escapeAddEmpty: '逃生舱预设 id 不能为空',
493
605
  // 派发优化建议注入(开关 + 只提示面板)
494
606
  adviceTitle: '派发优化建议',
607
+ advicePanelDesc: '根据历史派发数据向父会话附优化提示(例:某方案近期成功率偏低,可考虑换方案)。只提示,不自动改配置',
608
+ adviceEnableLabel: '开启建议',
609
+ adviceSectionReason: '理由',
610
+ adviceSectionPerf: '近期表现',
611
+ adviceSectionDetail: '派发明细',
495
612
  adviceSwitchOn: '开(只提示,不自动改配置)',
496
613
  adviceSwitchOff: '关(只提示,不自动改配置)',
497
- advicePanelEmpty: '暂无建议(需累计 ≥3 次派发且加权成功分低于阈值)',
614
+ advicePanelEmpty: '暂无建议(近期派发表现不佳时才会生成)',
498
615
  advicePanelGlobal: '全局',
499
- advicePanelReadonly: '(仅提示:统计与建议仅供参考,不改变派发行为)',
616
+ advicePanelReadonly: '(仅提示:统计与建议不改变派发行为)',
500
617
  adviceDirUp: '可上调预算',
501
618
  adviceDirDown: '建议降级',
502
619
  adviceDirFlat: '维持现状',
503
- adviceConfidence: '置信度',
504
- adviceRowLocate: '定位方案',
505
- adviceRowNoMatch: '无匹配方案',
620
+ adviceConfidence: '置信度:',
621
+ confidenceZh: { low: '', medium: '中等', high: '高' },
622
+ adviceRowLocate: '定位到方案管理',
623
+ // 建议行/理由行(建议直接带对象;理由灰字小字)
624
+ adviceSuggestionPrefix: '建议:',
625
+ adviceReasonPrefix: '理由:',
626
+ adviceDirShortDown: '降级',
627
+ adviceDirShortUp: '升预算',
628
+ // 建议对象标题(摘要行对象与底部备注共用)
629
+ adviceTitleNoProfile: '手动配置的派发',
630
+ adviceTitleProfilePrefix: '方案:',
506
631
  costEditProfile: '定位方案',
507
632
  costNoProfile: '无对应方案',
508
633
  advancedSafetyLine: '安全保证:派发不豁免审批 · 子代理工具不会超出父会话 · 成本与深度有硬上限。',
509
634
  resetAllBuiltins: '重置所有内置方案',
510
635
  statusLost: '审计丢失',
511
- profilesIntro: '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可在「高级」区重置回默认;每个方案可单独启用/禁用。',
636
+ profilesIntro: '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可在此重置回默认;每个方案可单独启用/禁用。',
512
637
  draftSavedWithId: '已保存到 draft,可在设置页「方案管理」中应用;方案 id:{id}',
513
638
  draftCopyId: '复制 id',
514
639
  ledgerCopyTarget: '复制',
@@ -520,14 +645,158 @@ const ZH = {
520
645
  costInheritRatio: '继承占比',
521
646
  costByModel: '按模型',
522
647
  draftPreview: '安全检查预览',
523
- draftPreviewPass: '安全检查预览:全部通过 ✓',
524
- draftPreviewFail: '安全检查预览:存在拒绝项'
648
+ draftPreviewPass: '安全检查:全部通过 ✓',
649
+ draftPreviewFail: '安全检查:存在拒绝项',
650
+ // 建议面板候选方案区(三层:标题行 → 说明句 → 候选项列表;有效期徽标在候选行右侧;
651
+ // 应用确认流随后续版本开放,不可应用提示收进展开区尾部)。
652
+ candidateTitle: '候选方案',
653
+ candidateNotApplied: '(未应用)',
654
+ candidateCountSuffix: ' 个 · 单选',
655
+ candidateCountNote: ' · 应用前需你确认',
656
+ candidateIntro: '系统基于近期表现生成的改进方案;应用前需要你确认,并进入观察期。',
657
+ candidateRegenerate: '重新生成候选',
658
+ candidateSafetyPending: '安全检查:正在核对…',
659
+ candidateGroupChanges: '改动',
660
+ candidateGroupGenerated: '生成信息',
661
+ candidateSafetyPrefix: '安全检查:',
662
+ candidateSafetyPass: '全部通过 ✓',
663
+ candidateSafetyFail: '存在拒绝项',
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: '查看建议',
699
+ candidateValidityForever: '⏱ 无期限',
700
+ candidateValidityPrefix: '⏱ 至 ',
701
+ candidateGeneratedPrefix: '生成于 ',
702
+ candidateSamplesPrefix: '基于最近 ',
703
+ candidateSamplesSuffix: ' 次派发',
704
+ candidatePerfPrefix: '近期表现 ',
705
+ candidatePerfSuffix: '(满分 100)',
706
+ candidateNoSample: '暂无近期派发样本',
707
+ // 候选卡底部:无对应方案存来源配置为新方案;有对应方案调整按钮灰置 + 定位。
708
+ adviceSaveProfile: '保存为方案',
709
+ adviceApplyDisabled: '按建议调整方案',
710
+ adviceSavedNotice: '已保存为新方案:{id}',
711
+ adviceFootNoteSave: '仅保存预设、模型与提供方;自定义人格与工具过滤无法还原(隐私保护不落盘),如需完整配置请手动编辑方案。',
712
+ adviceFootNoteMatchedPrefix: '对应方案:',
713
+ // 建议摘要行(v8:对象/建议/置信度;展开后见理由与明细)
714
+ adviceNoProfileTag: '无方案',
715
+ // 安全页签:安全保证卡 + 审计健康卡(只读明细表)
716
+ safetyCardTitle: '安全保证',
717
+ safetyCardDesc: '派发与审计的安全保证,明细只读',
718
+ safetyCheckNote: '派发前安全检查:白名单 / 成本 / 工具面 / 审批不豁免——拒绝分布见下方审计明细。',
719
+ auditCardTitle: '审计健康',
720
+ auditCardDesc: '治理审计状态与留痕,异常红字提示',
721
+ auditDetailToggle: '查看审计明细(点击展开)',
722
+ auditDetailEmpty: '暂无审计明细(治理事件会在这里留痕)',
723
+ auditDetailScope: '只读展示,暂不提供导出与过滤',
724
+ auditColTime: '时间',
725
+ auditColEvent: '事件',
726
+ auditColObject: '对象',
727
+ auditColResult: '结果',
728
+ auditEventZh: { 'adoption-false': '派发未采纳', 'escape-allow': '逃生舱放行', 'audit-degraded': '审计降级' },
729
+ auditResultPending: '未处理',
730
+ auditResultRead: '已读',
731
+ auditResultActedPrefix: '已处理:',
732
+ // 近期派发明细折叠表
733
+ adviceDetailTogglePrefix: '近期派发明细 · ',
734
+ adviceDetailToggleSuffix: ' 次(点击展开)',
735
+ adviceDetailEmpty: '近期暂无明细',
736
+ adviceDetailColTime: '时间',
737
+ adviceDetailColSession: '主会话(发起者)',
738
+ adviceDetailColChild: '子 Agent',
739
+ adviceDetailColOutcome: '结果',
740
+ adviceDetailColElapsed: '耗时',
741
+ adviceDetailColMode: '模式',
742
+ dispatchOutcomeZh: { completed: '完成', failed: '失败', killed: '终止' },
743
+ // 候选管理设置(顶部设置行 + 就地浮层)
744
+ candidateSettingsRow: '候选时效:',
745
+ candidateSettingsEdit: '修改 ⚙',
746
+ candidateSettingsSaved: '候选设置已保存',
747
+ candidateModeAuto: '自动换新',
748
+ candidateModeManual: '手动维护',
749
+ candidateTtlLabel: '有效期',
750
+ candidateTtl24: '24 小时',
751
+ candidateTtl72: '3 天',
752
+ candidateTtl168: '7 天',
753
+ candidateTtl0: '不自动过期',
754
+ candidateTtlCustom: '自定义',
755
+ candidateSettingsModeLabel: '更新模式',
756
+ candidateSettingsModeAutoText: '自动换新:数据显著变化时换代',
757
+ candidateSettingsModeManualText: '手动维护:不自动换代,候选保留,可点「重新生成候选」刷新',
758
+ candidateSettingsCustomLabel: '自定义天数(1~365)',
759
+ candidateSettingsCustomInvalid: '自定义有效期无效:请输入 1~365 的整数天数',
760
+ candidateSettingsSave: '保存',
761
+ candidateSettingsCancel: '取消',
762
+ // 编辑表单档位选择器口径说明(档位是估算口径,不决定模型)
763
+ tierFormHint: '档位仅影响估算与方案排序,不改变派发模型;显式选择模型才决定实际模型'
764
+ }
765
+
766
+ // profileKey 内部表示 → 人话(仅展示层;工具提示/日志保留原串):preset:X→预设:X、
767
+ // persona:1→含自定义人格、toolFilter:1→含工具过滤,段组合以 · 连接;
768
+ // (inline) 即未存方案;空键兜底未指定方案特征。
769
+ function profileKeyZh(key) {
770
+ const text = typeof key === 'string' ? key : ''
771
+ const parts = text.split('|').filter((part) => part !== '').map((part) => {
772
+ if (part === '(inline)') return '未存方案'
773
+ const idx = part.indexOf(':')
774
+ const head = idx >= 0 ? part.slice(0, idx) : part
775
+ const value = idx >= 0 ? part.slice(idx + 1) : ''
776
+ if (head === 'preset') return '预设:' + value
777
+ if (head === 'provider') return '提供方:' + value
778
+ if (head === 'model') return '模型:' + value
779
+ if (head === 'persona') return '含自定义人格'
780
+ if (head === 'toolFilter') return '含工具过滤'
781
+ if (head === 'effort') return '推理强度:' + value
782
+ return part
783
+ })
784
+ const zh = parts.join(' · ')
785
+ return zh === '' ? '未指定方案特征' : zh
525
786
  }
526
787
 
527
788
  // ── 纯数据:设置页常量 ─────────────────────────────────────────────────────
528
789
 
529
- const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', toolMode: 'none', toolList: [] }
790
+ const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', tokenTier: 'balanced', toolMode: 'none', toolList: [] }
530
791
  const EFFORT_ZH = { off: '关闭', high: '高', max: '最大' }
792
+ // 设置页五页签顺序(v9:维护并入方案管理尾部折叠卡;方案管理第一位并默认激活)。
793
+ const SETTINGS_TABS = [
794
+ { key: 'profiles', label: '方案管理' },
795
+ { key: 'cost', label: '成本与统计' },
796
+ { key: 'advice', label: '自进化与建议' },
797
+ { key: 'safety', label: '安全' },
798
+ { key: 'reminders', label: '通知与提醒' }
799
+ ]
531
800
  const TOOL_MODES = [
532
801
  ['none', '不限制(继承父会话的全部工具,自动剔除 run_code)'],
533
802
  ['allow', '白名单(只允许勾选的工具)'],
@@ -545,12 +814,14 @@ const GATE_FIX_HINTS = {
545
814
  approval: '审批策略冲突,请核对审批配置',
546
815
  budget: '并发派发数或本会话累计 token 已达上限,请等待在途任务完成或开启新会话'
547
816
  }
817
+ // 安全检查项 → 人话映射(可见面零内部术语;title 保留原串供排查;未知项原样回退)。
818
+ const CHECK_ZH = { sanitize: '配置格式检查', id: '方案编号不冲突', whitelist: '工具范围安全检查', catalog: '模型目录有效' }
548
819
  // 派发决策台账:决策轨迹字段键名 → 中文标签(字段级渲染去「JSON 墙」用)。
549
820
  // 派发决策台账:决策轨迹字段键名 → 中文标签(FIELD_ZH,字段级渲染去「JSON 墙」用)。
550
821
  // 未知键回退原样(fail-soft 不崩溃);同义键(执行/结算/派发摘要字段)一并收录。
551
822
  const FIELD_ZH = {
552
823
  parentPreset: '父预设', parentProvider: '父提供方', parentModel: '父模型', parentToolCount: '父工具数',
553
- tokenTier: '档位', persona_present: '是否设置人格', toolFilter_present: '是否设置工具过滤', envelope: '信封模式',
824
+ tokenTier: '档位(估算口径)', persona_present: '是否设置人格', toolFilter_present: '是否设置工具过滤', envelope: '信封模式',
554
825
  prompt_excerpt: '任务摘要', prompt_truncated: '任务已截断',
555
826
  systemTrustCandidates: '候选白名单', resolvedPreset: '解析后预设', allowed: '放行',
556
827
  field: '检查项', checkedAgainst: '对照', verdict: '结论', cap: '上限',
@@ -562,7 +833,7 @@ const FIELD_ZH = {
562
833
  requestedPreset: '目标预设', checks: '检查项', detail: '详情',
563
834
  effectiveAllowCount: '生效工具数', jobId: '任务ID', reason: '原因', limit: '上限',
564
835
  skipped: '跳过', allowFailOpen: '兼容模式', maxTokens: 'token 上限', maxDepth: '深度上限',
565
- requestedToolFilter: '请求工具过滤', providers: '提供方', modelEfforts: '模型支持档位',
836
+ requestedToolFilter: '请求工具过滤', providers: '提供方', modelEfforts: '模型支持的推理强度',
566
837
  effectiveToolNames: '生效工具', provider: '提供方', model: '模型',
567
838
  reasoningEffort: '推理强度',
568
839
  unknown_keys: '未知字段(已被拒绝)',
@@ -586,7 +857,8 @@ function readAuditFromData(data) {
586
857
  }
587
858
 
588
859
  // /options/summary 响应 → 设置页状态(enabled/options/audit/逃生舱开关与放行集)。
589
- // set 为各 setter 的注入对象;escapePresets 非数组回退空数组,escapeEnabled 仅 true 才开。
860
+ // set 为各 setter 的注入对象;escapePresets 非数组回退空数组,escapeEnabled 仅 true 才开;
861
+ // 候选设置(更新模式/有效期)缺失时回退默认。
590
862
  function applySummaryData(data, set) {
591
863
  set.enabled(data.enabled !== false)
592
864
  set.options({
@@ -598,6 +870,10 @@ function applySummaryData(data, set) {
598
870
  set.escapeEnabled(data.escapeEnabled === true)
599
871
  set.escapePresets(Array.isArray(data.escapePresets) ? data.escapePresets : [])
600
872
  set.evolutionAdvice(data.evolutionAdvice === true)
873
+ set.candidateSettings({
874
+ candidateMode: data.candidateMode === 'manual' ? 'manual' : 'auto',
875
+ candidateTtlH: Number.isFinite(data.candidateTtlH) ? data.candidateTtlH : 24
876
+ })
601
877
  }
602
878
 
603
879
  // ── 纯函数:api / 样式注入 / 文案映射 ──────────────────────────────────────
@@ -1183,11 +1459,11 @@ function buildSubagentBanner(el, subagent, parentPreset, modelState, bannerMode)
1183
1459
 
1184
1460
  // ── 提醒系统 ─────────────────────────────────────────────────────
1185
1461
 
1186
- // 通知中心(设置页高级区内):读 /reminders(与 [6] 审计同源 host store),全动作
1462
+ // 通知中心(设置页通知与提醒页签):读 /reminders(与审计同源 host store),全动作
1187
1463
  // (标记已读/采纳/忽略/拒绝/关闭)经 POST 回写 host 并刷新——一条提醒 = 一条审计记录。
1188
1464
  // sessions 服务供条目主/子会话标题查询与跳转;跳转失败静默(设置页不被导航异常打断)。
1189
1465
  function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1190
- function ReminderCenter() {
1466
+ function ReminderCenter(props) {
1191
1467
  const [reminders, setReminders] = useState([])
1192
1468
  const [error, setError] = useState('')
1193
1469
  const refresh = () => {
@@ -1199,18 +1475,43 @@ function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1199
1475
  const request = action === 'ack' ? api('/reminders/ack', { ids: [id] }) : api('/reminders/action', { id, action })
1200
1476
  request.then(refresh).catch((err) => setError(String((err && err.message) || err)))
1201
1477
  }
1202
- 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)
1203
1480
  }
1204
1481
  return ReminderCenter
1205
1482
  }
1206
1483
 
1207
- function buildReminderCenter(el, reminders, error, act, sessions, openChild) {
1208
- return el('div', { className: 'sap-rowCard' },
1209
- el('div', { className: 'sap-rowHead' }, el('span', { className: 'sap-rowName' }, ZH.reminderCenterTitle)),
1210
- error !== '' ? el('p', { className: 'sap-notice' }, error) : null,
1211
- reminders.length === 0
1212
- ? el('p', { className: 'sap-desc' }, ZH.reminderEmpty)
1213
- : el('ul', { className: 'sap-rows' }, reminders.map((r) => buildReminderRow(el, r, act, sessions, openChild)))
1484
+ // 提醒中心分层:未终态条目(action 为空)进默认列表;已终态条目折叠为「已处理(N)」
1485
+ // 可展开区(原生 details,展开后仍按现有行渲染并保留已处理标注,可追溯)。审计留痕
1486
+ // host 侧动作回写,已处理条目不再堆在默认列表里。全空才显示空态文案;仅剩已处理时
1487
+ // 只显示折叠区,不显示空态。
1488
+ function buildReminderCenter(el, reminders, error, act, sessions, openChild, jumpToAdvice) {
1489
+ const handled = reminders.filter((r) => typeof r.action === 'string' && r.action !== '')
1490
+ const pending = reminders.filter((r) => !(typeof r.action === 'string' && r.action !== ''))
1491
+ const pendingList = pending.length > 0
1492
+ ? el('ul', { className: 'sap-rows' }, pending.map((r) => buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice)))
1493
+ : null
1494
+ const handledList = handled.length > 0
1495
+ ? el('details', { className: 'sap-customized sap-customizedArrowEnd sap-reminderHandled' },
1496
+ el('summary', { className: 'sap-customizedSummary' }, '已处理(' + handled.length + ')', el('span', { className: 'sap-customizedEndArrow' }, '▸')),
1497
+ el('div', { className: 'sap-customizedBody' },
1498
+ el('ul', { className: 'sap-rows' }, handled.map((r) => buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice)))
1499
+ )
1500
+ )
1501
+ : null
1502
+ return el('div', { className: 'sap-card sap-cardStatic' },
1503
+ el('div', { className: 'sap-cardHead' },
1504
+ el('div', { className: 'sap-cardTitleBox' },
1505
+ el('div', { className: 'sap-cardTitle' }, ZH.reminderCenterTitle),
1506
+ el('div', { className: 'sap-cardDesc' }, ZH.reminderCenterDesc)
1507
+ )
1508
+ ),
1509
+ el('div', { className: 'sap-cardBody' },
1510
+ error !== '' ? el('p', { className: 'sap-notice' }, error) : null,
1511
+ pendingList === null && handledList === null
1512
+ ? el('p', { className: 'sap-desc' }, ZH.reminderEmpty)
1513
+ : [pendingList, handledList]
1514
+ )
1214
1515
  )
1215
1516
  }
1216
1517
 
@@ -1295,20 +1596,19 @@ function buildUnadoptedFields(el, r, sessions, openChild) {
1295
1596
  )
1296
1597
  }
1297
1598
 
1298
- // 未采纳条目按钮四件套:查看子会话(跳转)/ 采纳(审计回写,未采纳即可用)/
1299
- // 标记已读(未读显示)/ 关闭。已采纳后不再重复展示采纳。
1599
+ // 未采纳条目按钮三件套:查看子会话(跳转,恒保留)/ 标记已读(未读显示)/ 关闭。
1600
+ // 已终态条目隐藏标记已读与关闭(只留只读跳转),防对终态条目重复处置产生重复审计;
1601
+ // 提醒上的处置只改变提醒自身状态(已读/终态),不改变采纳判定——采纳与否由事件流自动汇总。
1300
1602
  function buildUnadoptedActions(el, r, act, childAddress, jumpChild) {
1603
+ const terminated = typeof r.action === 'string' && r.action !== ''
1301
1604
  return el('div', { className: 'sap-reminderActions' }, [
1302
1605
  el('button', { type: 'button', className: 'sap-detailToggle', disabled: childAddress === null, onClick: jumpChild }, ZH.reminderViewChild),
1303
- r.action !== 'adopt'
1304
- ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'adopt') }, ZH.reminderAdopt)
1305
- : null,
1306
- r.unread === true ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1307
- el('button', { type: 'button', className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1606
+ terminated ? null : r.unread === true ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1607
+ terminated ? null : el('button', { type: 'button', className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1308
1608
  ])
1309
1609
  }
1310
1610
 
1311
- // 未采纳提醒条目(完整版):标题 + 五字段 + 按钮四件套 + 已处理标注。
1611
+ // 未采纳提醒条目(完整版):标题 + 五字段 + 按钮三件套 + 已处理标注。
1312
1612
  function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1313
1613
  const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1314
1614
  const severityText = r.severity === 'P0' ? '紧急' : '注意'
@@ -1329,7 +1629,38 @@ function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1329
1629
  )
1330
1630
  }
1331
1631
 
1332
- function buildReminderRow(el, r, act, sessions, openChild) {
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
+
1659
+ // 通知类提醒行:P0 未读只保留「标记已读」(常驻至已读,不可跳过未读);已读后出现
1660
+ // 「关闭」(与 P1/P2 同);P1/P2 恒保留「标记已读 / 关闭」。已终态条目隐藏动作按钮,
1661
+ // 只显示已处理灰字标注(折叠区可追溯、防对终态条目误点产生重复审计)。
1662
+ function buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice) {
1663
+ if (r.kind === 'suggestion-rollback') return buildRollbackReminderRow(el, r, act, jumpToAdvice)
1333
1664
  if (r.kind === 'adoption-false') return buildUnadoptedReminderRow(el, r, act, sessions, openChild)
1334
1665
  const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1335
1666
  const severityText = r.severity === 'P0' ? '紧急' : '注意'
@@ -1341,10 +1672,14 @@ function buildReminderRow(el, r, act, sessions, openChild) {
1341
1672
  ),
1342
1673
  el('div', { className: 'sap-reminderMeta', title: session.title }, session.text),
1343
1674
  el('p', { className: 'sap-desc' }, r.detail),
1344
- el('div', { className: 'sap-reminderActions' }, [
1345
- r.unread === true ? el('button', { className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1346
- el('button', { className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1347
- ]),
1675
+ typeof r.action === 'string' && r.action !== ''
1676
+ ? null
1677
+ : el('div', { className: 'sap-reminderActions' }, [
1678
+ r.unread === true ? el('button', { className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1679
+ r.severity !== 'P0' || r.unread !== true
1680
+ ? el('button', { className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1681
+ : null
1682
+ ]),
1348
1683
  typeof r.action === 'string' && r.action !== ''
1349
1684
  ? el('p', { className: 'sap-metaValueDefault' }, ZH.reminderActed.replace('{action}', ZH.reminderActionZh[r.action] || r.action))
1350
1685
  : null
@@ -2107,6 +2442,7 @@ function makeEditActions(s, loadEfforts, loadTools) {
2107
2442
  provider: typeof profile.provider === 'string' ? profile.provider : '',
2108
2443
  model: typeof profile.model === 'string' ? profile.model : '',
2109
2444
  reasoningEffort: typeof profile.reasoningEffort === 'string' ? profile.reasoningEffort : '',
2445
+ tokenTier: typeof profile.tokenTier === 'string' && (profile.tokenTier === 'cheap' || profile.tokenTier === 'balanced' || profile.tokenTier === 'premium') ? profile.tokenTier : 'balanced',
2110
2446
  toolMode,
2111
2447
  toolList: toolMode === 'allow' ? allowArr : denyArr
2112
2448
  })
@@ -2134,6 +2470,7 @@ function makeSubmitActions(api, s, refresh, applyWrite) {
2134
2470
  add('provider', s.form.provider)
2135
2471
  add('model', s.form.model)
2136
2472
  add('reasoningEffort', s.form.reasoningEffort)
2473
+ if (typeof s.form.tokenTier === 'string' && s.form.tokenTier !== '') payload.tokenTier = s.form.tokenTier
2137
2474
  const list = Array.isArray(s.form.toolList) ? s.form.toolList : []
2138
2475
  if (s.form.toolMode === 'allow' && list.length > 0) payload.toolFilter = { allow: list }
2139
2476
  else if (s.form.toolMode === 'deny' && list.length > 0) payload.toolFilter = { deny: list }
@@ -2173,7 +2510,18 @@ function makeToggle(api, path, payloadKey, get, set, setError) {
2173
2510
  function makeRowActions(api, s, refresh, applyWrite) {
2174
2511
  const toggleEnabled = makeToggle(api, '/set-enabled', 'enabled', () => s.enabled, s.setEnabled, s.setError)
2175
2512
  const toggleEscape = makeToggle(api, '/set-escape', 'enabled', () => s.escapeEnabled, s.setEscapeEnabled, s.setError)
2176
- const toggleAdvice = makeToggle(api, '/set-evolution-advice', 'advice', () => s.evolutionAdvice, s.setEvolutionAdvice, s.setError)
2513
+ // 建议开关切换后立即刷新 /list:候选随建议链同生共死(开→生成可见,关→不再下发)。
2514
+ const toggleAdvice = () => {
2515
+ const next = !s.evolutionAdvice
2516
+ s.setEvolutionAdvice(next)
2517
+ s.setError('')
2518
+ api('/set-evolution-advice', { advice: next })
2519
+ .then(() => refresh())
2520
+ .catch((err) => {
2521
+ s.setEvolutionAdvice(!next)
2522
+ s.setError(String((err && err.message) || err))
2523
+ })
2524
+ }
2177
2525
  const setProfileEnabled = (profile, next) => () => {
2178
2526
  s.setError('')
2179
2527
  api('/set-profile-enabled', { id: profile.id, enabled: next })
@@ -2247,7 +2595,7 @@ function makeToolsLoader(api, setTools) {
2247
2595
  }
2248
2596
  }
2249
2597
 
2250
- // 版本探测 → 拉 /options/versions;warnings 非空时维护区展示告警,否则显示插件版本。
2598
+ // 版本探测 → 拉 /options/versions;维护区 0.5.0 再统一设计归属,数据暂留。
2251
2599
  function makeVersionsLoader(api, setVersionWarnings, setPluginVersion) {
2252
2600
  return () => {
2253
2601
  api('/options/versions')
@@ -2259,10 +2607,19 @@ function makeVersionsLoader(api, setVersionWarnings, setPluginVersion) {
2259
2607
  }
2260
2608
  }
2261
2609
 
2610
+ // 会话服务快照(派发明细表查主会话标题用);拿不到回退 null(fail-soft)。
2611
+ function sessionsSnapshot(getSessions) {
2612
+ try {
2613
+ return typeof getSessions === 'function' ? getSessions() : null
2614
+ } catch {
2615
+ return null
2616
+ }
2617
+ }
2618
+
2262
2619
  // 定位方案:展开方案管理区 + 高亮对应卡片 + 滚动到可视区(建议/成本行的去向)。
2263
2620
  function makeLocateProfile(s) {
2264
2621
  return (profileId) => () => {
2265
- s.setOpenSections((prev) => ({ ...prev, 1: true }))
2622
+ s.setActiveTab('profiles')
2266
2623
  s.setHighlightId(String(profileId))
2267
2624
  setTimeout(() => {
2268
2625
  const node = document.getElementById('sap-profile-' + String(profileId))
@@ -2272,6 +2629,155 @@ function makeLocateProfile(s) {
2272
2629
  }
2273
2630
  }
2274
2631
 
2632
+ // 候选动作:行点击展开/收起(默认收起,展开态不持久化;展开时复用 /draft/preview
2633
+ // 只读安全检查流);radio 仅选中(不改展开态);安全检查在展开区内部默认收起;
2634
+ // 派发明细折叠行独立开关。
2635
+ function makeCandidateActions(api, s) {
2636
+ const previewCandidate = (candidate) => () => {
2637
+ s.setError('')
2638
+ api('/draft/preview', { config: candidate.config ?? {}, name: candidate.name ?? '', description: candidate.description ?? '', source: 'evolution' })
2639
+ .then((data) => {
2640
+ const previews = { ...(s.draftPreviews || {}) }
2641
+ previews[candidate.id] = { ok: data.ok === true, checks: Array.isArray(data.checks) ? data.checks : [] }
2642
+ s.setDraftPreviews(previews)
2643
+ })
2644
+ .catch((err) => s.setError(String((err && err.message) || err)))
2645
+ }
2646
+ const toggleCandidateOpen = (candidate) => () => {
2647
+ const openState = s.candidateOpen !== null && typeof s.candidateOpen === 'object' ? s.candidateOpen : {}
2648
+ if (openState[candidate.id] === true) {
2649
+ s.setCandidateOpen({ ...openState, [candidate.id]: false })
2650
+ } else {
2651
+ s.setCandidateOpen({ ...openState, [candidate.id]: true })
2652
+ previewCandidate(candidate)()
2653
+ }
2654
+ }
2655
+ const toggleCandidateSafety = (candidate) => () => {
2656
+ const openState = s.candidateSafetyOpen !== null && typeof s.candidateSafetyOpen === 'object' ? s.candidateSafetyOpen : {}
2657
+ s.setCandidateSafetyOpen({ ...openState, [candidate.id]: openState[candidate.id] !== true })
2658
+ }
2659
+ const selectCandidate = (profileKey, candidateId) => () => {
2660
+ const map = s.candidateSelected !== null && typeof s.candidateSelected === 'object' ? s.candidateSelected : {}
2661
+ s.setCandidateSelected({ ...map, [profileKey]: candidateId })
2662
+ }
2663
+ const toggleDetail = (profileKey) => () => {
2664
+ const openState = s.detailOpen !== null && typeof s.detailOpen === 'object' ? s.detailOpen : {}
2665
+ s.setDetailOpen({ ...openState, [profileKey]: openState[profileKey] !== true })
2666
+ }
2667
+ return { previewCandidate, toggleCandidateOpen, toggleCandidateSafety, selectCandidate, toggleDetail }
2668
+ }
2669
+
2670
+ // 表单 → 有效期小时数:自定义档按天数×24(1~365 整数),其余档原样;
2671
+ // 非法输入返回 null(调用方就地报错不提交)。
2672
+ function candidateTtlFromForm(form) {
2673
+ if (form.candidateTtlChoice === 'custom') {
2674
+ const days = Number(String(form.customDays ?? '').trim())
2675
+ if (!Number.isInteger(days) || days < 1 || days > 365) return null
2676
+ return days * 24
2677
+ }
2678
+ const ttlH = Number(form.candidateTtlChoice)
2679
+ if (!Number.isInteger(ttlH) || !(ttlH === 0 || (ttlH >= 1 && ttlH <= 8760))) return 24
2680
+ return ttlH
2681
+ }
2682
+
2683
+ // 页签内交互动作:建议摘要行展开(按 profileKey)与审计明细折叠。
2684
+ function makeTabActions(s) {
2685
+ return {
2686
+ toggleAdviceOpen: (profileKey) => () => {
2687
+ const map = s.adviceOpen !== null && typeof s.adviceOpen === 'object' ? s.adviceOpen : {}
2688
+ s.setAdviceOpen({ ...map, [profileKey]: map[profileKey] !== true })
2689
+ },
2690
+ toggleAuditDetail: () => s.setAuditDetailOpen(!(s.auditDetailOpen === true))
2691
+ }
2692
+ }
2693
+
2694
+ // 候选管理设置动作:设置浮层开合、表单字段、保存(POST /settings/candidate)与
2695
+ // 取消;重新生成候选(POST /evolution/regenerate)与保存为方案(POST /advice/save-profile)。
2696
+ function makeCandidateSettingsActions(api, s, refresh, applyWrite) {
2697
+ const formOf = () => (s.candidateSettingsForm !== null && typeof s.candidateSettingsForm === 'object' ? s.candidateSettingsForm : {})
2698
+ const openSettings = () => {
2699
+ const settings = s.candidateSettings !== null && typeof s.candidateSettings === 'object' ? s.candidateSettings : { candidateMode: 'auto', candidateTtlH: 24 }
2700
+ const choice = candidateTtlChoiceOf(settings.candidateTtlH)
2701
+ s.setCandidateSettingsOpen(true)
2702
+ s.setCandidateSettingsForm({ candidateMode: settings.candidateMode, candidateTtlChoice: choice, customDays: choice === 'custom' ? String(settings.candidateTtlH / 24) : '', error: '' })
2703
+ }
2704
+ const closeSettings = () => {
2705
+ s.setCandidateSettingsOpen(false)
2706
+ s.setCandidateSettingsForm((prev) => ({ ...(prev ?? {}), error: '' }))
2707
+ }
2708
+ const setForm = (patch) => s.setCandidateSettingsForm((prev) => ({ ...(prev ?? {}), ...patch, error: '' }))
2709
+ const save = () => {
2710
+ const form = formOf()
2711
+ const ttlH = candidateTtlFromForm(form)
2712
+ if (ttlH === null) {
2713
+ s.setCandidateSettingsForm((prev) => ({ ...(prev ?? {}), error: ZH.candidateSettingsCustomInvalid }))
2714
+ return
2715
+ }
2716
+ s.setError('')
2717
+ api('/settings/candidate', { candidateMode: form.candidateMode === 'manual' ? 'manual' : 'auto', candidateTtlH: ttlH })
2718
+ .then((data) => {
2719
+ s.setCandidateSettings({ candidateMode: data.candidateMode === 'manual' ? 'manual' : 'auto', candidateTtlH: Number.isFinite(data.candidateTtlH) ? data.candidateTtlH : 24 })
2720
+ s.setCandidateSettingsOpen(false)
2721
+ applyWrite(data, ZH.candidateSettingsSaved)
2722
+ refresh()
2723
+ })
2724
+ .catch((err) => s.setError(String((err && err.message) || err)))
2725
+ }
2726
+ const post = (path, body, onData) => {
2727
+ s.setError('')
2728
+ api(path, body).then(onData).catch((err) => s.setError(String((err && err.message) || err)))
2729
+ }
2730
+ return {
2731
+ toggleCandidateSettings: () => (s.candidateSettingsOpen === true ? closeSettings() : openSettings()),
2732
+ setCandidateFormMode: (mode) => () => setForm({ candidateMode: mode }),
2733
+ setCandidateFormTtl: (choice) => () => setForm({ candidateTtlChoice: choice }),
2734
+ setCandidateFormCustomDays: (value) => setForm({ customDays: value }),
2735
+ saveCandidateSettings: save,
2736
+ cancelCandidateSettings: closeSettings,
2737
+ regenerateCandidates: (profileKey) => () => post('/evolution/regenerate', { profileKey }, () => { s.setSavedNotice(ZH.candidateRegenerate + ' 完成'); refresh() }),
2738
+ saveAdviceProfile: (profileKey) => () => post('/advice/save-profile', { profileKey }, (data) => { applyWrite(data, ZH.adviceSavedNotice.replace('{id}', String(data.id ?? ''))); refresh() }),
2739
+ }
2740
+ }
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
+
2275
2781
  function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools) {
2276
2782
  return {
2277
2783
  ...makeFormActions(s, loadEfforts, loadTools),
@@ -2279,6 +2785,10 @@ function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools
2279
2785
  ...makeSubmitActions(api, s, refresh, applyWrite),
2280
2786
  ...makeRowActions(api, s, refresh, applyWrite),
2281
2787
  ...makeEscapeActions(api, s),
2788
+ ...makeCandidateActions(api, s),
2789
+ ...makeCandidateSettingsActions(api, s, refresh, applyWrite),
2790
+ ...makeApplyFlowActions(api, s, refresh),
2791
+ ...makeTabActions(s),
2282
2792
  locateProfile: makeLocateProfile(s),
2283
2793
  }
2284
2794
  }
@@ -2410,13 +2920,24 @@ function buildProfileRows(el, chip, s, actions) {
2410
2920
  ? el('p', { className: 'sap-costLine' },
2411
2921
  [
2412
2922
  typeof profile.avgCost === 'number' ? `平均 ≈${profile.avgCost} 元/次` : '',
2413
- 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} 次派发)` : ''}` : ''
2414
2924
  ].filter(Boolean).join(' · '))
2415
2925
  : null
2416
2926
  )
2417
2927
  })
2418
2928
  }
2419
2929
 
2930
+ // 档位选择器(估算口径):仅影响估算与方案排序,不决定模型;下方附口径说明。
2931
+ function buildTierField(el, s, actions) {
2932
+ return el('div', { className: 'sap-field' },
2933
+ el('span', { className: 'sap-fieldLabel' }, ZH.chipTier),
2934
+ el('select', { className: 'sap-input sap-select', value: s.form.tokenTier, onChange: actions.setField('tokenTier') },
2935
+ Object.entries(ZH.tierZh).map(([id, label]) => el('option', { key: id, value: id }, label))
2936
+ ),
2937
+ el('p', { className: 'sap-metaValueDefault', style: { margin: '4px 0 0' } }, ZH.tierFormHint)
2938
+ )
2939
+ }
2940
+
2420
2941
  // 提供方只读展示:provider 由已选 model 联动派生;未选模型时回退表单存量
2421
2942
  // provider 值(编辑存量 profile 时照常展示),不可独立编辑。
2422
2943
  function buildProviderReadonlyLine(el, s) {
@@ -2449,8 +2970,8 @@ function buildAddCard(el, s, actions, toolSection) {
2449
2970
  buildSelectField(el, s, actions.setField, '模型(子 agent 使用的模型)', 'model', s.options.models,
2450
2971
  (m) => el('option', { key: m.id, value: m.id }, m.name && m.name !== m.id ? `${m.name}(${m.providerName || m.provider})` : m.id)),
2451
2972
  s.options.models.length === 0 ? el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, '未检测到模型目录(headless 部署)——派发成本安全检查将按「兼容模式」开关决定放行或拒绝') : null,
2452
- el('details', { className: 'sap-customized' },
2453
- el('summary', { className: 'sap-customizedSummary' }, '高级选项'),
2973
+ el('details', { className: 'sap-customized sap-customizedArrowEnd' },
2974
+ el('summary', { className: 'sap-customizedSummary' }, '高级选项', el('span', { className: 'sap-customizedEndArrow' }, '▸')),
2454
2975
  el('div', { className: 'sap-customizedBody' },
2455
2976
  el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, 'continuable 模式忽略 preset 换用与推理强度(子 Agent 继承父会话预设),派发结果以「ignored」标注'),
2456
2977
  buildSelectField(el, s, actions.setField, '目标预设(切换子 agent 使用的 Agent 预设)', 'preset', s.options.presets,
@@ -2465,6 +2986,7 @@ function buildAddCard(el, s, actions, toolSection) {
2465
2986
  )
2466
2987
  )
2467
2988
  : buildSelectField(el, s, actions.setField, '推理强度(思考深度)', 'reasoningEffort', effortsForModel, renderEffortOption),
2989
+ buildTierField(el, s, actions),
2468
2990
  toolSection
2469
2991
  )
2470
2992
  ),
@@ -2475,23 +2997,6 @@ function buildAddCard(el, s, actions, toolSection) {
2475
2997
  )
2476
2998
  }
2477
2999
 
2478
- // 派发优化建议注入:一行开关,只提示不改变派发行为。
2479
- function buildAdviceSection(el, s, actions) {
2480
- return el('div', { className: 'sap-rowCard' },
2481
- el('div', { className: 'sap-rowHead' },
2482
- el('span', { className: 'sap-rowIdentity' },
2483
- el('span', { className: 'sap-rowName' }, ZH.adviceTitle)
2484
- ),
2485
- el('span', { className: 'sap-rowActions' },
2486
- el('label', { className: 'sap-switch', title: ZH.adviceTitle },
2487
- el('input', { type: 'checkbox', checked: s.evolutionAdvice, onChange: actions.toggleAdvice }),
2488
- el('span', null, s.evolutionAdvice ? ZH.adviceSwitchOn : ZH.adviceSwitchOff)
2489
- )
2490
- )
2491
- )
2492
- )
2493
- }
2494
-
2495
3000
  // 按 profileKey 的 preset/model 段匹配方案管理区的 profile 卡片(建议行的去向)。
2496
3001
  function matchingProfileOf(profiles, profileKey) {
2497
3002
  if (!Array.isArray(profiles) || typeof profileKey !== 'string') return null
@@ -2508,42 +3013,594 @@ function matchingProfileOf(profiles, profileKey) {
2508
3013
  return null
2509
3014
  }
2510
3015
 
2511
- // 派发优化建议预览面板:每条建议可「定位方案」(高亮方案管理区对应卡片,数据有去向)。
2512
- function buildAdvicePanel(el, s, actions) {
3016
+ // ── 建议面板纯函数(epoch 格式化 / 文案换算 / 建议一句话)────────────────────
3017
+
3018
+ // epoch ms → MM-DD HH:mm(本地时区,与原型同口径);非有限数返回空串。
3019
+ function formatMonthDayTime(ts) {
3020
+ if (typeof ts !== 'number' || !Number.isFinite(ts)) return ''
3021
+ const d = new Date(ts)
3022
+ const p = (n) => String(n).padStart(2, '0')
3023
+ return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
3024
+ }
3025
+
3026
+ // 候选有效期徽标文案:null(不自动过期)= ⏱ 无期限;数字 = ⏱ 至 MM-DD HH:mm。
3027
+ function candidateValidityText(expiry) {
3028
+ if (typeof expiry !== 'number' || !Number.isFinite(expiry)) return ZH.candidateValidityForever
3029
+ return ZH.candidateValidityPrefix + formatMonthDayTime(expiry)
3030
+ }
3031
+
3032
+ // 指标行:把「平均耗时 <ms>ms」换算成短时(40.4s 等);第 1 个指标恒为成功分行,
3033
+ // 历史台账里的旧标签「加权成功分」在此显示层映射为「近期表现分」(值保持 0~1
3034
+ // 原样不改口径,0~100 视图在候选生成信息行)。其余指标原样(信息不删)。
3035
+ function adviceMetricsOf(performanceText) {
3036
+ return adviceEvidenceOf(performanceText).metrics.map((metric, index) => {
3037
+ const hit = /^平均耗时 (.+)$/.exec(metric)
3038
+ if (hit !== null) {
3039
+ const ms = Number(hit[1])
3040
+ return Number.isFinite(ms) ? '平均耗时 ' + formatDurationMs(ms) : metric
3041
+ }
3042
+ if (index === 0) {
3043
+ const space = metric.indexOf(' ')
3044
+ return space > 0 ? '近期表现分 ' + metric.slice(space + 1) : metric
3045
+ }
3046
+ return metric
3047
+ })
3048
+ }
3049
+
3050
+ // 方向与置信度中文(模块级共用:面板建议行与旧格式回退)。
3051
+ function directionZh(d) {
3052
+ return d === '升' ? ZH.adviceDirUp : d === '降' ? ZH.adviceDirDown : ZH.adviceDirFlat
3053
+ }
3054
+
3055
+ function confidenceZhOf(level) {
3056
+ return ZH.confidenceZh[level] || level
3057
+ }
3058
+
3059
+ // 摘要行建议短句:只带对象与方向(置信度在摘要行独立成列,不再并入一句)。
3060
+ function adviceSuggestionShort(a) {
3061
+ const object = typeof a.objectText === 'string' && a.objectText !== '' ? a.objectText : ''
3062
+ if (object === '') return directionZh(a.suggestion)
3063
+ const word = a.suggestion === '降' ? ZH.adviceDirShortDown : a.suggestion === '升' ? ZH.adviceDirShortUp : ''
3064
+ return ZH.adviceSuggestionPrefix + object + (word !== '' ? '(' + word + ')' : '')
3065
+ }
3066
+
3067
+ // 摘要行对象文本:已有方案「方案:名称」;无方案「手动配置的派发(含自定义人格…)」
3068
+ // 并附「无方案」小标签(特征取 profileKeyZh,与候选/对应关系同一口径)。
3069
+ function adviceSummaryObjectText(profile, profileKey) {
3070
+ if (profile !== null) return adviceObjectTitleOf(profile)
3071
+ const features = profileKeyZh(profileKey).split(' · ').filter((part) => part !== '未存方案' && part !== '未指定方案特征').join(' · ')
3072
+ return adviceObjectTitleOf(null) + (features !== '' ? '(' + features + ')' : '')
3073
+ }
3074
+
3075
+ // 卡头标题 = 建议对象:已有方案用「方案:名称」,未匹配用「手动配置的派发」。
3076
+ function adviceObjectTitleOf(profile) {
3077
+ return profile !== null ? ZH.adviceTitleProfilePrefix + (profile.name || profile.id) : ZH.adviceTitleNoProfile
3078
+ }
3079
+
3080
+ // 会话/子 Agent 标识截断(悬停由原生 title 保留全文;不足长度原样)。
3081
+ function sessionIdShortOf(id) {
3082
+ const text = typeof id === 'string' ? id : ''
3083
+ return text.length > 12 ? text.slice(0, 12) + '…' : text
3084
+ }
3085
+
3086
+ function childIdShortOf(id) {
3087
+ const text = typeof id === 'string' ? id : ''
3088
+ return text.length > 8 ? text.slice(0, 8) + '…' : text
3089
+ }
3090
+
3091
+ // 派发模式中文(前台/后台/持久);未知值原样(fail-soft)。
3092
+ function dispatchModeZh(mode) {
3093
+ if (mode === 'background') return ZH.modeBackground
3094
+ if (mode === 'continuable') return ZH.bannerModePersistent
3095
+ if (mode === 'foreground') return ZH.modeForeground
3096
+ return mode
3097
+ }
3098
+
3099
+ // 表现摘要拆两层:计数(完成/失败/终止)与指标(近期表现分/平均耗时/平均输出),
3100
+ // 各渲染一遍;解析不出的部分原样保留(信息不删)。
3101
+ function adviceEvidenceOf(performanceText) {
3102
+ const text = typeof performanceText === 'string' ? performanceText : ''
3103
+ const withoutWindow = text.replace(/^过去 \d+ 次:/, '')
3104
+ const parts = withoutWindow.split(',').map((part) => part.trim()).filter((part) => part !== '')
3105
+ return { counts: parts.length > 0 ? parts[0] : '', metrics: parts.slice(1) }
3106
+ }
3107
+
3108
+ // 候选安全检查四项:缩进列表对齐折叠文字下方(margin-left 16px),人话映射;
3109
+ // 只渲染四项本身,不重复「安全检查:全部通过 ✓」标题行(标题 = 折叠行)。
3110
+ function buildCandidateSafetyChecks(el, preview) {
3111
+ const checks = Array.isArray(preview.checks) ? preview.checks : []
3112
+ return el('ul', { className: 'sap-candidateSafetyList' },
3113
+ checks.map((c) => el('li', { key: String(c.name), className: 'sap-candidateSafetyItem', title: c.name },
3114
+ (c.verdict === 'pass' ? '✅ ' : '❌ ') + (CHECK_ZH[c.name] || c.name) + (typeof c.reason === 'string' && c.reason !== '' ? ':' + c.reason : '')
3115
+ ))
3116
+ )
3117
+ }
3118
+
3119
+ // 候选改动项:候选 config 与建议来源 profileKey 对比成人话列表(不加前缀,
3120
+ // 展开区语境已是改动清单)。
3121
+ // persona/toolFilter 从键里消失 = 去掉该结构;其余字段值不同 = 换成新值。
3122
+ function candidateChangesOf(profileKey, cfg) {
3123
+ const config = cfg !== null && typeof cfg === 'object' ? cfg : {}
3124
+ const text = typeof profileKey === 'string' ? profileKey : ''
3125
+ const seg = {}
3126
+ for (const part of text.split('|')) {
3127
+ const idx = part.indexOf(':')
3128
+ if (idx >= 0) seg[part.slice(0, idx)] = part.slice(idx + 1)
3129
+ }
3130
+ const changes = []
3131
+ const changed = (field, value) => typeof value === 'string' && value !== '' && seg[field] !== value
3132
+ if (seg.persona === '1' && config.persona_present !== true) changes.push('去掉自定义人格')
3133
+ if (seg.toolFilter === '1' && config.toolFilter_present !== true) changes.push('去掉工具过滤')
3134
+ if (changed('preset', config.preset)) changes.push('预设 → ' + config.preset)
3135
+ if (changed('model', config.model)) changes.push('模型 → ' + config.model)
3136
+ if (changed('effort', config.reasoningEffort)) changes.push('推理强度 → ' + config.reasoningEffort)
3137
+ if (typeof config.maxTokens === 'number' && config.maxTokens > 0) changes.push('token 上限 → ' + String(config.maxTokens))
3138
+ return changes
3139
+ }
3140
+
3141
+ // 建议大卡(v16):卡头只留标题 + 右侧「开启建议」开关;卡体顶部 = 管理子卡
3142
+ // (浅底紧凑,仅候选时效行 + 全局行,就地浮层随行);下方 = 建议列表。
3143
+ function buildAdvicePanel(el, s, actions, sessions) {
2513
3144
  const advice = Array.isArray(s.advice) ? s.advice : []
2514
3145
  const global = s.adviceGlobal !== null && typeof s.adviceGlobal === 'object' ? s.adviceGlobal : null
2515
- const directionZh = (d) => (d === '升' ? ZH.adviceDirUp : d === '降' ? ZH.adviceDirDown : ZH.adviceDirFlat)
3146
+ const candidates = Array.isArray(s.evolutionCandidates) ? s.evolutionCandidates : []
3147
+ const candidatesOf = (profileKey) => candidates.filter((c) => c && typeof c === 'object' && c.profile_key === profileKey)
2516
3148
  if (global === null) return null
2517
- if (advice.length === 0) return el('p', { className: 'sap-desc' }, ZH.advicePanelEmpty)
2518
- return el('div', { className: 'sap-advicePanel' },
2519
- el('p', { className: 'sap-metaValueDefault' }, ZH.advicePanelGlobal + ':累计 ' + global.total + ' 次(完成 ' + global.completed + '、失败 ' + global.failed + '、终止 ' + global.killed + ')' + ZH.advicePanelReadonly),
2520
- el('ul', { className: 'sap-rows' },
2521
- advice.map((a) => {
2522
- const profile = matchingProfileOf(s.profiles, a.profileKey)
2523
- return el('li', { key: String(a.profileKey), className: 'sap-rowCard' },
2524
- el('div', { className: 'sap-rowHead' },
2525
- el('span', { className: 'sap-rowName' }, String(a.profileKey)),
2526
- el('span', { className: 'sap-rowActions' },
2527
- profile !== null
2528
- ? el('button', { className: 'sap-detailToggle', onClick: actions.locateProfile(profile.id) }, ZH.adviceRowLocate)
2529
- : el('span', { className: 'sap-metaValueDefault' }, ZH.adviceRowNoMatch)
2530
- )
2531
- ),
2532
- el('p', { className: 'sap-desc' }, a.performanceText + ';' + directionZh(a.suggestion) + '(' + ZH.adviceConfidence + ' ' + a.confidence + ')')
3149
+ return el('div', { className: 'sap-card sap-cardStatic' },
3150
+ el('div', { className: 'sap-cardHead sap-adviceCardHead' },
3151
+ el('div', { className: 'sap-adviceHeadTitleRow' },
3152
+ el('div', { className: 'sap-cardTitleBox' },
3153
+ el('div', { className: 'sap-cardTitle' }, ZH.adviceTitle)
3154
+ ),
3155
+ el('span', { className: 'sap-rowActions' },
3156
+ ZH.adviceEnableLabel,
3157
+ el('label', { className: 'sap-switch', title: ZH.adviceTitle },
3158
+ el('input', { type: 'checkbox', checked: s.evolutionAdvice, onChange: actions.toggleAdvice }),
3159
+ el('span', null, s.evolutionAdvice ? ZH.adviceSwitchOn : ZH.adviceSwitchOff)
3160
+ )
2533
3161
  )
2534
- })
3162
+ ),
3163
+ el('div', { className: 'sap-cardDesc' }, ZH.advicePanelDesc)
3164
+ ),
3165
+ el('div', { className: 'sap-cardBody' },
3166
+ el('div', { className: 'sap-adviceManageCard' },
3167
+ buildCandidateSettingsRow(el, s, actions),
3168
+ el('p', { className: 'sap-adviceGlobalLine' }, ZH.advicePanelGlobal + ':累计 ' + global.total + ' 次(完成 ' + global.completed + '、失败 ' + global.failed + '、终止 ' + global.killed + ')' + ZH.advicePanelReadonly),
3169
+ s.candidateSettingsOpen === true ? buildCandidateSettingsPanel(el, s, actions) : null
3170
+ ),
3171
+ advice.length === 0
3172
+ ? el('p', { className: 'sap-desc' }, ZH.advicePanelEmpty)
3173
+ : el('ul', { className: 'sap-rows' },
3174
+ advice.map((a) => buildAdviceCard(el, s, actions, a, candidatesOf(a.profileKey), sessions))
3175
+ )
2535
3176
  )
2536
3177
  )
2537
3178
  }
2538
3179
 
3180
+ // 详情小节:纯文字加粗小标题(12px label-primary)+ 内容(13px),无左线/无底块。
3181
+ function adviceSectionOf(el, title, children) {
3182
+ return el('div', { className: 'sap-adviceSection' },
3183
+ el('p', { className: 'sap-adviceSectionTitle' }, title),
3184
+ children
3185
+ )
3186
+ }
3187
+
3188
+ // 单张建议卡(v10):摘要行 = 对象/建议/置信度/▸(结论三件套,无加权分);
3189
+ // 展开区 = 观察期状态行 → 小节序列(理由/近期表现/派发明细/候选方案)→ 撤销建议区
3190
+ // (regress)→ 进化轨迹 → 操作区;确认/撤销弹窗就地渲染在卡内。
3191
+ function buildAdviceCard(el, s, actions, a, candidates, sessions) {
3192
+ const profile = matchingProfileOf(s.profiles, a.profileKey)
3193
+ const metrics = adviceMetricsOf(a.performanceText)
3194
+ const rows = s.adviceDetails && typeof s.adviceDetails === 'object' && Array.isArray(s.adviceDetails[a.profileKey]) ? s.adviceDetails[a.profileKey] : []
3195
+ const openMap = s.adviceOpen !== null && typeof s.adviceOpen === 'object' ? s.adviceOpen : {}
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 ?? {}
3202
+ const summary = el('button', { type: 'button', className: 'sap-adviceSummary', 'aria-expanded': open, onClick: actions.toggleAdviceOpen(a.profileKey) },
3203
+ el('span', { className: 'sap-adviceObjLine' },
3204
+ el('span', { className: 'sap-adviceObj' }, adviceSummaryObjectText(profile, a.profileKey)),
3205
+ profile === null ? el('span', { className: 'sap-adviceNoProfileTag' }, ZH.adviceNoProfileTag) : null
3206
+ ),
3207
+ el('span', { className: 'sap-adviceSummaryLine' },
3208
+ el('span', { className: 'sap-adviceSummaryText' }, adviceSuggestionShort(a)),
3209
+ el('span', { className: 'sap-adviceConf' }, ZH.adviceConfidence + confidenceZhOf(a.confidence)),
3210
+ el('span', { className: 'sap-adviceSummaryArrow' }, '▸')))
3211
+ const detail = open !== true ? null : el('div', { className: 'sap-adviceDetail' },
3212
+ buildAdviceStatusLine(el, assetView),
3213
+ typeof a.reasonText === 'string' && a.reasonText !== '' ? adviceSectionOf(el, ZH.adviceSectionReason, el('p', { className: 'sap-adviceSectionBody' }, a.reasonText)) : null,
3214
+ metrics.length > 0 ? adviceSectionOf(el, ZH.adviceSectionPerf, el('p', { className: 'sap-adviceSectionBody' }, metrics.join(' · '))) : null,
3215
+ adviceSectionOf(el, ZH.adviceSectionDetail, buildAdviceDetails(el, s, actions, a.profileKey, rows, sessions)),
3216
+ candidates.length > 0 ? buildAdviceCandidates(el, s, actions, a, candidates) : null,
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)
3222
+ return el('li', { key: String(a.profileKey), className: 'sap-adviceItem' + (open === true ? ' sap-adviceItemOpen' : '') }, summary, detail)
3223
+ }
3224
+
3225
+ // 近期派发明细折叠行:▸ 近期派发明细 · N 次(点击展开)→ 六列表格;默认收起。
3226
+ function buildAdviceDetails(el, s, actions, profileKey, rows, sessions) {
3227
+ const map = s.detailOpen !== null && typeof s.detailOpen === 'object' ? s.detailOpen : {}
3228
+ const open = map[profileKey] === true
3229
+ const header = el('tr', null,
3230
+ el('th', null, ZH.adviceDetailColTime), el('th', null, ZH.adviceDetailColSession), el('th', null, ZH.adviceDetailColChild),
3231
+ el('th', null, ZH.adviceDetailColOutcome), el('th', null, ZH.adviceDetailColElapsed), el('th', null, ZH.adviceDetailColMode))
3232
+ const cell = (row) => {
3233
+ const outcomeZh = ZH.dispatchOutcomeZh[row.outcome] || row.outcome || ''
3234
+ const bad = row.outcome === 'failed' || row.outcome === 'killed'
3235
+ const title = reminderSessionTitle(sessions, row.sessionId)
3236
+ return el('tr', { key: String(row.ts ?? '') + String(row.childId ?? '') },
3237
+ el('td', { className: 'sap-adviceDetailTime' }, formatMonthDayTime(row.ts)),
3238
+ el('td', null, title !== '' ? el('span', { className: 'sap-adviceDetailSession' }, title) : null,
3239
+ sessionIdShortOf(row.sessionId) !== '' ? el('span', { className: 'sap-adviceDetailSessionId' }, sessionIdShortOf(row.sessionId)) : null),
3240
+ el('td', { className: 'sap-adviceDetailChild' }, childIdShortOf(row.childId)),
3241
+ el('td', { className: bad ? 'sap-warn-x' : '' }, outcomeZh),
3242
+ el('td', null, formatDurationMs(row.elapsedMs)),
3243
+ el('td', null, dispatchModeZh(row.mode)))
3244
+ }
3245
+ return el('div', { className: 'sap-adviceDetailBlock' },
3246
+ el('button', { type: 'button', className: 'sap-adviceDetailToggle', 'aria-expanded': open, onClick: actions.toggleDetail(profileKey) },
3247
+ el('span', null, ZH.adviceDetailTogglePrefix + rows.length + ZH.adviceDetailToggleSuffix),
3248
+ el('span', { className: 'sap-adviceDetailArrow' }, '▸')),
3249
+ open !== true ? null : (rows.length === 0
3250
+ ? el('p', { className: 'sap-candidateHint' }, ZH.adviceDetailEmpty)
3251
+ : el('table', { className: 'sap-adviceDetailTable' }, el('thead', null, header), el('tbody', null, rows.map(cell))))
3252
+ )
3253
+ }
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
+
3422
+ // 候选管理设置(顶部一行 + 就地浮层):更新模式与有效期用户可配;
3423
+ // 自定义按天数输入(1~365),提交时换算小时数。
3424
+ function candidateModeTextOf(mode) {
3425
+ return mode === 'manual' ? ZH.candidateModeManual : ZH.candidateModeAuto
3426
+ }
3427
+
3428
+ function candidateTtlTextOf(ttlH) {
3429
+ if (ttlH === 0) return ZH.candidateTtl0
3430
+ if (ttlH === 24) return ZH.candidateTtl24
3431
+ if (ttlH === 72) return ZH.candidateTtl72
3432
+ if (ttlH === 168) return ZH.candidateTtl168
3433
+ return ttlH + ' 小时'
3434
+ }
3435
+
3436
+ function candidateTtlChoiceOf(ttlH) {
3437
+ return ttlH === 0 || ttlH === 24 || ttlH === 72 || ttlH === 168 ? String(ttlH) : 'custom'
3438
+ }
3439
+
3440
+ function buildCandidateSettingsRow(el, s, actions) {
3441
+ const settings = s.candidateSettings !== null && typeof s.candidateSettings === 'object' ? s.candidateSettings : { candidateMode: 'auto', candidateTtlH: 24 }
3442
+ return el('p', { className: 'sap-candidateSettingsRow' },
3443
+ el('b', null, ZH.candidateSettingsRow),
3444
+ el('span', null, candidateModeTextOf(settings.candidateMode) + ' · ' + ZH.candidateTtlLabel + ' ' + candidateTtlTextOf(settings.candidateTtlH)),
3445
+ el('button', { type: 'button', className: 'sap-candidateSettingsEdit', onClick: actions.toggleCandidateSettings }, ZH.candidateSettingsEdit))
3446
+ }
3447
+
3448
+ function candidateSettingsRadio(el, label, value, current, onChange) {
3449
+ return el('label', { key: value, className: 'sap-candidateSettingsOption' },
3450
+ el('input', { type: 'radio', checked: current === value, onChange }),
3451
+ el('span', null, label))
3452
+ }
3453
+
3454
+ function buildCandidateSettingsPanel(el, s, actions) {
3455
+ const form = s.candidateSettingsForm !== null && typeof s.candidateSettingsForm === 'object' ? s.candidateSettingsForm : {}
3456
+ const mode = form.candidateMode === 'manual' ? 'manual' : 'auto'
3457
+ const ttlChoice = typeof form.candidateTtlChoice === 'string' ? form.candidateTtlChoice : '24'
3458
+ const ttlOptions = [['24', ZH.candidateTtl24], ['72', ZH.candidateTtl72], ['168', ZH.candidateTtl168], ['0', ZH.candidateTtl0], ['custom', ZH.candidateTtlCustom]]
3459
+ return el('div', { className: 'sap-candidateSettingsPanel' },
3460
+ el('div', { className: 'sap-candidateSettingsField' },
3461
+ el('span', { className: 'sap-fieldLabel' }, ZH.candidateSettingsModeLabel),
3462
+ el('div', { className: 'sap-candidateSettingsOptions' },
3463
+ candidateSettingsRadio(el, ZH.candidateModeAuto + '(推荐)', 'auto', mode, actions.setCandidateFormMode('auto')),
3464
+ candidateSettingsRadio(el, ZH.candidateModeManual, 'manual', mode, actions.setCandidateFormMode('manual'))),
3465
+ el('p', { className: 'sap-metaValueDefault' }, mode === 'manual' ? ZH.candidateSettingsModeManualText : ZH.candidateSettingsModeAutoText)),
3466
+ el('div', { className: 'sap-candidateSettingsField' },
3467
+ el('span', { className: 'sap-fieldLabel' }, ZH.candidateTtlLabel),
3468
+ el('div', { className: 'sap-candidateSettingsOptions' },
3469
+ ttlOptions.map(([value, label]) => candidateSettingsRadio(el, label, value, ttlChoice, actions.setCandidateFormTtl(value)))),
3470
+ ttlChoice === 'custom'
3471
+ ? el('div', { className: 'sap-candidateSettingsCustom' },
3472
+ el('input', { className: 'sap-input', value: typeof form.customDays === 'string' ? form.customDays : '', placeholder: '1~365', onChange: (event) => actions.setCandidateFormCustomDays(event && event.target ? event.target.value : '') }),
3473
+ el('span', { className: 'sap-metaValueDefault' }, ZH.candidateSettingsCustomLabel))
3474
+ : null,
3475
+ form.error !== '' ? el('p', { className: 'sap-notice' }, form.error) : null),
3476
+ el('div', { className: 'sap-candidateSettingsActions' },
3477
+ el('button', { type: 'button', className: 'sap-primaryButton', onClick: actions.saveCandidateSettings }, ZH.candidateSettingsSave),
3478
+ el('button', { type: 'button', className: 'sap-secondaryButton', onClick: actions.cancelCandidateSettings }, ZH.candidateSettingsCancel)))
3479
+ }
3480
+
3481
+ // 候选方案区(v10 小节):纯文字加粗小节标题「候选方案(未应用)」+ 上分隔线 →
3482
+ // 数量/单选说明行(手动维护附重新生成)→ 候选项列表(行点击只展开,radio 只选中)。
3483
+ function buildAdviceCandidates(el, s, actions, a, candidates) {
3484
+ const manual = s.candidateSettings !== null && typeof s.candidateSettings === 'object' && s.candidateSettings.candidateMode === 'manual'
3485
+ const selected = candidateSelectedIdOf(s, a.profileKey, candidates)
3486
+ return el('div', { className: 'sap-candidates' },
3487
+ el('p', { className: 'sap-candidatesTitle' }, ZH.candidateTitle, el('span', { className: 'sap-candidatesNotApplied' }, ZH.candidateNotApplied)),
3488
+ el('p', { className: 'sap-candidatesCount' }, candidates.length + ZH.candidateCountSuffix + ZH.candidateCountNote),
3489
+ manual === true
3490
+ ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: actions.regenerateCandidates(a.profileKey) }, ZH.candidateRegenerate)
3491
+ : null,
3492
+ candidates.map((candidate, index) => buildAdviceCandidateRow(el, s, actions, a, candidate, index, selected))
3493
+ )
3494
+ }
3495
+
3496
+ // 默认选中第一条候选;用户选择存进状态(仅选中,行点击只展开)。
3497
+ function candidateSelectedIdOf(s, profileKey, candidates) {
3498
+ const map = s.candidateSelected !== null && typeof s.candidateSelected === 'object' ? s.candidateSelected : {}
3499
+ const current = typeof map[profileKey] === 'string' ? map[profileKey] : ''
3500
+ if (current !== '' && candidates.some((c) => c && c.id === current)) return current
3501
+ return candidates.length > 0 ? candidates[0].id : ''
3502
+ }
3503
+
3504
+ function buildAdviceCandidateRow(el, s, actions, a, candidate, index, selected) {
3505
+ const cfg = candidate.config && typeof candidate.config === 'object' ? candidate.config : {}
3506
+ const openState = s.candidateOpen !== null && typeof s.candidateOpen === 'object' ? s.candidateOpen : {}
3507
+ const safetyState = s.candidateSafetyOpen !== null && typeof s.candidateSafetyOpen === 'object' ? s.candidateSafetyOpen : {}
3508
+ const open = openState[candidate.id] === true
3509
+ const safetyOpen = safetyState[candidate.id] === true
3510
+ const preview = s.draftPreviews && typeof s.draftPreviews === 'object' ? s.draftPreviews[candidate.id] : undefined
3511
+ const changes = candidateChangesOf(a.profileKey, cfg)
3512
+ const onRadio = (event) => {
3513
+ if (event && typeof event.stopPropagation === 'function') event.stopPropagation()
3514
+ actions.selectCandidate(a.profileKey, candidate.id)()
3515
+ }
3516
+ const asset = candidateAssetByIdOf(s, candidate.id)
3517
+ const stateLabel = asset !== null && typeof asset.stateLabel === 'string' && asset.stateLabel !== '' ? asset.stateLabel : null
3518
+ return el('div', { key: String(candidate.id), className: 'sap-candidate' + (selected === candidate.id ? ' sap-candidateSelected' : '') },
3519
+ el('button', { type: 'button', className: 'sap-candidateRow', 'aria-expanded': open, onClick: actions.toggleCandidateOpen(candidate) },
3520
+ el('span', { role: 'radio', 'aria-checked': selected === candidate.id, className: 'sap-candidateRadio', onClick: onRadio }),
3521
+ el('span', { className: 'sap-candidateName' }, '候选 ' + (index + 1) + ' · ' + candidate.name),
3522
+ stateLabel !== null ? el('span', { className: 'sap-candidateState' }, stateLabel) : null,
3523
+ el('span', { className: 'sap-candidateValidity' }, candidateValidityText(candidate.expiry)),
3524
+ el('span', { className: 'sap-candidateArrow' }, '▸')),
3525
+ open === true ? el('div', { className: 'sap-candidateBody' },
3526
+ changes.length > 0 ? el('div', { className: 'sap-candidateGroup' },
3527
+ el('p', { className: 'sap-candidateGroupTitle' }, ZH.candidateGroupChanges),
3528
+ el('ul', { className: 'sap-candidateChanges' },
3529
+ changes.map((change) => el('li', { key: change, className: 'sap-candidateChange' }, change)))) : null,
3530
+ preview !== undefined
3531
+ ? el('div', { className: 'sap-candidateGroup' },
3532
+ el('button', { type: 'button', className: 'sap-candidateSafetyToggle' + (safetyOpen === true ? ' sap-candidateSafetyToggleOpen' : ''), onClick: actions.toggleCandidateSafety(candidate) },
3533
+ ZH.candidateSafetyPrefix + (preview.ok === true ? ZH.candidateSafetyPass : ZH.candidateSafetyFail),
3534
+ el('span', { className: 'sap-candidateSafetyArrow' }, '▸')),
3535
+ safetyOpen === true ? buildCandidateSafetyChecks(el, preview) : null)
3536
+ : el('div', { className: 'sap-candidateGroup' },
3537
+ el('p', { className: 'sap-candidateHint' }, ZH.candidateSafetyPending)),
3538
+ el('div', { className: 'sap-candidateGroup' },
3539
+ el('p', { className: 'sap-candidateGroupTitle' }, ZH.candidateGroupGenerated),
3540
+ candidateGeneratedLine(el, candidate),
3541
+ el('p', { className: 'sap-candidateHint' }, ZH.candidateNotApplicable))) : null)
3542
+ }
3543
+
3544
+ // 生成信息行(最浅灰,两分支模板):有样本 = 生成于 MM-DD · 基于最近 N 次派发 ·
3545
+ // 近期表现 X(满分 100,X=round(表现分×100));无样本 = 生成于 MM-DD · 暂无近期派发
3546
+ // 样本,仅供参考(不显示 0 分);无生成时间省去「生成于」段。
3547
+ function candidateGeneratedLine(el, candidate) {
3548
+ const basis = candidate.basis !== null && typeof candidate.basis === 'object' ? candidate.basis : {}
3549
+ const n = Number.isFinite(basis.n) ? basis.n : 0
3550
+ const score100 = Number.isFinite(basis.score) ? Math.round(basis.score * 100) : 0
3551
+ const generated = formatMonthDayTime(basis.generatedAt)
3552
+ const parts = []
3553
+ if (generated !== '') parts.push(ZH.candidateGeneratedPrefix + generated)
3554
+ if (n >= 1) {
3555
+ parts.push(ZH.candidateSamplesPrefix + n + ZH.candidateSamplesSuffix)
3556
+ parts.push(ZH.candidatePerfPrefix + score100 + ZH.candidatePerfSuffix)
3557
+ } else {
3558
+ parts.push(ZH.candidateNoSample)
3559
+ }
3560
+ return el('p', { className: 'sap-candidateHint' }, parts.join(' · '))
3561
+ }
3562
+
3563
+ // 卡底部:无对应方案 = [保存为方案](统一主按钮)+ 备注;有对应方案 = [按建议调整
3564
+ // 方案] + 定位。按钮启用条件 = applyEnabled && 已选候选;禁用 title = 禁用原因
3565
+ // (冷启动 = reasons.join(';') 并附卡脚一行灰字;观察中 = 暂不支持再次调整)。
3566
+ function buildAdviceFoot(el, s, actions, a, profile, candidates, assetView) {
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 : {}
3568
+ const generatedAt = formatMonthDayTime(basis.generatedAt)
3569
+ const generatedText = generatedAt !== '' ? ZH.candidateGeneratedPrefix + generatedAt : ''
3570
+ const noteLines = (lines) => el('div', { className: 'sap-adviceFootNotes' }, lines.map((line) => el('p', { key: line, className: 'sap-adviceFootNote' }, line)))
3571
+ if (profile === null) {
3572
+ return el('div', { className: 'sap-adviceFoot' },
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)
3589
+ return el('div', { className: 'sap-adviceFoot' },
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))
3593
+ }
3594
+
2539
3595
  // 逃生舱卡片:开关 + (打开时)显式警告文案 + 放行集增删(输入框 + 添加按钮,
2540
3596
  // 每项「移除」按钮)。增删走 /escape-add /escape-remove,成功后用响应 presets 刷新。
2541
3597
  function buildEscapeSection(el, s, actions) {
2542
3598
  const list = Array.isArray(s.escapePresets) ? s.escapePresets : []
2543
- return el('div', { className: 'sap-rowCard' },
2544
- el('div', { className: 'sap-rowHead' },
2545
- el('span', { className: 'sap-rowIdentity' },
2546
- el('span', { className: 'sap-rowName' }, ZH.escapeTitle)
3599
+ return el('div', { className: 'sap-card sap-cardStatic' },
3600
+ el('div', { className: 'sap-cardHead' },
3601
+ el('div', { className: 'sap-cardTitleBox' },
3602
+ el('div', { className: 'sap-cardTitle' }, ZH.escapeTitle),
3603
+ el('div', { className: 'sap-cardDesc' }, ZH.escapeCardDesc)
2547
3604
  ),
2548
3605
  el('span', { className: 'sap-rowActions' },
2549
3606
  el('label', { className: 'sap-switch', title: '逃生舱开关' },
@@ -2552,26 +3609,28 @@ function buildEscapeSection(el, s, actions) {
2552
3609
  )
2553
3610
  )
2554
3611
  ),
2555
- s.escapeEnabled
2556
- ? el('p', { className: 'sap-notice', role: 'alert' }, ZH.escapeWarn)
2557
- : null,
2558
- el('div', { className: 'sap-escapeAdd' },
2559
- el('input', {
2560
- className: 'sap-input',
2561
- style: { flex: '1 1 0', minWidth: '160px' },
2562
- value: s.escapeInput,
2563
- placeholder: ZH.escapeInputPlaceholder,
2564
- onChange: (event) => s.setEscapeInput(event && event.target ? event.target.value : '')
2565
- }),
2566
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.escapeAdd }, ZH.escapeAdd)
2567
- ),
2568
- el('div', { className: 'sap-chips' },
2569
- list.length === 0
2570
- ? el('span', { className: 'sap-chipValueDim' }, ZH.escapeEmpty)
2571
- : list.map((id) => el('span', { key: String(id), className: 'sap-chip sap-chipWarn' },
2572
- String(id),
2573
- el('button', { type: 'button', className: 'sap-chipRemove', title: ZH.escapeRemove, onClick: actions.escapeRemove(id) }, ZH.escapeRemove)
2574
- ))
3612
+ el('div', { className: 'sap-cardBody' },
3613
+ s.escapeEnabled
3614
+ ? el('p', { className: 'sap-notice', role: 'alert' }, ZH.escapeWarn)
3615
+ : null,
3616
+ el('div', { className: 'sap-escapeAdd' },
3617
+ el('input', {
3618
+ className: 'sap-input',
3619
+ style: { flex: '1 1 0', minWidth: '160px' },
3620
+ value: s.escapeInput,
3621
+ placeholder: ZH.escapeInputPlaceholder,
3622
+ onChange: (event) => s.setEscapeInput(event && event.target ? event.target.value : '')
3623
+ }),
3624
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.escapeAdd }, ZH.escapeAdd)
3625
+ ),
3626
+ el('div', { className: 'sap-chips' },
3627
+ list.length === 0
3628
+ ? el('span', { className: 'sap-chipValueDim' }, ZH.escapeEmpty)
3629
+ : list.map((id) => el('span', { key: String(id), className: 'sap-chip sap-chipWarn' },
3630
+ String(id),
3631
+ el('button', { type: 'button', className: 'sap-chipRemove', title: ZH.escapeRemove, onClick: actions.escapeRemove(id) }, ZH.escapeRemove)
3632
+ ))
3633
+ )
2575
3634
  )
2576
3635
  )
2577
3636
  }
@@ -2596,8 +3655,8 @@ function buildSettingsCard(el, key, title, desc, open, toggle, body, isStatic) {
2596
3655
  )
2597
3656
  }
2598
3657
 
2599
- // 常驻状态条:health + 审计丢失(health 唯一位置;丢失 >0 红字)。目录/告警计数
2600
- // 不再重复(版本告警正文在高级区,catalog 计数随 [2] 区一并删除——一个数字只出现一次)。
3658
+ // 审计健康状态行:health + 审计丢失(health 唯一位置;丢失 >0 红字)。目录/告警
3659
+ // 计数不重复(版本告警正文在维护页签,一个数字只出现一次);现挂在安全页签审计健康卡。
2601
3660
  function buildStatusBar(el, s) {
2602
3661
  const degraded = s.audit.health === 'degraded'
2603
3662
  const lost = s.audit.lostTelemetry + s.audit.lostGovernance
@@ -2617,13 +3676,14 @@ function buildSectionNotices(el, s) {
2617
3676
  }
2618
3677
 
2619
3678
  // draft 安全检查预览结果:ok 全绿/存在拒绝,逐项展示 verdict + reason。
3679
+ // 检查项名走 CHECK_ZH 人话映射(可见面零内部术语),title 保留原串供排查。
2620
3680
  function buildDraftPreview(el, preview) {
2621
3681
  const checks = Array.isArray(preview.checks) ? preview.checks : []
2622
3682
  return el('div', { className: 'sap-draftPreview' },
2623
3683
  el('p', { className: preview.ok ? 'sap-savedNotice' : 'sap-warn', role: 'status' }, preview.ok ? ZH.draftPreviewPass : ZH.draftPreviewFail),
2624
3684
  el('ul', { className: 'sap-rows' },
2625
3685
  checks.map((c) => el('li', { key: String(c.name), className: 'sap-rowCard' },
2626
- el('div', { className: 'sap-rowHead' }, el('span', { className: 'sap-rowName' }, (c.verdict === 'pass' ? '✅ ' : '❌ ') + c.name)),
3686
+ el('div', { className: 'sap-rowHead' }, el('span', { className: 'sap-rowName', title: c.name }, (c.verdict === 'pass' ? '✅ ' : '❌ ') + (CHECK_ZH[c.name] || c.name))),
2627
3687
  c.reason ? el('p', { className: 'sap-desc' }, c.reason) : null
2628
3688
  ))
2629
3689
  )
@@ -2722,41 +3782,76 @@ function buildCostObservabilitySection(el, s, actions) {
2722
3782
  )
2723
3783
  }
2724
3784
 
2725
- // 高级区(默认折叠):卡内按四个子组展开,子组间用 12px 深灰加粗小标题 + 20px
2726
- // 组间距区分,不嵌套折叠;组内正文 12.5px 中灰(标题强于内容)。总开关在标题行,
2727
- // 方案表单在方案管理卡,其余开关全在此区。
2728
- function buildAdvancedSubgroup(el, title, hint, children) {
2729
- return el('div', { className: 'sap-subgroup' },
2730
- el('div', { className: 'sap-subgroupTitle' }, title),
2731
- hint !== '' ? el('p', { className: 'sap-subgroupHint' }, hint) : null,
2732
- ...children
3785
+ // 六页签骨架:横向 tab(下划线激活 + 加粗,样式对齐官方「插件」节)+ 内容区。
3786
+ function buildTabBar(el, s) {
3787
+ return el('div', { className: 'sap-tabs', role: 'tablist' },
3788
+ SETTINGS_TABS.map((tab) => el('button', {
3789
+ key: tab.key, type: 'button', role: 'tab', 'aria-selected': s.activeTab === tab.key,
3790
+ className: 'sap-tab' + (s.activeTab === tab.key ? ' sap-tabActive' : ''),
3791
+ onClick: () => s.setActiveTab(tab.key)
3792
+ }, tab.label))
2733
3793
  )
2734
3794
  }
2735
3795
 
2736
- function buildAdvancedSection(el, s, actions, reminderCenter) {
2737
- const versionText = s.pluginVersion !== '' ? '当前版本 v' + s.pluginVersion + ' · 未发现兼容性问题' : '未发现兼容性问题'
2738
- const versionBlock = s.versionWarnings.length > 0
2739
- ? el('div', { className: 'sap-versionWarn' }, s.versionWarnings.map((warning, index) => el('span', { key: index }, warning)))
2740
- : el('p', { className: 'sap-desc' }, versionText)
3796
+ // 页签内容区:全部挂载、非激活隐藏(组件状态不因切换丢失)。
3797
+ function buildTabZone(el, s, key, children) {
3798
+ return el('div', { role: 'tabpanel', className: 'sap-zone' + (s.activeTab === key ? ' sap-zoneActive' : '') }, children)
3799
+ }
3800
+
3801
+ // 安全页签:三卡一律 sap-card(与①方案管理同款)——安全保证、逃生舱、
3802
+ // 审计健康(状态行 + 只读审计明细折叠表,明细表类 = sap-audit*)。
3803
+ function buildSafetySection(el, s, actions, reminders) {
2741
3804
  return el('div', null,
2742
- buildAdvancedSubgroup(el, '安全', '放行非官方预设的高风险通道,默认关闭', [
3805
+ buildSettingsCard(el, 5, ZH.safetyCardTitle, ZH.safetyCardDesc, true, () => {}, [
2743
3806
  el('p', { className: 'sap-desc' }, ZH.advancedSafetyLine),
2744
- buildEscapeSection(el, s, actions)
2745
- ]),
2746
- buildAdvancedSubgroup(el, '建议注入', '根据历史派发数据向父 Agent 附优化提示(例:某方案近期成功率偏低,可考虑换方案)。只提示,不自动改配置', [
2747
- buildAdviceSection(el, s, actions),
2748
- buildAdvicePanel(el, s, actions)
2749
- ]),
2750
- buildAdvancedSubgroup(el, '通知', '需要你关注的事件汇总,所有动作均有审计记录', [
2751
- reminderCenter
2752
- ]),
2753
- buildAdvancedSubgroup(el, '维护', '插件版本与宿主兼容性状态', [
2754
- versionBlock
2755
- ])
3807
+ el('p', { className: 'sap-desc' }, ZH.safetyCheckNote)
3808
+ ], true),
3809
+ buildEscapeSection(el, s, actions),
3810
+ buildSettingsCard(el, 6, ZH.auditCardTitle, ZH.auditCardDesc, true, () => {}, [
3811
+ buildStatusBar(el, s),
3812
+ buildAuditDetails(el, s, actions, reminders)
3813
+ ], true)
2756
3814
  )
2757
3815
  }
2758
3816
 
2759
- function buildSectionReturn(el, s, actions, rows, addCard, openSections, toggleSection, reminderCenter) {
3817
+ // 审计明细事件与结果人话(与提醒同源;未知类别原样回退,信息不删)。
3818
+ function auditEventText(r) {
3819
+ const text = ZH.auditEventZh[r.kind] || r.kind
3820
+ return r.severity === 'P0' ? text + '(紧急)' : text
3821
+ }
3822
+
3823
+ function auditResultText(r) {
3824
+ if (typeof r.action === 'string' && r.action !== '') {
3825
+ const time = formatMonthDayTime(r.actedAt)
3826
+ return ZH.auditResultActedPrefix + (ZH.reminderActionZh[r.action] || r.action) + (time !== '' ? ' ' + time : '')
3827
+ }
3828
+ return r.unread === true ? ZH.auditResultPending : ZH.auditResultRead
3829
+ }
3830
+
3831
+ // 审计明细折叠表(只读展示,无导出/过滤):最近 20 条治理留痕。
3832
+ function buildAuditDetails(el, s, actions, reminders) {
3833
+ const rows = Array.isArray(reminders) ? reminders.slice(0, 20) : []
3834
+ const cell = (r) => el('tr', { key: String(r.id) },
3835
+ el('td', { className: 'sap-auditTime' }, formatMonthDayTime(r.createdAt)),
3836
+ el('td', { className: 'sap-auditEvent' }, auditEventText(r)),
3837
+ el('td', { className: 'sap-auditObject' }, r.title + ' · ' + (typeof r.sessionId === 'string' && r.sessionId !== '' ? '会话 ' + r.sessionId.slice(0, 8) + '…' : ZH.reminderGlobal)),
3838
+ el('td', null, auditResultText(r)))
3839
+ return el('div', null,
3840
+ el('button', { type: 'button', className: 'sap-auditCollapse', 'aria-expanded': s.auditDetailOpen === true, onClick: actions.toggleAuditDetail },
3841
+ el('span', null, ZH.auditDetailToggle),
3842
+ el('span', { className: 'sap-auditArrow' }, '▸')),
3843
+ s.auditDetailOpen !== true ? null : (rows.length === 0
3844
+ ? el('p', { className: 'sap-candidateHint' }, ZH.auditDetailEmpty)
3845
+ : [el('p', { className: 'sap-candidateHint' }, ZH.auditDetailScope),
3846
+ el('table', { className: 'sap-auditTable' },
3847
+ el('thead', null, el('tr', null,
3848
+ el('th', null, ZH.auditColTime), el('th', null, ZH.auditColEvent),
3849
+ el('th', null, ZH.auditColObject), el('th', null, ZH.auditColResult))),
3850
+ el('tbody', null, rows.map(cell)))])
3851
+ )
3852
+ }
3853
+
3854
+ function buildSectionReturn(el, s, actions, rows, addCard, reminderCenter, sessions) {
2760
3855
  const notices = buildSectionNotices(el, s)
2761
3856
  return el('div', { className: 'sap-section' },
2762
3857
  el('div', { className: 'sap-titleRow' },
@@ -2766,27 +3861,29 @@ function buildSectionReturn(el, s, actions, rows, addCard, openSections, toggleS
2766
3861
  el('span', null, s.enabled ? '已启用' : '已禁用')
2767
3862
  )
2768
3863
  ),
2769
- buildStatusBar(el, s),
2770
3864
  ...notices,
2771
- buildSettingsCard(el, 1, '方案管理', '管理派发可用的方案:启用、编辑、删除,含成本与成功率', openSections[1] === true, toggleSection, [
2772
- el('p', { className: 'sap-intro' }, ZH.profilesIntro),
2773
- el('ul', { className: 'sap-rows' }, rows),
2774
- buildDraftRows(el, s, actions),
2775
- el('div', { className: 'sap-addBlock' },
2776
- s.adding ? addCard : el('div', { className: 'sap-addActions' },
2777
- el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
3865
+ buildTabBar(el, s),
3866
+ buildTabZone(el, s, 'profiles',
3867
+ buildSettingsCard(el, 1, '方案管理', '管理派发可用的方案:启用、编辑、删除,含成本与成功率', true, () => {}, [
3868
+ el('p', { className: 'sap-intro' }, ZH.profilesIntro),
3869
+ el('ul', { className: 'sap-rows' }, rows),
3870
+ buildDraftRows(el, s, actions),
3871
+ el('div', { className: 'sap-addBlock' },
3872
+ s.adding ? addCard : el('div', { className: 'sap-addActions' },
3873
+ el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
3874
+ )
3875
+ ),
3876
+ el('div', { className: 'sap-addActions', style: { marginTop: '4px' } },
3877
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.resetAll }, ZH.resetAllBuiltins)
2778
3878
  )
2779
- ),
2780
- el('div', { className: 'sap-addActions', style: { marginTop: '4px' } },
2781
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.resetAll }, ZH.resetAllBuiltins)
2782
- )
2783
- ], false),
2784
- buildSettingsCard(el, 2, '全局成本', '跨全部会话的派发成本汇总,按模型与方案拆分', true, toggleSection, [
2785
- buildCostObservabilitySection(el, s, actions)
2786
- ], true),
2787
- buildSettingsCard(el, 3, '高级', '逃生舱、建议注入、通知中心与维护项', openSections[3] === true, toggleSection, [
2788
- buildAdvancedSection(el, s, actions, reminderCenter)
2789
- ], false)
3879
+ ], true)),
3880
+ buildTabZone(el, s, 'cost',
3881
+ buildSettingsCard(el, 2, '全局成本', '跨全部会话的派发成本汇总,按模型与方案拆分', true, () => {}, [
3882
+ buildCostObservabilitySection(el, s, actions)
3883
+ ], true)),
3884
+ buildTabZone(el, s, 'advice', buildAdvicePanel(el, s, actions, sessions)),
3885
+ buildTabZone(el, s, 'safety', buildSafetySection(el, s, actions, s.auditReminders)),
3886
+ buildTabZone(el, s, 'reminders', el(reminderCenter, { jumpToAdvice: () => s.setActiveTab('advice') }))
2790
3887
  )
2791
3888
  }
2792
3889
 
@@ -2809,7 +3906,7 @@ function attachDraftActions(api, s, refresh, applyWrite, actions) {
2809
3906
  applyWrite(data, '已应用 draft:' + draft.name)
2810
3907
  // 闭环可视化:应用成功后展开方案管理区并高亮新方案卡片。
2811
3908
  if (typeof data.id === 'string' && data.id !== '') {
2812
- s.setOpenSections((prev) => ({ ...prev, 1: true }))
3909
+ s.setActiveTab('profiles')
2813
3910
  s.setHighlightId(data.id)
2814
3911
  setTimeout(() => s.setHighlightId(null), 8000)
2815
3912
  }
@@ -2825,41 +3922,44 @@ function attachDraftActions(api, s, refresh, applyWrite, actions) {
2825
3922
  }
2826
3923
  }
2827
3924
 
2828
- function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter) {
3925
+ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter, getSessions) {
2829
3926
  function ProfilesSection() {
2830
3927
  const [profiles, setProfiles] = useState([]); const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] }); const [enabled, setEnabled] = useState(true)
2831
- const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [error, setError] = useState('')
2832
- const [savedNotice, setSavedNotice] = useState(''); const [persistWarning, setPersistWarning] = useState(''); const [form, setForm] = useState(EMPTY_FORM)
2833
- const [tools, setTools] = useState([]); const [versionWarnings, setVersionWarnings] = useState([]); const [pluginVersion, setPluginVersion] = useState(''); const [audit, setAudit] = useState(AUDIT_DEFAULTS)
2834
- const [escapeEnabled, setEscapeEnabled] = useState(false); const [escapePresets, setEscapePresets] = useState([]); const [escapeInput, setEscapeInput] = useState('')
2835
- const [evolutionAdvice, setEvolutionAdvice] = useState(false); const [drafts, setDrafts] = useState([]); const [advice, setAdvice] = useState([])
2836
- const [adviceGlobal, setAdviceGlobal] = useState(null); const [costSummary, setCostSummary] = useState({ global: null, byModel: [] }); const [draftPreviews, setDraftPreviews] = useState({})
2837
- const [openSections, setOpenSections] = useState({ 1: true })
2838
- const [highlightId, setHighlightId] = useState(null)
2839
- const toggleSection = (key) => setOpenSections((prev) => ({ ...prev, [key]: !(prev[key] === true) }))
2840
- const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, pluginVersion, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, drafts, advice, adviceGlobal, costSummary, draftPreviews, openSections, highlightId, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setPluginVersion, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice, setDrafts, setAdvice, setAdviceGlobal, setCostSummary, setDraftPreviews, setOpenSections, setHighlightId }
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('')
3931
+ const [evolutionAdvice, setEvolutionAdvice] = useState(false); const [drafts, setDrafts] = useState([]); const [advice, setAdvice] = useState([]); const [evolutionCandidates, setEvolutionCandidates] = useState([])
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)
3934
+ const [candidateSettingsForm, setCandidateSettingsForm] = useState({ candidateMode: 'auto', candidateTtlChoice: '24', customDays: '', error: '' }); const [adviceDetails, setAdviceDetails] = useState({}); const [detailOpen, setDetailOpen] = useState({})
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 }
2841
3939
  const refresh = useCallback(() => {
2842
3940
  api('/list')
2843
3941
  .then((data) => {
2844
3942
  setProfiles(Array.isArray(data.profiles) ? data.profiles : [])
2845
3943
  setAdvice(Array.isArray(data.advice) ? data.advice : [])
3944
+ setEvolutionCandidates(Array.isArray(data.evolutionCandidates) ? data.evolutionCandidates : [])
2846
3945
  setAdviceGlobal(data.adviceGlobal !== null && typeof data.adviceGlobal === 'object' ? data.adviceGlobal : null)
3946
+ setAdviceDetails(data.adviceDetails !== null && typeof data.adviceDetails === 'object' ? data.adviceDetails : {})
2847
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 : [] })
2848
3950
  })
2849
3951
  .catch((err) => setError(String((err && err.message) || err)))
2850
- api('/drafts')
2851
- .then((data) => setDrafts(Array.isArray(data.drafts) ? data.drafts : []))
2852
- .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(() => {})
3954
+ api('/reminders').then((data) => setAuditReminders(Array.isArray(data.reminders) ? data.reminders : [])).catch(() => {})
2853
3955
  }, [])
2854
3956
  const loadOptions = useCallback(() => {
2855
3957
  api('/options/summary')
2856
- .then((data) => applySummaryData(data, { enabled: setEnabled, options: setOptions, audit: setAudit, escapeEnabled: setEscapeEnabled, escapePresets: setEscapePresets, evolutionAdvice: setEvolutionAdvice }))
3958
+ .then((data) => applySummaryData(data, { enabled: setEnabled, options: setOptions, audit: setAudit, escapeEnabled: setEscapeEnabled, escapePresets: setEscapePresets, evolutionAdvice: setEvolutionAdvice, candidateSettings: setCandidateSettings }))
2857
3959
  .catch(() => {})
2858
3960
  }, [])
2859
- const loadEfforts = makeEffortsLoader(api, setOptions)
2860
- const loadTools = makeToolsLoader(api, setTools)
2861
- // 版本探测只挂载时拉一次:useCallback([]) 稳定引用,防 effect 依赖恒变导致设置页打开期间无限重取。
2862
- const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings, setPluginVersion)(), [])
3961
+ const loadEfforts = makeEffortsLoader(api, setOptions); const loadTools = makeToolsLoader(api, setTools)
3962
+ const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings, setPluginVersion)(), []) // 只挂载时拉一次:稳定引用防 effect 依赖恒变
2863
3963
  useEffect(() => {
2864
3964
  refresh()
2865
3965
  loadOptions()
@@ -2867,9 +3967,8 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, Re
2867
3967
  }, [refresh, loadOptions, loadVersions])
2868
3968
  const applyWrite = makeApplyWrite(s); const actions = makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools)
2869
3969
  attachDraftActions(api, s, refresh, applyWrite, actions)
2870
- const rows = buildProfileRows(el, chip, s, actions)
2871
- const toolSection = buildToolSection(el, s, actions)
2872
- return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection), openSections, toggleSection, el(ReminderCenter))
3970
+ const rows = buildProfileRows(el, chip, s, actions); const toolSection = buildToolSection(el, s, actions)
3971
+ return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection), ReminderCenter, sessionsSnapshot(getSessions))
2873
3972
  }
2874
3973
  return ProfilesSection
2875
3974
  }
@@ -3183,7 +4282,7 @@ function buildJsonBody(el, kv) {
3183
4282
 
3184
4283
  // ── 三层信息架构(结论层 / 证据层)纯渲染 ─────────────────────────
3185
4284
 
3186
- // 停滞检测阈值(待实测:measured-params 回填惯例,展示层同步标注「待实测」)。
4285
+ // 停滞检测上限(待实测:measured-params 回填惯例,展示层同步标注「待实测」)。
3187
4286
  const STALL_THRESHOLD_MS = 30000
3188
4287
 
3189
4288
  // 折叠态状态 chip:拒绝(红)/执行中(琥珀脉冲)/异常(琥珀)/成功(绿)。
@@ -4036,7 +5135,7 @@ window.__ModuleLoader__.load({
4036
5135
  // 与派发决策台账 openChild 同一 sessions 服务来源。
4037
5136
  const openChild = (address) => ctx.sessions.openSubagent(address)
4038
5137
  const ReminderCenter = makeReminderCenter(el, useState, useEffect, openChild, () => ctx.sessions)
4039
- const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter)
5138
+ const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter, () => ctx.sessions)
4040
5139
  ensureStyles()
4041
5140
  const SubagentHeaderUtility = makeSubagentHeaderUtility(el, useState, useEffect, () => ctx.get('modelDirectories'), () => ctx.sessions)
4042
5141
  ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: 'dispatch' }, DispatchToolview))
@@ -4067,4 +5166,4 @@ window.__ModuleLoader__.load({
4067
5166
 
4068
5167
  return { apply, inject }
4069
5168
  },
4070
- })
5169
+ })