From 785ee93505d57196d6df3635f272ec294ec4b364 Mon Sep 17 00:00:00 2001 From: Cita Date: Sat, 28 Mar 2026 20:06:20 +0800 Subject: [PATCH] fix gemini oauth validation site proxy (#298) --- .../services/modelService.discovery.test.ts | 61 +++++++++++++++++++ src/server/services/modelService.ts | 4 +- .../services/platformDiscoveryRegistry.ts | 8 +-- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/server/services/modelService.discovery.test.ts b/src/server/services/modelService.discovery.test.ts index 8bda03b..c3ea533 100644 --- a/src/server/services/modelService.discovery.test.ts +++ b/src/server/services/modelService.discovery.test.ts @@ -75,7 +75,12 @@ describe('refreshModelsForAccount credential discovery', () => { await db.delete(schema.modelAvailability).run(); await db.delete(schema.accountTokens).run(); await db.delete(schema.accounts).run(); + await db.delete(schema.settings).run(); await db.delete(schema.sites).run(); + const { config } = await import('../config.js'); + config.systemProxyUrl = ''; + const { invalidateSiteProxyCache } = await import('./siteProxy.js'); + invalidateSiteProxyCache(); }); afterAll(() => { @@ -874,6 +879,62 @@ describe('refreshModelsForAccount credential discovery', () => { }); }); + it('inherits site system proxy for gemini oauth validation requests', async () => { + getApiTokenMock.mockResolvedValue(null); + getModelsMock.mockRejectedValue(new Error('gemini oauth validation should not call adapter.getModels')); + undiciFetchMock.mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + state: 'ENABLED', + }), + text: async () => JSON.stringify({ state: 'ENABLED' }), + }); + + const { config } = await import('../config.js'); + config.systemProxyUrl = 'http://127.0.0.1:1081'; + + const site = await db.insert(schema.sites).values({ + name: 'gemini-site-proxy-site', + url: 'https://cloudcode-pa.googleapis.com', + platform: 'gemini-cli', + status: 'active', + useSystemProxy: true, + }).returning().get(); + + const account = await db.insert(schema.accounts).values({ + siteId: site.id, + username: 'gemini-site-proxy-user@example.com', + accessToken: 'gemini-access-token', + apiToken: null, + status: 'active', + extraConfig: JSON.stringify({ + credentialMode: 'session', + oauth: { + provider: 'gemini-cli', + projectId: 'project-site-proxy-demo', + email: 'gemini-site-proxy-user@example.com', + }, + }), + }).returning().get(); + + const result = await refreshModelsForAccount(account.id); + + expect(result).toMatchObject({ + accountId: account.id, + refreshed: true, + status: 'success', + modelCount: expect.any(Number), + }); + expect(undiciFetchMock).toHaveBeenCalledTimes(1); + expect(proxyAgentCtorMock).toHaveBeenCalledWith('http://127.0.0.1:1081'); + expect(String(undiciFetchMock.mock.calls[0]?.[0] || '')).toContain('/projects/project-site-proxy-demo/services/'); + expect(undiciFetchMock.mock.calls[0]?.[1]).toMatchObject({ + method: 'GET', + dispatcher: expect.any(MockProxyAgent), + }); + }); + it('refreshes gemini oauth access token during validation through singleflight and reuses the refreshed account state', async () => { getApiTokenMock.mockResolvedValue(null); getModelsMock.mockRejectedValue(new Error('gemini oauth validation should not call adapter.getModels')); diff --git a/src/server/services/modelService.ts b/src/server/services/modelService.ts index 4669e1d..b1586ae 100644 --- a/src/server/services/modelService.ts +++ b/src/server/services/modelService.ts @@ -529,7 +529,7 @@ export async function refreshModelsForAccount( try { await withTimeout( () => withAccountProxyOverride(accountProxyUrl, - () => validateGeminiCliOauthConnection({ account: discoveryAccount })), + () => validateGeminiCliOauthConnection({ site, account: discoveryAccount })), MODEL_DISCOVERY_TIMEOUT_MS, `gemini cli oauth validation timeout (${Math.round(MODEL_DISCOVERY_TIMEOUT_MS / 1000)}s)`, ); @@ -548,7 +548,7 @@ export async function refreshModelsForAccount( }; await withTimeout( () => withAccountProxyOverride(accountProxyUrl, - () => validateGeminiCliOauthConnection({ account: discoveryAccount })), + () => validateGeminiCliOauthConnection({ site, account: discoveryAccount })), MODEL_DISCOVERY_TIMEOUT_MS, `gemini cli oauth validation timeout (${Math.round(MODEL_DISCOVERY_TIMEOUT_MS / 1000)}s)`, ); diff --git a/src/server/services/platformDiscoveryRegistry.ts b/src/server/services/platformDiscoveryRegistry.ts index 3b1b1c0..7ac158b 100644 --- a/src/server/services/platformDiscoveryRegistry.ts +++ b/src/server/services/platformDiscoveryRegistry.ts @@ -1,7 +1,6 @@ import { fetch } from 'undici'; import { schema } from '../db/index.js'; -import { getProxyUrlFromExtraConfig } from './accountExtraConfig.js'; -import { withExplicitProxyRequestInit, withSiteRecordProxyRequestInit } from './siteProxy.js'; +import { withSiteRecordProxyRequestInit } from './siteProxy.js'; import { getOauthInfoFromAccount } from './oauth/oauthAccount.js'; import { CLAUDE_DEFAULT_ANTHROPIC_VERSION } from './oauth/claudeProvider.js'; import { @@ -175,6 +174,7 @@ export async function discoverClaudeModelsFromCloud(input: { } export async function validateGeminiCliOauthConnection(input: { + site: PlatformDiscoverySite; account: PlatformDiscoveryAccount; }): Promise { const accessToken = (input.account.accessToken || '').trim(); @@ -188,7 +188,7 @@ export async function validateGeminiCliOauthConnection(input: { } const response = await fetch( `https://serviceusage.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(GEMINI_CLI_REQUIRED_SERVICE)}`, - withExplicitProxyRequestInit(getProxyUrlFromExtraConfig(input.account.extraConfig), { + withSiteRecordProxyRequestInit(input.site, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, @@ -196,7 +196,7 @@ export async function validateGeminiCliOauthConnection(input: { 'User-Agent': GEMINI_CLI_USER_AGENT, 'X-Goog-Api-Client': GEMINI_CLI_GOOGLE_API_CLIENT, }, - }, true), + }), ); if (!response.ok) { const text = await response.text().catch(() => '');