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))
|
out := make([]dto.UsageLog, 0, len(records))
|
||||||
for i := range 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)
|
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 {
|
if l == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -269,12 +283,27 @@ func UsageLogFromService(l *service.UsageLog) *UsageLog {
|
|||||||
CreatedAt: l.CreatedAt,
|
CreatedAt: l.CreatedAt,
|
||||||
User: UserFromServiceShallow(l.User),
|
User: UserFromServiceShallow(l.User),
|
||||||
APIKey: APIKeyFromService(l.APIKey),
|
APIKey: APIKeyFromService(l.APIKey),
|
||||||
Account: AccountFromService(l.Account),
|
Account: account,
|
||||||
Group: GroupFromServiceShallow(l.Group),
|
Group: GroupFromServiceShallow(l.Group),
|
||||||
Subscription: UserSubscriptionFromService(l.Subscription),
|
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 {
|
func SettingFromService(s *service.Setting) *Setting {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -184,11 +184,18 @@ type UsageLog struct {
|
|||||||
|
|
||||||
User *User `json:"user,omitempty"`
|
User *User `json:"user,omitempty"`
|
||||||
APIKey *APIKey `json:"api_key,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"`
|
Group *Group `json:"group,omitempty"`
|
||||||
Subscription *UserSubscription `json:"subscription,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 {
|
type Setting struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
|||||||
@@ -7,28 +7,53 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"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)
|
||||||
req.Header.Set("requestType", "agent")
|
|
||||||
|
|
||||||
// 打印完整 HTTP 请求
|
// Accept Header 根据请求类型设置
|
||||||
log.Printf("[NewAPIRequest] POST %s\nHeaders: Content-Type=%s, Authorization=Bearer %s..., requestType=%s, User-Agent=%s\nBody: %s",
|
if isStream {
|
||||||
apiURL, req.Header.Get("Content-Type"), accessToken[:20], req.Header.Get("requestType"), req.Header.Get("User-Agent"), string(body))
|
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 (
|
|||||||
"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
|
||||||
|
|||||||
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)
|
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 {
|
||||||
@@ -211,14 +216,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,
|
||||||
@@ -268,38 +267,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
|
|
||||||
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 ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var texts []string
|
var texts []string
|
||||||
for _, part := range parts {
|
lines := bytes.Split(respBody, []byte("\n"))
|
||||||
if partMap, ok := part.(map[string]any); ok {
|
|
||||||
if text, ok := partMap["text"].(string); ok && text != "" {
|
for _, line := range lines {
|
||||||
texts = append(texts, text)
|
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{
|
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,
|
||||||
@@ -398,10 +425,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"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重试循环
|
// 重试循环
|
||||||
@@ -908,14 +935,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
|
||||||
|
|||||||
Reference in New Issue
Block a user