dsh-subagent-profile 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -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,67 @@ 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-candidateSettingsRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}',
137
+ '.sap-candidateSettingsRow b{font-weight:600;color:var(--dsw-alias-label-primary)}',
138
+ '.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}',
139
+ '.sap-candidateSettingsEdit:hover{color:var(--dsw-alias-label-primary)}',
140
+ '.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)}',
141
+ '.sap-candidateSettingsField{flex-direction:column;gap:6px;display:flex}',
142
+ '.sap-candidateSettingsOption{align-items:center;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);display:inline-flex;cursor:pointer}',
143
+ '.sap-candidateSettingsOptions{flex-wrap:wrap;gap:4px 14px;display:flex}',
144
+ '.sap-candidateSettingsCustom{align-items:center;gap:8px;display:flex}',
145
+ '.sap-candidateSettingsCustom .sap-input{max-width:120px}',
146
+ '.sap-candidateSettingsActions{align-items:center;gap:10px;display:flex}',
91
147
  '.sap-desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
92
148
  '.sap-warn{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}',
93
149
  '.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 +177,10 @@ const CSS = [
121
177
  '.sap-customized[open]>.sap-customizedSummary:before{transform:rotate(45deg)translate(-1px,-1px)}',
122
178
  '.sap-customizedSummary:hover{color:var(--dsw-alias-label-primary)}',
123
179
  '.sap-customizedBody{flex-direction:column;gap:12px;padding-top:12px;display:flex}',
180
+ '.sap-customizedArrowEnd .sap-customizedSummary:before{display:none}',
181
+ '.sap-customizedEndArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
182
+ '.sap-customizedArrowEnd[open] .sap-customizedEndArrow{transform:rotate(90deg)}',
183
+ '.sap-reminderHandled{border-top:none;padding-top:8px}',
124
184
  '.sap-editorActions{justify-content:flex-end;gap:8px;display:flex}',
125
185
  '.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
186
  '.sap-primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}',
@@ -284,6 +344,37 @@ const CSS = [
284
344
  '.sap-escapeAdd{align-items:center;gap:8px;display:flex}',
285
345
  '.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
346
  '.sap-chipRemove:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}',
347
+ // 六页签:横向 tab(下划线激活+加粗)+ 内容区(非激活隐藏,全部挂载保状态)
348
+ '.sap-tabs{display:flex;flex-wrap:wrap;gap:2px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}',
349
+ '.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}',
350
+ '.sap-tab:hover{color:var(--dsw-alias-label-primary)}',
351
+ '.sap-tabActive{color:var(--dsw-alias-label-primary);font-weight:700;border-bottom-color:var(--dsw-alias-label-primary)}',
352
+ '.sap-zone{display:none}',
353
+ '.sap-zoneActive{display:block}',
354
+ // 建议摘要行 + 展开区(v8:卡片边界 .advice-item,无二级标题)
355
+ '.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}',
356
+ '.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}',
357
+ '.sap-adviceObjLine{align-items:center;gap:6px;flex-wrap:wrap;min-width:0;display:flex}',
358
+ '.sap-adviceSummaryLine{align-items:center;gap:8px;flex-wrap:wrap;min-width:0;display:flex}',
359
+ '.sap-adviceSummary:hover{background:var(--dsw-alias-interactive-bg-hover)}',
360
+ '.sap-adviceObj{color:var(--dsw-alias-label-primary);font-weight:600;font-size:13px;line-height:20px}',
361
+ '.sap-adviceSummaryText{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}',
362
+ '.sap-adviceConf{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:nowrap}',
363
+ '.sap-adviceSummaryArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
364
+ '.sap-adviceItemOpen .sap-adviceSummaryArrow{transform:rotate(90deg)}',
365
+ '.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}',
366
+ '.sap-adviceDetail{border-top:1px dashed var(--dsw-alias-border-l2);padding:10px 14px 12px;flex-direction:column;gap:8px;display:flex}',
367
+ // 审计明细折叠表(只读展示)
368
+ '.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}',
369
+ '.sap-auditCollapse:hover{color:var(--dsw-alias-label-primary)}',
370
+ '.sap-auditArrow{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1;transition:transform .15s}',
371
+ '.sap-auditCollapse[aria-expanded="true"] .sap-auditArrow{transform:rotate(90deg)}',
372
+ '.sap-auditTable{width:100%;border-collapse:collapse;font-size:12px;line-height:18px;margin-top:4px}',
373
+ '.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)}',
374
+ '.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}',
375
+ '.sap-auditTime{color:var(--dsw-alias-label-primary);font-weight:500;white-space:nowrap}',
376
+ '.sap-auditEvent{color:var(--dsw-alias-label-secondary);white-space:nowrap}',
377
+ '.sap-auditObject{color:var(--dsw-alias-label-primary)}',
287
378
  ].join('\n')
288
379
 
289
380
  // ── 纯数据:dispatch 卡片面向用户固定文案 ──────────────────────────────────
