diff --git a/backend/internal/pkg/antigravity/client.go b/backend/internal/pkg/antigravity/client.go index 8b0ba6ec..3bcbf26b 100644 --- a/backend/internal/pkg/antigravity/client.go +++ b/backend/internal/pkg/antigravity/client.go @@ -215,20 +215,20 @@ func (c *Client) GetUserInfo(ctx context.Context, accessToken string) (*UserInfo return &userInfo, nil } -// LoadCodeAssist 获取 project_id -func (c *Client) LoadCodeAssist(ctx context.Context, accessToken string) (*LoadCodeAssistResponse, error) { +// LoadCodeAssist 获取账户信息,返回解析后的结构体和原始 JSON +func (c *Client) LoadCodeAssist(ctx context.Context, accessToken string) (*LoadCodeAssistResponse, map[string]any, error) { reqBody := LoadCodeAssistRequest{} reqBody.Metadata.IDEType = "ANTIGRAVITY" bodyBytes, err := json.Marshal(reqBody) if err != nil { - return nil, fmt.Errorf("序列化请求失败: %w", err) + return nil, nil, fmt.Errorf("序列化请求失败: %w", err) } url := BaseURL + "/v1internal:loadCodeAssist" req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(bodyBytes))) if err != nil { - return nil, fmt.Errorf("创建请求失败: %w", err) + return nil, nil, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Content-Type", "application/json") @@ -236,25 +236,29 @@ func (c *Client) LoadCodeAssist(ctx context.Context, accessToken string) (*LoadC resp, err := c.httpClient.Do(req) if err != nil { - return nil, fmt.Errorf("loadCodeAssist 请求失败: %w", err) + return nil, nil, fmt.Errorf("loadCodeAssist 请求失败: %w", err) } defer func() { _ = resp.Body.Close() }() respBodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("读取响应失败: %w", err) + return nil, nil, fmt.Errorf("读取响应失败: %w", err) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("loadCodeAssist 失败 (HTTP %d): %s", resp.StatusCode, string(respBodyBytes)) + return nil, nil, fmt.Errorf("loadCodeAssist 失败 (HTTP %d): %s", resp.StatusCode, string(respBodyBytes)) } var loadResp LoadCodeAssistResponse if err := json.Unmarshal(respBodyBytes, &loadResp); err != nil { - return nil, fmt.Errorf("响应解析失败: %w", err) + return nil, nil, fmt.Errorf("响应解析失败: %w", err) } - return &loadResp, nil + // 解析原始 JSON 为 map + var rawResp map[string]any + _ = json.Unmarshal(respBodyBytes, &rawResp) + + return &loadResp, rawResp, nil } // ModelQuotaInfo 模型配额信息 @@ -278,18 +282,18 @@ type FetchAvailableModelsResponse struct { Models map[string]ModelInfo `json:"models"` } -// FetchAvailableModels 获取可用模型和配额信息 -func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectID string) (*FetchAvailableModelsResponse, error) { +// FetchAvailableModels 获取可用模型和配额信息,返回解析后的结构体和原始 JSON +func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectID string) (*FetchAvailableModelsResponse, map[string]any, error) { reqBody := FetchAvailableModelsRequest{Project: projectID} bodyBytes, err := json.Marshal(reqBody) if err != nil { - return nil, fmt.Errorf("序列化请求失败: %w", err) + return nil, nil, fmt.Errorf("序列化请求失败: %w", err) } apiURL := BaseURL + "/v1internal:fetchAvailableModels" req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(bodyBytes))) if err != nil { - return nil, fmt.Errorf("创建请求失败: %w", err) + return nil, nil, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Content-Type", "application/json") @@ -297,23 +301,27 @@ func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectI resp, err := c.httpClient.Do(req) if err != nil { - return nil, fmt.Errorf("fetchAvailableModels 请求失败: %w", err) + return nil, nil, fmt.Errorf("fetchAvailableModels 请求失败: %w", err) } defer func() { _ = resp.Body.Close() }() respBodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("读取响应失败: %w", err) + return nil, nil, fmt.Errorf("读取响应失败: %w", err) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("fetchAvailableModels 失败 (HTTP %d): %s", resp.StatusCode, string(respBodyBytes)) + return nil, nil, fmt.Errorf("fetchAvailableModels 失败 (HTTP %d): %s", resp.StatusCode, string(respBodyBytes)) } var modelsResp FetchAvailableModelsResponse if err := json.Unmarshal(respBodyBytes, &modelsResp); err != nil { - return nil, fmt.Errorf("响应解析失败: %w", err) + return nil, nil, fmt.Errorf("响应解析失败: %w", err) } - return &modelsResp, nil + // 解析原始 JSON 为 map + var rawResp map[string]any + _ = json.Unmarshal(respBodyBytes, &rawResp) + + return &modelsResp, rawResp, nil } diff --git a/backend/internal/service/antigravity_oauth_service.go b/backend/internal/service/antigravity_oauth_service.go index 8192a9fb..9125999f 100644 --- a/backend/internal/service/antigravity_oauth_service.go +++ b/backend/internal/service/antigravity_oauth_service.go @@ -142,7 +142,7 @@ func (s *AntigravityOAuthService) ExchangeCode(ctx context.Context, input *Antig } // 获取 project_id(部分账户类型可能没有) - loadResp, err := client.LoadCodeAssist(ctx, tokenResp.AccessToken) + loadResp, _, err := client.LoadCodeAssist(ctx, tokenResp.AccessToken) if err != nil { fmt.Printf("[AntigravityOAuth] 警告: 获取 project_id 失败: %v\n", err) } else if loadResp != nil && loadResp.CloudAICompanionProject != "" { diff --git a/backend/internal/service/antigravity_quota_refresher.go b/backend/internal/service/antigravity_quota_refresher.go index 8f63fa38..dd579ef1 100644 --- a/backend/internal/service/antigravity_quota_refresher.go +++ b/backend/internal/service/antigravity_quota_refresher.go @@ -145,10 +145,16 @@ func (r *AntigravityQuotaRefresher) refreshAccountQuota(ctx context.Context, acc client := antigravity.NewClient(proxyURL) - // 获取账户类型(tier)和 project_id - loadResp, _ := client.LoadCodeAssist(ctx, accessToken) + if account.Extra == nil { + account.Extra = make(map[string]any) + } + + // 获取账户信息(tier、project_id 等) + loadResp, loadRaw, _ := client.LoadCodeAssist(ctx, accessToken) + if loadRaw != nil { + account.Extra["load_code_assist"] = loadRaw + } if loadResp != nil { - r.updateAccountTier(account, loadResp) // 尝试从 API 获取 project_id if projectID == "" && loadResp.CloudAICompanionProject != "" { projectID = loadResp.CloudAICompanionProject @@ -164,14 +170,21 @@ func (r *AntigravityQuotaRefresher) refreshAccountQuota(ctx context.Context, acc } // 调用 API 获取配额 - modelsResp, err := client.FetchAvailableModels(ctx, accessToken, projectID) + modelsResp, modelsRaw, err := client.FetchAvailableModels(ctx, accessToken, projectID) if err != nil { - return err + return r.accountRepo.Update(ctx, account) // 保存已有的 load_code_assist 信息 } - // 解析配额数据并更新 extra 字段 + // 保存完整的配额响应 + if modelsRaw != nil { + account.Extra["available_models"] = modelsRaw + } + + // 解析配额数据为前端使用的格式 r.updateAccountQuota(account, modelsResp) + account.Extra["last_refresh"] = time.Now().Format(time.RFC3339) + // 保存到数据库 return r.accountRepo.Update(ctx, account) } @@ -187,35 +200,8 @@ func (r *AntigravityQuotaRefresher) isTokenExpired(account *Account) bool { return time.Now().Add(5 * time.Minute).After(*expiresAt) } -// updateAccountTier 更新账户类型信息 -func (r *AntigravityQuotaRefresher) updateAccountTier(account *Account, loadResp *antigravity.LoadCodeAssistResponse) { - if account.Extra == nil { - account.Extra = make(map[string]any) - } - - tier := loadResp.GetTier() - if tier != "" { - account.Extra["tier"] = tier - } - - // 保存不符合条件的原因(如 INELIGIBLE_ACCOUNT) - if len(loadResp.IneligibleTiers) > 0 && loadResp.IneligibleTiers[0] != nil { - ineligible := loadResp.IneligibleTiers[0] - if ineligible.ReasonCode != "" { - account.Extra["ineligible_reason_code"] = ineligible.ReasonCode - } - if ineligible.ReasonMessage != "" { - account.Extra["ineligible_reason_message"] = ineligible.ReasonMessage - } - } -} - -// updateAccountQuota 更新账户的配额信息 +// updateAccountQuota 更新账户的配额信息(前端使用的格式) func (r *AntigravityQuotaRefresher) updateAccountQuota(account *Account, modelsResp *antigravity.FetchAvailableModelsResponse) { - if account.Extra == nil { - account.Extra = make(map[string]any) - } - quota := make(map[string]any) for modelName, modelInfo := range modelsResp.Models { @@ -233,5 +219,4 @@ func (r *AntigravityQuotaRefresher) updateAccountQuota(account *Account, modelsR } account.Extra["quota"] = quota - account.Extra["last_quota_check"] = time.Now().Format(time.RFC3339) } diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index ea222c33..cbd93df8 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -403,11 +403,26 @@ const antigravityClaude45Usage = computed(() => getAntigravityUsage(['claude-sonnet-4-5', 'claude-opus-4-5-thinking']) ) -// Antigravity 账户类型 +// Antigravity 账户类型(从 load_code_assist 响应中提取) const antigravityTier = computed(() => { const extra = props.account.extra as Record | undefined - if (!extra || typeof extra.tier !== 'string') return null - return extra.tier as string + if (!extra) return null + + const loadCodeAssist = extra.load_code_assist as Record | undefined + if (!loadCodeAssist) return null + + // 优先取 paidTier,否则取 currentTier + const paidTier = loadCodeAssist.paidTier as Record | undefined + if (paidTier && typeof paidTier.id === 'string') { + return paidTier.id + } + + const currentTier = loadCodeAssist.currentTier as Record | undefined + if (currentTier && typeof currentTier.id === 'string') { + return currentTier.id + } + + return null }) // 账户类型显示标签