feat: 全链路追踪系统 — ai-core追踪事件 + ethend管道视图

ai-core:
- 新增 TraceEvent 内存环形缓冲区(200条)
- GET /api/v1/trace/events 端点(支持session_id过滤)
- 预置 trace.go 追踪事件定义

ethend:
- trace/recent 聚合 platform-bridge消息日志 + ai-core追踪事件
- 消息按管线分组: msg_received→llm_call→tool_call→msg_sent
- 前端管线卡片视图: 每条消息显示完整处理步骤

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-23 19:11:55 +08:00
parent a8fa64325d
commit a47eeab808
4 changed files with 151 additions and 8 deletions
+87
View File
@@ -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),
})
})
}