- 源码移至 src/go/,编译产物输出到 build/(不入库) - 新增 .gitignore:排除 data/、build/、*.exe、*.log、说明.md - 新增 build.ps1:从 src/go 编译到 build/relay.exe - 移除 说明.md 的版本跟踪(含上游密钥,仅保留在本地) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
104 lines
2.5 KiB
Go
104 lines
2.5 KiB
Go
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
|
||
}
|