@@ -299,7 +390,7 @@ const ZH = {
299
390
  chipProvider: '提供方',
300
391
  chipModel: '模型',
301
392
  chipEffort: '推理强度',
302
- chipTier: '档位',
393
+ chipTier: '档位(估算口径)',
303
394
  chipEnabled: '已启用',
304
395
  chipDisabled: '已禁用',
305
396
  ignoredMark: '已忽略',
@@ -327,6 +418,7 @@ const ZH = {
327
418
  phaseLegacy: '已结算(旧版)',
328
419
  // 提醒系统
329
420
  reminderCenterTitle: '提醒中心',
421
+ reminderCenterDesc: '需要你关注的事件汇总,所有动作均有审计记录;处理过的条目折叠保存',
330
422
  reminderEmpty: '暂无提醒(治理事件会在这里留痕)',
331
423
  reminderAck: '标记已读',
332
424
  reminderAdopt: '采纳',
@@ -375,7 +467,7 @@ const ZH = {
375
467
  ledgerGateRejectDist: '安全检查拒绝分布',
376
468
  ledgerByProfile: '按方案',
377
469
  ledgerByModel: '按模型',
378
- ledgerByTier: '按档位',
470
+ ledgerByTier: '按档位(估算口径)',
379
471
  ledgerEmpty: '本会话暂无派发记录',
380
472
  ledgerEmptyHint: '在此会话中派发一次子 Agent 后,即可查看完整的决策轨迹。',
381
473
  ledgerLoadOlder: '加载更早',
@@ -484,6 +576,7 @@ const ZH = {
484
576
  ignoredChipTitleEmpty: '请求值已忽略',
485
577
  // 逃生舱(放行非官方预设)
486
578
  escapeTitle: '逃生舱(放行非官方预设)',
579
+ escapeCardDesc: '放行非官方预设的高风险通道,默认关闭',
487
580
  escapeWarn: '逃生舱已开启:允许派发非官方预设。风险自负——其余安全检查仍全量生效,每个放行都会留审计记录。',
488
581
  escapeEmpty: '暂无放行预设',
489
582
  escapeAdd: '添加',
@@ -492,6 +585,11 @@ const ZH = {
492
585
  escapeAddEmpty: '逃生舱预设 id 不能为空',
493
586
  // 派发优化建议注入(开关 + 只提示面板)
494
587
  adviceTitle: '派发优化建议',
588
+ advicePanelDesc: '根据历史派发数据向父会话附优化提示(例:某方案近期成功率偏低,可考虑换方案)。只提示,不自动改配置',
589
+ adviceEnableLabel: '开启建议',
590
+ adviceSectionReason: '理由',
591
+ adviceSectionPerf: '近期表现',
592
+ adviceSectionDetail: '派发明细',
495
593
  adviceSwitchOn: '开(只提示,不自动改配置)',
496
594
  adviceSwitchOff: '关(只提示,不自动改配置)',
497
595
  advicePanelEmpty: '暂无建议(需累计 ≥3 次派发且加权成功分低于阈值)',
@@ -500,15 +598,23 @@ const ZH = {
500
598
  adviceDirUp: '可上调预算',
501
599
  adviceDirDown: '建议降级',
502
600
  adviceDirFlat: '维持现状',
503
- adviceConfidence: '置信度',
504
- adviceRowLocate: '定位方案',
505
- adviceRowNoMatch: '无匹配方案',
601
+ adviceConfidence: '置信度:',
602
+ confidenceZh: { low: '', medium: '中等', high: '高' },
603
+ adviceRowLocate: '定位到方案管理',
604
+ // 建议行/理由行(建议直接带对象;理由灰字小字)
605
+ adviceSuggestionPrefix: '建议:',
606
+ adviceReasonPrefix: '理由:',
607
+ adviceDirShortDown: '降级',
608
+ adviceDirShortUp: '升预算',
609
+ // 建议对象标题(摘要行对象与底部备注共用)
610
+ adviceTitleNoProfile: '手动配置的派发',
611
+ adviceTitleProfilePrefix: '方案:',
506
612
  costEditProfile: '定位方案',
507
613
  costNoProfile: '无对应方案',
508
614
  advancedSafetyLine: '安全保证:派发不豁免审批 · 子代理工具不会超出父会话 · 成本与深度有硬上限。',
509
615
  resetAllBuiltins: '重置所有内置方案',
510
616
  statusLost: '审计丢失',
511
- profilesIntro: '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可在「高级」区重置回默认;每个方案可单独启用/禁用。',
617
+ profilesIntro: '在此管理「派发子 Agent」工具可用的 profile。内置方案可编辑、可删除、可在此重置回默认;每个方案可单独启用/禁用。',
512
618
  draftSavedWithId: '已保存到 draft,可在设置页「方案管理」中应用;方案 id:{id}',
513
619
  draftCopyId: '复制 id',
514
620
  ledgerCopyTarget: '复制',
@@ -520,14 +626,124 @@ const ZH = {
520
626
  costInheritRatio: '继承占比',
521
627
  costByModel: '按模型',
522
628
  draftPreview: '安全检查预览',
523
- draftPreviewPass: '安全检查预览:全部通过 ✓',
524
- draftPreviewFail: '安全检查预览:存在拒绝项'
629
+ draftPreviewPass: '安全检查:全部通过 ✓',
630
+ draftPreviewFail: '安全检查:存在拒绝项',
631
+ // 建议面板候选方案区(三层:标题行 → 说明句 → 候选项列表;有效期徽标在候选行右侧;
632
+ // 应用确认流随后续版本开放,不可应用提示收进展开区尾部)。
633
+ candidateTitle: '候选方案',
634
+ candidateNotApplied: '(未应用)',
635
+ candidateCountSuffix: ' 个 · 单选',
636
+ candidateCountNote: ' · 仅参考,不会自动应用',
637
+ candidateIntro: '系统基于近期表现生成的改进方案,仅参考、不自动应用。',
638
+ candidateRegenerate: '重新生成候选',
639
+ candidateSafetyPending: '安全检查:正在核对…',
640
+ candidateGroupChanges: '改动',
641
+ candidateGroupGenerated: '生成信息',
642
+ candidateSafetyPrefix: '安全检查:',
643
+ candidateSafetyPass: '全部通过 ✓',
644
+ candidateSafetyFail: '存在拒绝项',
645
+ candidateNotApplicable: '应用功能在后续版本开放(届时会先经你确认)',
646
+ candidateValidityForever: '⏱ 无期限',
647
+ candidateValidityPrefix: '⏱ 至 ',
648
+ candidateGeneratedPrefix: '生成于 ',
649
+ candidateSamplesPrefix: '基于最近 ',
650
+ candidateSamplesSuffix: ' 次派发',
651
+ candidatePerfPrefix: '近期表现 ',
652
+ candidatePerfSuffix: '(满分 100)',
653
+ candidateNoSample: '暂无近期派发样本,仅供参考',
654
+ // 候选卡底部:无对应方案存来源配置为新方案;有对应方案调整按钮灰置 + 定位。
655
+ adviceSaveProfile: '保存为方案',
656
+ adviceApplyDisabled: '按建议调整方案',
657
+ adviceSavedNotice: '已保存为新方案:{id}',
658
+ adviceFootNoteSave: '仅保存预设、模型与提供方;自定义人格与工具过滤无法还原(隐私保护不落盘),如需完整配置请手动编辑方案。',
659
+ adviceFootNoteMatchedPrefix: '对应方案:',
660
+ // 建议摘要行(v8:对象/建议/置信度/加权成功分;展开后见理由与明细)
661
+ adviceNoProfileTag: '无方案',
662
+ // 安全页签:安全保证卡 + 审计健康卡(只读明细表)
663
+ safetyCardTitle: '安全保证',
664
+ safetyCardDesc: '派发与审计的安全保证,明细只读',
665
+ safetyCheckNote: '派发前安全检查:白名单 / 成本 / 工具面 / 审批不豁免——拒绝分布见下方审计明细。',
666
+ auditCardTitle: '审计健康',
667
+ auditCardDesc: '治理审计状态与留痕,异常红字提示',
668
+ auditDetailToggle: '查看审计明细(点击展开)',
669
+ auditDetailEmpty: '暂无审计明细(治理事件会在这里留痕)',
670
+ auditDetailScope: '只读展示,暂不提供导出与过滤',
671
+ auditColTime: '时间',
672
+ auditColEvent: '事件',
673
+ auditColObject: '对象',
674
+ auditColResult: '结果',
675
+ auditEventZh: { 'adoption-false': '派发未采纳', 'escape-allow': '逃生舱放行', 'audit-degraded': '审计降级' },
676
+ auditResultPending: '未处理',
677
+ auditResultRead: '已读',
678
+ auditResultActedPrefix: '已处理:',
679
+ // 近期派发明细折叠表
680
+ adviceDetailTogglePrefix: '近期派发明细 · ',
681
+ adviceDetailToggleSuffix: ' 次(点击展开)',
682
+ adviceDetailEmpty: '近期暂无明细',
683
+ adviceDetailColTime: '时间',
684
+ adviceDetailColSession: '主会话(发起者)',
685
+ adviceDetailColChild: '子 Agent',
686
+ adviceDetailColOutcome: '结果',
687
+ adviceDetailColElapsed: '耗时',
688
+ adviceDetailColMode: '模式',
689
+ dispatchOutcomeZh: { completed: '完成', failed: '失败', killed: '终止' },
690
+ // 候选管理设置(顶部设置行 + 就地浮层)
691
+ candidateSettingsRow: '候选时效:',
692
+ candidateSettingsEdit: '修改 ⚙',
693
+ candidateSettingsSaved: '候选设置已保存',
694
+ candidateModeAuto: '自动换新',
695
+ candidateModeManual: '手动维护',
696
+ candidateTtlLabel: '有效期',
697
+ candidateTtl24: '24 小时',
698
+ candidateTtl72: '3 天',
699
+ candidateTtl168: '7 天',
700
+ candidateTtl0: '不自动过期',
701
+ candidateTtlCustom: '自定义',
702
+ candidateSettingsModeLabel: '更新模式',
703
+ candidateSettingsModeAutoText: '自动换新:数据显著变化时换代',
704
+ candidateSettingsModeManualText: '手动维护:不自动换代,候选保留,可点「重新生成候选」刷新',
705
+ candidateSettingsCustomLabel: '自定义天数(1~365)',
706
+ candidateSettingsCustomInvalid: '自定义有效期无效:请输入 1~365 的整数天数',
707
+ candidateSettingsSave: '保存',
708
+ candidateSettingsCancel: '取消',
709
+ // 编辑表单档位选择器口径说明(档位是估算口径,不决定模型)
710
+ tierFormHint: '档位仅影响估算与方案排序,不改变派发模型;显式选择模型才决定实际模型'
711
+ }
712
+
713
+ // profileKey 内部表示 → 人话(仅展示层;工具提示/日志保留原串):preset:X→预设:X、
714
+ // persona:1→含自定义人格、toolFilter:1→含工具过滤,段组合以 · 连接;
715
+ // (inline) 即未存方案;空键兜底未指定方案特征。
716
+ function profileKeyZh(key) {
717
+ const text = typeof key === 'string' ? key : ''
718
+ const parts = text.split('|').filter((part) => part !== '').map((part) => {
719
+ if (part === '(inline)') return '未存方案'
720
+ const idx = part.indexOf(':')
721
+ const head = idx >= 0 ? part.slice(0, idx) : part
722
+ const value = idx >= 0 ? part.slice(idx + 1) : ''
723
+ if (head === 'preset') return '预设:' + value
724
+ if (head === 'provider') return '提供方:' + value
725
+ if (head === 'model') return '模型:' + value
726
+ if (head === 'persona') return '含自定义人格'
727
+ if (head === 'toolFilter') return '含工具过滤'
728
+ if (head === 'effort') return '推理强度:' + value
729
+ return part
730
+ })
731
+ const zh = parts.join(' · ')
732
+ return zh === '' ? '未指定方案特征' : zh
525
733
  }
526
734
 
527
735
  // ── 纯数据:设置页常量 ─────────────────────────────────────────────────────
528
736
 
529
- const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', toolMode: 'none', toolList: [] }
737
+ const EMPTY_FORM = { id: '', description: '', preset: '', provider: '', model: '', reasoningEffort: '', tokenTier: 'balanced', toolMode: 'none', toolList: [] }
530
738
  const EFFORT_ZH = { off: '关闭', high: '高', max: '最大' }
739
+ // 设置页五页签顺序(v9:维护并入方案管理尾部折叠卡;方案管理第一位并默认激活)。
740
+ const SETTINGS_TABS = [
741
+ { key: 'profiles', label: '方案管理' },
742
+ { key: 'cost', label: '成本与统计' },
743
+ { key: 'advice', label: '自进化与建议' },
744
+ { key: 'safety', label: '安全' },
745
+ { key: 'reminders', label: '通知与提醒' }
746
+ ]
531
747
  const TOOL_MODES = [
532
748
  ['none', '不限制(继承父会话的全部工具,自动剔除 run_code)'],
533
749
  ['allow', '白名单(只允许勾选的工具)'],
@@ -545,12 +761,14 @@ const GATE_FIX_HINTS = {
545
761
  approval: '审批策略冲突,请核对审批配置',
546
762
  budget: '并发派发数或本会话累计 token 已达上限,请等待在途任务完成或开启新会话'
547
763
  }
764
+ // 安全检查项 → 人话映射(可见面零内部术语;title 保留原串供排查;未知项原样回退)。
765
+ const CHECK_ZH = { sanitize: '配置格式检查', id: '方案编号不冲突', whitelist: '工具范围安全检查', catalog: '模型目录有效' }
548
766
  // 派发决策台账:决策轨迹字段键名 → 中文标签(字段级渲染去「JSON 墙」用)。
549
767
  // 派发决策台账:决策轨迹字段键名 → 中文标签(FIELD_ZH,字段级渲染去「JSON 墙」用)。
550
768
  // 未知键回退原样(fail-soft 不崩溃);同义键(执行/结算/派发摘要字段)一并收录。
551
769
  const FIELD_ZH = {
552
770
  parentPreset: '父预设', parentProvider: '父提供方', parentModel: '父模型', parentToolCount: '父工具数',
553
- tokenTier: '档位', persona_present: '是否设置人格', toolFilter_present: '是否设置工具过滤', envelope: '信封模式',
771
+ tokenTier: '档位(估算口径)', persona_present: '是否设置人格', toolFilter_present: '是否设置工具过滤', envelope: '信封模式',
554
772
  prompt_excerpt: '任务摘要', prompt_truncated: '任务已截断',
555
773
  systemTrustCandidates: '候选白名单', resolvedPreset: '解析后预设', allowed: '放行',
556
774
  field: '检查项', checkedAgainst: '对照', verdict: '结论', cap: '上限',
@@ -562,7 +780,7 @@ const FIELD_ZH = {
562
780
  requestedPreset: '目标预设', checks: '检查项', detail: '详情',
563
781
  effectiveAllowCount: '生效工具数', jobId: '任务ID', reason: '原因', limit: '上限',
564
782
  skipped: '跳过', allowFailOpen: '兼容模式', maxTokens: 'token 上限', maxDepth: '深度上限',
565
- requestedToolFilter: '请求工具过滤', providers: '提供方', modelEfforts: '模型支持档位',
783
+ requestedToolFilter: '请求工具过滤', providers: '提供方', modelEfforts: '模型支持的推理强度',
566
784
  effectiveToolNames: '生效工具', provider: '提供方', model: '模型',
567
785
  reasoningEffort: '推理强度',
568
786
  unknown_keys: '未知字段(已被拒绝)',
@@ -586,7 +804,8 @@ function readAuditFromData(data) {
586
804
  }
587
805
 
588
806
  // /options/summary 响应 → 设置页状态(enabled/options/audit/逃生舱开关与放行集)。
589
- // set 为各 setter 的注入对象;escapePresets 非数组回退空数组,escapeEnabled 仅 true 才开。
807
+ // set 为各 setter 的注入对象;escapePresets 非数组回退空数组,escapeEnabled 仅 true 才开;
808
+ // 候选设置(更新模式/有效期)缺失时回退默认。
590
809
  function applySummaryData(data, set) {
591
810
  set.enabled(data.enabled !== false)
592
811
  set.options({
@@ -598,6 +817,10 @@ function applySummaryData(data, set) {
598
817
  set.escapeEnabled(data.escapeEnabled === true)
599
818
  set.escapePresets(Array.isArray(data.escapePresets) ? data.escapePresets : [])
600
819
  set.evolutionAdvice(data.evolutionAdvice === true)
820
+ set.candidateSettings({
821
+ candidateMode: data.candidateMode === 'manual' ? 'manual' : 'auto',
822
+ candidateTtlH: Number.isFinite(data.candidateTtlH) ? data.candidateTtlH : 24
823
+ })
601
824
  }
602
825
 
603
826
  // ── 纯函数:api / 样式注入 / 文案映射 ──────────────────────────────────────
@@ -1183,7 +1406,7 @@ function buildSubagentBanner(el, subagent, parentPreset, modelState, bannerMode)
1183
1406
 
1184
1407
  // ── 提醒系统 ─────────────────────────────────────────────────────
1185
1408
 
1186
- // 通知中心(设置页高级区内):读 /reminders(与 [6] 审计同源 host store),全动作
1409
+ // 通知中心(设置页通知与提醒页签):读 /reminders(与审计同源 host store),全动作
1187
1410
  // (标记已读/采纳/忽略/拒绝/关闭)经 POST 回写 host 并刷新——一条提醒 = 一条审计记录。
1188
1411
  // sessions 服务供条目主/子会话标题查询与跳转;跳转失败静默(设置页不被导航异常打断)。
1189
1412
  function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
@@ -1204,13 +1427,37 @@ function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1204
1427
  return ReminderCenter
1205
1428
  }
1206
1429
 
1430
+ // 提醒中心分层:未终态条目(action 为空)进默认列表;已终态条目折叠为「已处理(N)」
1431
+ // 可展开区(原生 details,展开后仍按现有行渲染并保留已处理标注,可追溯)。审计留痕
1432
+ // 由 host 侧动作回写,已处理条目不再堆在默认列表里。全空才显示空态文案;仅剩已处理时
1433
+ // 只显示折叠区,不显示空态。
1207
1434
  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)))
1435
+ const handled = reminders.filter((r) => typeof r.action === 'string' && r.action !== '')
1436
+ const pending = reminders.filter((r) => !(typeof r.action === 'string' && r.action !== ''))
1437
+ const pendingList = pending.length > 0
1438
+ ? el('ul', { className: 'sap-rows' }, pending.map((r) => buildReminderRow(el, r, act, sessions, openChild)))
1439
+ : null
1440
+ const handledList = handled.length > 0
1441
+ ? el('details', { className: 'sap-customized sap-customizedArrowEnd sap-reminderHandled' },
1442
+ el('summary', { className: 'sap-customizedSummary' }, '已处理(' + handled.length + ')', el('span', { className: 'sap-customizedEndArrow' }, '▸')),
1443
+ el('div', { className: 'sap-customizedBody' },
1444
+ el('ul', { className: 'sap-rows' }, handled.map((r) => buildReminderRow(el, r, act, sessions, openChild)))
1445
+ )
1446
+ )
1447
+ : null
1448
+ return el('div', { className: 'sap-card sap-cardStatic' },
1449
+ el('div', { className: 'sap-cardHead' },
1450
+ el('div', { className: 'sap-cardTitleBox' },
1451
+ el('div', { className: 'sap-cardTitle' }, ZH.reminderCenterTitle),
1452
+ el('div', { className: 'sap-cardDesc' }, ZH.reminderCenterDesc)
1453
+ )
1454
+ ),
1455
+ el('div', { className: 'sap-cardBody' },
1456
+ error !== '' ? el('p', { className: 'sap-notice' }, error) : null,
1457
+ pendingList === null && handledList === null
1458
+ ? el('p', { className: 'sap-desc' }, ZH.reminderEmpty)
1459
+ : [pendingList, handledList]
1460
+ )
1214
1461
  )
1215
1462
  }
1216
1463
 
@@ -1295,20 +1542,19 @@ function buildUnadoptedFields(el, r, sessions, openChild) {
1295
1542
  )
1296
1543
  }
1297
1544
 
1298
- // 未采纳条目按钮四件套:查看子会话(跳转)/ 采纳(审计回写,未采纳即可用)/
1299
- // 标记已读(未读显示)/ 关闭。已采纳后不再重复展示采纳。
1545
+ // 未采纳条目按钮三件套:查看子会话(跳转,恒保留)/ 标记已读(未读显示)/ 关闭。
1546
+ // 已终态条目隐藏标记已读与关闭(只留只读跳转),防对终态条目重复处置产生重复审计;
1547
+ // 提醒上的处置只改变提醒自身状态(已读/终态),不改变采纳判定——采纳与否由事件流自动汇总。
1300
1548
  function buildUnadoptedActions(el, r, act, childAddress, jumpChild) {
1549
+ const terminated = typeof r.action === 'string' && r.action !== ''
1301
1550
  return el('div', { className: 'sap-reminderActions' }, [
1302
1551
  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)
1552
+ terminated ? null : r.unread === true ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1553
+ terminated ? null : el('button', { type: 'button', className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1308
1554
  ])
1309
1555
  }
1310
1556
 
1311
- // 未采纳提醒条目(完整版):标题 + 五字段 + 按钮四件套 + 已处理标注。
1557
+ // 未采纳提醒条目(完整版):标题 + 五字段 + 按钮三件套 + 已处理标注。
1312
1558
  function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1313
1559
  const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1314
1560
  const severityText = r.severity === 'P0' ? '紧急' : '注意'
@@ -1329,6 +1575,9 @@ function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1329
1575
  )
1330
1576
  }
1331
1577
 
1578
+ // 通知类提醒行:P0 未读只保留「标记已读」(常驻至已读,不可跳过未读);已读后出现
1579
+ // 「关闭」(与 P1/P2 同);P1/P2 恒保留「标记已读 / 关闭」。已终态条目隐藏动作按钮,
1580
+ // 只显示已处理灰字标注(折叠区可追溯、防对终态条目误点产生重复审计)。
1332
1581
  function buildReminderRow(el, r, act, sessions, openChild) {
1333
1582
  if (r.kind === 'adoption-false') return buildUnadoptedReminderRow(el, r, act, sessions, openChild)
1334
1583
  const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
@@ -1341,10 +1590,14 @@ function buildReminderRow(el, r, act, sessions, openChild) {
1341
1590
  ),
1342
1591
  el('div', { className: 'sap-reminderMeta', title: session.title }, session.text),
1343
1592
  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
- ]),
1593
+ typeof r.action === 'string' && r.action !== ''
1594
+ ? null
1595
+ : el('div', { className: 'sap-reminderActions' }, [
1596
+ r.unread === true ? el('button', { className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1597
+ r.severity !== 'P0' || r.unread !== true
1598
+ ? el('button', { className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1599
+ : null
1600
+ ]),
1348
1601
  typeof r.action === 'string' && r.action !== ''
1349
1602
  ? el('p', { className: 'sap-metaValueDefault' }, ZH.reminderActed.replace('{action}', ZH.reminderActionZh[r.action] || r.action))
1350
1603
  : null
@@ -2107,6 +2360,7 @@ function makeEditActions(s, loadEfforts, loadTools) {
2107
2360
  provider: typeof profile.provider === 'string' ? profile.provider : '',
2108
2361
  model: typeof profile.model === 'string' ? profile.model : '',
2109
2362
  reasoningEffort: typeof profile.reasoningEffort === 'string' ? profile.reasoningEffort : '',
2363
+ tokenTier: typeof profile.tokenTier === 'string' && (profile.tokenTier === 'cheap' || profile.tokenTier === 'balanced' || profile.tokenTier === 'premium') ? profile.tokenTier : 'balanced',
2110
2364
  toolMode,
2111
2365
  toolList: toolMode === 'allow' ? allowArr : denyArr
2112
2366
  })
@@ -2134,6 +2388,7 @@ function makeSubmitActions(api, s, refresh, applyWrite) {
2134
2388
  add('provider', s.form.provider)
2135
2389
  add('model', s.form.model)
2136
2390
  add('reasoningEffort', s.form.reasoningEffort)
2391
+ if (typeof s.form.tokenTier === 'string' && s.form.tokenTier !== '') payload.tokenTier = s.form.tokenTier
2137
2392
  const list = Array.isArray(s.form.toolList) ? s.form.toolList : []
2138
2393
  if (s.form.toolMode === 'allow' && list.length > 0) payload.toolFilter = { allow: list }
2139
2394
  else if (s.form.toolMode === 'deny' && list.length > 0) payload.toolFilter = { deny: list }
@@ -2173,7 +2428,18 @@ function makeToggle(api, path, payloadKey, get, set, setError) {
2173
2428
  function makeRowActions(api, s, refresh, applyWrite) {
2174
2429
  const toggleEnabled = makeToggle(api, '/set-enabled', 'enabled', () => s.enabled, s.setEnabled, s.setError)
2175
2430
  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)
2431
+ // 建议开关切换后立即刷新 /list:候选随建议链同生共死(开→生成可见,关→不再下发)。
2432
+ const toggleAdvice = () => {
2433
+ const next = !s.evolutionAdvice
2434
+ s.setEvolutionAdvice(next)
2435
+ s.setError('')
2436
+ api('/set-evolution-advice', { advice: next })
2437
+ .then(() => refresh())
2438
+ .catch((err) => {
2439
+ s.setEvolutionAdvice(!next)
2440
+ s.setError(String((err && err.message) || err))
2441
+ })
2442
+ }
2177
2443
  const setProfileEnabled = (profile, next) => () => {
2178
2444
  s.setError('')
2179
2445
  api('/set-profile-enabled', { id: profile.id, enabled: next })
@@ -2247,7 +2513,7 @@ function makeToolsLoader(api, setTools) {
2247
2513
  }
2248
2514
  }
2249
2515
 
2250
- // 版本探测 → 拉 /options/versions;warnings 非空时维护区展示告警,否则显示插件版本。
2516
+ // 版本探测 → 拉 /options/versions;维护区 0.5.0 再统一设计归属,数据暂留。
2251
2517
  function makeVersionsLoader(api, setVersionWarnings, setPluginVersion) {
2252
2518
  return () => {
2253
2519
  api('/options/versions')
@@ -2259,10 +2525,19 @@ function makeVersionsLoader(api, setVersionWarnings, setPluginVersion) {
2259
2525
  }
2260
2526
  }
2261
2527
 
2528
+ // 会话服务快照(派发明细表查主会话标题用);拿不到回退 null(fail-soft)。
2529
+ function sessionsSnapshot(getSessions) {
2530
+ try {
2531
+ return typeof getSessions === 'function' ? getSessions() : null
2532
+ } catch {
2533
+ return null
2534
+ }
2535
+ }
2536
+
2262
2537
  // 定位方案:展开方案管理区 + 高亮对应卡片 + 滚动到可视区(建议/成本行的去向)。
2263
2538
  function makeLocateProfile(s) {
2264
2539
  return (profileId) => () => {
2265
- s.setOpenSections((prev) => ({ ...prev, 1: true }))
2540
+ s.setActiveTab('profiles')
2266
2541
  s.setHighlightId(String(profileId))
2267
2542
  setTimeout(() => {
2268
2543
  const node = document.getElementById('sap-profile-' + String(profileId))
@@ -2272,6 +2547,116 @@ function makeLocateProfile(s) {
2272
2547
  }
2273
2548
  }
2274
2549
 
2550
+ // 候选动作:行点击展开/收起(默认收起,展开态不持久化;展开时复用 /draft/preview
2551
+ // 只读安全检查流);radio 仅选中(不改展开态);安全检查在展开区内部默认收起;
2552
+ // 派发明细折叠行独立开关。
2553
+ function makeCandidateActions(api, s) {
2554
+ const previewCandidate = (candidate) => () => {
2555
+ s.setError('')
2556
+ api('/draft/preview', { config: candidate.config ?? {}, name: candidate.name ?? '', description: candidate.description ?? '', source: 'evolution' })
2557
+ .then((data) => {
2558
+ const previews = { ...(s.draftPreviews || {}) }
2559
+ previews[candidate.id] = { ok: data.ok === true, checks: Array.isArray(data.checks) ? data.checks : [] }
2560
+ s.setDraftPreviews(previews)
2561
+ })
2562
+ .catch((err) => s.setError(String((err && err.message) || err)))
2563
+ }
2564
+ const toggleCandidateOpen = (candidate) => () => {
2565
+ const openState = s.candidateOpen !== null && typeof s.candidateOpen === 'object' ? s.candidateOpen : {}
2566
+ if (openState[candidate.id] === true) {
2567
+ s.setCandidateOpen({ ...openState, [candidate.id]: false })
2568
+ } else {
2569
+ s.setCandidateOpen({ ...openState, [candidate.id]: true })
2570
+ previewCandidate(candidate)()
2571
+ }
2572
+ }
2573
+ const toggleCandidateSafety = (candidate) => () => {
2574
+ const openState = s.candidateSafetyOpen !== null && typeof s.candidateSafetyOpen === 'object' ? s.candidateSafetyOpen : {}
2575
+ s.setCandidateSafetyOpen({ ...openState, [candidate.id]: openState[candidate.id] !== true })
2576
+ }
2577
+ const selectCandidate = (profileKey, candidateId) => () => {
2578
+ const map = s.candidateSelected !== null && typeof s.candidateSelected === 'object' ? s.candidateSelected : {}
2579
+ s.setCandidateSelected({ ...map, [profileKey]: candidateId })
2580
+ }
2581
+ const toggleDetail = (profileKey) => () => {
2582
+ const openState = s.detailOpen !== null && typeof s.detailOpen === 'object' ? s.detailOpen : {}
2583
+ s.setDetailOpen({ ...openState, [profileKey]: openState[profileKey] !== true })
2584
+ }
2585
+ return { previewCandidate, toggleCandidateOpen, toggleCandidateSafety, selectCandidate, toggleDetail }
2586
+ }
2587
+
2588
+ // 表单 → 有效期小时数:自定义档按天数×24(1~365 整数),其余档原样;
2589
+ // 非法输入返回 null(调用方就地报错不提交)。
2590
+ function candidateTtlFromForm(form) {
2591
+ if (form.candidateTtlChoice === 'custom') {
2592
+ const days = Number(String(form.customDays ?? '').trim())
2593
+ if (!Number.isInteger(days) || days < 1 || days > 365) return null
2594
+ return days * 24
2595
+ }
2596
+ const ttlH = Number(form.candidateTtlChoice)
2597
+ if (!Number.isInteger(ttlH) || !(ttlH === 0 || (ttlH >= 1 && ttlH <= 8760))) return 24
2598
+ return ttlH
2599
+ }
2600
+
2601
+ // 页签内交互动作:建议摘要行展开(按 profileKey)与审计明细折叠。
2602
+ function makeTabActions(s) {
2603
+ return {
2604
+ toggleAdviceOpen: (profileKey) => () => {
2605
+ const map = s.adviceOpen !== null && typeof s.adviceOpen === 'object' ? s.adviceOpen : {}
2606
+ s.setAdviceOpen({ ...map, [profileKey]: map[profileKey] !== true })
2607
+ },
2608
+ toggleAuditDetail: () => s.setAuditDetailOpen(!(s.auditDetailOpen === true))
2609
+ }
2610
+ }
2611
+
2612
+ // 候选管理设置动作:设置浮层开合、表单字段、保存(POST /settings/candidate)与
2613
+ // 取消;重新生成候选(POST /evolution/regenerate)与保存为方案(POST /advice/save-profile)。
2614
+ function makeCandidateSettingsActions(api, s, refresh, applyWrite) {
2615
+ const formOf = () => (s.candidateSettingsForm !== null && typeof s.candidateSettingsForm === 'object' ? s.candidateSettingsForm : {})
2616
+ const openSettings = () => {
2617
+ const settings = s.candidateSettings !== null && typeof s.candidateSettings === 'object' ? s.candidateSettings : { candidateMode: 'auto', candidateTtlH: 24 }
2618
+ const choice = candidateTtlChoiceOf(settings.candidateTtlH)
2619
+ s.setCandidateSettingsOpen(true)
2620
+ s.setCandidateSettingsForm({ candidateMode: settings.candidateMode, candidateTtlChoice: choice, customDays: choice === 'custom' ? String(settings.candidateTtlH / 24) : '', error: '' })
2621
+ }
2622
+ const closeSettings = () => {
2623
+ s.setCandidateSettingsOpen(false)
2624
+ s.setCandidateSettingsForm((prev) => ({ ...(prev ?? {}), error: '' }))
2625
+ }
2626
+ const setForm = (patch) => s.setCandidateSettingsForm((prev) => ({ ...(prev ?? {}), ...patch, error: '' }))
2627
+ const save = () => {
2628
+ const form = formOf()
2629
+ const ttlH = candidateTtlFromForm(form)
2630
+ if (ttlH === null) {
2631
+ s.setCandidateSettingsForm((prev) => ({ ...(prev ?? {}), error: ZH.candidateSettingsCustomInvalid }))
2632
+ return
2633
+ }
2634
+ s.setError('')
2635
+ api('/settings/candidate', { candidateMode: form.candidateMode === 'manual' ? 'manual' : 'auto', candidateTtlH: ttlH })
2636
+ .then((data) => {
2637
+ s.setCandidateSettings({ candidateMode: data.candidateMode === 'manual' ? 'manual' : 'auto', candidateTtlH: Number.isFinite(data.candidateTtlH) ? data.candidateTtlH : 24 })
2638
+ s.setCandidateSettingsOpen(false)
2639
+ applyWrite(data, ZH.candidateSettingsSaved)
2640
+ refresh()
2641
+ })
2642
+ .catch((err) => s.setError(String((err && err.message) || err)))
2643
+ }
2644
+ const post = (path, body, onData) => {
2645
+ s.setError('')
2646
+ api(path, body).then(onData).catch((err) => s.setError(String((err && err.message) || err)))
2647
+ }
2648
+ return {
2649
+ toggleCandidateSettings: () => (s.candidateSettingsOpen === true ? closeSettings() : openSettings()),
2650
+ setCandidateFormMode: (mode) => () => setForm({ candidateMode: mode }),
2651
+ setCandidateFormTtl: (choice) => () => setForm({ candidateTtlChoice: choice }),
2652
+ setCandidateFormCustomDays: (value) => setForm({ customDays: value }),
2653
+ saveCandidateSettings: save,
2654
+ cancelCandidateSettings: closeSettings,
2655
+ regenerateCandidates: (profileKey) => () => post('/evolution/regenerate', { profileKey }, () => { s.setSavedNotice(ZH.candidateRegenerate + ' 完成'); refresh() }),
2656
+ saveAdviceProfile: (profileKey) => () => post('/advice/save-profile', { profileKey }, (data) => { applyWrite(data, ZH.adviceSavedNotice.replace('{id}', String(data.id ?? ''))); refresh() }),
2657
+ }
2658
+ }
2659
+
2275
2660
  function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools) {
2276
2661
  return {
2277
2662
  ...makeFormActions(s, loadEfforts, loadTools),
@@ -2279,6 +2664,9 @@ function makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools
2279
2664
  ...makeSubmitActions(api, s, refresh, applyWrite),
2280
2665
  ...makeRowActions(api, s, refresh, applyWrite),
2281
2666
  ...makeEscapeActions(api, s),
2667
+ ...makeCandidateActions(api, s),
2668
+ ...makeCandidateSettingsActions(api, s, refresh, applyWrite),
2669
+ ...makeTabActions(s),
2282
2670
  locateProfile: makeLocateProfile(s),
2283
2671
  }
2284
2672
  }
@@ -2417,6 +2805,17 @@ function buildProfileRows(el, chip, s, actions) {
2417
2805
  })
2418
2806
  }
2419
2807
 
2808
+ // 档位选择器(估算口径):仅影响估算与方案排序,不决定模型;下方附口径说明。
2809
+ function buildTierField(el, s, actions) {
2810
+ return el('div', { className: 'sap-field' },
2811
+ el('span', { className: 'sap-fieldLabel' }, ZH.chipTier),
2812
+ el('select', { className: 'sap-input sap-select', value: s.form.tokenTier, onChange: actions.setField('tokenTier') },
2813
+ Object.entries(ZH.tierZh).map(([id, label]) => el('option', { key: id, value: id }, label))
2814
+ ),
2815
+ el('p', { className: 'sap-metaValueDefault', style: { margin: '4px 0 0' } }, ZH.tierFormHint)
2816
+ )
2817
+ }
2818
+
2420
2819
  // 提供方只读展示:provider 由已选 model 联动派生;未选模型时回退表单存量
2421
2820
  // provider 值(编辑存量 profile 时照常展示),不可独立编辑。
2422
2821
  function buildProviderReadonlyLine(el, s) {
@@ -2449,8 +2848,8 @@ function buildAddCard(el, s, actions, toolSection) {
2449
2848
  buildSelectField(el, s, actions.setField, '模型(子 agent 使用的模型)', 'model', s.options.models,
2450
2849
  (m) => el('option', { key: m.id, value: m.id }, m.name && m.name !== m.id ? `${m.name}(${m.providerName || m.provider})` : m.id)),
2451
2850
  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' }, '高级选项'),
2851
+ el('details', { className: 'sap-customized sap-customizedArrowEnd' },
2852
+ el('summary', { className: 'sap-customizedSummary' }, '高级选项', el('span', { className: 'sap-customizedEndArrow' }, '▸')),
2454
2853
  el('div', { className: 'sap-customizedBody' },
2455
2854
  el('p', { className: 'sap-metaValueDefault', style: { margin: '0' } }, 'continuable 模式忽略 preset 换用与推理强度(子 Agent 继承父会话预设),派发结果以「ignored」标注'),
2456
2855
  buildSelectField(el, s, actions.setField, '目标预设(切换子 agent 使用的 Agent 预设)', 'preset', s.options.presets,
@@ -2465,6 +2864,7 @@ function buildAddCard(el, s, actions, toolSection) {
2465
2864
  )
2466
2865
  )
2467
2866
  : buildSelectField(el, s, actions.setField, '推理强度(思考深度)', 'reasoningEffort', effortsForModel, renderEffortOption),
2867
+ buildTierField(el, s, actions),
2468
2868
  toolSection
2469
2869
  )
2470
2870
  ),
@@ -2475,23 +2875,6 @@ function buildAddCard(el, s, actions, toolSection) {
2475
2875
  )
2476
2876
  }
2477
2877
 
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
2878
  // 按 profileKey 的 preset/model 段匹配方案管理区的 profile 卡片(建议行的去向)。
2496
2879
  function matchingProfileOf(profiles, profileKey) {
2497
2880
  if (!Array.isArray(profiles) || typeof profileKey !== 'string') return null
@@ -2508,42 +2891,395 @@ function matchingProfileOf(profiles, profileKey) {
2508
2891
  return null
2509
2892
  }
2510
2893
 
2511
- // 派发优化建议预览面板:每条建议可「定位方案」(高亮方案管理区对应卡片,数据有去向)。
2512
- function buildAdvicePanel(el, s, actions) {
2894
+ // ── 建议面板纯函数(epoch 格式化 / 文案换算 / 建议一句话)────────────────────
2895
+
2896
+ // epoch ms → MM-DD HH:mm(本地时区,与原型同口径);非有限数返回空串。
2897
+ function formatMonthDayTime(ts) {
2898
+ if (typeof ts !== 'number' || !Number.isFinite(ts)) return ''
2899
+ const d = new Date(ts)
2900
+ const p = (n) => String(n).padStart(2, '0')
2901
+ return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
2902
+ }
2903
+
2904
+ // 候选有效期徽标文案:null(不自动过期)= ⏱ 无期限;数字 = ⏱ 至 MM-DD HH:mm。
2905
+ function candidateValidityText(expiry) {
2906
+ if (typeof expiry !== 'number' || !Number.isFinite(expiry)) return ZH.candidateValidityForever
2907
+ return ZH.candidateValidityPrefix + formatMonthDayTime(expiry)
2908
+ }
2909
+
2910
+ // 指标行:把「平均耗时 <ms>ms」换算成短时(40.4s 等),其余指标原样(信息不删)。
2911
+ function adviceMetricsOf(performanceText) {
2912
+ return adviceEvidenceOf(performanceText).metrics.map((metric) => {
2913
+ const hit = /^平均耗时 (.+)$/.exec(metric)
2914
+ if (hit !== null) {
2915
+ const ms = Number(hit[1])
2916
+ return Number.isFinite(ms) ? '平均耗时 ' + formatDurationMs(ms) : metric
2917
+ }
2918
+ return metric
2919
+ })
2920
+ }
2921
+
2922
+ // 方向与置信度中文(模块级共用:面板建议行与旧格式回退)。
2923
+ function directionZh(d) {
2924
+ return d === '升' ? ZH.adviceDirUp : d === '降' ? ZH.adviceDirDown : ZH.adviceDirFlat
2925
+ }
2926
+
2927
+ function confidenceZhOf(level) {
2928
+ return ZH.confidenceZh[level] || level
2929
+ }
2930
+
2931
+ // 摘要行建议短句:只带对象与方向(置信度在摘要行独立成列,不再并入一句)。
2932
+ function adviceSuggestionShort(a) {
2933
+ const object = typeof a.objectText === 'string' && a.objectText !== '' ? a.objectText : ''
2934
+ if (object === '') return directionZh(a.suggestion)
2935
+ const word = a.suggestion === '降' ? ZH.adviceDirShortDown : a.suggestion === '升' ? ZH.adviceDirShortUp : ''
2936
+ return ZH.adviceSuggestionPrefix + object + (word !== '' ? '(' + word + ')' : '')
2937
+ }
2938
+
2939
+ // 摘要行对象文本:已有方案「方案:名称」;无方案「手动配置的派发(含自定义人格…)」
2940
+ // 并附「无方案」小标签(特征取 profileKeyZh,与候选/对应关系同一口径)。
2941
+ function adviceSummaryObjectText(profile, profileKey) {
2942
+ if (profile !== null) return adviceObjectTitleOf(profile)
2943
+ const features = profileKeyZh(profileKey).split(' · ').filter((part) => part !== '未存方案' && part !== '未指定方案特征').join(' · ')
2944
+ return adviceObjectTitleOf(null) + (features !== '' ? '(' + features + ')' : '')
2945
+ }
2946
+
2947
+ // 卡头标题 = 建议对象:已有方案用「方案:名称」,未匹配用「手动配置的派发」。
2948
+ function adviceObjectTitleOf(profile) {
2949
+ return profile !== null ? ZH.adviceTitleProfilePrefix + (profile.name || profile.id) : ZH.adviceTitleNoProfile
2950
+ }
2951
+
2952
+ // 会话/子 Agent 标识截断(悬停由原生 title 保留全文;不足长度原样)。
2953
+ function sessionIdShortOf(id) {
2954
+ const text = typeof id === 'string' ? id : ''
2955
+ return text.length > 12 ? text.slice(0, 12) + '…' : text
2956
+ }
2957
+
2958
+ function childIdShortOf(id) {
2959
+ const text = typeof id === 'string' ? id : ''
2960
+ return text.length > 8 ? text.slice(0, 8) + '…' : text
2961
+ }
2962
+
2963
+ // 派发模式中文(前台/后台/持久);未知值原样(fail-soft)。
2964
+ function dispatchModeZh(mode) {
2965
+ if (mode === 'background') return ZH.modeBackground
2966
+ if (mode === 'continuable') return ZH.bannerModePersistent
2967
+ if (mode === 'foreground') return ZH.modeForeground
2968
+ return mode
2969
+ }
2970
+
2971
+ // 表现摘要拆两层:计数(完成/失败/终止)与指标(加权成功分/平均耗时/平均输出),
2972
+ // 各渲染一遍;解析不出的部分原样保留(信息不删)。
2973
+ function adviceEvidenceOf(performanceText) {
2974
+ const text = typeof performanceText === 'string' ? performanceText : ''
2975
+ const withoutWindow = text.replace(/^过去 \d+ 次:/, '')
2976
+ const parts = withoutWindow.split(',').map((part) => part.trim()).filter((part) => part !== '')
2977
+ return { counts: parts.length > 0 ? parts[0] : '', metrics: parts.slice(1) }
2978
+ }
2979
+
2980
+ // 候选安全检查四项:缩进列表对齐折叠文字下方(margin-left 16px),人话映射;
2981
+ // 只渲染四项本身,不重复「安全检查:全部通过 ✓」标题行(标题 = 折叠行)。
2982
+ function buildCandidateSafetyChecks(el, preview) {
2983
+ const checks = Array.isArray(preview.checks) ? preview.checks : []
2984
+ return el('ul', { className: 'sap-candidateSafetyList' },
2985
+ checks.map((c) => el('li', { key: String(c.name), className: 'sap-candidateSafetyItem', title: c.name },
2986
+ (c.verdict === 'pass' ? '✅ ' : '❌ ') + (CHECK_ZH[c.name] || c.name) + (typeof c.reason === 'string' && c.reason !== '' ? ':' + c.reason : '')
2987
+ ))
2988
+ )
2989
+ }
2990
+
2991
+ // 候选改动项:候选 config 与建议来源 profileKey 对比成人话列表(不加前缀,
2992
+ // 展开区语境已是改动清单)。
2993
+ // persona/toolFilter 从键里消失 = 去掉该结构;其余字段值不同 = 换成新值。
2994
+ function candidateChangesOf(profileKey, cfg) {
2995
+ const config = cfg !== null && typeof cfg === 'object' ? cfg : {}
2996
+ const text = typeof profileKey === 'string' ? profileKey : ''
2997
+ const seg = {}
2998
+ for (const part of text.split('|')) {
2999
+ const idx = part.indexOf(':')
3000
+ if (idx >= 0) seg[part.slice(0, idx)] = part.slice(idx + 1)
3001
+ }
3002
+ const changes = []
3003
+ const changed = (field, value) => typeof value === 'string' && value !== '' && seg[field] !== value
3004
+ if (seg.persona === '1' && config.persona_present !== true) changes.push('去掉自定义人格')
3005
+ if (seg.toolFilter === '1' && config.toolFilter_present !== true) changes.push('去掉工具过滤')
3006
+ if (changed('preset', config.preset)) changes.push('预设 → ' + config.preset)
3007
+ if (changed('model', config.model)) changes.push('模型 → ' + config.model)
3008
+ if (changed('effort', config.reasoningEffort)) changes.push('推理强度 → ' + config.reasoningEffort)
3009
+ if (typeof config.maxTokens === 'number' && config.maxTokens > 0) changes.push('token 上限 → ' + String(config.maxTokens))
3010
+ return changes
3011
+ }
3012
+
3013
+ // 建议大卡(v16):卡头只留标题 + 右侧「开启建议」开关;卡体顶部 = 管理子卡
3014
+ // (浅底紧凑,仅候选时效行 + 全局行,就地浮层随行);下方 = 建议列表。
3015
+ function buildAdvicePanel(el, s, actions, sessions) {
2513
3016
  const advice = Array.isArray(s.advice) ? s.advice : []
2514
3017
  const global = s.adviceGlobal !== null && typeof s.adviceGlobal === 'object' ? s.adviceGlobal : null
2515
- const directionZh = (d) => (d === '升' ? ZH.adviceDirUp : d === '降' ? ZH.adviceDirDown : ZH.adviceDirFlat)
3018
+ const candidates = Array.isArray(s.evolutionCandidates) ? s.evolutionCandidates : []
3019
+ const candidatesOf = (profileKey) => candidates.filter((c) => c && typeof c === 'object' && c.profile_key === profileKey)
2516
3020
  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 + ')')
3021
+ return el('div', { className: 'sap-card sap-cardStatic' },
3022
+ el('div', { className: 'sap-cardHead sap-adviceCardHead' },
3023
+ el('div', { className: 'sap-adviceHeadTitleRow' },
3024
+ el('div', { className: 'sap-cardTitleBox' },
3025
+ el('div', { className: 'sap-cardTitle' }, ZH.adviceTitle)
3026
+ ),
3027
+ el('span', { className: 'sap-rowActions' },
3028
+ ZH.adviceEnableLabel,
3029
+ el('label', { className: 'sap-switch', title: ZH.adviceTitle },
3030
+ el('input', { type: 'checkbox', checked: s.evolutionAdvice, onChange: actions.toggleAdvice }),
3031
+ el('span', null, s.evolutionAdvice ? ZH.adviceSwitchOn : ZH.adviceSwitchOff)
3032
+ )
2533
3033
  )
2534
- })
3034
+ ),
3035
+ el('div', { className: 'sap-cardDesc' }, ZH.advicePanelDesc)
3036
+ ),
3037
+ el('div', { className: 'sap-cardBody' },
3038
+ el('div', { className: 'sap-adviceManageCard' },
3039
+ buildCandidateSettingsRow(el, s, actions),
3040
+ el('p', { className: 'sap-adviceGlobalLine' }, ZH.advicePanelGlobal + ':累计 ' + global.total + ' 次(完成 ' + global.completed + '、失败 ' + global.failed + '、终止 ' + global.killed + ')' + ZH.advicePanelReadonly),
3041
+ s.candidateSettingsOpen === true ? buildCandidateSettingsPanel(el, s, actions) : null
3042
+ ),
3043
+ advice.length === 0
3044
+ ? el('p', { className: 'sap-desc' }, ZH.advicePanelEmpty)
3045
+ : el('ul', { className: 'sap-rows' },
3046
+ advice.map((a) => buildAdviceCard(el, s, actions, a, candidatesOf(a.profileKey), sessions))
3047
+ )
2535
3048
  )
2536
3049
  )
2537
3050
  }
2538
3051
 
3052
+ // 详情小节:纯文字加粗小标题(12px label-primary)+ 内容(13px),无左线/无底块。
3053
+ function adviceSectionOf(el, title, children) {
3054
+ return el('div', { className: 'sap-adviceSection' },
3055
+ el('p', { className: 'sap-adviceSectionTitle' }, title),
3056
+ children
3057
+ )
3058
+ }
3059
+
3060
+ // 单张建议卡(v10):摘要行 = 对象/建议/置信度/▸(结论三件套,无加权分);
3061
+ // 展开区 = 小节序列 理由 → 近期表现 → 派发明细 → 候选方案(未应用)→ 操作区。
3062
+ function buildAdviceCard(el, s, actions, a, candidates, sessions) {
3063
+ const profile = matchingProfileOf(s.profiles, a.profileKey)
3064
+ const metrics = adviceMetricsOf(a.performanceText)
3065
+ const rows = s.adviceDetails && typeof s.adviceDetails === 'object' && Array.isArray(s.adviceDetails[a.profileKey]) ? s.adviceDetails[a.profileKey] : []
3066
+ const openMap = s.adviceOpen !== null && typeof s.adviceOpen === 'object' ? s.adviceOpen : {}
3067
+ const open = openMap[a.profileKey] === true
3068
+ const summary = el('button', { type: 'button', className: 'sap-adviceSummary', 'aria-expanded': open, onClick: actions.toggleAdviceOpen(a.profileKey) },
3069
+ el('span', { className: 'sap-adviceObjLine' },
3070
+ el('span', { className: 'sap-adviceObj' }, adviceSummaryObjectText(profile, a.profileKey)),
3071
+ profile === null ? el('span', { className: 'sap-adviceNoProfileTag' }, ZH.adviceNoProfileTag) : null
3072
+ ),
3073
+ el('span', { className: 'sap-adviceSummaryLine' },
3074
+ el('span', { className: 'sap-adviceSummaryText' }, adviceSuggestionShort(a)),
3075
+ el('span', { className: 'sap-adviceConf' }, ZH.adviceConfidence + confidenceZhOf(a.confidence)),
3076
+ el('span', { className: 'sap-adviceSummaryArrow' }, '▸')))
3077
+ const detail = open !== true ? null : el('div', { className: 'sap-adviceDetail' },
3078
+ typeof a.reasonText === 'string' && a.reasonText !== '' ? adviceSectionOf(el, ZH.adviceSectionReason, el('p', { className: 'sap-adviceSectionBody' }, a.reasonText)) : null,
3079
+ metrics.length > 0 ? adviceSectionOf(el, ZH.adviceSectionPerf, el('p', { className: 'sap-adviceSectionBody' }, metrics.join(' · '))) : null,
3080
+ adviceSectionOf(el, ZH.adviceSectionDetail, buildAdviceDetails(el, s, actions, a.profileKey, rows, sessions)),
3081
+ candidates.length > 0 ? buildAdviceCandidates(el, s, actions, a, candidates) : null,
3082
+ buildAdviceFoot(el, s, actions, a, profile, candidates))
3083
+ return el('li', { key: String(a.profileKey), className: 'sap-adviceItem' + (open === true ? ' sap-adviceItemOpen' : '') }, summary, detail)
3084
+ }
3085
+
3086
+ // 近期派发明细折叠行:▸ 近期派发明细 · N 次(点击展开)→ 六列表格;默认收起。
3087
+ function buildAdviceDetails(el, s, actions, profileKey, rows, sessions) {
3088
+ const map = s.detailOpen !== null && typeof s.detailOpen === 'object' ? s.detailOpen : {}
3089
+ const open = map[profileKey] === true
3090
+ const header = el('tr', null,
3091
+ el('th', null, ZH.adviceDetailColTime), el('th', null, ZH.adviceDetailColSession), el('th', null, ZH.adviceDetailColChild),
3092
+ el('th', null, ZH.adviceDetailColOutcome), el('th', null, ZH.adviceDetailColElapsed), el('th', null, ZH.adviceDetailColMode))
3093
+ const cell = (row) => {
3094
+ const outcomeZh = ZH.dispatchOutcomeZh[row.outcome] || row.outcome || ''
3095
+ const bad = row.outcome === 'failed' || row.outcome === 'killed'
3096
+ const title = reminderSessionTitle(sessions, row.sessionId)
3097
+ return el('tr', { key: String(row.ts ?? '') + String(row.childId ?? '') },
3098
+ el('td', { className: 'sap-adviceDetailTime' }, formatMonthDayTime(row.ts)),
3099
+ el('td', null, title !== '' ? el('span', { className: 'sap-adviceDetailSession' }, title) : null,
3100
+ sessionIdShortOf(row.sessionId) !== '' ? el('span', { className: 'sap-adviceDetailSessionId' }, sessionIdShortOf(row.sessionId)) : null),
3101
+ el('td', { className: 'sap-adviceDetailChild' }, childIdShortOf(row.childId)),
3102
+ el('td', { className: bad ? 'sap-warn-x' : '' }, outcomeZh),
3103
+ el('td', null, formatDurationMs(row.elapsedMs)),
3104
+ el('td', null, dispatchModeZh(row.mode)))
3105
+ }
3106
+ return el('div', { className: 'sap-adviceDetailBlock' },
3107
+ el('button', { type: 'button', className: 'sap-adviceDetailToggle', 'aria-expanded': open, onClick: actions.toggleDetail(profileKey) },
3108
+ el('span', null, ZH.adviceDetailTogglePrefix + rows.length + ZH.adviceDetailToggleSuffix),
3109
+ el('span', { className: 'sap-adviceDetailArrow' }, '▸')),
3110
+ open !== true ? null : (rows.length === 0
3111
+ ? el('p', { className: 'sap-candidateHint' }, ZH.adviceDetailEmpty)
3112
+ : el('table', { className: 'sap-adviceDetailTable' }, el('thead', null, header), el('tbody', null, rows.map(cell))))
3113
+ )
3114
+ }
3115
+
3116
+ // 候选管理设置(顶部一行 + 就地浮层):更新模式与有效期用户可配;
3117
+ // 自定义按天数输入(1~365),提交时换算小时数。
3118
+ function candidateModeTextOf(mode) {
3119
+ return mode === 'manual' ? ZH.candidateModeManual : ZH.candidateModeAuto
3120
+ }
3121
+
3122
+ function candidateTtlTextOf(ttlH) {
3123
+ if (ttlH === 0) return ZH.candidateTtl0
3124
+ if (ttlH === 24) return ZH.candidateTtl24
3125
+ if (ttlH === 72) return ZH.candidateTtl72
3126
+ if (ttlH === 168) return ZH.candidateTtl168
3127
+ return ttlH + ' 小时'
3128
+ }
3129
+
3130
+ function candidateTtlChoiceOf(ttlH) {
3131
+ return ttlH === 0 || ttlH === 24 || ttlH === 72 || ttlH === 168 ? String(ttlH) : 'custom'
3132
+ }
3133
+
3134
+ function buildCandidateSettingsRow(el, s, actions) {
3135
+ const settings = s.candidateSettings !== null && typeof s.candidateSettings === 'object' ? s.candidateSettings : { candidateMode: 'auto', candidateTtlH: 24 }
3136
+ return el('p', { className: 'sap-candidateSettingsRow' },
3137
+ el('b', null, ZH.candidateSettingsRow),
3138
+ el('span', null, candidateModeTextOf(settings.candidateMode) + ' · ' + ZH.candidateTtlLabel + ' ' + candidateTtlTextOf(settings.candidateTtlH)),
3139
+ el('button', { type: 'button', className: 'sap-candidateSettingsEdit', onClick: actions.toggleCandidateSettings }, ZH.candidateSettingsEdit))
3140
+ }
3141
+
3142
+ function candidateSettingsRadio(el, label, value, current, onChange) {
3143
+ return el('label', { key: value, className: 'sap-candidateSettingsOption' },
3144
+ el('input', { type: 'radio', checked: current === value, onChange }),
3145
+ el('span', null, label))
3146
+ }
3147
+
3148
+ function buildCandidateSettingsPanel(el, s, actions) {
3149
+ const form = s.candidateSettingsForm !== null && typeof s.candidateSettingsForm === 'object' ? s.candidateSettingsForm : {}
3150
+ const mode = form.candidateMode === 'manual' ? 'manual' : 'auto'
3151
+ const ttlChoice = typeof form.candidateTtlChoice === 'string' ? form.candidateTtlChoice : '24'
3152
+ const ttlOptions = [['24', ZH.candidateTtl24], ['72', ZH.candidateTtl72], ['168', ZH.candidateTtl168], ['0', ZH.candidateTtl0], ['custom', ZH.candidateTtlCustom]]
3153
+ return el('div', { className: 'sap-candidateSettingsPanel' },
3154
+ el('div', { className: 'sap-candidateSettingsField' },
3155
+ el('span', { className: 'sap-fieldLabel' }, ZH.candidateSettingsModeLabel),
3156
+ el('div', { className: 'sap-candidateSettingsOptions' },
3157
+ candidateSettingsRadio(el, ZH.candidateModeAuto + '(推荐)', 'auto', mode, actions.setCandidateFormMode('auto')),
3158
+ candidateSettingsRadio(el, ZH.candidateModeManual, 'manual', mode, actions.setCandidateFormMode('manual'))),
3159
+ el('p', { className: 'sap-metaValueDefault' }, mode === 'manual' ? ZH.candidateSettingsModeManualText : ZH.candidateSettingsModeAutoText)),
3160
+ el('div', { className: 'sap-candidateSettingsField' },
3161
+ el('span', { className: 'sap-fieldLabel' }, ZH.candidateTtlLabel),
3162
+ el('div', { className: 'sap-candidateSettingsOptions' },
3163
+ ttlOptions.map(([value, label]) => candidateSettingsRadio(el, label, value, ttlChoice, actions.setCandidateFormTtl(value)))),
3164
+ ttlChoice === 'custom'
3165
+ ? el('div', { className: 'sap-candidateSettingsCustom' },
3166
+ 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 : '') }),
3167
+ el('span', { className: 'sap-metaValueDefault' }, ZH.candidateSettingsCustomLabel))
3168
+ : null,
3169
+ form.error !== '' ? el('p', { className: 'sap-notice' }, form.error) : null),
3170
+ el('div', { className: 'sap-candidateSettingsActions' },
3171
+ el('button', { type: 'button', className: 'sap-primaryButton', onClick: actions.saveCandidateSettings }, ZH.candidateSettingsSave),
3172
+ el('button', { type: 'button', className: 'sap-secondaryButton', onClick: actions.cancelCandidateSettings }, ZH.candidateSettingsCancel)))
3173
+ }
3174
+
3175
+ // 候选方案区(v10 小节):纯文字加粗小节标题「候选方案(未应用)」+ 上分隔线 →
3176
+ // 数量/单选说明行(手动维护附重新生成)→ 候选项列表(行点击只展开,radio 只选中)。
3177
+ function buildAdviceCandidates(el, s, actions, a, candidates) {
3178
+ const manual = s.candidateSettings !== null && typeof s.candidateSettings === 'object' && s.candidateSettings.candidateMode === 'manual'
3179
+ const selected = candidateSelectedIdOf(s, a.profileKey, candidates)
3180
+ return el('div', { className: 'sap-candidates' },
3181
+ el('p', { className: 'sap-candidatesTitle' }, ZH.candidateTitle, el('span', { className: 'sap-candidatesNotApplied' }, ZH.candidateNotApplied)),
3182
+ el('p', { className: 'sap-candidatesCount' }, candidates.length + ZH.candidateCountSuffix + ZH.candidateCountNote),
3183
+ manual === true
3184
+ ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: actions.regenerateCandidates(a.profileKey) }, ZH.candidateRegenerate)
3185
+ : null,
3186
+ candidates.map((candidate, index) => buildAdviceCandidateRow(el, s, actions, a, candidate, index, selected))
3187
+ )
3188
+ }
3189
+
3190
+ // 默认选中第一条候选;用户选择存进状态(仅选中,行点击只展开)。
3191
+ function candidateSelectedIdOf(s, profileKey, candidates) {
3192
+ const map = s.candidateSelected !== null && typeof s.candidateSelected === 'object' ? s.candidateSelected : {}
3193
+ const current = typeof map[profileKey] === 'string' ? map[profileKey] : ''
3194
+ if (current !== '' && candidates.some((c) => c && c.id === current)) return current
3195
+ return candidates.length > 0 ? candidates[0].id : ''
3196
+ }
3197
+
3198
+ function buildAdviceCandidateRow(el, s, actions, a, candidate, index, selected) {
3199
+ const cfg = candidate.config && typeof candidate.config === 'object' ? candidate.config : {}
3200
+ const openState = s.candidateOpen !== null && typeof s.candidateOpen === 'object' ? s.candidateOpen : {}
3201
+ const safetyState = s.candidateSafetyOpen !== null && typeof s.candidateSafetyOpen === 'object' ? s.candidateSafetyOpen : {}
3202
+ const open = openState[candidate.id] === true
3203
+ const safetyOpen = safetyState[candidate.id] === true
3204
+ const preview = s.draftPreviews && typeof s.draftPreviews === 'object' ? s.draftPreviews[candidate.id] : undefined
3205
+ const changes = candidateChangesOf(a.profileKey, cfg)
3206
+ const onRadio = (event) => {
3207
+ if (event && typeof event.stopPropagation === 'function') event.stopPropagation()
3208
+ actions.selectCandidate(a.profileKey, candidate.id)()
3209
+ }
3210
+ return el('div', { key: String(candidate.id), className: 'sap-candidate' + (selected === candidate.id ? ' sap-candidateSelected' : '') },
3211
+ el('button', { type: 'button', className: 'sap-candidateRow', 'aria-expanded': open, onClick: actions.toggleCandidateOpen(candidate) },
3212
+ el('span', { role: 'radio', 'aria-checked': selected === candidate.id, className: 'sap-candidateRadio', onClick: onRadio }),
3213
+ el('span', { className: 'sap-candidateName' }, '候选 ' + (index + 1) + ' · ' + candidate.name),
3214
+ el('span', { className: 'sap-candidateValidity' }, candidateValidityText(candidate.expiry)),
3215
+ el('span', { className: 'sap-candidateArrow' }, '▸')),
3216
+ open === true ? el('div', { className: 'sap-candidateBody' },
3217
+ changes.length > 0 ? el('div', { className: 'sap-candidateGroup' },
3218
+ el('p', { className: 'sap-candidateGroupTitle' }, ZH.candidateGroupChanges),
3219
+ el('ul', { className: 'sap-candidateChanges' },
3220
+ changes.map((change) => el('li', { key: change, className: 'sap-candidateChange' }, change)))) : null,
3221
+ preview !== undefined
3222
+ ? el('div', { className: 'sap-candidateGroup' },
3223
+ el('button', { type: 'button', className: 'sap-candidateSafetyToggle' + (safetyOpen === true ? ' sap-candidateSafetyToggleOpen' : ''), onClick: actions.toggleCandidateSafety(candidate) },
3224
+ ZH.candidateSafetyPrefix + (preview.ok === true ? ZH.candidateSafetyPass : ZH.candidateSafetyFail),
3225
+ el('span', { className: 'sap-candidateSafetyArrow' }, '▸')),
3226
+ safetyOpen === true ? buildCandidateSafetyChecks(el, preview) : null)
3227
+ : el('div', { className: 'sap-candidateGroup' },
3228
+ el('p', { className: 'sap-candidateHint' }, ZH.candidateSafetyPending)),
3229
+ el('div', { className: 'sap-candidateGroup' },
3230
+ el('p', { className: 'sap-candidateGroupTitle' }, ZH.candidateGroupGenerated),
3231
+ candidateGeneratedLine(el, candidate),
3232
+ el('p', { className: 'sap-candidateHint' }, ZH.candidateNotApplicable))) : null)
3233
+ }
3234
+
3235
+ // 生成信息行(最浅灰,两分支模板):有样本 = 生成于 MM-DD · 基于最近 N 次派发 ·
3236
+ // 近期表现 X(满分 100,X=round(加权分×100));无样本 = 生成于 MM-DD · 暂无近期派发
3237
+ // 样本,仅供参考(不显示 0 分);无生成时间省去「生成于」段。
3238
+ function candidateGeneratedLine(el, candidate) {
3239
+ const basis = candidate.basis !== null && typeof candidate.basis === 'object' ? candidate.basis : {}
3240
+ const n = Number.isFinite(basis.n) ? basis.n : 0
3241
+ const score100 = Number.isFinite(basis.score) ? Math.round(basis.score * 100) : 0
3242
+ const generated = formatMonthDayTime(basis.generatedAt)
3243
+ const parts = []
3244
+ if (generated !== '') parts.push(ZH.candidateGeneratedPrefix + generated)
3245
+ if (n >= 1) {
3246
+ parts.push(ZH.candidateSamplesPrefix + n + ZH.candidateSamplesSuffix)
3247
+ parts.push(ZH.candidatePerfPrefix + score100 + ZH.candidatePerfSuffix)
3248
+ } else {
3249
+ parts.push(ZH.candidateNoSample)
3250
+ }
3251
+ return el('p', { className: 'sap-candidateHint' }, parts.join(' · '))
3252
+ }
3253
+
3254
+ // 卡底部:无对应方案 = [保存为方案](统一主按钮)+ 备注两行;有对应方案 =
3255
+ // 灰置[按建议调整方案](统一次级按钮)+ 定位;说明与「生成于…」拆两行不连排。
3256
+ function buildAdviceFoot(el, s, actions, a, profile, candidates) {
3257
+ const basis = (Array.isArray(candidates) && candidates.length > 0 && candidates[0] !== null && typeof candidates[0] === 'object' && candidates[0].basis !== null && typeof candidates[0].basis === 'object') ? candidates[0].basis : {}
3258
+ const generatedAt = formatMonthDayTime(basis.generatedAt)
3259
+ const generatedText = generatedAt !== '' ? ZH.candidateGeneratedPrefix + generatedAt : ''
3260
+ const noteLines = (line) => el('div', { className: 'sap-adviceFootNotes' },
3261
+ el('p', { className: 'sap-adviceFootNote' }, line),
3262
+ generatedText !== '' ? el('p', { className: 'sap-adviceFootNote' }, generatedText) : null)
3263
+ if (profile !== null) {
3264
+ return el('div', { className: 'sap-adviceFoot' },
3265
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, disabled: true, title: ZH.candidateNotApplicable }, ZH.adviceApplyDisabled),
3266
+ el('button', { type: 'button', className: 'sap-detailToggle', onClick: actions.locateProfile(profile.id) }, ZH.adviceRowLocate),
3267
+ noteLines(ZH.adviceFootNoteMatchedPrefix + (profile.name || profile.id) + '(' + profile.id + ')'))
3268
+ }
3269
+ return el('div', { className: 'sap-adviceFoot' },
3270
+ el('button', { type: 'button', className: 'sap-primaryButton', style: smallBtn, onClick: actions.saveAdviceProfile(a.profileKey) }, ZH.adviceSaveProfile),
3271
+ noteLines(ZH.adviceFootNoteSave))
3272
+ }
3273
+
2539
3274
  // 逃生舱卡片:开关 + (打开时)显式警告文案 + 放行集增删(输入框 + 添加按钮,
2540
3275
  // 每项「移除」按钮)。增删走 /escape-add /escape-remove,成功后用响应 presets 刷新。
2541
3276
  function buildEscapeSection(el, s, actions) {
2542
3277
  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)
3278
+ return el('div', { className: 'sap-card sap-cardStatic' },
3279
+ el('div', { className: 'sap-cardHead' },
3280
+ el('div', { className: 'sap-cardTitleBox' },
3281
+ el('div', { className: 'sap-cardTitle' }, ZH.escapeTitle),
3282
+ el('div', { className: 'sap-cardDesc' }, ZH.escapeCardDesc)
2547
3283
  ),
2548
3284
  el('span', { className: 'sap-rowActions' },
2549
3285
  el('label', { className: 'sap-switch', title: '逃生舱开关' },
@@ -2552,26 +3288,28 @@ function buildEscapeSection(el, s, actions) {
2552
3288
  )
2553
3289
  )
2554
3290
  ),
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
- ))
3291
+ el('div', { className: 'sap-cardBody' },
3292
+ s.escapeEnabled
3293
+ ? el('p', { className: 'sap-notice', role: 'alert' }, ZH.escapeWarn)
3294
+ : null,
3295
+ el('div', { className: 'sap-escapeAdd' },
3296
+ el('input', {
3297
+ className: 'sap-input',
3298
+ style: { flex: '1 1 0', minWidth: '160px' },
3299
+ value: s.escapeInput,
3300
+ placeholder: ZH.escapeInputPlaceholder,
3301
+ onChange: (event) => s.setEscapeInput(event && event.target ? event.target.value : '')
3302
+ }),
3303
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.escapeAdd }, ZH.escapeAdd)
3304
+ ),
3305
+ el('div', { className: 'sap-chips' },
3306
+ list.length === 0
3307
+ ? el('span', { className: 'sap-chipValueDim' }, ZH.escapeEmpty)
3308
+ : list.map((id) => el('span', { key: String(id), className: 'sap-chip sap-chipWarn' },
3309
+ String(id),
3310
+ el('button', { type: 'button', className: 'sap-chipRemove', title: ZH.escapeRemove, onClick: actions.escapeRemove(id) }, ZH.escapeRemove)
3311
+ ))
3312
+ )
2575
3313
  )
2576
3314
  )
