341 lines
9.6 KiB
Go
341 lines
9.6 KiB
Go
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()})
|
|
}
|