diff --git a/provider_pools.json.example b/provider_pools.json.example index 0a416c4..2a5e4dc 100644 --- a/provider_pools.json.example +++ b/provider_pools.json.example @@ -1,6 +1,7 @@ { "openai-custom": [ { + "customName": "OpenAI节点1", "OPENAI_API_KEY": "sk-openai-key1", "OPENAI_BASE_URL": "https://api.openai.com/v1", "checkModelName": null, @@ -15,6 +16,7 @@ "lastErrorTime": null }, { + "customName": "OpenAI节点2", "OPENAI_API_KEY": "sk-openai-key2", "OPENAI_BASE_URL": "https://api.openai.com/v1", "checkModelName": null, @@ -31,6 +33,7 @@ ], "openaiResponses-custom": [ { + "customName": "OpenAI Responses节点", "OPENAI_API_KEY": "sk-openai-key", "OPENAI_BASE_URL": "https://api.openai.com/v1", "checkModelName": null, @@ -46,6 +49,7 @@ ], "gemini-cli-oauth": [ { + "customName": "Gemini OAuth节点1", "GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials1.json", "PROJECT_ID": "your-project-id-1", "checkModelName": null, @@ -59,6 +63,7 @@ "lastErrorTime": null }, { + "customName": "Gemini OAuth节点2", "GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials2.json", "PROJECT_ID": "your-project-id-2", "checkModelName": null, @@ -74,6 +79,7 @@ ], "claude-custom": [ { + "customName": "Claude节点1", "CLAUDE_API_KEY": "sk-claude-key1", "CLAUDE_BASE_URL": "https://api.anthropic.com", "checkModelName": null, @@ -87,6 +93,7 @@ "lastErrorTime": null }, { + "customName": "Claude节点2", "CLAUDE_API_KEY": "sk-claude-key2", "CLAUDE_BASE_URL": "https://api.anthropic.com", "checkModelName": null, @@ -102,6 +109,7 @@ ], "claude-kiro-oauth": [ { + "customName": "Kiro OAuth节点1", "KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds1.json", "uuid": "2c69d0ac-b86f-43d8-9d17-0d300afc5cfd", "checkModelName": null, @@ -114,6 +122,7 @@ "lastErrorTime": null }, { + "customName": "Kiro OAuth节点2", "KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds2.json", "uuid": "7482abe6-8083-4288-bb7d-d8ecb7c461e2", "checkModelName": null, @@ -128,6 +137,7 @@ ], "openai-qwen-oauth": [ { + "customName": "Qwen OAuth节点", "QWEN_OAUTH_CREDS_FILE_PATH": "./qwen_creds.json", "uuid": "658a2114-c4c9-d713-b8d4-ceabf0e0bf18", "checkModelName": null, @@ -142,6 +152,7 @@ ], "gemini-antigravity": [ { + "customName": "Antigravity节点1", "ANTIGRAVITY_OAUTH_CREDS_FILE_PATH": "./antigravity_creds1.json", "PROJECT_ID": "antigravity-project-1", "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", @@ -155,6 +166,7 @@ "lastErrorTime": null }, { + "customName": "Antigravity节点2", "ANTIGRAVITY_OAUTH_CREDS_FILE_PATH": "./antigravity_creds2.json", "PROJECT_ID": "antigravity-project-2", "uuid": "f0e9d8c7-b6a5-4321-fedc-ba9876543210", diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index 070ee26..1813685 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -17,7 +17,7 @@ const KIRO_CONSTANTS = { AMAZON_Q_URL: 'https://codewhisperer.{{region}}.amazonaws.com/SendMessageStreaming', USAGE_LIMITS_URL: 'https://q.{{region}}.amazonaws.com/getUsageLimits', DEFAULT_MODEL_NAME: 'claude-opus-4-5', - AXIOS_TIMEOUT: 120000, // 2 minutes timeout + AXIOS_TIMEOUT: 300000, // 5 minutes timeout (increased from 2 minutes) USER_AGENT: 'KiroIDE', KIRO_VERSION: '0.7.5', CONTENT_TYPE_JSON: 'application/json', @@ -312,13 +312,13 @@ export class KiroApiService { keepAlive: true, maxSockets: 100, // 每个主机最多 10 个连接 maxFreeSockets: 5, // 最多保留 5 个空闲连接 - timeout: 120000, // 空闲连接 60 秒后关闭 + timeout: KIRO_CONSTANTS.AXIOS_TIMEOUT, }); const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 100, maxFreeSockets: 5, - timeout: 120000, + timeout: KIRO_CONSTANTS.AXIOS_TIMEOUT, }); const axiosConfig = { diff --git a/src/common.js b/src/common.js index adcfe6a..590f8ee 100644 --- a/src/common.js +++ b/src/common.js @@ -2,8 +2,7 @@ import { promises as fs } from 'fs'; import * as path from 'path'; import * as http from 'http'; // Add http for IncomingMessage and ServerResponse types import * as crypto from 'crypto'; // Import crypto for MD5 hashing -import { ApiServiceAdapter } from './adapter.js'; // Import ApiServiceAdapter -import { convertData, getOpenAIStreamChunkStop, getOpenAIResponsesStreamChunkBegin, getOpenAIResponsesStreamChunkEnd } from './convert.js'; +import { convertData, getOpenAIStreamChunkStop } from './convert.js'; import { ProviderStrategyFactory } from './provider-strategies.js'; export const API_ACTIONS = { @@ -317,7 +316,6 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP * and sends the JSON response. * @param {http.IncomingMessage} req The HTTP request object. * @param {http.ServerResponse} res The HTTP response object. - * @param {ApiServiceAdapter} service The API service adapter. * @param {string} endpointType The type of endpoint being called (e.g., OPENAI_MODEL_LIST). * @param {Object} CONFIG - The server configuration object. */ @@ -368,7 +366,6 @@ export async function handleModelListRequest(req, res, service, endpointType, CO * logging, and dispatching to the appropriate stream or unary handler. * @param {http.IncomingMessage} req The HTTP request object. * @param {http.ServerResponse} res The HTTP response object. - * @param {ApiServiceAdapter} service The API service adapter. * @param {string} endpointType The type of endpoint being called (e.g., OPENAI_CHAT). * @param {Object} CONFIG - The server configuration object. * @param {string} PROMPT_LOG_FILENAME - The prompt log filename. diff --git a/src/gemini/antigravity-core.js b/src/gemini/antigravity-core.js index 1ed2804..0c1782e 100644 --- a/src/gemini/antigravity-core.js +++ b/src/gemini/antigravity-core.js @@ -9,6 +9,7 @@ import { v4 as uuidv4 } from 'uuid'; import open from 'open'; import { formatExpiryTime } from '../common.js'; import { getProviderModels } from '../provider-models.js'; +import { handleGeminiAntigravityOAuth } from '../oauth-handlers.js'; // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 const httpAgent = new http.Agent({ @@ -305,85 +306,51 @@ export class AntigravityApiService { } async getNewToken(credPath) { - let host = this.host; - if (!host || host === 'undefined') { - host = '127.0.0.1'; - } - const redirectUri = `http://${host}:${AUTH_REDIRECT_PORT}`; - this.authClient.redirectUri = redirectUri; + // 使用统一的 OAuth 处理方法 + const { authUrl, authInfo } = await handleGeminiAntigravityOAuth(this.config); + + console.log('\n[Antigravity Auth] 正在自动打开浏览器进行授权...'); + console.log('[Antigravity Auth] 授权链接:', authUrl, '\n'); - return new Promise(async (resolve, reject) => { - const authUrl = this.authClient.generateAuthUrl({ - access_type: 'offline', - scope: ['https://www.googleapis.com/auth/cloud-platform'] - }); + // 自动打开浏览器 + const showFallbackMessage = () => { + console.log('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); + }; - console.log('\n[Antigravity Auth] 正在自动打开浏览器进行授权...'); - console.log('[Antigravity Auth] 授权链接:', authUrl, '\n'); - - // 自动打开浏览器 - const showFallbackMessage = () => { - console.log('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); - }; - - if (this.config) { - try { - const childProcess = await open(authUrl); - if (childProcess) { - childProcess.on('error', () => showFallbackMessage()); - } - } catch (_err) { - showFallbackMessage(); + if (this.config) { + try { + const childProcess = await open(authUrl); + if (childProcess) { + childProcess.on('error', () => showFallbackMessage()); } - } else { + } catch (_err) { showFallbackMessage(); } + } else { + showFallbackMessage(); + } - const server = http.createServer(async (req, res) => { + // 等待 OAuth 回调完成并读取保存的凭据 + return new Promise((resolve, reject) => { + const checkInterval = setInterval(async () => { try { - const url = new URL(req.url, redirectUri); - const code = url.searchParams.get('code'); - const errorParam = url.searchParams.get('error'); - - if (code) { - console.log(`[Antigravity Auth] Received successful callback from Google: ${req.url}`); - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Authentication successful! You can close this browser tab.'); - server.close(); - - const { tokens } = await this.authClient.getToken(code); - await fs.mkdir(path.dirname(credPath), { recursive: true }); - await fs.writeFile(credPath, JSON.stringify(tokens, null, 2)); - console.log('[Antigravity Auth] New token received and saved to file.'); - resolve(tokens); - } else if (errorParam) { - const errorMessage = `Authentication failed. Google returned an error: ${errorParam}.`; - res.writeHead(400, { 'Content-Type': 'text/plain' }); - res.end(errorMessage); - server.close(); - reject(new Error(errorMessage)); - } else { - console.log(`[Antigravity Auth] Ignoring irrelevant request: ${req.url}`); - res.writeHead(204); - res.end(); + const data = await fs.readFile(credPath, 'utf8'); + const credentials = JSON.parse(data); + if (credentials.access_token) { + clearInterval(checkInterval); + console.log('[Antigravity Auth] New token obtained successfully.'); + resolve(credentials); } - } catch (e) { - if (server.listening) server.close(); - reject(e); + } catch (error) { + // 文件尚未创建或无效,继续等待 } - }); + }, 1000); - server.on('error', (err) => { - if (err.code === 'EADDRINUSE') { - const errorMessage = `[Antigravity Auth] Port ${AUTH_REDIRECT_PORT} on ${host} is already in use.`; - console.error(errorMessage); - reject(new Error(errorMessage)); - } else { - reject(err); - } - }); - - server.listen(AUTH_REDIRECT_PORT, host); + // 设置超时(5分钟) + setTimeout(() => { + clearInterval(checkInterval); + reject(new Error('[Antigravity Auth] OAuth 授权超时')); + }, 5 * 60 * 1000); }); } diff --git a/src/gemini/gemini-core.js b/src/gemini/gemini-core.js index 2b15ee9..1778e18 100644 --- a/src/gemini/gemini-core.js +++ b/src/gemini/gemini-core.js @@ -8,6 +8,7 @@ import * as readline from 'readline'; import open from 'open'; import { API_ACTIONS, formatExpiryTime } from '../common.js'; import { getProviderModels } from '../provider-models.js'; +import { handleGeminiCliOAuth } from '../oauth-handlers.js'; // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 const httpAgent = new http.Agent({ @@ -274,75 +275,51 @@ export class GeminiApiService { } async getNewToken(credPath) { - let host = this.host; - if (!host || host === 'undefined') { - host = '127.0.0.1'; - } - const redirectUri = `http://${host}:${AUTH_REDIRECT_PORT}`; - this.authClient.redirectUri = redirectUri; - return new Promise(async (resolve, reject) => { - const authUrl = this.authClient.generateAuthUrl({ access_type: 'offline', scope: ['https://www.googleapis.com/auth/cloud-platform'] }); - console.log('\n[Gemini Auth] 正在自动打开浏览器进行授权...'); - - // 自动打开浏览器 - const showFallbackMessage = () => { - console.log('[Gemini Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); - }; - - if (this.config) { - try { - const childProcess = await open(authUrl); - if (childProcess) { - childProcess.on('error', () => showFallbackMessage()); - } - } catch (_err) { - showFallbackMessage(); + // 使用统一的 OAuth 处理方法 + const { authUrl, authInfo } = await handleGeminiCliOAuth(this.config); + + console.log('\n[Gemini Auth] 正在自动打开浏览器进行授权...'); + console.log('[Gemini Auth] 授权链接:', authUrl, '\n'); + + // 自动打开浏览器 + const showFallbackMessage = () => { + console.log('[Gemini Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); + }; + + if (this.config) { + try { + const childProcess = await open(authUrl); + if (childProcess) { + childProcess.on('error', () => showFallbackMessage()); } - } else { + } catch (_err) { showFallbackMessage(); } - console.log('[Gemini Auth] 授权链接:', authUrl, '\n'); - const server = http.createServer(async (req, res) => { + } else { + showFallbackMessage(); + } + + // 等待 OAuth 回调完成并读取保存的凭据 + return new Promise((resolve, reject) => { + const checkInterval = setInterval(async () => { try { - const url = new URL(req.url, redirectUri); - const code = url.searchParams.get('code'); - const errorParam = url.searchParams.get('error'); - if (code) { - console.log(`[Gemini Auth] Received successful callback from Google: ${req.url}`); - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Authentication successful! You can close this browser tab.'); - server.close(); - const { tokens } = await this.authClient.getToken(code); - await fs.mkdir(path.dirname(credPath), { recursive: true }); - await fs.writeFile(credPath, JSON.stringify(tokens, null, 2)); - console.log('[Gemini Auth] New token received and saved to file.'); - resolve(tokens); - } else if (errorParam) { - const errorMessage = `Authentication failed. Google returned an error: ${errorParam}.`; - res.writeHead(400, { 'Content-Type': 'text/plain' }); - res.end(errorMessage); - server.close(); - reject(new Error(errorMessage)); - } else { - console.log(`[Gemini Auth] Ignoring irrelevant request: ${req.url}`); - res.writeHead(204); - res.end(); + const data = await fs.readFile(credPath, 'utf8'); + const credentials = JSON.parse(data); + if (credentials.access_token) { + clearInterval(checkInterval); + console.log('[Gemini Auth] New token obtained successfully.'); + resolve(credentials); } - } catch (e) { - if (server.listening) server.close(); - reject(e); + } catch (error) { + // 文件尚未创建或无效,继续等待 } - }); - server.on('error', (err) => { - if (err.code === 'EADDRINUSE') { - const errorMessage = `[Gemini Auth] Port ${AUTH_REDIRECT_PORT} on ${this.host} is already in use.`; - console.error(errorMessage); - reject(new Error(errorMessage)); - } else { - reject(err); - } - }); - server.listen(AUTH_REDIRECT_PORT, this.host); + }, 1000); + + // 设置超时(5分钟) + setTimeout(() => { + clearInterval(checkInterval); + reject(new Error('[Gemini Auth] OAuth 授权超时')); + }, 5 * 60 * 1000); }); } diff --git a/src/openai/qwen-core.js b/src/openai/qwen-core.js index 61dc65b..7f344c6 100644 --- a/src/openai/qwen-core.js +++ b/src/openai/qwen-core.js @@ -9,6 +9,7 @@ import open from 'open'; import { EventEmitter } from 'events'; import { randomUUID } from 'node:crypto'; import { getProviderModels } from '../provider-models.js'; +import { handleQwenOAuth } from '../oauth-handlers.js'; // --- Constants --- const QWEN_DIR = '.qwen'; @@ -284,39 +285,30 @@ export class QwenApiService { } async _authWithQwenDeviceFlow(client, config) { - let isCancelled = false; - const cancelHandler = () => { isCancelled = true; }; - const sigintHandler = () => { - isCancelled = true; - qwenOAuth2Events.emit(QwenOAuth2Event.AuthCancel); - }; - qwenOAuth2Events.once(QwenOAuth2Event.AuthCancel, cancelHandler); - process.once('SIGINT', sigintHandler); - try { - const { code_verifier, code_challenge } = generatePKCEPair(); - const deviceAuth = await client.requestDeviceAuthorization({ - scope: QWEN_OAUTH_SCOPE, - code_challenge, - code_challenge_method: 'S256', + // 使用统一的 OAuth 处理方法 + const { authUrl, authInfo } = await handleQwenOAuth(config); + + // 发送授权 URI 事件 + qwenOAuth2Events.emit(QwenOAuth2Event.AuthUri, { + verification_uri_complete: authUrl, + user_code: authInfo.userCode, + verification_uri: authInfo.verificationUri, + device_code: authInfo.deviceCode, + expires_in: authInfo.expiresIn, + interval: authInfo.interval }); - if (!isDeviceAuthorizationSuccess(deviceAuth)) { - throw new Error(`Device authorization failed: ${deviceAuth?.error || 'Unknown error'} - ${deviceAuth?.error_description || 'No details'}`); - } - - qwenOAuth2Events.emit(QwenOAuth2Event.AuthUri, deviceAuth); - const showFallbackMessage = () => { console.log('\n=== Qwen OAuth Device Authorization ==='); console.log('Please visit the following URL in your browser to authorize:'); - console.log(`\n${deviceAuth.verification_uri_complete}\n`); + console.log(`\n${authUrl}\n`); console.log('Waiting for authorization to complete...\n'); }; if (config) { try { - const childProcess = await open(deviceAuth.verification_uri_complete); + const childProcess = await open(authUrl); if (childProcess) { childProcess.on('error', () => showFallbackMessage()); } @@ -330,70 +322,37 @@ export class QwenApiService { qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'polling', 'Waiting for authorization...'); console.debug('Waiting for authorization...\n'); - let pollInterval = 2000; - const maxAttempts = Math.ceil(deviceAuth.expires_in / (pollInterval / 1000)); - - for (let attempt = 0; attempt < maxAttempts; attempt++) { - if (isCancelled) { - qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', 'Authentication cancelled by user.'); - return { success: false, reason: 'cancelled' }; - } - - try { - const tokenResponse = await client.pollDeviceToken({ device_code: deviceAuth.device_code, code_verifier }); - if (isDeviceTokenSuccess(tokenResponse)) { - const credentials = { - access_token: tokenResponse.access_token, - refresh_token: tokenResponse.refresh_token || undefined, - token_type: tokenResponse.token_type, - resource_url: tokenResponse.resource_url, - expiry_date: tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1000 : undefined, - }; - client.setCredentials(credentials); - await this._cacheQwenCredentials(credentials); - qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'success', 'Authentication successful! Access token obtained.'); - return { success: true }; - } - - if (isDeviceTokenPending(tokenResponse)) { - if (tokenResponse.slowDown) { - pollInterval = Math.min(pollInterval * 1.5, 10000); - } else { - pollInterval = 2000; + // 等待 OAuth 回调完成并读取保存的凭据 + const credPath = this._getQwenCachedCredentialPath(); + const credentials = await new Promise((resolve, reject) => { + const checkInterval = setInterval(async () => { + try { + const data = await fs.readFile(credPath, 'utf8'); + const creds = JSON.parse(data); + if (creds.access_token) { + clearInterval(checkInterval); + console.log('[Qwen Auth] New token obtained successfully.'); + resolve(creds); } - // Fall through to wait and continue - } else if (isErrorResponse(tokenResponse)) { - console.warn(`Token polling failed with error: ${tokenResponse?.error || 'Unknown error'}`); - // Fall through to wait and continue + } catch (error) { + // 文件尚未创建或无效,继续等待 } - } catch (error) { - console.warn(`Token polling threw an exception: ${error.message}`); - // Fall through to wait for the next attempt - } - - // Wait for the polling interval before the next attempt - await new Promise(resolve => { - const timeoutId = setTimeout(resolve, pollInterval); - // If cancelled during wait, clear timeout and resolve immediately - if (isCancelled) { - clearTimeout(timeoutId); - resolve(); - } - }); - - // Check again after waiting - if (isCancelled) { - qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', 'Authentication cancelled by user.'); - return { success: false, reason: 'cancelled' }; - } - } - return { success: false, reason: 'timeout' }; + }, 1000); + + // 设置超时(5分钟) + setTimeout(() => { + clearInterval(checkInterval); + reject(new Error('[Qwen Auth] OAuth 授权超时')); + }, 5 * 60 * 1000); + }); + + client.setCredentials(credentials); + qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'success', 'Authentication successful! Access token obtained.'); + return { success: true }; } catch (error) { console.error('Device authorization flow failed:', error.message); + qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', error.message); return { success: false, reason: 'error' }; - } finally { - qwenOAuth2Events.off(QwenOAuth2Event.AuthCancel, cancelHandler); - process.off('SIGINT', sigintHandler); } } diff --git a/static/app/modal.js b/static/app/modal.js index 8a054f0..f5ef42e 100644 --- a/static/app/modal.js +++ b/static/app/modal.js @@ -381,7 +381,7 @@ function renderProviderList(providers) {