merge: 合并 upstream/main 使用上游版本解决冲突
This commit is contained in:
@@ -144,7 +144,7 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
|
||||
out := make([]dto.UsageLog, 0, len(records))
|
||||
for i := range records {
|
||||
out = append(out, *dto.UsageLogFromService(&records[i]))
|
||||
out = append(out, *dto.UsageLogFromServiceAdmin(&records[i]))
|
||||
}
|
||||
response.Paginated(c, out, result.Total, page, pageSize)
|
||||
}
|
||||
|
||||
@@ -234,7 +234,21 @@ func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
|
||||
}
|
||||
}
|
||||
|
||||
func UsageLogFromService(l *service.UsageLog) *UsageLog {
|
||||
// AccountSummaryFromService returns a minimal AccountSummary for usage log display.
|
||||
// Only includes ID and Name - no sensitive fields like Credentials, Proxy, etc.
|
||||
func AccountSummaryFromService(a *service.Account) *AccountSummary {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return &AccountSummary{
|
||||
ID: a.ID,
|
||||
Name: a.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// usageLogFromServiceBase is a helper that converts service UsageLog to DTO.
|
||||
// The account parameter allows caller to control what Account info is included.
|
||||
func usageLogFromServiceBase(l *service.UsageLog, account *AccountSummary) *UsageLog {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -269,12 +283,27 @@ func UsageLogFromService(l *service.UsageLog) *UsageLog {
|
||||
CreatedAt: l.CreatedAt,
|
||||
User: UserFromServiceShallow(l.User),
|
||||
APIKey: APIKeyFromService(l.APIKey),
|
||||
Account: AccountFromService(l.Account),
|
||||
Account: account,
|
||||
Group: GroupFromServiceShallow(l.Group),
|
||||
Subscription: UserSubscriptionFromService(l.Subscription),
|
||||
}
|
||||
}
|
||||
|
||||
// UsageLogFromService converts a service UsageLog to DTO for regular users.
|
||||
// It excludes Account details - users should not see account information.
|
||||
func UsageLogFromService(l *service.UsageLog) *UsageLog {
|
||||
return usageLogFromServiceBase(l, nil)
|
||||
}
|
||||
|
||||
// UsageLogFromServiceAdmin converts a service UsageLog to DTO for admin users.
|
||||
// It includes minimal Account info (ID, Name only).
|
||||
func UsageLogFromServiceAdmin(l *service.UsageLog) *UsageLog {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return usageLogFromServiceBase(l, AccountSummaryFromService(l.Account))
|
||||
}
|
||||
|
||||
func SettingFromService(s *service.Setting) *Setting {
|
||||
if s == nil {
|
||||
return nil
|
||||
|
||||
@@ -184,11 +184,18 @@ type UsageLog struct {
|
||||
|
||||
User *User `json:"user,omitempty"`
|
||||
APIKey *APIKey `json:"api_key,omitempty"`
|
||||
Account *Account `json:"account,omitempty"`
|
||||
Account *AccountSummary `json:"account,omitempty"` // Use minimal AccountSummary to prevent data leakage
|
||||
Group *Group `json:"group,omitempty"`
|
||||
Subscription *UserSubscription `json:"subscription,omitempty"`
|
||||
}
|
||||
|
||||
// AccountSummary is a minimal account info for usage log display.
|
||||
// It intentionally excludes sensitive fields like Credentials, Proxy, etc.
|
||||
type AccountSummary struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
ID int64 `json:"id"`
|
||||
Key string `json:"key"`
|
||||
|
||||
@@ -7,28 +7,53 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"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 端点)
|
||||
func NewAPIRequest(ctx context.Context, action, accessToken string, body []byte) (*http.Request, error) {
|
||||
// 构建 URL,流式请求添加 ?alt=sse 参数
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 基础 Headers
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
req.Header.Set("requestType", "agent")
|
||||
|
||||
// 打印完整 HTTP 请求
|
||||
log.Printf("[NewAPIRequest] POST %s\nHeaders: Content-Type=%s, Authorization=Bearer %s..., requestType=%s, User-Agent=%s\nBody: %s",
|
||||
apiURL, req.Header.Get("Content-Type"), accessToken[:20], req.Header.Get("requestType"), req.Header.Get("User-Agent"), string(body))
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@ const (
|
||||
"https://www.googleapis.com/auth/experimentsandconfigs"
|
||||
|
||||
// API 端点
|
||||
BaseURL = "https://cloudcode-pa.googleapis.com"
|
||||
// 优先使用 sandbox daily URL,配额更宽松
|
||||
BaseURL = "https://daily-cloudcode-pa.sandbox.googleapis.com"
|
||||
|
||||
// User-Agent
|
||||
UserAgent = "antigravity/1.11.9 windows/amd64"
|
||||
// User-Agent(模拟官方客户端)
|
||||
UserAgent = "antigravity/1.104.0 darwin/arm64"
|
||||
|
||||
// Session 过期时间
|
||||
SessionTTL = 30 * time.Minute
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -182,12 +182,17 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account
|
||||
return nil, fmt.Errorf("构建请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建 HTTP 请求(非流式)
|
||||
req, err := antigravity.NewAPIRequest(ctx, "generateContent", accessToken, requestBody)
|
||||
// 构建 HTTP 请求(总是使用流式 endpoint,与官方客户端一致)
|
||||
req, err := antigravity.NewAPIRequest(ctx, "streamGenerateContent", accessToken, requestBody)
|
||||
if err != nil {
|
||||
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
|
||||
proxyURL := ""
|
||||
if account.ProxyID != nil && account.Proxy != nil {
|
||||
@@ -211,14 +216,8 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account
|
||||
return nil, fmt.Errorf("API 返回 %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
// 解包 v1internal 响应
|
||||
unwrapped, err := s.unwrapV1InternalResponse(respBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解包响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 提取响应文本
|
||||
text := extractGeminiResponseText(unwrapped)
|
||||
// 解析流式响应,提取文本
|
||||
text := extractTextFromSSEResponse(respBody)
|
||||
|
||||
return &TestConnectionResult{
|
||||
Text: text,
|
||||
@@ -268,38 +267,66 @@ func (s *AntigravityGatewayService) getClaudeTransformOptions(ctx context.Contex
|
||||
return opts
|
||||
}
|
||||
|
||||
// extractGeminiResponseText 从 Gemini 响应中提取文本
|
||||
func extractGeminiResponseText(respBody []byte) string {
|
||||
var resp map[string]any
|
||||
if err := json.Unmarshal(respBody, &resp); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
candidates, ok := resp["candidates"].([]any)
|
||||
if !ok || len(candidates) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
candidate, ok := candidates[0].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
content, ok := candidate["content"].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts, ok := content["parts"].([]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractTextFromSSEResponse 从 SSE 流式响应中提取文本
|
||||
func extractTextFromSSEResponse(respBody []byte) string {
|
||||
var texts []string
|
||||
for _, part := range parts {
|
||||
if partMap, ok := part.(map[string]any); ok {
|
||||
if text, ok := partMap["text"].(string); ok && text != "" {
|
||||
texts = append(texts, text)
|
||||
lines := bytes.Split(respBody, []byte("\n"))
|
||||
|
||||
for _, line := range lines {
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 跳过 SSE 前缀
|
||||
if bytes.HasPrefix(line, []byte("data:")) {
|
||||
line = bytes.TrimPrefix(line, []byte("data:"))
|
||||
line = bytes.TrimSpace(line)
|
||||
}
|
||||
|
||||
// 跳过非 JSON 行
|
||||
if len(line) == 0 || line[0] != '{' {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析 JSON
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(line, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 尝试从 response.candidates[0].content.parts[].text 提取
|
||||
response, ok := data["response"].(map[string]any)
|
||||
if !ok {
|
||||
// 尝试直接从 candidates 提取(某些响应格式)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,7 +344,7 @@ func (s *AntigravityGatewayService) wrapV1InternalRequest(projectID, model strin
|
||||
wrapped := map[string]any{
|
||||
"project": projectID,
|
||||
"requestId": "agent-" + uuid.New().String(),
|
||||
"userAgent": "sub2api",
|
||||
"userAgent": "antigravity", // 固定值,与官方客户端一致
|
||||
"requestType": "agent",
|
||||
"model": model,
|
||||
"request": request,
|
||||
@@ -398,10 +425,10 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
|
||||
return nil, fmt.Errorf("transform request: %w", err)
|
||||
}
|
||||
|
||||
// 构建上游 action
|
||||
// 构建上游 action(NewAPIRequest 会自动处理 ?alt=sse 和 Accept Header)
|
||||
action := "generateContent"
|
||||
if claudeReq.Stream {
|
||||
action = "streamGenerateContent?alt=sse"
|
||||
action = "streamGenerateContent"
|
||||
}
|
||||
|
||||
// 重试循环
|
||||
@@ -908,14 +935,11 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建上游 action
|
||||
// 构建上游 action(NewAPIRequest 会自动处理 ?alt=sse 和 Accept Header)
|
||||
upstreamAction := action
|
||||
if action == "generateContent" && stream {
|
||||
upstreamAction = "streamGenerateContent"
|
||||
}
|
||||
if stream || upstreamAction == "streamGenerateContent" {
|
||||
upstreamAction += "?alt=sse"
|
||||
}
|
||||
|
||||
// 重试循环
|
||||
var resp *http.Response
|
||||
|
||||
Reference in New Issue
Block a user