diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index aa5d948c..6b900a34 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -862,6 +862,7 @@ type AntigravityGatewayService struct { settingService *SettingService cache GatewayCache // 用于模型级限流时清除粘性会话绑定 schedulerSnapshot *SchedulerSnapshotService + heartbeat *AntigravityHeartbeat } func NewAntigravityGatewayService( @@ -881,6 +882,7 @@ func NewAntigravityGatewayService( settingService: settingService, cache: cache, schedulerSnapshot: schedulerSnapshot, + heartbeat: NewAntigravityHeartbeat(), } } @@ -1377,6 +1379,11 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, proxyURL = account.Proxy.URL() } + // 注册心跳(首次 API 调用时自动注册,后续更新 token) + if s.heartbeat != nil && projectID != "" { + s.heartbeat.Register(account.ID, accessToken, projectID, proxyURL) + } + // 获取转换选项 // Antigravity 上游要求必须包含身份提示词,否则会返回 429 transformOpts := s.getClaudeTransformOptions(ctx) @@ -3565,11 +3572,7 @@ func mergeTextPartsToResponse(response map[string]any, textParts []string) map[s } func (s *AntigravityGatewayService) writeClaudeError(c *gin.Context, status int, errType, message string) error { - c.JSON(status, gin.H{ - "type": "error", - "error": gin.H{"type": errType, "message": message}, - }) - return fmt.Errorf("%s", message) + return WriteClaudeErrorResponse(c, status, errType, message) } // WriteMappedClaudeError 导出版本,供 handler 层使用(如 fallback 错误处理) @@ -3655,28 +3658,7 @@ func (s *AntigravityGatewayService) writeMappedClaudeError(c *gin.Context, accou } func (s *AntigravityGatewayService) writeGoogleError(c *gin.Context, status int, message string) error { - statusStr := "UNKNOWN" - switch status { - case 400: - statusStr = "INVALID_ARGUMENT" - case 404: - statusStr = "NOT_FOUND" - case 429: - statusStr = "RESOURCE_EXHAUSTED" - case 500: - statusStr = "INTERNAL" - case 502, 503: - statusStr = "UNAVAILABLE" - } - - c.JSON(status, gin.H{ - "error": gin.H{ - "code": status, - "message": message, - "status": statusStr, - }, - }) - return fmt.Errorf("%s", message) + return WriteGoogleErrorResponse(c, status, message) } // handleClaudeStreamToNonStreaming 收集上游流式响应,转换为 Claude 非流式格式返回 diff --git a/backend/internal/service/gateway_errors.go b/backend/internal/service/gateway_errors.go new file mode 100644 index 00000000..2ee548c2 --- /dev/null +++ b/backend/internal/service/gateway_errors.go @@ -0,0 +1,31 @@ +package service + +import ( + "fmt" + + "github.com/Wei-Shaw/sub2api/internal/pkg/googleapi" + "github.com/gin-gonic/gin" +) + +// WriteClaudeErrorResponse 写入 Claude 格式的错误响应(共享实现) +// 用于 AntigravityGatewayService 和 GeminiMessagesCompatService +func WriteClaudeErrorResponse(c *gin.Context, status int, errType, message string) error { + c.JSON(status, gin.H{ + "type": "error", + "error": gin.H{"type": errType, "message": message}, + }) + return fmt.Errorf("%s", message) +} + +// WriteGoogleErrorResponse 写入 Google 格式的错误响应(共享实现) +// 使用 googleapi.HTTPStatusToGoogleStatus 统一映射 HTTP 状态码 +func WriteGoogleErrorResponse(c *gin.Context, status int, message string) error { + c.JSON(status, gin.H{ + "error": gin.H{ + "code": status, + "message": message, + "status": googleapi.HTTPStatusToGoogleStatus(status), + }, + }) + return fmt.Errorf("%s", message) +} diff --git a/backend/internal/service/gemini_messages_compat_service.go b/backend/internal/service/gemini_messages_compat_service.go index f1eac565..0e0898b6 100644 --- a/backend/internal/service/gemini_messages_compat_service.go +++ b/backend/internal/service/gemini_messages_compat_service.go @@ -21,7 +21,6 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey" "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli" - "github.com/Wei-Shaw/sub2api/internal/pkg/googleapi" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" "github.com/Wei-Shaw/sub2api/internal/util/responseheaders" "github.com/Wei-Shaw/sub2api/internal/util/urlvalidator" @@ -2150,22 +2149,11 @@ func randomHex(nBytes int) string { } func (s *GeminiMessagesCompatService) writeClaudeError(c *gin.Context, status int, errType, message string) error { - c.JSON(status, gin.H{ - "type": "error", - "error": gin.H{"type": errType, "message": message}, - }) - return fmt.Errorf("%s", message) + return WriteClaudeErrorResponse(c, status, errType, message) } func (s *GeminiMessagesCompatService) writeGoogleError(c *gin.Context, status int, message string) error { - c.JSON(status, gin.H{ - "error": gin.H{ - "code": status, - "message": message, - "status": googleapi.HTTPStatusToGoogleStatus(status), - }, - }) - return fmt.Errorf("%s", message) + return WriteGoogleErrorResponse(c, status, message) } func unwrapIfNeeded(isOAuth bool, raw []byte) []byte { diff --git a/docs/antigravity-fingerprint-diagnostic.md b/docs/antigravity-fingerprint-diagnostic.md new file mode 100644 index 00000000..7c03421e --- /dev/null +++ b/docs/antigravity-fingerprint-diagnostic.md @@ -0,0 +1,257 @@ +# Antigravity 指纹诊断与封号排查手册 + +## 一、当前指纹基线(2026-03-27) + +### 真实 Antigravity IDE + +| 项目 | 值 | 来源 | +|------|-----|------| +| Extension 版本 | `0.2.0` | package.json | +| Go 版本 | `go1.27-20260305-RC01` | 二进制 strings | +| TLS 库 | BoringCrypto | 二进制编译标记 | +| gRPC | `grpc-go/1.81.0-dev` | 二进制 strings | +| gax | `gax-go/v2` | 二进制 strings | +| User-Agent | `antigravity/{ver} {os}/{arch}` | 二进制 `User-Agent: %s` | +| x-goog-api-client | `gl-go/{goVer} gax-go/v2 grpc-go/1.81.0-dev` | 二进制 strings | +| Client ID | `884354919052-...` (主) / `1071006060591-...` (备) | 二进制 strings | +| API 端点 | `daily-cloudcode-pa.googleapis.com` | Antigravity 运行日志 | +| 心跳间隔 | 每 5 分钟 | cloudcode.log | +| 心跳内容 | loadCodeAssist + fetchAvailableModels | cloudcode.log | +| Token 刷新 | 约每 1 小时 | auth.log | +| Redirect URI | `http://localhost:{port}/oauth-callback` | extension.js | +| ideType | `ANTIGRAVITY` | 二进制 strings | + +### sub2api 模拟值 + +| 项目 | 值 | 匹配度 | +|------|-----|--------| +| Extension 版本 | `0.2.0` | ✅ | +| Go 版本 | `go1.26.1` | ⚠️ 小版本差异 | +| TLS 库 | BoringCrypto (`GOEXPERIMENT=boringcrypto`) | ✅ | +| gRPC | `grpc-go/1.81.0-dev` | ✅ | +| User-Agent | `antigravity/0.2.0 {runtime.GOOS}/{runtime.GOARCH}` | ✅ | +| x-goog-api-client | `gl-go/{runtime.Version()} gax-go/v2 grpc-go/1.81.0-dev` | ✅ | +| Client ID | `1071006060591-...` | ✅ 真实二进制中也有 | +| API 端点 | `daily-cloudcode-pa.googleapis.com` | ✅ | +| 心跳 | Go 后端 5 分钟定时 | ✅ | +| TLS 路径 | Go BoringCrypto → GOST proxy → googleapis | ✅ | + +--- + +## 二、封号排查决策树 + +``` +账号被封 + │ + ├── 只封 1 个账号 + │ └── 大概率账号本身问题(被举报/异常使用/违规内容) + │ → 换号,不需要改代码 + │ + ├── 同一 IP 下多个账号同时封 + │ └── IP 关联检测 + │ → 检查该 GOST proxy IP 绑了几个账号 + │ → 减少到 1 IP : 1-2 账号 + │ → 换 IP + │ + ├── 所有账号陆续被封(不同 IP) + │ └── 指纹问题(TLS 或 HTTP headers) + │ → 执行「指纹对比流程」(见第三节) + │ → 检查 Antigravity IDE 是否有新版本 + │ + └── 用了一段时间后才封 + └── 行为模式问题 + → 检查请求频率(是否远超正常 IDE 使用) + → 检查心跳是否正常(有没有漏发) + → 检查是否有异常的模型调用模式 +``` + +--- + +## 三、指纹对比流程 + +### 3.1 HTTP Headers 对比 + +**sub2api 侧(服务器):** + +```bash +# 开启 debug 日志,抓取发往 googleapis 的请求头 +docker logs sub2api 2>&1 | grep -E "googleapis|antigravity" | tail -50 +``` + +**真实 Antigravity 侧(装有 IDE 的电脑):** + +```bash +# 方法 1:读 IDE 日志 +LATEST=$(ls -t ~/Library/Application\ Support/Antigravity/logs/ | head -1) +cat ~/Library/Application\ Support/Antigravity/logs/$LATEST/cloudcode.log | tail -30 +cat ~/Library/Application\ Support/Antigravity/logs/$LATEST/auth.log | tail -10 + +# 方法 2:mitmproxy 抓包(需信任证书) +mitmproxy --mode regular --listen-port 8888 +# 设置 Antigravity 的 HTTP 代理为 127.0.0.1:8888 +# 观察 daily-cloudcode-pa.googleapis.com 的请求头 +``` + +**对比项:** + +``` +□ User-Agent 格式和值是否一致 +□ x-goog-api-client 是否一致 +□ Authorization 格式是否一致 +□ Content-Type 是否一致 +□ 有没有多余的 header(sub2api 多发了) +□ 有没有缺少的 header(sub2api 少发了) +``` + +### 3.2 TLS 指纹对比 + +**sub2api 服务器上抓:** + +```bash +# 抓 TLS ClientHello 包 +sudo tcpdump -i eth0 -w /tmp/sub2api_tls.pcap \ + 'dst port 443 and (dst host cloudcode-pa.googleapis.com or dst host daily-cloudcode-pa.googleapis.com)' \ + -c 10 + +# 提取 JA3 +python3 antigravity/capture/ja3_extract.py /tmp/sub2api_tls.pcap +``` + +**真实 Antigravity 机器上抓:** + +```bash +sudo tcpdump -i en0 -w /tmp/real_tls.pcap \ + 'dst port 443 and (dst host cloudcode-pa.googleapis.com or dst host daily-cloudcode-pa.googleapis.com)' \ + -c 10 + +python3 antigravity/capture/ja3_extract.py /tmp/real_tls.pcap +``` + +**对比项:** + +``` +□ JA3 hash 是否一致 +□ TLS 版本是否一致(应该都是 TLS 1.3) +□ Cipher suite 列表和顺序是否一致 +□ TLS extensions 是否一致 +□ ALPN 协议列表是否一致(应该是 h2, http/1.1) +``` + +### 3.3 行为模式对比 + +**sub2api 侧:** + +```bash +# 统计某账号的请求频率 +docker logs sub2api 2>&1 | grep "account_id=73" | \ + awk '{print $1}' | cut -d: -f1-2 | uniq -c | tail -20 + +# 检查心跳 +docker logs sub2api 2>&1 | grep "heartbeat" | tail -20 +``` + +**真实 Antigravity 侧:** + +```bash +LATEST=$(ls -t ~/Library/Application\ Support/Antigravity/logs/ | head -1) + +# 心跳间隔(应该 ~5 分钟) +grep "loadCodeAssist" ~/Library/Application\ Support/Antigravity/logs/$LATEST/cloudcode.log | \ + awk '{print $1, $2}' | head -20 + +# Token 刷新间隔(应该 ~1 小时) +grep "handleAuthRefresh" ~/Library/Application\ Support/Antigravity/logs/$LATEST/auth.log | \ + awk '{print $1, $2}' +``` + +**对比项:** + +``` +□ 心跳间隔是否 ~5 分钟 +□ 每次心跳是否发 loadCodeAssist + fetchAvailableModels 两个请求 +□ 两个请求间隔是否 ~500ms +□ Token 刷新间隔是否 ~1 小时 +□ 请求频率是否在正常 IDE 使用范围内 +``` + +--- + +## 四、Antigravity IDE 版本更新检查 + +当 Antigravity IDE 更新后,执行以下检查: + +```bash +BINARY="/Applications/Antigravity.app/Contents/Resources/app/extensions/antigravity/bin/language_server_macos_arm" +PKG="/Applications/Antigravity.app/Contents/Resources/app/extensions/antigravity/package.json" + +echo "=== 1. Extension 版本 ===" +python3 -c "import json; d=json.load(open('$PKG')); print(d['version'])" + +echo "=== 2. Go 版本 ===" +strings "$BINARY" | grep "^go1\." | head -1 + +echo "=== 3. gRPC 版本 ===" +strings "$BINARY" | grep -oE "grpc-go/[^ ]+" | head -1 + +echo "=== 4. Client ID ===" +strings "$BINARY" | grep -oE "[0-9]+-[a-z0-9]+\.apps\.googleusercontent\.com" | sort -u + +echo "=== 5. OAuth Scopes ===" +strings "$BINARY" | grep -oE "googleapis.com/auth/[a-z._-]+" | sort -u + +echo "=== 6. API 端点确认 ===" +LATEST=$(ls -t ~/Library/Application\ Support/Antigravity/logs/ | head -1) +grep "URL:" ~/Library/Application\ Support/Antigravity/logs/$LATEST/window*/exthost/google.antigravity/Antigravity.log | head -5 +``` + +**如果有变更,需要更新的文件:** + +| 变更项 | 更新文件 | +|--------|----------| +| Extension 版本 | `backend/internal/pkg/antigravity/oauth.go` → `defaultUserAgentVersion` | +| gRPC 版本 | `backend/internal/pkg/antigravity/client.go` → `GetGoogAPIClient()` | +| Client ID | `backend/internal/pkg/antigravity/oauth.go` → `ClientID` | +| Scopes | `backend/internal/pkg/antigravity/oauth.go` → `Scopes` | +| API 端点 | `backend/internal/pkg/antigravity/oauth.go` → `antigravityDailyBaseURL` | + +--- + +## 五、关键文件定位 + +``` +指纹配置 +├── backend/internal/pkg/antigravity/oauth.go # Client ID, URL, UA 版本, Scopes, Redirect URI +├── backend/internal/pkg/antigravity/client.go # x-goog-api-client, 请求头组装, DoRaw +├── backend/internal/pkg/geminicli/constants.go # GeminiCLI UA +└── backend/internal/service/gemini_messages_compat_service.go # AI Studio / Code Assist 请求头 + +行为模拟 +├── backend/internal/service/antigravity_heartbeat.go # 5 分钟心跳 +└── antigravity/node-tls-proxy/proxy.js # Claude 遥测模拟(非 Antigravity) + +网络路由 +├── backend/internal/repository/http_upstream.go # googleapis → Go 原生, anthropic → Node.js proxy +└── Dockerfile / backend/Makefile # BoringCrypto 编译 + +真实 IDE 日志(对比用) +├── ~/Library/Application Support/Antigravity/logs/{timestamp}/cloudcode.log # API 调用记录 +├── ~/Library/Application Support/Antigravity/logs/{timestamp}/auth.log # Token 刷新 +└── ~/Library/Application Support/Antigravity/logs/{timestamp}/window*/exthost/google.antigravity/Antigravity.log # 语言服务器日志 + +抓包工具 +├── antigravity/capture/ja3_extract.py # JA3 指纹提取 +├── antigravity/capture/capture_traffic.py # mitmproxy HTTP 抓包 +└── antigravity/capture/capture_tls.sh # TLS 抓包脚本 +``` + +--- + +## 六、预防措施 + +| # | 措施 | 说明 | +|---|------|------| +| 1 | 1 IP : 1-2 账号 | 同一 GOST proxy 最多绑 2 个 Antigravity 账号 | +| 2 | 监控 403 | 连续 403 立刻暂停账号,不继续请求 | +| 3 | 心跳随机化 | 5 分钟 ±30 秒随机偏移,避免精确整数间隔 | +| 4 | 定期对比 | 每次 IDE 更新后执行第四节检查 | +| 5 | 保留旧日志 | 封号前后的 docker logs 保存至少 7 天 |