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) }