2577
3315
  }
@@ -2596,8 +3334,8 @@ function buildSettingsCard(el, key, title, desc, open, toggle, body, isStatic) {
2596
3334
  )
2597
3335
  }
2598
3336
 
2599
- // 常驻状态条:health + 审计丢失(health 唯一位置;丢失 >0 红字)。目录/告警计数
2600
- // 不再重复(版本告警正文在高级区,catalog 计数随 [2] 区一并删除——一个数字只出现一次)。
3337
+ // 审计健康状态行:health + 审计丢失(health 唯一位置;丢失 >0 红字)。目录/告警
3338
+ // 计数不重复(版本告警正文在维护页签,一个数字只出现一次);现挂在安全页签审计健康卡。
2601
3339
  function buildStatusBar(el, s) {
2602
3340
  const degraded = s.audit.health === 'degraded'
2603
3341
  const lost = s.audit.lostTelemetry + s.audit.lostGovernance
@@ -2617,13 +3355,14 @@ function buildSectionNotices(el, s) {
2617
3355
  }
2618
3356
 
2619
3357
  // draft 安全检查预览结果:ok 全绿/存在拒绝,逐项展示 verdict + reason。
3358
+ // 检查项名走 CHECK_ZH 人话映射(可见面零内部术语),title 保留原串供排查。
2620
3359
  function buildDraftPreview(el, preview) {
2621
3360
  const checks = Array.isArray(preview.checks) ? preview.checks : []
2622
3361
  return el('div', { className: 'sap-draftPreview' },
2623
3362
  el('p', { className: preview.ok ? 'sap-savedNotice' : 'sap-warn', role: 'status' }, preview.ok ? ZH.draftPreviewPass : ZH.draftPreviewFail),
2624
3363
  el('ul', { className: 'sap-rows' },
2625
3364
  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)),
3365
+ el('div', { className: 'sap-rowHead' }, el('span', { className: 'sap-rowName', title: c.name }, (c.verdict === 'pass' ? '✅ ' : '❌ ') + (CHECK_ZH[c.name] || c.name))),
2627
3366
  c.reason ? el('p', { className: 'sap-desc' }, c.reason) : null
2628
3367
  ))
