Ð޸´ȫÏîĿÉó²éÎÊÌⲢ·¢²¼ v1.1.9

¸ÄÓñ¾»ú¶ÀÁ¢ CA£¬»ָ´ CLI TLS УÑ飬±£»¤ƾ¾ÝÓëÅäÖò¢·¢±£´档
Ð޸´ Cursor Êý¾ݡ¢»Ự·¾¶¡¢´úÀíÉúÃüÖÜÆڡ¢ģÐÍÇëÇóÓë Skills Îļþ²Ù×÷¡£
Ϊ¶ÀÁ¢ Tab ·þÎñÔö¼ӿͻ§¶˼øȨ£¬Éý¼¶ÒÀÀµÓ빤¾ßÁ´£¬ÍêÉƷ¢²¼ԪÐÅϢͬ²½¡£

Ñé֤£ºÁ½¸ö Go ģ¿éµIJâÊÔ/vet¡¢¹ؼüģ¿é race¡¢ǰ¶˲âÊÔ/Éú²ú¹¹½¨/Éó¼ơ¢
Go ¿ɴï©¶´ɨÃèÓë macOS/Linux ¿ÉÐÐģ¿齻²æ±àÒë¾ùͨ¹ý¡£
Windows ¸ôÀëÆô¶¯ͨ¹ý£»δÍê³ɵĵã»÷ðÑ̺ÍԭÉúƽ̨Ñé֤¼ûÉó²鱨¸档
This commit is contained in:
FuDie0915
2026-11-10 09:59:34 +08:00
parent db73cf5a39
commit 188225fae6
98 changed files with 3324 additions and 930 deletions

View File

