@winspan/claude-forge 8.12.0 → 8.13.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/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +78 -18
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/core/utils/error-handler.d.ts +45 -0
- package/dist/core/utils/error-handler.d.ts.map +1 -0
- package/dist/core/utils/error-handler.js +233 -0
- package/dist/core/utils/error-handler.js.map +1 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +8 -1
- package/dist/web/server.js.map +1 -1
- package/dist/web/static/index.html +275 -209
- package/package.json +6 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Claude Forge
|
|
6
|
+
<title>Claude Forge 管理后台</title>
|
|
7
7
|
<script src="vendor/chart.umd.min.js"></script>
|
|
8
8
|
<style>
|
|
9
9
|
/* === Reset & Theme === */
|
|
@@ -333,42 +333,42 @@
|
|
|
333
333
|
<div class="brand-icon">CF</div>
|
|
334
334
|
<div class="brand-text">Claude Forge</div>
|
|
335
335
|
</div>
|
|
336
|
-
<div class="nav-section-title"
|
|
336
|
+
<div class="nav-section-title">总览</div>
|
|
337
337
|
<a onclick="nav('dashboard')" id="nav-dashboard" class="active">
|
|
338
338
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
|
339
|
-
|
|
339
|
+
仪表盘
|
|
340
340
|
</a>
|
|
341
|
-
<div class="nav-section-title"
|
|
341
|
+
<div class="nav-section-title">活动</div>
|
|
342
342
|
<a onclick="nav('sessions')" id="nav-sessions">
|
|
343
343
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
344
|
-
|
|
344
|
+
会话
|
|
345
345
|
</a>
|
|
346
346
|
<a onclick="nav('events')" id="nav-events">
|
|
347
347
|
<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>
|
|
348
|
-
|
|
348
|
+
事件
|
|
349
349
|
</a>
|
|
350
350
|
<a onclick="nav('injections')" id="nav-injections">
|
|
351
351
|
<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>
|
|
352
|
-
|
|
352
|
+
注入
|
|
353
353
|
</a>
|
|
354
354
|
<a onclick="nav('live')" id="nav-live">
|
|
355
355
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/></svg>
|
|
356
|
-
|
|
356
|
+
实时
|
|
357
357
|
</a>
|
|
358
|
-
<div class="nav-section-title"
|
|
358
|
+
<div class="nav-section-title">配置</div>
|
|
359
359
|
<a onclick="nav('rules')" id="nav-rules">
|
|
360
360
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
|
|
361
|
-
|
|
361
|
+
规则
|
|
362
362
|
</a>
|
|
363
363
|
</aside>
|
|
364
364
|
|
|
365
365
|
<!-- Main -->
|
|
366
366
|
<main class="main">
|
|
367
367
|
<div class="topbar">
|
|
368
|
-
<div class="page-title" id="topbar-title"
|
|
368
|
+
<div class="page-title" id="topbar-title">仪表盘</div>
|
|
369
369
|
<div class="actions">
|
|
370
|
-
<span id="daemon-status" class="badge badge-allow" style="font-size:0.75rem"
|
|
371
|
-
<button class="btn" onclick="refreshPage()">↻
|
|
370
|
+
<span id="daemon-status" class="badge badge-allow" style="font-size:0.75rem">检查中...</span>
|
|
371
|
+
<button class="btn" onclick="refreshPage()">↻ 刷新</button>
|
|
372
372
|
</div>
|
|
373
373
|
</div>
|
|
374
374
|
<div class="container">
|
|
@@ -379,13 +379,13 @@
|
|
|
379
379
|
<div class="grid-2" style="margin-bottom:1.25rem">
|
|
380
380
|
<div class="panel">
|
|
381
381
|
<div class="panel-header">
|
|
382
|
-
<span class="panel-title"
|
|
382
|
+
<span class="panel-title">近 7 天活动</span>
|
|
383
383
|
</div>
|
|
384
384
|
<div class="panel-body"><div class="chart-wrap"><canvas id="chart-activity"></canvas></div></div>
|
|
385
385
|
</div>
|
|
386
386
|
<div class="panel">
|
|
387
387
|
<div class="panel-header">
|
|
388
|
-
<span class="panel-title"
|
|
388
|
+
<span class="panel-title">工具使用分布</span>
|
|
389
389
|
</div>
|
|
390
390
|
<div class="panel-body"><div class="chart-wrap"><canvas id="chart-tools"></canvas></div></div>
|
|
391
391
|
</div>
|
|
@@ -393,14 +393,14 @@
|
|
|
393
393
|
<div class="grid-2">
|
|
394
394
|
<div class="panel">
|
|
395
395
|
<div class="panel-header">
|
|
396
|
-
<span class="panel-title"
|
|
397
|
-
<a onclick="nav('sessions')" style="font-size:0.8rem;color:var(--primary);cursor:pointer"
|
|
396
|
+
<span class="panel-title">最近会话</span>
|
|
397
|
+
<a onclick="nav('sessions')" style="font-size:0.8rem;color:var(--primary);cursor:pointer">查看全部 →</a>
|
|
398
398
|
</div>
|
|
399
399
|
<div id="dash-sessions"></div>
|
|
400
400
|
</div>
|
|
401
401
|
<div class="panel">
|
|
402
402
|
<div class="panel-header">
|
|
403
|
-
<span class="panel-title"
|
|
403
|
+
<span class="panel-title">最近活动</span>
|
|
404
404
|
</div>
|
|
405
405
|
<div class="panel-body" id="dash-activity"></div>
|
|
406
406
|
</div>
|
|
@@ -410,7 +410,7 @@
|
|
|
410
410
|
<!-- Sessions -->
|
|
411
411
|
<div id="page-sessions" class="page">
|
|
412
412
|
<div class="toolbar">
|
|
413
|
-
<input class="search-box" id="sessions-search" placeholder="
|
|
413
|
+
<input class="search-box" id="sessions-search" placeholder="搜索会话..." oninput="filterSessions()">
|
|
414
414
|
</div>
|
|
415
415
|
<div class="panel" id="sessions-panel">
|
|
416
416
|
<div id="sessions-list"></div>
|
|
@@ -420,9 +420,9 @@
|
|
|
420
420
|
<!-- Events -->
|
|
421
421
|
<div id="page-events" class="page">
|
|
422
422
|
<div class="toolbar">
|
|
423
|
-
<input class="search-box" id="events-search" placeholder="
|
|
423
|
+
<input class="search-box" id="events-search" placeholder="按类型、工具、项目搜索..." oninput="filterEvents()">
|
|
424
424
|
<select class="btn" id="events-type" onchange="filterEvents()">
|
|
425
|
-
<option value=""
|
|
425
|
+
<option value="">全部类型</option>
|
|
426
426
|
<option value="PreToolUse">PreToolUse</option>
|
|
427
427
|
<option value="PostToolUse">PostToolUse</option>
|
|
428
428
|
<option value="UserPromptSubmit">UserPromptSubmit</option>
|
|
@@ -431,7 +431,7 @@
|
|
|
431
431
|
</div>
|
|
432
432
|
<div class="panel">
|
|
433
433
|
<table>
|
|
434
|
-
<thead><tr><th
|
|
434
|
+
<thead><tr><th>时间</th><th>类型</th><th>工具</th><th>项目</th></tr></thead>
|
|
435
435
|
<tbody id="events-body"></tbody>
|
|
436
436
|
</table>
|
|
437
437
|
</div>
|
|
@@ -440,9 +440,9 @@
|
|
|
440
440
|
<!-- Injections -->
|
|
441
441
|
<div id="page-injections" class="page">
|
|
442
442
|
<div class="toolbar">
|
|
443
|
-
<input class="search-box" id="inj-search" placeholder="
|
|
443
|
+
<input class="search-box" id="inj-search" placeholder="搜索注入内容..." oninput="filterInjections()">
|
|
444
444
|
<select class="btn" id="inj-handler" onchange="filterInjections()">
|
|
445
|
-
<option value=""
|
|
445
|
+
<option value="">全部 Handler</option>
|
|
446
446
|
<option value="UserPromptSubmitHandler">UserPromptSubmit</option>
|
|
447
447
|
<option value="PreToolUseHandler">PreToolUse</option>
|
|
448
448
|
<option value="PostToolUseHandler">PostToolUse</option>
|
|
@@ -456,9 +456,9 @@
|
|
|
456
456
|
<!-- Live -->
|
|
457
457
|
<div id="page-live" class="page">
|
|
458
458
|
<div class="toolbar">
|
|
459
|
-
<span id="live-status" class="badge badge-warn"
|
|
460
|
-
<button class="btn btn-primary" id="live-btn" onclick="toggleLive()"
|
|
461
|
-
<button class="btn" onclick="document.getElementById('live-log').innerHTML=''"
|
|
459
|
+
<span id="live-status" class="badge badge-warn">未连接</span>
|
|
460
|
+
<button class="btn btn-primary" id="live-btn" onclick="toggleLive()">连接</button>
|
|
461
|
+
<button class="btn" onclick="document.getElementById('live-log').innerHTML=''">清空</button>
|
|
462
462
|
</div>
|
|
463
463
|
<div class="live-log" id="live-log"></div>
|
|
464
464
|
</div>
|
|
@@ -466,7 +466,7 @@
|
|
|
466
466
|
<!-- Rules -->
|
|
467
467
|
<div id="page-rules" class="page">
|
|
468
468
|
<div class="toolbar">
|
|
469
|
-
<input class="search-box" id="rules-search" placeholder="
|
|
469
|
+
<input class="search-box" id="rules-search" placeholder="搜索规则..." oninput="filterRules()">
|
|
470
470
|
</div>
|
|
471
471
|
<div class="panel" id="rules-panel">
|
|
472
472
|
<div id="rules-list"></div>
|
|
@@ -482,7 +482,7 @@
|
|
|
482
482
|
<!-- Drawer -->
|
|
483
483
|
<div class="drawer" id="drawer">
|
|
484
484
|
<div class="drawer-header">
|
|
485
|
-
<div class="drawer-title" id="drawer-title"
|
|
485
|
+
<div class="drawer-title" id="drawer-title">详情</div>
|
|
486
486
|
<button class="drawer-close" onclick="closeDrawer()">
|
|
487
487
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
488
488
|
</button>
|
|
@@ -506,7 +506,7 @@ function nav(page) {
|
|
|
506
506
|
const pageEl = document.getElementById('page-' + page);
|
|
507
507
|
if (navEl) navEl.classList.add('active');
|
|
508
508
|
if (pageEl) pageEl.classList.add('active');
|
|
509
|
-
const titles = { dashboard:'
|
|
509
|
+
const titles = { dashboard:'仪表盘', sessions:'会话', events:'事件', injections:'注入', live:'实时', rules:'规则' };
|
|
510
510
|
document.getElementById('topbar-title').textContent = titles[page] || page;
|
|
511
511
|
closeDrawer();
|
|
512
512
|
if (page === 'dashboard') loadDashboard();
|
|
@@ -553,7 +553,7 @@ function empty(msg) {
|
|
|
553
553
|
return `<div class="empty"><div class="empty-icon">📭</div><div class="empty-text">${msg}</div></div>`;
|
|
554
554
|
}
|
|
555
555
|
function loading() {
|
|
556
|
-
return `<div class="loading"
|
|
556
|
+
return `<div class="loading">加载中...</div>`;
|
|
557
557
|
}
|
|
558
558
|
|
|
559
559
|
// === Dashboard ===
|
|
@@ -569,23 +569,23 @@ async function loadDashboard() {
|
|
|
569
569
|
|
|
570
570
|
// Stat cards
|
|
571
571
|
const uptime = Math.floor(status.uptime);
|
|
572
|
-
const uptimeStr = uptime < 60 ? uptime + '
|
|
572
|
+
const uptimeStr = uptime < 60 ? uptime + ' 秒' : Math.floor(uptime/60) + ' 分钟';
|
|
573
573
|
const mem = Math.round(status.memory.heapUsed / 1024 / 1024);
|
|
574
574
|
document.getElementById('dash-stats').innerHTML = `
|
|
575
|
-
<div class="stat-card"><div class="label"
|
|
576
|
-
<div class="stat-card"><div class="label"
|
|
577
|
-
<div class="stat-card"><div class="label"
|
|
578
|
-
<div class="stat-card"><div class="label"
|
|
575
|
+
<div class="stat-card"><div class="label">进程 ID</div><div class="value">${status.pid}</div></div>
|
|
576
|
+
<div class="stat-card"><div class="label">运行时长</div><div class="value">${uptimeStr}</div></div>
|
|
577
|
+
<div class="stat-card"><div class="label">内存占用</div><div class="value">${mem}MB</div></div>
|
|
578
|
+
<div class="stat-card"><div class="label">事件总数</div><div class="value">${status.eventCount}</div></div>
|
|
579
579
|
`;
|
|
580
|
-
document.getElementById('daemon-status').textContent = '
|
|
580
|
+
document.getElementById('daemon-status').textContent = '运行中';
|
|
581
581
|
document.getElementById('daemon-status').className = 'badge badge-allow';
|
|
582
582
|
|
|
583
583
|
// Charts
|
|
584
584
|
renderActivityChart(stats.dailyActivity || []);
|
|
585
585
|
renderToolChart(stats.toolUsage || {});
|
|
586
586
|
} catch {
|
|
587
|
-
document.getElementById('dash-stats').innerHTML = `<div class="stat-card"><div class="label"
|
|
588
|
-
document.getElementById('daemon-status').textContent = '
|
|
587
|
+
document.getElementById('dash-stats').innerHTML = `<div class="stat-card"><div class="label">状态</div><div class="value" style="font-size:1rem;color:var(--red)">离线</div></div>`;
|
|
588
|
+
document.getElementById('daemon-status').textContent = '离线';
|
|
589
589
|
document.getElementById('daemon-status').className = 'badge badge-block';
|
|
590
590
|
}
|
|
591
591
|
|
|
@@ -594,10 +594,10 @@ async function loadDashboard() {
|
|
|
594
594
|
const res = await fetch(API + '/api/sessions?limit=5');
|
|
595
595
|
const sessions = await res.json();
|
|
596
596
|
document.getElementById('dash-sessions').innerHTML = sessions.length === 0
|
|
597
|
-
? empty('
|
|
597
|
+
? empty('暂无会话')
|
|
598
598
|
: sessions.map(s => sessionListItem(s)).join('');
|
|
599
599
|
} catch {
|
|
600
|
-
document.getElementById('dash-sessions').innerHTML = empty('
|
|
600
|
+
document.getElementById('dash-sessions').innerHTML = empty('加载失败');
|
|
601
601
|
}
|
|
602
602
|
|
|
603
603
|
// Recent activity
|
|
@@ -605,7 +605,7 @@ async function loadDashboard() {
|
|
|
605
605
|
const res = await fetch(API + '/api/events?limit=10');
|
|
606
606
|
const events = await res.json();
|
|
607
607
|
document.getElementById('dash-activity').innerHTML = events.length === 0
|
|
608
|
-
? empty('
|
|
608
|
+
? empty('暂无活动')
|
|
609
609
|
: events.map(e => `
|
|
610
610
|
<div class="activity-item">
|
|
611
611
|
<div class="activity-dot activity-dot-${hookColor(e.hook_type)}"></div>
|
|
@@ -629,7 +629,7 @@ function renderActivityChart(data) {
|
|
|
629
629
|
type: 'line',
|
|
630
630
|
data: {
|
|
631
631
|
labels: data.map(d => d.date),
|
|
632
|
-
datasets: [{ label: '
|
|
632
|
+
datasets: [{ label: '事件数', data: data.map(d => d.count), borderColor: '#4f46e5', backgroundColor: 'rgba(79,70,229,0.08)', tension: 0.3, fill: true }]
|
|
633
633
|
},
|
|
634
634
|
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { ticks: { color: '#9ca3af' }, grid: { color: '#e4e7ec' } }, x: { ticks: { color: '#9ca3af' }, grid: { color: '#e4e7ec' } } } }
|
|
635
635
|
});
|
|
@@ -652,16 +652,16 @@ function renderToolChart(data) {
|
|
|
652
652
|
function toggleLive() {
|
|
653
653
|
if (liveSource) {
|
|
654
654
|
liveSource.close(); liveSource = null;
|
|
655
|
-
document.getElementById('live-status').textContent = '
|
|
655
|
+
document.getElementById('live-status').textContent = '未连接';
|
|
656
656
|
document.getElementById('live-status').className = 'badge badge-warn';
|
|
657
|
-
document.getElementById('live-btn').textContent = '
|
|
657
|
+
document.getElementById('live-btn').textContent = '连接';
|
|
658
658
|
document.getElementById('live-btn').className = 'btn btn-primary';
|
|
659
659
|
return;
|
|
660
660
|
}
|
|
661
661
|
liveSource = new EventSource(API + '/api/events/stream');
|
|
662
|
-
document.getElementById('live-status').textContent = '
|
|
662
|
+
document.getElementById('live-status').textContent = '实时中';
|
|
663
663
|
document.getElementById('live-status').className = 'badge badge-live';
|
|
664
|
-
document.getElementById('live-btn').textContent = '
|
|
664
|
+
document.getElementById('live-btn').textContent = '断开连接';
|
|
665
665
|
document.getElementById('live-btn').className = 'btn';
|
|
666
666
|
liveSource.onmessage = function(ev) {
|
|
667
667
|
try {
|
|
@@ -678,133 +678,207 @@ function toggleLive() {
|
|
|
678
678
|
} catch {}
|
|
679
679
|
};
|
|
680
680
|
liveSource.onerror = function() {
|
|
681
|
-
document.getElementById('live-status').textContent = '
|
|
681
|
+
document.getElementById('live-status').textContent = '连接错误';
|
|
682
682
|
document.getElementById('live-status').className = 'badge badge-block';
|
|
683
683
|
};
|
|
684
684
|
}
|
|
685
685
|
|
|
686
|
-
// ===
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
686
|
+
// === Sessions ===
|
|
687
|
+
function sessionListItem(s) {
|
|
688
|
+
const prompt = (s.first_prompt || '(无提示词)').slice(0, 60);
|
|
689
|
+
return `<div class="list-item fade-in" onclick="openSessionDrawer('${s.session_id}')">
|
|
690
|
+
<div style="flex:1;min-width:0">
|
|
691
|
+
<div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">${prompt}</div>
|
|
692
|
+
<div style="font-size:0.75rem;color:var(--text-dim)">${fmt(s.start_time)} · ${s.event_count} 个事件 · ${s.session_id.slice(0,8)}</div>
|
|
693
|
+
</div>
|
|
694
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>
|
|
695
|
+
</div>`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function loadSessions() {
|
|
699
|
+
document.getElementById('sessions-list').innerHTML = loading();
|
|
693
700
|
try {
|
|
694
|
-
const res = await fetch(API + '/api/
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
Object.entries(data).forEach(([id, conv]) => {
|
|
698
|
-
(conv.rules || []).forEach(r => allRules.push({ ...r, _convId: id, _convName: conv.name }));
|
|
699
|
-
});
|
|
700
|
-
renderRules(allRules);
|
|
701
|
+
const res = await fetch(API + '/api/sessions?limit=50');
|
|
702
|
+
allSessions = await res.json();
|
|
703
|
+
renderSessions(allSessions);
|
|
701
704
|
} catch {
|
|
702
|
-
document.getElementById('
|
|
705
|
+
document.getElementById('sessions-list').innerHTML = empty('加载会话失败');
|
|
703
706
|
}
|
|
704
707
|
}
|
|
705
708
|
|
|
706
|
-
function
|
|
707
|
-
document.getElementById('
|
|
708
|
-
? empty('
|
|
709
|
-
: list.map(
|
|
710
|
-
<div style="flex:1;min-width:0">
|
|
711
|
-
<div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">${r.name || r.id || 'Unnamed'}</div>
|
|
712
|
-
<div style="font-size:0.75rem;color:var(--text-dim)">${r._convName || r._convId} ${r.tags ? '· ' + r.tags.join(', ') : ''}</div>
|
|
713
|
-
</div>
|
|
714
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>
|
|
715
|
-
</div>`).join('');
|
|
709
|
+
function renderSessions(list) {
|
|
710
|
+
document.getElementById('sessions-list').innerHTML = list.length === 0
|
|
711
|
+
? empty('未找到会话')
|
|
712
|
+
: list.map(s => sessionListItem(s)).join('');
|
|
716
713
|
}
|
|
717
714
|
|
|
718
|
-
function
|
|
719
|
-
const q = document.getElementById('
|
|
720
|
-
|
|
721
|
-
(
|
|
722
|
-
|
|
723
|
-
(r._convName||'').toLowerCase().includes(q) ||
|
|
724
|
-
(r.tags||[]).some(t => t.toLowerCase().includes(q))
|
|
715
|
+
function filterSessions() {
|
|
716
|
+
const q = document.getElementById('sessions-search').value.toLowerCase();
|
|
717
|
+
renderSessions(allSessions.filter(s =>
|
|
718
|
+
(s.first_prompt || '').toLowerCase().includes(q) ||
|
|
719
|
+
s.session_id.toLowerCase().includes(q)
|
|
725
720
|
));
|
|
726
721
|
}
|
|
727
722
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
<div class="detail-section"><div class="detail-label">Name</div><div class="detail-value" style="font-weight:600">${r.name || '—'}</div></div>
|
|
733
|
-
${tags ? `<div class="detail-section"><div class="detail-label">Tags</div><div class="detail-value">${tags}</div></div>` : ''}
|
|
734
|
-
<div class="detail-section"><div class="detail-label">Description</div><div class="detail-value">${r.description || '—'}</div></div>
|
|
735
|
-
${r.when ? `<div class="detail-section"><div class="detail-label">Condition</div><pre class="detail-code">${r.when}</pre></div>` : ''}
|
|
736
|
-
${r.message ? `<div class="detail-section"><div class="detail-label">Message</div><div class="detail-value">${r.message}</div></div>` : ''}
|
|
737
|
-
${r.operator_guidance ? `<div class="detail-section"><div class="detail-label">Guidance</div><div class="detail-value" style="white-space:pre-wrap">${r.operator_guidance}</div></div>` : ''}
|
|
738
|
-
${r.doc_ref ? `<div class="detail-section"><div class="detail-label">Docs</div><div class="detail-value"><a href="${r.doc_ref}" target="_blank" style="color:var(--primary)">${r.doc_ref}</a></div></div>` : ''}
|
|
739
|
-
`;
|
|
740
|
-
openDrawer(r.name || 'Rule', html);
|
|
741
|
-
}
|
|
742
|
-
async function loadInjections() {
|
|
743
|
-
document.getElementById('inj-list').innerHTML = loading();
|
|
723
|
+
let currentSessionData = null;
|
|
724
|
+
|
|
725
|
+
async function openSessionDrawer(sessionId) {
|
|
726
|
+
openDrawer('会话详情', loading());
|
|
744
727
|
try {
|
|
745
|
-
const res = await fetch(API + '/api/
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
728
|
+
const res = await fetch(API + '/api/sessions/' + sessionId + '/detail');
|
|
729
|
+
const data = await res.json();
|
|
730
|
+
currentSessionData = data;
|
|
731
|
+
const s = data.session;
|
|
732
|
+
let html = '<div class="detail-section"><div class="detail-label">会话 ID</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + s.session_id + '</div></div>';
|
|
733
|
+
html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(s.start_time) + ' → ' + fmt(s.end_time) + '</div></div>';
|
|
734
|
+
html += '<div class="detail-section"><div class="detail-label">首条提示词</div><div class="detail-value">' + (s.first_prompt || '—') + '</div></div>';
|
|
735
|
+
html += '<div class="detail-section"><div class="detail-label">任务(' + data.tasks.length + ')</div>';
|
|
736
|
+
data.tasks.forEach((t, i) => {
|
|
737
|
+
const tools = t.events.filter(e => e.tool_name).length;
|
|
738
|
+
html += '<div onclick="openTaskDrawer(' + i + ')" style="padding:0.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.5rem;border:1px solid var(--border);cursor:pointer;transition:all 0.15s" onmouseover="this.style.borderColor=\'var(--primary)\';this.style.background=\'var(--primary-soft)\'" onmouseout="this.style.borderColor=\'var(--border)\';this.style.background=\'var(--bg-secondary)\'">';
|
|
739
|
+
html += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px"><div style="font-weight:500;font-size:0.875rem;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:0.5rem">任务 ' + (i+1) + ':' + t.title + '</div><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>';
|
|
740
|
+
html += '<div style="font-size:0.75rem;color:var(--text-dim)">' + fmtTime(t.start_time) + ' · ' + tools + ' 次工具调用 · ' + t.summary.filesChanged.length + ' 个文件</div>';
|
|
741
|
+
if (t.summary.commits.length > 0) html += '<div style="font-size:0.75rem;color:var(--green);margin-top:4px">✓ ' + t.summary.commits.length + ' 次提交</div>';
|
|
742
|
+
html += '</div>';
|
|
743
|
+
});
|
|
744
|
+
html += '</div>';
|
|
745
|
+
openDrawer('会话 · ' + s.session_id.slice(0,8), html);
|
|
746
|
+
} catch(e) {
|
|
747
|
+
openDrawer('会话详情', empty('加载失败:' + e.message));
|
|
750
748
|
}
|
|
751
749
|
}
|
|
752
750
|
|
|
753
|
-
function
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
751
|
+
function openTaskDrawer(taskIndex) {
|
|
752
|
+
if (!currentSessionData || !currentSessionData.tasks[taskIndex]) return;
|
|
753
|
+
const t = currentSessionData.tasks[taskIndex];
|
|
754
|
+
const tools = t.events.filter(e => e.tool_name).length;
|
|
755
|
+
|
|
756
|
+
let html = '<div class="detail-section"><div class="detail-label">标题</div><div class="detail-value" style="font-weight:600">' + t.title + '</div></div>';
|
|
757
|
+
html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(t.start_time) + (t.end_time ? ' → ' + fmt(t.end_time) : '') + '</div></div>';
|
|
758
|
+
html += '<div class="detail-section"><div class="detail-label">统计</div><div class="detail-value">' + tools + ' 次工具调用 · ' + t.summary.filesChanged.length + ' 个文件变更 · ' + t.summary.commits.length + ' 次提交</div></div>';
|
|
759
|
+
|
|
760
|
+
// User prompts
|
|
761
|
+
if (t.prompts && t.prompts.length > 0) {
|
|
762
|
+
html += '<div class="detail-section"><div class="detail-label">用户提示词(' + t.prompts.length + ')</div>';
|
|
763
|
+
t.prompts.forEach(p => {
|
|
764
|
+
html += '<div style="padding:0.625rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
|
|
765
|
+
html += '<div style="color:var(--text-dim);font-size:0.7rem;margin-bottom:2px">' + fmtTime(p.timestamp) + '</div>';
|
|
766
|
+
html += '<div style="white-space:pre-wrap;word-break:break-word">' + (p.content || '—') + '</div>';
|
|
767
|
+
html += '</div>';
|
|
768
|
+
});
|
|
769
|
+
html += '</div>';
|
|
770
|
+
}
|
|
768
771
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
772
|
+
// Tool usage summary
|
|
773
|
+
if (t.summary.toolUsage && Object.keys(t.summary.toolUsage).length > 0) {
|
|
774
|
+
html += '<div class="detail-section"><div class="detail-label">工具使用分布</div><div class="detail-value">';
|
|
775
|
+
Object.entries(t.summary.toolUsage).sort((a,b) => b[1]-a[1]).forEach(([name, count]) => {
|
|
776
|
+
html += '<span class="tag" style="margin-bottom:4px">' + name + ' × ' + count + '</span>';
|
|
777
|
+
});
|
|
778
|
+
html += '</div></div>';
|
|
779
|
+
}
|
|
777
780
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
<div class="detail-section"><div class="detail-label"
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
781
|
+
// Files changed
|
|
782
|
+
if (t.summary.filesChanged && t.summary.filesChanged.length > 0) {
|
|
783
|
+
html += '<div class="detail-section"><div class="detail-label">变更的文件(' + t.summary.filesChanged.length + ')</div>';
|
|
784
|
+
t.summary.filesChanged.forEach(f => {
|
|
785
|
+
const short = f.split('/').slice(-2).join('/');
|
|
786
|
+
html += '<div style="font-family:monospace;font-size:0.75rem;color:var(--text-muted);padding:2px 0" title="' + f + '">' + short + '</div>';
|
|
787
|
+
});
|
|
788
|
+
html += '</div>';
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Commits
|
|
792
|
+
if (t.summary.commits && t.summary.commits.length > 0) {
|
|
793
|
+
html += '<div class="detail-section"><div class="detail-label">提交记录(' + t.summary.commits.length + ')</div>';
|
|
794
|
+
t.summary.commits.forEach(c => {
|
|
795
|
+
html += '<div style="padding:0.5rem;background:var(--green-soft);border-radius:var(--radius-sm);margin-bottom:0.25rem;font-size:0.8rem;color:var(--text)"><span style="color:var(--green)">✓</span> ' + c.message + '</div>';
|
|
796
|
+
});
|
|
797
|
+
html += '</div>';
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Injections
|
|
801
|
+
if (t.injections && t.injections.length > 0) {
|
|
802
|
+
html += '<div class="detail-section"><div class="detail-label">注入内容(' + t.injections.length + ')</div>';
|
|
803
|
+
t.injections.forEach(inj => {
|
|
804
|
+
const escaped = inj.content.replace(/</g, '<').replace(/>/g, '>');
|
|
805
|
+
html += '<div style="padding:0.625rem;background:var(--primary-soft);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--primary-border)">';
|
|
806
|
+
html += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:4px"><span class="badge badge-info">' + inj.injection_type + '</span><span style="font-size:0.7rem;color:var(--text-dim)">' + inj.source_handler + ' · ' + fmtTime(inj.timestamp) + '</span></div>';
|
|
807
|
+
html += '<div style="font-size:0.75rem;color:var(--text);white-space:pre-wrap;word-break:break-word;max-height:200px;overflow-y:auto;font-family:ui-monospace,monospace">' + escaped + '</div>';
|
|
808
|
+
html += '</div>';
|
|
809
|
+
});
|
|
810
|
+
html += '</div>';
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Quality issues
|
|
814
|
+
if (t.qualityIssues && t.qualityIssues.length > 0) {
|
|
815
|
+
html += '<div class="detail-section"><div class="detail-label">质量问题(' + t.qualityIssues.length + ')</div>';
|
|
816
|
+
t.qualityIssues.forEach(q => {
|
|
817
|
+
const sev = q.severity || 'info';
|
|
818
|
+
const badgeClass = sev === 'must' ? 'badge-block' : sev === 'should' ? 'badge-warn' : 'badge-info';
|
|
819
|
+
const sevLabel = sev === 'must' ? '必须' : sev === 'should' ? '建议' : '提示';
|
|
820
|
+
html += '<div style="padding:0.5rem;background:var(--yellow-soft);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
|
|
821
|
+
html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px"><span class="badge ' + badgeClass + '">' + sevLabel + '</span><span style="font-family:monospace;font-size:0.7rem;color:var(--text-dim)">' + (q.file_path || '').split('/').slice(-2).join('/') + '</span></div>';
|
|
822
|
+
html += '<div style="color:var(--text)">' + (q.description || q.message || '') + '</div>';
|
|
823
|
+
html += '</div>';
|
|
824
|
+
});
|
|
825
|
+
html += '</div>';
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Governance decisions
|
|
829
|
+
if (t.decisions && t.decisions.length > 0) {
|
|
830
|
+
html += '<div class="detail-section"><div class="detail-label">治理决策(' + t.decisions.length + ')</div>';
|
|
831
|
+
t.decisions.forEach(d => {
|
|
832
|
+
const lv = d.level || 'info';
|
|
833
|
+
const badgeClass = lv === 'block' ? 'badge-block' : lv === 'warn' ? 'badge-warn' : lv === 'allow' ? 'badge-allow' : 'badge-info';
|
|
834
|
+
const lvLabel = lv === 'block' ? '阻断' : lv === 'warn' ? '警告' : lv === 'allow' ? '放行' : lv === 'confirm' ? '确认' : lv;
|
|
835
|
+
html += '<div style="padding:0.5rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.375rem;border:1px solid var(--border);font-size:0.8rem">';
|
|
836
|
+
html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px"><span class="badge ' + badgeClass + '">' + lvLabel + '</span><span style="font-family:monospace;font-size:0.7rem;color:var(--text-dim)">' + (d.rule_id || '') + '</span></div>';
|
|
837
|
+
html += '<div style="color:var(--text-muted)">' + (d.reason || '—') + '</div>';
|
|
838
|
+
html += '</div>';
|
|
839
|
+
});
|
|
840
|
+
html += '</div>';
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Recent events
|
|
844
|
+
if (t.events && t.events.length > 0) {
|
|
845
|
+
html += '<div class="detail-section"><div class="detail-label">事件(展示 ' + Math.min(t.events.length, 30) + ' / ' + t.events.length + ')</div>';
|
|
846
|
+
t.events.slice(0, 30).forEach(e => {
|
|
847
|
+
const inputPreview = e.tool_input ? (JSON.stringify(e.tool_input).slice(0, 80) + (JSON.stringify(e.tool_input).length > 80 ? '...' : '')) : '';
|
|
848
|
+
html += '<div style="padding:0.5rem;border-bottom:1px solid var(--border);font-size:0.75rem">';
|
|
849
|
+
html += '<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:2px">';
|
|
850
|
+
html += '<span style="color:var(--text-dim)">' + fmtTime(e.timestamp) + '</span>';
|
|
851
|
+
html += badgeHook(e.hook_type);
|
|
852
|
+
if (e.tool_name) html += '<span style="font-family:monospace;color:var(--text-muted)">' + e.tool_name + '</span>';
|
|
853
|
+
html += '</div>';
|
|
854
|
+
if (inputPreview) html += '<div style="font-family:monospace;color:var(--text-dim);font-size:0.7rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + inputPreview + '</div>';
|
|
855
|
+
html += '</div>';
|
|
856
|
+
});
|
|
857
|
+
html += '</div>';
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Back button
|
|
861
|
+
html = '<div style="margin-bottom:1rem"><button class="btn" onclick="openSessionDrawer(\'' + currentSessionData.session.session_id + '\')">← 返回会话</button></div>' + html;
|
|
862
|
+
|
|
863
|
+
openDrawer('任务:' + t.title.slice(0, 40), html);
|
|
787
864
|
}
|
|
865
|
+
|
|
866
|
+
// === Events ===
|
|
788
867
|
async function loadEvents() {
|
|
789
|
-
document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="loading"
|
|
868
|
+
document.getElementById('events-body').innerHTML = '<tr><td colspan="4" class="loading">加载中...</td></tr>';
|
|
790
869
|
try {
|
|
791
870
|
const res = await fetch(API + '/api/events?limit=100');
|
|
792
871
|
allEvents = await res.json();
|
|
793
872
|
renderEvents(allEvents);
|
|
794
873
|
} catch {
|
|
795
|
-
document.getElementById('events-body').innerHTML = '<tr><td colspan="4">' + empty('
|
|
874
|
+
document.getElementById('events-body').innerHTML = '<tr><td colspan="4">' + empty('加载失败') + '</td></tr>';
|
|
796
875
|
}
|
|
797
876
|
}
|
|
798
877
|
|
|
799
878
|
function renderEvents(list) {
|
|
800
879
|
document.getElementById('events-body').innerHTML = list.length === 0
|
|
801
|
-
? '<tr><td colspan="4">' + empty('
|
|
802
|
-
: list.map(e =>
|
|
803
|
-
<td style="color:var(--text-dim);font-size:0.8rem">${fmtTime(e.timestamp)}</td>
|
|
804
|
-
<td>${badgeHook(e.hook_type)}</td>
|
|
805
|
-
<td style="font-family:monospace;font-size:0.8rem">${e.tool_name || '—'}</td>
|
|
806
|
-
<td style="color:var(--text-dim);font-size:0.8rem">${(e.project_path||'').split('/').pop() || '—'}</td>
|
|
807
|
-
</tr>`).join('');
|
|
880
|
+
? '<tr><td colspan="4">' + empty('暂无事件') + '</td></tr>'
|
|
881
|
+
: list.map(e => '<tr onclick="openEventDrawer(' + JSON.stringify(e).replace(/"/g,'"') + ')"><td style="color:var(--text-dim);font-size:0.8rem">' + fmtTime(e.timestamp) + '</td><td>' + badgeHook(e.hook_type) + '</td><td style="font-family:monospace;font-size:0.8rem">' + (e.tool_name || '—') + '</td><td style="color:var(--text-dim);font-size:0.8rem">' + ((e.project_path||'').split('/').pop() || '—') + '</td></tr>').join('');
|
|
808
882
|
}
|
|
809
883
|
|
|
810
884
|
function filterEvents() {
|
|
@@ -817,84 +891,76 @@ function filterEvents() {
|
|
|
817
891
|
}
|
|
818
892
|
|
|
819
893
|
function openEventDrawer(e) {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
`;
|
|
828
|
-
openDrawer(e.hook_type + ' · ' + (e.tool_name || 'event'), html);
|
|
829
|
-
}
|
|
830
|
-
function sessionListItem(s) {
|
|
831
|
-
const prompt = (s.first_prompt || '(no prompt)').slice(0, 60);
|
|
832
|
-
return `<div class="list-item fade-in" onclick="openSessionDrawer('${s.session_id}')">
|
|
833
|
-
<div style="flex:1;min-width:0">
|
|
834
|
-
<div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">${prompt}</div>
|
|
835
|
-
<div style="font-size:0.75rem;color:var(--text-dim)">${fmt(s.start_time)} · ${s.event_count} events · ${s.session_id.slice(0,8)}</div>
|
|
836
|
-
</div>
|
|
837
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>
|
|
838
|
-
</div>`;
|
|
894
|
+
let html = '<div class="detail-section"><div class="detail-label">类型</div><div class="detail-value">' + badgeHook(e.hook_type) + '</div></div>';
|
|
895
|
+
html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(e.timestamp) + '</div></div>';
|
|
896
|
+
html += '<div class="detail-section"><div class="detail-label">工具</div><div class="detail-value" style="font-family:monospace">' + (e.tool_name || '—') + '</div></div>';
|
|
897
|
+
html += '<div class="detail-section"><div class="detail-label">项目</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + (e.project_path || '—') + '</div></div>';
|
|
898
|
+
html += '<div class="detail-section"><div class="detail-label">会话</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + (e.session_id || '—') + '</div></div>';
|
|
899
|
+
if (e.tool_input) html += '<div class="detail-section"><div class="detail-label">输入参数</div><pre class="detail-code">' + JSON.stringify(e.tool_input, null, 2) + '</pre></div>';
|
|
900
|
+
openDrawer(e.hook_type + ' · ' + (e.tool_name || '事件'), html);
|
|
839
901
|
}
|
|
840
902
|
|
|
841
|
-
|
|
842
|
-
|
|
903
|
+
// === Injections ===
|
|
904
|
+
async function loadInjections() {
|
|
905
|
+
document.getElementById('inj-list').innerHTML = loading();
|
|
843
906
|
try {
|
|
844
|
-
const res = await fetch(API + '/api/
|
|
845
|
-
|
|
846
|
-
|
|
907
|
+
const res = await fetch(API + '/api/injections?limit=100');
|
|
908
|
+
allInjections = await res.json();
|
|
909
|
+
renderInjections(allInjections);
|
|
847
910
|
} catch {
|
|
848
|
-
document.getElementById('
|
|
911
|
+
document.getElementById('inj-list').innerHTML = empty('加载失败');
|
|
849
912
|
}
|
|
850
913
|
}
|
|
851
914
|
|
|
852
|
-
function
|
|
853
|
-
document.getElementById('
|
|
854
|
-
? empty('No sessions found')
|
|
855
|
-
: list.map(s => sessionListItem(s)).join('');
|
|
915
|
+
function renderInjections(list) {
|
|
916
|
+
document.getElementById('inj-list').innerHTML = list.length === 0 ? empty('暂无注入记录') : list.map(inj => '<div class="list-item fade-in" onclick="openInjDrawer(' + JSON.stringify(inj).replace(/"/g,'"') + ')"><div style="flex:1;min-width:0"><div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:2px"><span class="badge badge-info">' + inj.injection_type + '</span><span style="font-size:0.75rem;color:var(--text-dim)">' + inj.source_handler + '</span></div><div style="font-size:0.8rem;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + inj.content.slice(0,80) + '</div><div style="font-size:0.75rem;color:var(--text-dim);margin-top:2px">' + fmt(inj.timestamp) + ' · ' + inj.session_id.slice(0,8) + '</div></div><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>').join('');
|
|
856
917
|
}
|
|
857
918
|
|
|
858
|
-
function
|
|
859
|
-
const q = document.getElementById('
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
s.session_id.toLowerCase().includes(q)
|
|
863
|
-
));
|
|
919
|
+
function filterInjections() {
|
|
920
|
+
const q = document.getElementById('inj-search').value.toLowerCase();
|
|
921
|
+
const h = document.getElementById('inj-handler').value;
|
|
922
|
+
renderInjections(allInjections.filter(inj => (!h || inj.source_handler === h) && (!q || inj.content.toLowerCase().includes(q) || inj.injection_type.toLowerCase().includes(q))));
|
|
864
923
|
}
|
|
865
924
|
|
|
866
|
-
|
|
867
|
-
|
|
925
|
+
function openInjDrawer(inj) {
|
|
926
|
+
let html = '<div class="detail-section"><div class="detail-label">类型</div><div class="detail-value"><span class="badge badge-info">' + inj.injection_type + '</span></div></div>';
|
|
927
|
+
html += '<div class="detail-section"><div class="detail-label">Handler</div><div class="detail-value">' + inj.source_handler + '</div></div>';
|
|
928
|
+
html += '<div class="detail-section"><div class="detail-label">时间</div><div class="detail-value">' + fmt(inj.timestamp) + '</div></div>';
|
|
929
|
+
html += '<div class="detail-section"><div class="detail-label">会话</div><div class="detail-value" style="font-family:monospace;font-size:0.8rem">' + inj.session_id + '</div></div>';
|
|
930
|
+
html += '<div class="detail-section"><div class="detail-label">注入内容</div><pre class="detail-code">' + inj.content + '</pre></div>';
|
|
931
|
+
openDrawer(inj.injection_type, html);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// === Rules ===
|
|
935
|
+
async function loadRules() {
|
|
936
|
+
document.getElementById('rules-list').innerHTML = loading();
|
|
868
937
|
try {
|
|
869
|
-
const res = await fetch(API + '/api/
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
<div class="detail-label">Session ID</div>
|
|
875
|
-
<div class="detail-value" style="font-family:monospace;font-size:0.8rem">${s.session_id}</div>
|
|
876
|
-
</div>
|
|
877
|
-
<div class="detail-section">
|
|
878
|
-
<div class="detail-label">Time</div>
|
|
879
|
-
<div class="detail-value">${fmt(s.start_time)} → ${fmt(s.end_time)}</div>
|
|
880
|
-
</div>
|
|
881
|
-
<div class="detail-section">
|
|
882
|
-
<div class="detail-label">First Prompt</div>
|
|
883
|
-
<div class="detail-value">${s.first_prompt || '—'}</div>
|
|
884
|
-
</div>
|
|
885
|
-
<div class="detail-section">
|
|
886
|
-
<div class="detail-label">Tasks (${data.tasks.length})</div>`;
|
|
887
|
-
data.tasks.forEach((t, i) => {
|
|
888
|
-
const tools = t.events.filter(e => e.tool_name).length;
|
|
889
|
-
html += `<div style="padding:0.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);margin-bottom:0.5rem;border:1px solid var(--border)">
|
|
890
|
-
<div style="font-weight:500;font-size:0.875rem;margin-bottom:4px">Task ${i+1}: ${t.title}</div>
|
|
891
|
-
<div style="font-size:0.75rem;color:var(--text-dim)">${fmtTime(t.start_time)} · ${tools} tools · ${t.summary.filesChanged.length} files</div>
|
|
892
|
-
${t.summary.commits.length > 0 ? `<div style="font-size:0.75rem;color:var(--green);margin-top:4px">✓ ${t.summary.commits.length} commit(s)</div>` : ''}
|
|
893
|
-
</div>`;
|
|
894
|
-
});
|
|
895
|
-
html += `</div>`;
|
|
896
|
-
openDrawer('Session · ' + s.session_id.slice(0,8), html);
|
|
897
|
-
} catch(e) {
|
|
898
|
-
openDrawer('Session Detail', empty('Failed to load: ' + e.message));
|
|
938
|
+
const res = await fetch(API + '/api/rules');
|
|
939
|
+
allRules = await res.json();
|
|
940
|
+
renderRules(allRules);
|
|
941
|
+
} catch {
|
|
942
|
+
document.getElementById('rules-list').innerHTML = empty('加载规则失败');
|
|
899
943
|
}
|
|
900
944
|
}
|
|
945
|
+
|
|
946
|
+
function renderRules(list) {
|
|
947
|
+
document.getElementById('rules-list').innerHTML = list.length === 0 ? empty('未找到规则') : list.map(r => '<div class="list-item fade-in" onclick="openRuleDrawer(' + JSON.stringify(r).replace(/"/g,'"') + ')"><div style="flex:1;min-width:0"><div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">' + (r.name || r.id) + '</div><div style="font-size:0.75rem;color:var(--text-dim)">' + (r.description || '') + '</div></div><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>').join('');
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function filterRules() {
|
|
951
|
+
const q = document.getElementById('rules-search').value.toLowerCase();
|
|
952
|
+
renderRules(allRules.filter(r => (r.name||'').toLowerCase().includes(q) || (r.description||'').toLowerCase().includes(q)));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function openRuleDrawer(r) {
|
|
956
|
+
let html = '<div class="detail-section"><div class="detail-label">名称</div><div class="detail-value" style="font-weight:600">' + (r.name || '—') + '</div></div>';
|
|
957
|
+
html += '<div class="detail-section"><div class="detail-label">描述</div><div class="detail-value">' + (r.description || '—') + '</div></div>';
|
|
958
|
+
if (r.stats) html += '<div class="detail-section"><div class="detail-label">统计</div><div class="detail-value">触发 ' + (r.stats.totalTriggers || 0) + ' 次 | 阻断 ' + (r.stats.blockCount || 0) + ' 次 | 警告 ' + (r.stats.warnCount || 0) + ' 次</div></div>';
|
|
959
|
+
openDrawer(r.name || '规则', html);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// === Init ===
|
|
963
|
+
nav('dashboard');
|
|
964
|
+
</script>
|
|
965
|
+
</body>
|
|
966
|
+
</html>
|