2629
3368
  )
@@ -2722,41 +3461,76 @@ function buildCostObservabilitySection(el, s, actions) {
2722
3461
  )
2723
3462
  }
2724
3463
 
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
3464
+ // 六页签骨架:横向 tab(下划线激活 + 加粗,样式对齐官方「插件」节)+ 内容区。
3465
+ function buildTabBar(el, s) {
3466
+ return el('div', { className: 'sap-tabs', role: 'tablist' },
3467
+ SETTINGS_TABS.map((tab) => el('button', {
3468
+ key: tab.key, type: 'button', role: 'tab', 'aria-selected': s.activeTab === tab.key,
3469
+ className: 'sap-tab' + (s.activeTab === tab.key ? ' sap-tabActive' : ''),
3470
+ onClick: () => s.setActiveTab(tab.key)
3471
+ }, tab.label))
2733
3472
  )
2734
3473
  }
2735
3474
 
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)
3475
+ // 页签内容区:全部挂载、非激活隐藏(组件状态不因切换丢失)。
3476
+ function buildTabZone(el, s, key, children) {
3477
+ return el('div', { role: 'tabpanel', className: 'sap-zone' + (s.activeTab === key ? ' sap-zoneActive' : '') }, children)
3478
+ }
3479
+
3480
+ // 安全页签:三卡一律 sap-card(与①方案管理同款)——安全保证、逃生舱、
3481
+ // 审计健康(状态行 + 只读审计明细折叠表,明细表类 = sap-audit*)。
3482
+ function buildSafetySection(el, s, actions, reminders) {
2741
3483
  return el('div', null,
2742
- buildAdvancedSubgroup(el, '安全', '放行非官方预设的高风险通道,默认关闭', [
3484
+ buildSettingsCard(el, 5, ZH.safetyCardTitle, ZH.safetyCardDesc, true, () => {}, [
2743
3485
  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
- ])
3486
+ el('p', { className: 'sap-desc' }, ZH.safetyCheckNote)
3487
+ ], true),
3488
+ buildEscapeSection(el, s, actions),
3489
+ buildSettingsCard(el, 6, ZH.auditCardTitle, ZH.auditCardDesc, true, () => {}, [
3490
+ buildStatusBar(el, s),
3491
+ buildAuditDetails(el, s, actions, reminders)
3492
+ ], true)
3493
+ )
3494
+ }
3495
+
3496
+ // 审计明细事件与结果人话(与提醒同源;未知类别原样回退,信息不删)。
3497
+ function auditEventText(r) {
3498
+ const text = ZH.auditEventZh[r.kind] || r.kind
3499
+ return r.severity === 'P0' ? text + '(紧急)' : text
3500
+ }
3501
+
3502
+ function auditResultText(r) {
3503
+ if (typeof r.action === 'string' && r.action !== '') {
3504
+ const time = formatMonthDayTime(r.actedAt)
3505
+ return ZH.auditResultActedPrefix + (ZH.reminderActionZh[r.action] || r.action) + (time !== '' ? ' ' + time : '')
3506
+ }
3507
+ return r.unread === true ? ZH.auditResultPending : ZH.auditResultRead
3508
+ }
3509
+
3510
+ // 审计明细折叠表(只读展示,无导出/过滤):最近 20 条治理留痕。
3511
+ function buildAuditDetails(el, s, actions, reminders) {
3512
+ const rows = Array.isArray(reminders) ? reminders.slice(0, 20) : []
3513
+ const cell = (r) => el('tr', { key: String(r.id) },
3514
+ el('td', { className: 'sap-auditTime' }, formatMonthDayTime(r.createdAt)),
3515
+ el('td', { className: 'sap-auditEvent' }, auditEventText(r)),
3516
+ el('td', { className: 'sap-auditObject' }, r.title + ' · ' + (typeof r.sessionId === 'string' && r.sessionId !== '' ? '会话 ' + r.sessionId.slice(0, 8) + '…' : ZH.reminderGlobal)),
3517
+ el('td', null, auditResultText(r)))
3518
+ return el('div', null,
3519
+ el('button', { type: 'button', className: 'sap-auditCollapse', 'aria-expanded': s.auditDetailOpen === true, onClick: actions.toggleAuditDetail },
3520
+ el('span', null, ZH.auditDetailToggle),
3521
+ el('span', { className: 'sap-auditArrow' }, '▸')),
3522
+ s.auditDetailOpen !== true ? null : (rows.length === 0
3523
+ ? el('p', { className: 'sap-candidateHint' }, ZH.auditDetailEmpty)
3524
+ : [el('p', { className: 'sap-candidateHint' }, ZH.auditDetailScope),
3525
+ el('table', { className: 'sap-auditTable' },
3526
+ el('thead', null, el('tr', null,
3527
+ el('th', null, ZH.auditColTime), el('th', null, ZH.auditColEvent),
3528
+ el('th', null, ZH.auditColObject), el('th', null, ZH.auditColResult))),
3529
+ el('tbody', null, rows.map(cell)))])
2756
3530
  )
2757
3531
  }
