Agent/ui/audit-ui.ts
2026-08-06 22:17:39 +08:00

193 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-nocheck
import { getAll, clear, exportJson, subscribe, formatTs, formatType } from '../utils/audit-log.js';
let _badgeCountEl = null;
let _filter = 'all';
let _expanded = new Set();
let _renderFn = null;
export function initBadge(sidebarEl) {
_badgeCountEl = sidebarEl.querySelector('#audit-count');
sidebarEl.querySelector('#btn-open-audit').addEventListener('click', () => {
showAuditDialog();
});
subscribe(() => updateBadge());
updateBadge();
}
function updateBadge() {
if (_badgeCountEl) {
_badgeCountEl.textContent = getAll().length;
}
}
export function showAuditDialog() {
const overlay = document.createElement('div');
overlay.className = 'dialog-overlay';
overlay.innerHTML = `
<div class="dialog audit-dialog" role="dialog" aria-modal="true">
<div class="dialog-header">
<h2>审计日志</h2>
<button class="dialog-close" type="button" aria-label="关闭">×</button>
</div>
<div class="audit-toolbar">
<div class="audit-filters">
<button class="filter-btn active" data-filter="all" type="button">全部</button>
<button class="filter-btn" data-filter="user" type="button">用户</button>
<button class="filter-btn" data-filter="assistant" type="button">模型</button>
<button class="filter-btn" data-filter="tool" type="button">工具</button>
<button class="filter-btn" data-filter="system" type="button">系统</button>
<button class="filter-btn" data-filter="error" type="button">错误</button>
</div>
<div class="audit-toolbar-actions">
<button class="mini-btn" id="audit-export" type="button">导出 JSON</button>
<button class="mini-btn danger-btn" id="audit-clear" type="button">清空</button>
</div>
</div>
<div class="audit-table-wrapper">
<div class="audit-table-head">
<div class="col-time">时间</div>
<div class="col-type">类型</div>
<div class="col-summary">摘要</div>
<div class="col-action"></div>
</div>
<div class="audit-table-body" id="audit-tbody"></div>
</div>
<div class="audit-stats" id="audit-stats"></div>
</div>
`;
document.body.appendChild(overlay);
document.body.classList.add('modal-open');
const tbody = overlay.querySelector('#audit-tbody');
const statsEl = overlay.querySelector('#audit-stats');
const renderTable = () => {
const all = getAll();
const filtered = _filter === 'all'
? all
: all.filter(e => e.type === _filter || e.type.startsWith(_filter + ':'));
const counts = {
user: all.filter(e => e.type === 'user').length,
assistant: all.filter(e => e.type === 'assistant').length,
tool: all.filter(e => e.type.startsWith('tool')).length,
system: all.filter(e => e.type === 'system').length,
error: all.filter(e => e.type === 'error').length,
};
statsEl.innerHTML = `共 <b>${all.length}</b> 条 · 用户 ${counts.user} / 模型 ${counts.assistant} / 工具 ${counts.tool} / 系统 ${counts.system} / 错误 ${counts.error}${_filter !== 'all' ? ` · 当前筛选 ${filtered.length}` : ''}`;
if (filtered.length === 0) {
tbody.innerHTML = '<div class="audit-empty">暂无日志</div>';
return;
}
tbody.innerHTML = '';
for (const entry of filtered) {
const expanded = _expanded.has(entry.id);
const hasDetails = !!entry.details;
const row = document.createElement('div');
row.className = `audit-row audit-${entry.type.replace(':', '-')} ${expanded ? 'expanded' : ''}`;
row.innerHTML = `
<div class="col-time">${formatTs(entry.ts)}</div>
<div class="col-type">${formatType(entry.type)}</div>
<div class="col-summary">${escapeHtml(entry.summary || '(空)')}</div>
<div class="col-action">${hasDetails ? `<span class="expand-icon">${expanded ? '▼' : '▶'}</span>` : ''}</div>
`;
if (hasDetails) {
row.addEventListener('click', () => {
if (_expanded.has(entry.id)) _expanded.delete(entry.id);
else _expanded.add(entry.id);
renderTable();
});
}
tbody.appendChild(row);
if (expanded && hasDetails) {
const detail = document.createElement('div');
detail.className = 'audit-detail';
const json = JSON.stringify(entry.details, null, 2);
const display = json.replace(/\\n/g, '\n').replace(/\\t/g, ' ');
detail.innerHTML = `<pre>${escapeHtml(display)}</pre>`;
tbody.appendChild(detail);
}
}
};
_renderFn = renderTable;
renderTable();
overlay.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
_filter = btn.dataset.filter;
overlay.querySelectorAll('.filter-btn').forEach(b => b.classList.toggle('active', b === btn));
renderTable();
});
});
overlay.querySelector('#audit-clear').addEventListener('click', () => {
if (!confirm('确定清空所有日志?此操作不可撤销。')) return;
clear();
_expanded.clear();
});
overlay.querySelector('#audit-export').addEventListener('click', () => {
exportJson();
});
const close = () => {
const unsub = subscribe(() => {
if (document.body.contains(overlay)) {
renderTable();
} else {
unsub();
}
});
overlay.remove();
document.body.classList.remove('modal-open');
};
overlay.querySelector('.dialog-close').addEventListener('click', close);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
const escHandler = (e) => {
if (e.key === 'Escape') {
close();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
const unsubOnLog = subscribe(() => {
if (document.body.contains(overlay)) {
renderTable();
} else {
unsubOnLog();
}
});
}
function escapeHtml(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}