@winspan/claude-forge 8.18.0 → 8.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/claudemd/resume-manager.d.ts.map +1 -1
  2. package/dist/claudemd/resume-manager.js +8 -7
  3. package/dist/claudemd/resume-manager.js.map +1 -1
  4. package/dist/cli/commands/agents.d.ts +3 -0
  5. package/dist/cli/commands/agents.d.ts.map +1 -0
  6. package/dist/cli/commands/agents.js +62 -0
  7. package/dist/cli/commands/agents.js.map +1 -0
  8. package/dist/cli/index.js +2 -0
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/core/storage/schema.sql +3 -0
  11. package/dist/core/storage/sqlite.d.ts +4 -0
  12. package/dist/core/storage/sqlite.d.ts.map +1 -1
  13. package/dist/core/storage/sqlite.js +24 -3
  14. package/dist/core/storage/sqlite.js.map +1 -1
  15. package/dist/daemon/handlers/stop.d.ts +3 -1
  16. package/dist/daemon/handlers/stop.d.ts.map +1 -1
  17. package/dist/daemon/handlers/stop.js +13 -2
  18. package/dist/daemon/handlers/stop.js.map +1 -1
  19. package/dist/daemon/index.d.ts.map +1 -1
  20. package/dist/daemon/index.js +1 -1
  21. package/dist/daemon/index.js.map +1 -1
  22. package/dist/daemon/routing-observer.d.ts +1 -0
  23. package/dist/daemon/routing-observer.d.ts.map +1 -1
  24. package/dist/daemon/routing-observer.js +29 -23
  25. package/dist/daemon/routing-observer.js.map +1 -1
  26. package/dist/intelligence/task-segmenter.d.ts +1 -0
  27. package/dist/intelligence/task-segmenter.d.ts.map +1 -1
  28. package/dist/intelligence/task-segmenter.js +10 -0
  29. package/dist/intelligence/task-segmenter.js.map +1 -1
  30. package/dist/web/server.d.ts +1 -0
  31. package/dist/web/server.d.ts.map +1 -1
  32. package/dist/web/server.js +832 -0
  33. package/dist/web/server.js.map +1 -1
  34. package/dist/web/static/index.html +786 -2
  35. package/package.json +1 -1
@@ -353,6 +353,10 @@
353
353
  <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
354
354
  注入
355
355
  </a>
356
+ <a onclick="nav('execution-trace')" id="nav-execution-trace">
357
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
358
+ 执行追踪
359
+ </a>
356
360
  <div class="nav-section-title">配置</div>
357
361
  <a onclick="nav('agents')" id="nav-agents">
358
362
  <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
@@ -523,6 +527,33 @@
523
527
  </div>
524
528
  </div>
525
529
 
530
+ <!-- Execution Trace -->
531
+ <div id="page-execution-trace" class="page">
532
+ <div class="toolbar">
533
+ <input class="search-box" id="trace-search" placeholder="搜索 prompt..." oninput="filterTraces()">
534
+ <select class="btn" id="trace-agent-filter" onchange="loadExecutionTraces()">
535
+ <option value="">全部 Agent/Skill</option>
536
+ </select>
537
+ <select class="btn" id="trace-obeyed-filter" onchange="loadExecutionTraces()">
538
+ <option value="">全部状态</option>
539
+ <option value="true">✓ 已遵守</option>
540
+ <option value="false">✗ 违抗</option>
541
+ </select>
542
+ <button class="btn" onclick="loadExecutionTraces()">↻ 刷新</button>
543
+ </div>
544
+ <div class="panel" style="margin-bottom:1rem">
545
+ <div class="panel-header"><span class="panel-title">实时状态</span></div>
546
+ <div class="panel-body">
547
+ <div id="trace-live-status" style="max-height:200px;overflow-y:auto;font-size:0.875rem"></div>
548
+ </div>
549
+ </div>
550
+ <div class="panel">
551
+ <div class="panel-body">
552
+ <div id="trace-list"></div>
553
+ </div>
554
+ </div>
555
+ </div>
556
+
526
557
  <!-- Agent Management -->
527
558
  <div id="page-agents" class="page">
528
559
  <div class="panel">
@@ -579,6 +610,8 @@
579
610
  <button class="btn" id="routing-tab-overview" onclick="routingTab('overview')" style="border-radius:0;border:none">总览</button>
580
611
  <button class="btn" id="routing-tab-events" onclick="routingTab('events')" style="border-radius:0;border:none">决策明细</button>
581
612
  <button class="btn" id="routing-tab-refusals" onclick="routingTab('refusals')" style="border-radius:0;border:none">违抗聚合</button>
613
+ <button class="btn" id="routing-tab-performance" onclick="routingTab('performance')" style="border-radius:0;border:none">性能分析</button>
614
+ <button class="btn" id="routing-tab-ai-optimization" onclick="routingTab('ai-optimization')" style="border-radius:0;border:none">AI 优化</button>
582
615
  <button class="btn" id="routing-tab-editor" onclick="routingTab('editor')" style="border-radius:0;border:none">路由调优</button>
583
616
  <button class="btn" id="routing-tab-experiments" onclick="routingTab('experiments')" style="border-radius:0;border:none">A/B 测试</button>
584
617
  <button class="btn" id="routing-tab-recommendations" onclick="routingTab('recommendations')" style="border-radius:0;border:none">规则推荐</button>
@@ -634,6 +667,57 @@
634
667
  </div>
635
668
  </div>
636
669
 
