feat: Antigravity 100% 指纹还原 + BoringCrypto TLS
Some checks failed
CI / test (push) Failing after 4s
CI / golangci-lint (push) Failing after 3s
Security Scan / backend-security (push) Failing after 1m0s
Security Scan / frontend-security (push) Failing after 32s

Antigravity:
- Client ID 保留双 ID 支持(二进制确认两个都存在)
- Daily URL 去掉 .sandbox 后缀(日志确认)
- Redirect URI /callback → /oauth-callback(extension.js 确认)
- User-Agent 动态 OS/arch: antigravity/{ver} {os}/{arch}
- 新增 x-goog-api-client: gl-go/{goVer} gax-go/v2 grpc-go/1.81.0-dev
- googleapis 不再走 Node.js proxy → Go 原生 TLS(匹配真实 BoringCrypto)
- 新增 Go 后端心跳服务(每5分钟 loadCodeAssist + fetchAvailableModels)
- Dockerfile 切换 BoringCrypto 编译(CGO_ENABLED=1 GOEXPERIMENT=boringcrypto)

GeminiCLI:
- User-Agent 动态化: GeminiCLI/0.1.5 ({OS}; {ARCH})
- AI Studio 请求补上 User-Agent

Claude:
- CLI 版本 2.1.84, 包版本 0.74.0, 运行时 v24.3.0
- Token 交换 axios/1.13.6, timeout 15s
- proxy.js 仅服务 api.anthropic.com(Claude 专属)

架构变更:
- Node.js proxy 仅用于 Claude (api.anthropic.com)
- Antigravity (googleapis) 走 Go 原生 HTTP + GOST proxy
- TLS 指纹: Go BoringCrypto ≈ 真实 Antigravity BoringCrypto
This commit is contained in:
win 2026-03-27 02:24:03 +08:00
parent 8c6e578a84
commit ffe6a5e331
13 changed files with 311 additions and 65 deletions

View File

@ -7,7 +7,7 @@
# =============================================================================
ARG NODE_IMAGE=node:24-alpine
ARG GOLANG_IMAGE=golang:1.26.1-alpine
ARG GOLANG_IMAGE=golang:1.26.1
ARG ALPINE_IMAGE=alpine:3.21
ARG POSTGRES_IMAGE=postgres:18-alpine
ARG GOPROXY=https://goproxy.cn,direct
@ -46,8 +46,8 @@ ARG GOSUMDB
ENV GOPROXY=${GOPROXY}
ENV GOSUMDB=${GOSUMDB}
# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata
# Install build dependencies (non-alpine image uses apt)
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /app/backend
@ -61,14 +61,14 @@ COPY backend/ ./
# Copy frontend dist from previous stage (must be after backend copy to avoid being overwritten)
COPY --from=frontend-builder /app/backend/internal/web/dist ./internal/web/dist
# Build the binary (BuildType=release for CI builds, embed frontend)
# Version precedence: build arg VERSION > cmd/server/VERSION
# Build the binary with BoringCrypto (matches real Antigravity TLS fingerprint)
# CGO_ENABLED=1 required for BoringCrypto; static linking via -extldflags for scratch-like deployment
RUN VERSION_VALUE="${VERSION}" && \
if [ -z "${VERSION_VALUE}" ]; then VERSION_VALUE="$(tr -d '\r\n' < ./cmd/server/VERSION)"; fi && \
DATE_VALUE="${DATE:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}" && \
CGO_ENABLED=0 GOOS=linux go build \
CGO_ENABLED=1 GOEXPERIMENT=boringcrypto GOOS=linux go build \
-tags embed \
-ldflags="-s -w -X main.Version=${VERSION_VALUE} -X main.Commit=${COMMIT} -X main.Date=${DATE_VALUE} -X main.BuildType=release" \
-ldflags="-s -w -linkmode external -extldflags '-static' -X main.Version=${VERSION_VALUE} -X main.Commit=${COMMIT} -X main.Date=${DATE_VALUE} -X main.BuildType=release" \
-trimpath \
-o /app/sub2api \
./cmd/server

