fix: 心跳接入启动 + 网关错误去重
Some checks failed
CI / test (push) Failing after 1m32s
CI / golangci-lint (push) Failing after 32s
Security Scan / backend-security (push) Failing after 32s
Security Scan / frontend-security (push) Failing after 1m32s

- AntigravityGatewayService 嵌入心跳,构造时自动启动
- Forward() 方法中注册心跳(首次 API 调用触发,后续更新 token)
- 新建 gateway_errors.go: WriteClaudeErrorResponse/WriteGoogleErrorResponse 共享实现
- antigravity writeGoogleError 去掉手写映射,统一用 googleapi.HTTPStatusToGoogleStatus()
- gemini writeClaudeError/writeGoogleError 委托到共享实现
- 新增 docs/antigravity-fingerprint-diagnostic.md 诊断手册
This commit is contained in:
win 2026-03-27 12:11:22 +08:00
parent ffe6a5e331
commit 2279bde564
4 changed files with 299 additions and 41 deletions

View File

@ -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 非流式格式返回

View File

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

View File

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

View File

@ -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
# 方法 2mitmproxy 抓包(需信任证书)
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 是否一致
□ 有没有多余的 headersub2api 多发了)
□ 有没有缺少的 headersub2api 少发了)
```
### 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 天 |