mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 11:37:20 +08:00
145 lines
4.5 KiB
Go
145 lines
4.5 KiB
Go
// server.go 负责固定上游服务、流量捕获和调试界面的生命周期。
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// Server 运行 Cursor API 转发服务及其本机调试界面。
|
|
type Server struct {
|
|
config Config
|
|
upstream *url.URL
|
|
store *exchangeStore
|
|
counter atomic.Uint64
|
|
serviceServer *http.Server
|
|
serviceLn net.Listener
|
|
runMu sync.Mutex
|
|
captureMu sync.RWMutex
|
|
}
|
|
|
|
// New 创建固定转发到 Cursor API 的协议调试服务。
|
|
func New(config Config) (*Server, error) {
|
|
config = config.normalized()
|
|
if err := validateLoopbackAddress(config.ServiceAddr); err != nil {
|
|
return nil, fmt.Errorf("服务监听地址无效:%w", err)
|
|
}
|
|
upstream, err := url.Parse(defaultUpstreamURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("解析固定上游地址:%w", err)
|
|
}
|
|
store, err := newPersistentExchangeStore(config.DatabasePath, config.MaxExchanges)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
server := &Server{
|
|
config: config,
|
|
upstream: upstream,
|
|
store: store,
|
|
}
|
|
server.counter.Store(store.maxNumericID())
|
|
server.serviceServer = &http.Server{
|
|
Handler: server.newServiceHandler(),
|
|
ErrorLog: log.New(io.Discard, "", 0),
|
|
}
|
|
return server, nil
|
|
}
|
|
|
|
// Start 启动同时承载 API 转发和调试界面的单端口服务。
|
|
func (server *Server) Start() error {
|
|
server.runMu.Lock()
|
|
defer server.runMu.Unlock()
|
|
if server.serviceLn != nil {
|
|
return errors.New("Cursor API 调试服务已经启动")
|
|
}
|
|
serviceListener, err := net.Listen("tcp", server.config.ServiceAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("启动 API 服务监听失败:%w", err)
|
|
}
|
|
server.serviceLn = serviceListener
|
|
go func() { _ = server.serviceServer.Serve(serviceListener) }()
|
|
return nil
|
|
}
|
|
|
|
// Close 立即关闭监听器、活跃连接并释放捕获存储。
|
|
func (server *Server) Close() error {
|
|
server.runMu.Lock()
|
|
serviceServer := server.serviceServer
|
|
server.serviceLn = nil
|
|
server.runMu.Unlock()
|
|
var errorsList []error
|
|
if serviceServer != nil {
|
|
if err := serviceServer.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
errorsList = append(errorsList, err)
|
|
}
|
|
}
|
|
if server.store != nil {
|
|
if err := server.store.close(); err != nil {
|
|
errorsList = append(errorsList, err)
|
|
}
|
|
}
|
|
return errors.Join(errorsList...)
|
|
}
|
|
|
|
// ServiceAddr 返回 Cursor API 服务监听地址。
|
|
func (server *Server) ServiceAddr() string { return server.config.ServiceAddr }
|
|
|
|
// UIURL 返回可在浏览器中打开的调试界面地址。
|
|
func (server *Server) UIURL() string {
|
|
return "http://" + browserAddress(server.config.ServiceAddr) + debugBasePath + "/"
|
|
}
|
|
|
|
// DatabasePath 返回捕获数据库路径。
|
|
func (server *Server) DatabasePath() string {
|
|
return server.config.DatabasePath
|
|
}
|
|
|
|
// newServiceHandler 创建单端口调试路由和固定上游流式转发。
|
|
func (server *Server) newServiceHandler() http.Handler {
|
|
reverseProxy := httputil.NewSingleHostReverseProxy(server.upstream)
|
|
reverseProxy.FlushInterval = -1
|
|
reverseProxy.ErrorLog = log.New(io.Discard, "", 0)
|
|
originalDirector := reverseProxy.Director
|
|
reverseProxy.Director = func(request *http.Request) {
|
|
originalDirector(request)
|
|
request.Host = server.upstream.Host
|
|
request.Header["X-Forwarded-For"] = nil
|
|
}
|
|
reverseProxy.Transport = &http.Transport{
|
|
Proxy: nil,
|
|
DialContext: (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
|
ForceAttemptHTTP2: true,
|
|
DisableCompression: true,
|
|
MaxIdleConns: 200,
|
|
MaxIdleConnsPerHost: 32,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
reverseProxy.ModifyResponse = server.captureResponse
|
|
reverseProxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, upstreamErr error) {
|
|
server.failExchange(request, upstreamErr)
|
|
http.Error(writer, "Cursor API upstream unavailable", http.StatusBadGateway)
|
|
}
|
|
forwardHandler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
reverseProxy.ServeHTTP(writer, server.captureRequest(request))
|
|
})
|
|
debugHandler := http.StripPrefix(debugBasePath, server.newUIHandler())
|
|
mux := http.NewServeMux()
|
|
mux.Handle(debugBasePath+"/", debugHandler)
|
|
mux.HandleFunc(debugBasePath, func(writer http.ResponseWriter, request *http.Request) {
|
|
http.Redirect(writer, request, debugBasePath+"/", http.StatusTemporaryRedirect)
|
|
})
|
|
mux.Handle("/", forwardHandler)
|
|
return mux
|
|
}
|