package gateway import ( "fmt" "net/http" "time" "gitea.rutua.cn/kate522/CrossRelay/internal/claudecfg" "gitea.rutua.cn/kate522/CrossRelay/internal/codexcfg" "gitea.rutua.cn/kate522/CrossRelay/internal/config" ) // providerInput is the provider payload accepted by the management API. type providerInput struct { Name string `json:"name"` Preset string `json:"preset"` BaseURL string `json:"base_url"` APIKey string `json:"api_key"` Models config.Models `json:"models"` } func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { cfg := s.store.Config() var active any if p := s.store.ActiveClaude(); p != nil { active = map[string]any{ "id": p.ID, "name": p.Name, "base_url": p.BaseURL, "models": p.Models, } } var activeCodex any if p := s.store.ActiveCodex(); p != nil { activeCodex = map[string]any{ "id": p.ID, "name": p.Name, "base_url": p.BaseURL, "models": p.Models, } } writeJSON(w, http.StatusOK, map[string]any{ "running": true, "version": Version, "port": s.port, "uptime_sec": int(time.Since(s.started).Seconds()), "active_claude_provider": active, "active_codex_provider": activeCodex, "stats": s.snapshotStats(), "language": cfg.Language, "port_mode": cfg.Gateway.PortMode, "fixed_port": cfg.Gateway.FixedPort, }) } func (s *Server) handleListProviders(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"items": s.store.Config().Providers}) } func (s *Server) handleCreateProvider(w http.ResponseWriter, r *http.Request) { var in providerInput if err := decodeJSON(r, &in, 1<<20); err != nil { writeAPIError(w, http.StatusBadRequest, "invalid request body: "+err.Error()) return } p, err := s.store.AddProvider(in.Name, in.Preset, in.BaseURL, in.APIKey, in.Models) if err != nil { writeAPIError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, p) } func (s *Server) handleUpdateProvider(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var in providerInput if err := decodeJSON(r, &in, 1<<20); err != nil { writeAPIError(w, http.StatusBadRequest, "invalid request body: "+err.Error()) return } p, err := s.store.UpdateProvider(id, in.Name, in.Preset, in.BaseURL, in.APIKey, in.Models) if err != nil { writeAPIError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, p) } func (s *Server) handleDeleteProvider(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := s.store.DeleteProvider(id); err != nil { writeAPIError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"deleted": id}) } // applyInput selects which client configuration receives the provider. type applyInput struct { Target string `json:"target"` } // handleApply activates a provider and writes it into the target // client's configuration. The body is optional; an absent or empty // target means Claude Code, keeping older clients working. func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") var in applyInput if r.Body != nil { _ = decodeJSON(r, &in, 1<<20) // an empty body leaves the zero value } switch in.Target { case "", "claude": s.applyClaude(w, id) case "codex": s.applyCodex(w, id) default: writeAPIError(w, http.StatusBadRequest, `target must be "claude" or "codex"`) } } // applyClaude activates a provider and writes it into Claude Code's // settings.json. func (s *Server) applyClaude(w http.ResponseWriter, id string) { p, err := s.store.ActivateClaude(id) if err != nil { writeAPIError(w, http.StatusNotFound, err.Error()) return } mgr, err := s.claudeManager() if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } settings, err := mgr.Apply(claudecfg.ApplyOptions{ BaseURL: fmt.Sprintf("http://127.0.0.1:%d", s.port), Token: s.store.Config().RelayToken, Models: p.Models, }) if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{ "provider": p, "target": "claude", "env": settings.Env, }) } // applyCodex activates a provider and writes it into Codex's // config.toml. func (s *Server) applyCodex(w http.ResponseWriter, id string) { p, err := s.store.ActivateCodex(id) if err != nil { writeAPIError(w, http.StatusNotFound, err.Error()) return } mgr, err := s.codexManager() if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } injection, err := mgr.Apply(codexcfg.ApplyOptions{ BaseURL: fmt.Sprintf("http://127.0.0.1:%d", s.port), Token: s.store.Config().RelayToken, }) if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } // Remember the provider the user had before the first apply so // restore can put it back instead of dropping the key. if injection.PriorProvider != "" && s.store.Config().CodexPriorProvider == "" { if err := s.store.SetCodexPriorProvider(injection.PriorProvider); err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } } writeJSON(w, http.StatusOK, map[string]any{ "provider": p, "target": "codex", "injection": injection, }) } // restoreInput selects the client and, optionally, one specific backup // file to roll back to. type restoreInput struct { Target string `json:"target"` Backup string `json:"backup,omitempty"` } // handleRestore puts a client's configuration back. Claude restores // the newest backup (or the named one); Codex removes the managed // keys, or rolls back to the named backup. func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) { var in restoreInput if r.Body != nil { _ = decodeJSON(r, &in, 1<<20) } switch in.Target { case "", "claude": s.restoreClaude(w, in.Backup) case "codex": s.restoreCodex(w, in.Backup) default: writeAPIError(w, http.StatusBadRequest, `target must be "claude" or "codex"`) } } func (s *Server) restoreClaude(w http.ResponseWriter, backup string) { mgr, err := s.claudeManager() if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } var name string if backup != "" { name, err = mgr.RestoreFrom(backup) } else { name, err = mgr.Restore() } if err != nil { writeAPIError(w, http.StatusNotFound, err.Error()) return } _ = s.store.ClearActiveClaude() writeJSON(w, http.StatusOK, map[string]any{"restored_from": name}) } func (s *Server) restoreCodex(w http.ResponseWriter, backup string) { mgr, err := s.codexManager() if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } if backup != "" { name, err := mgr.RestoreBackup(backup) if err != nil { writeAPIError(w, http.StatusNotFound, err.Error()) return } _ = s.store.ClearActiveCodex() writeJSON(w, http.StatusOK, map[string]any{"restored_from": name}) return } if err := mgr.RemoveManaged(s.store.Config().CodexPriorProvider); err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } _ = s.store.ClearActiveCodex() _ = s.store.SetCodexPriorProvider("") writeJSON(w, http.StatusOK, map[string]any{"restored_from": ""}) } func (s *Server) handleBackups(w http.ResponseWriter, r *http.Request) { switch target := r.URL.Query().Get("target"); target { case "", "claude": mgr, err := s.claudeManager() if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } items, err := mgr.Backups() if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } if items == nil { items = []claudecfg.BackupInfo{} } writeJSON(w, http.StatusOK, map[string]any{"items": items}) case "codex": mgr, err := s.codexManager() if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } items, err := mgr.Backups() if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } if items == nil { items = []codexcfg.BackupInfo{} } writeJSON(w, http.StatusOK, map[string]any{"items": items}) default: writeAPIError(w, http.StatusBadRequest, `target must be "claude" or "codex"`) } } func (s *Server) handleGetSettings(w http.ResponseWriter, r *http.Request) { cfg := s.store.Config() writeJSON(w, http.StatusOK, map[string]any{ "language": cfg.Language, "port_mode": cfg.Gateway.PortMode, "fixed_port": cfg.Gateway.FixedPort, }) } type settingsInput struct { Language string `json:"language"` PortMode string `json:"port_mode"` FixedPort int `json:"fixed_port"` } func (s *Server) handleUpdateSettings(w http.ResponseWriter, r *http.Request) { var in settingsInput if err := decodeJSON(r, &in, 1<<20); err != nil { writeAPIError(w, http.StatusBadRequest, "invalid request body: "+err.Error()) return } if in.Language != "" { if err := s.store.SetLanguage(in.Language); err != nil { writeAPIError(w, http.StatusBadRequest, err.Error()) return } } if in.PortMode != "" { if err := s.store.SetGateway(config.Gateway{ PortMode: in.PortMode, FixedPort: in.FixedPort, }); err != nil { writeAPIError(w, http.StatusBadRequest, err.Error()) return } } cfg := s.store.Config() writeJSON(w, http.StatusOK, map[string]any{ "language": cfg.Language, "port_mode": cfg.Gateway.PortMode, "fixed_port": cfg.Gateway.FixedPort, }) } func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"items": s.logs.list()}) }