View File

@ -16,10 +16,10 @@ const CONNECT_TIMEOUT = parseInt(process.env.CONNECT_TIMEOUT || '30000', 10);
const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '600000', 10);
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED !== 'false'; // 默认开启
const DD_API_KEY = process.env.DD_API_KEY || 'pubbbf48e6d78dae54bceaa4acf463299bf';
const CLI_VERSION = process.env.CLI_VERSION || '2.1.81';
const BUILD_TIME = process.env.BUILD_TIME || '2026-03-20T21:26:18Z';
// 伪装的 Node 版本CLI 2.1.81 打包的 Node 版本)
const FAKE_NODE_VERSION = process.env.FAKE_NODE_VERSION || 'v22.14.0';
const CLI_VERSION = process.env.CLI_VERSION || '2.1.84';
const BUILD_TIME = process.env.BUILD_TIME || '2026-03-25T23:49:18Z';
// 伪装的 Node 版本CLI 2.1.84 打包的 Bun 报告的 Node 兼容版本)
const FAKE_NODE_VERSION = process.env.FAKE_NODE_VERSION || 'v24.3.0';
const log = (level, msg, extra = {}) => {
const entry = { time: new Date().toISOString(), level, msg, ...extra };
@ -166,9 +166,9 @@ function buildEnvBlock(hostId) {
platform: platformStr,
node_version: FAKE_NODE_VERSION,
terminal: hostId.terminal,
package_managers: 'npm',
runtimes: 'node',
is_running_with_bun: false,
package_managers: 'npm,pnpm',
runtimes: 'deno,node',
is_running_with_bun: true,
is_ci: false,
is_claubbit: false,
is_github_action: false,
@ -199,8 +199,8 @@ function buildProcessMetrics(uptime) {
heapTotal,
heapUsed,
external: 14_000_000 + Math.floor(Math.random() * 2_000_000),
arrayBuffers: Math.floor(Math.random() * 10_000),
constrainedMemory: 0,
arrayBuffers: Math.floor(Math.random() * 200_000),
constrainedMemory: 51539607552,
cpuUsage: {
user: Math.floor(uptime * 10_000 + Math.random() * 300_000),
system: Math.floor(uptime * 2_000 + Math.random() * 80_000),
@ -251,12 +251,8 @@ function sendTelemetryEvents(events, session) {
'x-service-name': 'claude-code',
'Content-Length': Buffer.byteLength(body),
};
// 如果有 session注入 OTEL trace headers匹配 CLI 的 W3C Trace Context
if (session) {
const traceId = crypto.randomBytes(16).toString('hex');
const spanId = crypto.randomBytes(8).toString('hex');
headers['traceparent'] = `00-${traceId}-${spanId}-01`;
}
// 注意:真实 CLI 2.1.84 的 event_logging/batch 不发 traceparent
// traceparent 仅在 OTLP exporter单独通道中使用不在这个端点
const opts = {
hostname: 'api.anthropic.com',
@ -682,6 +678,8 @@ async function proxyRequest(req, res) {
'api.anthropic.com',
'cloudaicompanion.googleapis.com',
'generativelanguage.googleapis.com',
'cloudcode-pa.googleapis.com',
'daily-cloudcode-pa.googleapis.com',
]);
if (H2_PREFER_HOSTS.has(targetHost) || h2Hosts.has(targetHost)) {
await sendViaH2(targetHost, req.method, req.url, req.headers, body, res, savedHeaders);

View File

@ -4,7 +4,7 @@ VERSION ?= $(shell tr -d '\r\n' < ./cmd/server/VERSION)
LDFLAGS ?= -s -w -X main.Version=$(VERSION)
build:
CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -trimpath -o bin/server ./cmd/server
CGO_ENABLED=1 GOEXPERIMENT=boringcrypto go build -ldflags="$(LDFLAGS)" -trimpath -o bin/server ./cmd/server
generate:
go generate ./ent

View File

@ -12,6 +12,7 @@ import (
"net"
"net/http"
"net/url"
"runtime"
"strings"
"time"
@ -29,6 +30,21 @@ func (e *ForbiddenError) Error() string {
return fmt.Sprintf("fetchAvailableModels 失败 (HTTP %d): %s", e.StatusCode, e.Body)
}
// GetGoogAPIClient 返回 x-goog-api-client 头的值(导出供心跳等外部使用)
// 格式与真实 Antigravity 的 Go SDK 一致: gl-go/{goVersion} gax-go/v2 grpc-go/1.81.0-dev
func GetGoogAPIClient() string {
goVer := runtime.Version() // e.g. "go1.22.0"
return fmt.Sprintf("gl-go/%s gax-go/v2 grpc-go/1.81.0-dev", goVer)
}
// setAntigravityHeaders 设置与真实 Antigravity IDE 一致的 HTTP 请求头
func setAntigravityHeaders(req *http.Request, accessToken string) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("User-Agent", GetUserAgent())
req.Header.Set("X-Goog-Api-Client", GetGoogAPIClient())
}
// NewAPIRequestWithURL 使用指定的 base URL 创建 Antigravity API 请求v1internal 端点)
func NewAPIRequestWithURL(ctx context.Context, baseURL, action, accessToken string, body []byte) (*http.Request, error) {
// 构建 URL流式请求添加 ?alt=sse 参数
@ -43,10 +59,8 @@ func NewAPIRequestWithURL(ctx context.Context, baseURL, action, accessToken stri
return nil, err
}
// 基础 Headers与 Antigravity-Manager 保持一致,只设置这 3 个)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("User-Agent", GetUserAgent())
// 设置与真实 Antigravity IDE 一致的请求头
setAntigravityHeaders(req, accessToken)
return req, nil
}
@ -274,6 +288,11 @@ func NewClient(proxyURL string) (*Client, error) {
}, nil
}
// DoRaw 执行原始 HTTP 请求(供心跳等内部使用)
func (c *Client) DoRaw(req *http.Request) (*http.Response, error) {
return c.httpClient.Do(req)
}
// IsConnectionError 判断是否为连接错误网络超时、DNS 失败、连接拒绝)
func IsConnectionError(err error) bool {
if err == nil {
@ -451,6 +470,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("X-Goog-Api-Client", GetGoogAPIClient())
resp, err := c.httpClient.Do(req)
if err != nil {
@ -530,6 +550,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("X-Goog-Api-Client", GetGoogAPIClient())
resp, err := c.httpClient.Do(req)
if err != nil {
@ -664,6 +685,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("X-Goog-Api-Client", GetGoogAPIClient())
resp, err := c.httpClient.Do(req)
if err != nil {

View File

@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"os"
"runtime"
"strings"
"sync"
"time"
@ -23,13 +24,16 @@ const (
UserInfoURL = "https://www.googleapis.com/oauth2/v2/userinfo"
// Antigravity OAuth 客户端凭证
// 注意:真实 Antigravity 主 Client ID 是 884354919052-...,但需要对应的 client_secret
// 当前使用的 1071006060591-... 同样存在于真实二进制中(可能是备用登录模式)
// 如需切换,必须同时更新 client_secret通过环境变量 ANTIGRAVITY_OAUTH_CLIENT_SECRET
ClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
// AntigravityOAuthClientSecretEnv 是 Antigravity OAuth client_secret 的环境变量名。
AntigravityOAuthClientSecretEnv = "ANTIGRAVITY_OAUTH_CLIENT_SECRET"
// 固定的 redirect_uri用户需手动复制 code
RedirectURI = "http://localhost:8085/callback"
// redirect_uri — 真实 Antigravity IDE 使用 localhost 动态端口 + /oauth-callback 路径
RedirectURI = "http://localhost:8085/oauth-callback"
// OAuth scopes
Scopes = "https://www.googleapis.com/auth/cloud-platform " +
@ -44,13 +48,13 @@ const (
// URL 可用性 TTL不可用 URL 的恢复时间)
URLAvailabilityTTL = 5 * time.Minute
// Antigravity API 端点
// Antigravity API 端点(真实 Antigravity 日志确认使用 daily-cloudcode-pa 无 sandbox 后缀)
antigravityProdBaseURL = "https://cloudcode-pa.googleapis.com"
antigravityDailyBaseURL = "https://daily-cloudcode-pa.sandbox.googleapis.com"
antigravityDailyBaseURL = "https://daily-cloudcode-pa.googleapis.com"
)
// defaultUserAgentVersion 可通过环境变量 ANTIGRAVITY_USER_AGENT_VERSION 配置,默认 1.20.5
var defaultUserAgentVersion = "1.20.5"
// defaultUserAgentVersion 可通过环境变量 ANTIGRAVITY_USER_AGENT_VERSION 配置,默认 0.2.0(匹配真实 extension 版本)
var defaultUserAgentVersion = "0.2.0"
// defaultClientSecret 可通过环境变量 ANTIGRAVITY_OAUTH_CLIENT_SECRET 配置
var defaultClientSecret = "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"
@ -66,9 +70,11 @@ func init() {
}
}
// GetUserAgent 返回当前配置的 User-Agent
// GetUserAgent 返回当前配置的 User-Agent(匹配真实 Antigravity 格式: antigravity/{version} {os}/{arch}
func GetUserAgent() string {
return fmt.Sprintf("antigravity/%s windows/amd64", defaultUserAgentVersion)
osName := runtime.GOOS // darwin, linux, windows
arch := runtime.GOARCH // arm64, amd64
return fmt.Sprintf("antigravity/%s %s/%s", defaultUserAgentVersion, osName, arch)
}
func getClientSecret() (string, error) {

View File

@ -52,13 +52,13 @@ const APIKeyHaikuBetaHeader = BetaInterleavedThinking
var DefaultHeaders = map[string]string{
// Keep these in sync with recent Claude CLI traffic to reduce the chance
// that Claude Code-scoped OAuth credentials are rejected as "non-CLI" usage.
"User-Agent": "claude-cli/2.1.81 (external, cli)",
"User-Agent": "claude-cli/2.1.84 (external, cli)",
"X-Stainless-Lang": "js",
"X-Stainless-Package-Version": "0.80.0",
"X-Stainless-OS": "Linux",
"X-Stainless-Package-Version": "0.74.0",
"X-Stainless-OS": "MacOS",
"X-Stainless-Arch": "arm64",
"X-Stainless-Runtime": "node",
"X-Stainless-Runtime-Version": "v24.13.0",
"X-Stainless-Runtime-Version": "v24.3.0",
"X-Stainless-Retry-Count": "0",
"X-Stainless-Timeout": "600",
"X-App": "cli",

View File

@ -1,7 +1,12 @@
// Package geminicli provides helpers for interacting with Gemini CLI tools.
package geminicli
import "time"
import (
"fmt"
"runtime"
"strings"
"time"
)
const (
AIStudioBaseURL = "https://generativelanguage.googleapis.com"
@ -47,5 +52,14 @@ const (
SessionTTL = 30 * time.Minute
// GeminiCLIUserAgent mimics Gemini CLI to maximize compatibility with internal endpoints.
GeminiCLIUserAgent = "GeminiCLI/0.1.5 (Windows; AMD64)"
// Note: The real Gemini CLI uses OS-appropriate platform strings.
// Use GetGeminiCLIUserAgent() for runtime-aware User-Agent.
GeminiCLIUserAgent = "GeminiCLI/0.1.5"
)
// GetGeminiCLIUserAgent 返回带有正确平台信息的 Gemini CLI User-Agent
func GetGeminiCLIUserAgent() string {
osName := strings.Title(runtime.GOOS) // Darwin, Linux, Windows
arch := strings.ToUpper(runtime.GOARCH)
return fmt.Sprintf("GeminiCLI/0.1.5 (%s; %s)", osName, arch)
}

View File

@ -212,7 +212,7 @@ func (s *claudeOAuthService) ExchangeCodeForToken(ctx context.Context, code, cod
SetContext(ctx).
SetHeader("Accept", "application/json, text/plain, */*").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "axios/1.8.4").
SetHeader("User-Agent", "axios/1.13.6").
SetBody(reqBody).
SetSuccessResult(&tokenResp).
Post(s.tokenURL)
@ -242,6 +242,7 @@ func (s *claudeOAuthService) RefreshToken(ctx context.Context, refreshToken, pro
"grant_type": "refresh_token",
"refresh_token": refreshToken,
"client_id": oauth.ClientID,
"scope": oauth.ScopeAPI,
}
var tokenResp oauth.TokenResponse
@ -250,7 +251,7 @@ func (s *claudeOAuthService) RefreshToken(ctx context.Context, refreshToken, pro
SetContext(ctx).
SetHeader("Accept", "application/json, text/plain, */*").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "axios/1.8.4").
SetHeader("User-Agent", "axios/1.13.6").
SetBody(reqBody).
SetSuccessResult(&tokenResp).
Post(s.tokenURL)
@ -268,9 +269,9 @@ func (s *claudeOAuthService) RefreshToken(ctx context.Context, refreshToken, pro
func createReqClient(proxyURL string) (*req.Client, error) {
// 禁用 CookieJar确保每次授权都是干净的会话
// 不使用 ImpersonateChrome() — 真实 Claude CLI 用 axios (Bun fetch)TLS 指纹应为 Node.js/Bun
client := req.C().
SetTimeout(60 * time.Second).
ImpersonateChrome().
SetTimeout(15 * time.Second).
SetCookieJar(nil) // 禁用 CookieJar
trimmed, _, err := proxyurl.Parse(proxyURL)

View File

@ -124,13 +124,12 @@ func NewHTTPUpstream(cfg *config.Config) service.HTTPUpstream {
// - 调用方必须关闭 resp.Body否则会导致 inFlight 计数泄漏
// - inFlight > 0 的客户端不会被淘汰,确保活跃请求不被中断
func (s *httpUpstreamService) Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) {
// Node.js TLS 代理:Anthropic + Google APIs
// 无论是否绑定 per-account 代理,都走 node-tls-proxy指纹伪装
// Node.js TLS 代理:Anthropic API
// Antigravity (googleapis) 使用 Go 原生 TLS更接近真实 BoringCrypto 指纹
// proxyURL 通过 X-Upstream-Proxy header 传递给 node-tls-proxy 动态选择出口
if s.isNodeTLSProxyEnabled() && req != nil && req.URL != nil && req.URL.Scheme == "https" {
host := req.URL.Hostname()
if host == "api.anthropic.com" ||
strings.HasSuffix(host, ".googleapis.com") {
if host == "api.anthropic.com" {
return s.doViaNodeTLSProxy(req, proxyURL, accountID, accountConcurrency)
}
}
@ -186,11 +185,11 @@ func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, acco
return s.Do(req, proxyURL, accountID, accountConcurrency)
}
// 优先使用 Node.js TLS 代理模式(Anthropic + Google APIs
// 无论是否绑定 per-account 代理,都走 node-tls-proxy指纹伪装
// 优先使用 Node.js TLS 代理模式(Anthropic API
// Antigravity (googleapis) 使用 Go 原生 TLS更接近真实 BoringCrypto 指纹
if s.isNodeTLSProxyEnabled() && req != nil && req.URL != nil {
host := req.URL.Hostname()
if host == "api.anthropic.com" || strings.HasSuffix(host, ".googleapis.com") {
if host == "api.anthropic.com" {
return s.doViaNodeTLSProxy(req, proxyURL, accountID, accountConcurrency)
}
}

View File

@ -0,0 +1,204 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"sync"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
)
// AntigravityHeartbeat 模拟真实 Antigravity IDE 的心跳行为
// 真实 IDE 每 5 分钟发送 loadCodeAssist + fetchAvailableModels
type AntigravityHeartbeat struct {
mu sync.Mutex
sessions map[int64]*heartbeatSession // accountID -> session
stopCh chan struct{}
}
type heartbeatSession struct {
accountID int64
accessToken string
projectID string
proxyURL string
lastBeat time.Time
}
// NewAntigravityHeartbeat 创建心跳管理器
func NewAntigravityHeartbeat() *AntigravityHeartbeat {
hb := &AntigravityHeartbeat{
sessions: make(map[int64]*heartbeatSession),
stopCh: make(chan struct{}),
}
go hb.loop()
return hb
}
// Register 注册账号心跳(首次 API 调用时调用)
func (h *AntigravityHeartbeat) Register(accountID int64, accessToken, projectID, proxyURL string) {
h.mu.Lock()
defer h.mu.Unlock()
if _, exists := h.sessions[accountID]; exists {
// 更新 token可能已刷新
h.sessions[accountID].accessToken = accessToken
return
}
h.sessions[accountID] = &heartbeatSession{
accountID: accountID,
accessToken: accessToken,
projectID: projectID,
proxyURL: proxyURL,
lastBeat: time.Now(),
}
log.Printf("[antigravity-heartbeat] registered account %d (project: %s)", accountID, projectID)
}
// UpdateToken 更新账号的 access tokentoken 刷新后调用)
func (h *AntigravityHeartbeat) UpdateToken(accountID int64, accessToken string) {
h.mu.Lock()
defer h.mu.Unlock()
if s, ok := h.sessions[accountID]; ok {
s.accessToken = accessToken
}
}
// Unregister 移除账号心跳
func (h *AntigravityHeartbeat) Unregister(accountID int64) {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.sessions, accountID)
}
// Stop 停止心跳
func (h *AntigravityHeartbeat) Stop() {
select {
case <-h.stopCh:
default:
close(h.stopCh)
}
}
func (h *AntigravityHeartbeat) loop() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-h.stopCh:
return
case <-ticker.C:
h.tick()
}
}
}
func (h *AntigravityHeartbeat) tick() {
h.mu.Lock()
// 收集需要心跳的 session
var toSend []*heartbeatSession
now := time.Now()
for _, s := range h.sessions {
if now.Sub(s.lastBeat) >= 5*time.Minute {
s.lastBeat = now
// 复制一份避免持锁时发请求
cp := *s
toSend = append(toSend, &cp)
}
}
h.mu.Unlock()
for _, s := range toSend {
go h.sendHeartbeat(s)
}
}
func (h *AntigravityHeartbeat) sendHeartbeat(s *heartbeatSession) {
client, err := antigravity.NewClient(s.proxyURL)
if err != nil {
log.Printf("[antigravity-heartbeat] account %d: client error: %v", s.accountID, err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// 1. loadCodeAssist
h.doLoadCodeAssist(ctx, client, s)
// 模拟真实 IDE 的延迟(~500ms
time.Sleep(time.Duration(400+rand.Intn(200)) * time.Millisecond)
// 2. fetchAvailableModels
h.doFetchAvailableModels(ctx, client, s)
}
func (h *AntigravityHeartbeat) doLoadCodeAssist(ctx context.Context, client *antigravity.Client, s *heartbeatSession) {
reqBody := map[string]any{
"metadata": map[string]string{
"ideType": "ANTIGRAVITY",
},
}
body, _ := json.Marshal(reqBody)
for _, baseURL := range antigravity.BaseURLs {
apiURL := fmt.Sprintf("%s/v1internal:loadCodeAssist", baseURL)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body))
if err != nil {
continue
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+s.accessToken)
req.Header.Set("User-Agent", antigravity.GetUserAgent())
req.Header.Set("X-Goog-Api-Client", antigravity.GetGoogAPIClient())
resp, err := client.DoRaw(req)
if err != nil {
continue
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return
}
}
}
func (h *AntigravityHeartbeat) doFetchAvailableModels(ctx context.Context, client *antigravity.Client, s *heartbeatSession) {
reqBody := map[string]string{
"project": s.projectID,
}
body, _ := json.Marshal(reqBody)
for _, baseURL := range antigravity.BaseURLs {
apiURL := fmt.Sprintf("%s/v1internal:fetchAvailableModels", baseURL)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body))
if err != nil {
continue
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+s.accessToken)
req.Header.Set("User-Agent", antigravity.GetUserAgent())
req.Header.Set("X-Goog-Api-Client", antigravity.GetGoogAPIClient())
resp, err := client.DoRaw(req)
if err != nil {
continue
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return
}
}
}

View File

@ -670,7 +670,7 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex
}
upstreamReq.Header.Set("Content-Type", "application/json")
upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
upstreamReq.Header.Set("User-Agent", geminicli.GeminiCLIUserAgent)
upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent())
return upstreamReq, "x-request-id", nil
} else {
// Mode 2: AI Studio API with OAuth (like API key mode, but using Bearer token)
@ -691,6 +691,7 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex
}
upstreamReq.Header.Set("Content-Type", "application/json")
upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent())
return upstreamReq, "x-request-id", nil
}
}
@ -722,7 +723,7 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex
c.Set(OpsUpstreamRequestBodyKey, string(body))
}
resp, err = s.httpUpstream.Do(upstreamReq, proxyURL, account.ID, account.Concurrency)
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
if err != nil {
safeErr := sanitizeUpstreamErrorMessage(err.Error())
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
@ -1171,7 +1172,7 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin.
}
upstreamReq.Header.Set("Content-Type", "application/json")
upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
upstreamReq.Header.Set("User-Agent", geminicli.GeminiCLIUserAgent)
upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent())
return upstreamReq, "x-request-id", nil
} else {
// Mode 2: AI Studio API with OAuth (like API key mode, but using Bearer token)
@ -1192,6 +1193,7 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin.
}
upstreamReq.Header.Set("Content-Type", "application/json")
upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
upstreamReq.Header.Set("User-Agent", geminicli.GetGeminiCLIUserAgent())
return upstreamReq, "x-request-id", nil
}
}
@ -1222,7 +1224,7 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin.
c.Set(OpsUpstreamRequestBodyKey, string(body))
}
resp, err = s.httpUpstream.Do(upstreamReq, proxyURL, account.ID, account.Concurrency)
resp, err = s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
if err != nil {
safeErr := sanitizeUpstreamErrorMessage(err.Error())
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
@ -2589,7 +2591,7 @@ func (s *GeminiMessagesCompatService) ForwardAIStudioGET(ctx context.Context, ac
return nil, fmt.Errorf("unsupported account type: %s", account.Type)
}
resp, err := s.httpUpstream.Do(req, proxyURL, account.ID, account.Concurrency)
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, account.IsTLSFingerprintEnabled())
if err != nil {
return nil, err
}