670
+ <!-- Performance subpanel -->
671
+ <div id="routing-sub-performance" style="display:none">
672
+ <div class="cards" id="routing-performance-cards"></div>
673
+ <div class="grid-2" style="margin-top:1.25rem">
674
+ <div class="panel">
675
+ <div class="panel-header"><span class="panel-title">每日路由趋势</span></div>
676
+ <div class="panel-body"><div class="chart-wrap"><canvas id="chart-routing-trend"></canvas></div></div>
677
+ </div>
678
+ <div class="panel">
679
+ <div class="panel-header"><span class="panel-title">平均真实执行耗时趋势</span></div>
680
+ <div class="panel-body"><div class="chart-wrap"><canvas id="chart-routing-latency"></canvas></div></div>
681
+ </div>
682
+ </div>
683
+ <div class="grid-2" style="margin-top:1.25rem">
684
+ <div class="panel">
685
+ <div class="panel-header"><span class="panel-title">Per-agent 指标</span></div>
686
+ <div class="panel-body">
687
+ <table>
688
+ <thead><tr><th>Agent</th><th>总次数</th><th>已判定</th><th>遵守率</th><th>违抗率</th><th>分类耗时</th><th>真实耗时</th></tr></thead>
689
+ <tbody id="routing-performance-by-agent"></tbody>
690
+ </table>
691
+ </div>
692
+ </div>
693
+ <div class="panel">
694
+ <div class="panel-header"><span class="panel-title">高违抗率榜单</span></div>
695
+ <div class="panel-body" id="routing-high-refusal"></div>
696
+ </div>
697
+ </div>
698
+ </div>
699
+
700
+ <!-- AI optimization subpanel -->
701
+ <div id="routing-sub-ai-optimization" style="display:none">
702
+ <div class="panel">
703
+ <div class="panel-header">
704
+ <span class="panel-title">AI 优化建议</span>
705
+ <div style="display:flex;gap:0.5rem;align-items:center">
706
+ <label style="font-size:0.875rem;color:var(--text-dim)">最小样本
707
+ <input id="ai-opt-min-attempts" type="number" value="10" min="1" max="100" style="width:72px;margin-left:0.25rem" class="btn">
708
+ </label>
709
+ <button class="btn" onclick="loadRoutingAIOptimization()">↻ 生成建议</button>
710
+ </div>
711
+ </div>
712
+ <div class="panel-body">
713
+ <div style="margin-bottom:0.75rem;font-size:0.875rem;color:var(--text-dim)">
714
+ 基于高违抗率 agent、规则推荐、执行追踪证据,由 AI 生成针对 agent / skill / routing rule 的优化建议。
715
+ </div>
716
+ <div id="routing-ai-optimization"></div>
717
+ </div>
718
+ </div>
719
+ </div>
720
+
637
721
  <!-- Editor subpanel (Phase 3 Feature 2, upgraded to Monaco in Phase 4 Feature 1) -->
638
722
  <div id="routing-sub-editor" style="display:none">
639
723
  <div class="panel">
@@ -742,6 +826,10 @@
742
826
  const API = '';
743
827
  let allEvents = [], allSessions = [], allInjections = [];
744
828
  let charts = {};
829
+ let traceStream = null;
830
+ let traceRefreshTimer = null;
831
+ let currentPatchPreview = null;
832
+ let currentAIOptimizationData = null;
745
833
 
746
834
  // === Navigation ===