2758
3532
 
2759
- function buildSectionReturn(el, s, actions, rows, addCard, openSections, toggleSection, reminderCenter) {
3533
+ function buildSectionReturn(el, s, actions, rows, addCard, reminderCenter, sessions) {
2760
3534
  const notices = buildSectionNotices(el, s)
2761
3535
  return el('div', { className: 'sap-section' },
2762
3536
  el('div', { className: 'sap-titleRow' },
@@ -2766,27 +3540,29 @@ function buildSectionReturn(el, s, actions, rows, addCard, openSections, toggleS
2766
3540
  el('span', null, s.enabled ? '已启用' : '已禁用')
2767
3541
  )
2768
3542
  ),
2769
- buildStatusBar(el, s),
2770
3543
  ...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')
3544
+ buildTabBar(el, s),
3545
+ buildTabZone(el, s, 'profiles',
3546
+ buildSettingsCard(el, 1, '方案管理', '管理派发可用的方案:启用、编辑、删除,含成本与成功率', true, () => {}, [
3547
+ el('p', { className: 'sap-intro' }, ZH.profilesIntro),
3548
+ el('ul', { className: 'sap-rows' }, rows),
3549
+ buildDraftRows(el, s, actions),
3550
+ el('div', { className: 'sap-addBlock' },
3551
+ s.adding ? addCard : el('div', { className: 'sap-addActions' },
3552
+ el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
3553
+ )
3554
+ ),
3555
+ el('div', { className: 'sap-addActions', style: { marginTop: '4px' } },
3556
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.resetAll }, ZH.resetAllBuiltins)
2778
3557
  )
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)
3558
+ ], true)),
3559
+ buildTabZone(el, s, 'cost',
3560
+ buildSettingsCard(el, 2, '全局成本', '跨全部会话的派发成本汇总,按模型与方案拆分', true, () => {}, [
3561
+ buildCostObservabilitySection(el, s, actions)
3562
+ ], true)),
3563
+ buildTabZone(el, s, 'advice', buildAdvicePanel(el, s, actions, sessions)),
3564
+ buildTabZone(el, s, 'safety', buildSafetySection(el, s, actions, s.auditReminders)),
3565
+ buildTabZone(el, s, 'reminders', reminderCenter)
2790
3566
  )
