diff --git a/backend/ai-core/cmd/main.go b/backend/ai-core/cmd/main.go
index d61f167..812c2df 100644
--- a/backend/ai-core/cmd/main.go
+++ b/backend/ai-core/cmd/main.go
@@ -426,6 +426,9 @@ func main() {
// 健康检查与对话API的HTTP mux
mux := http.NewServeMux()
+ // 注册全链路追踪事件端点
+ registerTraceEndpoint(mux)
+
// 初始化子会话管理器
subManager := subsession.NewManager(chatAdapter)
diff --git a/backend/ai-core/cmd/trace.go b/backend/ai-core/cmd/trace.go
new file mode 100644
index 0000000..2f545a3
--- /dev/null
+++ b/backend/ai-core/cmd/trace.go
@@ -0,0 +1,87 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strconv"
+ "sync"
+ "time"
+)
+
+// TraceEvent represents a single step in the message processing pipeline.
+type TraceEvent struct {
+ ID string `json:"id"`
+ Timestamp time.Time `json:"timestamp"`
+ SessionID string `json:"session_id,omitempty"`
+ UserID string `json:"user_id,omitempty"`
+ Hop string `json:"hop"` // message_received, intent, subsession, llm_call, tool_call, vision, synthesis, review, response, think
+ Label string `json:"label"`
+ Status string `json:"status"` // success, error, running
+ Detail string `json:"detail,omitempty"`
+ DurationMs int64 `json:"duration_ms,omitempty"`
+ Data map[string]interface{} `json:"data,omitempty"`
+}
+
+var (
+ traceMu sync.Mutex
+ traceEvents []TraceEvent
+ traceMax = 200
+)
+
+// AddTraceEvent appends a trace event to the in-memory ring buffer.
+func AddTraceEvent(hop, sessionID, userID, label, status, detail string, durationMs int64, data map[string]interface{}) {
+ traceMu.Lock()
+ defer traceMu.Unlock()
+ ev := TraceEvent{
+ ID: fmt.Sprintf("%s-%d", hop, time.Now().UnixNano()),
+ Timestamp: time.Now(),
+ SessionID: sessionID,
+ UserID: userID,
+ Hop: hop,
+ Label: label,
+ Status: status,
+ Detail: detail,
+ DurationMs: durationMs,
+ Data: data,
+ }
+ traceEvents = append(traceEvents, ev)
+ if len(traceEvents) > traceMax {
+ traceEvents = traceEvents[len(traceEvents)-traceMax:]
+ }
+}
+
+// GetTraceEvents returns recent trace events, optionally filtered by session.
+func GetTraceEvents(sessionID string, limit int) []TraceEvent {
+ traceMu.Lock()
+ defer traceMu.Unlock()
+ if limit <= 0 || limit > len(traceEvents) {
+ limit = len(traceEvents)
+ }
+ var result []TraceEvent
+ // Return newest first.
+ for i := len(traceEvents) - 1; i >= 0 && len(result) < limit; i-- {
+ ev := traceEvents[i]
+ if sessionID == "" || ev.SessionID == sessionID {
+ result = append(result, ev)
+ }
+ }
+ return result
+}
+
+// registerTraceEndpoint adds the trace events API.
+func registerTraceEndpoint(mux *http.ServeMux) {
+ mux.HandleFunc("/api/v1/trace/events", func(w http.ResponseWriter, r *http.Request) {
+ sessionID := r.URL.Query().Get("session_id")
+ limit := 100
+ if l, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && l > 0 && l <= 500 {
+ limit = l
+ }
+ events := GetTraceEvents(sessionID, limit)
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "events": events,
+ "total": len(events),
+ })
+ })
+}
diff --git a/ethend/public/index.html b/ethend/public/index.html
index bf6694e..42cb285 100644
--- a/ethend/public/index.html
+++ b/ethend/public/index.html
@@ -6284,15 +6284,51 @@ async function refreshTrace() {
}
}
- // 如果是首次加载(content 只有 loading 提示),全量渲染
+ // Try pipeline view first (new format)
+ var pipelines = data.pipelines || [];
+ if (pipelines.length > 0) {
+ var html = '
';
+ for (var pi = 0; pi < pipelines.length; pi++) {
+ var pl = pipelines[pi];
+ var steps = pl.steps || [];
+ var hasMsg = steps.some(function(s){ return s.hop === 'msg_received'; });
+ html += '
';
+ // Pipeline header
+ html += '
';
+ html += '📨 ' + escHtml(pl.sender || '?') + (pl.platform ? ' · ' + escHtml(pl.platform) : '') + '';
+ html += '' + (pl.timestamp || '').substring(11,19) + '';
+ html += '
';
+ // Pipeline steps
+ html += '
';
+ for (var si = 0; si < steps.length; si++) {
+ var s = steps[si];
+ var hopIcon = {msg_received:'📥',msg_sent:'📤',llm_call:'🧠',tool_call:'🔧',session:'👤'}[s.hop] || '●';
+ var hopColor = s.status === 'error' ? 'var(--red)' : (s.hop === 'msg_received' ? 'var(--blue)' : s.hop === 'msg_sent' ? 'var(--green)' : 'var(--text2)');
+ html += '
';
+ html += '' + hopIcon + '';
+ html += '' + escHtml(s.label) + '';
+ if (s.detail) html += '
' + escHtml(s.detail) + '';
+ if (s.durationMs > 0) html += ' (' + (s.durationMs >= 1000 ? (s.durationMs/1000).toFixed(1)+'s' : s.durationMs+'ms') + ')';
+ html += '
';
+ }
+ html += '
';
+ }
+ html += '
';
+ contentEl.innerHTML = html;
+ // Also update stats
+ updateTraceStatsEl(statsEl, traces, stats, data.session);
+ return;
+ }
+
+ // Fallback: flat timeline
var timeline = contentEl.querySelector('.trace-timeline');
if (!timeline) {
- if (traces.length === 0) {
+ if (traces.length === 0 && pipelines.length === 0) {
contentEl.innerHTML = '';
statsEl.innerHTML = '';
return;
}
- // 全量渲染
+ // 全量渲染 (old format)
var html = '';
for (var i = 0; i < traces.length; i++) {
html += traceHopHtml(traces[i], i > 0 ? traces[i-1] : null);
diff --git a/ethend/src/index.js b/ethend/src/index.js
index da46223..ce16d5c 100644
--- a/ethend/src/index.js
+++ b/ethend/src/index.js
@@ -1277,16 +1277,16 @@ function parseLogTimestamp(line) {
app.get('/api/trace/recent', async (req, res) => {
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
try {
- const [llmResult, toolResult, sessionsResult, bridgeLogsResult, aiCoreLogsResult] = await Promise.all([
+ const [llmResult, toolResult, sessionsResult, bridgeLogsResult, traceEventsResult] = 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: [] } }))
+ Promise.all((['obv11']).map(platform =>
+ proxyToPlatformBridge(`/api/v1/logs/${platform}?limit=50`).catch(() => ({ status: 502, body: { logs: [] } }))
)),
- // 拉 ai-core 最近的日志(解析处理事件)
- proxyToAICore('/api/v1/llm-calls/stream?limit=0').catch(() => ({ status: 502, body: [] })),
+ // 拉 ai-core 追踪事件
+ proxyToAICore(`/api/v1/trace/events?limit=${limit}`).catch(() => ({ status: 502, body: { events: [] } })),
]);
const traces = [];
@@ -1349,6 +1349,23 @@ app.get('/api/trace/recent', async (req, res) => {
});
}
+ // === ai-core 追踪事件 (intent, subsession, synthesis, etc.) ===
+ const traceEvents = traceEventsResult.body?.events || [];
+ for (const ev of traceEvents) {
+ const ts = ev.timestamp ? new Date(ev.timestamp).getTime() : Date.now();
+ traces.push({
+ id: ev.id || `trace-${ts}`,
+ timestamp: new Date(ts).toISOString(), ts,
+ service: 'ai-core',
+ hop: ev.hop || 'trace',
+ label: ev.label || '',
+ status: ev.status || 'success',
+ durationMs: ev.duration_ms || 0,
+ detail: ev.detail || '',
+ data: ev.data || {},
+ });
+ }
+
// === 活跃会话 ===
const users = sessionsResult.body?.users || {};
for (const [userID, sessions] of Object.entries(users)) {