fix: 消息日志增强 + 历史消息抑制 + SSE实时追踪 + 群聊上下文优化
- 日志:收/发消息均显示群名称,管理员显示真实QQ昵称而非"开拓者" - 历史消息:服务重启后NapCat回放的历史消息不再触发回复,静默注入上下文 - 消息时间戳:转发给AI时附带【消息时间: HH:MM:SS (XmXs前)】标记 - ♪ 分割符:QQ消息支持♪作为句子断点 - AI-Core SSE端点:全链路追踪实时推送,ethend不再5秒轮询 - 群聊上下文:AI-Core明确被告知消息来自群聊,以实际发送者为主语 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+206
-108
@@ -1200,8 +1200,8 @@ function switchPanel(name) {
|
||||
history.replaceState(null, '', '#' + name);
|
||||
}
|
||||
|
||||
// 停止链路追踪定时器
|
||||
stopTraceAutoRefresh();
|
||||
// 停止链路追踪 SSE
|
||||
disconnectTraceStream();
|
||||
|
||||
// 更新侧边栏
|
||||
document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
|
||||
@@ -1232,24 +1232,24 @@ function switchPanel(name) {
|
||||
|
||||
// 渲染面板
|
||||
switch (name) {
|
||||
case 'dashboard': renderDashboard(); startDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'memory': renderMemoryPanel(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'sessions': renderSessionsPanel(); startSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'services': renderServicesPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'iot': renderIoTPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); startIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'performance': renderPerformancePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'database': renderDatabasePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); startDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'vmMonitor': renderVMMonitorPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'toolCalls': renderToolCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'stt': renderSTTPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'plugins': renderPluginsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'thinking': renderThinkingPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'dashboard': renderDashboard(); startDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'memory': renderMemoryPanel(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'sessions': renderSessionsPanel(); startSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'services': renderServicesPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'iot': renderIoTPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); startIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'performance': renderPerformancePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'database': renderDatabasePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); startDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'vmMonitor': renderVMMonitorPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'toolCalls': renderToolCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'stt': renderSTTPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'plugins': renderPluginsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'thinking': renderThinkingPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'timeline': renderTimelinePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); startTimelineAutoRefresh(); break;
|
||||
case 'chatPlatforms': renderChatPlatformsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); startChatAutoRefresh(); break;
|
||||
case 'clients': renderClientsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'modelConfig': renderModelConfigPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'thinkingSchedule': renderThinkingSchedulePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'llmCalls': renderLlmCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
case 'clients': renderClientsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'modelConfig': renderModelConfigPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'thinkingSchedule': renderThinkingSchedulePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'llmCalls': renderLlmCallsPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); disconnectTraceStream(); break;
|
||||
case 'trace': renderTracePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); stopTimelineAutoRefresh(); break;
|
||||
}
|
||||
}
|
||||
@@ -6135,22 +6135,23 @@ async function renderVMMonitorPanel() {
|
||||
}
|
||||
|
||||
// ========== 全链路追踪面板 ==========
|
||||
var traceAutoRefreshId = null;
|
||||
var traceMode = 'recent'; // 'recent' | 'session'
|
||||
var traceSessionId = '';
|
||||
var traceStreamSource = null;
|
||||
var traceSeenIds = new Set();
|
||||
var traceMaxDomEntries = 200;
|
||||
|
||||
function renderTracePanel() {
|
||||
document.getElementById('panel-actions').innerHTML =
|
||||
'<button class="btn btn-sm" onclick="refreshTrace()" id="trace-refresh-btn">🔄 刷新</button>' +
|
||||
'<label style="font-size:11px;color:var(--text2);display:flex;align-items:center;gap:4px;cursor:pointer">' +
|
||||
'<input type="checkbox" id="trace-auto-refresh" onchange="toggleTraceAutoRefresh()"> 自动刷新 (5s)' +
|
||||
'</label>';
|
||||
'<span id="trace-live-status" style="font-size:11px;color:var(--green);display:flex;align-items:center;gap:4px">' +
|
||||
'<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--green);animation:pulse 2s infinite"></span> 实时</span>';
|
||||
|
||||
document.getElementById('panel-trace').innerHTML =
|
||||
'<div class="card">' +
|
||||
'<div class="card-header">' +
|
||||
'<span class="card-title">🔗 全链路消息追踪</span>' +
|
||||
'<span style="font-size:11px;color:var(--text2)">追踪 Client → Gateway → AI-Core → LLM 全链路</span>' +
|
||||
'<span style="font-size:11px;color:var(--text2)">追踪 Client → Gateway → AI-Core → LLM 全链路 (实时推送)</span>' +
|
||||
'</div>' +
|
||||
'<div class="trace-control-bar">' +
|
||||
'<button class="btn btn-sm ' + (traceMode === 'recent' ? 'btn-accent' : '') + '" onclick="switchTraceMode(\'recent\')">📋 最近活动</button>' +
|
||||
@@ -6166,35 +6167,69 @@ function renderTracePanel() {
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
connectTraceStream();
|
||||
refreshTrace();
|
||||
}
|
||||
|
||||
function switchTraceMode(mode) {
|
||||
traceMode = mode;
|
||||
stopTraceAutoRefresh();
|
||||
document.getElementById('trace-auto-refresh').checked = false;
|
||||
traceSeenIds.clear();
|
||||
renderTracePanel();
|
||||
}
|
||||
|
||||
function toggleTraceAutoRefresh() {
|
||||
var checked = document.getElementById('trace-auto-refresh').checked;
|
||||
if (checked) {
|
||||
startTraceAutoRefresh();
|
||||
// --- SSE 实时推送 ---
|
||||
|
||||
function connectTraceStream() {
|
||||
disconnectTraceStream();
|
||||
var url = getAICoreBaseUrl() + '/api/v1/llm-calls/stream';
|
||||
traceStreamSource = new EventSource(url);
|
||||
traceStreamSource.onmessage = function(e) {
|
||||
try {
|
||||
var call = JSON.parse(e.data);
|
||||
var trace = llmCallToTrace(call);
|
||||
if (trace && !traceSeenIds.has(trace.id)) {
|
||||
traceSeenIds.add(trace.id);
|
||||
addTraceToTimeline(trace, true);
|
||||
}
|
||||
} catch(ignore) {}
|
||||
};
|
||||
traceStreamSource.onerror = function() {
|
||||
updateTraceLiveStatus(false);
|
||||
traceStreamSource.close();
|
||||
setTimeout(function() {
|
||||
if (STATE.activePanel === 'trace') connectTraceStream();
|
||||
}, 3000);
|
||||
};
|
||||
traceStreamSource.onopen = function() {
|
||||
updateTraceLiveStatus(true);
|
||||
};
|
||||
}
|
||||
|
||||
function disconnectTraceStream() {
|
||||
if (traceStreamSource) {
|
||||
traceStreamSource.close();
|
||||
traceStreamSource = null;
|
||||
}
|
||||
updateTraceLiveStatus(false);
|
||||
}
|
||||
|
||||
function updateTraceLiveStatus(connected) {
|
||||
var el = document.getElementById('trace-live-status');
|
||||
if (!el) return;
|
||||
if (connected) {
|
||||
el.innerHTML = '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--green);animation:pulse 2s infinite"></span> 实时';
|
||||
el.style.color = 'var(--green)';
|
||||
} else {
|
||||
stopTraceAutoRefresh();
|
||||
el.innerHTML = '⚫ 离线';
|
||||
el.style.color = 'var(--text3)';
|
||||
}
|
||||
}
|
||||
|
||||
function startTraceAutoRefresh() {
|
||||
stopTraceAutoRefresh();
|
||||
traceAutoRefreshId = setInterval(function() {
|
||||
if (STATE.activePanel === 'trace') refreshTrace();
|
||||
}, 5000);
|
||||
function getAICoreBaseUrl() {
|
||||
return 'http://localhost:8081';
|
||||
}
|
||||
|
||||
function stopTraceAutoRefresh() {
|
||||
if (traceAutoRefreshId) { clearInterval(traceAutoRefreshId); traceAutoRefreshId = null; }
|
||||
}
|
||||
// --- 初始加载 & 手动刷新 ---
|
||||
|
||||
async function refreshTrace() {
|
||||
var contentEl = document.getElementById('trace-content');
|
||||
@@ -6202,8 +6237,6 @@ async function refreshTrace() {
|
||||
var btn = document.getElementById('trace-refresh-btn');
|
||||
if (btn) btn.classList.add('spinning');
|
||||
|
||||
// 热更新:不清空现有内容,仅在按钮上显示加载状态
|
||||
|
||||
var url, data;
|
||||
if (traceMode === 'session') {
|
||||
var sid = document.getElementById('trace-session-id').value.trim();
|
||||
@@ -6242,93 +6275,158 @@ async function refreshTrace() {
|
||||
var traces = data.traces || [];
|
||||
var stats = data.stats || {};
|
||||
|
||||
// 统计栏 — 热更新
|
||||
var servicesCount = (stats.services || []).length;
|
||||
var statsHtml =
|
||||
'<div class="trace-summary-item">📊 <span class="tsi-val">' + traces.length + '</span> 个追踪节点</div>' +
|
||||
'<div class="trace-summary-item">🖥 <span class="tsi-val">' + servicesCount + '</span> 个服务</div>' +
|
||||
(stats.errors > 0 ? '<div class="trace-summary-item" style="color:var(--red)">❌ <span class="tsi-val">' + stats.errors + '</span> 个错误</div>' : '<div class="trace-summary-item" style="color:var(--green)">✅ 全部成功</div>') +
|
||||
(stats.totalSpanMs > 0 ? '<div class="trace-summary-item">⏱ 总跨度 <span class="tsi-val">' + (stats.totalSpanMs / 1000).toFixed(2) + 's</span></div>' : '') +
|
||||
(stats.totalDurationMs > 0 ? '<div class="trace-summary-item">⏳ 总耗时 <span class="tsi-val">' + (stats.totalDurationMs / 1000).toFixed(2) + 's</span></div>' : '');
|
||||
|
||||
// Session info
|
||||
if (data.session) {
|
||||
var s = data.session;
|
||||
statsHtml +=
|
||||
'<div class="trace-summary-item">💬 Session: <code style="font-size:10px">' + escHtml((s.session_id || '').substring(0, 20)) + '...</code></div>' +
|
||||
'<div class="trace-summary-item">👤 User: ' + escHtml(s.user_id || '—') + '</div>';
|
||||
// 筛选出新的追踪节点
|
||||
var newTraces = [];
|
||||
for (var i = 0; i < traces.length; i++) {
|
||||
if (!traceSeenIds.has(traces[i].id)) {
|
||||
traceSeenIds.add(traces[i].id);
|
||||
newTraces.push(traces[i]);
|
||||
}
|
||||
}
|
||||
statsEl.innerHTML = statsHtml;
|
||||
|
||||
// 更新侧边栏徽章
|
||||
// 如果是首次加载(content 只有 loading 提示),全量渲染
|
||||
var timeline = contentEl.querySelector('.trace-timeline');
|
||||
if (!timeline) {
|
||||
if (traces.length === 0) {
|
||||
contentEl.innerHTML = '<div class="trace-empty"><div class="icon">📭</div>暂无链路追踪数据<br><span style="font-size:11px;color:var(--text3)">请确保服务正在运行且有消息活动</span></div>';
|
||||
statsEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
// 全量渲染
|
||||
var html = '<div class="trace-timeline">';
|
||||
for (var i = 0; i < traces.length; i++) {
|
||||
html += traceHopHtml(traces[i], i > 0 ? traces[i-1] : null);
|
||||
}
|
||||
html += '</div>';
|
||||
contentEl.innerHTML = html;
|
||||
} else if (newTraces.length > 0) {
|
||||
// 增量添加
|
||||
for (var j = newTraces.length - 1; j >= 0; j--) {
|
||||
addTraceToTimeline(newTraces[j], false);
|
||||
}
|
||||
pruneTimeline();
|
||||
}
|
||||
|
||||
// 更新统计
|
||||
updateTraceStatsEl(statsEl, traces, stats, data.session);
|
||||
|
||||
// 更新徽章
|
||||
var badge = document.getElementById('trace-badge');
|
||||
if (badge) {
|
||||
badge.textContent = traces.length;
|
||||
badge.style.display = traces.length > 0 ? 'inline-block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (traces.length === 0) {
|
||||
contentEl.innerHTML = '<div class="trace-empty"><div class="icon">📭</div>暂无链路追踪数据<br><span style="font-size:11px;color:var(--text3)">请确保服务正在运行且有消息活动</span></div>';
|
||||
return;
|
||||
// --- 增量 DOM 操作 ---
|
||||
|
||||
function addTraceToTimeline(trace, isLive) {
|
||||
var contentEl = document.getElementById('trace-content');
|
||||
var timeline = contentEl.querySelector('.trace-timeline');
|
||||
if (!timeline) return;
|
||||
|
||||
var gapIndicator = timeline.querySelector('div[data-gap]');
|
||||
var insertPoint = gapIndicator || timeline.firstChild;
|
||||
|
||||
var html = traceHopHtml(trace, null);
|
||||
var temp = document.createElement('div');
|
||||
temp.innerHTML = html;
|
||||
|
||||
while (temp.firstChild) {
|
||||
timeline.insertBefore(temp.firstChild, insertPoint);
|
||||
}
|
||||
|
||||
// 保存当前展开状态,重建 DOM 后恢复
|
||||
var expandedHops = [];
|
||||
var existingHops = contentEl.querySelectorAll('.trace-hop-detail.open');
|
||||
for (var ei = 0; ei < existingHops.length; ei++) {
|
||||
// 用 hop 的 label+timestamp 做 key 来恢复
|
||||
var prevHop = existingHops[ei].previousElementSibling;
|
||||
if (prevHop) {
|
||||
var hopLabel = prevHop.querySelector('.hop-label');
|
||||
var hopTime = prevHop.querySelector('.hop-time');
|
||||
if (hopLabel && hopTime) {
|
||||
expandedHops.push(hopTime.textContent + '|' + hopLabel.textContent);
|
||||
}
|
||||
if (isLive) {
|
||||
var newHop = timeline.querySelector('.trace-hop');
|
||||
if (newHop) {
|
||||
newHop.style.transition = 'background 0.3s';
|
||||
newHop.style.background = 'var(--accent-bg)';
|
||||
setTimeout(function() { newHop.style.background = ''; }, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染时间线
|
||||
var html = '<div class="trace-timeline">';
|
||||
pruneTimeline();
|
||||
}
|
||||
|
||||
for (var i = 0; i < traces.length; i++) {
|
||||
var t = traces[i];
|
||||
var time = new Date(t.timestamp).toISOString().replace('T', ' ').slice(11, 19);
|
||||
var isError = t.status === 'error';
|
||||
var hopKey = time + '|' + t.label;
|
||||
var wasExpanded = expandedHops.indexOf(hopKey) >= 0;
|
||||
var gapHtml = '';
|
||||
|
||||
// 显示与上一跳的时间间隔
|
||||
if (i > 0) {
|
||||
var gap = t.ts - traces[i - 1].ts;
|
||||
if (gap > 50) {
|
||||
gapHtml = '<div style="text-align:center;padding:2px 0;font-size:10px;color:var(--text3);margin-left:48px">↓ ' + (gap > 1000 ? (gap / 1000).toFixed(2) + 's' : gap + 'ms') + '</div>';
|
||||
}
|
||||
function pruneTimeline() {
|
||||
var timeline = document.querySelector('.trace-timeline');
|
||||
if (!timeline) return;
|
||||
var hops = timeline.querySelectorAll('.trace-hop');
|
||||
while (hops.length > traceMaxDomEntries) {
|
||||
var last = hops[hops.length - 1];
|
||||
var detail = last.nextElementSibling;
|
||||
if (detail && detail.classList.contains('trace-hop-detail')) {
|
||||
detail.remove();
|
||||
}
|
||||
last.remove();
|
||||
hops = timeline.querySelectorAll('.trace-hop');
|
||||
}
|
||||
}
|
||||
|
||||
html += gapHtml;
|
||||
function traceHopHtml(t, prevT) {
|
||||
var time = new Date(t.timestamp).toISOString().replace('T', ' ').slice(11, 19);
|
||||
var isError = t.status === 'error';
|
||||
var gapHtml = '';
|
||||
|
||||
if (prevT) {
|
||||
var gap = t.ts - prevT.ts;
|
||||
if (gap > 50) {
|
||||
gapHtml = '<div data-gap style="text-align:center;padding:2px 0;font-size:10px;color:var(--text3);margin-left:48px">↓ ' + (gap > 1000 ? (gap / 1000).toFixed(2) + 's' : gap + 'ms') + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
return gapHtml +
|
||||
'<div class="trace-hop ' + (isError ? 'error' : 'success') + '" data-id="' + escHtml(t.id) + '" onclick="toggleTraceHop(this)" title="点击展开详情">' +
|
||||
'<div class="hop-dot ' + (isError ? 'error' : escHtml(t.service)) + '"></div>' +
|
||||
'<span class="hop-time">' + escHtml(time) + '</span>' +
|
||||
'<span class="hop-service ' + escHtml(t.service) + '">' + escHtml(t.service) + '</span>' +
|
||||
'<span class="hop-label">' + escHtml(t.label) + '</span>' +
|
||||
(t.durationMs > 0 ? '<span class="hop-duration">' + (t.durationMs >= 1000 ? (t.durationMs / 1000).toFixed(2) + 's' : t.durationMs + 'ms') + '</span>' : '') +
|
||||
'<span class="hop-status">' + (isError ? '❌' : '✅') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="trace-hop-detail">' +
|
||||
'<div><strong>时间:</strong> ' + escHtml(t.timestamp) + '</div>' +
|
||||
'<div><strong>服务:</strong> ' + escHtml(t.service) + '</div>' +
|
||||
'<div><strong>节点:</strong> ' + escHtml(t.hop) + '</div>' +
|
||||
'<div><strong>标签:</strong> ' + escHtml(t.label) + '</div>' +
|
||||
(t.durationMs > 0 ? '<div><strong>耗时:</strong> ' + (t.durationMs >= 1000 ? (t.durationMs / 1000).toFixed(2) + 's' : t.durationMs + 'ms') + '</div>' : '') +
|
||||
'<div><strong>状态:</strong> ' + (isError ? '❌ 失败' : '✅ 成功') + '</div>' +
|
||||
(t.detail ? '<div style="margin-top:6px"><strong>详情:</strong><br>' + escHtml(String(t.detail)) + '</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function updateTraceStatsEl(statsEl, traces, stats, session) {
|
||||
var servicesCount = (stats.services || []).length;
|
||||
var html =
|
||||
'<div class="trace-summary-item">📊 <span class="tsi-val">' + traces.length + '</span> 个追踪节点</div>' +
|
||||
'<div class="trace-summary-item">🖥 <span class="tsi-val">' + servicesCount + '</span> 个服务</div>' +
|
||||
(stats.errors > 0 ? '<div class="trace-summary-item" style="color:var(--red)">❌ <span class="tsi-val">' + stats.errors + '</span> 个错误</div>' : '<div class="trace-summary-item" style="color:var(--green)">✅ 全部成功</div>') +
|
||||
(stats.totalSpanMs > 0 ? '<div class="trace-summary-item">⏱ 总跨度 <span class="tsi-val">' + (stats.totalSpanMs / 1000).toFixed(2) + 's</span></div>' : '') +
|
||||
(stats.totalDurationMs > 0 ? '<div class="trace-summary-item">⏳ 总耗时 <span class="tsi-val">' + (stats.totalDurationMs / 1000).toFixed(2) + 's</span></div>' : '');
|
||||
if (session) {
|
||||
var s = session;
|
||||
html +=
|
||||
'<div class="trace-hop ' + (isError ? 'error' : 'success') + '" onclick="toggleTraceHop(this)" title="点击展开详情">' +
|
||||
'<div class="hop-dot ' + (isError ? 'error' : escHtml(t.service)) + '"></div>' +
|
||||
'<span class="hop-time">' + escHtml(time) + '</span>' +
|
||||
'<span class="hop-service ' + escHtml(t.service) + '">' + escHtml(t.service) + '</span>' +
|
||||
'<span class="hop-label">' + escHtml(t.label) + '</span>' +
|
||||
(t.durationMs > 0 ? '<span class="hop-duration">' + (t.durationMs >= 1000 ? (t.durationMs / 1000).toFixed(2) + 's' : t.durationMs + 'ms') + '</span>' : '') +
|
||||
'<span class="hop-status">' + (isError ? '❌' : '✅') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="trace-hop-detail' + (wasExpanded ? ' open' : '') + '">' +
|
||||
'<div><strong>时间:</strong> ' + escHtml(t.timestamp) + '</div>' +
|
||||
'<div><strong>服务:</strong> ' + escHtml(t.service) + '</div>' +
|
||||
'<div><strong>节点:</strong> ' + escHtml(t.hop) + '</div>' +
|
||||
'<div><strong>标签:</strong> ' + escHtml(t.label) + '</div>' +
|
||||
(t.durationMs > 0 ? '<div><strong>耗时:</strong> ' + (t.durationMs >= 1000 ? (t.durationMs / 1000).toFixed(2) + 's' : t.durationMs + 'ms') + '</div>' : '') +
|
||||
'<div><strong>状态:</strong> ' + (isError ? '❌ 失败' : '✅ 成功') + '</div>' +
|
||||
(t.detail ? '<div style="margin-top:6px"><strong>详情:</strong><br>' + escHtml(String(t.detail)) + '</div>' : '') +
|
||||
'</div>';
|
||||
'<div class="trace-summary-item">💬 Session: <code style="font-size:10px">' + escHtml((s.session_id || '').substring(0, 20)) + '...</code></div>' +
|
||||
'<div class="trace-summary-item">👤 User: ' + escHtml(s.user_id || '—') + '</div>';
|
||||
}
|
||||
statsEl.innerHTML = html;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
contentEl.innerHTML = html;
|
||||
// 转换 SSE 推送的 LLM CallRecord 为 trace 格式
|
||||
function llmCallToTrace(call) {
|
||||
var ts = call.time ? new Date(call.time).getTime() : Date.now();
|
||||
return {
|
||||
id: 'llm-' + ts + '-' + Math.random().toString(36).slice(2, 6),
|
||||
timestamp: new Date(ts).toISOString(),
|
||||
ts: ts,
|
||||
service: 'ai-core',
|
||||
hop: 'llm_call',
|
||||
label: 'LLM 调用: ' + (call.model || 'unknown'),
|
||||
status: call.success ? 'success' : 'error',
|
||||
durationMs: call.duration_ms || call.Duration || 0,
|
||||
detail: call.error || (call.prompt_tokens || 0) + '+' + (call.completion_tokens || 0) + ' tokens',
|
||||
data: call
|
||||
};
|
||||
}
|
||||
|
||||
function toggleTraceHop(el) {
|
||||
|
||||
Reference in New Issue
Block a user