View File

@ -26,13 +26,13 @@ var (
// 默认指纹值(当客户端未提供时使用)
var defaultFingerprint = Fingerprint{
UserAgent: "claude-cli/2.1.81 (external, cli)",
UserAgent: "claude-cli/2.1.84 (external, cli)",
StainlessLang: "js",
StainlessPackageVersion: "0.80.0",
StainlessOS: "Linux",
StainlessPackageVersion: "0.74.0",
StainlessOS: "MacOS",
StainlessArch: "arm64",
StainlessRuntime: "node",
StainlessRuntimeVersion: "v24.13.0",
StainlessRuntimeVersion: "v24.3.0",
}

View File

@ -409,10 +409,10 @@ gateway:
# other sub2api deployments. Empty values use built-in defaults.
# 每个实例可设置不同的版本号,与其他 sub2api 部署区分。空值使用内置默认值。
fingerprint_defaults:
# claude_cli_version: "2.1.81"
# stainless_package_version: "0.80.0"
# stainless_runtime_version: "v24.13.0"
# stainless_os: "Linux" # Linux / Darwin
# claude_cli_version: "2.1.84"
# stainless_package_version: "0.74.0"
# stainless_runtime_version: "v24.3.0"
# stainless_os: "MacOS" # MacOS / Linux (注意大小写真实CLI用MacOS)
# stainless_arch: "arm64" # arm64 / x64
# =============================================================================