dsh-subagent-profile 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -23,6 +23,7 @@
23
23
 
24
24
  const CSS = [
25
25
  '.sap-section{width:100%;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}',
26
+ '.sap-zoneStack{flex-direction:column;gap:12px;display:flex}',
26
27
  '.sap-statusBar{position:sticky;top:0;z-index:1;background:transparent;border:0;border-bottom:1px solid var(--dsw-alias-border-l2);flex-wrap:wrap;gap:4px 14px;padding:4px 0 8px;display:flex;font-size:12px;line-height:18px}',
27
28
  '.sap-statusDim{color:var(--dsw-alias-label-tertiary)}',
28
29
  '.sap-rowHighlight{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}',
@@ -164,6 +165,8 @@ const CSS = [
164
165
  '.sap-candidateSettingsCustom .sap-input{max-width:120px}',
165
166
  '.sap-candidateSettingsActions{align-items:center;gap:10px;display:flex}',
166
167
  '.sap-desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}',
168
+ '.sap-descMinor{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:16px}',
169
+ '.sap-maintenanceSwitchDesc{flex-direction:column;gap:2px;display:flex}',
167
170
  '.sap-warn{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}',
168
171
  '.sap-versionWarn{background:var(--dsw-alias-state-warn-tertiary);border:1px solid var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);border-radius:8px;flex-direction:column;gap:4px;padding:10px 12px;font-size:12px;line-height:18px;margin:0;display:flex}',
169
172
  '.sap-chips{flex-wrap:wrap;gap:6px;display:flex}',
@@ -305,6 +308,7 @@ const CSS = [
305
308
  '.sap-ledgerRowHead:hover{background:var(--dsw-alias-interactive-bg-hover)}',
306
309
  '.sap-ledgerTime{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;font-family:monospace;flex:none}',
307
310
  '.sap-ledgerMeta{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;min-width:0;word-break:break-word}',
311
+ '.sap-ledgerImageText{color:var(--dsw-alias-label-tertiary);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:none}',
308
312
  '.sap-ledgerToggle{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;transition:transform .12s;margin-left:auto;flex:none;display:inline-flex}',
309
313
  '.sap-ledgerRowOpen .sap-ledgerToggle{transform:rotate(90deg)}',
310
314
  '.sap-ledgerExpand{flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2);padding:10px 14px 12px;display:flex}',
@@ -438,7 +442,7 @@ const ZH = {
438
442
  // 提醒系统
439
443
  reminderCenterTitle: '提醒中心',
440
444
  reminderCenterDesc: '需要你关注的事件汇总,所有动作均有审计记录;处理过的条目折叠保存',
441
- reminderEmpty: '暂无提醒(治理事件会在这里留痕)',
445
+ reminderEmpty: '暂无提醒(历史操作与事件会在这里留痕)',
442
446
  reminderAck: '标记已读',
443
447
  reminderAdopt: '采纳',
444
448
  reminderIgnore: '忽略',
@@ -457,6 +461,27 @@ const ZH = {
457
461
  reminderNoteLabel: '说明',
458
462
  reminderNoSettlement: '未结算',
459
463
  reminderNoExcerpt: '(未记录)',
464
+ reminderArchivedEmpty: '暂无已归档条目(数据维护区执行归档后在此分页查看)',
465
+ reminderArchivedMore: '加载更早',
466
+ // 数据维护(设置页 → 方案管理尾部)
467
+ maintenanceCardTitle: '数据维护',
468
+ maintenanceCardDesc: '候选、快照、提醒与统计数据的清理入口;每次清理都会留下记录',
469
+ snapshotSwitchDesc: '派发时在本机自动保存完整配置(含自定义人格),供「保存为方案」与对比使用。\n数据仅存您本机(~/.dsh/subagent-evolution/snapshots/),插件更新不影响,可随时清理。\n关闭开关即停止新快照(已存保留)。',
470
+ snapshotCount: '已存快照:{n} / {cap} 条',
471
+ snapshotCapHint: '快照已达护栏上限,停止新快照(可清理后继续)',
472
+ snapshotNone: '暂无快照(开启开关后每次派发保存一条)',
473
+ snapshotView: '查看',
474
+ snapshotViewClose: '收起',
475
+ snapshotMeta: '快照 {id} · 于 {ts}',
476
+ snapshotPersonaFold: '人格({n} 字符,点击展开)',
477
+ snapshotEmptyConfig: '(无配置字段)',
478
+ cleanCandidates: '清理候选/建议',
479
+ cleanSnapshots: '清理快照',
480
+ cleanReminders: '归档已处理提醒',
481
+ cleanStats: '重置统计',
482
+ maintenanceConfirm: '确认要执行「{zone}」吗?清理操作会留下记录。',
483
+ maintenanceDone: '已执行 {zone}:{count} 条',
484
+ maintenanceCleanErr: '清理失败:',
460
485
  toolRunning: '运行中',
461
486
  tierZh: { cheap: '省 token(cheap)', balanced: '均衡(balanced)', premium: '高成本(premium)' },
462
487
  stopReasonZh: { completed: '完成', 'max-tokens': '超出预算', aborted: '已中止', refusal: '已拒绝', error: '出错' },
@@ -719,7 +744,7 @@ const ZH = {
719
744
  auditCardTitle: '审计健康',
720
745
  auditCardDesc: '治理审计状态与留痕,异常红字提示',
721
746
  auditDetailToggle: '查看审计明细(点击展开)',
722
- auditDetailEmpty: '暂无审计明细(治理事件会在这里留痕)',
747
+ auditDetailEmpty: '暂无审计明细(历史操作与事件会在这里留痕)',
723
748
  auditDetailScope: '只读展示,暂不提供导出与过滤',
724
749
  auditColTime: '时间',
725
750
  auditColEvent: '事件',
@@ -1100,6 +1125,27 @@ function readDispatchResult(block) {
1100
1125
  }
1101
1126
 
1102
1127
 
1128
+ // 生效画像一行(派发卡片/台账行展示):优先 B3 交付的 meta.imageText(后端同源
1129
+ // 格式,;分隔转 UI 的 · 分隔、路由段加「模型」前缀);旧结果无 imageText 时按
1130
+ // 生效字段同格式合成。未绑定方案 → 「继承父方案 · …」;两者都不可得返回空串
1131
+ // (不渲染该行)。fail-soft:坏输入绝不 throw。
1132
+ function imageTextOfBlock(meta, eff) {
1133
+ const e = eff && typeof eff === 'object' ? eff : {}
1134
+ const m = meta && typeof meta === 'object' ? meta : {}
1135
+ let base = typeof m.imageText === 'string' && m.imageText !== '' ? m.imageText : ''
1136
+ if (base === '') {
1137
+ const profile = typeof e.profile === 'string' && e.profile !== '' && e.profile !== '(inline)' ? e.profile : ''
1138
+ const provider = typeof e.provider === 'string' && e.provider !== '' && e.provider !== '(parent)' ? e.provider : ''
1139
+ const model = typeof e.model === 'string' && e.model !== '' && e.model !== '(parent)' ? e.model : ''
1140
+ const route = provider !== '' && model !== '' ? provider + '/' + model : '继承父'
1141
+ const tier = typeof m.tokenTier === 'string' && m.tokenTier !== '' ? m.tokenTier : 'balanced'
1142
+ base = profile !== '' ? `方案 ${profile};${route};档位 ${tier}` : `继承父方案;${route};档位 ${tier}`
1143
+ }
1144
+ const parts = base.split(';').map((p) => p.trim()).filter((p) => p !== '')
1145
+ if (parts.length >= 2) parts[1] = '模型 ' + parts[1]
1146
+ return parts.join(' · ')
1147
+ }
1148
+
1103
1149
  // 结算数字提取:tokens 数与缓存命中分别取,供「≈N tokens(缓存命中 M)」
1104
1150
  // 单括号合并展示。tokens 不可得返回 null。
1105
1151
  function summaryNumbersOf(result, block) {
@@ -1462,10 +1508,14 @@ function buildSubagentBanner(el, subagent, parentPreset, modelState, bannerMode)
1462
1508
  // 通知中心(设置页通知与提醒页签):读 /reminders(与审计同源 host store),全动作
1463
1509
  // (标记已读/采纳/忽略/拒绝/关闭)经 POST 回写 host 并刷新——一条提醒 = 一条审计记录。
1464
1510
  // sessions 服务供条目主/子会话标题查询与跳转;跳转失败静默(设置页不被导航异常打断)。
1511
+ // 归档视图每页条数(防大量已处理条目一次性渲染卡 UI)。
1512
+ const ARCHIVED_PAGE_SIZE = 20
1513
+
1465
1514
  function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1466
1515
  function ReminderCenter(props) {
1467
1516
  const [reminders, setReminders] = useState([])
1468
1517
  const [error, setError] = useState('')
1518
+ const [archived, setArchived] = useState({ open: false, list: [], total: 0, offset: 0, loading: false })
1469
1519
  const refresh = () => {
1470
1520
  api('/reminders').then((data) => setReminders(Array.isArray(data.reminders) ? data.reminders : [])).catch(() => {})
1471
1521
  }
@@ -1475,17 +1525,50 @@ function makeReminderCenter(el, useState, useEffect, openChild, getSessions) {
1475
1525
  const request = action === 'ack' ? api('/reminders/ack', { ids: [id] }) : api('/reminders/action', { id, action })
1476
1526
  request.then(refresh).catch((err) => setError(String((err && err.message) || err)))
1477
1527
  }
1528
+ // 归档视图分页加载:打开时取第一页,「加载更早」按 offset 追加(存储只读)。
1529
+ const loadArchivedPage = (offset) => {
1530
+ setArchived((prev) => ({ ...prev, loading: true }))
1531
+ api('/reminders/archived?offset=' + offset + '&limit=' + ARCHIVED_PAGE_SIZE)
1532
+ .then((data) => {
1533
+ const rows = Array.isArray(data.reminders) ? data.reminders : []
1534
+ setArchived((prev) => ({ ...prev, loading: false, total: typeof data.total === 'number' ? data.total : prev.total, offset, list: offset === 0 ? rows : [...prev.list, ...rows] }))
1535
+ })
1536
+ .catch(() => setArchived((prev) => ({ ...prev, loading: false })))
1537
+ }
1538
+ const toggleArchived = () => {
1539
+ const nextOpen = !archived.open
1540
+ setArchived((prev) => ({ ...prev, open: nextOpen }))
1541
+ if (nextOpen && archived.list.length === 0) loadArchivedPage(0)
1542
+ }
1478
1543
  const jumpToAdvice = props !== null && typeof props === 'object' && typeof props.jumpToAdvice === 'function' ? props.jumpToAdvice : null
1479
- return buildReminderCenter(el, reminders, error, act, getSessions(), openChild, jumpToAdvice)
1544
+ return buildReminderCenter(el, reminders, error, act, getSessions(), openChild, jumpToAdvice, archived, toggleArchived, loadArchivedPage)
1480
1545
  }
1481
1546
  return ReminderCenter
1482
1547
  }
1483
1548
 
1549
+ // 归档视图区:折叠开关(总条数)+ 只读行列表(已处理条目复渲染)+ 加载更早。
1550
+ function buildArchivedReminderSection(el, archived, act, sessions, openChild, jumpToAdvice, toggleArchived, loadArchivedPage) {
1551
+ const rows = Array.isArray(archived.list) ? archived.list : []
1552
+ const hasMore = archived.list.length < archived.total
1553
+ return el('details', { className: 'sap-customized sap-customizedArrowEnd sap-reminderHandled', open: archived.open === true },
1554
+ el('summary', { className: 'sap-customizedSummary', onClick: toggleArchived }, '已归档(' + archived.total + ')', el('span', { className: 'sap-customizedEndArrow' }, '▸')),
1555
+ el('div', { className: 'sap-customizedBody' },
1556
+ rows.length === 0
1557
+ ? el('p', { className: 'sap-desc' }, ZH.reminderArchivedEmpty)
1558
+ : [
1559
+ el('ul', { className: 'sap-rows' }, rows.map((r) => buildReminderRow(el, r, act, sessions, openChild, jumpToAdvice))),
1560
+ hasMore
1561
+ ? el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, disabled: archived.loading === true, onClick: () => loadArchivedPage(archived.offset + ARCHIVED_PAGE_SIZE) }, ZH.reminderArchivedMore)
1562
+ : null
1563
+ ]
1564
+ )
1565
+ )
1566
+ }
1567
+
1484
1568
  // 提醒中心分层:未终态条目(action 为空)进默认列表;已终态条目折叠为「已处理(N)」
1485
- // 可展开区(原生 details,展开后仍按现有行渲染并保留已处理标注,可追溯)。审计留痕
1486
- // 由 host 侧动作回写,已处理条目不再堆在默认列表里。全空才显示空态文案;仅剩已处理时
1487
- // 只显示折叠区,不显示空态。
1488
- function buildReminderCenter(el, reminders, error, act, sessions, openChild, jumpToAdvice) {
1569
+ // 可展开区(原生 details,展开后仍按现有行渲染并保留已处理标注,可追溯);归档后的
1570
+ // 条目进入「已归档(N)」只读分页视图(存储与审计保留)。审计留痕由 host 侧动作回写。
1571
+ function buildReminderCenter(el, reminders, error, act, sessions, openChild, jumpToAdvice, archived, toggleArchived, loadArchivedPage) {
1489
1572
  const handled = reminders.filter((r) => typeof r.action === 'string' && r.action !== '')
1490
1573
  const pending = reminders.filter((r) => !(typeof r.action === 'string' && r.action !== ''))
1491
1574
  const pendingList = pending.length > 0
@@ -1499,6 +1582,9 @@ function buildReminderCenter(el, reminders, error, act, sessions, openChild, jum
1499
1582
  )
1500
1583
  )
1501
1584
  : null
1585
+ const archivedSection = archived !== null && typeof archived === 'object'
1586
+ ? buildArchivedReminderSection(el, archived, act, sessions, openChild, jumpToAdvice, toggleArchived, loadArchivedPage)
1587
+ : null
1502
1588
  return el('div', { className: 'sap-card sap-cardStatic' },
1503
1589
  el('div', { className: 'sap-cardHead' },
1504
1590
  el('div', { className: 'sap-cardTitleBox' },
@@ -1510,7 +1596,8 @@ function buildReminderCenter(el, reminders, error, act, sessions, openChild, jum
1510
1596
  error !== '' ? el('p', { className: 'sap-notice' }, error) : null,
1511
1597
  pendingList === null && handledList === null
1512
1598
  ? el('p', { className: 'sap-desc' }, ZH.reminderEmpty)
1513
- : [pendingList, handledList]
1599
+ : [pendingList, handledList],
1600
+ archivedSection
1514
1601
  )
1515
1602
  )
1516
1603
  }
@@ -1577,10 +1664,20 @@ function reminderJumpButton(el, jump, disabled, onClick) {
1577
1664
  )
1578
1665
  }
1579
1666
 
1667
+ // 会话存在性探测(孤儿灰置):跳转引用指向已删会话(binding 拿不到)时灰置跳转
1668
+ // 按钮——回溯安全,不点进不可达会话。空 id/异常一律视为不可用(fail-soft)。
1669
+ function sessionExists(sessions, id) {
1670
+ if (typeof id !== 'string' || id === '') return false
1671
+ try { return sessions.binding(id) !== undefined } catch { return false }
1672
+ }
1673
+
1580
1674
  // 未采纳条目五字段区:主会话/子会话可点击跳转,任务/结果/说明只读展示。
1581
1675
  function buildUnadoptedFields(el, r, sessions, openChild) {
1676
+ const childId = r.childSessionId !== null && r.childSessionId !== undefined && r.childSessionId !== '' ? r.childSessionId : r.childId
1582
1677
  const parentJump = reminderJumpText(sessions, r.sessionId, ZH.reminderParentLabel)
1583
- const childJump = reminderJumpText(sessions, r.childSessionId !== null && r.childSessionId !== undefined && r.childSessionId !== '' ? r.childSessionId : r.childId, ZH.reminderChildLabel)
1678
+ const childJump = reminderJumpText(sessions, childId, ZH.reminderChildLabel)
1679
+ const parentGone = !sessionExists(sessions, r.sessionId)
1680
+ const childGone = !sessionExists(sessions, childId)
1584
1681
  const openParent = () => { try { sessions.open(r.sessionId) } catch { /* 会话不可用:静默 */ } }
1585
1682
  const childAddress = typeof r.sessionId === 'string' && r.sessionId !== '' && typeof r.childSessionId === 'string' && r.childSessionId !== ''
1586
1683
  ? { parentSessionId: r.sessionId, childSessionId: r.childSessionId, mode: reminderChildMode(r.childMode) }
@@ -1588,8 +1685,8 @@ function buildUnadoptedFields(el, r, sessions, openChild) {
1588
1685
  const jumpChild = () => { if (childAddress !== null) { try { openChild(childAddress) } catch { /* 子会话不可用:静默 */ } } }
1589
1686
  const excerpt = typeof r.promptExcerpt === 'string' && r.promptExcerpt !== '' ? '「' + r.promptExcerpt + '」' : ZH.reminderNoExcerpt
1590
1687
  return el('div', { className: 'sap-reminderFields' },
1591
- reminderJumpButton(el, parentJump, typeof r.sessionId !== 'string' || r.sessionId === '', openParent),
1592
- reminderJumpButton(el, childJump, childAddress === null, jumpChild),
1688
+ reminderJumpButton(el, parentJump, parentGone, openParent),
1689
+ reminderJumpButton(el, childJump, childAddress === null || childGone, jumpChild),
1593
1690
  reminderFieldLine(el, ZH.reminderTaskLabel, excerpt),
1594
1691
  reminderFieldLine(el, ZH.reminderResultLabel, reminderResultText(r)),
1595
1692
  reminderFieldLine(el, ZH.reminderNoteLabel, r.detail)
@@ -1599,10 +1696,11 @@ function buildUnadoptedFields(el, r, sessions, openChild) {
1599
1696
  // 未采纳条目按钮三件套:查看子会话(跳转,恒保留)/ 标记已读(未读显示)/ 关闭。
1600
1697
  // 已终态条目隐藏标记已读与关闭(只留只读跳转),防对终态条目重复处置产生重复审计;
1601
1698
  // 提醒上的处置只改变提醒自身状态(已读/终态),不改变采纳判定——采纳与否由事件流自动汇总。
1602
- function buildUnadoptedActions(el, r, act, childAddress, jumpChild) {
1699
+ // 子会话已删(孤儿)时查看按钮灰置(回溯安全)。
1700
+ function buildUnadoptedActions(el, r, act, childAddress, jumpChild, childGone) {
1603
1701
  const terminated = typeof r.action === 'string' && r.action !== ''
1604
1702
  return el('div', { className: 'sap-reminderActions' }, [
1605
- el('button', { type: 'button', className: 'sap-detailToggle', disabled: childAddress === null, onClick: jumpChild }, ZH.reminderViewChild),
1703
+ el('button', { type: 'button', className: 'sap-detailToggle', disabled: childAddress === null || childGone === true, onClick: jumpChild }, ZH.reminderViewChild),
1606
1704
  terminated ? null : r.unread === true ? el('button', { type: 'button', className: 'sap-detailToggle', onClick: act(r.id, 'ack') }, ZH.reminderAck) : null,
1607
1705
  terminated ? null : el('button', { type: 'button', className: 'sap-dangerButton', onClick: act(r.id, 'dismiss') }, ZH.reminderDismiss)
1608
1706
  ])
@@ -1612,9 +1710,11 @@ function buildUnadoptedActions(el, r, act, childAddress, jumpChild) {
1612
1710
  function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1613
1711
  const severityCls = r.severity === 'P0' ? 'sap-reminderP0' : 'sap-reminderP1'
1614
1712
  const severityText = r.severity === 'P0' ? '紧急' : '注意'
1713
+ const childId = r.childSessionId !== null && r.childSessionId !== undefined && r.childSessionId !== '' ? r.childSessionId : r.childId
1615
1714
  const childAddress = typeof r.sessionId === 'string' && r.sessionId !== '' && typeof r.childSessionId === 'string' && r.childSessionId !== ''
1616
1715
  ? { parentSessionId: r.sessionId, childSessionId: r.childSessionId, mode: reminderChildMode(r.childMode) }
1617
1716
  : null
1717
+ const childGone = !sessionExists(sessions, childId)
1618
1718
  const jumpChild = () => { if (childAddress !== null) { try { openChild(childAddress) } catch { /* 子会话不可用:静默 */ } } }
1619
1719
  return el('li', { key: String(r.id), className: 'sap-rowCard' + (r.unread === true ? ' sap-reminderUnread' : '') },
1620
1720
  el('div', { className: 'sap-reminderHead' },
@@ -1622,7 +1722,7 @@ function buildUnadoptedReminderRow(el, r, act, sessions, openChild) {
1622
1722
  el('span', { className: 'sap-reminderTitle', title: r.title }, r.title)
1623
1723
  ),
1624
1724
  buildUnadoptedFields(el, r, sessions, openChild),
1625
- buildUnadoptedActions(el, r, act, childAddress, jumpChild),
1725
+ buildUnadoptedActions(el, r, act, childAddress, jumpChild, childGone),
1626
1726
  typeof r.action === 'string' && r.action !== ''
1627
1727
  ? el('p', { className: 'sap-metaValueDefault' }, ZH.reminderActed.replace('{action}', ZH.reminderActionZh[r.action] || r.action))
1628
1728
  : null
@@ -1794,7 +1894,8 @@ function collectDispatchRecords(nodes) {
1794
1894
  request: readDispatchRequest(node),
1795
1895
  result: readDispatchResult(node),
1796
1896
  argsSummary: summarizeArgsRaw(node.call.argsRaw),
1797
- contentFirstLine: firstContentLine(node.content)
1897
+ contentFirstLine: firstContentLine(node.content),
1898
+ imageText: node.meta !== null && typeof node.meta === 'object' && typeof node.meta.imageText === 'string' ? node.meta.imageText : ''
1798
1899
  })
1799
1900
  }
1800
1901
  return records
@@ -2304,17 +2405,17 @@ function useBackgroundJobPoll(useState, useEffect, api, sessionId, callId, settl
2304
2405
  function makeDispatchToolview(el, chip, useState, useEffect) {
2305
2406
  function DispatchToolview(props) {
2306
2407
  const block = props && props.block; const request = readDispatchRequest(block); const result = readDispatchResult(block)
2307
- const settled = !!(block && typeof block === 'object' && 'kind' in block)
2308
- const trace = block && block.meta ? normalizeDecisionTrace(block.meta) : null
2408
+ const settled = !!(block && typeof block === 'object' && 'kind' in block); const trace = block && block.meta ? normalizeDecisionTrace(block.meta) : null
2309
2409
  const { eff, ignored, kindLabel } = resolveDispatchState(request, result, settled)
2310
2410
  const chips = buildDispatchChips(el, chip, eff, result, block, ignored, request)
2311
- const safetyLine = safetyLineText(trace); const toolLine = toolLineText(request, trace)
2312
- const metaLine = (safetyLine !== '' || toolLine !== '') ? el('div', { className: 'sap-toolMeta' },
2411
+ // 生效画像行并入 meta 行(B3 imageText 回读;旧结果按生效字段合成,空则不出现)。
2412
+ const safetyLine = safetyLineText(trace); const toolLine = toolLineText(request, trace); const imageText = imageTextOfBlock(block && typeof block === 'object' ? block.meta : null, eff)
2413
+ const metaLine = (safetyLine !== '' || toolLine !== '' || imageText !== '') ? el('div', { className: 'sap-toolMeta' },
2313
2414
  safetyLine !== '' ? el('span', { className: 'sap-toolMetaItem' }, safetyLine) : null,
2314
- toolLine !== '' ? el('span', { className: 'sap-toolMetaItem' }, toolLine) : null
2415
+ toolLine !== '' ? el('span', { className: 'sap-toolMetaItem' }, toolLine) : null,
2416
+ imageText !== '' ? el('span', { className: 'sap-toolMetaItem' }, imageText) : null
2315
2417
  ) : null
2316
2418
  const usage = usageFrom(result, block)
2317
- // 后台运行中按 callId 轮询 /ledger/jobs 配对结算(前台/continuable 静态终态)。
2318
2419
  const jobSettled = useBackgroundJobPoll(useState, useEffect, api, props.sessionId, props.callId, settled, request)
2319
2420
  const [open, setOpen] = useState(false); const suggestion = draftSuggestionFromDispatch(request, trace, result); const draft = useDraftCapture(useState, suggestion)
2320
2421
  // 摘要行只承载动态结果(成本/耗时/结束原因/缓存命中);静态配置(方案/预设/
@@ -3864,19 +3965,22 @@ function buildSectionReturn(el, s, actions, rows, addCard, reminderCenter, sessi
3864
3965
  ...notices,
3865
3966
  buildTabBar(el, s),
3866
3967
  buildTabZone(el, s, 'profiles',
3867
- buildSettingsCard(el, 1, '方案管理', '管理派发可用的方案:启用、编辑、删除,含成本与成功率', true, () => {}, [
3868
- el('p', { className: 'sap-intro' }, ZH.profilesIntro),
3869
- el('ul', { className: 'sap-rows' }, rows),
3870
- buildDraftRows(el, s, actions),
3871
- el('div', { className: 'sap-addBlock' },
3872
- s.adding ? addCard : el('div', { className: 'sap-addActions' },
3873
- el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
3968
+ el('div', { className: 'sap-zoneStack' },
3969
+ buildSettingsCard(el, 1, '方案管理', '管理派发可用的方案:启用、编辑、删除,含成本与成功率', true, () => {}, [
3970
+ el('p', { className: 'sap-intro' }, ZH.profilesIntro),
3971
+ el('ul', { className: 'sap-rows' }, rows),
3972
+ buildDraftRows(el, s, actions),
3973
+ el('div', { className: 'sap-addBlock' },
3974
+ s.adding ? addCard : el('div', { className: 'sap-addActions' },
3975
+ el('button', { className: 'sap-addButton', onClick: actions.openAdd }, '+ 新增 profile')
3976
+ )
3977
+ ),
3978
+ el('div', { className: 'sap-addActions', style: { marginTop: '4px' } },
3979
+ el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.resetAll }, ZH.resetAllBuiltins)
3874
3980
  )
3875
- ),
3876
- el('div', { className: 'sap-addActions', style: { marginTop: '4px' } },
3877
- el('button', { type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.resetAll }, ZH.resetAllBuiltins)
3878
- )
3879
- ], true)),
3981
+ ], true),
3982
+ buildMaintenanceCard(el, s, actions)
3983
+ )),
3880
3984
  buildTabZone(el, s, 'cost',
3881
3985
  buildSettingsCard(el, 2, '全局成本', '跨全部会话的派发成本汇总,按模型与方案拆分', true, () => {}, [
3882
3986
  buildCostObservabilitySection(el, s, actions)
@@ -3922,6 +4026,124 @@ function attachDraftActions(api, s, refresh, applyWrite, actions) {
3922
4026
  }
3923
4027
  }
3924
4028
 
4029
+ // ── 设置页:数据维护区(快照开关/单条查看 + 四区清理按钮组)───────────────────
4030
+
4031
+ // 快照区数据装载 hook:挂载拉一次,开关切换与清理后回读(单条查看状态同驻)。
4032
+ function useSnapshotData(useState, useEffect) {
4033
+ const [snapshots, setSnapshots] = useState({ snapshotEnabled: false, count: 0, cap: 200, entries: [] })
4034
+ const [snapshotView, setSnapshotView] = useState({})
4035
+ const [maintenanceNotice, setMaintenanceNotice] = useState('')
4036
+ const refreshSnapshots = () => {
4037
+ api('/snapshots').then((data) => {
4038
+ setSnapshots({
4039
+ snapshotEnabled: data !== null && typeof data === 'object' && data.snapshotEnabled === true,
4040
+ count: data !== null && typeof data === 'object' && typeof data.count === 'number' ? data.count : 0,
4041
+ cap: data !== null && typeof data === 'object' && typeof data.cap === 'number' ? data.cap : 200,
4042
+ entries: data !== null && typeof data === 'object' && Array.isArray(data.entries) ? data.entries : []
4043
+ })
4044
+ }).catch(() => {})
4045
+ }
4046
+ useEffect(() => { refreshSnapshots() }, [])
4047
+ return { snapshots, refreshSnapshots, snapshotView, setSnapshotView, maintenanceNotice, setMaintenanceNotice }
4048
+ }
4049
+
4050
+ // 数据维护动作:快照开关、单条查看(展开/收起)、四区清理(确认一次、结果 echo)。
4051
+ function attachMaintenanceActions(api, s, refresh, actions) {
4052
+ actions.toggleSnapshots = () => {
4053
+ api('/snapshots/set-enabled', { snapshotEnabled: !s.snapshots.snapshotEnabled })
4054
+ .then((data) => {
4055
+ if (data !== null && typeof data === 'object' && data.persisted === false) s.setPersistWarning(ZH.persistFail)
4056
+ else s.setPersistWarning('')
4057
+ s.refreshSnapshots()
4058
+ })
4059
+ .catch((err) => s.setError(String((err && err.message) || err)))
4060
+ }
4061
+ actions.viewSnapshot = (id) => () => {
4062
+ const next = { ...s.snapshotView }
4063
+ if (next[id] !== undefined) { delete next[id]; s.setSnapshotView(next); return }
4064
+ api('/snapshot?id=' + encodeURIComponent(id))
4065
+ .then((data) => {
4066
+ if (data !== null && typeof data === 'object' && data.ok === true && data.snapshot !== undefined) {
4067
+ s.setSnapshotView({ ...s.snapshotView, [id]: data.snapshot })
4068
+ }
4069
+ })
4070
+ .catch(() => {})
4071
+ }
4072
+ actions.cleanZone = (zone, label) => () => {
4073
+ if (!window.confirm(ZH.maintenanceConfirm.replace('{zone}', label))) return
4074
+ api('/maintenance/clean', { zone })
4075
+ .then((data) => {
4076
+ const count = data !== null && typeof data === 'object' && typeof data.count === 'number' ? data.count : 0
4077
+ s.setMaintenanceNotice(ZH.maintenanceDone.replace('{zone}', label).replace('{count}', String(count)))
4078
+ s.refreshSnapshots()
4079
+ refresh()
4080
+ })
4081
+ .catch((err) => s.setError(ZH.maintenanceCleanErr + String((err && err.message) || err)))
4082
+ }
4083
+ }
4084
+
4085
+ // 单条快照只读体:persona 默认折叠(本地只读允许看原文,UI 不默认展开);其余
4086
+ // 字段等宽 JSON 展示。
4087
+ function snapshotViewBody(el, payload) {
4088
+ const cfg = payload !== null && typeof payload === 'object' && payload.config !== null && typeof payload.config === 'object' ? payload.config : {}
4089
+ const keys = Object.keys(cfg)
4090
+ const persona = typeof cfg.persona === 'string' ? cfg.persona : ''
4091
+ return el('div', { className: 'sap-reminderFields' },
4092
+ keys.length === 0 ? el('p', { className: 'sap-desc' }, ZH.snapshotEmptyConfig) : null,
4093
+ keys.map((key) => {
4094
+ if (key === 'persona') {
4095
+ return el('details', { key: 'snap-persona', className: 'sap-customized' },
4096
+ el('summary', { className: 'sap-customizedSummary' }, ZH.snapshotPersonaFold.replace('{n}', String(persona.length))),
4097
+ el('pre', { className: 'sap-metaValueDefault' }, persona)
4098
+ )
4099
+ }
4100
+ return reminderFieldLine(el, key, formatGateValue(cfg[key]))
4101
+ })
4102
+ )
4103
+ }
4104
+
4105
+ // 快照开关说明三行分层:首行主说明,后两行小字次要色(数据落点与关闭语义)。
4106
+ function snapshotSwitchDescRows(el) {
4107
+ return ZH.snapshotSwitchDesc.split('\n').filter((line) => line !== '').map((line, index) => el('p', { key: 'snap-desc-' + index, className: index === 0 ? 'sap-desc' : 'sap-descMinor' }, line))
4108
+ }
4109
+
4110
+ // 数据维护卡(方案管理尾部):快照开关 + B7 文案 + 数量/护栏提示 + 单条查看 +
4111
+ // 维护按钮组(四区各自独立、一次确认、结果 echo、DOM 一行)。
4112
+ function buildMaintenanceCard(el, s, actions) {
4113
+ const snap = s.snapshots
4114
+ const atCap = snap.count >= snap.cap && snap.count > 0
4115
+ const entries = Array.isArray(snap.entries) ? snap.entries : []
4116
+ const zoneButtons = [
4117
+ { zone: 'candidates', label: ZH.cleanCandidates },
4118
+ { zone: 'snapshots', label: ZH.cleanSnapshots },
4119
+ { zone: 'reminders', label: ZH.cleanReminders },
4120
+ { zone: 'stats', label: ZH.cleanStats }
4121
+ ].map((z) => el('button', { key: 'clean-' + z.zone, type: 'button', className: 'sap-secondaryButton', style: smallBtn, onClick: actions.cleanZone(z.zone, z.label) }, z.label))
4122
+ return buildSettingsCard(el, 11, ZH.maintenanceCardTitle, ZH.maintenanceCardDesc, true, () => {}, [
4123
+ el('label', { className: 'sap-switch' },
4124
+ el('input', { type: 'checkbox', checked: snap.snapshotEnabled === true, onChange: actions.toggleSnapshots }),
4125
+ el('span', null, '配置快照')
4126
+ ),
4127
+ el('div', { className: 'sap-maintenanceSwitchDesc' }, snapshotSwitchDescRows(el)),
4128
+ el('p', { className: 'sap-desc' },
4129
+ snap.count > 0 ? ZH.snapshotCount.replace('{n}', String(snap.count)).replace('{cap}', String(snap.cap)) : ZH.snapshotNone
4130
+ ),
4131
+ atCap ? el('p', { className: 'sap-notice' }, ZH.snapshotCapHint) : null,
4132
+ entries.map((entry) => {
4133
+ const payload = s.snapshotView[entry.id]
4134
+ return el('div', { key: 'snap-' + entry.id, className: 'sap-candidateSettingsPanel' },
4135
+ el('div', { className: 'sap-candidateSettingsRow' },
4136
+ el('b', null, ZH.snapshotMeta.replace('{id}', entry.id).replace('{ts}', formatMonthDayTime(entry.ts))),
4137
+ el('button', { type: 'button', className: 'sap-candidateSettingsEdit', onClick: actions.viewSnapshot(entry.id) }, payload !== undefined ? ZH.snapshotViewClose : ZH.snapshotView)
4138
+ ),
4139
+ payload !== undefined ? snapshotViewBody(el, payload) : null
4140
+ )
4141
+ }),
4142
+ s.maintenanceNotice !== '' ? el('p', { className: 'sap-notice' }, s.maintenanceNotice) : null,
4143
+ el('div', { className: 'sap-candidateSettingsActions' }, zoneButtons)
4144
+ ], true)
4145
+ }
4146
+
3925
4147
  function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, ReminderCenter, getSessions) {
3926
4148
  function ProfilesSection() {
3927
4149
  const [profiles, setProfiles] = useState([]); const [options, setOptions] = useState({ models: [], efforts: {}, presets: [] }); const [enabled, setEnabled] = useState(true)
@@ -3934,8 +4156,8 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, Re
3934
4156
  const [candidateSettingsForm, setCandidateSettingsForm] = useState({ candidateMode: 'auto', candidateTtlChoice: '24', customDays: '', error: '' }); const [adviceDetails, setAdviceDetails] = useState({}); const [detailOpen, setDetailOpen] = useState({})
3935
4157
  const [activeTab, setActiveTab] = useState('profiles'); const [highlightId, setHighlightId] = useState(null); const [adviceOpen, setAdviceOpen] = useState({}); const [auditDetailOpen, setAuditDetailOpen] = useState(false); const [auditReminders, setAuditReminders] = useState([])
3936
4158
  const [evolutionStatus, setEvolutionStatus] = useState({ applyEnabled: false, canaryEnabled: false, reasons: [] }); const [evolutionAssets, setEvolutionAssets] = useState([])
3937
- const [applyModal, setApplyModal] = useState({ profileKey: '', candidateId: '', open: false, error: '' }); const [rollbackModal, setRollbackModal] = useState({ assetId: '', open: false, error: '' })
3938
- const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, pluginVersion, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, drafts, advice, evolutionCandidates, adviceGlobal, costSummary, draftPreviews, candidateOpen, candidateSelected, candidateSafetyOpen, candidateSettings, candidateSettingsOpen, candidateSettingsForm, adviceDetails, detailOpen, activeTab, highlightId, adviceOpen, auditDetailOpen, auditReminders, evolutionStatus, evolutionAssets, applyModal, rollbackModal, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setPluginVersion, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice, setDrafts, setAdvice, setEvolutionCandidates, setAdviceGlobal, setCostSummary, setDraftPreviews, setCandidateOpen, setCandidateSelected, setCandidateSafetyOpen, setCandidateSettings, setCandidateSettingsOpen, setCandidateSettingsForm, setAdviceDetails, setDetailOpen, setActiveTab, setHighlightId, setAdviceOpen, setAuditDetailOpen, setAuditReminders, setEvolutionStatus, setEvolutionAssets, setApplyModal, setRollbackModal }
4159
+ const [applyModal, setApplyModal] = useState({ profileKey: '', candidateId: '', open: false, error: '' }); const [rollbackModal, setRollbackModal] = useState({ assetId: '', open: false, error: '' }); const snapshotCtl = useSnapshotData(useState, useEffect)
4160
+ const s = { profiles, options, enabled, adding, editingId, error, savedNotice, persistWarning, form, tools, versionWarnings, pluginVersion, audit, escapeEnabled, escapePresets, escapeInput, evolutionAdvice, drafts, advice, evolutionCandidates, adviceGlobal, costSummary, draftPreviews, candidateOpen, candidateSelected, candidateSafetyOpen, candidateSettings, candidateSettingsOpen, candidateSettingsForm, adviceDetails, detailOpen, activeTab, highlightId, adviceOpen, auditDetailOpen, auditReminders, evolutionStatus, evolutionAssets, applyModal, rollbackModal, snapshots: snapshotCtl.snapshots, snapshotView: snapshotCtl.snapshotView, maintenanceNotice: snapshotCtl.maintenanceNotice, refreshSnapshots: snapshotCtl.refreshSnapshots, setSnapshotView: snapshotCtl.setSnapshotView, setMaintenanceNotice: snapshotCtl.setMaintenanceNotice, setProfiles, setOptions, setEnabled, setAdding, setEditingId, setError, setSavedNotice, setPersistWarning, setForm, setTools, setVersionWarnings, setPluginVersion, setAudit, setEscapeEnabled, setEscapePresets, setEscapeInput, setEvolutionAdvice, setDrafts, setAdvice, setEvolutionCandidates, setAdviceGlobal, setCostSummary, setDraftPreviews, setCandidateOpen, setCandidateSelected, setCandidateSafetyOpen, setCandidateSettings, setCandidateSettingsOpen, setCandidateSettingsForm, setAdviceDetails, setDetailOpen, setActiveTab, setHighlightId, setAdviceOpen, setAuditDetailOpen, setAuditReminders, setEvolutionStatus, setEvolutionAssets, setApplyModal, setRollbackModal }
3939
4161
  const refresh = useCallback(() => {
3940
4162
  api('/list')
3941
4163
  .then((data) => {
@@ -3960,13 +4182,9 @@ function makeProfilesSection(el, api, chip, useState, useEffect, useCallback, Re
3960
4182
  }, [])
3961
4183
  const loadEfforts = makeEffortsLoader(api, setOptions); const loadTools = makeToolsLoader(api, setTools)
3962
4184
  const loadVersions = useCallback(() => makeVersionsLoader(api, setVersionWarnings, setPluginVersion)(), []) // 只挂载时拉一次:稳定引用防 effect 依赖恒变
3963
- useEffect(() => {
3964
- refresh()
3965
- loadOptions()
3966
- loadVersions()
3967
- }, [refresh, loadOptions, loadVersions])
4185
+ useEffect(() => { refresh(); loadOptions(); loadVersions() }, [refresh, loadOptions, loadVersions])
3968
4186
  const applyWrite = makeApplyWrite(s); const actions = makeProfilesActions(api, s, refresh, applyWrite, loadEfforts, loadTools)
3969
- attachDraftActions(api, s, refresh, applyWrite, actions)
4187
+ attachDraftActions(api, s, refresh, applyWrite, actions); attachMaintenanceActions(api, s, refresh, actions)
3970
4188
  const rows = buildProfileRows(el, chip, s, actions); const toolSection = buildToolSection(el, s, actions)
3971
4189
  return buildSectionReturn(el, s, actions, rows, buildAddCard(el, s, actions, toolSection), ReminderCenter, sessionsSnapshot(getSessions))
3972
4190
  }
@@ -4952,6 +5170,7 @@ function buildLedgerRowHead(el, record, profileNames, proc, open, onToggle) {
4952
5170
  const costText = rejected ? '—' : (!settled ? ZH.ledgerCostRunning : (costInfo !== null ? '≈' + costInfo.actual + ' 元' : ''))
4953
5171
  const profileId = recordProfile(record)
4954
5172
  const profileText = profileId !== '' ? profileDisplay(profileId, profileNames) : ZH.inlineProfile
5173
+ const imageText = imageTextOfBlock({ imageText: record.imageText }, record.trace !== null && typeof record.trace === 'object' ? record.trace.effective : null)
4955
5174
  const toolsText = ledgerToolsCountOf(record, proc)
4956
5175
  const interveneText = proc !== null && proc.interventions > 0 ? ZH.ledgerInterventionBadge.replace('{n}', String(proc.interventions)) : ''
4957
5176
  return el('button', {
@@ -4962,6 +5181,9 @@ function buildLedgerRowHead(el, record, profileNames, proc, open, onToggle) {
4962
5181
  },
4963
5182
  el('span', { className: 'sap-ledgerTime' }, formatClockTime(record.time)),
4964
5183
  el('span', { className: 'sap-ledgerMeta' }, profileText),
5184
+ imageText !== ''
5185
+ ? el('span', { className: 'sap-ledgerMeta sap-ledgerImageText' }, imageText)
5186
+ : null,
4965
5187
  el('span', { className: 'sap-ledgerStatus ' + status.cls }, status.label),
4966
5188
  costText !== '' ? el('span', { className: 'sap-ledgerCostText' }, costText) : null,
4967
5189
  toolsText !== '' ? el('span', { className: 'sap-ledgerToolsText' }, toolsText) : null,
@@ -0,0 +1,33 @@
1
+ // lib/core/cleanup.mjs — 启动与卸载清理辅助(从 index.mjs 移出以守装配文件行门)。
2
+ // 卸载清理:本插件 3 个数据文件 + orchestrator-v2 预设目录改名备份(effect 清理器
3
+ // 在禁用/热重载/退出时都可能触发;硬删除会静默丢用户数据)。启动清理:删上个
4
+ // 生命周期留下的 .removed-* 备份残留(只清本插件前缀,防热重载反复产生新备份
5
+ // 积累);卸载后用户手动改回原名的场景不受影响。全部 fail-soft(改名/删除失败
6
+ // 不抛)。import-free(仅 node 内置),可被裸 CI 单测。
7
+
8
+ import { readdirSync, renameSync, rmSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ export function removeOwnedData(home) {
12
+ const stamp = Date.now();
13
+ for (const file of ['subagent-profiles.json', 'subagent-profiles.state.json', 'subagent-profiles.failed-traces.json']) {
14
+ try { renameSync(join(home, file), join(home, `${file}.removed-${stamp}`)); } catch { /* 源不存在(正常首启)等:best effort */ }
15
+ }
16
+ try { renameSync(join(home, '.agent-presets', 'orchestrator-v2'), join(home, '.agent-presets', `orchestrator-v2.removed-${stamp}`)); } catch { /* best effort */ }
17
+ }
18
+
19
+ export function cleanRemovedBackups(home) {
20
+ for (const dir of [home, join(home, '.agent-presets')]) {
21
+ let names;
22
+ try { names = readdirSync(dir); } catch { continue; }
23
+ for (const name of names) {
24
+ try {
25
+ if (name.startsWith('subagent-profiles.') && name.includes('.removed-')) {
26
+ rmSync(join(dir, name), { force: true });
27
+ } else if (name.startsWith('orchestrator-v2.removed-')) {
28
+ rmSync(join(dir, name), { recursive: true, force: true });
29
+ }
30
+ } catch { /* best effort */ }
31
+ }
32
+ }
33
+ }