opencode-cursor-agent/internal/router/router.go

148 lines
4.2 KiB
Go

package router
import (
"cursor-api-proxy/internal/config"
"cursor-api-proxy/internal/handlers"
"cursor-api-proxy/internal/httputil"
"cursor-api-proxy/internal/logger"
"cursor-api-proxy/internal/pool"
"fmt"
"net/http"
"os"
"time"
)
type RouterOptions struct {
Version string
Config config.BridgeConfig
ModelCache *handlers.ModelCacheRef
LastModel *string
Pool pool.PoolHandle
}
func NewRouter(opts RouterOptions) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cfg := opts.Config
pathname := r.URL.Path
method := r.Method
remoteAddress := r.RemoteAddr
if r.Header.Get("X-Real-IP") != "" {
remoteAddress = r.Header.Get("X-Real-IP")
}
logger.LogIncoming(method, pathname, remoteAddress)
defer func() {
logger.AppendSessionLine(cfg.SessionsLogPath, method, pathname, remoteAddress, 200)
}()
if cfg.RequiredKey != "" {
token := httputil.ExtractBearerToken(r)
if token != cfg.RequiredKey {
httputil.WriteJSON(w, 401, map[string]interface{}{
"error": map[string]string{"message": "Invalid API key", "code": "unauthorized"},
}, nil)
return
}
}
switch {
case method == "GET" && pathname == "/health":
handlers.HandleHealth(w, r, opts.Version, cfg)
case method == "GET" && pathname == "/v1/models":
opts.ModelCache.HandleModels(w, r, cfg)
case method == "POST" && pathname == "/v1/chat/completions":
raw, err := httputil.ReadBody(r)
if err != nil {
httputil.WriteJSON(w, 400, map[string]interface{}{
"error": map[string]string{"message": "failed to read body", "code": "bad_request"},
}, nil)
return
}
// 根據 Provider 選擇處理方式
provider := cfg.Provider
if provider == "" {
provider = "cursor"
}
if provider == "gemini-web" {
handlers.HandleGeminiChatCompletions(w, r, cfg, raw, method, pathname, remoteAddress)
} else {
handlers.HandleChatCompletions(w, r, cfg, opts.Pool, opts.LastModel, raw, method, pathname, remoteAddress)
}
case method == "POST" && pathname == "/v1/messages":
raw, err := httputil.ReadBody(r)
if err != nil {
httputil.WriteJSON(w, 400, map[string]interface{}{
"error": map[string]string{"message": "failed to read body", "code": "bad_request"},
}, nil)
return
}
handlers.HandleAnthropicMessages(w, r, cfg, opts.Pool, opts.LastModel, raw, method, pathname, remoteAddress)
case (method == "POST" || method == "GET") && pathname == "/v1/completions":
httputil.WriteJSON(w, 404, map[string]interface{}{
"error": map[string]string{
"message": "Legacy completions endpoint is not supported. Use POST /v1/chat/completions instead.",
"code": "not_found",
},
}, nil)
case pathname == "/v1/embeddings":
httputil.WriteJSON(w, 404, map[string]interface{}{
"error": map[string]string{
"message": "Embeddings are not supported by this proxy.",
"code": "not_found",
},
}, nil)
default:
httputil.WriteJSON(w, 404, map[string]interface{}{
"error": map[string]string{"message": "Not found", "code": "not_found"},
}, nil)
}
}
}
func recoveryMiddleware(logPath string, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
msg := fmt.Sprintf("%v", rec)
fmt.Fprintf(os.Stderr, "[%s] Proxy panic: %s\n", time.Now().UTC().Format(time.RFC3339), msg)
line := fmt.Sprintf("%s ERROR %s %s %s %s\n",
time.Now().UTC().Format(time.RFC3339), r.Method, r.URL.Path, r.RemoteAddr,
msg[:min(200, len(msg))])
if f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
_, _ = f.WriteString(line)
f.Close()
}
if !isHeaderWritten(w) {
httputil.WriteJSON(w, 500, map[string]interface{}{
"error": map[string]string{"message": msg, "code": "internal_error"},
}, nil)
}
}
}()
next(w, r)
}
}
func isHeaderWritten(w http.ResponseWriter) bool {
// Can't reliably detect without wrapping; always try to write
return false
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func WrapWithRecovery(logPath string, handler http.HandlerFunc) http.HandlerFunc {
return recoveryMiddleware(logPath, handler)
}