@@ -2,6 +2,7 @@ package client
import (
"context"
"fmt"
"cursor/internal/appdata"
serverconfig "cursor/internal/backend/server/config"
@@ -22,8 +23,8 @@ func (s *ProxyService) LoadUserConfig() (UserConfig, error) {
if app != nil {
ctx = app.Context()
}
if s.backendHost != nil {
return s.backendHost.LoadConfig(ctx)
if s.backend() != nil {
return s.backend().LoadConfig(ctx)
}
if s.store == nil {
return serverconfig.DefaultConfig(), nil
@@ -36,6 +37,9 @@ func (s *ProxyService) SaveUserConfig(cfg UserConfig) error {
if s == nil {
return nil
}
if cfg.Revision == "" {
return fmt.Errorf("配置缺少 revision,请重新加载后保存")
}
app := application.Get()
ctx := context.Background()
if app != nil {
@@ -45,8 +49,8 @@ func (s *ProxyService) SaveUserConfig(cfg UserConfig) error {
normalized UserConfig
err error
)
if s.backendHost != nil {
normalized, err = s.backendHost.SaveConfig(ctx, cfg)
if s.backend() != nil {
normalized, err = s.backend().SaveConfig(ctx, cfg)
} else if s.store != nil {
normalized, err = s.store.Save(ctx, cfg)
} else {

View File

@@ -1,6 +1,7 @@
package client
import (
"errors"
"fmt"
goruntime "runtime"
@@ -10,7 +11,7 @@ import (
// ApplyCursorSettings 用于处理与 ApplyCursorSettings 相关的逻辑。
func (s *ProxyService) ApplyCursorSettings() error {
if s == nil || s.proxy == nil {
if s == nil || s.proxyServer() == nil {
return fmt.Errorf("proxy is not initialized")
}
s.caFileMu.Lock()
@@ -37,7 +38,7 @@ func (s *ProxyService) ApplyCursorSettings() error {
}
}
proxyURL := cursor.ProxyURLFromListenAddr(s.proxy.Snapshot().ListenAddr)
proxyURL := cursor.ProxyURLFromListenAddr(s.proxyServer().Snapshot().ListenAddr)
if err := cursor.WriteUserProxySettings(proxyURL); err != nil {
return err
@@ -48,8 +49,8 @@ func (s *ProxyService) ApplyCursorSettings() error {
// 如果未检测到 CLI 安装,跳过但不报错
if cliPath := cursor.DetectCLIInstallPath(); cliPath != "" {
cliHTTPSAddr := ""
if s.backendHost != nil {
cliHTTPSAddr = s.backendHost.CLIHTTPSListenAddr()
if s.backend() != nil {
cliHTTPSAddr = s.backend().CLIHTTPSListenAddr()
}
if err := cursor.WriteCLIProxyWrapper(cliHTTPSAddr, caCertPath); err != nil {
logger.Errorf("write cli proxy wrapper failed: %v", err)
@@ -68,22 +69,21 @@ func (s *ProxyService) ApplyCursorSettings() error {
// ClearCursorSettings 用于处理与 ClearCursorSettings 相关的逻辑。
func (s *ProxyService) ClearCursorSettings() error {
var errs []error
if goruntime.GOOS == "darwin" {
if err := cursor.ClearSystemNodeExtraCACerts(); err != nil {
return err
errs = append(errs, err)
}
}
if err := cursor.ClearUserProxySettings(); err != nil {
return err
for _, clear := range []func() error{cursor.ClearUserProxySettings, cursor.ClearCLIProxyWrapper, cursor.ClearCLIAuthFile} {
if err := clear(); err != nil {
errs = append(errs, err)
}
}
if err := cursor.ClearCLIProxyWrapper(); err != nil {
logger.Errorf("clear cli proxy wrapper failed: %v", err)
if len(errs) == 0 {
s.setCursorSettingsApplied(false)
}
if err := cursor.ClearCLIAuthFile(); err != nil {
logger.Errorf("clear cli auth.json failed: %v", err)
}
s.setCursorSettingsApplied(false)
return nil
return errors.Join(errs...)
}
// GetDeviceID 用于处理与 GetDeviceID 相关的逻辑。

View File

@@ -58,8 +58,34 @@ type ProxyState struct {
// StartProxy 用于处理与 StartProxy 相关的逻辑。
func (s *ProxyService) StartProxy() (ProxyState, error) {
if s == nil {
return ProxyState{}, fmt.Errorf("服务未初始化")
}
s.lifecycleMu.Lock()
defer s.lifecycleMu.Unlock()
if s.backend() != nil && s.backend().IsRunning() && s.proxyServer() != nil && s.proxyServer().IsRunning() {
return s.GetState(), nil
}
startedBackend, startedProxy, settingsAttempted := false, false, false
rollback := func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if startedProxy && s.proxyServer() != nil {
_ = s.proxyServer().Stop(ctx)
}
if settingsAttempted {
if err := s.ClearCursorSettings(); err != nil {
logger.Error("startup rollback failed", "error", err)
}
}
if startedBackend && s.backend() != nil {
_ = s.backend().Stop(ctx)
}
}
logger.Infof("start service requested config_path=%s logs_root=%s", s.configPath, s.logsRoot)
fail := func(step string, err error) (ProxyState, error) {
rollback()
logger.Errorf("start service failed step=%s err=%v", step, err)
s.setLastError(err)
s.emitState()
@@ -72,51 +98,43 @@ func (s *ProxyService) StartProxy() (ProxyState, error) {
if err := s.ensureBackendHost(); err != nil {
return fail("ensure_backend_host", err)
}
if !s.backendHost.IsRunning() {
logger.Infof("starting embedded backend listen_addr=%s", s.backendHost.ListenAddr())
if err := s.backendHost.Start(); err != nil {
if !s.backend().IsRunning() {
logger.Infof("starting embedded backend listen_addr=%s", s.backend().ListenAddr())
if err := s.backend().Start(); err != nil {
return fail("start_backend", err)
}
startedBackend = true
} else {
logger.Infof("embedded backend already running listen_addr=%s", s.backendHost.ListenAddr())
logger.Infof("embedded backend already running listen_addr=%s", s.backend().ListenAddr())
}
healthCtx, healthCancel := context.WithTimeout(context.Background(), backendReadyTimeout)
defer healthCancel()
if err := s.waitForBackend(healthCtx); err != nil {
return fail("wait_backend_ready", err)
}
logger.Infof("embedded backend ready listen_addr=%s", s.backendHost.ListenAddr())
logger.Infof("embedded backend ready listen_addr=%s", s.backend().ListenAddr())
if err := s.ensureProxy(cfg); err != nil {
return fail("ensure_proxy", err)
}
if s.proxyServer() != nil && !s.proxyServer().IsRunning() {
logger.Infof("starting mitm proxy listen_addr=%s", s.proxyServer().Snapshot().ListenAddr)
if err := s.proxyServer().Start(); err != nil {
return fail("start_mitm_proxy", err)
}
startedProxy = true
}
settingsAttempted = true
if err := s.ApplyCursorSettings(); err != nil {
return fail("apply_cursor_settings", err)
}
// 启动时注入账号信息
if err := cursor.InjectCursorUserInfo(localruntime.InjectAccountEmail, localruntime.InjectAuthToken); err != nil {
logger.Errorf("injectCursorUserInfo failed: %v", err)
// 不阻断启动,仅记录日志
}
if s.proxy != nil && !s.proxy.IsRunning() {
logger.Infof("starting mitm proxy listen_addr=%s", s.proxy.Snapshot().ListenAddr)
if err := s.proxy.Start(); err != nil {
return fail("start_mitm_proxy", err)
}
}
if err := s.ApplyCursorSettings(); err != nil {
stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer stopCancel()
if s.proxy != nil {
_ = s.proxy.Stop(stopCtx)
}
_ = s.backendHost.Stop(stopCtx)
startErr := fmt.Errorf("服务已启动,但注入 Cursor 配置失败: %w", err)
logger.Errorf("start service failed step=apply_cursor_settings err=%v", startErr)
s.setLastError(startErr)
s.emitState()
return s.GetState(), startErr
}
s.setLastError(nil)
s.emitState()
state := s.GetState()
@@ -133,6 +151,11 @@ func (s *ProxyService) StartProxy() (ProxyState, error) {
// 各清理步骤失败时记录错误但继续执行后续步骤,保证 backend 最终被停止,
// 避免"Cursor 配置已清理但 backend 仍在运行"的不一致状态。
func (s *ProxyService) StopProxy() (ProxyState, error) {
if s == nil {
return ProxyState{}, nil
}
s.lifecycleMu.Lock()
defer s.lifecycleMu.Unlock()
logger.Infof("stop service requested")
var stopErrors []error
fail := func(step string, err error) {
@@ -144,15 +167,15 @@ func (s *ProxyService) StopProxy() (ProxyState, error) {
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if s.proxy != nil && s.proxy.IsRunning() {
logger.Infof("stopping mitm proxy listen_addr=%s", s.proxy.Snapshot().ListenAddr)
fail("stop_mitm_proxy", s.proxy.Stop(ctx))
if s.proxyServer() != nil && s.proxyServer().IsRunning() {
logger.Infof("stopping mitm proxy listen_addr=%s", s.proxyServer().Snapshot().ListenAddr)
fail("stop_mitm_proxy", s.proxyServer().Stop(ctx))
}
fail("clear_cursor_settings", s.ClearCursorSettings())
if s.backendHost != nil {
logger.Infof("stopping embedded backend listen_addr=%s", s.backendHost.ListenAddr())
fail("stop_backend", s.backendHost.Stop(ctx))
if s.backend() != nil {
logger.Infof("stopping embedded backend listen_addr=%s", s.backend().ListenAddr())
fail("stop_backend", s.backend().Stop(ctx))
}
if len(stopErrors) > 0 {
@@ -177,8 +200,8 @@ func (s *ProxyService) StopProxy() (ProxyState, error) {
// GetState 用于处理与 GetState 相关的逻辑。
func (s *ProxyService) GetState() ProxyState {
var proxySnap mitm.Snapshot
if s.proxy != nil {
proxySnap = s.proxy.Snapshot()
if s.proxyServer() != nil {
proxySnap = s.proxyServer().Snapshot()
}
s.mu.RLock()
lastError := s.lastError
@@ -188,11 +211,11 @@ func (s *ProxyService) GetState() ProxyState {
backendRunning := false
cliHTTPSListenAddr := ""
cliHTTPSRunning := false
if s.backendHost != nil {
backendListenAddr = s.backendHost.ListenAddr()
backendRunning = s.backendHost.IsRunning()
cliHTTPSListenAddr = s.backendHost.CLIHTTPSListenAddr()
cliHTTPSRunning = s.backendHost.CLIHTTPSRunning()
if s.backend() != nil {
backendListenAddr = s.backend().ListenAddr()
backendRunning = s.backend().IsRunning()
cliHTTPSListenAddr = s.backend().CLIHTTPSListenAddr()
cliHTTPSRunning = s.backend().CLIHTTPSRunning()
}
netProxy := netproxy.CurrentStatus()
return ProxyState{
@@ -263,20 +286,27 @@ func (s *ProxyService) emitState() {
// ShutdownForQuit 用于处理与 ShutdownForQuit 相关的逻辑。
func (s *ProxyService) ShutdownForQuit() {
s.lifecycleMu.Lock()
defer s.lifecycleMu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var finalErr error
if s.proxy != nil {
if err := s.proxy.Stop(ctx); err != nil && !errors.Is(err, context.Canceled) {
if s.proxyServer() != nil {
if err := s.proxyServer().Stop(ctx); err != nil && !errors.Is(err, context.Canceled) {
finalErr = err
}
}
if err := s.ClearCursorSettings(); err != nil {
finalErr = errors.Join(finalErr, err)
s.mu.RLock()
applied := s.cursorSettingsApplied
s.mu.RUnlock()
if applied {
if err := s.ClearCursorSettings(); err != nil {
finalErr = errors.Join(finalErr, err)
}
}
if s.backendHost != nil {
if err := s.backendHost.Stop(ctx); err != nil && !errors.Is(err, context.Canceled) {
if s.backend() != nil {
if err := s.backend().Stop(ctx); err != nil && !errors.Is(err, context.Canceled) {
finalErr = errors.Join(finalErr, err)
}
}

View File

@@ -0,0 +1,33 @@
package client
import (
serverconfig "cursor/internal/backend/server/config"
"cursor/internal/certs"
"fmt"
"sync"
"testing"
)
func TestStatePollingDuringProxyInitialization(t *testing.T) {
ca, err := certs.LoadOrCreateLocalCA(t.TempDir())
if err != nil {
t.Fatal(err)
}
service := &ProxyService{certManager: ca}
cfg := serverconfig.DefaultConfig()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for range 300 {
_ = service.GetState()
}
}()
for i := range 30 {
cfg.ProxyListenAddr = fmt.Sprintf("127.0.0.1:%d", 30000+i)
if err := service.ensureProxy(cfg); err != nil {
t.Fatal(err)
}
}
wg.Wait()
}

View File

@@ -9,7 +9,9 @@ import (
"strings"
"time"
"cursor/internal/logger"
"cursor/internal/modelchannel"
"cursor/internal/netproxy"
)
// RemoteModel 表示从模型服务 /models 端点拉取到的一个模型。
@@ -33,6 +35,7 @@ func (s *ProxyService) ListRemoteModels(adapterType string, baseURL string, apiK
adapterType = strings.ToLower(strings.TrimSpace(adapterType))
baseURL = strings.TrimSpace(baseURL)
apiKey = strings.TrimSpace(apiKey)
logger.RegisterSecrets(apiKey)
if adapterType != "openai" && adapterType != "anthropic" {
return result, fmt.Errorf("模型类型仅支持 openai 或 anthropic")
}
@@ -63,7 +66,7 @@ func (s *ProxyService) ListRemoteModels(adapterType string, baseURL string, apiK
}
request.Header.Set("Accept", "application/json")
response, err := http.DefaultClient.Do(request)
response, err := netproxy.CredentialSafeClient(http.DefaultClient).Do(request)
if err != nil {
return result, fmt.Errorf("请求模型列表失败: %w", err)
}

View File

@@ -73,4 +73,4 @@ func TestBuildModelsEndpointDefaults(t *testing.T) {
if !strings.HasPrefix(buildModelsEndpoint("openai", "https://example.com/v1"), "https://example.com/v1/") {
t.Fatalf("/v1 结尾去重后应保留原路径前缀")
}
}
}

View File

@@ -11,8 +11,8 @@ func (s *ProxyService) runtimeConfigSnapshot(_ context.Context) (localruntime.Ru
if s == nil {
return localruntime.RuntimeConfigSnapshot{}, nil
}
if s.backendHost != nil && s.backendHost.ConfigManager() != nil {
return s.backendHost.ConfigManager().LegacyRuntimeSnapshot(context.Background())
if s.backend() != nil && s.backend().ConfigManager() != nil {
return s.backend().ConfigManager().LegacyRuntimeSnapshot(context.Background())
}
if s.store == nil {
return localruntime.RuntimeConfigSnapshot{}, nil

View File

@@ -29,6 +29,7 @@ const (
// ProxyService 定义了当前模块中的 ProxyService 类型。
type ProxyService struct {
lifecycleMu sync.Mutex
// proxy 表示 IDE 版 MITM 代理。
proxy *mitm.ProxyServer
// certManager 用于在代理监听地址变化时重建 MITM 服务,同时为 CLI HTTPS 监听器签发证书。
@@ -98,34 +99,44 @@ func (s *ProxyService) ensureBackendHost() error {
if s == nil {
return nil
}
if s.backendHost != nil {
s.configMu.Lock()
defer s.configMu.Unlock()
if s == nil {
return nil
}
if s.backend() != nil {
return nil
}
host, err := backend.NewHost(s.store, s.certManager)
if err != nil {
return err
}
s.backendHost = host
s.setBackend(host)
return nil
}
func (s *ProxyService) ensureProxy(cfg serverconfig.Config) error {
if s == nil {
return nil
}
s.configMu.Lock()
defer s.configMu.Unlock()
if s == nil {
return nil
}
baseURL := ""
if s.backendHost != nil {
baseURL = s.backendHost.BaseURL()
if s.backend() != nil {
baseURL = s.backend().BaseURL()
}
if baseURL == "" {
baseURL = "http://" + cfg.BackendListenAddr
}
listenAddr := cfg.ProxyListenAddr
if s.proxy != nil {
snapshot := s.proxy.Snapshot()
if s.proxyServer() != nil {
snapshot := s.proxyServer().Snapshot()
if snapshot.ListenAddr == listenAddr {
return s.proxy.UpdateBaseURL(baseURL)
return s.proxyServer().UpdateBaseURL(baseURL)
}
if snapshot.Running {
return fmt.Errorf("代理正在运行,不能从 %s 切换到 %s,请先停止服务", snapshot.ListenAddr, listenAddr)
@@ -136,12 +147,12 @@ func (s *ProxyService) ensureProxy(cfg serverconfig.Config) error {
if err != nil {
return err
}
s.proxy = proxyServer
s.setProxy(proxyServer)
return nil
}
func (s *ProxyService) waitForBackend(ctx context.Context) error {
if s == nil || s.backendHost == nil {
if s == nil || s.backend() == nil {
return nil
}
ticker := time.NewTicker(backendHealthCheckInterval)
@@ -149,7 +160,7 @@ func (s *ProxyService) waitForBackend(ctx context.Context) error {
var lastErr error
for {
healthCtx, healthCancel := context.WithTimeout(ctx, backendHealthCheckAttemptTimeout)
err := s.backendHost.HealthCheck(healthCtx)
err := s.backend().HealthCheck(healthCtx)
healthCancel()
if err == nil {
return nil
@@ -165,3 +176,24 @@ func (s *ProxyService) waitForBackend(ctx context.Context) error {
}
}
}
func (s *ProxyService) backend() *backend.Host {
s.mu.RLock()
defer s.mu.RUnlock()
return s.backendHost
}
func (s *ProxyService) proxyServer() *mitm.ProxyServer {
s.mu.RLock()
defer s.mu.RUnlock()
return s.proxy
}
func (s *ProxyService) setBackend(host *backend.Host) {
s.mu.Lock()
defer s.mu.Unlock()
s.backendHost = host
}
func (s *ProxyService) setProxy(proxy *mitm.ProxyServer) {
s.mu.Lock()
defer s.mu.Unlock()
s.proxy = proxy
}