Files
CrossRelay/internal/gateway/handlers.go
T
kate522 f8acf31ebc CrossRelay stage 0+1: Go gateway with Anthropic<->Chat Completions relay, Tauri shell, bilingual management UI
- Go gateway (stdlib only): /v1/messages non-stream + SSE, /v1/messages/count_tokens,
  management API /api/* (providers CRUD, apply/restore, settings, backups, logs)
- Protocol conversion: Anthropic Messages <-> Chat Completions with a streaming
  state machine keyed by content block index (text, tool_use, parallel calls)
- ~/.claude/settings.json guarded read/write: timestamped backups, managed env
  keys only, one-click restore
- Tauri 2 shell: spawns the gateway sidecar, parses the random port from stdout,
  stdin pipe watchdog prevents orphans, kills the child on exit
- React frontend: Dashboard/Providers/Settings, zh-CN + en-US, TanStack Query
- Tests: Go table-driven + httptest integration (all green), vitest + RTL
2026-10-18 17:19:13 +08:00

196 lines
5.5 KiB
Go

package gateway
import (
"fmt"
"net/http"
"time"
"gitea.rutua.cn/kate522/CrossRelay/internal/claudecfg"
"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,
}
}
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,
"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})
}
// handleApply activates a provider and writes it into Claude Code's
// settings.json.
func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
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,
"env": settings.Env,
})
}
// handleRestore puts back the most recent backup of settings.json.
func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
mgr, err := s.claudeManager()
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
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) handleBackups(w http.ResponseWriter, r *http.Request) {
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})
}
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()})
}