package trace import ( "errors" "net/http" "strconv" "strings" "time" "aigateway.local/core/internal/identity" "aigateway.local/core/internal/platform/apiresponse" ) type AdminHTTPHandler struct { store *Store identity *identity.Service mux *http.ServeMux } func NewAdminHTTPHandler(store *Store, identityService *identity.Service) *AdminHTTPHandler { h := &AdminHTTPHandler{store: store, identity: identityService, mux: http.NewServeMux()} h.mux.HandleFunc("GET /api/v1/admin/traces", h.list) h.mux.HandleFunc("GET /api/v1/admin/traces/{id}", h.get) h.mux.HandleFunc("GET /api/v1/admin/agent-sessions", h.listSessions) return h } func (h *AdminHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) } func (h *AdminHTTPHandler) require(w http.ResponseWriter, r *http.Request) bool { account, err := h.identity.Authenticate(r.Context(), identity.KindAdmin, r.Header.Get("Authorization")) if err != nil { status := http.StatusUnauthorized if !errors.Is(err, identity.ErrInvalidSession) && !errors.Is(err, identity.ErrNotFound) { status = http.StatusServiceUnavailable } apiresponse.Error(w, status, "登录状态无效或身份服务暂不可用") return false } if !identity.HasPermission(account, identity.PermissionTraceRead) { apiresponse.Error(w, http.StatusForbidden, "缺少 LLM Trace 查看权限") return false } return true } func (h *AdminHTTPHandler) list(w http.ResponseWriter, r *http.Request) { if !h.require(w, r) { return } now := time.Now().UTC() from, err := queryTime(r, "from", now.Add(-24*time.Hour)) if err != nil { apiresponse.Error(w, http.StatusBadRequest, "from 时间无效") return } to, err := queryTime(r, "to", now.Add(time.Second)) if err != nil || !to.After(from) || to.Sub(from) > 366*24*time.Hour { apiresponse.Error(w, http.StatusBadRequest, "Trace 查询时间范围无效或超过 366 天") return } limit := 50 if value := strings.TrimSpace(r.URL.Query().Get("limit")); value != "" { parsed, scanErr := strconv.Atoi(value) if scanErr != nil || parsed < 1 || parsed > 200 { apiresponse.Error(w, http.StatusBadRequest, "limit 必须在 1 到 200 之间") return } limit = parsed } status := strings.TrimSpace(r.URL.Query().Get("status")) if status != "" && !validStatus(status) { apiresponse.Error(w, http.StatusBadRequest, "Trace 状态无效") return } traceType := strings.TrimSpace(r.URL.Query().Get("trace_type")) if traceType != "" && !validTraceType(traceType) { apiresponse.Error(w, http.StatusBadRequest, "Trace 类型无效") return } items, err := h.store.List(r.Context(), Filter{From: from, To: to, TraceType: traceType, TargetCode: strings.TrimSpace(r.URL.Query().Get("target_code")), RequestID: strings.TrimSpace(r.URL.Query().Get("request_id")), Status: status, Limit: limit}) if err != nil { apiresponse.Error(w, http.StatusServiceUnavailable, "Trace 查询服务暂不可用") return } apiresponse.OK(w, map[string]any{"items": items}) } func (h *AdminHTTPHandler) listSessions(w http.ResponseWriter, r *http.Request) { if !h.require(w, r) { return } now := time.Now().UTC() from, err := queryTime(r, "from", now.Add(-30*24*time.Hour)) if err != nil { apiresponse.Error(w, http.StatusBadRequest, "from 时间无效") return } to, err := queryTime(r, "to", now.Add(time.Second)) if err != nil || !to.After(from) || to.Sub(from) > 366*24*time.Hour { apiresponse.Error(w, http.StatusBadRequest, "会话查询时间范围无效或超过 366 天") return } limit := 50 if value := strings.TrimSpace(r.URL.Query().Get("limit")); value != "" { parsed, scanErr := strconv.Atoi(value) if scanErr != nil || parsed < 1 || parsed > 200 { apiresponse.Error(w, http.StatusBadRequest, "limit 必须在 1 到 200 之间") return } limit = parsed } traceType := strings.TrimSpace(r.URL.Query().Get("trace_type")) if traceType != "" && !validTraceType(traceType) { apiresponse.Error(w, http.StatusBadRequest, "会话类型无效") return } targetCode := strings.TrimSpace(r.URL.Query().Get("target_code")) if len(targetCode) > 128 { apiresponse.Error(w, http.StatusBadRequest, "目标编码过长") return } sessionID := strings.TrimSpace(r.URL.Query().Get("session_id")) if len(sessionID) > 512 { apiresponse.Error(w, http.StatusBadRequest, "会话 ID 过长") return } items, err := h.store.ListSessions(r.Context(), SessionFilter{From: from, To: to, TraceType: traceType, TargetCode: targetCode, SessionID: sessionID, Limit: limit}) if err != nil { apiresponse.Error(w, http.StatusServiceUnavailable, "智能体会话查询服务暂不可用") return } apiresponse.OK(w, map[string]any{"items": items}) } func (h *AdminHTTPHandler) get(w http.ResponseWriter, r *http.Request) { if !h.require(w, r) { return } item, err := h.store.Get(r.Context(), r.PathValue("id")) if errors.Is(err, ErrNotFound) { apiresponse.Error(w, http.StatusNotFound, "Trace 不存在") return } if err != nil { apiresponse.Error(w, http.StatusServiceUnavailable, "Trace 查询服务暂不可用") return } apiresponse.OK(w, item) } func queryTime(r *http.Request, name string, fallback time.Time) (time.Time, error) { value := strings.TrimSpace(r.URL.Query().Get(name)) if value == "" { return fallback, nil } return time.Parse(time.RFC3339, value) } func validTraceType(value string) bool { return value == "application" || value == "digital_employee" } func validStatus(value string) bool { return value == "running" || value == "success" || value == "error" }