From a07a0dac63a3e14b902ef7e2862b73e346ac88e7 Mon Sep 17 00:00:00 2001 From: shaw Date: Mon, 11 May 2026 22:25:20 +0800 Subject: [PATCH] feat: add configurable Antigravity user agent version --- .../internal/handler/admin/setting_handler.go | 30 +++++-- backend/internal/handler/dto/settings.go | 11 +-- backend/internal/pkg/antigravity/client.go | 14 +-- backend/internal/pkg/antigravity/oauth.go | 77 ++++++++++++++-- .../internal/pkg/antigravity/oauth_test.go | 2 +- backend/internal/service/domain_constants.go | 2 + backend/internal/service/setting_service.go | 88 +++++++++++++++++-- .../service/setting_service_update_test.go | 71 +++++++++++++++ backend/internal/service/settings_view.go | 11 +-- backend/internal/service/wire.go | 2 + deploy/.env.example | 3 + deploy/docker-compose.local.yml | 1 + deploy/docker-compose.standalone.yml | 1 + deploy/docker-compose.yml | 1 + frontend/src/api/admin/settings.ts | 2 + frontend/src/i18n/locales/en.ts | 3 + frontend/src/i18n/locales/zh.ts | 3 + frontend/src/views/admin/SettingsView.vue | 33 +++++++ .../admin/__tests__/SettingsView.spec.ts | 21 +++++ 19 files changed, 341 insertions(+), 35 deletions(-) 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", + ) + }} +

+
@@ -6571,6 +6601,7 @@ const form = reactive({ enable_cch_signing: false, enable_anthropic_cache_ttl_1h_injection: false, rewrite_message_cache_control: false, + antigravity_user_agent_version: "", // Balance & quota notification balance_low_notify_enabled: false, balance_low_notify_threshold: 0, @@ -7642,6 +7673,8 @@ async function saveSettings() { enable_anthropic_cache_ttl_1h_injection: form.enable_anthropic_cache_ttl_1h_injection, rewrite_message_cache_control: form.rewrite_message_cache_control, + antigravity_user_agent_version: + form.antigravity_user_agent_version?.trim() || "", // Payment configuration payment_enabled: form.payment_enabled, risk_control_enabled: form.risk_control_enabled, diff --git a/frontend/src/views/admin/__tests__/SettingsView.spec.ts b/frontend/src/views/admin/__tests__/SettingsView.spec.ts index dce93f07..275e38c5 100644 --- a/frontend/src/views/admin/__tests__/SettingsView.spec.ts +++ b/frontend/src/views/admin/__tests__/SettingsView.spec.ts @@ -370,6 +370,7 @@ const baseSettingsResponse = { enable_cch_signing: false, enable_anthropic_cache_ttl_1h_injection: false, rewrite_message_cache_control: false, + antigravity_user_agent_version: "", payment_enabled: true, payment_min_amount: 1, payment_max_amount: 10000, @@ -622,6 +623,26 @@ describe("admin SettingsView payment visible method controls", () => { ); }); + it("submits Antigravity user agent version gateway setting", async () => { + getSettings.mockResolvedValueOnce({ + ...baseSettingsResponse, + antigravity_user_agent_version: "1.23.2", + }); + + const wrapper = mountView(); + + await flushPromises(); + await wrapper.find("form").trigger("submit.prevent"); + await flushPromises(); + + expect(updateSettings).toHaveBeenCalledTimes(1); + expect(updateSettings).toHaveBeenCalledWith( + expect.objectContaining({ + antigravity_user_agent_version: "1.23.2", + }), + ); + }); + it("updates provider enablement immediately and reloads providers", async () => { const provider = { id: 7,