Files
Local-Cursor/internal/backend/host.go
FuDie0915 db73cf5a39 chore: import Local-Cursor v1.1.8 source snapshot
Includes the current working directory changes. Historical commits and release binaries are excluded to limit Gitea storage.
Source base: d7515973fbfd6dd2001fa156c483455b8081c7cb
2026-11-10 01:24:28 +08:00

966 lines
36 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package backend
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"time"
"cursor/internal/appdata"
"cursor/internal/backend/forwarder"
"cursor/internal/backend/server"
serverconfig "cursor/internal/backend/server/config"
"cursor/internal/backend/server/upstream"
"cursor/internal/certs"
"cursor/internal/logger"
"cursor/internal/netproxy"
legacyruntime "cursor/internal/runtime"
)
const healthPath = "/healthz"
type Host struct {
store *serverconfig.Store
listenAddr string
cliHTTPSListenAddr string
tabServerBaseURL string
configs *serverconfig.Manager
healthHTTP *http.Client
certManager *certs.Manager
runMu sync.RWMutex
httpServer *http.Server
httpsServer *http.Server
lastRunErr error
mux http.Handler
}
func NewHost(store *serverconfig.Store, certManager *certs.Manager) (*Host, error) {
if store == nil {
return nil, fmt.Errorf("backend config store is required")
}
configs, err := serverconfig.NewManager(context.Background(), store)
if err != nil {
return nil, err
}
cfg := configs.Current()
host := &Host{
store: store,
listenAddr: cfg.BackendListenAddr,
cliHTTPSListenAddr: cfg.CLIHTTPSListenAddr,
tabServerBaseURL: cfg.TabServerBaseURL,
configs: configs,
healthHTTP: newLoopbackHTTPClient(),
certManager: certManager,
}
if err := host.rebuild(cfg); err != nil {
return nil, err
}
return host, nil
}
func (host *Host) ConfigManager() *serverconfig.Manager {
if host == nil {
return nil
}
return host.configs
}
func (host *Host) LoadConfig(ctx context.Context) (serverconfig.Config, error) {
if host == nil || host.configs == nil {
return serverconfig.DefaultConfig(), nil
}
return host.configs.Load(ctx)
}
func (host *Host) SaveConfig(ctx context.Context, cfg serverconfig.Config) (serverconfig.Config, error) {
if host == nil || host.configs == nil {
return serverconfig.Config{}, fmt.Errorf("backend config manager is not initialized")
}
normalized, err := host.configs.Save(ctx, cfg)
if err != nil {
return serverconfig.Config{}, err
}
if host.httpServer == nil {
if rebuildErr := host.rebuild(normalized); rebuildErr != nil {
return serverconfig.Config{}, rebuildErr
}
}
return normalized, nil
}
func (host *Host) ListenAddr() string {
if host == nil {
return ""
}
host.runMu.RLock()
defer host.runMu.RUnlock()
return host.listenAddr
}
// CLIHTTPSListenAddr 返回 CLI HTTPS 监听地址。
func (host *Host) CLIHTTPSListenAddr() string {
if host == nil {
return ""
}
host.runMu.RLock()
defer host.runMu.RUnlock()
return host.cliHTTPSListenAddr
}
// CLIHTTPSRunning 返回 CLI HTTPS 监听器是否在运行。
func (host *Host) CLIHTTPSRunning() bool {
if host == nil {
return false
}
host.runMu.RLock()
defer host.runMu.RUnlock()
return host.httpsServer != nil
}
func (host *Host) BaseURL() string {
listenAddr := strings.TrimSpace(host.ListenAddr())
if listenAddr == "" {
return ""
}
return "http://" + listenAddr
}
func (host *Host) IsRunning() bool {
if host == nil {
return false
}
host.runMu.RLock()
defer host.runMu.RUnlock()
return host.httpServer != nil
}
func (host *Host) LastRunError() error {
if host == nil {
return nil
}
host.runMu.RLock()
defer host.runMu.RUnlock()
return host.lastRunErr
}
func (host *Host) Start() error {
if host == nil {
return fmt.Errorf("backend host is nil")
}
cfg := host.configs.Current()
host.runMu.Lock()
defer host.runMu.Unlock()
if host.httpServer != nil {
return fmt.Errorf("backend is already running")
}
if err := host.rebuildLocked(cfg); err != nil {
return err
}
httpServer := &http.Server{
Addr: host.listenAddr,
Handler: host.mux,
ReadHeaderTimeout: 10 * time.Second,
}
listener, err := net.Listen("tcp", host.listenAddr)
if err != nil {
host.lastRunErr = fmt.Errorf("监听内置后端 %s 失败: %w", host.listenAddr, err)
return host.lastRunErr
}
host.listenAddr = listener.Addr().String()
host.httpServer = httpServer
host.lastRunErr = nil
logger.Infof("内置后端监听成功 listen_addr=%s", host.listenAddr)
go func(serverInstance *http.Server, serverListener net.Listener) {
logger.Infof("内置后端开始提供服务 listen_addr=%s", serverListener.Addr().String())
if err := serverInstance.Serve(serverListener); err != nil && err != http.ErrServerClosed {
runErr := fmt.Errorf("内置后端在 %s 上异常退出: %w", serverListener.Addr().String(), err)
host.runMu.Lock()
if host.httpServer == serverInstance {
host.httpServer = nil
}
host.lastRunErr = runErr
host.runMu.Unlock()
logger.Errorf("%v", runErr)
}
}(httpServer, listener)
// 启动 CLI HTTPS+HTTP/2 监听器
if err := host.startHTTPSLocked(cfg); err != nil {
// HTTPS 失败不阻断 HTTP,仅记录
logger.Errorf("启动 CLI HTTPS 监听器失败: %v", err)
}
return nil
}
// startHTTPSLocked 启动 CLI HTTPS+HTTP/2 监听器。
// Go http.Server.ServeTLS 自动启用 HTTP/2 ALPN,完美兼容 connect-node 的 HTTP/2 传输。
// 证书由 certManager 为请求的 SNI 动态签发。
// 必须在 host.runMu.Lock() 下调用。
func (host *Host) startHTTPSLocked(cfg serverconfig.Config) error {
if host.certManager == nil {
logger.Infof("CLI HTTPS 监听器跳过: certManager 未配置")
return nil
}
addr := host.cliHTTPSListenAddr
tlsConfig := &tls.Config{
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
serverName := hello.ServerName
if serverName == "" {
serverName = "api2.cursor.sh"
}
return host.certManager.CertificateForServerName(serverName)
},
NextProtos: []string{"h2", "http/1.1"},
}
httpsServer := &http.Server{
Addr: addr,
Handler: host.mux,
TLSConfig: tlsConfig,
ReadHeaderTimeout: 10 * time.Second,
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("监听 CLI HTTPS %s 失败: %w", addr, err)
}
host.cliHTTPSListenAddr = ln.Addr().String()
host.httpsServer = httpsServer
go func(srv *http.Server, listener net.Listener) {
logger.Infof("CLI HTTPS 监听器开始提供服务 listen_addr=%s", listener.Addr().String())
if err := srv.ServeTLS(listener, "", ""); err != nil && err != http.ErrServerClosed {
logger.Errorf("CLI HTTPS 监听器异常退出: %v", err)
host.runMu.Lock()
if host.httpsServer == srv {
host.httpsServer = nil
}
host.runMu.Unlock()
}
}(httpsServer, ln)
logger.Infof("CLI HTTPS 监听器启动成功 listen_addr=%s", host.cliHTTPSListenAddr)
return nil
}
func (host *Host) Stop(ctx context.Context) error {
if host == nil {
return nil
}
host.runMu.Lock()
httpServer := host.httpServer
httpsServer := host.httpsServer
host.httpServer = nil
host.httpsServer = nil
host.runMu.Unlock()
var errs []error
if httpsServer != nil {
if err := httpsServer.Shutdown(ctx); err != nil {
errs = append(errs, fmt.Errorf("stop CLI HTTPS: %w", err))
}
}
if serverInstance := httpServer; serverInstance != nil {
err := serverInstance.Shutdown(ctx)
if err != nil && !errors.Is(err, context.Canceled) {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (host *Host) HealthCheck(ctx context.Context) error {
if host == nil {
return fmt.Errorf("backend host is nil")
}
if runErr := host.LastRunError(); runErr != nil {
return runErr
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, host.BaseURL()+healthPath, nil)
if err != nil {
return err
}
client := host.healthHTTP
if client == nil {
client = newLoopbackHTTPClient()
}
response, err := client.Do(request)
if err != nil {
inProcessErr := host.InProcessHealthCheck()
if inProcessErr == nil {
logger.Errorf("内置后端进程内健康检查成功,但 loopback 访问失败 base_url=%s err=%v", host.BaseURL(), err)
return fmt.Errorf("内置后端进程内健康检查成功,但本机 loopback 访问失败: %w", err)
}
logger.Errorf("内置后端 loopback 与进程内健康检查均失败 loopback_err=%v in_process_err=%v", err, inProcessErr)
if runErr := host.LastRunError(); runErr != nil {
return runErr
}
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("内置后端健康检查返回状态码 %d", response.StatusCode)
}
return nil
}
func (host *Host) InProcessHealthCheck() error {
if host == nil {
return fmt.Errorf("backend host is nil")
}
if host.mux == nil {
return fmt.Errorf("backend handler is nil")
}
request := httptest.NewRequest(http.MethodGet, "http://inprocess"+healthPath, nil)
recorder := httptest.NewRecorder()
host.mux.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
return fmt.Errorf("in-process health status %d", recorder.Code)
}
body := strings.TrimSpace(recorder.Body.String())
if body != "ok" {
return fmt.Errorf("in-process health body %q", body)
}
logger.Infof("内置后端进程内健康检查成功")
return nil
}
func newLoopbackHTTPClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DialContext: (&net.Dialer{
Timeout: 1 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: false,
MaxIdleConns: 1,
MaxIdleConnsPerHost: 1,
IdleConnTimeout: 30 * time.Second,
},
}
}
func (host *Host) rebuild(cfg serverconfig.Config) error {
host.runMu.Lock()
defer host.runMu.Unlock()
return host.rebuildLocked(cfg)
}
func (host *Host) rebuildLocked(cfg serverconfig.Config) error {
host.listenAddr = cfg.BackendListenAddr
host.tabServerBaseURL = cfg.TabServerBaseURL
agentModule := forwarder.NewModule(appdata.HistoryRootPath(), host.configs)
legacyBidiAppendProcedure := "/aiserver.v1.BidiService/BidiAppend"
legacyRunSSEProcedure := "/agent.v1.AgentService/RunSSE"
routeDeps := upstream.Dependencies{
SystemSettingService: &serverSystemSettings{configs: host.configs},
HTTPClient: netproxy.NewHTTPClient(30000 * time.Second),
}
host.mux = server.New(
server.Use(
server.Recover(),
server.ServerContext(),
server.PolicyMiddleware(host.configs),
server.ErrorEncoder(),
),
server.GET(healthPath,
server.Name("healthz"),
server.HTTP(),
server.Local(server.Health()),
),
server.POST(legacyBidiAppendProcedure,
server.Name("bidi_append"),
server.ConnectUnary(),
server.Local(server.HTTPHandlerAction(agentModule.LocalBidiHandler)),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "bidi_append",
})),
),
server.POST(legacyRunSSEProcedure,
server.Name("run_sse"),
server.ConnectStream(),
server.Local(server.HTTPHandlerAction(agentModule.LocalRunSSE)),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "run_sse",
})),
),
server.POST("/aiserver.v1.AiService/ServerTime",
server.Name("server_time"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "server_time",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.ServerTimeResponse",
MockBuilder: upstream.ServerTimeMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "server_time",
})),
),
server.POST("/aiserver.v1.AiService/GetServerConfig",
server.Name("server_config"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "server_config",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetServerConfigResponse",
MockBuilder: upstream.ServerConfigMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "server_config",
})),
),
// CLI 版 cursor-agent 通过独立的 ServerConfigService 获取服务器配置。
// 复用 AiService/GetServerConfig 的 mock builder。
server.POST("/aiserver.v1.ServerConfigService/GetServerConfig",
server.Name("server_config_service"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "server_config_service",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetServerConfigResponse",
MockBuilder: upstream.ServerConfigMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "server_config_service",
})),
),
server.POST("/aiserver.v1.AiService/AvailableModels",
server.Name("available_models"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "available_models",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.AvailableModelsResponse",
MockBuilder: upstream.AvailableModelsMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "available_models",
})),
),
// CLI 版 cursor-agent (2026.07+) 通过 AiService 获取可用模型列表和默认模型,
// 而非旧版 AgentService。两条路由使用相同的 mock builder。
server.POST("/aiserver.v1.AiService/GetUsableModels",
server.Name("ai_service_usable_models"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "ai_service_usable_models",
StatusCode: http.StatusOK,
MockProtoType: "agent.v1.GetUsableModelsResponse",
MockBuilder: upstream.AgentUsableModelsMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "ai_service_usable_models",
})),
),
server.POST("/aiserver.v1.AiService/GetDefaultModelForCli",
server.Name("ai_service_default_model_for_cli"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "ai_service_default_model_for_cli",
StatusCode: http.StatusOK,
MockProtoType: "agent.v1.GetDefaultModelForCliResponse",
MockBuilder: upstream.AgentDefaultModelForCliMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "ai_service_default_model_for_cli",
})),
),
// CLI 版 cursor-agent 通过 AgentService 获取可用模型列表和默认模型。
server.POST("/agent.v1.AgentService/GetUsableModels",
server.Name("agent_usable_models"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "agent_usable_models",
StatusCode: http.StatusOK,
MockProtoType: "agent.v1.GetUsableModelsResponse",
MockBuilder: upstream.AgentUsableModelsMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "agent_usable_models",
})),
),
server.POST("/agent.v1.AgentService/GetDefaultModelForCli",
server.Name("agent_default_model_for_cli"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "agent_default_model_for_cli",
StatusCode: http.StatusOK,
MockProtoType: "agent.v1.GetDefaultModelForCliResponse",
MockBuilder: upstream.AgentDefaultModelForCliMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "agent_default_model_for_cli",
})),
),
server.POST("/aiserver.v1.AiService/GetDefaultModelNudgeData",
server.Name("default_model_nudge"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "default_model_nudge",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetDefaultModelNudgeDataResponse",
MockBuilder: upstream.DefaultModelNudgeMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "default_model_nudge",
})),
),
server.POST("/aiserver.v1.AnalyticsService/BootstrapStatsig",
server.Name("bootstrap_statsig"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "bootstrap_statsig",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.BootstrapStatsigResponse",
MockBuilder: upstream.BootstrapStatsigMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "bootstrap_statsig",
})),
),
server.POST("/aiserver.v1.AnalyticsService/GetFirstWindowStatsigDecision",
server.Name("first_window_statsig_decision"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "first_window_statsig_decision",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetFirstWindowStatsigDecisionResponse",
MockBuilder: upstream.FirstWindowStatsigDecisionMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "first_window_statsig_decision",
})),
),
server.POST("/oauth/token",
server.Name("oauth_token"),
server.HTTP(),
server.Local(upstream.MockOAuthAction(routeDeps, upstream.CompatRouteConfig{
Name: "oauth_token",
StatusCode: http.StatusOK,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "oauth_token",
})),
),
server.POST("/aiserver.v1.AuthService/GetEmail",
server.Name("auth_service_get_email"),
server.ConnectUnary(),
server.Local(upstream.MockAuthEmailAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_service_get_email",
StatusCode: http.StatusOK,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_service_get_email",
})),
),
tabServerUpstreamProcedure("/aiserver.v1.AiService/StreamCpp", "ai_stream_cpp", server.ConnectStream(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.AiService/StreamNextCursorPrediction", "ai_stream_next_cursor_prediction", server.ConnectStream(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.AiService/GetCppEditClassification", "ai_get_cpp_edit_classification", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.AiService/RefreshTabContext", "ai_refresh_tab_context", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.AiService/CppConfig", "ai_cpp_config", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.AiService/CppEditHistoryStatus", "ai_cpp_edit_history_status", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.AiService/CppAppend", "ai_cpp_append", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.AiService/CppEditHistoryAppend", "ai_cpp_edit_history_append", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.AiService/ReportAiCodeChangeMetrics", "ai_report_ai_code_change_metrics", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.AiService/WriteGitCommitMessage", "ai_write_git_commit_message", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.AiService/WriteGitBranchName", "ai_write_git_branch_name", server.ConnectUnary(), routeDeps, host),
repositoryServiceProcedure(forwarder.RepositoryServiceFastRepoInitHandshakeV2Procedure, "repository_fast_repo_init_handshake_v2", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceFastRepoInitHandshakeProcedure, "repository_fast_repo_init_handshake", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceFastRepoSyncCompleteProcedure, "repository_fast_repo_sync_complete", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceSyncMerkleSubtreeV2Procedure, "repository_sync_merkle_subtree_v2", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceSyncMerkleSubtreeProcedure, "repository_sync_merkle_subtree", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceFastUpdateFileV2Procedure, "repository_fast_update_file_v2", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceFastUpdateFileProcedure, "repository_fast_update_file", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceEnsureIndexCreatedProcedure, "repository_ensure_index_created", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceGetCopyStatusProcedure, "repository_get_copy_status", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceGetUploadLimitsProcedure, "repository_get_upload_limits", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceGetNumFilesToSendProcedure, "repository_get_num_files_to_send", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceGetAvailableChunkingStrategiesProcedure, "repository_get_available_chunking_strategies", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceGetHighLevelFolderDescriptionProcedure, "repository_get_high_level_folder_description", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceRepositoryStatusProcedure, "repository_status", server.ConnectUnary(), agentModule, routeDeps),
repositoryServiceProcedure(forwarder.RepositoryServiceBatchRepositoryStatusProcedure, "repository_batch_status", server.ConnectUnary(), agentModule, routeDeps),
uploadServiceProcedure(forwarder.UploadServiceUploadDocumentationProcedure, "upload_documentation", server.ConnectUnary(), agentModule, routeDeps),
uploadServiceProcedure(forwarder.UploadServiceGetDocProcedure, "upload_get_doc", server.ConnectUnary(), agentModule, routeDeps),
uploadServiceProcedure(forwarder.UploadServiceGetPagesProcedure, "upload_get_pages", server.ConnectUnary(), agentModule, routeDeps),
uploadServiceProcedure(forwarder.UploadServiceUploadedStatusProcedure, "upload_uploaded_status", server.ConnectUnary(), agentModule, routeDeps),
server.Any("/aiserver.v1.AiService/*",
server.Name("ai_service"),
server.HTTP(),
server.Local(server.HTTPHandlerAction(agentModule.AiHandler)),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "ai_service",
})),
),
tabServerUpstreamProcedure("/aiserver.v1.CppService/AvailableModels", "cpp_available_models", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.CppService/RecordCppFate", "cpp_record_cpp_fate", server.ConnectUnary(), routeDeps, host),
server.Any("/aiserver.v1.CppService/*",
server.Name("cpp_service"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "cpp_service",
})),
),
tabServerUpstreamProcedure("/aiserver.v1.FileSyncService/FSSyncFile", "file_sync_sync_file", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.FileSyncService/FSIsEnabledForUser", "file_sync_is_enabled_for_user", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.FileSyncService/FSConfig", "file_sync_config", server.ConnectUnary(), routeDeps, host),
tabServerUpstreamProcedure("/aiserver.v1.FileSyncService/FSUploadFile", "file_sync_upload_file", server.ConnectUnary(), routeDeps, host),
server.Any("/aiserver.v1.FileSyncService/*",
server.Name("file_sync"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "file_sync",
})),
),
server.POST("/aiserver.v1.DashboardService/GetTokenUsage",
server.Name("dashboard_token_usage"),
server.HTTP(),
server.Local(server.HTTPHandlerAction(agentModule.AiHandler)),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_token_usage",
})),
),
server.POST("/aiserver.v1.DashboardService/GetGlassEarlyPreviewEnrollment",
server.Name("dashboard_glass_early_preview_enrollment"),
server.ConnectUnary(),
server.Local(server.HTTPHandlerAction(agentModule.AiHandler)),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_glass_early_preview_enrollment",
})),
),
server.POST("/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
server.Name("dashboard_current_period_usage"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_current_period_usage",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetCurrentPeriodUsageResponse",
MockBuilder: upstream.DashboardCurrentPeriodUsageMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_current_period_usage",
})),
),
server.POST("/aiserver.v1.DashboardService/GetTeams",
server.Name("dashboard_get_teams"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_teams",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetTeamsResponse",
MockBuilder: upstream.DashboardTeamsMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_teams",
})),
),
server.POST("/aiserver.v1.DashboardService/GetManagedSkills",
server.Name("dashboard_get_managed_skills"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_managed_skills",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetManagedSkillsResponse",
MockBuilder: upstream.DashboardManagedSkillsMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_managed_skills",
})),
),
server.POST("/aiserver.v1.DashboardService/GetMe",
server.Name("dashboard_get_me"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_me",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetMeResponse",
MockBuilder: upstream.DashboardGetMeMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_me",
})),
),
server.POST("/aiserver.v1.DashboardService/GetUserPrivacyMode",
server.Name("dashboard_user_privacy_mode"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_user_privacy_mode",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetUserPrivacyModeResponse",
MockBuilder: upstream.DashboardUserPrivacyModeMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_user_privacy_mode",
})),
),
server.POST("/aiserver.v1.DashboardService/GetPlanInfo",
server.Name("dashboard_plan_info"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_plan_info",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetPlanInfoResponse",
MockBuilder: upstream.DashboardPlanInfoMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_plan_info",
})),
),
server.POST("/aiserver.v1.DashboardService/GetUsageLimitStatusAndActiveGrants",
server.Name("dashboard_usage_limit_status"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_usage_limit_status",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetUsageLimitStatusAndActiveGrantsResponse",
MockBuilder: upstream.DashboardUsageLimitStatusAndActiveGrantsMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_usage_limit_status",
})),
),
server.POST("/aiserver.v1.DashboardService/IsOnNewPricing",
server.Name("dashboard_is_on_new_pricing"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_is_on_new_pricing",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.IsOnNewPricingResponse",
MockBuilder: upstream.DashboardIsOnNewPricingMockBuilder,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_is_on_new_pricing",
})),
),
// tabServerUpstreamProcedure("/aiserver.v1.DashboardService/GetEffectiveUserPlugins", "dashboard_get_effective_user_plugins", server.ConnectUnary(), routeDeps, host),
server.Any("/aiserver.v1.DashboardService/*",
server.Name("dashboard"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard",
})),
),
server.Any("/aiserver.v1.NetworkService/*",
server.Name("network_service"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "network_service",
})),
),
server.Any("/aiserver.v1.InAppAdService/*",
server.Name("in_app_ad"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "in_app_ad",
})),
),
server.GET("/auth/full_stripe_profile",
server.Name("auth_full_stripe_profile"),
server.HTTP(),
server.Local(upstream.MockAuthFullStripeProfileAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_full_stripe_profile",
StatusCode: http.StatusOK,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_full_stripe_profile",
})),
),
server.GET("/auth/stripe_profile",
server.Name("auth_stripe_profile"),
server.HTTP(),
server.Local(upstream.MockAuthStripeProfileAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_stripe_profile",
StatusCode: http.StatusOK,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_stripe_profile",
})),
),
server.GET("/auth/has_valid_payment_method",
server.Name("auth_has_valid_payment_method"),
server.HTTP(),
server.Local(upstream.MockJSONAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_has_valid_payment_method",
StatusCode: http.StatusOK,
JSONBody: map[string]any{
"hasValidPaymentMethod": true,
},
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_has_valid_payment_method",
})),
),
server.Any("/auth/poll",
server.Name("auth_poll"),
server.HTTP(),
server.Local(upstream.MockAuthPollAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_poll",
StatusCode: http.StatusOK,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_poll",
})),
),
server.POST("/auth/logout",
server.Name("auth_logout"),
server.HTTP(),
server.Local(upstream.FixedStatusAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_logout",
StatusCode: http.StatusNoContent,
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_logout",
})),
),
// CLI 版 cursor-agent 通过 loginWithApiKey 调用此端点交换 token。
// 返回 InjectAuthToken 使 CLI 跳过浏览器登录流程。
server.POST("/auth/exchange_user_api_key",
server.Name("auth_exchange_user_api_key"),
server.HTTP(),
server.Local(upstream.MockJSONAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_exchange_user_api_key",
StatusCode: http.StatusOK,
JSONBody: map[string]any{
"accessToken": legacyruntime.InjectAuthToken,
"refreshToken": legacyruntime.InjectAuthToken,
},
})),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_exchange_user_api_key",
})),
),
server.Any("/auth/*",
server.Name("auth_proxy"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
server.Upstream(upstream.DirectAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_proxy",
})),
),
)
return nil
}
func directUpstreamProcedure(pattern string, name string, protocol server.RouteOption, deps upstream.Dependencies) server.Option {
direct := upstream.DirectAction(deps, upstream.CompatRouteConfig{Name: name})
action := func(ctx *server.Context) error {
if ctx != nil && ctx.UpstreamURL == nil && ctx.Request != nil && ctx.Request.URL != nil {
targetURL := *ctx.Request.URL
targetURL.Scheme = "https"
targetURL.Host = "api2.cursor.sh:443"
ctx.UpstreamURL = &targetURL
}
return direct(ctx)
}
return server.POST(pattern,
server.Name(name),
protocol,
server.Local(action),
server.Upstream(action),
)
}
func repositoryServiceProcedure(pattern string, name string, protocol server.RouteOption, module *forwarder.Module, deps upstream.Dependencies) server.Option {
localAction := server.HTTPHandlerAction(module.RepositoryServiceHandler)
upstreamAction := upstream.DirectAction(deps, upstream.CompatRouteConfig{Name: name})
return server.POST(pattern,
server.Name(name),
protocol,
server.Local(localAction),
server.Upstream(upstreamAction),
)
}
func uploadServiceProcedure(pattern string, name string, protocol server.RouteOption, module *forwarder.Module, deps upstream.Dependencies) server.Option {
localAction := server.HTTPHandlerAction(module.UploadServiceHandler)
upstreamAction := upstream.DirectAction(deps, upstream.CompatRouteConfig{Name: name})
return server.POST(pattern,
server.Name(name),
protocol,
server.Local(localAction),
server.Upstream(upstreamAction),
)
}
func tabServerUpstreamProcedure(pattern string, name string, protocol server.RouteOption, deps upstream.Dependencies, host *Host) server.Option {
direct := upstream.DirectAction(deps, upstream.CompatRouteConfig{Name: name})
action := func(ctx *server.Context) error {
if host == nil || host.tabServerBaseURL == "" {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}
if ctx != nil && ctx.Request != nil && ctx.Request.URL != nil {
baseURL, err := url.Parse(host.tabServerBaseURL)
if err != nil {
return fmt.Errorf("解析 tab server 地址失败: %w", err)
}
targetURL := *ctx.Request.URL
targetURL.Scheme = baseURL.Scheme
targetURL.Host = baseURL.Host
ctx.UpstreamURL = &targetURL
}
return direct(ctx)
}
return server.POST(pattern,
server.Name(name),
protocol,
server.Local(action),
server.Upstream(action),
)
}
type serverSystemSettings struct {
configs *serverconfig.Manager
}
func (settings *serverSystemSettings) ResolveModelAdapters(ctx context.Context) ([]legacyruntime.ModelAdapterConfig, error) {
snapshot, err := settings.configs.LegacyRuntimeSnapshot(ctx)
if err != nil {
return nil, err
}
return snapshot.ModelAdapters, nil
}