package main import ( "encoding/json" "log" "net/http" "strconv" ) // startWebUI 在独立端口起一个只读管理界面,对外提供 dashboard + JSON API。 // 单独端口避免和反代端口冲突(反代会把所有路径转发上游)。 func startWebUI(addr string, rec *Recorder) { mux := webMux(rec) log.Printf("Web 管理界面启动: http://%s", addr) if err := http.ListenAndServe(addr, mux); err != nil { log.Printf("[!] Web 管理界面退出: %v", err) } } // webMux 组装管理界面的路由,单独抽出便于测试。 func webMux(rec *Recorder) http.Handler { mux := http.NewServeMux() // 首页:dashboard HTML mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(dashboardHTML)) }) // 聚合统计 mux.HandleFunc("GET /api/stats", func(w http.ResponseWriter, r *http.Request) { s, err := rec.Stats() if err != nil { writeErr(w, err) return } writeJSON(w, s) }) // 记录列表(支持 model/format/status/limit/offset 过滤) mux.HandleFunc("GET /api/records", func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() opts := ListOptions{ Model: q.Get("model"), Format: q.Get("format"), Limit: atoiDefault(q.Get("limit"), 100), Offset: atoiDefault(q.Get("offset"), 0), Status: atoiDefault(q.Get("status"), 0), } recs, total, err := rec.List(opts) if err != nil { writeErr(w, err) return } if recs == nil { recs = []Record{} } writeJSON(w, map[string]any{"total": total, "records": recs}) }) // 单条记录原文详情(含完整请求/响应) mux.HandleFunc("GET /api/records/{id}", func(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { http.Error(w, "无效 id", http.StatusBadRequest) return } b, err := rec.ReadBlob(id) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } writeJSON(w, b) }) return mux } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") if err := json.NewEncoder(w).Encode(v); err != nil { log.Printf("[!] 写 JSON 响应失败: %v", err) } } func writeErr(w http.ResponseWriter, err error) { http.Error(w, err.Error(), http.StatusInternalServerError) } func atoiDefault(s string, def int) int { if s == "" { return def } n, err := strconv.Atoi(s) if err != nil { return def } return n }