重构项目结构并纳入版本管理

- 源码移至 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>
This commit is contained in:
2026-06-01 22:03:45 +08:00
parent ade410c289
commit 1216ad0145
16 changed files with 1738 additions and 0 deletions

103
src/go/web.go Normal file
View File

@@ -0,0 +1,103 @@
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
}