From 3d296d8898e027b4ea8a1379052f623190444336 Mon Sep 17 00:00:00 2001 From: shaw Date: Tue, 30 Dec 2025 17:08:36 +0800 Subject: [PATCH 01/10] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8D=20gofmt=20?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 格式化以下测试文件以符合 Go 代码风格规范: - fixtures_integration_test.go - user_repo_integration_test.go - api_key_service_delete_test.go --- backend/internal/repository/fixtures_integration_test.go | 1 - backend/internal/repository/user_repo_integration_test.go | 5 ++--- backend/internal/service/api_key_service_delete_test.go | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/internal/repository/fixtures_integration_test.go b/backend/internal/repository/fixtures_integration_test.go index 253f24f0..8f13c532 100644 --- a/backend/internal/repository/fixtures_integration_test.go +++ b/backend/internal/repository/fixtures_integration_test.go @@ -390,4 +390,3 @@ func mustBindAccountToGroup(t *testing.T, client *dbent.Client, accountID, group Save(ctx) require.NoError(t, err, "create account_group") } - diff --git a/backend/internal/repository/user_repo_integration_test.go b/backend/internal/repository/user_repo_integration_test.go index a59d2312..59616185 100644 --- a/backend/internal/repository/user_repo_integration_test.go +++ b/backend/internal/repository/user_repo_integration_test.go @@ -76,8 +76,8 @@ func (s *UserRepoSuite) mustCreateSubscription(userID, groupID int64, mutate fun create := s.client.UserSubscription.Create(). SetUserID(userID). SetGroupID(groupID). - SetStartsAt(now.Add(-1*time.Hour)). - SetExpiresAt(now.Add(24*time.Hour)). + SetStartsAt(now.Add(-1 * time.Hour)). + SetExpiresAt(now.Add(24 * time.Hour)). SetStatus(service.SubscriptionStatusActive). SetAssignedAt(now). SetNotes("") @@ -507,4 +507,3 @@ func (s *UserRepoSuite) TestCRUD_And_Filters_And_AtomicUpdates() { s.Require().Len(users, 1, "ListWithFilters len mismatch") s.Require().Equal(user2.ID, users[0].ID, "ListWithFilters result mismatch") } - diff --git a/backend/internal/service/api_key_service_delete_test.go b/backend/internal/service/api_key_service_delete_test.go index a531d0b8..deac8499 100644 --- a/backend/internal/service/api_key_service_delete_test.go +++ b/backend/internal/service/api_key_service_delete_test.go @@ -146,8 +146,8 @@ func TestApiKeyService_Delete_OwnerMismatch(t *testing.T) { err := svc.Delete(context.Background(), 10, 2) // API Key ID=10, 调用者 userID=2 require.ErrorIs(t, err, ErrInsufficientPerms) - require.Empty(t, repo.deletedIDs) // 验证删除操作未被调用 - require.Empty(t, cache.invalidated) // 验证缓存未被清除 + require.Empty(t, repo.deletedIDs) // 验证删除操作未被调用 + require.Empty(t, cache.invalidated) // 验证缓存未被清除 } // TestApiKeyService_Delete_Success 测试所有者成功删除 API Key 的场景。 @@ -164,7 +164,7 @@ func TestApiKeyService_Delete_Success(t *testing.T) { err := svc.Delete(context.Background(), 42, 7) // API Key ID=42, 调用者 userID=7 require.NoError(t, err) - require.Equal(t, []int64{42}, repo.deletedIDs) // 验证正确的 API Key 被删除 + require.Equal(t, []int64{42}, repo.deletedIDs) // 验证正确的 API Key 被删除 require.Equal(t, []int64{7}, cache.invalidated) // 验证所有者的缓存被清除 } From 5844ea7e6e1b212cced55b674be3171658391eda Mon Sep 17 00:00:00 2001 From: song Date: Tue, 30 Dec 2025 22:42:00 +0800 Subject: [PATCH 02/10] =?UTF-8?q?fix(Antigravity):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E6=B5=8B=E8=AF=95=E8=BF=9E=E6=8E=A5=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AntigravityGatewayService.TestConnection 方法,支持 Claude/Gemini 双协议测试 - AccountTestService 改用 AntigravityGatewayService 进行测试连接 - GetAvailableModels 为 Antigravity 账号返回 Claude + Gemini 模型列表 --- backend/cmd/server/wire_gen.go | 22 +-- .../internal/handler/admin/account_handler.go | 31 ++++ .../internal/service/account_test_service.go | 74 ++++++-- .../service/antigravity_gateway_service.go | 159 ++++++++++++++++++ 4 files changed, 264 insertions(+), 22 deletions(-) diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index f37d696b..79827b26 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -39,11 +39,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { if err != nil { return nil, err } - sqlDB, err := infrastructure.ProvideSQLDB(client) + db, err := infrastructure.ProvideSQLDB(client) if err != nil { return nil, err } - userRepository := repository.NewUserRepository(client, sqlDB) + userRepository := repository.NewUserRepository(client, db) settingRepository := repository.NewSettingRepository(client) settingService := service.NewSettingService(settingRepository, configConfig) redisClient := infrastructure.ProvideRedis(configConfig) @@ -57,12 +57,12 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { authHandler := handler.NewAuthHandler(configConfig, authService, userService) userHandler := handler.NewUserHandler(userService) apiKeyRepository := repository.NewApiKeyRepository(client) - groupRepository := repository.NewGroupRepository(client, sqlDB) + groupRepository := repository.NewGroupRepository(client, db) userSubscriptionRepository := repository.NewUserSubscriptionRepository(client) apiKeyCache := repository.NewApiKeyCache(redisClient) apiKeyService := service.NewApiKeyService(apiKeyRepository, userRepository, groupRepository, userSubscriptionRepository, apiKeyCache, configConfig) apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService) - usageLogRepository := repository.NewUsageLogRepository(client, sqlDB) + usageLogRepository := repository.NewUsageLogRepository(client, db) usageService := service.NewUsageService(usageLogRepository, userRepository) usageHandler := handler.NewUsageHandler(usageService, apiKeyService) redeemCodeRepository := repository.NewRedeemCodeRepository(client) @@ -75,8 +75,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService) dashboardService := service.NewDashboardService(usageLogRepository) dashboardHandler := admin.NewDashboardHandler(dashboardService) - accountRepository := repository.NewAccountRepository(client, sqlDB) - proxyRepository := repository.NewProxyRepository(client, sqlDB) + accountRepository := repository.NewAccountRepository(client, db) + proxyRepository := repository.NewProxyRepository(client, db) proxyExitInfoProber := repository.NewProxyExitInfoProber() adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, billingCacheService, proxyExitInfoProber) adminUserHandler := admin.NewUserHandler(adminService) @@ -93,8 +93,12 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher) geminiTokenCache := repository.NewGeminiTokenCache(redisClient) geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService) + gatewayCache := repository.NewGatewayCache(redisClient) + antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository) + antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService) httpUpstream := repository.NewHTTPUpstream(configConfig) - accountTestService := service.NewAccountTestService(accountRepository, oAuthService, openAIOAuthService, geminiTokenProvider, httpUpstream) + antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, antigravityTokenProvider, rateLimitService, httpUpstream) + accountTestService := service.NewAccountTestService(accountRepository, oAuthService, openAIOAuthService, geminiTokenProvider, antigravityGatewayService, httpUpstream) concurrencyCache := repository.NewConcurrencyCache(redisClient) concurrencyService := service.NewConcurrencyService(concurrencyCache) crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService) @@ -102,7 +106,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { oAuthHandler := admin.NewOAuthHandler(oAuthService) openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService) geminiOAuthHandler := admin.NewGeminiOAuthHandler(geminiOAuthService) - antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository) antigravityOAuthHandler := admin.NewAntigravityOAuthHandler(antigravityOAuthService) proxyHandler := admin.NewProxyHandler(adminService) adminRedeemHandler := admin.NewRedeemHandler(adminService) @@ -115,7 +118,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { adminSubscriptionHandler := admin.NewSubscriptionHandler(subscriptionService) adminUsageHandler := admin.NewUsageHandler(usageService, apiKeyService, adminService) adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler) - gatewayCache := repository.NewGatewayCache(redisClient) pricingRemoteClient := repository.NewPricingRemoteClient() pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient) if err != nil { @@ -127,8 +129,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { timingWheelService := service.ProvideTimingWheelService() deferredService := service.ProvideDeferredService(accountRepository, timingWheelService) gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService) - antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService) - antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, antigravityTokenProvider, rateLimitService, httpUpstream) geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, groupRepository, gatewayCache, geminiTokenProvider, rateLimitService, httpUpstream, antigravityGatewayService) gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService) openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService) diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 5fa2f4e1..ac938f8c 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -918,6 +918,37 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) { return } + // Handle Antigravity accounts: return Claude + Gemini models + if account.Platform == service.PlatformAntigravity { + // Antigravity 支持 Claude 和部分 Gemini 模型 + type UnifiedModel struct { + ID string `json:"id"` + Type string `json:"type"` + DisplayName string `json:"display_name"` + } + + var models []UnifiedModel + + // 添加 Claude 模型 + for _, m := range claude.DefaultModels { + models = append(models, UnifiedModel{ + ID: m.ID, + Type: m.Type, + DisplayName: m.DisplayName, + }) + } + + // 添加 Gemini 3 系列模型用于测试 + geminiTestModels := []UnifiedModel{ + {ID: "gemini-3-flash", Type: "model", DisplayName: "Gemini 3 Flash"}, + {ID: "gemini-3-pro-preview", Type: "model", DisplayName: "Gemini 3 Pro Preview"}, + } + models = append(models, geminiTestModels...) + + response.Success(c, models) + return + } + // Handle Claude/Anthropic accounts // For OAuth and Setup-Token accounts: return default models if account.IsOAuth() { diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 6296f2fe..3223eb18 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -44,11 +44,12 @@ type TestEvent struct { // AccountTestService handles account testing operations type AccountTestService struct { - accountRepo AccountRepository - oauthService *OAuthService - openaiOAuthService *OpenAIOAuthService - geminiTokenProvider *GeminiTokenProvider - httpUpstream HTTPUpstream + accountRepo AccountRepository + oauthService *OAuthService + openaiOAuthService *OpenAIOAuthService + geminiTokenProvider *GeminiTokenProvider + antigravityGatewayService *AntigravityGatewayService + httpUpstream HTTPUpstream } // NewAccountTestService creates a new AccountTestService @@ -57,14 +58,16 @@ func NewAccountTestService( oauthService *OAuthService, openaiOAuthService *OpenAIOAuthService, geminiTokenProvider *GeminiTokenProvider, + antigravityGatewayService *AntigravityGatewayService, httpUpstream HTTPUpstream, ) *AccountTestService { return &AccountTestService{ - accountRepo: accountRepo, - oauthService: oauthService, - openaiOAuthService: openaiOAuthService, - geminiTokenProvider: geminiTokenProvider, - httpUpstream: httpUpstream, + accountRepo: accountRepo, + oauthService: oauthService, + openaiOAuthService: openaiOAuthService, + geminiTokenProvider: geminiTokenProvider, + antigravityGatewayService: antigravityGatewayService, + httpUpstream: httpUpstream, } } @@ -141,6 +144,10 @@ func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int return s.testGeminiAccountConnection(c, account, modelID) } + if account.Platform == PlatformAntigravity { + return s.testAntigravityAccountConnection(c, account, modelID) + } + return s.testClaudeAccountConnection(c, account, modelID) } @@ -457,6 +464,46 @@ func (s *AccountTestService) testGeminiAccountConnection(c *gin.Context, account return s.processGeminiStream(c, resp.Body) } +// testAntigravityAccountConnection tests an Antigravity account's connection +// 支持 Claude 和 Gemini 两种协议,使用非流式请求 +func (s *AccountTestService) testAntigravityAccountConnection(c *gin.Context, account *Account, modelID string) error { + ctx := c.Request.Context() + + // 默认模型:Claude 使用 claude-sonnet-4-5,Gemini 使用 gemini-3-pro-preview + testModelID := modelID + if testModelID == "" { + testModelID = "claude-sonnet-4-5" + } + + if s.antigravityGatewayService == nil { + return s.sendErrorAndEnd(c, "Antigravity gateway service not configured") + } + + // Set SSE headers + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("X-Accel-Buffering", "no") + c.Writer.Flush() + + // Send test_start event + s.sendEvent(c, TestEvent{Type: "test_start", Model: testModelID}) + + // 调用 AntigravityGatewayService.TestConnection(复用协议转换逻辑) + result, err := s.antigravityGatewayService.TestConnection(ctx, account, testModelID) + if err != nil { + return s.sendErrorAndEnd(c, err.Error()) + } + + // 发送响应内容 + if result.Text != "" { + s.sendEvent(c, TestEvent{Type: "content", Text: result.Text}) + } + + s.sendEvent(c, TestEvent{Type: "test_complete", Success: true}) + return nil +} + // buildGeminiAPIKeyRequest builds request for Gemini API Key accounts func (s *AccountTestService) buildGeminiAPIKeyRequest(ctx context.Context, account *Account, modelID string, payload []byte) (*http.Request, error) { apiKey := account.GetCredential("api_key") @@ -514,7 +561,12 @@ func (s *AccountTestService) buildGeminiOAuthRequest(ctx context.Context, accoun return req, nil } - // Wrap payload in Code Assist format + // Code Assist mode (with project_id) + return s.buildCodeAssistRequest(ctx, accessToken, projectID, modelID, payload) +} + +// buildCodeAssistRequest builds request for Google Code Assist API (used by Gemini CLI and Antigravity) +func (s *AccountTestService) buildCodeAssistRequest(ctx context.Context, accessToken, projectID, modelID string, payload []byte) (*http.Request, error) { var inner map[string]any if err := json.Unmarshal(payload, &inner); err != nil { return nil, err diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 18a67fdf..670f53ee 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -130,6 +130,165 @@ func (s *AntigravityGatewayService) IsModelSupported(requestedModel string) bool return false } +// TestConnectionResult 测试连接结果 +type TestConnectionResult struct { + Text string // 响应文本 + MappedModel string // 实际使用的模型 +} + +// TestConnection 测试 Antigravity 账号连接(非流式,无重试、无计费) +// 支持 Claude 和 Gemini 两种协议,根据 modelID 前缀自动选择 +func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account *Account, modelID string) (*TestConnectionResult, error) { + // 获取 token + if s.tokenProvider == nil { + return nil, errors.New("antigravity token provider not configured") + } + accessToken, err := s.tokenProvider.GetAccessToken(ctx, account) + if err != nil { + return nil, fmt.Errorf("获取 access_token 失败: %w", err) + } + + // 获取 project_id + projectID := strings.TrimSpace(account.GetCredential("project_id")) + if projectID == "" { + return nil, errors.New("project_id not found in credentials") + } + + // 模型映射 + mappedModel := s.getMappedModel(account, modelID) + + // 构建请求体 + var requestBody []byte + if strings.HasPrefix(modelID, "gemini-") { + // Gemini 模型:直接使用 Gemini 格式 + requestBody, err = s.buildGeminiTestRequest(projectID, mappedModel) + } else { + // Claude 模型:使用协议转换 + requestBody, err = s.buildClaudeTestRequest(projectID, mappedModel) + } + if err != nil { + return nil, fmt.Errorf("构建请求失败: %w", err) + } + + // 构建 HTTP 请求(非流式) + fullURL := fmt.Sprintf("%s/v1internal:generateContent", antigravity.BaseURL) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(requestBody)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("User-Agent", antigravity.UserAgent) + + // 代理 URL + proxyURL := "" + if account.ProxyID != nil && account.Proxy != nil { + proxyURL = account.Proxy.URL() + } + + // 发送请求 + resp, err := s.httpUpstream.Do(req, proxyURL) + if err != nil { + return nil, fmt.Errorf("请求失败: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + // 读取响应 + respBody, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) + if err != nil { + return nil, fmt.Errorf("读取响应失败: %w", err) + } + + if resp.StatusCode >= 400 { + 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) + + return &TestConnectionResult{ + Text: text, + MappedModel: mappedModel, + }, nil +} + +// buildGeminiTestRequest 构建 Gemini 格式测试请求 +func (s *AntigravityGatewayService) buildGeminiTestRequest(projectID, model string) ([]byte, error) { + payload := map[string]any{ + "contents": []map[string]any{ + { + "role": "user", + "parts": []map[string]any{ + {"text": "hi"}, + }, + }, + }, + } + payloadBytes, _ := json.Marshal(payload) + return s.wrapV1InternalRequest(projectID, model, payloadBytes) +} + +// buildClaudeTestRequest 构建 Claude 格式测试请求并转换为 Gemini 格式 +func (s *AntigravityGatewayService) buildClaudeTestRequest(projectID, mappedModel string) ([]byte, error) { + claudeReq := &antigravity.ClaudeRequest{ + Model: mappedModel, + Messages: []antigravity.ClaudeMessage{ + { + Role: "user", + Content: json.RawMessage(`"hi"`), + }, + }, + MaxTokens: 1024, + Stream: false, + } + return antigravity.TransformClaudeToGemini(claudeReq, projectID, mappedModel) +} + +// 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 "" + } + + 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) + } + } + } + + return strings.Join(texts, "") +} + // wrapV1InternalRequest 包装请求为 v1internal 格式 func (s *AntigravityGatewayService) wrapV1InternalRequest(projectID, model string, originalBody []byte) ([]byte, error) { var request any From 1ecef269f7942640f0347702817cff0ea100eade Mon Sep 17 00:00:00 2001 From: shaw Date: Tue, 30 Dec 2025 23:07:25 +0800 Subject: [PATCH 03/10] =?UTF-8?q?fix:=20=E5=8E=BB=E9=99=A4openai-apkey?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E8=AF=B7=E6=B1=82=E8=B7=AF=E5=BE=84=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E7=9A=84v1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 25 ------------------- .../internal/service/account_test_service.go | 2 +- .../service/openai_gateway_service.go | 2 +- build_image.sh | 0 4 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 AGENTS.md mode change 100755 => 100644 build_image.sh diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 99e1f981..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,25 +0,0 @@ - -# OpenSpec Instructions - -These instructions are for AI assistants working in this project. - -Always open `@/openspec/AGENTS.md` when the request: -- Mentions planning or proposals (words like proposal, spec, change, plan) -- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work -- Sounds ambiguous and you need the authoritative spec before coding - -Use `@/openspec/AGENTS.md` to learn: -- How to create and apply change proposals -- Spec format and conventions -- Project structure and guidelines - -Keep this managed block so 'openspec update' can refresh the instructions. - - - -## 强制语言规范 - -以下为强制规定: -- 与用户交流一律使用中文。 -- 代码文档与代码注释一律使用中文。 -- OpenSpec 提案与相关说明一律使用中文。 diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 6296f2fe..c62b4210 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -328,7 +328,7 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account if baseURL == "" { baseURL = "https://api.openai.com" } - apiURL = strings.TrimSuffix(baseURL, "/") + "/v1/responses" + apiURL = strings.TrimSuffix(baseURL, "/") + "/responses" } else { return s.sendErrorAndEnd(c, fmt.Sprintf("Unsupported account type: %s", account.Type)) } diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index 79801b29..2bb0bee8 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -371,7 +371,7 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin. // API Key accounts use Platform API or custom base URL baseURL := account.GetOpenAIBaseURL() if baseURL != "" { - targetURL = baseURL + "/v1/responses" + targetURL = baseURL + "/responses" } else { targetURL = openaiPlatformAPIURL } diff --git a/build_image.sh b/build_image.sh old mode 100755 new mode 100644 From 4319cf7f31ceca863fa8674bb6309a03ed1ad1d1 Mon Sep 17 00:00:00 2001 From: shaw Date: Tue, 30 Dec 2025 23:11:49 +0800 Subject: [PATCH 04/10] =?UTF-8?q?fix(=E4=BB=93=E5=82=A8):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20BatchUpdateLastUsed=20=E6=97=B6=E9=97=B4=E6=88=B3?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=8D=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在原生 SQL 的 CASE WHEN 语句中,PostgreSQL 无法自动推断占位符参数类型, 导致 time.Time 被当作 text 类型处理,与 last_used_at 列的 timestamptz 类型不匹配。 添加显式类型转换 ::timestamptz 解决此问题。 --- backend/internal/repository/account_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go index 19dff447..aa880269 100644 --- a/backend/internal/repository/account_repo.go +++ b/backend/internal/repository/account_repo.go @@ -334,7 +334,7 @@ func (r *accountRepository) BatchUpdateLastUsed(ctx context.Context, updates map idx := 1 for id, ts := range updates { - caseSQL += " WHEN $" + itoa(idx) + " THEN $" + itoa(idx+1) + caseSQL += " WHEN $" + itoa(idx) + " THEN $" + itoa(idx+1) + "::timestamptz" args = append(args, id, ts) ids = append(ids, id) idx += 2 From 1c42403e6df0cf7a06a264bdc438ebe7d68286f6 Mon Sep 17 00:00:00 2001 From: song Date: Tue, 30 Dec 2025 23:42:50 +0800 Subject: [PATCH 05/10] =?UTF-8?q?fix(Antigravity):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=97=A0=20project=5Fid=20=E7=9A=84=E8=B4=A6=E6=88=B7=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 project_id 强制检查,部分账户类型 API 不返回此字段 - 重构:提取 antigravity.NewAPIRequest() 统一创建 API 请求 - quota_refresher: 无 project_id 时仍可更新 tier 信息 --- backend/internal/pkg/antigravity/client.go | 14 ++++++ .../service/antigravity_gateway_service.go | 44 +++++-------------- .../service/antigravity_oauth_service.go | 2 +- .../service/antigravity_quota_refresher.go | 9 ++-- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/backend/internal/pkg/antigravity/client.go b/backend/internal/pkg/antigravity/client.go index d425b881..8b0ba6ec 100644 --- a/backend/internal/pkg/antigravity/client.go +++ b/backend/internal/pkg/antigravity/client.go @@ -1,6 +1,7 @@ package antigravity import ( + "bytes" "context" "encoding/json" "fmt" @@ -11,6 +12,19 @@ import ( "time" ) +// NewAPIRequest 创建 Antigravity API 请求(v1internal 端点) +func NewAPIRequest(ctx context.Context, action, accessToken string, body []byte) (*http.Request, error) { + apiURL := fmt.Sprintf("%s/v1internal:%s", BaseURL, action) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("User-Agent", UserAgent) + return req, nil +} + // TokenResponse Google OAuth token 响应 type TokenResponse struct { AccessToken string `json:"access_token"` diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 670f53ee..94b37371 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -148,11 +148,8 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account return nil, fmt.Errorf("获取 access_token 失败: %w", err) } - // 获取 project_id + // 获取 project_id(部分账户类型可能没有) projectID := strings.TrimSpace(account.GetCredential("project_id")) - if projectID == "" { - return nil, errors.New("project_id not found in credentials") - } // 模型映射 mappedModel := s.getMappedModel(account, modelID) @@ -171,14 +168,10 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account } // 构建 HTTP 请求(非流式) - fullURL := fmt.Sprintf("%s/v1internal:generateContent", antigravity.BaseURL) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(requestBody)) + req, err := antigravity.NewAPIRequest(ctx, "generateContent", accessToken, requestBody) if err != nil { return nil, err } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+accessToken) - req.Header.Set("User-Agent", antigravity.UserAgent) // 代理 URL proxyURL := "" @@ -350,11 +343,8 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, return nil, fmt.Errorf("获取 access_token 失败: %w", err) } - // 获取 project_id + // 获取 project_id(部分账户类型可能没有) projectID := strings.TrimSpace(account.GetCredential("project_id")) - if projectID == "" { - return nil, errors.New("project_id not found in credentials") - } // 代理 URL proxyURL := "" @@ -368,26 +358,19 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, return nil, fmt.Errorf("transform request: %w", err) } - // 构建上游 URL + // 构建上游 action action := "generateContent" if claudeReq.Stream { - action = "streamGenerateContent" - } - fullURL := fmt.Sprintf("%s/v1internal:%s", antigravity.BaseURL, action) - if claudeReq.Stream { - fullURL += "?alt=sse" + action = "streamGenerateContent?alt=sse" } // 重试循环 var resp *http.Response for attempt := 1; attempt <= antigravityMaxRetries; attempt++ { - upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(geminiBody)) + upstreamReq, err := antigravity.NewAPIRequest(ctx, action, accessToken, geminiBody) if err != nil { return nil, err } - upstreamReq.Header.Set("Content-Type", "application/json") - upstreamReq.Header.Set("Authorization", "Bearer "+accessToken) - upstreamReq.Header.Set("User-Agent", antigravity.UserAgent) resp, err = s.httpUpstream.Do(upstreamReq, proxyURL) if err != nil { @@ -500,11 +483,8 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co return nil, fmt.Errorf("获取 access_token 失败: %w", err) } - // 获取 project_id + // 获取 project_id(部分账户类型可能没有) projectID := strings.TrimSpace(account.GetCredential("project_id")) - if projectID == "" { - return nil, errors.New("project_id not found in credentials") - } // 代理 URL proxyURL := "" @@ -518,26 +498,22 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co return nil, err } - // 构建上游 URL + // 构建上游 action upstreamAction := action if action == "generateContent" && stream { upstreamAction = "streamGenerateContent" } - fullURL := fmt.Sprintf("%s/v1internal:%s", antigravity.BaseURL, upstreamAction) if stream || upstreamAction == "streamGenerateContent" { - fullURL += "?alt=sse" + upstreamAction += "?alt=sse" } // 重试循环 var resp *http.Response for attempt := 1; attempt <= antigravityMaxRetries; attempt++ { - upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(wrappedBody)) + upstreamReq, err := antigravity.NewAPIRequest(ctx, upstreamAction, accessToken, wrappedBody) if err != nil { return nil, err } - upstreamReq.Header.Set("Content-Type", "application/json") - upstreamReq.Header.Set("Authorization", "Bearer "+accessToken) - upstreamReq.Header.Set("User-Agent", antigravity.UserAgent) resp, err = s.httpUpstream.Do(upstreamReq, proxyURL) if err != nil { diff --git a/backend/internal/service/antigravity_oauth_service.go b/backend/internal/service/antigravity_oauth_service.go index fc6cc74d..0d104043 100644 --- a/backend/internal/service/antigravity_oauth_service.go +++ b/backend/internal/service/antigravity_oauth_service.go @@ -141,7 +141,7 @@ func (s *AntigravityOAuthService) ExchangeCode(ctx context.Context, input *Antig result.Email = userInfo.Email } - // 获取 project_id + // 获取 project_id(部分账户类型可能没有) loadResp, err := client.LoadCodeAssist(ctx, tokenResp.AccessToken) if err != nil { fmt.Printf("[AntigravityOAuth] 警告: 获取 project_id 失败: %v\n", err) diff --git a/backend/internal/service/antigravity_quota_refresher.go b/backend/internal/service/antigravity_quota_refresher.go index 5ed59d2f..de067b07 100644 --- a/backend/internal/service/antigravity_quota_refresher.go +++ b/backend/internal/service/antigravity_quota_refresher.go @@ -125,8 +125,8 @@ func (r *AntigravityQuotaRefresher) refreshAccountQuota(ctx context.Context, acc accessToken := account.GetCredential("access_token") projectID := account.GetCredential("project_id") - if accessToken == "" || projectID == "" { - return nil // 没有有效凭证,跳过 + if accessToken == "" { + return nil // 没有 access_token,跳过 } // token 过期则跳过,由 TokenRefreshService 负责刷新 @@ -151,7 +151,10 @@ func (r *AntigravityQuotaRefresher) refreshAccountQuota(ctx context.Context, acc r.updateAccountTier(account, loadResp) } - // 调用 API 获取配额 + // 调用 API 获取配额(需要 projectID) + if projectID == "" { + return r.accountRepo.Update(ctx, account) // 没有 projectID,只更新 tier + } modelsResp, err := client.FetchAvailableModels(ctx, accessToken, projectID) if err != nil { return err From fa48cf27eb166e176427c83cec55a4997bc3454c Mon Sep 17 00:00:00 2001 From: song Date: Tue, 30 Dec 2025 23:54:33 +0800 Subject: [PATCH 06/10] =?UTF-8?q?feat(Antigravity):=20=E4=B8=BA=E6=97=A0?= =?UTF-8?q?=20project=5Fid=20=E7=9A=84=E8=B4=A6=E6=88=B7=E7=94=9F=E6=88=90?= =?UTF-8?q?=E9=9A=8F=E6=9C=BA=20project=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 部分账户类型(如 g1-pro-tier)API 不返回 cloudaicompanionProject, 但实际接受任意格式的 project_id,故添加随机生成逻辑作为兜底。 --- backend/internal/pkg/antigravity/oauth.go | 21 +++++++++++++++++++ .../service/antigravity_oauth_service.go | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/backend/internal/pkg/antigravity/oauth.go b/backend/internal/pkg/antigravity/oauth.go index 54ac8bb1..bdc018f2 100644 --- a/backend/internal/pkg/antigravity/oauth.go +++ b/backend/internal/pkg/antigravity/oauth.go @@ -177,3 +177,24 @@ func BuildAuthorizationURL(state, codeChallenge string) string { return fmt.Sprintf("%s?%s", AuthorizeURL, params.Encode()) } + +// GenerateMockProjectID 生成随机 project_id(当 API 不返回时使用) +// 格式:{形容词}-{名词}-{5位随机字符} +func GenerateMockProjectID() string { + adjectives := []string{"useful", "bright", "swift", "calm", "bold"} + nouns := []string{"fuze", "wave", "spark", "flow", "core"} + + randBytes, _ := GenerateRandomBytes(7) + + adj := adjectives[int(randBytes[0])%len(adjectives)] + noun := nouns[int(randBytes[1])%len(nouns)] + + // 生成 5 位随机字符(a-z0-9) + const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + suffix := make([]byte, 5) + for i := 0; i < 5; i++ { + suffix[i] = charset[int(randBytes[i+2])%len(charset)] + } + + return fmt.Sprintf("%s-%s-%s", adj, noun, string(suffix)) +} diff --git a/backend/internal/service/antigravity_oauth_service.go b/backend/internal/service/antigravity_oauth_service.go index 0d104043..8192a9fb 100644 --- a/backend/internal/service/antigravity_oauth_service.go +++ b/backend/internal/service/antigravity_oauth_service.go @@ -149,6 +149,12 @@ func (s *AntigravityOAuthService) ExchangeCode(ctx context.Context, input *Antig result.ProjectID = loadResp.CloudAICompanionProject } + // 兜底:随机生成 project_id + if result.ProjectID == "" { + result.ProjectID = antigravity.GenerateMockProjectID() + fmt.Printf("[AntigravityOAuth] 使用随机生成的 project_id: %s\n", result.ProjectID) + } + return result, nil } From 0a4e0edc85c03e6a5c0479b37e958eb27944c39e Mon Sep 17 00:00:00 2001 From: song Date: Tue, 30 Dec 2025 23:58:03 +0800 Subject: [PATCH 07/10] =?UTF-8?q?fix(Antigravity):=20=E9=85=8D=E9=A2=9D?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E6=97=B6=E8=87=AA=E5=8A=A8=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E7=BC=BA=E5=A4=B1=E7=9A=84=20project=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧账户可能没有 project_id,在刷新配额时自动生成并保存。 --- .../service/antigravity_quota_refresher.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/internal/service/antigravity_quota_refresher.go b/backend/internal/service/antigravity_quota_refresher.go index de067b07..8f63fa38 100644 --- a/backend/internal/service/antigravity_quota_refresher.go +++ b/backend/internal/service/antigravity_quota_refresher.go @@ -145,16 +145,25 @@ func (r *AntigravityQuotaRefresher) refreshAccountQuota(ctx context.Context, acc client := antigravity.NewClient(proxyURL) - // 获取账户类型(tier) + // 获取账户类型(tier)和 project_id loadResp, _ := client.LoadCodeAssist(ctx, accessToken) if loadResp != nil { r.updateAccountTier(account, loadResp) + // 尝试从 API 获取 project_id + if projectID == "" && loadResp.CloudAICompanionProject != "" { + projectID = loadResp.CloudAICompanionProject + account.Credentials["project_id"] = projectID + } } - // 调用 API 获取配额(需要 projectID) + // 如果仍然没有 project_id,随机生成一个并保存 if projectID == "" { - return r.accountRepo.Update(ctx, account) // 没有 projectID,只更新 tier + projectID = antigravity.GenerateMockProjectID() + account.Credentials["project_id"] = projectID + log.Printf("[AntigravityQuotaRefresher] 为账户 %d 生成随机 project_id: %s", account.ID, projectID) } + + // 调用 API 获取配额 modelsResp, err := client.FetchAvailableModels(ctx, accessToken, projectID) if err != nil { return err From f284ea72fc1c7d114f5603f6d41ddda8a609773b Mon Sep 17 00:00:00 2001 From: song Date: Wed, 31 Dec 2025 00:15:25 +0800 Subject: [PATCH 08/10] =?UTF-8?q?refactor(Antigravity):=20=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E5=AE=8C=E6=95=B4=20API=20=E5=93=8D=E5=BA=94=E5=88=B0?= =?UTF-8?q?=20extra=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LoadCodeAssist/FetchAvailableModels 返回原始 JSON - extra 新增 load_code_assist 和 available_models 保存原始响应 - 前端 tier 从 load_code_assist.paidTier.id 提取 - 删除冗余的 updateAccountTier 函数 --- backend/internal/pkg/antigravity/client.go | 44 +++++++++------ .../service/antigravity_oauth_service.go | 2 +- .../service/antigravity_quota_refresher.go | 55 +++++++------------ .../components/account/AccountUsageCell.vue | 21 ++++++- 4 files changed, 65 insertions(+), 57 deletions(-) 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 }) // 账户类型显示标签 From 4a0008df47a1955b3552fa3809dd536423b6f1d1 Mon Sep 17 00:00:00 2001 From: song Date: Wed, 31 Dec 2025 00:34:24 +0800 Subject: [PATCH 09/10] =?UTF-8?q?feat(Antigravity):=20=E4=B8=BA=E4=B8=8D?= =?UTF-8?q?=E5=90=88=E6=A0=BC=E8=B4=A6=E6=88=B7=E6=98=BE=E7=A4=BA=E8=AD=A6?= =?UTF-8?q?=E5=91=8A=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/account/AccountUsageCell.vue | 36 ++++++++++++++++++- frontend/src/i18n/locales/en.ts | 4 ++- frontend/src/i18n/locales/zh.ts | 2 ++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index cbd93df8..d064c55a 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -96,7 +96,7 @@