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
This commit is contained in:
1 parent
8ee5e98ea3
commit
f8acf31ebc
71 files changed
+15727
-1
No files matched your search
@@ -0,0 +1,116 @@
|
||||
// Command gateway runs the CrossRelay local gateway: it serves the
|
||||
// Anthropic Messages API for Claude Code and the management API for the
|
||||
// CrossRelay desktop app.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gitea.rutua.cn/kate522/CrossRelay/internal/config"
|
||||
"gitea.rutua.cn/kate522/CrossRelay/internal/gateway"
|
||||
)
|
||||
|
||||
// watchParentShutdown exits the gateway when the parent process goes
|
||||
// away. The Tauri shell spawns the gateway with a piped stdin and sets
|
||||
// CROSSRELAY_WATCH_PARENT=1; when the app exits, crashes, or is killed,
|
||||
// the OS closes the pipe and the gateway shuts down instead of lingering
|
||||
// as an orphan.
|
||||
func watchParentShutdown(httpSrv *http.Server, statePath string, done chan struct{}) {
|
||||
if os.Getenv("CROSSRELAY_WATCH_PARENT") != "1" {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
buf := make([]byte, 512)
|
||||
for {
|
||||
if _, err := os.Stdin.Read(buf); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = httpSrv.Shutdown(ctx)
|
||||
_ = os.Remove(statePath)
|
||||
close(done)
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lmsgprefix)
|
||||
log.SetPrefix("crossrelay: ")
|
||||
|
||||
store, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("load config: %v", err)
|
||||
}
|
||||
|
||||
addr := "127.0.0.1:0"
|
||||
cfg := store.Config()
|
||||
if cfg.Gateway.PortMode == config.PortModeFixed && cfg.Gateway.FixedPort > 0 {
|
||||
addr = fmt.Sprintf("127.0.0.1:%d", cfg.Gateway.FixedPort)
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "CROSSRELAY_LISTEN_FAILED %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
|
||||
srv := gateway.New(store, port)
|
||||
httpSrv := &http.Server{Handler: srv.Handler()}
|
||||
|
||||
statePath := filepath.Join(config.Dir(), "state.json")
|
||||
if err := writeState(statePath, port); err != nil {
|
||||
log.Printf("write state file: %v", err)
|
||||
}
|
||||
|
||||
// The desktop app parses this line to learn the actual port.
|
||||
fmt.Printf("CROSSRELAY_LISTENING 127.0.0.1:%d\n", port)
|
||||
|
||||
shutdownDone := make(chan struct{})
|
||||
watchParentShutdown(httpSrv, statePath, shutdownDone)
|
||||
go func() {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sig
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = httpSrv.Shutdown(ctx)
|
||||
_ = os.Remove(statePath)
|
||||
close(shutdownDone)
|
||||
}()
|
||||
|
||||
if err := httpSrv.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Fprintf(os.Stderr, "CROSSRELAY_SERVE_FAILED %s\n", err)
|
||||
_ = os.Remove(statePath)
|
||||
os.Exit(1)
|
||||
}
|
||||
<-shutdownDone
|
||||
}
|
||||
|
||||
// writeState records the running gateway for diagnostics.
|
||||
func writeState(path string, port int) error {
|
||||
state := map[string]any{
|
||||
"pid": os.Getpid(),
|
||||
"port": port,
|
||||
"started_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"version": gateway.Version,
|
||||
}
|
||||
data, err := json.MarshalIndent(state, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0o600)
|
||||
}
|
||||
Reference in new issue
Block a user