fix: 心跳接入启动 + 网关错误去重
- 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:
parent
ffe6a5e331
commit
2279bde564
@ -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 非流式格式返回
|
||||
|
||||
31
backend/internal/service/gateway_errors.go
Normal file
31
backend/internal/service/gateway_errors.go
Normal 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)
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
257
docs/antigravity-fingerprint-diagnostic.md
Normal file
257
docs/antigravity-fingerprint-diagnostic.md
Normal 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
|
||||
|
||||
# 方法 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 天 |
|
||||
Loading…
x
Reference in New Issue
Block a user