2791
3567
  }
2792
3568
 
@@ -2809,7 +3585,7 @@ function attachDraftActions(api, s, refresh, applyWrite, actions) {
2809
3585
  applyWrite(data, '已应用 draft:' + draft.name)
2810
3586
  // 闭环可视化:应用成功后展开方案管理区并高亮新方案卡片。
2811
3587
  if (typeof data.id === 'string' && data.id !== '') {
2812
- s.setOpenSections((prev) => ({ ...prev, 1: true }))
3588
+ s.setActiveTab('profiles')
2813
3589
  s.setHighlightId(data.id)
2814
3590
  setTimeout(() => s.setHighlightId(null), 8000)
2815
3591
  }
@@ -2825,41 +3601,44 @@ function attachDraftActions(api, s, refresh, applyWrite, actions) {
2825
3601
  }
2826
3602
  }
2827
3603
 
2828
- function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter) {
3604
+ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter, getSessions) {
2829
3605
  function ProfilesSection() {
2830
3606
  const [profiles, setProfiles] = useState([]); const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] }); const [enabled, setEnabled] = useState(true)
2831
3607
  const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [error, setError] = useState('')
2832
3608
  const [savedNotice, setSavedNotice] = useState(''); const [persistWarning, setPersistWarning] = useState(''); const [form, setForm] = useState(EMPTY_FORM)
