feat: add configurable Antigravity user agent version

This commit is contained in:
shaw 2026-05-11 22:25:20 +08:00
parent 9377c96746
commit a07a0dac63
19 changed files with 341 additions and 35 deletions

View File

@ -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")
}

View File

@ -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"`

View File

@ -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"

View File

@ -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

View File

@ -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 {

View File

@ -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" // 全局开关

View File

@ -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 != "" {

View File

@ -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{})

View File

@ -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 模拟

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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;

View File

@ -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',

View File

@ -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 模拟',

View File

@ -3451,6 +3451,36 @@
</div>
<Toggle v-model="form.rewrite_message_cache_control" />
</div>
<!-- Antigravity UA 版本 -->
<div>
<label
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t(
"admin.settings.gatewayForwarding.antigravityUserAgentVersion",
)
}}
</label>
<input
v-model="form.antigravity_user_agent_version"
type="text"
class="input max-w-xs font-mono text-sm"
:placeholder="
t(
'admin.settings.gatewayForwarding.antigravityUserAgentVersionPlaceholder',
)
"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{
t(
"admin.settings.gatewayForwarding.antigravityUserAgentVersionHint",
)
}}
</p>
</div>
</div>
</div>
<!-- Web Search Emulation -->
@ -6571,6 +6601,7 @@ const form = reactive<SettingsForm>({
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,

View File

@ -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,