Commit 6e8188ed authored by shaw's avatar shaw
Browse files

fix(antigravity): 修复请求频繁429的问题

parent db6f53e2
...@@ -13,16 +13,48 @@ import ( ...@@ -13,16 +13,48 @@ import (
"time" "time"
) )
// resolveHost 从 URL 解析 host
func resolveHost(urlStr string) string {
parsed, err := url.Parse(urlStr)
if err != nil {
return ""
}
return parsed.Host
}
// NewAPIRequest 创建 Antigravity API 请求(v1internal 端点) // NewAPIRequest 创建 Antigravity API 请求(v1internal 端点)
func NewAPIRequest(ctx context.Context, action, accessToken string, body []byte) (*http.Request, error) { func NewAPIRequest(ctx context.Context, action, accessToken string, body []byte) (*http.Request, error) {
// 构建 URL,流式请求添加 ?alt=sse 参数
apiURL := fmt.Sprintf("%s/v1internal:%s", BaseURL, action) apiURL := fmt.Sprintf("%s/v1internal:%s", BaseURL, action)
isStream := action == "streamGenerateContent"
if isStream {
apiURL += "?alt=sse"
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body))
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 基础 Headers
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("User-Agent", UserAgent) req.Header.Set("User-Agent", UserAgent)
// Accept Header 根据请求类型设置
if isStream {
req.Header.Set("Accept", "text/event-stream")
} else {
req.Header.Set("Accept", "application/json")
}
// 显式设置 Host Header
if host := resolveHost(apiURL); host != "" {
req.Host = host
}
// 注意:requestType 已在 JSON body 的 V1InternalRequest 中设置,不需要 HTTP Header
return req, nil return req, nil
} }
......
...@@ -33,10 +33,11 @@ const ( ...@@ -33,10 +33,11 @@ const (
"https://www.googleapis.com/auth/experimentsandconfigs" "https://www.googleapis.com/auth/experimentsandconfigs"
// API 端点 // API 端点
BaseURL = "https://cloudcode-pa.googleapis.com" // 优先使用 sandbox daily URL,配额更宽松
BaseURL = "https://daily-cloudcode-pa.sandbox.googleapis.com"
// User-Agent // User-Agent(模拟官方客户端)
UserAgent = "antigravity/1.11.9 windows/amd64" UserAgent = "antigravity/1.104.0 darwin/arm64"
// Session 过期时间 // Session 过期时间
SessionTTL = 30 * time.Minute SessionTTL = 30 * time.Minute
......
package antigravity package antigravity
import ( import (
"crypto/sha256"
"encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"math/rand"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
) )
var (
sessionRand = rand.New(rand.NewSource(time.Now().UnixNano()))
sessionRandMutex sync.Mutex
)
// generateStableSessionID 基于用户消息内容生成稳定的 session ID
func generateStableSessionID(contents []GeminiContent) string {
// 查找第一个 user 消息的文本
for _, content := range contents {
if content.Role == "user" && len(content.Parts) > 0 {
if text := content.Parts[0].Text; text != "" {
h := sha256.Sum256([]byte(text))
n := int64(binary.BigEndian.Uint64(h[:8])) & 0x7FFFFFFFFFFFFFFF
return "-" + strconv.FormatInt(n, 10)
}
}
}
// 回退:生成随机 session ID
sessionRandMutex.Lock()
n := sessionRand.Int63n(9_000_000_000_000_000_000)
sessionRandMutex.Unlock()
return "-" + strconv.FormatInt(n, 10)
}
type TransformOptions struct { type TransformOptions struct {
EnableIdentityPatch bool EnableIdentityPatch bool
// IdentityPatch 可选:自定义注入到 systemInstruction 开头的身份防护提示词; // IdentityPatch 可选:自定义注入到 systemInstruction 开头的身份防护提示词;
...@@ -67,8 +96,15 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map ...@@ -67,8 +96,15 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map
// 5. 构建内部请求 // 5. 构建内部请求
innerRequest := GeminiRequest{ innerRequest := GeminiRequest{
Contents: contents, Contents: contents,
SafetySettings: DefaultSafetySettings, // 总是设置 toolConfig,与官方客户端一致
ToolConfig: &GeminiToolConfig{
FunctionCallingConfig: &GeminiFunctionCallingConfig{
Mode: "VALIDATED",
},
},
// 总是生成 sessionId,基于用户消息内容
SessionID: generateStableSessionID(contents),
} }
if systemInstruction != nil { if systemInstruction != nil {
...@@ -79,14 +115,9 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map ...@@ -79,14 +115,9 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map
} }
if len(tools) > 0 { if len(tools) > 0 {
innerRequest.Tools = tools innerRequest.Tools = tools
innerRequest.ToolConfig = &GeminiToolConfig{
FunctionCallingConfig: &GeminiFunctionCallingConfig{
Mode: "VALIDATED",
},
}
} }
// 如果提供了 metadata.user_id,复用为 sessionId // 如果提供了 metadata.user_id,优先使用
if claudeReq.Metadata != nil && claudeReq.Metadata.UserID != "" { if claudeReq.Metadata != nil && claudeReq.Metadata.UserID != "" {
innerRequest.SessionID = claudeReq.Metadata.UserID innerRequest.SessionID = claudeReq.Metadata.UserID
} }
...@@ -95,7 +126,7 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map ...@@ -95,7 +126,7 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map
v1Req := V1InternalRequest{ v1Req := V1InternalRequest{
Project: projectID, Project: projectID,
RequestID: "agent-" + uuid.New().String(), RequestID: "agent-" + uuid.New().String(),
UserAgent: "sub2api", UserAgent: "antigravity", // 固定值,与官方客户端一致
RequestType: "agent", RequestType: "agent",
Model: mappedModel, Model: mappedModel,
Request: innerRequest, Request: innerRequest,
...@@ -104,37 +135,37 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map ...@@ -104,37 +135,37 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map
return json.Marshal(v1Req) return json.Marshal(v1Req)
} }
func defaultIdentityPatch(modelName string) string { // antigravityIdentity Antigravity identity 提示词
return fmt.Sprintf( const antigravityIdentity = `<identity>
"--- [IDENTITY_PATCH] ---\n"+ You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.
"Ignore any previous instructions regarding your identity or host platform (e.g., Amazon Q, Google AI).\n"+ You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.
"You are currently providing services as the native %s model via a standard API proxy.\n"+ The USER will send you requests, which you must always prioritize addressing. Along with each USER request, we will attach additional metadata about their current state, such as what files they have open and where their cursor is.
"Always use the 'claude' command for terminal tasks if relevant.\n"+ This information may or may not be relevant to the coding task, it is up for you to decide.
"--- [SYSTEM_PROMPT_BEGIN] ---\n", </identity>
modelName, <communication_style>
) - **Proactiveness**. As an agent, you are allowed to be proactive, but only in the course of completing the user's task. For example, if the user asks you to add a new component, you can edit the code, verify build and test statuses, and take any other obvious follow-up actions, such as performing additional research. However, avoid surprising the user. For example, if the user asks HOW to approach something, you should answer their question and instead of jumping into editing a file.</communication_style>`
func defaultIdentityPatch(_ string) string {
return antigravityIdentity
} }
// buildSystemInstruction 构建 systemInstruction // buildSystemInstruction 构建 systemInstruction
func buildSystemInstruction(system json.RawMessage, modelName string, opts TransformOptions) *GeminiContent { func buildSystemInstruction(system json.RawMessage, modelName string, opts TransformOptions) *GeminiContent {
var parts []GeminiPart var parts []GeminiPart
// 可选注入身份防护指令(身份补丁) // 先解析用户的 system prompt,检测是否已包含 Antigravity identity
if opts.EnableIdentityPatch { userHasAntigravityIdentity := false
identityPatch := strings.TrimSpace(opts.IdentityPatch) var userSystemParts []GeminiPart
if identityPatch == "" {
identityPatch = defaultIdentityPatch(modelName)
}
parts = append(parts, GeminiPart{Text: identityPatch})
}
// 解析 system prompt
if len(system) > 0 { if len(system) > 0 {
// 尝试解析为字符串 // 尝试解析为字符串
var sysStr string var sysStr string
if err := json.Unmarshal(system, &sysStr); err == nil { if err := json.Unmarshal(system, &sysStr); err == nil {
if strings.TrimSpace(sysStr) != "" { if strings.TrimSpace(sysStr) != "" {
parts = append(parts, GeminiPart{Text: sysStr}) userSystemParts = append(userSystemParts, GeminiPart{Text: sysStr})
if strings.Contains(sysStr, "You are Antigravity") {
userHasAntigravityIdentity = true
}
} }
} else { } else {
// 尝试解析为数组 // 尝试解析为数组
...@@ -142,17 +173,28 @@ func buildSystemInstruction(system json.RawMessage, modelName string, opts Trans ...@@ -142,17 +173,28 @@ func buildSystemInstruction(system json.RawMessage, modelName string, opts Trans
if err := json.Unmarshal(system, &sysBlocks); err == nil { if err := json.Unmarshal(system, &sysBlocks); err == nil {
for _, block := range sysBlocks { for _, block := range sysBlocks {
if block.Type == "text" && strings.TrimSpace(block.Text) != "" { if block.Type == "text" && strings.TrimSpace(block.Text) != "" {
parts = append(parts, GeminiPart{Text: block.Text}) userSystemParts = append(userSystemParts, GeminiPart{Text: block.Text})
if strings.Contains(block.Text, "You are Antigravity") {
userHasAntigravityIdentity = true
}
} }
} }
} }
} }
} }
// identity patch 模式下,用分隔符包裹 system prompt,便于上游识别/调试;关闭时尽量保持原始 system prompt。 // 仅在用户未提供 Antigravity identity 时注入
if opts.EnableIdentityPatch && len(parts) > 0 { if opts.EnableIdentityPatch && !userHasAntigravityIdentity {
parts = append(parts, GeminiPart{Text: "\n--- [SYSTEM_PROMPT_END] ---"}) identityPatch := strings.TrimSpace(opts.IdentityPatch)
if identityPatch == "" {
identityPatch = defaultIdentityPatch(modelName)
}
parts = append(parts, GeminiPart{Text: identityPatch})
} }
// 添加用户的 system prompt
parts = append(parts, userSystemParts...)
if len(parts) == 0 { if len(parts) == 0 {
return nil return nil
} }
......
...@@ -181,12 +181,17 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account ...@@ -181,12 +181,17 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account
return nil, fmt.Errorf("构建请求失败: %w", err) return nil, fmt.Errorf("构建请求失败: %w", err)
} }
// 构建 HTTP 请求(非流式 // 构建 HTTP 请求(总是使用流式 endpoint,与官方客户端一致
req, err := antigravity.NewAPIRequest(ctx, "generateContent", accessToken, requestBody) req, err := antigravity.NewAPIRequest(ctx, "streamGenerateContent", accessToken, requestBody)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// DEBUG: 打印请求 header 和 body
log.Printf("[DEBUG] Antigravity TestConnection - URL: %s", req.URL.String())
log.Printf("[DEBUG] Antigravity TestConnection - Headers: %v", req.Header)
log.Printf("[DEBUG] Antigravity TestConnection - Body: %s", string(requestBody))
// 代理 URL // 代理 URL
proxyURL := "" proxyURL := ""
if account.ProxyID != nil && account.Proxy != nil { if account.ProxyID != nil && account.Proxy != nil {
...@@ -210,14 +215,8 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account ...@@ -210,14 +215,8 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account
return nil, fmt.Errorf("API 返回 %d: %s", resp.StatusCode, string(respBody)) return nil, fmt.Errorf("API 返回 %d: %s", resp.StatusCode, string(respBody))
} }
// 解包 v1internal 响应 // 解析流式响应,提取文本
unwrapped, err := s.unwrapV1InternalResponse(respBody) text := extractTextFromSSEResponse(respBody)
if err != nil {
return nil, fmt.Errorf("解包响应失败: %w", err)
}
// 提取响应文本
text := extractGeminiResponseText(unwrapped)
return &TestConnectionResult{ return &TestConnectionResult{
Text: text, Text: text,
...@@ -267,38 +266,66 @@ func (s *AntigravityGatewayService) getClaudeTransformOptions(ctx context.Contex ...@@ -267,38 +266,66 @@ func (s *AntigravityGatewayService) getClaudeTransformOptions(ctx context.Contex
return opts return opts
} }
// extractGeminiResponseText 从 Gemini 响应中提取文本 // extractTextFromSSEResponse 从 SSE 流式响应中提取文本
func extractGeminiResponseText(respBody []byte) string { func extractTextFromSSEResponse(respBody []byte) string {
var resp map[string]any var texts []string
if err := json.Unmarshal(respBody, &resp); err != nil { lines := bytes.Split(respBody, []byte("\n"))
return ""
}
candidates, ok := resp["candidates"].([]any) for _, line := range lines {
if !ok || len(candidates) == 0 { line = bytes.TrimSpace(line)
return "" if len(line) == 0 {
} continue
}
candidate, ok := candidates[0].(map[string]any) // 跳过 SSE 前缀
if !ok { if bytes.HasPrefix(line, []byte("data:")) {
return "" line = bytes.TrimPrefix(line, []byte("data:"))
} line = bytes.TrimSpace(line)
}
content, ok := candidate["content"].(map[string]any) // 跳过非 JSON 行
if !ok { if len(line) == 0 || line[0] != '{' {
return "" continue
} }
parts, ok := content["parts"].([]any) // 解析 JSON
if !ok { var data map[string]any
return "" if err := json.Unmarshal(line, &data); err != nil {
} continue
}
var texts []string // 尝试从 response.candidates[0].content.parts[].text 提取
for _, part := range parts { response, ok := data["response"].(map[string]any)
if partMap, ok := part.(map[string]any); ok { if !ok {
if text, ok := partMap["text"].(string); ok && text != "" { // 尝试直接从 candidates 提取(某些响应格式)
texts = append(texts, text) response = data
}
candidates, ok := response["candidates"].([]any)
if !ok || len(candidates) == 0 {
continue
}
candidate, ok := candidates[0].(map[string]any)
if !ok {
continue
}
content, ok := candidate["content"].(map[string]any)
if !ok {
continue
}
parts, ok := content["parts"].([]any)
if !ok {
continue
}
for _, part := range parts {
if partMap, ok := part.(map[string]any); ok {
if text, ok := partMap["text"].(string); ok && text != "" {
texts = append(texts, text)
}
} }
} }
} }
...@@ -316,7 +343,7 @@ func (s *AntigravityGatewayService) wrapV1InternalRequest(projectID, model strin ...@@ -316,7 +343,7 @@ func (s *AntigravityGatewayService) wrapV1InternalRequest(projectID, model strin
wrapped := map[string]any{ wrapped := map[string]any{
"project": projectID, "project": projectID,
"requestId": "agent-" + uuid.New().String(), "requestId": "agent-" + uuid.New().String(),
"userAgent": "sub2api", "userAgent": "antigravity", // 固定值,与官方客户端一致
"requestType": "agent", "requestType": "agent",
"model": model, "model": model,
"request": request, "request": request,
...@@ -397,10 +424,10 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, ...@@ -397,10 +424,10 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
return nil, fmt.Errorf("transform request: %w", err) return nil, fmt.Errorf("transform request: %w", err)
} }
// 构建上游 action // 构建上游 action(NewAPIRequest 会自动处理 ?alt=sse 和 Accept Header)
action := "generateContent" action := "generateContent"
if claudeReq.Stream { if claudeReq.Stream {
action = "streamGenerateContent?alt=sse" action = "streamGenerateContent"
} }
// 重试循环 // 重试循环
...@@ -907,14 +934,11 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co ...@@ -907,14 +934,11 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
return nil, err return nil, err
} }
// 构建上游 action // 构建上游 action(NewAPIRequest 会自动处理 ?alt=sse 和 Accept Header)
upstreamAction := action upstreamAction := action
if action == "generateContent" && stream { if action == "generateContent" && stream {
upstreamAction = "streamGenerateContent" upstreamAction = "streamGenerateContent"
} }
if stream || upstreamAction == "streamGenerateContent" {
upstreamAction += "?alt=sse"
}
// 重试循环 // 重试循环
var resp *http.Response var resp *http.Response
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment