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 }