747
835
  function nav(page) {
@@ -751,13 +839,14 @@ function nav(page) {
751
839
  const pageEl = document.getElementById('page-' + page);
752
840
  if (navEl) navEl.classList.add('active');
753
841
  if (pageEl) pageEl.classList.add('active');
754
- const titles = { dashboard:'仪表盘', sessions:'会话', events:'事件', injections:'注入', agents:'Agent 管理', skills:'Skill 管理', 'ai-config':'AI 配置', routing:'Agent 路由' };
842
+ const titles = { dashboard:'仪表盘', sessions:'会话', events:'事件', injections:'注入', 'execution-trace':'执行追踪', agents:'Agent 管理', skills:'Skill 管理', 'ai-config':'AI 配置', routing:'Agent 路由' };
755
843
  document.getElementById('topbar-title').textContent = titles[page] || page;
756
844
  closeDrawer();
757
845
  if (page === 'dashboard') loadDashboard();
758
846
  else if (page === 'sessions') loadSessions();
759
847
  else if (page === 'events') loadEvents();
760
848
  else if (page === 'injections') loadInjections();
849
+ else if (page === 'execution-trace') loadExecutionTraces();
761
850
  else if (page === 'agents') loadAgents();
762
851
  else if (page === 'skills') loadSkills();
763
852
  else if (page === 'ai-config') loadAIConfig();
@@ -1150,7 +1239,7 @@ let routingCurrentTab = 'overview';
1150
1239
 
1151
1240
  function routingTab(tab) {
1152
1241
  routingCurrentTab = tab;
1153
- ['overview', 'events', 'refusals', 'editor', 'experiments', 'recommendations'].forEach(t => {
1242
+ ['overview', 'events', 'refusals', 'performance', 'ai-optimization', 'editor', 'experiments', 'recommendations'].forEach(t => {
1154
1243
  const btn = document.getElementById('routing-tab-' + t);
1155
1244
  const sub = document.getElementById('routing-sub-' + t);
1156
1245
  if (btn) btn.classList.toggle('active', t === tab);
@@ -1171,6 +1260,8 @@ async function loadRouting() {
1171
1260
  if (routingCurrentTab === 'overview') return loadRoutingOverview();
1172
1261
  if (routingCurrentTab === 'events') return loadRoutingEvents();
1173
1262
  if (routingCurrentTab === 'refusals') return loadRoutingRefusals();
1263
+ if (routingCurrentTab === 'performance') return loadRoutingPerformance();
1264
+ if (routingCurrentTab === 'ai-optimization') return loadRoutingAIOptimization();
1174
1265
  if (routingCurrentTab === 'editor') return loadRoutingConfig();
1175
1266
  if (routingCurrentTab === 'experiments') {
1176
1267
  await loadExperimentsConfig();
@@ -1408,6 +1499,9 @@ async function viewAgent(name) {
1408
1499
  ${data.tools ? `<span class="badge badge-allow">${data.tools}</span>` : ''}
1409
1500
  </div>
1410
1501
  </div>
1502
+ <div style="margin-bottom:1rem;display:flex;gap:0.5rem">
1503
+ <button class="btn" onclick="viewAgentVersions('${name}')">📜 版本历史</button>
1504
+ </div>
1411
1505
  <div style="background:var(--bg-secondary);padding:1rem;border-radius:var(--radius-sm);max-height:60vh;overflow-y:auto">
1412
1506
  <pre style="margin:0;white-space:pre-wrap;font-size:0.875rem">${escapeHtml(data.content)}</pre>
1413
1507
  </div>
@@ -1491,6 +1585,87 @@ async function loadSkills() {
1491
1585
  }
1492
1586
  }
1493
1587
 
1588
+ async function viewAgentVersions(name) {
1589
+ try {
1590
+ const r = await fetch(API + `/api/agents/${name}/versions`);
1591
+ if (!r.ok) throw new Error('Failed to load versions');
1592
+ const data = await r.json();
1593
+
1594
+ if (data.versions.length === 0) {
1595
+ alert('暂无版本历史');
1596
+ return;
1597
+ }
1598
+
1599
+ const html = `
1600
+ <div style="margin-bottom:1rem">
1601
+ <h3>版本历史: ${name}</h3>
1602
+ <p style="color:var(--text-dim)">共 ${data.versions.length} 个备份版本</p>
1603
+ </div>
1604
+ <div style="max-height:60vh;overflow-y:auto">
1605
+ ${data.versions.map(v => `
1606
+ <div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.75rem;margin-bottom:0.5rem">
1607
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
1608
+ <div>
1609
+ <div style="font-weight:500">${new Date(v.mtime).toLocaleString('zh-CN')}</div>
1610
+ <div style="font-size:0.75rem;color:var(--text-dim)">${(v.size / 1024).toFixed(1)} KB</div>
1611
+ </div>
1612
+ <div style="display:flex;gap:0.25rem">
1613
+ <button class="btn" onclick="viewAgentVersion('${name}', '${v.timestamp}')">查看</button>
1614
+ <button class="btn btn-primary" onclick="rollbackAgent('${name}', '${v.timestamp}')">回滚</button>
1615
+ </div>
1616
+ </div>
1617
+ </div>
1618
+ `).join('')}
1619
+ </div>
1620
+ `;
1621
+ openDrawer(`版本历史: ${name}`, html);
1622
+ } catch (err) {
1623
+ alert('加载失败: ' + err.message);
1624
+ }
1625
+ }
1626
+
1627
+ async function viewAgentVersion(name, timestamp) {
1628
+ try {
1629
+ const r = await fetch(API + `/api/agents/${name}/versions/${encodeURIComponent(timestamp)}`);
1630
+ if (!r.ok) throw new Error('Failed to load version');
1631
+ const data = await r.json();
1632
+
1633
+ const html = `
1634
+ <div style="margin-bottom:1rem">
1635
+ <h3>历史版本: ${name}</h3>
1636
+ <p style="color:var(--text-dim)">${new Date(timestamp).toLocaleString('zh-CN')}</p>
1637
+ </div>
1638
+ <div style="background:var(--bg-secondary);padding:1rem;border-radius:var(--radius-sm);max-height:60vh;overflow-y:auto">
1639
+ <pre style="margin:0;white-space:pre-wrap;font-size:0.875rem">${escapeHtml(data.content)}</pre>
1640
+ </div>
1641
+ `;
1642
+ openDrawer(`历史版本: ${name}`, html);
1643
+ } catch (err) {
1644
+ alert('加载失败: ' + err.message);
1645
+ }
1646
+ }
1647
+
1648
+ async function rollbackAgent(name, timestamp) {
1649
+ if (!confirm(`确定要回滚到 ${new Date(timestamp).toLocaleString('zh-CN')} 的版本吗?\n\n当前版本会自动备份。`)) {
1650
+ return;
1651
+ }
1652
+
1653
+ try {
1654
+ const r = await fetch(API + `/api/agents/${name}/rollback`, {
1655
+ method: 'POST',
1656
+ headers: { 'Content-Type': 'application/json' },
1657
+ body: JSON.stringify({ timestamp }),
1658
+ });
1659
+ if (!r.ok) throw new Error('Failed to rollback');
1660
+ const data = await r.json();
1661
+ alert(`✓ 回滚成功!\n\n当前版本已备份到:\n${data.backup}`);
1662
+ closeDrawer();
1663
+ loadAgents();
1664
+ } catch (err) {
1665
+ alert('回滚失败: ' + err.message);
1666
+ }
1667
+ }
1668
+
1494
1669
  async function viewSkill(name) {
1495
1670
  try {
1496
1671
  const r = await fetch(API + `/api/skills/${name}`);
@@ -1506,6 +1681,9 @@ async function viewSkill(name) {
1506
1681
  <span class="badge ${data.source === 'official' ? 'badge-allow' : 'badge-warn'}">${data.source === 'official' ? '官方' : '用户'}</span>
1507
1682
  </div>
1508
1683
  </div>
1684
+ <div style="margin-bottom:1rem;display:flex;gap:0.5rem">
1685
+ <button class="btn" onclick="viewSkillVersions('${name}')">📜 版本历史</button>
1686
+ </div>
1509
1687
  <div style="background:var(--bg-secondary);padding:1rem;border-radius:var(--radius-sm);max-height:60vh;overflow-y:auto">
1510
1688
  <pre style="margin:0;white-space:pre-wrap;font-size:0.875rem">${escapeHtml(data.content)}</pre>
1511
1689
  </div>
@@ -1516,6 +1694,87 @@ async function viewSkill(name) {
1516
1694
  }
1517
1695
  }
1518
1696
 
1697
+ async function viewSkillVersions(name) {
1698
+ try {
1699
+ const r = await fetch(API + `/api/skills/${name}/versions`);
1700
+ if (!r.ok) throw new Error('Failed to load versions');
1701
+ const data = await r.json();
1702
+
1703
+ if (data.versions.length === 0) {
1704
+ alert('暂无版本历史');
1705
+ return;
1706
+ }
1707
+
1708
+ const html = `
1709
+ <div style="margin-bottom:1rem">
1710
+ <h3>版本历史: ${name}</h3>
1711
+ <p style="color:var(--text-dim)">共 ${data.versions.length} 个备份版本</p>
1712
+ </div>
1713
+ <div style="max-height:60vh;overflow-y:auto">
1714
+ ${data.versions.map(v => `
1715
+ <div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.75rem;margin-bottom:0.5rem">
1716
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
1717
+ <div>
1718
+ <div style="font-weight:500">${new Date(v.mtime).toLocaleString('zh-CN')}</div>
1719
+ <div style="font-size:0.75rem;color:var(--text-dim)">${(v.size / 1024).toFixed(1)} KB</div>
1720
+ </div>
1721
+ <div style="display:flex;gap:0.25rem">
1722
+ <button class="btn" onclick="viewSkillVersion('${name}', '${v.timestamp}')">查看</button>
1723
+ <button class="btn btn-primary" onclick="rollbackSkill('${name}', '${v.timestamp}')">回滚</button>
1724
+ </div>
1725
+ </div>
1726
+ </div>
1727
+ `).join('')}
1728
+ </div>
1729
+ `;
1730
+ openDrawer(`版本历史: ${name}`, html);
1731
+ } catch (err) {
1732
+ alert('加载失败: ' + err.message);
1733
+ }
1734
+ }
1735
+
1736
+ async function viewSkillVersion(name, timestamp) {
1737
+ try {
1738
+ const r = await fetch(API + `/api/skills/${name}/versions/${encodeURIComponent(timestamp)}`);
1739
+ if (!r.ok) throw new Error('Failed to load version');
1740
+ const data = await r.json();
1741
+
1742
+ const html = `
1743
+ <div style="margin-bottom:1rem">
1744
+ <h3>历史版本: ${name}</h3>
1745
+ <p style="color:var(--text-dim)">${new Date(timestamp).toLocaleString('zh-CN')}</p>
1746
+ </div>
1747
+ <div style="background:var(--bg-secondary);padding:1rem;border-radius:var(--radius-sm);max-height:60vh;overflow-y:auto">
1748
+ <pre style="margin:0;white-space:pre-wrap;font-size:0.875rem">${escapeHtml(data.content)}</pre>
1749
+ </div>
1750
+ `;
1751
+ openDrawer(`历史版本: ${name}`, html);
1752
+ } catch (err) {
1753
+ alert('加载失败: ' + err.message);
1754
+ }
1755
+ }
1756
+
1757
+ async function rollbackSkill(name, timestamp) {
1758
+ if (!confirm(`确定要回滚到 ${new Date(timestamp).toLocaleString('zh-CN')} 的版本吗?\n\n当前版本会自动备份。`)) {
1759
+ return;
1760
+ }
1761
+
1762
+ try {
1763
+ const r = await fetch(API + `/api/skills/${name}/rollback`, {
1764
+ method: 'POST',
1765
+ headers: { 'Content-Type': 'application/json' },
1766
+ body: JSON.stringify({ timestamp }),
1767
+ });
1768
+ if (!r.ok) throw new Error('Failed to rollback');
1769
+ const data = await r.json();
1770
+ alert(`✓ 回滚成功!\n\n当前版本已备份到:\n${data.backup}`);
1771
+ closeDrawer();
1772
+ loadSkills();
1773
+ } catch (err) {
1774
+ alert('回滚失败: ' + err.message);
1775
+ }
1776
+ }
1777
+
1519
1778
  async function editSkill(name) {
1520
1779
  try {
1521
1780
  const r = await fetch(API + `/api/skills/${name}`);
@@ -1570,6 +1829,258 @@ function escapeHtml(text) {
1570
1829
  return div.innerHTML;
1571
1830
  }
1572
1831
 
1832
+ // === Execution Trace ===
1833
+ let allTraces = [];
1834
+
1835
+ async function loadExecutionTraces() {
1836
+ try {
1837
+ const agentFilter = document.getElementById('trace-agent-filter').value;
1838
+ const obeyedFilter = document.getElementById('trace-obeyed-filter').value;
1839
+ const params = new URLSearchParams({ limit: '100' });
1840
+ if (agentFilter) params.set('agent', agentFilter);
1841
+ if (obeyedFilter) params.set('obeyed', obeyedFilter);
1842
+
1843
+ const r = await fetch(API + '/api/execution-trace?' + params.toString());
1844
+ if (!r.ok) throw new Error('Failed to load traces');
1845
+ const data = await r.json();
1846
+ allTraces = data.traces;
1847
+
1848
+ // Populate agent filter dropdown
1849
+ const agentSet = new Set();
1850
+ allTraces.forEach(t => {
1851
+ if (t.routedToName) agentSet.add(t.routedToName);
1852
+ });
1853
+ const agentSelect = document.getElementById('trace-agent-filter');
1854
+ const currentAgent = agentSelect.value;
1855
+ agentSelect.innerHTML = '<option value="">全部 Agent/Skill</option>' +
1856
+ Array.from(agentSet).sort().map(a =>
1857
+ `<option value="${a}" ${a === currentAgent ? 'selected' : ''}>${a}</option>`
1858
+ ).join('');
1859
+
1860
+ renderTraces();
1861
+ ensureTraceStream();
1862
+ } catch (err) {
1863
+ console.error('Failed to load traces:', err);
1864
+ }
1865
+ }
1866
+
1867
+ function ensureTraceStream() {
1868
+ if (traceStream) return;
1869
+ traceStream = new EventSource(API + '/api/execution-trace/stream');
1870
+ traceStream.onmessage = (e) => {
1871
+ try {
1872
+ const data = JSON.parse(e.data);
1873
+ if (data.type === 'execution-status') {
1874
+ appendLiveStatus(data);
1875
+ if (traceRefreshTimer) clearTimeout(traceRefreshTimer);
1876
+ traceRefreshTimer = setTimeout(() => loadExecutionTraces(), 500);
1877
+ }
1878
+ } catch (err) {
1879
+ console.error('Failed to parse SSE:', err);
1880
+ }
1881
+ };
1882
+ traceStream.onerror = () => {
1883
+ traceStream.close();
1884
+ traceStream = null;
1885
+ };
1886
+ }
1887
+
1888
+ function appendLiveStatus(data) {
1889
+ const statusBox = document.getElementById('trace-live-status');
1890
+ if (!statusBox) return;
1891
+ const time = new Date(data.timestamp).toLocaleTimeString('zh-CN');
1892
+ const statusBadge = data.status === 'routing' ? '<span class="badge badge-info">正在路由</span>'
1893
+ : data.status === 'executing' ? '<span class="badge badge-warn">正在执行</span>'
1894
+ : data.status === 'completed' ? '<span class="badge badge-allow">已完成</span>'
1895
+ : '<span class="badge badge-block">失败</span>';
1896
+ const item = `<div style="padding:0.5rem;border-bottom:1px solid var(--border)">${time} · Session ${data.sessionId.slice(0,8)} · ${statusBadge}${data.tool ? ` · ${data.tool}` : ''}${data.error ? ` · ${data.error}` : ''}</div>`;
1897
+ statusBox.innerHTML = item + statusBox.innerHTML;
1898
+ const children = statusBox.children;
1899
+ while (children.length > 20) statusBox.removeChild(children[children.length - 1]);
1900
+ }
1901
+
1902
+ function renderTraces() {
1903
+ const searchTerm = document.getElementById('trace-search').value.toLowerCase();
1904
+ const filtered = allTraces.filter(t =>
1905
+ !searchTerm || t.prompt.toLowerCase().includes(searchTerm)
1906
+ );
1907
+
1908
+ const list = document.getElementById('trace-list');
1909
+ if (filtered.length === 0) {
1910
+ list.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-dim)">暂无执行追踪记录</div>';
1911
+ return;
1912
+ }
1913
+
1914
+ const items = filtered.map(t => {
1915
+ const time = new Date(t.timestamp).toLocaleString('zh-CN');
1916
+ const obeyedBadge = t.obeyed === 1
1917
+ ? '<span class="badge badge-allow">✓ 遵守</span>'
1918
+ : t.obeyed === 0
1919
+ ? '<span class="badge badge-block">✗ 违抗</span>'
1920
+ : '<span class="badge badge-info">— 未路由</span>';
1921
+
1922
+ const routeBadge = t.routedToName
1923
+ ? `<span class="badge badge-info">${t.routedToType === 'agent' ? '🤖' : '⚡'} ${t.routedToName}</span>`
1924
+ : '<span class="badge badge-warn">无路由</span>';
1925
+
1926
+ const intentBadge = t.intent.taskType
1927
+ ? `<span class="badge" style="background:var(--bg-secondary)">${t.intent.taskType}</span>`
1928
+ : '';
1929
+
1930
+ const firstToolBadge = t.firstTool
1931
+ ? `<span class="badge" style="background:var(--bg-secondary)">首工具: ${t.firstTool}</span>`
1932
+ : '';
1933
+
1934
+ const classificationBadge = t.fallbackUsed
1935
+ ? '<span class="badge badge-warn">正则兜底</span>'
1936
+ : t.classificationMs
1937
+ ? `<span class="badge badge-allow">AI 分类 (${t.classificationMs}ms)</span>`
1938
+ : '';
1939
+
1940
+ const executionBadge = t.totalExecutionMs
1941
+ ? `<span class="badge" style="background:var(--purple-soft);color:var(--purple)">真实耗时: ${t.totalExecutionMs}ms</span>`
1942
+ : '';
1943
+
1944
+ const completionBadge = t.completionReason
1945
+ ? `<span class="badge" style="background:var(--bg-secondary)">完成来源: ${t.completionReason}</span>`
1946
+ : '';
1947
+
1948
+ return `
1949
+ <div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:1rem;margin-bottom:0.75rem;cursor:pointer;transition:background 0.2s"
1950
+ onclick="viewTraceDetails('${t.sessionId}')"
1951
+ onmouseover="this.style.background='var(--bg-secondary)'"
1952
+ onmouseout="this.style.background='transparent'">
1953
+ <div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:0.5rem">
1954
+ <div style="flex:1">
1955
+ <div style="font-weight:500;margin-bottom:0.25rem">${escapeHtml(t.prompt.slice(0, 100))}${t.prompt.length > 100 ? '...' : ''}</div>
1956
+ <div style="font-size:0.75rem;color:var(--text-dim)">${time} · Session: ${t.sessionId.slice(0, 8)}</div>
1957
+ </div>
1958
+ <div style="display:flex;gap:0.25rem;flex-wrap:wrap;justify-content:flex-end">
1959
+ ${obeyedBadge}
1960
+ ${routeBadge}
1961
+ </div>
1962
+ </div>
1963
+ <div style="display:flex;gap:0.5rem;flex-wrap:wrap;font-size:0.75rem">
1964
+ ${intentBadge}
1965
+ ${firstToolBadge}
1966
+ ${classificationBadge}
1967
+ ${executionBadge}
1968
+ ${completionBadge}
1969
+ ${t.taskChain.length > 0 ? `<span class="badge badge-info">子 Agent: ${t.taskChain.join(' → ')}</span>` : ''}
1970
+ </div>
1971
+ </div>
1972
+ `;
1973
+ }).join('');
1974
+
1975
+ list.innerHTML = items;
1976
+ }
1977
+
1978
+ function filterTraces() {
1979
+ renderTraces();
1980
+ }
1981
+
1982
+ async function viewTraceDetails(sessionId) {
1983
+ try {
1984
+ const r = await fetch(API + `/api/execution-trace/${sessionId}`);
1985
+ if (!r.ok) throw new Error('Failed to load trace details');
1986
+ const data = await r.json();
1987
+
1988
+ const routing = data.routing;
1989
+ const obeyedText = routing.obeyed === 1
1990
+ ? '<span class="badge badge-allow">✓ 遵守</span>'
1991
+ : routing.obeyed === 0
1992
+ ? '<span class="badge badge-block">✗ 违抗</span>'
1993
+ : '<span class="badge badge-info">— 未路由</span>';
1994
+
1995
+ const html = `
1996
+ <div style="margin-bottom:1.5rem">
1997
+ <h3>执行追踪详情</h3>
1998
+ <div style="font-size:0.875rem;color:var(--text-dim);margin-top:0.5rem">Session: ${sessionId}</div>
1999
+ </div>
2000
+
2001
+ <div style="margin-bottom:1.5rem">
2002
+ <h4 style="margin-bottom:0.5rem">用户 Prompt</h4>
2003
+ <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2004
+ ${escapeHtml(routing.prompt)}
2005
+ </div>
2006
+ </div>
2007
+
2008
+ <div style="margin-bottom:1.5rem">
2009
+ <h4 style="margin-bottom:0.5rem">AI 意图分类</h4>
2010
+ <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2011
+ <div><strong>任务类型:</strong> ${routing.intent.taskType || '—'}</div>
2012
+ <div><strong>复杂度:</strong> ${routing.intent.complexity || '—'}</div>
2013
+ <div><strong>分类耗时:</strong> ${routing.classificationMs ? routing.classificationMs + 'ms' : '—'}</div>
2014
+ <div><strong>分类方式:</strong> ${routing.fallbackUsed ? '<span class="badge badge-warn">正则兜底</span>' : '<span class="badge badge-allow">AI 分类</span>'}</div>
2015
+ ${routing.intent.reasoning ? `<div style="margin-top:0.5rem"><strong>推理:</strong> ${escapeHtml(routing.intent.reasoning)}</div>` : ''}
2016
+ </div>
2017
+ </div>
2018
+
2019
+ <div style="margin-bottom:1.5rem">
2020
+ <h4 style="margin-bottom:0.5rem">路由决策</h4>
2021
+ <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2022
+ <div><strong>路由类型:</strong> ${routing.routedToType || '无'}</div>
2023
+ <div><strong>路由目标:</strong> ${routing.routedToName || '—'}</div>
2024
+ <div><strong>执行状态:</strong> ${obeyedText}</div>
2025
+ <div><strong>完成来源:</strong> ${routing.completionReason || '—'}</div>
2026
+ ${routing.refusalReason ? `<div style="margin-top:0.5rem;color:var(--red)"><strong>违抗原因:</strong> ${escapeHtml(routing.refusalReason)}</div>` : ''}
2027
+ </div>
2028
+ </div>
2029
+
2030
+ <div style="margin-bottom:1.5rem">
2031
+ <h4 style="margin-bottom:0.5rem">时间分解</h4>
2032
+ <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2033
+ <div><strong>路由开始:</strong> ${routing.timestamp ? new Date(routing.timestamp).toLocaleString('zh-CN') : '—'}</div>
2034
+ <div><strong>首工具时间:</strong> ${routing.firstToolTs ? new Date(routing.firstToolTs).toLocaleString('zh-CN') : '—'}</div>
2035
+ <div><strong>执行完成:</strong> ${routing.completedTs ? new Date(routing.completedTs).toLocaleString('zh-CN') : '—'}</div>
2036
+ <div><strong>分类耗时:</strong> ${routing.classificationMs ? routing.classificationMs + 'ms' : '—'}</div>
2037
+ <div><strong>首工具延迟:</strong> ${routing.firstToolTs ? (routing.firstToolTs - routing.timestamp) + 'ms' : '—'}</div>
2038
+ <div><strong>真实执行耗时:</strong> ${routing.totalExecutionMs ? routing.totalExecutionMs + 'ms' : '—'}</div>
2039
+ </div>
2040
+ </div>
2041
+
2042
+ <div style="margin-bottom:1.5rem">
2043
+ <h4 style="margin-bottom:0.5rem">实际执行</h4>
2044
+ <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);font-size:0.875rem">
2045
+ <div><strong>首个工具:</strong> ${routing.firstTool || '—'}</div>
2046
+ ${routing.taskChain.length > 0 ? `<div><strong>子 Agent 链:</strong> ${routing.taskChain.join(' → ')}</div>` : ''}
2047
+ </div>
2048
+ </div>
2049
+
2050
+ <div style="margin-bottom:1.5rem">
2051
+ <h4 style="margin-bottom:0.5rem">事件时间线 (${data.events.length} 个事件)</h4>
2052
+ <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);max-height:300px;overflow-y:auto;font-size:0.875rem">
2053
+ ${data.events.map(e => `
2054
+ <div style="padding:0.25rem 0;border-bottom:1px solid var(--border)">
2055
+ <span style="color:var(--text-dim)">${new Date(e.timestamp).toLocaleTimeString('zh-CN')}</span>
2056
+ <span class="badge badge-info" style="margin:0 0.5rem">${e.hook_type}</span>
2057
+ ${e.tool_name ? `<span class="badge">${e.tool_name}</span>` : ''}
2058
+ </div>
2059
+ `).join('')}
2060
+ </div>
2061
+ </div>
2062
+
2063
+ ${data.injections.length > 0 ? `
2064
+ <div style="margin-bottom:1.5rem">
2065
+ <h4 style="margin-bottom:0.5rem">注入内容 (${data.injections.length} 条)</h4>
2066
+ <div style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);max-height:300px;overflow-y:auto;font-size:0.875rem">
2067
+ ${data.injections.map(inj => `
2068
+ <div style="padding:0.5rem 0;border-bottom:1px solid var(--border)">
2069
+ <div><span class="badge badge-info">${inj.injection_type}</span></div>
2070
+ <div style="margin-top:0.25rem;white-space:pre-wrap;font-family:monospace;font-size:0.8rem">${escapeHtml(inj.content.slice(0, 200))}${inj.content.length > 200 ? '...' : ''}</div>
2071
+ </div>
2072
+ `).join('')}
2073
+ </div>
2074
+ </div>
2075
+ ` : ''}
2076
+ `;
2077
+
2078
+ openDrawer('执行追踪详情', html);
2079
+ } catch (err) {
2080
+ alert('加载失败: ' + err.message);
2081
+ }
2082
+ }
2083
+
1573
2084
  function routingWindow() {
1574
2085
  const el = document.getElementById('routing-window');
1575
2086
  return el ? el.value : '168';
@@ -1632,6 +2143,279 @@ function card(label, value, color) {
1632
2143
  + '</div>';
1633
2144
  }
1634
2145
 
2146
+ async function loadRoutingPerformance() {
2147
+ try {
2148
+ const r = await fetch(API + '/api/routing/performance?window=' + routingWindow() + '&minAttempts=10');
2149
+ const data = await r.json();
2150
+
2151
+ document.getElementById('routing-performance-cards').innerHTML = [
2152
+ card('总路由数', data.summary.totalRouted),
2153
+ card('已判定', data.summary.totalJudged),
2154
+ card('遵守率', pct(data.summary.obedienceRate)),
2155
+ card('违抗率', pct(data.summary.refusalRate)),
2156
+ card('平均分类耗时', ms(data.summary.avgClassificationMs)),
2157
+ card('平均真实耗时', ms(data.summary.avgExecutionMs)),
2158
+ ].join('');
2159
+
2160
+ const byAgentTbody = document.getElementById('routing-performance-by-agent');
2161
+ byAgentTbody.innerHTML = (data.byAgent || []).length === 0
2162
+ ? '<tr><td colspan="7">' + empty('暂无性能数据') + '</td></tr>'
2163
+ : data.byAgent.map(a => '<tr>'
2164
+ + '<td style="font-family:monospace">' + a.agent + '</td>'
2165
+ + '<td>' + a.total + '</td>'
2166
+ + '<td>' + a.judged + '</td>'
2167
+ + '<td>' + pct(a.obedienceRate) + '</td>'
2168
+ + '<td>' + pct(a.refusalRate) + '</td>'
2169
+ + '<td>' + ms(a.avgClassificationMs) + '</td>'
2170
+ + '<td>' + ms(a.avgExecutionMs) + '</td>'
2171
+ + '</tr>').join('');
2172
+
2173
+ const highRefusal = document.getElementById('routing-high-refusal');
2174
+ highRefusal.innerHTML = (data.highRefusalAgents || []).length === 0
2175
+ ? empty('暂无达到阈值的高违抗率 Agent')
2176
+ : '<table><thead><tr><th>Agent</th><th>样本</th><th>违抗率</th><th>分类耗时</th><th>真实耗时</th></tr></thead><tbody>'
2177
+ + data.highRefusalAgents.map(a => '<tr>'
2178
+ + '<td style="font-family:monospace">' + a.agent + '</td>'
2179
+ + '<td>' + a.totalAttempts + '</td>'
2180
+ + '<td>' + pct(a.refusalRate) + '</td>'
2181
+ + '<td>' + ms(a.avgClassificationMs) + '</td>'
2182
+ + '<td>' + ms(a.avgExecutionMs) + '</td>'
2183
+ + '</tr>').join('')
2184
+ + '</tbody></table>';
2185
+
2186
+ renderRoutingPerformanceCharts(data.dailyTrend || []);
2187
+ } catch (err) {
2188
+ document.getElementById('routing-performance-cards').innerHTML = empty('加载失败: ' + err.message);
2189
+ document.getElementById('routing-performance-by-agent').innerHTML = '<tr><td colspan="7">' + empty('加载失败: ' + err.message) + '</td></tr>';
2190
+ document.getElementById('routing-high-refusal').innerHTML = empty('加载失败: ' + err.message);
2191
+ }
2192
+ }
2193
+
2194
+ function renderRoutingPerformanceCharts(trend) {
2195
+ const labels = trend.map(d => d.date.slice(5));
2196
+ const total = trend.map(d => d.total);
2197
+ const obeyed = trend.map(d => d.obeyed);
2198
+ const refused = trend.map(d => d.refused);
2199
+ const latency = trend.map(d => d.avgExecutionMs ?? 0);
2200
+
2201
+ if (charts.routingTrend) charts.routingTrend.destroy();
2202
+ if (charts.routingLatency) charts.routingLatency.destroy();
2203
+
2204
+ const trendCtx = document.getElementById('chart-routing-trend');
2205
+ if (trendCtx) {
2206
+ charts.routingTrend = new Chart(trendCtx, {
2207
+ type: 'line',
2208
+ data: {
2209
+ labels,
2210
+ datasets: [
2211
+ { label: '总路由', data: total, borderColor: '#4f46e5', backgroundColor: 'rgba(79,70,229,0.12)', tension: 0.25 },
2212
+ { label: '遵守', data: obeyed, borderColor: '#16a34a', backgroundColor: 'rgba(22,163,74,0.12)', tension: 0.25 },
2213
+ { label: '违抗', data: refused, borderColor: '#dc2626', backgroundColor: 'rgba(220,38,38,0.12)', tension: 0.25 },
2214
+ ],
2215
+ },
2216
+ options: { responsive: true, maintainAspectRatio: false },
2217
+ });
2218
+ }
2219
+
2220
+ const latencyCtx = document.getElementById('chart-routing-latency');
2221
+ if (latencyCtx) {
2222
+ charts.routingLatency = new Chart(latencyCtx, {
2223
+ type: 'bar',
2224
+ data: {
2225
+ labels,
2226
+ datasets: [{ label: '平均真实执行耗时(ms)', data: latency, backgroundColor: 'rgba(37,99,235,0.65)' }],
2227
+ },
2228
+ options: { responsive: true, maintainAspectRatio: false },
2229
+ });
2230
+ }
2231
+ }
2232
+
2233
+ async function loadRoutingAIOptimization() {
2234
+ const container = document.getElementById('routing-ai-optimization');
2235
+ container.innerHTML = loading();
2236
+ try {
2237
+ const minAttempts = parseInt(document.getElementById('ai-opt-min-attempts').value) || 10;
2238
+ const r = await fetch(API + '/api/routing/ai-optimization?window=' + routingWindow() + '&minAttempts=' + minAttempts);
2239
+ if (!r.ok) {
2240
+ const err = await r.json().catch(() => ({ error: r.statusText }));
2241
+ throw new Error(err.error || r.statusText);
2242
+ }
2243
+ const data = await r.json();
2244
+ currentAIOptimizationData = data;
2245
+
2246
+ const priorities = (data.priorities || []).length === 0
2247
+ ? empty('暂无优先级建议')
2248
+ : (data.priorities || []).map((p, idx) => `
2249
+ <div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.75rem;margin-bottom:0.5rem">
2250
+ <div style="display:flex;justify-content:space-between;gap:0.5rem;align-items:center;margin-bottom:0.35rem">
2251
+ <strong>#${idx + 1} ${escapeHtml(p.area || '优化项')}</strong>
2252
+ <span class="badge badge-info">${Math.round((p.confidence || 0) * 100)}%</span>
2253
+ </div>
2254
+ <div style="font-size:0.875rem;margin-bottom:0.25rem"><strong>发现:</strong>${escapeHtml(p.finding || '')}</div>
2255
+ <div style="font-size:0.875rem"><strong>影响:</strong>${escapeHtml(p.impact || '')}</div>
2256
+ </div>
2257
+ `).join('');
2258
+
2259
+ const changes = (data.suggestedChanges || []).length === 0
2260
+ ? empty('暂无可执行改动建议')
2261
+ : (data.suggestedChanges || []).map((c, idx) => `
2262
+ <div style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.75rem;margin-bottom:0.5rem">
2263
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.35rem">
2264
+ <strong>#${idx + 1} ${escapeHtml(c.targetType || '')}: ${escapeHtml(c.targetName || '—')}</strong>
2265
+ <div style="display:flex;gap:0.5rem">
2266
+ <span class="badge ${c.targetType === 'routing_rule' ? 'badge-warn' : 'badge-allow'}">${escapeHtml(c.targetType || '—')}</span>
2267
+ <button class="btn" style="font-size:0.75rem;padding:0.25rem 0.5rem" onclick="previewPatch(${idx})">预览 Patch</button>
2268
+ </div>
2269
+ </div>
2270
+ <div style="font-size:0.875rem;margin-bottom:0.25rem"><strong>建议:</strong>${escapeHtml(c.recommendation || '')}</div>
2271
+ <div style="font-size:0.875rem;margin-bottom:0.25rem"><strong>理由:</strong>${escapeHtml(c.rationale || '')}</div>
2272
+ <div style="font-size:0.875rem;color:var(--text-dim)"><strong>收益:</strong>${escapeHtml(c.expectedBenefit || '')}</div>
2273
+ </div>
2274
+ `).join('');
2275
+
2276
+ const evidence = data.evidence || {};
2277
+ container.innerHTML = `
2278
+ <div class="cards" style="margin-bottom:1rem">
2279
+ ${card('高违抗率 Agent', (evidence.highRefusalAgents || []).length)}
2280
+ ${card('违抗模式', (evidence.topViolations || []).length)}
2281
+ ${card('规则建议', (evidence.ruleRecommendations || []).length)}
2282
+ </div>
2283
+ <div class="grid-2">
2284
+ <div class="panel">
2285
+ <div class="panel-header"><span class="panel-title">AI 摘要</span></div>
2286
+ <div class="panel-body" style="white-space:pre-wrap;line-height:1.6">${escapeHtml(data.summary || '—')}</div>
2287
+ </div>
2288
+ <div class="panel">
2289
+ <div class="panel-header"><span class="panel-title">优先级</span></div>
2290
+ <div class="panel-body">${priorities}</div>
2291
+ </div>
2292
+ </div>
2293
+ <div class="grid-2" style="margin-top:1rem">
2294
+ <div class="panel">
2295
+ <div class="panel-header"><span class="panel-title">可执行改动建议</span></div>
2296
+ <div class="panel-body">${changes}</div>
2297
+ </div>
2298
+ <div class="panel">
2299
+ <div class="panel-header"><span class="panel-title">证据</span></div>
2300
+ <div class="panel-body">
2301
+ <details open style="margin-bottom:0.5rem"><summary style="cursor:pointer;font-weight:600">高违抗率 Agent</summary><pre style="white-space:pre-wrap;font-size:0.8rem;background:var(--bg-secondary);padding:0.5rem;border-radius:4px">${escapeHtml(JSON.stringify(evidence.highRefusalAgents || [], null, 2))}</pre></details>
2302
+ <details style="margin-bottom:0.5rem"><summary style="cursor:pointer;font-weight:600">违抗模式</summary><pre style="white-space:pre-wrap;font-size:0.8rem;background:var(--bg-secondary);padding:0.5rem;border-radius:4px">${escapeHtml(JSON.stringify(evidence.topViolations || [], null, 2))}</pre></details>
2303
+ <details><summary style="cursor:pointer;font-weight:600">规则建议</summary><pre style="white-space:pre-wrap;font-size:0.8rem;background:var(--bg-secondary);padding:0.5rem;border-radius:4px">${escapeHtml(JSON.stringify(evidence.ruleRecommendations || [], null, 2))}</pre></details>
2304
+ </div>
2305
+ </div>
2306
+ </div>
2307
+ `;
2308
+ } catch (err) {
2309
+ container.innerHTML = empty('加载失败: ' + err.message);
2310
+ }
2311
+ }
2312
+
2313
+ // Patch preview and apply functions
2314
+ async function previewPatch(idx) {
2315
+ if (!currentAIOptimizationData || !currentAIOptimizationData.suggestedChanges) {
2316
+ alert('请先加载 AI 优化建议');
2317
+ return;
2318
+ }
2319
+ const change = currentAIOptimizationData.suggestedChanges[idx];
2320
+ if (!change) {
2321
+ alert('建议不存在');
2322
+ return;
2323
+ }
2324
+
2325
+ const drawerTitle = document.getElementById('drawer-title');
2326
+ const drawerBody = document.getElementById('drawer-body');
2327
+ drawerTitle.textContent = 'Patch 预览';
2328
+ drawerBody.innerHTML = '<div style="text-align:center;padding:2rem">正在生成 patch...</div>';
2329
+ openDrawer();
2330
+
2331
+ try {
2332
+ const r = await fetch(API + '/api/patch/preview', {
2333
+ method: 'POST',
2334
+ headers: { 'Content-Type': 'application/json' },
2335
+ body: JSON.stringify({
2336
+ targetType: change.targetType,
2337
+ targetName: change.targetName,
2338
+ recommendation: change.recommendation,
2339
+ rationale: change.rationale,
2340
+ }),
2341
+ });
2342
+ if (!r.ok) {
2343
+ const err = await r.json().catch(() => ({ error: r.statusText }));
2344
+ throw new Error(err.error || r.statusText);
2345
+ }
2346
+ const preview = await r.json();
2347
+ currentPatchPreview = preview;
2348
+
2349
+ const riskBadge = preview.risk === 'low' ? '<span class="badge badge-allow">低风险</span>'
2350
+ : preview.risk === 'high' ? '<span class="badge badge-block">高风险</span>'
2351
+ : '<span class="badge badge-warn">中风险</span>';
2352
+
2353
+ drawerBody.innerHTML = `
2354
+ <div style="margin-bottom:1rem">
2355
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
2356
+ <strong>${preview.targetType}: ${preview.targetName}</strong>
2357
+ ${riskBadge}
2358
+ </div>
2359
+ <div style="font-size:0.875rem;color:var(--text-dim);margin-bottom:0.5rem">${escapeHtml(preview.summary)}</div>
2360
+ <div style="font-size:0.875rem;color:var(--text-dim)">文件: ${escapeHtml(preview.filePath)}</div>
2361
+ </div>
2362
+ <div style="margin-bottom:1rem">
2363
+ <strong>变更前:</strong>
2364
+ <pre style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);max-height:300px;overflow:auto;font-size:0.75rem">${escapeHtml(preview.before)}</pre>
2365
+ </div>
2366
+ <div style="margin-bottom:1rem">
2367
+ <strong>变更后:</strong>
2368
+ <pre style="background:var(--bg-secondary);padding:0.75rem;border-radius:var(--radius-sm);max-height:300px;overflow:auto;font-size:0.75rem">${escapeHtml(preview.after)}</pre>
2369
+ </div>
2370
+ <div style="display:flex;gap:0.5rem;justify-content:flex-end">
2371
+ <button class="btn" onclick="closeDrawer()">取消</button>
2372
+ <button class="btn btn-primary" onclick="applyPatch()">应用 Patch</button>
2373
+ </div>
2374
+ `;
2375
+ } catch (err) {
2376
+ drawerBody.innerHTML = `<div style="color:var(--red);padding:1rem">预览失败: ${escapeHtml(err.message)}</div>`;
2377
+ }
2378
+ }
2379
+
2380
+ async function applyPatch() {
2381
+ if (!currentPatchPreview) {
2382
+ alert('请先预览 patch');
2383
+ return;
2384
+ }
2385
+
2386
+ if (!confirm(`确定要应用此 patch 吗?\n\n目标: ${currentPatchPreview.targetType}/${currentPatchPreview.targetName}\n\n系统会自动备份当前版本。`)) {
2387
+ return;
2388
+ }
2389
+
2390
+ try {
2391
+ const r = await fetch(API + '/api/patch/apply', {
2392
+ method: 'POST',
2393
+ headers: { 'Content-Type': 'application/json' },
2394
+ body: JSON.stringify({
2395
+ targetType: currentPatchPreview.targetType,
2396
+ targetName: currentPatchPreview.targetName,
2397
+ afterContent: currentPatchPreview.after,
2398
+ }),
2399
+ });
2400
+ if (!r.ok) {
2401
+ const err = await r.json().catch(() => ({ error: r.statusText }));
2402
+ throw new Error(err.error || r.statusText);
2403
+ }
2404
+ const result = await r.json();
2405
+ alert(`Patch 应用成功!\n\n备份路径: ${result.backupPath}\n\n请刷新相关页面查看结果。`);
2406
+ closeDrawer();
2407
+ const targetType = currentPatchPreview.targetType;
2408
+ currentPatchPreview = null;
2409
+
2410
+ // Refresh relevant pages
2411
+ if (targetType === 'agent') loadAgents();
2412
+ else if (targetType === 'skill') loadSkills();
2413
+ else if (targetType === 'routing_rule') loadRoutingConfig();
2414
+ } catch (err) {
2415
+ alert('应用失败: ' + err.message);
2416
+ }
2417
+ }
2418
+
1635
2419
  async function loadRoutingEvents() {
1636
2420
  try {
1637
2421
  const obeyed = document.getElementById('routing-filter-obeyed')?.value || '';