feat: 全链路追踪增强 + 超时修复

- ethend /api/trace/recent: 新增消息收发事件(platform-bridge日志)
- 消息按管线聚合: msg_received → llm_call → tool_call → msg_sent
- platform-bridge→ai-core超时 120s→180s

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-23 19:08:16 +08:00
parent 3081ac38e0
commit a8fa64325d
2 changed files with 73 additions and 31 deletions
+72 -30
View File
@@ -1275,87 +1275,129 @@ function parseLogTimestamp(line) {
// GET /api/trace/recent — 最近的全链路追踪数据
app.get('/api/trace/recent', async (req, res) => {
const limit = Math.min(parseInt(req.query.limit) || 30, 100);
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
try {
const [llmResult, toolResult, sessionsResult] = await Promise.all([
const [llmResult, toolResult, sessionsResult, bridgeLogsResult, aiCoreLogsResult] = await Promise.all([
proxyToAICore(`/api/v1/llm-calls?limit=${limit}`).catch(() => ({ status: 502, body: [] })),
proxyToAICore(`/api/v1/tools/calls?limit=${limit}`).catch(() => ({ status: 502, body: { calls: [] } })),
proxyToGateway('/api/v1/admin/sessions/active').catch(() => ({ status: 502, body: { users: {} } })),
// 拉 platform-bridge 最近的消息日志
Promise.all((['obv11', 'qq']).map(platform =>
proxyToPlatformBridge(`/api/v1/logs/${platform}?limit=50`).catch(() => ({ status: 502, body: { entries: [] } }))
)),
// 拉 ai-core 最近的日志(解析处理事件)
proxyToAICore('/api/v1/llm-calls/stream?limit=0').catch(() => ({ status: 502, body: [] })),
]);
const traces = [];
// LLM 调用 → 追踪节点
// === 消息收发 (platform-bridge 日志) ===
for (const logResult of bridgeLogsResult) {
const entries = logResult.body?.logs || logResult.body?.entries || logResult.body || [];
for (const entry of entries) {
const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
const isIncoming = entry.direction === 'incoming';
const hop = isIncoming ? 'msg_received' : 'msg_sent';
const icon = isIncoming ? '📥' : '📤';
const sender = entry.sender_name || entry.sender_id || '?';
const channel = entry.channel_id || '?';
const groupName = entry.group_name ? ` (${entry.group_name})` : '';
traces.push({
id: `msg-${ts}-${Math.random().toString(36).slice(2, 6)}`,
timestamp: new Date(ts).toISOString(), ts,
service: 'platform-bridge',
hop,
label: `${icon} ${isIncoming ? '收到' : '发送'}消息 · ${entry.platform || '?'}${groupName}`,
status: entry.success ? 'success' : 'error',
durationMs: 0,
detail: `${isIncoming ? '来自' : '发给'} ${sender} | 频道 ${channel} | ${(entry.content || '').substring(0, 80)}`,
data: { platform: entry.platform, sender, channel, content: entry.content, direction: entry.direction, contentType: entry.content_type },
});
}
}
// === LLM 调用 ===
const llmCalls = Array.isArray(llmResult.body) ? llmResult.body : (llmResult.body?.calls || []);
for (const call of llmCalls) {
const ts = call.time ? new Date(call.time).getTime() : Date.now();
traces.push({
id: `llm-${ts}-${Math.random().toString(36).slice(2, 6)}`,
timestamp: new Date(ts).toISOString(),
ts,
service: 'ai-core',
hop: 'llm_call',
label: `LLM 调用: ${call.model || 'unknown'}`,
timestamp: new Date(ts).toISOString(), ts,
service: 'ai-core', hop: 'llm_call',
label: `🧠 LLM: ${call.model || 'unknown'}`,
status: call.success ? 'success' : 'error',
durationMs: call.duration_ms || (call.Duration ? Math.round(call.Duration / 1e6) : 0),
detail: call.error || `${call.prompt_tokens || 0}+${call.completion_tokens || 0} tokens`,
detail: call.error || `${call.prompt_tokens || 0}${call.completion_tokens || 0} tokens`,
data: call,
});
}
// 工具调用
// === 工具调用 ===
const toolCalls = toolResult.body?.calls || (Array.isArray(toolResult.body) ? toolResult.body : []);
for (const tc of toolCalls) {
const ts = tc.time || tc.timestamp || tc.created_at;
const tsNum = ts ? new Date(ts).getTime() : Date.now();
traces.push({
id: `tool-${tsNum}-${Math.random().toString(36).slice(2, 6)}`,
timestamp: new Date(tsNum).toISOString(),
ts: tsNum,
service: 'ai-core',
hop: 'tool_call',
label: `工具调用: ${tc.tool_name || tc.name || 'unknown'}`,
timestamp: new Date(tsNum).toISOString(), ts: tsNum,
service: 'ai-core', hop: 'tool_call',
label: `🔧 ${tc.tool_name || tc.name || 'unknown'}`,
status: tc.error ? 'error' : 'success',
durationMs: tc.duration_ms || (tc.Duration ? Math.round(tc.Duration / 1e6) : 0),
detail: tc.error || tc.result?.substring?.(0, 100) || '',
detail: tc.error || tc.result?.substring?.(0, 100) || tc.output?.substring?.(0, 100) || '',
data: tc,
});
}
// 活跃会话
// === 活跃会话 ===
const users = sessionsResult.body?.users || {};
for (const [userID, sessions] of Object.entries(users)) {
for (const s of sessions) {
const ts = s.last_activity ? new Date(s.last_activity).getTime() : Date.now();
traces.push({
id: `session-${s.session_id || ''}`,
timestamp: new Date(ts).toISOString(),
ts,
service: 'gateway',
hop: 'session_active',
label: `会话活跃: ${userID}`,
status: 'success',
timestamp: new Date(ts).toISOString(), ts,
service: 'gateway', hop: 'session',
label: `👤 会话: ${userID}`,
status: s.state === 'streaming' ? 'running' : 'success',
durationMs: 0,
detail: `Session: ${(s.session_id || '').substring(0, 16)}... State: ${s.state || 'idle'}`,
detail: `${(s.session_id || '').substring(0, 20)}... [${s.state || 'idle'}]`,
data: { userID, sessionId: s.session_id, state: s.state },
});
}
}
// 按时间倒序排列
// 按时间倒序,按 hop 分组
traces.sort((a, b) => b.ts - a.ts);
const recent = traces.slice(0, limit);
// 统计摘要
// 按消息分组:将相近时间的 msg_received + llm_call + tool_call + msg_sent 聚合成消息管线
const pipelines = [];
let currentPipeline = null;
const sorted = [...recent].sort((a, b) => a.ts - b.ts);
for (const t of sorted) {
if (t.hop === 'msg_received') {
if (currentPipeline) pipelines.push(currentPipeline);
currentPipeline = { id: t.id, ts: t.ts, timestamp: t.timestamp, sender: t.data?.sender, channel: t.data?.channel, platform: t.data?.platform, content: t.data?.content, steps: [t] };
} else if (currentPipeline) {
// 只聚合时间相近的事件 (60s 内)
if (t.ts - currentPipeline.ts < 60000) {
currentPipeline.steps.push(t);
}
} else {
currentPipeline = { id: t.id, ts: t.ts, timestamp: t.timestamp, steps: [t] };
}
}
if (currentPipeline) pipelines.push(currentPipeline);
const services = [...new Set(recent.map(t => t.service))];
const errors = recent.filter(t => t.status === 'error').length;
const totalDuration = recent.reduce((sum, t) => sum + (t.durationMs || 0), 0);
res.json({
timestamp: Date.now(),
total: traces.length,
shown: recent.length,
stats: { services, errors, totalDurationMs: Math.round(totalDuration) },
total: traces.length, shown: recent.length,
pipelines: pipelines.slice(-limit),
stats: { services, errors, pipelineCount: pipelines.length },
traces: recent,
});
} catch (err) {