- 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
196 lines
5.5 KiB
Go
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()})
|
|
}
|