package antigravity import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "runtime" "strconv" "strings" "sync" "sync/atomic" "time" ) // 上游 Antigravity auto-updater 服务,返回有序的版本数组(最新在前)。 // // 真实响应格式(截取): // // [{"version":"1.23.2","execution_id":"..."},{"version":"1.22.2",...},...] const antigravityReleasesURL = "https://antigravity-auto-updater-974169037036.us-central1.run.app/releases" const ( // versionRefreshInterval 与 CLIProxyAPI 一致:3 小时刷新一次真实版本号。 versionRefreshInterval = 3 * time.Hour // versionFetchTimeout 单次拉取超时;失败不影响请求路径,沿用旧版本号即可。 versionFetchTimeout = 5 * time.Second ) // versionFetcher 负责异步刷新 Antigravity 真实最新版本号。 // // 设计: // - 启动时若有 cached 版本则立即生效,否则保持兜底版本(defaultUserAgentVersion)。 // - 后台 goroutine 每 versionRefreshInterval 拉取一次。 // - 拉取失败不传播错误:保持现值即可(永远不让 UA 变成空字符串)。 // - 用户通过 ANTIGRAVITY_USER_AGENT_VERSION 显式指定版本时,禁用自动刷新。 type versionFetcher struct { httpClient *http.Client endpoint string mu sync.RWMutex current atomic.Pointer[string] once sync.Once stopCh chan struct{} overridden bool } var defaultVersionFetcher = newVersionFetcher() func newVersionFetcher() *versionFetcher { return &versionFetcher{ httpClient: &http.Client{Timeout: versionFetchTimeout}, endpoint: antigravityReleasesURL, stopCh: make(chan struct{}), } } // newVersionFetcherForTest 用于注入自定义 endpoint 进行单元测试。 func newVersionFetcherForTest(endpoint string) *versionFetcher { return &versionFetcher{ httpClient: &http.Client{Timeout: versionFetchTimeout}, endpoint: endpoint, stopCh: make(chan struct{}), } } // Current 返回当前缓存的版本号,未拉取过时返回空串。 func (f *versionFetcher) Current() string { if v := f.current.Load(); v != nil { return *v } return "" } // MarkOverridden 标记版本号被环境变量显式覆盖,避免后台刷新覆盖用户配置。 func (f *versionFetcher) MarkOverridden() { f.mu.Lock() defer f.mu.Unlock() f.overridden = true } // Start 启动后台刷新循环,幂等。 func (f *versionFetcher) Start() { f.once.Do(func() { go f.loop() }) } // Stop 停止后台刷新循环(用于测试)。 func (f *versionFetcher) Stop() { select { case <-f.stopCh: default: close(f.stopCh) } } func (f *versionFetcher) loop() { // 启动后立即拉一次,确保 UA 在第一次请求前已是真实版本(最多等待 versionFetchTimeout)。 f.refreshOnce() ticker := time.NewTicker(versionRefreshInterval) defer ticker.Stop() for { select { case <-f.stopCh: return case <-ticker.C: f.refreshOnce() } } } func (f *versionFetcher) refreshOnce() { f.mu.RLock() overridden := f.overridden f.mu.RUnlock() if overridden { return } ctx, cancel := context.WithTimeout(context.Background(), versionFetchTimeout) defer cancel() endpoint := f.endpoint if endpoint == "" { endpoint = antigravityReleasesURL } version, err := fetchVersionFromURL(ctx, f.httpClient, endpoint) if err != nil { // 失败不传播:保持现值;下个 tick 再试。 return } f.current.Store(&version) // 同步给 GetUserAgent 的兜底全局变量,使旧路径也能拿到新版本。 setDefaultUserAgentVersion(version) } // fetchVersionFromURL 从指定 URL 拉取最新版本号(数组首元素的 version 字段)。 // 抽离 endpoint 参数以便单元测试注入 httptest 服务器。 func fetchVersionFromURL(ctx context.Context, client *http.Client, endpoint string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return "", fmt.Errorf("build releases request: %w", err) } // 用真实客户端的 UA 模式拉取,使流量看起来像一次正常的更新检查。 req.Header.Set("User-Agent", fmt.Sprintf("antigravity-updater/%s %s/%s", currentUserAgentVersion(), runtime.GOOS, runtime.GOARCH)) req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("fetch releases: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("releases responded with status %d", resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) if err != nil { return "", fmt.Errorf("read releases body: %w", err) } var entries []struct { Version string `json:"version"` } if err := json.Unmarshal(body, &entries); err != nil { return "", fmt.Errorf("decode releases body: %w", err) } for _, e := range entries { v := strings.TrimSpace(e.Version) if v != "" && isPlausibleAntigravityVersion(v) { return v, nil } } return "", errors.New("no version entries in releases response") } // isPlausibleAntigravityVersion 防御性检查:避免错误响应把 UA 污染成无效字符串。 // 形如 1.23.2、1.21.9、1.20.6;接受 2-4 段数字。 func isPlausibleAntigravityVersion(v string) bool { parts := strings.Split(v, ".") if len(parts) < 2 || len(parts) > 4 { return false } for _, p := range parts { if p == "" || len(p) > 5 { return false } if _, err := strconv.Atoi(p); err != nil { return false } } return true }