2833
3609
  const [tools, setTools] = useState([]); const [versionWarnings, setVersionWarnings] = useState([]); const [pluginVersion, setPluginVersion] = useState(''); const [audit, setAudit] = useState(AUDIT_DEFAULTS)
2834
3610
  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([])
3611
+ const [evolutionAdvice, setEvolutionAdvice] = useState(false); const [drafts, setDrafts] = useState([]); const [advice, setAdvice] = useState([]); const [evolutionCandidates, setEvolutionCandidates] = useState([])
2836
3612
  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 }
3613
+ const [candidateOpen, setCandidateOpen] = useState({}); const [candidateSelected, setCandidateSelected] = useState({}); const [candidateSafetyOpen, setCandidateSafetyOpen] = useState({})
3614
+ const [candidateSettings, setCandidateSettings] = useState({ candidateMode: 'auto', candidateTtlH: 24 }); const [candidateSettingsOpen, setCandidateSettingsOpen] = useState(false)
3615
+ const [candidateSettingsForm, setCandidateSettingsForm] = useState({ candidateMode: 'auto', candidateTtlChoice: '24', customDays: '', error: '' }); const [adviceDetails, setAdviceDetails] = useState({}); const [detailOpen, setDetailOpen] = useState({})
3616
+ const [activeTab, setActiveTab] = useState('profiles'); const [highlightId, setHighlightId] = useState(null)
3617
+ const [adviceOpen, setAdviceOpen] = useState({}); const [auditDetailOpen, setAuditDetailOpen] = useState(false); const [auditReminders, setAuditReminders] = useState([])
3618
+ const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, pluginVersion, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, drafts, advice, evolutionCandidates, adviceGlobal, costSummary, draftPreviews, candidateOpen, candidateSelected, candidateSafetyOpen, candidateSettings, candidateSettingsOpen, candidateSettingsForm, adviceDetails, detailOpen, activeTab, highlightId, adviceOpen, auditDetailOpen, auditReminders, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setPluginVersion, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice, setDrafts, setAdvice, setEvolutionCandidates, setAdviceGlobal, setCostSummary, setDraftPreviews, setCandidateOpen, setCandidateSelected, setCandidateSafetyOpen, setCandidateSettings, setCandidateSettingsOpen, setCandidateSettingsForm, setAdviceDetails, setDetailOpen, setActiveTab, setHighlightId, setAdviceOpen, setAuditDetailOpen, setAuditReminders }
2841
3619
  const refresh = useCallback(() => {
2842
3620
  api('/list')
2843
3621
  .then((data) => {
2844
3622
  setProfiles(Array.isArray(data.profiles) ? data.profiles : [])
2845
3623
  setAdvice(Array.isArray(data.advice) ? data.advice : [])
3624
+ setEvolutionCandidates(Array.isArray(data.evolutionCandidates) ? data.evolutionCandidates : [])
2846
3625
  setAdviceGlobal(data.adviceGlobal !== null && typeof data.adviceGlobal === 'object' ? data.adviceGlobal : null)
3626
+ setAdviceDetails(data.adviceDetails !== null && typeof data.adviceDetails === 'object' ? data.adviceDetails : {})
2847
3627
  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: [] })
2848
3628
  })
