diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go
index 66661579..0ea664d8 100644
--- a/backend/internal/handler/admin/setting_handler.go
+++ b/backend/internal/handler/admin/setting_handler.go
@@ -226,6 +226,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
EnableCCHSigning: settings.EnableCCHSigning,
EnableAnthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
RewriteMessageCacheControl: settings.RewriteMessageCacheControl,
+ AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion,
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource,
PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource,
@@ -512,11 +513,12 @@ type UpdateSettingsRequest struct {
BackendModeEnabled bool `json:"backend_mode_enabled"`
// Gateway forwarding behavior
- EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
- EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
- EnableCCHSigning *bool `json:"enable_cch_signing"`
- EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`
- RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
+ EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
+ EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
+ EnableCCHSigning *bool `json:"enable_cch_signing"`
+ EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`
+ RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
+ AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
// Payment visible method routing
PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
@@ -1252,6 +1254,14 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
return
}
}
+ if req.AntigravityUserAgentVersion != nil {
+ normalized := strings.TrimSpace(*req.AntigravityUserAgentVersion)
+ req.AntigravityUserAgentVersion = &normalized
+ if normalized != "" && !semverPattern.MatchString(normalized) {
+ response.Error(c, http.StatusBadRequest, "antigravity_user_agent_version must be empty or a valid semver (e.g. 1.23.2)")
+ return
+ }
+ }
// 交叉验证:如果同时设置了最低和最高版本号,最高版本号必须 >= 最低版本号
if req.MinClaudeCodeVersion != "" && req.MaxClaudeCodeVersion != "" {
@@ -1423,6 +1433,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return previousSettings.RewriteMessageCacheControl
}(),
+ AntigravityUserAgentVersion: func() string {
+ if req.AntigravityUserAgentVersion != nil {
+ return *req.AntigravityUserAgentVersion
+ }
+ return previousSettings.AntigravityUserAgentVersion
+ }(),
PaymentVisibleMethodAlipaySource: func() string {
if req.PaymentVisibleMethodAlipaySource != nil {
return strings.TrimSpace(*req.PaymentVisibleMethodAlipaySource)
@@ -1756,6 +1772,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
EnableCCHSigning: updatedSettings.EnableCCHSigning,
EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection,
RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
+ AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled,
@@ -2155,6 +2172,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.RewriteMessageCacheControl != after.RewriteMessageCacheControl {
changed = append(changed, "rewrite_message_cache_control")
}
+ if before.AntigravityUserAgentVersion != after.AntigravityUserAgentVersion {
+ changed = append(changed, "antigravity_user_agent_version")
+ }
if before.PaymentVisibleMethodAlipaySource != after.PaymentVisibleMethodAlipaySource {
changed = append(changed, "payment_visible_method_alipay_source")
}
diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go
index 1c231597..551cf0dc 100644
--- a/backend/internal/handler/dto/settings.go
+++ b/backend/internal/handler/dto/settings.go
@@ -158,11 +158,12 @@ type SystemSettings struct {
BackendModeEnabled bool `json:"backend_mode_enabled"`
// Gateway forwarding behavior
- EnableFingerprintUnification bool `json:"enable_fingerprint_unification"`
- EnableMetadataPassthrough bool `json:"enable_metadata_passthrough"`
- EnableCCHSigning bool `json:"enable_cch_signing"`
- EnableAnthropicCacheTTL1hInjection bool `json:"enable_anthropic_cache_ttl_1h_injection"`
- RewriteMessageCacheControl bool `json:"rewrite_message_cache_control"`
+ EnableFingerprintUnification bool `json:"enable_fingerprint_unification"`
+ EnableMetadataPassthrough bool `json:"enable_metadata_passthrough"`
+ EnableCCHSigning bool `json:"enable_cch_signing"`
+ EnableAnthropicCacheTTL1hInjection bool `json:"enable_anthropic_cache_ttl_1h_injection"`
+ RewriteMessageCacheControl bool `json:"rewrite_message_cache_control"`
+ AntigravityUserAgentVersion string `json:"antigravity_user_agent_version"`
// Web Search Emulation
WebSearchEmulationEnabled bool `json:"web_search_emulation_enabled"`
diff --git a/backend/internal/pkg/antigravity/client.go b/backend/internal/pkg/antigravity/client.go
index fdd7fea1..16aff9f8 100644
--- a/backend/internal/pkg/antigravity/client.go
+++ b/backend/internal/pkg/antigravity/client.go
@@ -46,7 +46,7 @@ func NewAPIRequestWithURL(ctx context.Context, baseURL, action, accessToken stri
// 基础 Headers(与 Antigravity-Manager 保持一致,只设置这 3 个)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+accessToken)
- req.Header.Set("User-Agent", GetUserAgent())
+ req.Header.Set("User-Agent", GetUserAgentForContext(ctx))
return req, nil
}
@@ -440,7 +440,7 @@ func (c *Client) GetUserInfo(ctx context.Context, accessToken string) (*UserInfo
func (c *Client) LoadCodeAssist(ctx context.Context, accessToken string) (*LoadCodeAssistResponse, map[string]any, error) {
reqBody := LoadCodeAssistRequest{}
reqBody.Metadata.IDEType = "ANTIGRAVITY"
- reqBody.Metadata.IDEVersion = "1.20.6"
+ reqBody.Metadata.IDEVersion = GetUserAgentVersionForContext(ctx)
reqBody.Metadata.IDEName = "antigravity"
bodyBytes, err := json.Marshal(reqBody)
@@ -461,7 +461,7 @@ func (c *Client) LoadCodeAssist(ctx context.Context, accessToken string) (*LoadC
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
- req.Header.Set("User-Agent", GetUserAgent())
+ req.Header.Set("User-Agent", GetUserAgentForContext(ctx))
resp, err := c.httpClient.Do(req)
if err != nil {
@@ -540,7 +540,7 @@ func (c *Client) OnboardUser(ctx context.Context, accessToken, tierID string) (s
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
- req.Header.Set("User-Agent", GetUserAgent())
+ req.Header.Set("User-Agent", GetUserAgentForContext(ctx))
resp, err := c.httpClient.Do(req)
if err != nil {
@@ -674,7 +674,7 @@ func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectI
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
- req.Header.Set("User-Agent", GetUserAgent())
+ req.Header.Set("User-Agent", GetUserAgentForContext(ctx))
resp, err := c.httpClient.Do(req)
if err != nil {
@@ -792,7 +792,7 @@ func (c *Client) SetUserSettings(ctx context.Context, accessToken string) (*SetU
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "*/*")
- req.Header.Set("User-Agent", GetUserAgent())
+ req.Header.Set("User-Agent", GetUserAgentForContext(ctx))
req.Header.Set("X-Goog-Api-Client", "gl-node/22.21.1")
req.Host = "daily-cloudcode-pa.googleapis.com"
@@ -835,7 +835,7 @@ func (c *Client) FetchUserInfo(ctx context.Context, accessToken, projectID strin
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "*/*")
- req.Header.Set("User-Agent", GetUserAgent())
+ req.Header.Set("User-Agent", GetUserAgentForContext(ctx))
req.Header.Set("X-Goog-Api-Client", "gl-node/22.21.1")
req.Host = "daily-cloudcode-pa.googleapis.com"
diff --git a/backend/internal/pkg/antigravity/oauth.go b/backend/internal/pkg/antigravity/oauth.go
index 7c963d9e..4ffb0cba 100644
--- a/backend/internal/pkg/antigravity/oauth.go
+++ b/backend/internal/pkg/antigravity/oauth.go
@@ -1,6 +1,7 @@
package antigravity
import (
+ "context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
@@ -9,6 +10,7 @@ import (
"net/http"
"net/url"
"os"
+ "regexp"
"strings"
"sync"
"time"
@@ -28,6 +30,12 @@ const (
// AntigravityOAuthClientSecretEnv 是 Antigravity OAuth client_secret 的环境变量名。
AntigravityOAuthClientSecretEnv = "ANTIGRAVITY_OAUTH_CLIENT_SECRET"
+ // AntigravityUserAgentVersionEnv 是 Antigravity User-Agent 版本号的环境变量名。
+ AntigravityUserAgentVersionEnv = "ANTIGRAVITY_USER_AGENT_VERSION"
+
+ // DefaultUserAgentVersion 是未通过环境变量或后台设置覆盖时使用的默认版本号。
+ DefaultUserAgentVersion = "1.23.2"
+
// 固定的 redirect_uri(用户需手动复制 code)
RedirectURI = "http://localhost:8085/callback"
@@ -49,15 +57,24 @@ const (
antigravityDailyBaseURL = "https://daily-cloudcode-pa.sandbox.googleapis.com"
)
-// defaultUserAgentVersion 可通过环境变量 ANTIGRAVITY_USER_AGENT_VERSION 配置,默认 1.20.5
-var defaultUserAgentVersion = "1.21.9"
+var userAgentVersionPattern = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
+
+// UserAgentVersionResolver 提供运行时 User-Agent 版本号覆盖能力。
+type UserAgentVersionResolver func(ctx context.Context) string
+
+var (
+ // defaultUserAgentVersion 可通过环境变量 ANTIGRAVITY_USER_AGENT_VERSION 配置。
+ defaultUserAgentVersion = DefaultUserAgentVersion
+ userAgentVersionMu sync.RWMutex
+ userAgentVersionResolver UserAgentVersionResolver
+)
// defaultClientSecret 可通过环境变量 ANTIGRAVITY_OAUTH_CLIENT_SECRET 配置
var defaultClientSecret = "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"
func init() {
// 从环境变量读取版本号,未设置则使用默认值
- if version := os.Getenv("ANTIGRAVITY_USER_AGENT_VERSION"); version != "" {
+ if version := NormalizeUserAgentVersion(os.Getenv(AntigravityUserAgentVersionEnv)); version != "" {
defaultUserAgentVersion = version
}
// 从环境变量读取 client_secret,未设置则使用默认值
@@ -66,11 +83,61 @@ func init() {
}
}
-// GetUserAgent 返回当前配置的 User-Agent
-func GetUserAgent() string {
+// NormalizeUserAgentVersion 校验并归一化 Antigravity User-Agent 版本号。
+func NormalizeUserAgentVersion(version string) string {
+ version = strings.TrimSpace(version)
+ if version == "" || !userAgentVersionPattern.MatchString(version) {
+ return ""
+ }
+ return version
+}
+
+// GetDefaultUserAgentVersion 返回配置文件/环境变量层面的默认版本号。
+func GetDefaultUserAgentVersion() string {
+ return defaultUserAgentVersion
+}
+
+// SetUserAgentVersionResolver 设置运行时版本号解析器,通常由后台 settings 注入。
+func SetUserAgentVersionResolver(resolver UserAgentVersionResolver) {
+ userAgentVersionMu.Lock()
+ defer userAgentVersionMu.Unlock()
+ userAgentVersionResolver = resolver
+}
+
+// GetUserAgentVersionForContext 返回当前请求应使用的 Antigravity 版本号。
+func GetUserAgentVersionForContext(ctx context.Context) string {
+ if ctx == nil {
+ ctx = context.Background()
+ }
+ userAgentVersionMu.RLock()
+ resolver := userAgentVersionResolver
+ userAgentVersionMu.RUnlock()
+ if resolver != nil {
+ if version := NormalizeUserAgentVersion(resolver(ctx)); version != "" {
+ return version
+ }
+ }
+ return defaultUserAgentVersion
+}
+
+// BuildUserAgent 使用指定版本号构造 User-Agent;版本为空或非法时回退默认值。
+func BuildUserAgent(version string) string {
+ if normalized := NormalizeUserAgentVersion(version); normalized != "" {
+ return fmt.Sprintf("antigravity/%s windows/amd64", normalized)
+ }
return fmt.Sprintf("antigravity/%s windows/amd64", defaultUserAgentVersion)
}
+// GetUserAgentForContext 返回当前请求应使用的 User-Agent。
+func GetUserAgentForContext(ctx context.Context) string {
+ return BuildUserAgent(GetUserAgentVersionForContext(ctx))
+}
+
+// GetUserAgent 返回当前配置的 User-Agent。
+func GetUserAgent() string {
+ return GetUserAgentForContext(context.Background())
+}
+
func getClientSecret() (string, error) {
if v := strings.TrimSpace(defaultClientSecret); v != "" {
return v, nil
diff --git a/backend/internal/pkg/antigravity/oauth_test.go b/backend/internal/pkg/antigravity/oauth_test.go
index 9850af17..69d9ddc4 100644
--- a/backend/internal/pkg/antigravity/oauth_test.go
+++ b/backend/internal/pkg/antigravity/oauth_test.go
@@ -690,7 +690,7 @@ func TestConstants_值正确(t *testing.T) {
if RedirectURI != "http://localhost:8085/callback" {
t.Errorf("RedirectURI 不匹配: got %s", RedirectURI)
}
- if GetUserAgent() != "antigravity/1.21.9 windows/amd64" {
+ if GetUserAgent() != "antigravity/1.23.2 windows/amd64" {
t.Errorf("UserAgent 不匹配: got %s", GetUserAgent())
}
if SessionTTL != 30*time.Minute {
diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go
index 481b8015..17c40ba1 100644
--- a/backend/internal/service/domain_constants.go
+++ b/backend/internal/service/domain_constants.go
@@ -372,6 +372,8 @@ const (
SettingKeyEnableAnthropicCacheTTL1hInjection = "enable_anthropic_cache_ttl_1h_injection"
// SettingKeyRewriteMessageCacheControl 是否改写 messages[*].content[*].cache_control(默认 false)
SettingKeyRewriteMessageCacheControl = "rewrite_message_cache_control"
+ // SettingKeyAntigravityUserAgentVersion Antigravity 上游 User-Agent 版本号(空值使用环境变量/默认值)
+ SettingKeyAntigravityUserAgentVersion = "antigravity_user_agent_version"
// Balance Low Notification
SettingKeyBalanceLowNotifyEnabled = "balance_low_notify_enabled" // 全局开关
diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go
index 37c7bb8d..86978eec 100644
--- a/backend/internal/service/setting_service.go
+++ b/backend/internal/service/setting_service.go
@@ -18,6 +18,7 @@ import (
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
+ "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/imroc/req/v3"
"golang.org/x/sync/singleflight"
@@ -98,6 +99,16 @@ const gatewayForwardingCacheTTL = 60 * time.Second
const gatewayForwardingErrorTTL = 5 * time.Second
const gatewayForwardingDBTimeout = 5 * time.Second
+// cachedAntigravityUserAgentVersion 缓存 Antigravity UA 版本号(进程内缓存,60s TTL)
+type cachedAntigravityUserAgentVersion struct {
+ version string
+ expiresAt int64 // unix nano
+}
+
+const antigravityUserAgentVersionCacheTTL = 60 * time.Second
+const antigravityUserAgentVersionErrorTTL = 5 * time.Second
+const antigravityUserAgentVersionDBTimeout = 5 * time.Second
+
// DefaultSubscriptionGroupReader validates group references used by default subscriptions.
type DefaultSubscriptionGroupReader interface {
GetByID(ctx context.Context, id int64) (*Group, error)
@@ -109,13 +120,15 @@ type WebSearchManagerBuilder func(cfg *WebSearchEmulationConfig, proxyURLs map[i
// SettingService 系统设置服务
type SettingService struct {
- settingRepo SettingRepository
- defaultSubGroupReader DefaultSubscriptionGroupReader
- proxyRepo ProxyRepository // for resolving websearch provider proxy URLs
- cfg *config.Config
- onUpdate func() // Callback when settings are updated (for cache invalidation)
- version string // Application version
- webSearchManagerBuilder WebSearchManagerBuilder
+ settingRepo SettingRepository
+ defaultSubGroupReader DefaultSubscriptionGroupReader
+ proxyRepo ProxyRepository // for resolving websearch provider proxy URLs
+ cfg *config.Config
+ onUpdate func() // Callback when settings are updated (for cache invalidation)
+ version string // Application version
+ webSearchManagerBuilder WebSearchManagerBuilder
+ antigravityUAVersionCache atomic.Value // *cachedAntigravityUserAgentVersion
+ antigravityUAVersionSF singleflight.Group
}
type ProviderDefaultGrantSettings struct {
@@ -810,6 +823,55 @@ func (s *SettingService) GetAvailableChannelsRuntime(ctx context.Context) Availa
}
}
+// GetAntigravityUserAgentVersion 返回 Antigravity 上游请求使用的版本号。
+// 后台设置优先;为空、缺失或非法时回退到 ANTIGRAVITY_USER_AGENT_VERSION / 内置默认值。
+func (s *SettingService) GetAntigravityUserAgentVersion(ctx context.Context) string {
+ fallback := antigravity.GetDefaultUserAgentVersion()
+ if s == nil || s.settingRepo == nil {
+ return fallback
+ }
+ if cached, ok := s.antigravityUAVersionCache.Load().(*cachedAntigravityUserAgentVersion); ok && cached != nil {
+ if time.Now().UnixNano() < cached.expiresAt {
+ return cached.version
+ }
+ }
+
+ result, _, _ := s.antigravityUAVersionSF.Do("antigravity_user_agent_version", func() (any, error) {
+ if cached, ok := s.antigravityUAVersionCache.Load().(*cachedAntigravityUserAgentVersion); ok && cached != nil {
+ if time.Now().UnixNano() < cached.expiresAt {
+ return cached.version, nil
+ }
+ }
+ if ctx == nil {
+ ctx = context.Background()
+ }
+ dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), antigravityUserAgentVersionDBTimeout)
+ defer cancel()
+ value, err := s.settingRepo.GetValue(dbCtx, SettingKeyAntigravityUserAgentVersion)
+ if err != nil && !errors.Is(err, ErrSettingNotFound) {
+ slog.Warn("failed to get antigravity user agent version setting", "error", err)
+ s.antigravityUAVersionCache.Store(&cachedAntigravityUserAgentVersion{
+ version: fallback,
+ expiresAt: time.Now().Add(antigravityUserAgentVersionErrorTTL).UnixNano(),
+ })
+ return fallback, nil
+ }
+ version := antigravity.NormalizeUserAgentVersion(value)
+ if version == "" {
+ version = fallback
+ }
+ s.antigravityUAVersionCache.Store(&cachedAntigravityUserAgentVersion{
+ version: version,
+ expiresAt: time.Now().Add(antigravityUserAgentVersionCacheTTL).UnixNano(),
+ })
+ return version, nil
+ })
+ if version, ok := result.(string); ok && version != "" {
+ return version
+ }
+ return fallback
+}
+
// SetOnUpdateCallback sets a callback function to be called when settings are updated
// This is used for cache invalidation (e.g., HTML cache in frontend server)
func (s *SettingService) SetOnUpdateCallback(callback func()) {
@@ -1586,6 +1648,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
updates[SettingKeyEnableCCHSigning] = strconv.FormatBool(settings.EnableCCHSigning)
updates[SettingKeyEnableAnthropicCacheTTL1hInjection] = strconv.FormatBool(settings.EnableAnthropicCacheTTL1hInjection)
updates[SettingKeyRewriteMessageCacheControl] = strconv.FormatBool(settings.RewriteMessageCacheControl)
+ updates[SettingKeyAntigravityUserAgentVersion] = antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion)
updates[SettingPaymentVisibleMethodAlipaySource] = settings.PaymentVisibleMethodAlipaySource
updates[SettingPaymentVisibleMethodWxpaySource] = settings.PaymentVisibleMethodWxpaySource
updates[SettingPaymentVisibleMethodAlipayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodAlipayEnabled)
@@ -1657,6 +1720,15 @@ func (s *SettingService) refreshCachedSettings(settings *SystemSettings) {
rewriteMessageCacheControl: settings.RewriteMessageCacheControl,
expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
})
+ s.antigravityUAVersionSF.Forget("antigravity_user_agent_version")
+ antigravityUserAgentVersion := antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion)
+ if antigravityUserAgentVersion == "" {
+ antigravityUserAgentVersion = antigravity.GetDefaultUserAgentVersion()
+ }
+ s.antigravityUAVersionCache.Store(&cachedAntigravityUserAgentVersion{
+ version: antigravityUserAgentVersion,
+ expiresAt: time.Now().Add(antigravityUserAgentVersionCacheTTL).UnixNano(),
+ })
openAIAdvancedSchedulerSettingSF.Forget(openAIAdvancedSchedulerSettingKey)
openAIAdvancedSchedulerSettingCache.Store(&cachedOpenAIAdvancedSchedulerSetting{
enabled: settings.OpenAIAdvancedSchedulerEnabled,
@@ -2386,6 +2458,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyAllowUngroupedKeyScheduling: "false",
SettingKeyEnableAnthropicCacheTTL1hInjection: "false",
SettingKeyRewriteMessageCacheControl: strconv.FormatBool(s.defaultRewriteMessageCacheControl()),
+ SettingKeyAntigravityUserAgentVersion: "",
SettingPaymentVisibleMethodAlipaySource: "",
SettingPaymentVisibleMethodWxpaySource: "",
SettingPaymentVisibleMethodAlipayEnabled: "false",
@@ -2767,6 +2840,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
} else {
result.RewriteMessageCacheControl = s.defaultRewriteMessageCacheControl()
}
+ result.AntigravityUserAgentVersion = antigravity.NormalizeUserAgentVersion(settings[SettingKeyAntigravityUserAgentVersion])
// Web search emulation: quick enabled check from the JSON config
if raw := settings[SettingKeyWebSearchEmulationConfig]; raw != "" {
diff --git a/backend/internal/service/setting_service_update_test.go b/backend/internal/service/setting_service_update_test.go
index 9dc0ca59..d6b6b6cd 100644
--- a/backend/internal/service/setting_service_update_test.go
+++ b/backend/internal/service/setting_service_update_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
+ "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/stretchr/testify/require"
)
@@ -48,6 +49,41 @@ func (s *settingUpdateRepoStub) Delete(ctx context.Context, key string) error {
panic("unexpected Delete call")
}
+type settingAntigravityUARepoStub struct {
+ values map[string]string
+}
+
+func (s *settingAntigravityUARepoStub) Get(ctx context.Context, key string) (*Setting, error) {
+ panic("unexpected Get call")
+}
+
+func (s *settingAntigravityUARepoStub) GetValue(ctx context.Context, key string) (string, error) {
+ if value, ok := s.values[key]; ok {
+ return value, nil
+ }
+ return "", ErrSettingNotFound
+}
+
+func (s *settingAntigravityUARepoStub) Set(ctx context.Context, key, value string) error {
+ panic("unexpected Set call")
+}
+
+func (s *settingAntigravityUARepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
+ panic("unexpected GetMultiple call")
+}
+
+func (s *settingAntigravityUARepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
+ panic("unexpected SetMultiple call")
+}
+
+func (s *settingAntigravityUARepoStub) GetAll(ctx context.Context) (map[string]string, error) {
+ panic("unexpected GetAll call")
+}
+
+func (s *settingAntigravityUARepoStub) Delete(ctx context.Context, key string) error {
+ panic("unexpected Delete call")
+}
+
type defaultSubGroupReaderStub struct {
byID map[int64]*Group
errBy map[int64]error
@@ -243,6 +279,41 @@ func TestSettingService_UpdateSettings_PaymentVisibleMethodsAndAdvancedScheduler
require.Equal(t, "true", repo.updates[openAIAdvancedSchedulerSettingKey])
}
+func TestSettingService_UpdateSettings_AntigravityUserAgentVersion(t *testing.T) {
+ repo := &settingUpdateRepoStub{}
+ svc := NewSettingService(repo, &config.Config{})
+
+ err := svc.UpdateSettings(context.Background(), &SystemSettings{
+ AntigravityUserAgentVersion: "1.23.2",
+ })
+ require.NoError(t, err)
+ require.Equal(t, "1.23.2", repo.updates[SettingKeyAntigravityUserAgentVersion])
+}
+
+func TestSettingService_GetAntigravityUserAgentVersion_Precedence(t *testing.T) {
+ t.Run("后台设置优先", func(t *testing.T) {
+ svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{
+ SettingKeyAntigravityUserAgentVersion: "1.24.0",
+ }}, &config.Config{})
+
+ require.Equal(t, "1.24.0", svc.GetAntigravityUserAgentVersion(context.Background()))
+ })
+
+ t.Run("空值回退配置默认值", func(t *testing.T) {
+ svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{
+ SettingKeyAntigravityUserAgentVersion: "",
+ }}, &config.Config{})
+
+ require.Equal(t, antigravity.GetDefaultUserAgentVersion(), svc.GetAntigravityUserAgentVersion(context.Background()))
+ })
+
+ t.Run("缺失回退配置默认值", func(t *testing.T) {
+ svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{}}, &config.Config{})
+
+ require.Equal(t, antigravity.GetDefaultUserAgentVersion(), svc.GetAntigravityUserAgentVersion(context.Background()))
+ })
+}
+
func TestSettingService_UpdateSettings_RejectsInvalidPaymentVisibleMethodSource(t *testing.T) {
repo := &settingUpdateRepoStub{}
svc := NewSettingService(repo, &config.Config{})
diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go
index ebef0d9d..bfe85995 100644
--- a/backend/internal/service/settings_view.go
+++ b/backend/internal/service/settings_view.go
@@ -168,11 +168,12 @@ type SystemSettings struct {
BackendModeEnabled bool
// Gateway forwarding behavior
- EnableFingerprintUnification bool // 是否统一 OAuth 账号的指纹头(默认 true)
- EnableMetadataPassthrough bool // 是否透传客户端原始 metadata(默认 false)
- EnableCCHSigning bool // 是否对 billing header cch 进行签名(默认 false)
- EnableAnthropicCacheTTL1hInjection bool // 是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl(默认 false)
- RewriteMessageCacheControl bool // 是否改写 messages[*].content[*].cache_control(默认 false)
+ EnableFingerprintUnification bool // 是否统一 OAuth 账号的指纹头(默认 true)
+ EnableMetadataPassthrough bool // 是否透传客户端原始 metadata(默认 false)
+ EnableCCHSigning bool // 是否对 billing header cch 进行签名(默认 false)
+ EnableAnthropicCacheTTL1hInjection bool // 是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl(默认 false)
+ RewriteMessageCacheControl bool // 是否改写 messages[*].content[*].cache_control(默认 false)
+ AntigravityUserAgentVersion string // Antigravity 上游 User-Agent 版本号;空值使用配置/默认值
// Web Search Emulation
WebSearchEmulationEnabled bool // 是否启用 web search 模拟
diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go
index dc96be0c..f0f5ff14 100644
--- a/backend/internal/service/wire.go
+++ b/backend/internal/service/wire.go
@@ -8,6 +8,7 @@ import (
dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/payment"
+ "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/google/wire"
"github.com/redis/go-redis/v9"
@@ -395,6 +396,7 @@ func ProvideSettingService(settingRepo SettingRepository, groupRepo GroupReposit
svc := NewSettingService(settingRepo, cfg)
svc.SetDefaultSubscriptionGroupReader(groupRepo)
svc.SetProxyRepository(proxyRepo)
+ antigravity.SetUserAgentVersionResolver(svc.GetAntigravityUserAgentVersion)
return svc
}
diff --git a/deploy/.env.example b/deploy/.env.example
index 28205f7c..b38c6305 100644
--- a/deploy/.env.example
+++ b/deploy/.env.example
@@ -228,6 +228,9 @@ TOTP_ENCRYPTION_KEY=
#
# Antigravity OAuth client_secret(用于 Antigravity OAuth 登录流)
# ANTIGRAVITY_OAUTH_CLIENT_SECRET=
+#
+# Antigravity User-Agent 版本号(后台设置 antigravity_user_agent_version 优先;留空使用内置默认 1.23.2)
+# ANTIGRAVITY_USER_AGENT_VERSION=
# -----------------------------------------------------------------------------
# Rate Limiting (Optional)
diff --git a/deploy/docker-compose.local.yml b/deploy/docker-compose.local.yml
index 51a80227..ca915112 100644
--- a/deploy/docker-compose.local.yml
+++ b/deploy/docker-compose.local.yml
@@ -127,6 +127,7 @@ services:
# SECURITY: This repo does not embed third-party client_secret.
- GEMINI_CLI_OAUTH_CLIENT_SECRET=${GEMINI_CLI_OAUTH_CLIENT_SECRET:-}
- ANTIGRAVITY_OAUTH_CLIENT_SECRET=${ANTIGRAVITY_OAUTH_CLIENT_SECRET:-}
+ - ANTIGRAVITY_USER_AGENT_VERSION=${ANTIGRAVITY_USER_AGENT_VERSION:-}
# =======================================================================
# Security Configuration (URL Allowlist)
diff --git a/deploy/docker-compose.standalone.yml b/deploy/docker-compose.standalone.yml
index 438d0a8a..44383dbe 100644
--- a/deploy/docker-compose.standalone.yml
+++ b/deploy/docker-compose.standalone.yml
@@ -93,6 +93,7 @@ services:
# SECURITY: This repo does not embed third-party client_secret.
- GEMINI_CLI_OAUTH_CLIENT_SECRET=${GEMINI_CLI_OAUTH_CLIENT_SECRET:-}
- ANTIGRAVITY_OAUTH_CLIENT_SECRET=${ANTIGRAVITY_OAUTH_CLIENT_SECRET:-}
+ - ANTIGRAVITY_USER_AGENT_VERSION=${ANTIGRAVITY_USER_AGENT_VERSION:-}
# =======================================================================
# Image Generation Stream & Concurrency
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
index 1d639ea4..a022f9ce 100644
--- a/deploy/docker-compose.yml
+++ b/deploy/docker-compose.yml
@@ -123,6 +123,7 @@ services:
# SECURITY: This repo does not embed third-party client_secret.
- GEMINI_CLI_OAUTH_CLIENT_SECRET=${GEMINI_CLI_OAUTH_CLIENT_SECRET:-}
- ANTIGRAVITY_OAUTH_CLIENT_SECRET=${ANTIGRAVITY_OAUTH_CLIENT_SECRET:-}
+ - ANTIGRAVITY_USER_AGENT_VERSION=${ANTIGRAVITY_USER_AGENT_VERSION:-}
# =======================================================================
# Security Configuration (URL Allowlist)
diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts
index 9cccdf3e..03e9e58f 100644
--- a/frontend/src/api/admin/settings.ts
+++ b/frontend/src/api/admin/settings.ts
@@ -478,6 +478,7 @@ export interface SystemSettings {
enable_cch_signing: boolean;
enable_anthropic_cache_ttl_1h_injection: boolean;
rewrite_message_cache_control: boolean;
+ antigravity_user_agent_version: string;
web_search_emulation_enabled?: boolean;
// Payment configuration
@@ -675,6 +676,7 @@ export interface UpdateSettingsRequest {
enable_cch_signing?: boolean;
enable_anthropic_cache_ttl_1h_injection?: boolean;
rewrite_message_cache_control?: boolean;
+ antigravity_user_agent_version?: string;
// Payment configuration
payment_enabled?: boolean;
risk_control_enabled?: boolean;
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index df65c2cc..02d044ef 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -5337,6 +5337,9 @@ export default {
anthropicCacheTTL1hInjectionHint: 'When enabled, existing ephemeral cache_control blocks in Anthropic OAuth/Setup Token request bodies are forced to 1h; response usage is billed back as 5m by default, with account-level TTL billing override taking priority.',
rewriteMessageCacheControl: 'Rewrite Message Cache Breakpoints',
rewriteMessageCacheControlHint: 'Default off: preserve client cache_control on message content blocks. When enabled, client breakpoints are stripped and proxy breakpoints are injected for clients that do not manage caching themselves.',
+ antigravityUserAgentVersion: 'Antigravity UA Version',
+ antigravityUserAgentVersionPlaceholder: '1.23.2',
+ antigravityUserAgentVersionHint: 'Leave empty to use ANTIGRAVITY_USER_AGENT_VERSION or the built-in default 1.23.2; when set, the admin setting takes precedence.',
},
webSearchEmulation: {
title: 'Web Search Emulation',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index 5204c37e..687c2df6 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -5496,6 +5496,9 @@ export default {
anthropicCacheTTL1hInjectionHint: '开启后,对 Anthropic OAuth/Setup Token 请求体中已有的 ephemeral 缓存块强制写入 1h;响应 usage 默认按 5m 回写计费,账号级 TTL 计费设置优先。',
rewriteMessageCacheControl: '改写消息缓存断点',
rewriteMessageCacheControlHint: '默认关闭,保留客户端在 messages 内容块中的 cache_control。开启后会清除客户端断点并注入代理断点,适合不自行管理缓存策略的客户端。',
+ antigravityUserAgentVersion: 'Antigravity UA 版本',
+ antigravityUserAgentVersionPlaceholder: '1.23.2',
+ antigravityUserAgentVersionHint: '留空时使用 ANTIGRAVITY_USER_AGENT_VERSION 或内置默认值 1.23.2;填写后后台设置优先。',
},
webSearchEmulation: {
title: 'Web Search 模拟',
diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue
index 23655f8f..7c3735b6 100644
--- a/frontend/src/views/admin/SettingsView.vue
+++ b/frontend/src/views/admin/SettingsView.vue
@@ -3451,6 +3451,36 @@
+ {{ + t( + "admin.settings.gatewayForwarding.antigravityUserAgentVersionHint", + ) + }} +
+