package main import ( "crypto/subtle" "encoding/json" "fmt" "html/template" "net/http" "os" "path/filepath" "strconv" "strings" "time" "chat-agent-system/internal/handoff" ) type maintState struct { Message string Start time.Time End time.Time Hours float64 Token string ServerBin string DataDir string WindowID int64 } func main() { addr := envOr("MAINT_LISTEN_ADDR", "127.0.0.1:8080") st := maintState{ Message: envOr("MAINT_MESSAGE", "系统正在维护升级,请稍后再试。"), Token: strings.TrimSpace(os.Getenv("MAINT_ADMIN_TOKEN")), ServerBin: envOr("MAINT_SERVER_BIN", "./bin/agent-server"), DataDir: envOr("DATA_DIR", "./data/dev"), } if v := os.Getenv("MAINT_WINDOW_ID"); v != "" { st.WindowID, _ = strconv.ParseInt(v, 10, 64) } if v := os.Getenv("MAINT_START"); v != "" { st.Start, _ = time.Parse(time.RFC3339, v) } if v := os.Getenv("MAINT_END"); v != "" { st.End, _ = time.Parse(time.RFC3339, v) } if !st.End.IsZero() && !st.Start.IsZero() { st.Hours = st.End.Sub(st.Start).Hours() } ln, err := handoff.ListenOrAdopt(addr) if err != nil { fmt.Fprintf(os.Stderr, "maintpage listen: %v\n", err) os.Exit(1) } if err := handoff.SignalReady(); err != nil { fmt.Fprintf(os.Stderr, "maintpage ready: %v\n", err) } mux := http.NewServeMux() mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, 200, map[string]any{"ok": true, "mode": "maintenance"}) }) mux.HandleFunc("GET /api/v1/health", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, 200, map[string]any{"ok": true, "mode": "maintenance"}) }) mux.HandleFunc("GET /api/v1/maintenance/status", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, 200, map[string]any{ "active": true, "scheduled": false, "message": st.Message, "scheduledStart": st.Start, "scheduledEnd": st.End, "expectedHours": st.Hours, "windowId": st.WindowID, "status": "active", }) }) mux.HandleFunc("POST /api/v1/maintenance/admin/start", func(w http.ResponseWriter, r *http.Request) { token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if token == "" { token = r.URL.Query().Get("token") } if st.Token == "" || subtle.ConstantTimeCompare([]byte(token), []byte(st.Token)) != 1 { http.Error(w, "unauthorized", http.StatusUnauthorized) return } bin := st.ServerBin if !filepath.IsAbs(bin) { if abs, err := filepath.Abs(bin); err == nil { bin = abs } } extra := []string{ "MAINT_RESUME=1", fmt.Sprintf("MAINT_WINDOW_ID=%d", st.WindowID), fmt.Sprintf("DATA_DIR=%s", st.DataDir), } cmd, err := handoff.PassToChild(ln, bin, nil, extra, 30*time.Second) if err != nil { http.Error(w, "start failed: "+err.Error(), http.StatusInternalServerError) return } writeJSON(w, 200, map[string]any{"ok": true, "pid": cmd.Process.Pid}) go func() { time.Sleep(500 * time.Millisecond) os.Exit(0) }() }) mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { _ = pageTmpl.Execute(w, st) }) mux.HandleFunc("GET /admin", func(w http.ResponseWriter, r *http.Request) { _ = adminTmpl.Execute(w, st) }) srv := &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second} fmt.Printf("maintpage listening (mode=maintenance) window=%d\n", st.WindowID) if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed { fmt.Fprintf(os.Stderr, "maintpage serve: %v\n", err) os.Exit(1) } } func envOr(k, def string) string { if v := strings.TrimSpace(os.Getenv(k)); v != "" { return v } return def } func writeJSON(w http.ResponseWriter, code int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } var pageTmpl = template.Must(template.New("page").Parse(`
{{.Message}}
替换服务端二进制后,输入激活时展示的管理员 token,启动新服务并下线本公告页。
`))