2849
3629
  .catch((err) => setError(String((err && err.message) || err)))
2850
3630
  api('/drafts')
2851
3631
  .then((data) => setDrafts(Array.isArray(data.drafts) ? data.drafts : []))
2852
3632
  .catch(() => {})
3633
+ api('/reminders').then((data) => setAuditReminders(Array.isArray(data.reminders) ? data.reminders : [])).catch(() => {})
2853
3634
  }, [])
2854
3635
  const loadOptions = useCallback(() => {
2855
3636
  api('/options/summary')
2856
- .then((data) => applySummaryData(data, { enabled: setEnabled, options: setOptions, audit: setAudit, escapeEnabled: setEscapeEnabled, escapePresets: setEscapePresets, evolutionAdvice: setEvolutionAdvice }))
3637
+ .then((data) => applySummaryData(data, { enabled: setEnabled, options: setOptions, audit: setAudit, escapeEnabled: setEscapeEnabled, escapePresets: setEscapePresets, evolutionAdvice: setEvolutionAdvice, candidateSettings: setCandidateSettings }))
2857
3638
  .catch(() => {})
2858
3639
  }, [])
2859
- const loadEfforts = makeEffortsLoader(api, setOptions)
2860
- const loadTools = makeToolsLoader(api, setTools)
2861
- // 版本探测只挂载时拉一次:useCallback([]) 稳定引用,防 effect 依赖恒变导致设置页打开期间无限重取。
2862
- const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings, setPluginVersion)(), [])
3640
+ const loadEfforts = makeEffortsLoader(api, setOptions); const loadTools = makeToolsLoader(api, setTools)
3641
+ const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings, setPluginVersion)(), []) // 只挂载时拉一次:稳定引用防 effect 依赖恒变
2863
3642
  useEffect(() => {
2864
3643
  refresh()
2865
3644
  loadOptions()
@@ -2867,9 +3646,8 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, Re
2867
3646
  }, [refresh, loadOptions, loadVersions])
2868
3647
  const applyWrite = makeApplyWrite(s); const actions = makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools)
2869
3648
  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))
3649
+ const rows = buildProfileRows(el, chip, s, actions); const toolSection = buildToolSection(el, s, actions)
3650
+ return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection), el(ReminderCenter), sessionsSnapshot(getSessions))
2873
3651
  }
2874
3652
  return ProfilesSection
2875
3653
  }
@@ -4036,7 +4814,7 @@ window.__ModuleLoader__.load({
4036
4814
  // 与派发决策台账 openChild 同一 sessions 服务来源。
4037
4815
  const openChild = (address) => ctx.sessions.openSubagent(address)
4038
4816
  const ReminderCenter = makeReminderCenter(el, useState, useEffect, openChild, () => ctx.sessions)
4039
- const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter)
4817
+ const ProfilesSection = makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter, () => ctx.sessions)
4040
4818
  ensureStyles()
4041
4819
  const SubagentHeaderUtility = makeSubagentHeaderUtility(el, useState, useEffect, () => ctx.get('modelDirectories'), () => ctx.sessions)
4042
4820
  ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: 'dispatch' }, DispatchToolview))
@@ -4067,4 +4845,4 @@ window.__ModuleLoader__.load({
4067
4845
 
4068
4846
  return { apply, inject }
4069
4847
  },
4070
- })
4848
+ })