diff --git a/configs/config.json.example b/configs/config.json.example index 406c2bf..76f9e66 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -43,7 +43,15 @@ }, "PROXY_URL": "http://127.0.0.1:1089", "PROXY_ENABLED_PROVIDERS": [ - "gemini-cli-oauth", - "gemini-antigravity" - ] - } + "gemini-cli-oauth", + "gemini-antigravity" + ], + "LOG_ENABLED": true, + "LOG_OUTPUT_MODE": "all", + "LOG_LEVEL": "info", + "LOG_DIR": "logs", + "LOG_INCLUDE_REQUEST_ID": true, + "LOG_INCLUDE_TIMESTAMP": true, + "LOG_MAX_FILE_SIZE": 10485760, + "LOG_MAX_FILES": 10 +} diff --git a/install-and-run.bat b/install-and-run.bat index f44cada..a96e228 100755 --- a/install-and-run.bat +++ b/install-and-run.bat @@ -80,7 +80,7 @@ echo [成功] 项目文件检查完成 :: 启动应用程序 echo. echo ======================================== -echo 启动AI Client 2 API服务器... +echo 启动AIClient2API服务器... echo ======================================== echo. echo 服务器将在 http://localhost:3000 启动 diff --git a/install-and-run.sh b/install-and-run.sh index 5806076..865430e 100755 --- a/install-and-run.sh +++ b/install-and-run.sh @@ -86,7 +86,7 @@ echo "[成功] 项目文件检查完成" # 启动应用程序 echo echo "========================================" -echo " 启动AI Client 2 API服务器..." +echo " 启动AIClient2API服务器..." echo "========================================" echo echo "服务器将在 http://localhost:3000 启动" diff --git a/src/auth/codex-oauth.js b/src/auth/codex-oauth.js index f4fd741..e1f979e 100644 --- a/src/auth/codex-oauth.js +++ b/src/auth/codex-oauth.js @@ -1,4 +1,5 @@ import http from 'http'; +import logger from '../utils/logger.js'; import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; @@ -36,7 +37,7 @@ class CodexAuth { if (proxyConfig) { axiosConfig.httpAgent = proxyConfig.httpAgent; axiosConfig.httpsAgent = proxyConfig.httpsAgent; - console.log('[Codex Auth] Proxy enabled for OAuth requests'); + logger.info('[Codex Auth] Proxy enabled for OAuth requests'); } this.httpClient = axios.create(axiosConfig); @@ -68,16 +69,16 @@ class CodexAuth { const pkce = this.generatePKCECodes(); const state = crypto.randomBytes(16).toString('hex'); - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Generating auth URL...`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Generating auth URL...`); // 如果已有服务器在运行,先关闭 if (this.server) { - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Closing existing callback server...`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Closing existing callback server...`); try { this.server.close(); this.server = null; } catch (error) { - console.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to close existing server:`, error.message); + logger.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to close existing server:`, error.message); } } @@ -143,9 +144,9 @@ class CodexAuth { const credPath = saveResult.credsPath; const relativePath = saveResult.relativePath; - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`); - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`); - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`); // 关闭服务器 if (this.server) { @@ -168,7 +169,7 @@ class CodexAuth { const pkce = this.generatePKCECodes(); const state = crypto.randomBytes(16).toString('hex'); - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Starting OAuth flow...`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Starting OAuth flow...`); // 启动本地回调服务器 const server = await this.startCallbackServer(); @@ -186,13 +187,13 @@ class CodexAuth { authUrl.searchParams.set('id_token_add_organizations', 'true'); authUrl.searchParams.set('codex_cli_simplified_flow', 'true'); - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Opening browser for authentication...`); - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} If browser doesn't open, visit: ${authUrl.toString()}`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Opening browser for authentication...`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} If browser doesn't open, visit: ${authUrl.toString()}`); try { await open(authUrl.toString()); } catch (error) { - console.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to open browser automatically:`, error.message); + logger.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to open browser automatically:`, error.message); } // 等待回调 @@ -218,9 +219,9 @@ class CodexAuth { await this.saveCredentials(credentials); - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`); - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`); - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`); return credentials; } @@ -302,7 +303,7 @@ class CodexAuth { }); server.listen(CODEX_OAUTH_CONFIG.port, () => { - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Callback server listening on port ${CODEX_OAUTH_CONFIG.port}`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Callback server listening on port ${CODEX_OAUTH_CONFIG.port}`); resolve(server); }); @@ -355,7 +356,7 @@ class CodexAuth { * @returns {Promise} */ async exchangeCodeForTokens(code, codeVerifier) { - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Exchanging authorization code for tokens...`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Exchanging authorization code for tokens...`); try { const response = await this.httpClient.post( @@ -377,7 +378,7 @@ class CodexAuth { return response.data; } catch (error) { - console.error(`${CODEX_OAUTH_CONFIG.logPrefix} Token exchange failed:`, error.response?.data || error.message); + logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Token exchange failed:`, error.response?.data || error.message); throw new Error(`Failed to exchange code for tokens: ${error.response?.data?.error_description || error.message}`); } } @@ -388,7 +389,7 @@ class CodexAuth { * @returns {Promise} */ async refreshTokens(refreshToken) { - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Refreshing access token...`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Refreshing access token...`); try { const response = await this.httpClient.post( @@ -420,7 +421,7 @@ class CodexAuth { expired: new Date(Date.now() + (tokens.expires_in || 3600) * 1000).toISOString() }; } catch (error) { - console.error(`${CODEX_OAUTH_CONFIG.logPrefix} Token refresh failed:`, error.response?.data || error.message); + logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Token refresh failed:`, error.response?.data || error.message); throw new Error(`Failed to refresh tokens: ${error.response?.data?.error_description || error.message}`); } } @@ -441,7 +442,7 @@ class CodexAuth { const payload = Buffer.from(parts[1], 'base64url').toString('utf8'); return JSON.parse(payload); } catch (error) { - console.error(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to parse JWT:`, error.message); + logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to parse JWT:`, error.message); throw new Error(`Failed to parse JWT token: ${error.message}`); } } @@ -474,12 +475,12 @@ class CodexAuth { await fs.promises.writeFile(credsPath, JSON.stringify(creds, null, 2), { mode: 0o600 }); const relativePath = path.relative(process.cwd(), credsPath); - console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Credentials saved to ${relativePath}`); + logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Credentials saved to ${relativePath}`); // 返回保存路径供后续使用 return { credsPath, relativePath }; } catch (error) { - console.error(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to save credentials:`, error.message); + logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to save credentials:`, error.message); throw new Error(`Failed to save credentials: ${error.message}`); } } @@ -583,7 +584,7 @@ export async function refreshCodexTokensWithRetry(refreshToken, config = {}, max return await auth.refreshTokens(refreshToken); } catch (error) { lastError = error; - console.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Retry ${i + 1}/${maxRetries} failed:`, error.message); + logger.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Retry ${i + 1}/${maxRetries} failed:`, error.message); if (i < maxRetries - 1) { // 指数退避 @@ -606,11 +607,11 @@ export async function handleCodexOAuth(currentConfig, options = {}) { const auth = new CodexAuth(currentConfig); try { - console.log('[Codex Auth] Generating OAuth URL...'); + logger.info('[Codex Auth] Generating OAuth URL...'); // 清理所有旧的会话和服务器 if (global.codexOAuthSessions && global.codexOAuthSessions.size > 0) { - console.log('[Codex Auth] Cleaning up old OAuth sessions...'); + logger.info('[Codex Auth] Cleaning up old OAuth sessions...'); for (const [sessionId, session] of global.codexOAuthSessions.entries()) { try { // 清理定时器 @@ -623,7 +624,7 @@ export async function handleCodexOAuth(currentConfig, options = {}) { } global.codexOAuthSessions.delete(sessionId); } catch (error) { - console.warn(`[Codex Auth] Failed to clean up session ${sessionId}:`, error.message); + logger.warn(`[Codex Auth] Failed to clean up session ${sessionId}:`, error.message); } } } @@ -631,7 +632,7 @@ export async function handleCodexOAuth(currentConfig, options = {}) { // 生成授权 URL 和启动回调服务器 const { authUrl, state, pkce, server } = await auth.generateAuthUrl(); - console.log('[Codex Auth] OAuth URL generated successfully'); + logger.info('[Codex Auth] OAuth URL generated successfully'); // 存储 OAuth 会话信息,供后续回调使用 if (!global.codexOAuthSessions) { @@ -663,13 +664,13 @@ export async function handleCodexOAuth(currentConfig, options = {}) { pollTimer = setInterval(() => { pollCount++; if (pollCount <= maxPollCount && !isCompleted) { - console.log(`[Codex Auth] Waiting for callback... (${pollCount}/${maxPollCount})`); + logger.info(`[Codex Auth] Waiting for callback... (${pollCount}/${maxPollCount})`); } if (pollCount >= maxPollCount && !isCompleted) { clearInterval(pollTimer); const totalSeconds = (maxPollCount * pollInterval) / 1000; - console.log(`[Codex Auth] Polling timeout (${totalSeconds}s), releasing session for next authorization`); + logger.info(`[Codex Auth] Polling timeout (${totalSeconds}s), releasing session for next authorization`); // 清理会话和服务器 if (global.codexOAuthSessions.has(sessionId)) { @@ -693,11 +694,11 @@ export async function handleCodexOAuth(currentConfig, options = {}) { } try { - console.log('[Codex Auth] Received auth callback, completing OAuth flow...'); + logger.info('[Codex Auth] Received auth callback, completing OAuth flow...'); const session = global.codexOAuthSessions.get(sessionId); if (!session) { - console.error('[Codex Auth] Session not found'); + logger.error('[Codex Auth] Session not found'); return; } @@ -720,9 +721,9 @@ export async function handleCodexOAuth(currentConfig, options = {}) { // 自动关联新生成的凭据到 Pools await autoLinkProviderConfigs(CONFIG); - console.log('[Codex Auth] OAuth flow completed successfully'); + logger.info('[Codex Auth] OAuth flow completed successfully'); } catch (error) { - console.error('[Codex Auth] Failed to complete OAuth flow:', error.message); + logger.error('[Codex Auth] Failed to complete OAuth flow:', error.message); // 广播认证失败事件 broadcastEvent('oauth_error', { @@ -739,7 +740,7 @@ export async function handleCodexOAuth(currentConfig, options = {}) { clearInterval(pollTimer); } - console.error('[Codex Auth] Auth error:', error.message); + logger.error('[Codex Auth] Auth error:', error.message); global.codexOAuthSessions.delete(sessionId); broadcastEvent('oauth_error', { @@ -767,7 +768,7 @@ export async function handleCodexOAuth(currentConfig, options = {}) { } }; } catch (error) { - console.error('[Codex Auth] Failed to generate OAuth URL:', error.message); + logger.error('[Codex Auth] Failed to generate OAuth URL:', error.message); return { success: false, @@ -801,7 +802,7 @@ export async function handleCodexOAuthCallback(code, state) { const session = global.codexOAuthSessions.get(state); const { auth, state: expectedState, pkce } = session; - console.log('[Codex Auth] Processing OAuth callback...'); + logger.info('[Codex Auth] Processing OAuth callback...'); // 完成 OAuth 流程 const result = await auth.completeOAuthFlow(code, state, expectedState, pkce); @@ -822,7 +823,7 @@ export async function handleCodexOAuthCallback(code, state) { // 自动关联新生成的凭据到 Pools await autoLinkProviderConfigs(CONFIG); - console.log('[Codex Auth] OAuth callback processed successfully'); + logger.info('[Codex Auth] OAuth callback processed successfully'); return { success: true, @@ -834,7 +835,7 @@ export async function handleCodexOAuthCallback(code, state) { relativePath: result.relativePath }; } catch (error) { - console.error('[Codex Auth] OAuth callback failed:', error.message); + logger.error('[Codex Auth] OAuth callback failed:', error.message); // 广播认证失败事件 broadcastEvent({ diff --git a/src/auth/gemini-oauth.js b/src/auth/gemini-oauth.js index 1e563c5..935734e 100644 --- a/src/auth/gemini-oauth.js +++ b/src/auth/gemini-oauth.js @@ -1,4 +1,5 @@ import { OAuth2Client } from 'google-auth-library'; +import logger from '../utils/logger.js'; import http from 'http'; import fs from 'fs'; import path from 'path'; @@ -74,7 +75,7 @@ async function closeActiveServer(provider, port = null) { await new Promise((resolve) => { existing.server.close(() => { activeServers.delete(provider); - console.log(`[OAuth] 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); + logger.info(`[OAuth] 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); resolve(); }); }); @@ -87,7 +88,7 @@ async function closeActiveServer(provider, port = null) { await new Promise((resolve) => { info.server.close(() => { activeServers.delete(p); - console.log(`[OAuth] 已关闭端口 ${port} 上被占用(提供商: ${p})的旧服务器`); + logger.info(`[OAuth] 已关闭端口 ${port} 上被占用(提供商: ${p})的旧服务器`); resolve(); }); }); @@ -118,7 +119,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa const errorParam = url.searchParams.get('error'); if (code) { - console.log(`${config.logPrefix} 收到来自 Google 的成功回调: ${req.url}`); + logger.info(`${config.logPrefix} 收到来自 Google 的成功回调: ${req.url}`); try { const { tokens } = await authClient.getToken(code); @@ -136,7 +137,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa await fs.promises.mkdir(path.dirname(finalCredPath), { recursive: true }); await fs.promises.writeFile(finalCredPath, JSON.stringify(tokens, null, 2)); - console.log(`${config.logPrefix} 新令牌已接收并保存到文件: ${finalCredPath}`); + logger.info(`${config.logPrefix} 新令牌已接收并保存到文件: ${finalCredPath}`); const relativePath = path.relative(process.cwd(), finalCredPath); @@ -154,7 +155,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(true, '您可以关闭此页面')); } catch (tokenError) { - console.error(`${config.logPrefix} 获取令牌失败:`, tokenError); + logger.error(`${config.logPrefix} 获取令牌失败:`, tokenError); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`)); } finally { @@ -164,7 +165,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa } } else if (errorParam) { const errorMessage = `授权失败。Google 返回错误: ${errorParam}`; - console.error(`${config.logPrefix}`, errorMessage); + logger.error(`${config.logPrefix}`, errorMessage); res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, errorMessage)); @@ -172,12 +173,12 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa activeServers.delete(provider); }); } else { - console.log(`${config.logPrefix} 忽略无关请求: ${req.url}`); + logger.info(`${config.logPrefix} 忽略无关请求: ${req.url}`); res.writeHead(204); res.end(); } } catch (error) { - console.error(`${config.logPrefix} 处理回调时出错:`, error); + logger.error(`${config.logPrefix} 处理回调时出错:`, error); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, `服务器错误: ${error.message}`)); @@ -191,17 +192,17 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa server.on('error', (err) => { if (err.code === 'EADDRINUSE') { - console.error(`${config.logPrefix} 端口 ${port} 已被占用`); + logger.error(`${config.logPrefix} 端口 ${port} 已被占用`); reject(new Error(`端口 ${port} 已被占用`)); } else { - console.error(`${config.logPrefix} 服务器错误:`, err); + logger.error(`${config.logPrefix} 服务器错误:`, err); reject(err); } }); const host = '0.0.0.0'; server.listen(port, host, () => { - console.log(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`); + logger.info(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`); activeServers.set(provider, { server, port }); resolve(server); }); @@ -236,7 +237,7 @@ async function handleGoogleOAuth(providerKey, currentConfig, options = {}) { if (proxyConfig) { oauth2Options.transporterOptions = proxyConfig; - console.log(`${config.logPrefix} Using proxy for OAuth token exchange`); + logger.info(`${config.logPrefix} Using proxy for OAuth token exchange`); } const authClient = new OAuth2Client(oauth2Options); diff --git a/src/auth/iflow-oauth.js b/src/auth/iflow-oauth.js index 43de88a..6af58e3 100644 --- a/src/auth/iflow-oauth.js +++ b/src/auth/iflow-oauth.js @@ -1,4 +1,5 @@ import http from 'http'; +import logger from '../utils/logger.js'; import fs from 'fs'; import path from 'path'; import os from 'os'; @@ -67,7 +68,7 @@ async function fetchWithProxy(url, options = {}, providerType) { axiosConfig.httpAgent = proxyConfig.httpAgent; axiosConfig.httpsAgent = proxyConfig.httpsAgent; axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent - console.log(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`); + logger.info(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`); } try { @@ -255,7 +256,7 @@ async function closeIFlowServer(provider, port = null) { await new Promise((resolve) => { existing.server.close(() => { activeIFlowServers.delete(provider); - console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); + logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); resolve(); }); }); @@ -267,7 +268,7 @@ async function closeIFlowServer(provider, port = null) { await new Promise((resolve) => { info.server.close(() => { activeIFlowServers.delete(p); - console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); + logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); resolve(); }); }); @@ -296,7 +297,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = { const errorParam = url.searchParams.get('error'); if (errorParam) { - console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 授权失败: ${errorParam}`); + logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 授权失败: ${errorParam}`); res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, `授权失败: ${errorParam}`)); server.close(() => { @@ -306,7 +307,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = { } if (state !== expectedState) { - console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} State 验证失败`); + logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} State 验证失败`); res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, 'State 验证失败')); server.close(() => { @@ -316,7 +317,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = { } if (!code) { - console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 缺少授权码`); + logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 缺少授权码`); res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, '缺少授权码')); server.close(() => { @@ -325,16 +326,16 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = { return; } - console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 收到授权回调,正在交换令牌...`); + logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 收到授权回调,正在交换令牌...`); try { // 1. 交换授权码获取令牌 const tokenData = await exchangeIFlowCodeForTokens(code, redirectUri); - console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌交换成功`); + logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌交换成功`); // 2. 获取用户信息(包含 API Key) const userInfo = await fetchIFlowUserInfo(tokenData.accessToken); - console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 用户信息获取成功: ${userInfo.email}`); + logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 用户信息获取成功: ${userInfo.email}`); // 3. 组合完整的凭据数据 const credentialsData = { @@ -360,7 +361,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = { await fs.promises.mkdir(path.dirname(credPath), { recursive: true }); await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2)); - console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 凭据已保存: ${credPath}`); + logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 凭据已保存: ${credPath}`); const relativePath = path.relative(process.cwd(), credPath); @@ -380,7 +381,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = { res.end(generateResponsePage(true, `授权成功!账户: ${userInfo.email},您可以关闭此页面`)); } catch (tokenError) { - console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌处理失败:`, tokenError); + logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌处理失败:`, tokenError); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, `令牌处理失败: ${tokenError.message}`)); } finally { @@ -394,7 +395,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = { res.end(); } } catch (error) { - console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error); + logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, `服务器错误: ${error.message}`)); @@ -408,24 +409,24 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = { server.on('error', (err) => { if (err.code === 'EADDRINUSE') { - console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 端口 ${port} 已被占用`); + logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 端口 ${port} 已被占用`); reject(new Error(`端口 ${port} 已被占用`)); } else { - console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 服务器错误:`, err); + logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 服务器错误:`, err); reject(err); } }); const host = '0.0.0.0'; server.listen(port, host, () => { - console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`); + logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`); resolve(server); }); // 10 分钟超时自动关闭 setTimeout(() => { if (server.listening) { - console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 回调服务器超时,自动关闭`); + logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 回调服务器超时,自动关闭`); server.close(() => { activeIFlowServers.delete('openai-iflow'); }); @@ -453,7 +454,7 @@ export async function handleIFlowOAuth(currentConfig, options = {}) { // 生成授权链接 const { authUrl, redirectUri } = generateIFlowAuthorizationURL(state, port); - console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 生成授权链接: ${authUrl}`); + logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 生成授权链接: ${authUrl}`); // 关闭之前可能存在的服务器 await closeIFlowServer(providerKey, port); diff --git a/src/auth/kiro-oauth.js b/src/auth/kiro-oauth.js index ecf2168..8bd0325 100644 --- a/src/auth/kiro-oauth.js +++ b/src/auth/kiro-oauth.js @@ -1,4 +1,5 @@ import http from 'http'; +import logger from '../utils/logger.js'; import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; @@ -85,7 +86,7 @@ async function fetchWithProxy(url, options = {}, providerType) { axiosConfig.httpAgent = proxyConfig.httpAgent; axiosConfig.httpsAgent = proxyConfig.httpsAgent; axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent - console.log(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`); + logger.info(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`); } try { @@ -174,7 +175,7 @@ function generateCodeChallenge(codeVerifier) { export async function handleKiroOAuth(currentConfig, options = {}) { const method = options.method || options.authMethod || 'google'; // 默认使用 Google,同时支持 authMethod 参数 - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Starting OAuth with method: ${method}`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Starting OAuth with method: ${method}`); switch (method) { case 'google': @@ -249,7 +250,7 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) { // 获取 Builder ID Start URL(优先使用前端传入的值,否则使用默认值) const builderIDStartURL = options.builderIDStartURL || KIRO_OAUTH_CONFIG.builderIDStartURL; - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Using Builder ID Start URL: ${builderIDStartURL}`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Using Builder ID Start URL: ${builderIDStartURL}`); // 1. 注册 OIDC 客户端 const region = options.region || 'us-east-1'; @@ -308,7 +309,7 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) { taskId, { ...options, region } ).catch(error => { - console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error); + logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error); broadcastEvent('oauth_error', { provider: 'claude-kiro-oauth', error: error.message, @@ -343,7 +344,7 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv const taskControl = { shouldStop: false }; activeKiroPollingTasks.set(taskId, taskControl); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}]`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}]`); const poll = async () => { if (taskControl.shouldStop) { @@ -377,7 +378,7 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv const data = await response.json(); if (response.ok && data.accessToken) { - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`); // 保存令牌(符合现有规范) if (options.saveToConfigs) { @@ -419,7 +420,7 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv // 检查错误类型 if (data.error === 'authorization_pending') { - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (${attempts}/${maxAttempts})`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (${attempts}/${maxAttempts})`); await new Promise(resolve => setTimeout(resolve, interval * 1000)); return poll(); } else if (data.error === 'slow_down') { @@ -449,7 +450,7 @@ function stopKiroPollingTask(taskId) { if (task) { task.shouldStop = true; activeKiroPollingTasks.delete(taskId); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`); } } @@ -467,10 +468,10 @@ async function startKiroCallbackServer(codeVerifier, expectedState, options = {} try { const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options); activeKiroServers.set('claude-kiro-oauth', { server, port }); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`); return port; } catch (err) { - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 端口 ${port} 被占用,尝试下一个...`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 端口 ${port} 被占用,尝试下一个...`); } } @@ -486,7 +487,7 @@ async function closeKiroServer(provider, port = null) { await new Promise((resolve) => { existing.server.close(() => { activeKiroServers.delete(provider); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); resolve(); }); }); @@ -498,7 +499,7 @@ async function closeKiroServer(provider, port = null) { await new Promise((resolve) => { info.server.close(() => { activeKiroServers.delete(p); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); resolve(); }); }); @@ -551,7 +552,7 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); - console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token exchange failed:`, errorText); + logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token exchange failed:`, errorText); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, `获取令牌失败: ${tokenResponse.status}`)); return; @@ -582,7 +583,7 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options await fs.promises.mkdir(path.dirname(credPath), { recursive: true }); await fs.promises.writeFile(credPath, JSON.stringify(saveData, null, 2)); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 令牌已保存: ${credPath}`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 令牌已保存: ${credPath}`); // 广播成功事件 broadcastEvent('oauth_success', { @@ -608,7 +609,7 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options res.end(); } } catch (error) { - console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error); + logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, `服务器错误: ${error.message}`)); } @@ -733,7 +734,7 @@ export async function checkKiroCredentialsDuplicate(refreshToken, provider = 'cl // 检查 refreshToken 是否匹配 if (credentials.refreshToken && credentials.refreshToken === refreshToken) { const relativePath = path.relative(process.cwd(), fullPath); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Found duplicate refreshToken in: ${relativePath}`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Found duplicate refreshToken in: ${relativePath}`); return { isDuplicate: true, existingPath: relativePath @@ -751,7 +752,7 @@ export async function checkKiroCredentialsDuplicate(refreshToken, provider = 'cl return await scanDirectory(kiroDir); } catch (error) { - console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Error checking duplicates:`, error.message); + logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Error checking duplicates:`, error.message); return { isDuplicate: false }; } } @@ -800,7 +801,7 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_ } try { - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`); const tokenData = await refreshKiroToken(refreshToken, region); @@ -815,7 +816,7 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_ const relativePath = path.relative(process.cwd(), credPath); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`); results.details.push({ index: i + 1, @@ -826,7 +827,7 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_ results.success++; } catch (error) { - console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message); + logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message); results.details.push({ index: i + 1, @@ -922,7 +923,7 @@ export async function batchImportKiroRefreshTokensStream(refreshTokens, region = } try { - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`); const tokenData = await refreshKiroToken(refreshToken, region); @@ -937,7 +938,7 @@ export async function batchImportKiroRefreshTokensStream(refreshTokens, region = const relativePath = path.relative(process.cwd(), credPath); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`); progressData.current = { index: i + 1, @@ -949,7 +950,7 @@ export async function batchImportKiroRefreshTokensStream(refreshTokens, region = results.success++; } catch (error) { - console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message); + logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message); progressData.current = { index: i + 1, @@ -1020,7 +1021,7 @@ export async function importAwsCredentials(credentials, skipDuplicateCheck = fal } } - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Importing AWS credentials...`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Importing AWS credentials...`); // 准备凭据数据 - 四个字段都是必需的 const credentialsData = { @@ -1046,7 +1047,7 @@ export async function importAwsCredentials(credentials, skipDuplicateCheck = fal // 尝试刷新获取最新的 accessToken try { - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Attempting to refresh token with provided credentials...`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Attempting to refresh token with provided credentials...`); const refreshRegion = credentials.idcRegion || KIRO_REFRESH_CONSTANTS.IDC_REGION; const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_IDC_URL.replace('{{region}}', refreshRegion); @@ -1070,12 +1071,12 @@ export async function importAwsCredentials(credentials, skipDuplicateCheck = fal credentialsData.refreshToken = tokenData.refreshToken; const expiresIn = tokenData.expiresIn || 3600; credentialsData.expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Token refreshed successfully`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Token refreshed successfully`); } else { - console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh failed, saving original credentials`); + logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh failed, saving original credentials`); } } catch (refreshError) { - console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh error:`, refreshError.message); + logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh error:`, refreshError.message); // 继续保存原始凭据 } @@ -1090,7 +1091,7 @@ export async function importAwsCredentials(credentials, skipDuplicateCheck = fal const relativePath = path.relative(process.cwd(), credPath); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials saved to: ${relativePath}`); + logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials saved to: ${relativePath}`); // 广播事件 broadcastEvent('oauth_success', { @@ -1108,10 +1109,12 @@ export async function importAwsCredentials(credentials, skipDuplicateCheck = fal }; } catch (error) { - console.error(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials import failed:`, error); + logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials import failed:`, error); return { success: false, error: error.message }; } -} \ No newline at end of file +} + + diff --git a/src/auth/qwen-oauth.js b/src/auth/qwen-oauth.js index 76c186b..8e21f15 100644 --- a/src/auth/qwen-oauth.js +++ b/src/auth/qwen-oauth.js @@ -1,4 +1,5 @@ import fs from 'fs'; +import logger from '../utils/logger.js'; import path from 'path'; import os from 'os'; import crypto from 'crypto'; @@ -55,7 +56,7 @@ async function fetchWithProxy(url, options = {}, providerType) { axiosConfig.httpAgent = proxyConfig.httpAgent; axiosConfig.httpsAgent = proxyConfig.httpsAgent; axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent - console.log(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`); + logger.info(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`); } try { @@ -117,7 +118,7 @@ function stopPollingTask(taskId) { if (task) { task.shouldStop = true; activePollingTasks.delete(taskId); - console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`); + logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`); } } @@ -140,12 +141,12 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = const taskControl = { shouldStop: false }; activePollingTasks.set(taskId, taskControl); - console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}],间隔 ${interval} 秒,最多尝试 ${maxAttempts} 次`); + logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}],间隔 ${interval} 秒,最多尝试 ${maxAttempts} 次`); const poll = async () => { // 检查是否需要停止 if (taskControl.shouldStop) { - console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询任务 [${taskId}] 已被停止`); + logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询任务 [${taskId}] 已被停止`); throw new Error('轮询任务已被取消'); } @@ -181,7 +182,7 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = if (response.ok && data.access_token) { // 成功获取令牌 - console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`); + logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`); // 如果指定了保存到 configs 目录 if (options.saveToConfigs) { @@ -195,7 +196,7 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = // 保存令牌到文件 await fs.promises.mkdir(path.dirname(credPath), { recursive: true }); await fs.promises.writeFile(credPath, JSON.stringify(data, null, 2)); - console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 令牌已保存到 ${credPath}`); + logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 令牌已保存到 ${credPath}`); const relativePath = path.relative(process.cwd(), credPath); @@ -219,12 +220,12 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = // 检查错误类型 if (data.error === 'authorization_pending') { // 用户尚未完成授权,继续轮询 - console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (第 ${attempts}/${maxAttempts} 次尝试)`); + logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (第 ${attempts}/${maxAttempts} 次尝试)`); await new Promise(resolve => setTimeout(resolve, interval * 1000)); return poll(); } else if (data.error === 'slow_down') { // 需要降低轮询频率 - console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 降低轮询频率`); + logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 降低轮询频率`); await new Promise(resolve => setTimeout(resolve, (interval + 5) * 1000)); return poll(); } else if (data.error === 'expired_token') { @@ -241,7 +242,7 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = if (error.message.includes('授权') || error.message.includes('过期') || error.message.includes('拒绝')) { throw error; } - console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询出错:`, error); + logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询出错:`, error); // 网络错误,继续重试 await new Promise(resolve => setTimeout(resolve, interval * 1000)); return poll(); @@ -310,7 +311,7 @@ export async function handleQwenOAuth(currentConfig, options = {}) { // 不等待轮询完成,立即返回授权信息 pollQwenToken(deviceAuth.device_code, codeVerifier, interval, expiresIn, taskId, options) .catch(error => { - console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error); + logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error); // 广播授权失败事件 broadcastEvent('oauth_error', { provider: 'openai-qwen-oauth', @@ -333,7 +334,7 @@ export async function handleQwenOAuth(currentConfig, options = {}) { } }; } catch (error) { - console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 请求失败:`, error); + logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 请求失败:`, error); throw new Error(`Qwen OAuth 授权失败: ${error.message}`); } } \ No newline at end of file diff --git a/src/convert/convert-old.js b/src/convert/convert-old.js index f4b89e8..5c2a0cc 100644 --- a/src/convert/convert-old.js +++ b/src/convert/convert-old.js @@ -1,4 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; +import logger from '../utils/logger.js'; import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js'; import { streamStateManager, @@ -113,7 +114,7 @@ function _cleanJsonSchemaProperties(schema) { function _determineReasoningEffortFromBudget(budgetTokens) { // 如果没有提供budget_tokens,默认为high if (budgetTokens === null || budgetTokens === undefined) { - console.info("No budget_tokens provided, defaulting to reasoning_effort='high'"); + logger.info("No budget_tokens provided, defaulting to reasoning_effort='high'"); return "high"; } @@ -121,7 +122,7 @@ function _determineReasoningEffortFromBudget(budgetTokens) { const LOW_THRESHOLD = 50; // 低推理努力的阈值 const HIGH_THRESHOLD = 200; // 高推理努力的阈值 - console.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`); + logger.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`); let effort; if (budgetTokens <= LOW_THRESHOLD) { @@ -132,7 +133,7 @@ function _determineReasoningEffortFromBudget(budgetTokens) { effort = "high"; } - console.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`); + logger.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`); return effort; } @@ -253,7 +254,7 @@ export function convertData(data, type, fromProvider, toProvider, model) { throw new Error(`No conversion function found from ${getProtocolPrefix(fromProvider)} to ${toProvider} for type: ${type}`); } - console.log(conversionFunction); + logger.info(conversionFunction); if (type === 'response' || type === 'streamChunk' || type === 'modelList') { return conversionFunction(data, model); } else { @@ -856,16 +857,16 @@ export function toOpenAIRequestFromClaude(claudeRequest) { if (claudeRequest.max_tokens !== undefined) { maxCompletionTokens = claudeRequest.max_tokens; delete openaiRequest.max_tokens; // 移除max_tokens,使用max_completion_tokens - console.info(`Using client max_tokens as max_completion_tokens: ${maxCompletionTokens}`); + logger.info(`Using client max_tokens as max_completion_tokens: ${maxCompletionTokens}`); } else { // 优先级2:环境变量OPENAI_REASONING_MAX_TOKENS const envMaxTokens = process.env.OPENAI_REASONING_MAX_TOKENS; if (envMaxTokens) { try { maxCompletionTokens = parseInt(envMaxTokens, 10); - console.info(`Using OPENAI_REASONING_MAX_TOKENS from environment: ${maxCompletionTokens}`); + logger.info(`Using OPENAI_REASONING_MAX_TOKENS from environment: ${maxCompletionTokens}`); } catch (e) { - console.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}', must be integer`); + logger.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}', must be integer`); } } @@ -875,9 +876,9 @@ export function toOpenAIRequestFromClaude(claudeRequest) { } } openaiRequest.max_completion_tokens = maxCompletionTokens; - console.info(`Anthropic thinking enabled -> OpenAI reasoning_effort='${reasoningEffort}', max_completion_tokens=${maxCompletionTokens}`); + logger.info(`Anthropic thinking enabled -> OpenAI reasoning_effort='${reasoningEffort}', max_completion_tokens=${maxCompletionTokens}`); if (budgetTokens) { - console.info(`Budget tokens: ${budgetTokens} -> reasoning_effort: '${reasoningEffort}'`); + logger.info(`Budget tokens: ${budgetTokens} -> reasoning_effort: '${reasoningEffort}'`); } } @@ -1026,7 +1027,7 @@ export function toGeminiRequestFromOpenAI(openaiRequest) { functionDeclarations: openaiRequest.tools.map(t => { // Ensure tool is a valid object and has function property if (!t || typeof t !== 'object' || !t.function) { - console.warn("Skipping invalid tool declaration in openaiRequest.tools."); + logger.warn("Skipping invalid tool declaration in openaiRequest.tools."); return null; // Return null for invalid tools, filter out later } @@ -1057,7 +1058,7 @@ export function toGeminiRequestFromOpenAI(openaiRequest) { // Validation if (geminiRequest.contents[0]?.role !== 'user') { - console.warn(`[Request Conversion] Warning: Conversation does not start with a 'user' role.`); + logger.warn(`[Request Conversion] Warning: Conversation does not start with a 'user' role.`); } return geminiRequest; @@ -1618,7 +1619,7 @@ export function extractTextFromMessageContent(content) { export function toGeminiRequestFromClaude(claudeRequest) { // Ensure claudeRequest is a valid object if (!claudeRequest || typeof claudeRequest !== 'object') { - console.warn("Invalid claudeRequest provided to toGeminiRequestFromClaude."); + logger.warn("Invalid claudeRequest provided to toGeminiRequestFromClaude."); return { contents: [] }; } @@ -1654,7 +1655,7 @@ export function toGeminiRequestFromClaude(claudeRequest) { claudeRequest.messages.forEach(message => { // Ensure message is a valid object and has a role and content if (!message || typeof message !== 'object' || !message.role || !message.content) { - console.warn("Skipping invalid message in claudeRequest.messages."); + logger.warn("Skipping invalid message in claudeRequest.messages."); return; } @@ -1698,13 +1699,13 @@ export function toGeminiRequestFromClaude(claudeRequest) { functionDeclarations: claudeRequest.tools.map(tool => { // Ensure tool is a valid object and has a name if (!tool || typeof tool !== 'object' || !tool.name) { - console.warn("Skipping invalid tool declaration in claudeRequest.tools."); + logger.warn("Skipping invalid tool declaration in claudeRequest.tools."); return null; // Return null for invalid tools, filter out later } // Filter out TodoWrite tool // if (tool.name === 'TodoWrite') { - // console.log("Filtering out TodoWrite tool"); + // logger.info("Filtering out TodoWrite tool"); // return null; // } @@ -1737,7 +1738,7 @@ export function toGeminiRequestFromClaude(claudeRequest) { */ function buildGeminiToolConfigFromClaude(claudeToolChoice) { if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) { - console.warn("Invalid claudeToolChoice provided to buildGeminiToolConfigFromClaude."); + logger.warn("Invalid claudeToolChoice provided to buildGeminiToolConfigFromClaude."); return undefined; } @@ -1750,10 +1751,10 @@ function buildGeminiToolConfigFromClaude(claudeToolChoice) { if (claudeToolChoice.name && typeof claudeToolChoice.name === 'string') { return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [claudeToolChoice.name] } }; } - console.warn("Invalid tool name in claudeToolChoice of type 'tool'."); + logger.warn("Invalid tool name in claudeToolChoice of type 'tool'."); return undefined; default: - console.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`); + logger.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`); return undefined; } } @@ -1778,7 +1779,7 @@ function processClaudeContentToGeminiParts(content) { content.forEach(block => { // Ensure block is a valid object and has a type if (!block || typeof block !== 'object' || !block.type) { - console.warn("Skipping invalid content block in processClaudeContentToGeminiParts."); + logger.warn("Skipping invalid content block in processClaudeContentToGeminiParts."); return; } @@ -1787,7 +1788,7 @@ function processClaudeContentToGeminiParts(content) { if (typeof block.text === 'string') { parts.push({ text: block.text }); } else { - console.warn("Invalid text content in Claude text block."); + logger.warn("Invalid text content in Claude text block."); } break; @@ -1801,7 +1802,7 @@ function processClaudeContentToGeminiParts(content) { } }); } else { - console.warn("Invalid image source in Claude image block."); + logger.warn("Invalid image source in Claude image block."); } break; @@ -1809,7 +1810,7 @@ function processClaudeContentToGeminiParts(content) { if (typeof block.name === 'string' && block.input && typeof block.input === 'object') { // Filter out TodoWrite tool use // if (block.name === 'TodoWrite') { - // console.log("Filtering out TodoWrite tool use"); + // logger.info("Filtering out TodoWrite tool use"); // break; // Skip adding this tool to parts // } parts.push({ @@ -1819,7 +1820,7 @@ function processClaudeContentToGeminiParts(content) { } }); } else { - console.warn("Invalid tool_use block in Claude content."); + logger.warn("Invalid tool_use block in Claude content."); } break; @@ -1837,7 +1838,7 @@ function processClaudeContentToGeminiParts(content) { } }); } else { - console.warn("Invalid tool_result block in Claude content: missing tool_use_id."); + logger.warn("Invalid tool_result block in Claude content: missing tool_use_id."); } break; @@ -1846,7 +1847,7 @@ function processClaudeContentToGeminiParts(content) { if (typeof block.text === 'string') { parts.push({ text: block.text }); } else { - console.warn(`Unsupported Claude content block type: ${block.type}. Skipping.`); + logger.warn(`Unsupported Claude content block type: ${block.type}. Skipping.`); } } }); @@ -1936,8 +1937,8 @@ function processGeminiResponseToClaudeContent(geminiResponse) { for (const candidate of geminiResponse.candidates) { // 检查完成原因是否为错误类型 if (candidate.finishReason && candidate.finishReason !== 'STOP') { - // console.log('Gemini response finishReason:', JSON.stringify(candidate)); - // console.warn('Gemini response contains malformed function call:', candidate.finishMessage || 'No finish message'); + // logger.info('Gemini response finishReason:', JSON.stringify(candidate)); + // logger.warn('Gemini response contains malformed function call:', candidate.finishMessage || 'No finish message'); // 将错误信息作为文本内容返回 if (candidate.finishMessage) { @@ -1946,7 +1947,7 @@ function processGeminiResponseToClaudeContent(geminiResponse) { text: `Error: ${candidate.finishMessage}` }); } - // console.log("Processed content:", content); + // logger.info("Processed content:", content); continue; // 跳过当前候选的进一步处理 } @@ -2568,4 +2569,6 @@ export function getOpenAIResponsesStreamChunkEnd(id){ generateOutputItemDone(id), generateResponseCompleted(id) ]; -} \ No newline at end of file +} + + diff --git a/src/convert/convert.js b/src/convert/convert.js index 5e6d1bc..e846d16 100644 --- a/src/convert/convert.js +++ b/src/convert/convert.js @@ -7,6 +7,7 @@ */ import { v4 as uuidv4 } from 'uuid'; +import logger from '../utils/logger.js'; import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js'; import { ConverterFactory } from '../converters/ConverterFactory.js'; import { @@ -46,7 +47,7 @@ export function convertData(data, type, fromProvider, toProvider, model) { // 如果目标协议为 forward,直接返回原始数据,无需转换 if (toProtocol === MODEL_PROTOCOL_PREFIX.FORWARD || fromProtocol === MODEL_PROTOCOL_PREFIX.FORWARD) { - console.log(`[Convert] Target protocol is forward, skipping conversion`); + logger.info(`[Convert] Target protocol is forward, skipping conversion`); return data; } @@ -75,7 +76,7 @@ export function convertData(data, type, fromProvider, toProvider, model) { throw new Error(`Unsupported conversion type: ${type}`); } } catch (error) { - console.error(`Conversion error: ${error.message}`); + logger.error(`Conversion error: ${error.message}`); throw error; } } @@ -386,4 +387,6 @@ export default { toOpenAIStreamChunkFromOpenAIResponses, toClaudeRequestFromOpenAIResponses, toGeminiRequestFromOpenAIResponses, -}; \ No newline at end of file +}; + + diff --git a/src/converters/ConverterFactory.js b/src/converters/ConverterFactory.js index cabddd5..ba97e3d 100644 --- a/src/converters/ConverterFactory.js +++ b/src/converters/ConverterFactory.js @@ -4,6 +4,7 @@ */ import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js'; +import logger from '../utils/logger.js'; /** * 转换器工厂(单例模式 + 工厂模式) @@ -125,7 +126,7 @@ export class ContentProcessorFactory { static createProcessor(sourceFormat, targetFormat) { // 这里返回null,实际使用时需要导入具体的处理器类 // 为了避免循环依赖,处理器类应该在使用时动态导入 - console.warn(`Content processor for ${sourceFormat} to ${targetFormat} not yet implemented`); + logger.warn(`Content processor for ${sourceFormat} to ${targetFormat} not yet implemented`); return null; } @@ -166,7 +167,7 @@ export class ToolProcessorFactory { * @returns {ToolProcessor} 工具处理器实例 */ static createProcessor(sourceFormat, targetFormat) { - console.warn(`Tool processor for ${sourceFormat} to ${targetFormat} not yet implemented`); + logger.warn(`Tool processor for ${sourceFormat} to ${targetFormat} not yet implemented`); return null; } diff --git a/src/converters/strategies/ClaudeConverter.js b/src/converters/strategies/ClaudeConverter.js index 3349498..3743ff7 100644 --- a/src/converters/strategies/ClaudeConverter.js +++ b/src/converters/strategies/ClaudeConverter.js @@ -4,6 +4,7 @@ */ import { v4 as uuidv4 } from 'uuid'; +import logger from '../../utils/logger.js'; import { BaseConverter } from '../BaseConverter.js'; import { checkAndAssignOrDefault, @@ -249,7 +250,7 @@ export class ClaudeConverter extends BaseConverter { try { maxCompletionTokens = parseInt(envMaxTokens, 10); } catch (e) { - console.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}'`); + logger.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}'`); } } if (!envMaxTokens) { @@ -754,7 +755,7 @@ export class ClaudeConverter extends BaseConverter { */ toGeminiRequest(claudeRequest) { if (!claudeRequest || typeof claudeRequest !== 'object') { - console.warn("Invalid claudeRequest provided to toGeminiRequest."); + logger.warn("Invalid claudeRequest provided to toGeminiRequest."); return { contents: [] }; } @@ -795,7 +796,7 @@ export class ClaudeConverter extends BaseConverter { if (Array.isArray(claudeRequest.messages)) { claudeRequest.messages.forEach(message => { if (!message || typeof message !== 'object' || !message.role) { - console.warn("Skipping invalid message in claudeRequest.messages."); + logger.warn("Skipping invalid message in claudeRequest.messages."); return; } @@ -992,7 +993,7 @@ export class ClaudeConverter extends BaseConverter { claudeRequest.tools.forEach(tool => { if (!tool || typeof tool !== 'object' || !tool.name) { - console.warn("Skipping invalid tool declaration in claudeRequest.tools."); + logger.warn("Skipping invalid tool declaration in claudeRequest.tools."); return; } @@ -1334,7 +1335,7 @@ export class ClaudeConverter extends BaseConverter { content.forEach(block => { if (!block || typeof block !== 'object' || !block.type) { - console.warn("Skipping invalid content block."); + logger.warn("Skipping invalid content block."); return; } @@ -1400,7 +1401,7 @@ export class ClaudeConverter extends BaseConverter { */ buildGeminiToolConfigFromClaude(claudeToolChoice) { if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) { - console.warn("Invalid claudeToolChoice provided."); + logger.warn("Invalid claudeToolChoice provided."); return undefined; } @@ -1418,10 +1419,10 @@ export class ClaudeConverter extends BaseConverter { } }; } - console.warn("Invalid tool name in claudeToolChoice of type 'tool'."); + logger.warn("Invalid tool name in claudeToolChoice of type 'tool'."); return undefined; default: - console.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`); + logger.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`); return undefined; } } diff --git a/src/converters/strategies/OpenAIConverter.js b/src/converters/strategies/OpenAIConverter.js index 2bec45f..2d887e7 100644 --- a/src/converters/strategies/OpenAIConverter.js +++ b/src/converters/strategies/OpenAIConverter.js @@ -4,6 +4,7 @@ */ import { v4 as uuidv4 } from 'uuid'; +import logger from '../../utils/logger.js'; import { BaseConverter } from '../BaseConverter.js'; import { CodexConverter } from './CodexConverter.js'; import { @@ -1172,10 +1173,10 @@ export class OpenAIConverter extends BaseConverter { // But this parameter cannot be added when using tools (causes 400 error) const hasTools = tools && Array.isArray(tools) && tools.length > 0; if (!hasTools && model && (model.includes('2.5') || model.includes('thinking') || model.includes('2.0-flash-thinking'))) { - console.log(`[OpenAI->Gemini] Adding responseModalities: ["TEXT"] for model: ${model}`); + logger.info(`[OpenAI->Gemini] Adding responseModalities: ["TEXT"] for model: ${model}`); config.responseModalities = ["TEXT"]; } else if (hasTools && model && (model.includes('2.5') || model.includes('thinking') || model.includes('2.0-flash-thinking'))) { - console.log(`[OpenAI->Gemini] Skipping responseModalities for model ${model} because tools are present`); + logger.info(`[OpenAI->Gemini] Skipping responseModalities for model ${model} because tools are present`); } return config; diff --git a/src/converters/utils.js b/src/converters/utils.js index 55c5933..fe6bac0 100644 --- a/src/converters/utils.js +++ b/src/converters/utils.js @@ -4,6 +4,7 @@ */ import { v4 as uuidv4 } from 'uuid'; +import logger from '../utils/logger.js'; // ============================================================================= // 常量定义 @@ -327,14 +328,14 @@ export function mapFinishReason(reason, sourceFormat, targetFormat) { */ export function determineReasoningEffortFromBudget(budgetTokens) { if (budgetTokens === null || budgetTokens === undefined) { - console.info("No budget_tokens provided, defaulting to reasoning_effort='high'"); + logger.info("No budget_tokens provided, defaulting to reasoning_effort='high'"); return "high"; } const LOW_THRESHOLD = 50; const HIGH_THRESHOLD = 200; - console.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`); + logger.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`); let effort; if (budgetTokens <= LOW_THRESHOLD) { @@ -345,7 +346,7 @@ export function determineReasoningEffortFromBudget(budgetTokens) { effort = "high"; } - console.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`); + logger.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`); return effort; } diff --git a/src/core/config-manager.js b/src/core/config-manager.js index fe1d018..cda962d 100644 --- a/src/core/config-manager.js +++ b/src/core/config-manager.js @@ -1,6 +1,7 @@ import * as fs from 'fs'; import { promises as pfs } from 'fs'; import { INPUT_SYSTEM_PROMPT_FILE, MODEL_PROVIDER } from '../utils/common.js'; +import logger from '../utils/logger.js'; export let CONFIG = {}; // Make CONFIG exportable export let PROMPT_LOG_FILENAME = ''; // Make PROMPT_LOG_FILENAME exportable @@ -21,7 +22,7 @@ function normalizeConfiguredProviders(config) { } const matched = ALL_MODEL_PROVIDERS.find((provider) => provider.toLowerCase() === trimmed.toLowerCase()); if (!matched) { - console.warn(`[Config Warning] Unknown model provider '${trimmed}'. This entry will be ignored.`); + logger.warn(`[Config Warning] Unknown model provider '${trimmed}'. This entry will be ignored.`); return; } if (!dedupedProviders.includes(matched)) { @@ -58,9 +59,9 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP try { const configData = fs.readFileSync(configFilePath, 'utf8'); currentConfig = JSON.parse(configData); - console.log('[Config] Loaded configuration from configs/config.json'); + logger.info('[Config] Loaded configuration from configs/config.json'); } catch (error) { - console.error('[Config Error] Failed to load configs/config.json:', error.message); + logger.error('[Config Error] Failed to load configs/config.json:', error.message); // Fallback to default values if config.json is not found or invalid currentConfig = { REQUIRED_API_KEY: "123456", @@ -80,9 +81,17 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP CRON_REFRESH_TOKEN: false, PROVIDER_POOLS_FILE_PATH: null, // 新增号池配置文件路径 MAX_ERROR_COUNT: 10, // 提供商最大错误次数 - providerFallbackChain: {} // 跨类型 Fallback 链配置 + providerFallbackChain: {}, // 跨类型 Fallback 链配置 + LOG_ENABLED: true, + LOG_OUTPUT_MODE: "all", + LOG_LEVEL: "info", + LOG_DIR: "logs", + LOG_INCLUDE_REQUEST_ID: true, + LOG_INCLUDE_TIMESTAMP: true, + LOG_MAX_FILE_SIZE: 10485760, + LOG_MAX_FILES: 10 }; - console.log('[Config] Using default configuration.'); + logger.info('[Config] Using default configuration.'); } // Parse command-line arguments @@ -92,7 +101,7 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP currentConfig.REQUIRED_API_KEY = args[i + 1]; i++; } else { - console.warn(`[Config Warning] --api-key flag requires a value.`); + logger.warn(`[Config Warning] --api-key flag requires a value.`); } } else if (args[i] === '--log-prompts') { if (i + 1 < args.length) { @@ -100,32 +109,32 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP if (mode === 'console' || mode === 'file') { currentConfig.PROMPT_LOG_MODE = mode; } else { - console.warn(`[Config Warning] Invalid mode for --log-prompts. Expected 'console' or 'file'. Prompt logging is disabled.`); + logger.warn(`[Config Warning] Invalid mode for --log-prompts. Expected 'console' or 'file'. Prompt logging is disabled.`); } i++; } else { - console.warn(`[Config Warning] --log-prompts flag requires a value.`); + logger.warn(`[Config Warning] --log-prompts flag requires a value.`); } } else if (args[i] === '--port') { if (i + 1 < args.length) { currentConfig.SERVER_PORT = parseInt(args[i + 1], 10); i++; } else { - console.warn(`[Config Warning] --port flag requires a value.`); + logger.warn(`[Config Warning] --port flag requires a value.`); } } else if (args[i] === '--model-provider') { if (i + 1 < args.length) { currentConfig.MODEL_PROVIDER = args[i + 1]; i++; } else { - console.warn(`[Config Warning] --model-provider flag requires a value.`); + logger.warn(`[Config Warning] --model-provider flag requires a value.`); } } else if (args[i] === '--system-prompt-file') { if (i + 1 < args.length) { currentConfig.SYSTEM_PROMPT_FILE_PATH = args[i + 1]; i++; } else { - console.warn(`[Config Warning] --system-prompt-file flag requires a value.`); + logger.warn(`[Config Warning] --system-prompt-file flag requires a value.`); } } else if (args[i] === '--system-prompt-mode') { if (i + 1 < args.length) { @@ -133,53 +142,53 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP if (mode === 'overwrite' || mode === 'append') { currentConfig.SYSTEM_PROMPT_MODE = mode; } else { - console.warn(`[Config Warning] Invalid mode for --system-prompt-mode. Expected 'overwrite' or 'append'. Using default 'overwrite'.`); + logger.warn(`[Config Warning] Invalid mode for --system-prompt-mode. Expected 'overwrite' or 'append'. Using default 'overwrite'.`); } i++; } else { - console.warn(`[Config Warning] --system-prompt-mode flag requires a value.`); + logger.warn(`[Config Warning] --system-prompt-mode flag requires a value.`); } } else if (args[i] === '--host') { if (i + 1 < args.length) { currentConfig.HOST = args[i + 1]; i++; } else { - console.warn(`[Config Warning] --host flag requires a value.`); + logger.warn(`[Config Warning] --host flag requires a value.`); } } else if (args[i] === '--prompt-log-base-name') { if (i + 1 < args.length) { currentConfig.PROMPT_LOG_BASE_NAME = args[i + 1]; i++; } else { - console.warn(`[Config Warning] --prompt-log-base-name flag requires a value.`); + logger.warn(`[Config Warning] --prompt-log-base-name flag requires a value.`); } } else if (args[i] === '--cron-near-minutes') { if (i + 1 < args.length) { currentConfig.CRON_NEAR_MINUTES = parseInt(args[i + 1], 10); i++; } else { - console.warn(`[Config Warning] --cron-near-minutes flag requires a value.`); + logger.warn(`[Config Warning] --cron-near-minutes flag requires a value.`); } } else if (args[i] === '--cron-refresh-token') { if (i + 1 < args.length) { currentConfig.CRON_REFRESH_TOKEN = args[i + 1].toLowerCase() === 'true'; i++; } else { - console.warn(`[Config Warning] --cron-refresh-token flag requires a value.`); + logger.warn(`[Config Warning] --cron-refresh-token flag requires a value.`); } } else if (args[i] === '--provider-pools-file') { if (i + 1 < args.length) { currentConfig.PROVIDER_POOLS_FILE_PATH = args[i + 1]; i++; } else { - console.warn(`[Config Warning] --provider-pools-file flag requires a value.`); + logger.warn(`[Config Warning] --provider-pools-file flag requires a value.`); } } else if (args[i] === '--max-error-count') { if (i + 1 < args.length) { currentConfig.MAX_ERROR_COUNT = parseInt(args[i + 1], 10); i++; } else { - console.warn(`[Config Warning] --max-error-count flag requires a value.`); + logger.warn(`[Config Warning] --max-error-count flag requires a value.`); } } } @@ -199,9 +208,9 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP try { const poolsData = await pfs.readFile(currentConfig.PROVIDER_POOLS_FILE_PATH, 'utf8'); currentConfig.providerPools = JSON.parse(poolsData); - console.log(`[Config] Loaded provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}`); + logger.info(`[Config] Loaded provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}`); } catch (error) { - console.error(`[Config Error] Failed to load provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}: ${error.message}`); + logger.error(`[Config Error] Failed to load provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}: ${error.message}`); currentConfig.providerPools = {}; } } else { @@ -220,6 +229,22 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP // Assign to the exported CONFIG Object.assign(CONFIG, currentConfig); + + // Initialize logger + logger.initialize({ + enabled: CONFIG.LOG_ENABLED, + outputMode: CONFIG.LOG_OUTPUT_MODE, + logLevel: CONFIG.LOG_LEVEL, + logDir: CONFIG.LOG_DIR, + includeRequestId: CONFIG.LOG_INCLUDE_REQUEST_ID, + includeTimestamp: CONFIG.LOG_INCLUDE_TIMESTAMP, + maxFileSize: CONFIG.LOG_MAX_FILE_SIZE, + maxFiles: CONFIG.LOG_MAX_FILES + }); + + // Cleanup old logs periodically + logger.cleanupOldLogs(); + return CONFIG; } @@ -233,9 +258,9 @@ export async function getSystemPromptFileContent(filePath) { await pfs.access(filePath, pfs.constants.F_OK); } catch (error) { if (error.code === 'ENOENT') { - console.warn(`[System Prompt] Specified system prompt file not found: ${filePath}`); + logger.warn(`[System Prompt] Specified system prompt file not found: ${filePath}`); } else { - console.error(`[System Prompt] Error accessing system prompt file ${filePath}: ${error.message}`); + logger.error(`[System Prompt] Error accessing system prompt file ${filePath}: ${error.message}`); } return null; } @@ -245,10 +270,10 @@ export async function getSystemPromptFileContent(filePath) { if (!content.trim()) { return null; } - console.log(`[System Prompt] Loaded system prompt from ${filePath}`); + logger.info(`[System Prompt] Loaded system prompt from ${filePath}`); return content; } catch (error) { - console.error(`[System Prompt] Error reading system prompt file ${filePath}: ${error.message}`); + logger.error(`[System Prompt] Error reading system prompt file ${filePath}: ${error.message}`); return null; } } diff --git a/src/core/master.js b/src/core/master.js index 09232df..ff354f0 100644 --- a/src/core/master.js +++ b/src/core/master.js @@ -12,6 +12,7 @@ */ import { fork } from 'child_process'; +import logger from '../utils/logger.js'; import * as http from 'http'; import * as path from 'path'; import { fileURLToPath } from 'url'; @@ -45,13 +46,13 @@ const config = { */ function startWorker() { if (workerProcess) { - console.log('[Master] Worker process already running, PID:', workerProcess.pid); + logger.info('[Master] Worker process already running, PID:', workerProcess.pid); return; } - console.log('[Master] Starting worker process...'); - console.log('[Master] Worker script:', config.workerScript); - console.log('[Master] Worker args:', config.args.join(' ')); + logger.info('[Master] Starting worker process...'); + logger.info('[Master] Worker script:', config.workerScript); + logger.info('[Master] Worker args:', config.args.join(' ')); workerProcess = fork(config.workerScript, config.args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], @@ -64,30 +65,30 @@ function startWorker() { workerStatus.pid = workerProcess.pid; workerStatus.startTime = new Date().toISOString(); - console.log('[Master] Worker process started, PID:', workerProcess.pid); + logger.info('[Master] Worker process started, PID:', workerProcess.pid); // 监听子进程消息 workerProcess.on('message', (message) => { - console.log('[Master] Received message from worker:', message); + logger.info('[Master] Received message from worker:', message); handleWorkerMessage(message); }); // 监听子进程退出 workerProcess.on('exit', (code, signal) => { - console.log(`[Master] Worker process exited with code ${code}, signal ${signal}`); + logger.info(`[Master] Worker process exited with code ${code}, signal ${signal}`); workerProcess = null; workerStatus.pid = null; // 如果不是主动重启导致的退出,尝试自动重启 if (!workerStatus.isRestarting && code !== 0) { - console.log('[Master] Worker crashed, attempting auto-restart...'); + logger.info('[Master] Worker crashed, attempting auto-restart...'); scheduleRestart(); } }); // 监听子进程错误 workerProcess.on('error', (error) => { - console.error('[Master] Worker process error:', error.message); + logger.error('[Master] Worker process error:', error.message); }); } @@ -99,16 +100,16 @@ function startWorker() { function stopWorker(graceful = true) { return new Promise((resolve) => { if (!workerProcess) { - console.log('[Master] No worker process to stop'); + logger.info('[Master] No worker process to stop'); resolve(); return; } - console.log('[Master] Stopping worker process, PID:', workerProcess.pid); + logger.info('[Master] Stopping worker process, PID:', workerProcess.pid); const timeout = setTimeout(() => { if (workerProcess) { - console.log('[Master] Force killing worker process...'); + logger.info('[Master] Force killing worker process...'); workerProcess.kill('SIGKILL'); } resolve(); @@ -118,7 +119,7 @@ function stopWorker(graceful = true) { clearTimeout(timeout); workerProcess = null; workerStatus.pid = null; - console.log('[Master] Worker process stopped'); + logger.info('[Master] Worker process stopped'); resolve(); }); @@ -138,7 +139,7 @@ function stopWorker(graceful = true) { */ async function restartWorker() { if (workerStatus.isRestarting) { - console.log('[Master] Restart already in progress'); + logger.info('[Master] Restart already in progress'); return { success: false, message: 'Restart already in progress' }; } @@ -146,7 +147,7 @@ async function restartWorker() { workerStatus.restartCount++; workerStatus.lastRestartTime = new Date().toISOString(); - console.log('[Master] Restarting worker process...'); + logger.info('[Master] Restarting worker process...'); try { await stopWorker(true); @@ -165,7 +166,7 @@ async function restartWorker() { }; } catch (error) { workerStatus.isRestarting = false; - console.error('[Master] Failed to restart worker:', error.message); + logger.error('[Master] Failed to restart worker:', error.message); return { success: false, message: 'Failed to restart worker: ' + error.message @@ -178,12 +179,12 @@ async function restartWorker() { */ function scheduleRestart() { if (workerStatus.restartCount >= config.maxRestartAttempts) { - console.error('[Master] Max restart attempts reached, giving up'); + logger.error('[Master] Max restart attempts reached, giving up'); return; } const delay = Math.min(config.restartDelay * Math.pow(2, workerStatus.restartCount), 30000); - console.log(`[Master] Scheduling restart in ${delay}ms...`); + logger.info(`[Master] Scheduling restart in ${delay}ms...`); setTimeout(() => { restartWorker(); @@ -199,17 +200,17 @@ function handleWorkerMessage(message) { switch (message.type) { case 'ready': - console.log('[Master] Worker is ready'); + logger.info('[Master] Worker is ready'); break; case 'restart_request': - console.log('[Master] Worker requested restart'); + logger.info('[Master] Worker requested restart'); restartWorker(); break; case 'status': - console.log('[Master] Worker status:', message.data); + logger.info('[Master] Worker status:', message.data); break; default: - console.log('[Master] Unknown message type:', message.type); + logger.info('[Master] Unknown message type:', message.type); } } @@ -264,7 +265,7 @@ function createMasterServer() { // 重启端点 if (method === 'POST' && path === '/master/restart') { - console.log('[Master] Restart requested via API'); + logger.info('[Master] Restart requested via API'); const result = await restartWorker(); res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result)); @@ -273,7 +274,7 @@ function createMasterServer() { // 停止端点 if (method === 'POST' && path === '/master/stop') { - console.log('[Master] Stop requested via API'); + logger.info('[Master] Stop requested via API'); await stopWorker(true); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Worker stopped' })); @@ -282,7 +283,7 @@ function createMasterServer() { // 启动端点 if (method === 'POST' && path === '/master/start') { - console.log('[Master] Start requested via API'); + logger.info('[Master] Start requested via API'); if (workerProcess) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Worker already running' })); @@ -311,13 +312,13 @@ function createMasterServer() { }); server.listen(config.masterPort, () => { - console.log(`[Master] Management server listening on port ${config.masterPort}`); - console.log(`[Master] Available endpoints:`); - console.log(` GET /master/status - Get master and worker status`); - console.log(` GET /master/health - Health check`); - console.log(` POST /master/restart - Restart worker process`); - console.log(` POST /master/stop - Stop worker process`); - console.log(` POST /master/start - Start worker process`); + logger.info(`[Master] Management server listening on port ${config.masterPort}`); + logger.info(`[Master] Available endpoints:`); + logger.info(` GET /master/status - Get master and worker status`); + logger.info(` GET /master/health - Health check`); + logger.info(` POST /master/restart - Restart worker process`); + logger.info(` POST /master/stop - Stop worker process`); + logger.info(` POST /master/start - Start worker process`); }); return server; @@ -329,24 +330,24 @@ function createMasterServer() { function setupSignalHandlers() { // 优雅关闭 process.on('SIGTERM', async () => { - console.log('[Master] Received SIGTERM, shutting down...'); + logger.info('[Master] Received SIGTERM, shutting down...'); await stopWorker(true); process.exit(0); }); process.on('SIGINT', async () => { - console.log('[Master] Received SIGINT, shutting down...'); + logger.info('[Master] Received SIGINT, shutting down...'); await stopWorker(true); process.exit(0); }); // 未捕获的异常 process.on('uncaughtException', (error) => { - console.error('[Master] Uncaught exception:', error); + logger.error('[Master] Uncaught exception:', error); }); process.on('unhandledRejection', (reason, promise) => { - console.error('[Master] Unhandled rejection at:', promise, 'reason:', reason); + logger.error('[Master] Unhandled rejection at:', promise, 'reason:', reason); }); } @@ -354,12 +355,12 @@ function setupSignalHandlers() { * 主函数 */ async function main() { - console.log('='.repeat(50)); - console.log('[Master] AIClient2API Master Process'); - console.log('[Master] PID:', process.pid); - console.log('[Master] Node version:', process.version); - console.log('[Master] Working directory:', process.cwd()); - console.log('='.repeat(50)); + logger.info('='.repeat(50)); + logger.info('[Master] AIClient2API Master Process'); + logger.info('[Master] PID:', process.pid); + logger.info('[Master] Node version:', process.version); + logger.info('[Master] Working directory:', process.cwd()); + logger.info('='.repeat(50)); // 设置信号处理 setupSignalHandlers(); @@ -373,6 +374,6 @@ async function main() { // 启动主进程 main().catch(error => { - console.error('[Master] Failed to start:', error); + logger.error('[Master] Failed to start:', error); process.exit(1); }); \ No newline at end of file diff --git a/src/core/plugin-manager.js b/src/core/plugin-manager.js index 264c057..b8108b1 100644 --- a/src/core/plugin-manager.js +++ b/src/core/plugin-manager.js @@ -9,6 +9,7 @@ */ import { promises as fs } from 'fs'; +import logger from '../utils/logger.js'; import { existsSync } from 'fs'; import path from 'path'; @@ -72,7 +73,7 @@ class PluginManager { await this.saveConfig(); } } catch (error) { - console.error('[PluginManager] Failed to load config:', error.message); + logger.error('[PluginManager] Failed to load config:', error.message); this.pluginsConfig = { plugins: {} }; } } @@ -108,7 +109,7 @@ class PluginManager { enabled: true, description: plugin.description || '' }; - console.log(`[PluginManager] Found plugin for default config: ${plugin.name}`); + logger.info(`[PluginManager] Found plugin for default config: ${plugin.name}`); } } catch (importError) { // 如果导入失败,使用目录名作为插件名 @@ -116,11 +117,11 @@ class PluginManager { enabled: true, description: '' }; - console.warn(`[PluginManager] Could not import plugin ${entry.name}, using directory name:`, importError.message); + logger.warn(`[PluginManager] Could not import plugin ${entry.name}, using directory name:`, importError.message); } } } catch (error) { - console.error('[PluginManager] Failed to scan plugins directory:', error.message); + logger.error('[PluginManager] Failed to scan plugins directory:', error.message); } return defaultConfig; @@ -137,7 +138,7 @@ class PluginManager { } await fs.writeFile(PLUGINS_CONFIG_FILE, JSON.stringify(this.pluginsConfig, null, 2), 'utf8'); } catch (error) { - console.error('[PluginManager] Failed to save config:', error.message); + logger.error('[PluginManager] Failed to save config:', error.message); } } @@ -150,11 +151,11 @@ class PluginManager { throw new Error('Plugin must have a name'); } if (this.plugins.has(plugin.name)) { - console.warn(`[PluginManager] Plugin "${plugin.name}" is already registered, skipping`); + logger.warn(`[PluginManager] Plugin "${plugin.name}" is already registered, skipping`); return; } this.plugins.set(plugin.name, plugin); - console.log(`[PluginManager] Registered plugin: ${plugin.name} v${plugin.version || '1.0.0'}`); + logger.info(`[PluginManager] Registered plugin: ${plugin.name} v${plugin.version || '1.0.0'}`); } /** @@ -169,18 +170,18 @@ class PluginManager { const enabled = pluginConfig.enabled !== false; // 默认启用 if (!enabled) { - console.log(`[PluginManager] Plugin "${name}" is disabled, skipping init`); + logger.info(`[PluginManager] Plugin "${name}" is disabled, skipping init`); continue; } try { if (typeof plugin.init === 'function') { await plugin.init(config); - console.log(`[PluginManager] Initialized plugin: ${name}`); + logger.info(`[PluginManager] Initialized plugin: ${name}`); } plugin._enabled = true; } catch (error) { - console.error(`[PluginManager] Failed to init plugin "${name}":`, error.message); + logger.error(`[PluginManager] Failed to init plugin "${name}":`, error.message); plugin._enabled = false; } } @@ -198,10 +199,10 @@ class PluginManager { try { if (typeof plugin.destroy === 'function') { await plugin.destroy(); - console.log(`[PluginManager] Destroyed plugin: ${name}`); + logger.info(`[PluginManager] Destroyed plugin: ${name}`); } } catch (error) { - console.error(`[PluginManager] Failed to destroy plugin "${name}":`, error.message); + logger.error(`[PluginManager] Failed to destroy plugin "${name}":`, error.message); } } this.initialized = false; @@ -303,7 +304,7 @@ class PluginManager { // authorized === null 表示此插件不处理,继续下一个 } catch (error) { - console.error(`[PluginManager] Auth error in plugin "${plugin.name}":`, error.message); + logger.error(`[PluginManager] Auth error in plugin "${plugin.name}":`, error.message); } } @@ -345,7 +346,7 @@ class PluginManager { Object.assign(config, result.data); } } catch (error) { - console.error(`[PluginManager] Middleware error in plugin "${plugin.name}":`, error.message); + logger.error(`[PluginManager] Middleware error in plugin "${plugin.name}":`, error.message); } } @@ -380,7 +381,7 @@ class PluginManager { const handled = await route.handler(method, path, req, res); if (handled) return true; } catch (error) { - console.error(`[PluginManager] Route error in plugin "${plugin.name}":`, error.message); + logger.error(`[PluginManager] Route error in plugin "${plugin.name}":`, error.message); } } } @@ -424,7 +425,7 @@ class PluginManager { try { await plugin.hooks[hookName](...args); } catch (error) { - console.error(`[PluginManager] Hook "${hookName}" error in plugin "${plugin.name}":`, error.message); + logger.error(`[PluginManager] Hook "${hookName}" error in plugin "${plugin.name}":`, error.message); } } } @@ -482,7 +483,7 @@ export async function discoverPlugins() { try { if (!existsSync(pluginsDir)) { await fs.mkdir(pluginsDir, { recursive: true }); - console.log('[PluginManager] Created plugins directory'); + logger.info('[PluginManager] Created plugins directory'); } const entries = await fs.readdir(pluginsDir, { withFileTypes: true }); @@ -502,11 +503,11 @@ export async function discoverPlugins() { pluginManager.register(plugin); } } catch (error) { - console.error(`[PluginManager] Failed to load plugin from ${entry.name}:`, error.message); + logger.error(`[PluginManager] Failed to load plugin from ${entry.name}:`, error.message); } } } catch (error) { - console.error('[PluginManager] Failed to discover plugins:', error.message); + logger.error('[PluginManager] Failed to discover plugins:', error.message); } } diff --git a/src/handlers/ollama-handler.js b/src/handlers/ollama-handler.js index f154a51..2a1abfb 100644 --- a/src/handlers/ollama-handler.js +++ b/src/handlers/ollama-handler.js @@ -4,10 +4,10 @@ */ import { getRequestBody, handleError, MODEL_PROTOCOL_PREFIX, MODEL_PROVIDER, getProtocolPrefix } from '../utils/common.js'; +import logger from '../utils/logger.js'; import { convertData } from '../convert/convert.js'; import { ConverterFactory } from '../converters/ConverterFactory.js'; import { getProviderModels } from '../providers/provider-models.js'; - // Ollama版本号 /** * Model name prefix mapping for different providers @@ -133,7 +133,7 @@ export function getProviderByModelName(modelName, providerPoolManager, defaultPr // First, check if model name has a prefix that directly indicates the provider const providerFromPrefix = getProviderFromPrefix(modelName); if (providerFromPrefix) { - console.log(`[Provider Selection] Provider determined from prefix: ${providerFromPrefix}`); + logger.info(`[Provider Selection] Provider determined from prefix: ${providerFromPrefix}`); return providerFromPrefix; } @@ -222,17 +222,17 @@ export function getProviderForModel(modelName, defaultProvider) { // Remove prefix for further analysis const cleanModelName = removeModelPrefix(modelName); - console.log(`[Provider Selection] Clean model name: ${cleanModelName}`); + logger.info(`[Provider Selection] Clean model name: ${cleanModelName}`); // Try to find the provider by checking if the model is in the provider's model list // This handles cases where different providers have the same model name const providerType = findProviderByModelName(cleanModelName); - console.log(`[Provider Selection] Provider determined from model list: ${providerType}`); + logger.info(`[Provider Selection] Provider determined from model list: ${providerType}`); if (providerType) { return providerType; } - console.log(`[Provider Selection] Model name not found in provider models. Using default provider: ${defaultProvider}`); + logger.info(`[Provider Selection] Model name not found in provider models. Using default provider: ${defaultProvider}`); // Default to the provided default provider return defaultProvider; } @@ -385,7 +385,7 @@ export async function handleOllamaEndpointsAfterAuth(method, path, req, res, api */ export async function handleOllamaTags(req, res, apiService, currentConfig, providerPoolManager) { try { - console.log('[Ollama] Handling /api/tags request'); + logger.info('[Ollama] Handling /api/tags request'); const ollamaConverter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OLLAMA); const { getServiceAdapter } = await import('../providers/adapter.js'); @@ -402,7 +402,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov } return []; } catch (error) { - console.error(`[Ollama] Error from ${providerType}:`, error.message); + logger.error(`[Ollama] Error from ${providerType}:`, error.message); return []; } }; @@ -435,7 +435,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov // If no providers available, return empty list if (fetchPromises.length === 0) { - console.warn('[Ollama] No healthy providers available to fetch models'); + logger.warn('[Ollama] No healthy providers available to fetch models'); const response = { models: [] }; res.writeHead(200, { 'Content-Type': 'application/json', @@ -450,7 +450,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov const results = await Promise.all(fetchPromises); const allModels = results.flat(); - console.log(`[Ollama] Fetched ${allModels.length} models from ${processedProviderTypes.size} provider(s)`); + logger.info(`[Ollama] Fetched ${allModels.length} models from ${processedProviderTypes.size} provider(s)`); const response = { models: allModels }; @@ -461,7 +461,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov }); res.end(JSON.stringify(response)); } catch (error) { - console.error('[Ollama Tags Error]', error); + logger.error('[Ollama Tags Error]', error); handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA); } } @@ -471,7 +471,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov */ export async function handleOllamaShow(req, res) { try { - // console.log('[Ollama] Handling /api/show request'); + // logger.info('[Ollama] Handling /api/show request'); const body = await getRequestBody(req); const modelName = body.name || body.model || 'unknown'; @@ -486,7 +486,7 @@ export async function handleOllamaShow(req, res) { }); res.end(JSON.stringify(showResponse)); } catch (error) { - console.error('[Ollama Show Error]', error); + logger.error('[Ollama Show Error]', error); handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA); } } @@ -505,7 +505,7 @@ export function handleOllamaVersion(res) { }); res.end(JSON.stringify(response)); } catch (error) { - console.error('[Ollama Version Error]', error); + logger.error('[Ollama Version Error]', error); handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA); } } @@ -516,7 +516,7 @@ export function handleOllamaVersion(res) { */ export async function handleOllamaChat(req, res, apiService, currentConfig, providerPoolManager) { try { - console.log('[Ollama] Handling /api/chat request'); + logger.info('[Ollama] Handling /api/chat request'); const ollamaRequest = await getRequestBody(req); const { getServiceAdapter } = await import('../providers/adapter.js'); @@ -527,7 +527,7 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov ollamaRequest.model = modelName; // Use clean model name const detectedProvider = getProviderForModel(rawModelName, currentConfig.MODEL_PROVIDER); - console.log(`[Ollama] Model: ${modelName}, Detected provider: ${detectedProvider}`); + logger.info(`[Ollama] Model: ${modelName}, Detected provider: ${detectedProvider}`); // Get the appropriate service based on detected provider let actualApiService = apiService; @@ -545,10 +545,10 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov MODEL_PROVIDER: detectedProvider }; actualApiService = getServiceAdapter(actualConfig); - console.log(`[Ollama] Using provider from pool: ${detectedProvider}`); + logger.info(`[Ollama] Using provider from pool: ${detectedProvider}`); } else { // No healthy provider in pool, try to create service directly - console.warn(`[Ollama] No healthy provider found for ${detectedProvider} in pool`); + logger.warn(`[Ollama] No healthy provider found for ${detectedProvider} in pool`); if (!apiService) { throw new Error(`No healthy provider available for ${detectedProvider}`); } @@ -557,7 +557,7 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov // No pool manager and no apiService, try to create service directly actualConfig = { ...currentConfig, MODEL_PROVIDER: detectedProvider }; actualApiService = getServiceAdapter(actualConfig); - console.log(`[Ollama] Created service adapter for: ${detectedProvider}`); + logger.info(`[Ollama] Created service adapter for: ${detectedProvider}`); } } @@ -591,7 +591,7 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov const ollamaChunk = ollamaConverter.convertStreamChunk(chunk, sourceProtocol, ollamaRequest.model, false); res.write(JSON.stringify(ollamaChunk) + '\n'); } catch (chunkError) { - console.error('[Ollama] Error processing chunk:', chunkError); + logger.error('[Ollama] Error processing chunk:', chunkError); } } @@ -612,7 +612,7 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov res.end(JSON.stringify(ollamaResponse)); } } catch (error) { - console.error('[Ollama Chat Error]', error); + logger.error('[Ollama Chat Error]', error); handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA); } } @@ -623,7 +623,7 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov */ export async function handleOllamaGenerate(req, res, apiService, currentConfig, providerPoolManager) { try { - console.log('[Ollama] Handling /api/generate request'); + logger.info('[Ollama] Handling /api/generate request'); const ollamaRequest = await getRequestBody(req); const { getServiceAdapter } = await import('../providers/adapter.js'); @@ -634,7 +634,7 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig, ollamaRequest.model = modelName; // Use clean model name const detectedProvider = getProviderForModel(rawModelName, currentConfig.MODEL_PROVIDER); - console.log(`[Ollama] Model: ${modelName}, Detected provider: ${detectedProvider}`); + logger.info(`[Ollama] Model: ${modelName}, Detected provider: ${detectedProvider}`); // Get the appropriate service based on detected provider let actualApiService = apiService; @@ -652,10 +652,10 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig, MODEL_PROVIDER: detectedProvider }; actualApiService = getServiceAdapter(actualConfig); - console.log(`[Ollama] Using provider from pool: ${detectedProvider}`); + logger.info(`[Ollama] Using provider from pool: ${detectedProvider}`); } else { // No healthy provider in pool, try to create service directly - console.warn(`[Ollama] No healthy provider found for ${detectedProvider} in pool`); + logger.warn(`[Ollama] No healthy provider found for ${detectedProvider} in pool`); if (!apiService) { throw new Error(`No healthy provider available for ${detectedProvider}`); } @@ -664,7 +664,7 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig, // No pool manager and no apiService, try to create service directly actualConfig = { ...currentConfig, MODEL_PROVIDER: detectedProvider }; actualApiService = getServiceAdapter(actualConfig); - console.log(`[Ollama] Created service adapter for: ${detectedProvider}`); + logger.info(`[Ollama] Created service adapter for: ${detectedProvider}`); } } @@ -698,7 +698,7 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig, const ollamaChunk = ollamaConverter.toOllamaGenerateStreamChunk(chunk, ollamaRequest.model, false); res.write(JSON.stringify(ollamaChunk) + '\n'); } catch (chunkError) { - console.error('[Ollama] Error processing chunk:', chunkError); + logger.error('[Ollama] Error processing chunk:', chunkError); } } @@ -719,7 +719,7 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig, res.end(JSON.stringify(ollamaResponse)); } } catch (error) { - console.error('[Ollama Generate Error]', error); + logger.error('[Ollama Generate Error]', error); handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA); } } diff --git a/src/handlers/request-handler.js b/src/handlers/request-handler.js index e90a452..7689176 100644 --- a/src/handlers/request-handler.js +++ b/src/handlers/request-handler.js @@ -1,4 +1,5 @@ import deepmerge from 'deepmerge'; +import logger from '../utils/logger.js'; import { handleError } from '../utils/common.js'; import { handleUIApiRequests, serveStaticFiles } from '../services/ui-manager.js'; import { handleAPIRequests } from '../services/api-manager.js'; @@ -8,7 +9,14 @@ import { MODEL_PROVIDER } from '../utils/common.js'; import { PROMPT_LOG_FILENAME } from '../core/config-manager.js'; import { handleOllamaRequest, handleOllamaShow } from './ollama-handler.js'; import { getPluginManager } from '../core/plugin-manager.js'; +import { randomUUID } from 'crypto'; +/** + * Generate a short unique request ID (8 characters) + */ +function generateRequestId() { + return randomUUID().slice(0, 8); +} /** * Parse request body as JSON */ @@ -36,6 +44,10 @@ function parseRequestBody(req) { */ export function createRequestHandler(config, providerPoolManager) { return async function requestHandler(req, res) { + // Generate unique request ID and set it in logger context + const requestId = generateRequestId(); + logger.setRequestContext(requestId); + // Deep copy the config for each request to allow dynamic modification const currentConfig = deepmerge({}, config); const requestUrl = new URL(req.url, `http://${req.headers.host}`); @@ -77,8 +89,8 @@ export function createRequestHandler(config, providerPoolManager) { return true; } - console.log(`\n${new Date().toLocaleString()}`); - console.log(`[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`); + // logger.info(`\n${new Date().toLocaleString()}`); + logger.info(`[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`); // Health check endpoint if (method === 'GET' && path === '/health') { @@ -118,7 +130,7 @@ export function createRequestHandler(config, providerPoolManager) { })); return true; } catch (error) { - console.log(`[Server] req provider_health error: ${error.message}`); + logger.info(`[Server] req provider_health error: ${error.message}`); handleError(res, { statusCode: 500, message: `Failed to get providers health: ${error.message}` }, currentConfig.MODEL_PROVIDER); return; } @@ -130,7 +142,7 @@ export function createRequestHandler(config, providerPoolManager) { const modelProviderHeader = req.headers['model-provider']; if (modelProviderHeader) { currentConfig.MODEL_PROVIDER = modelProviderHeader; - console.log(`[Config] MODEL_PROVIDER overridden by header to: ${currentConfig.MODEL_PROVIDER}`); + logger.info(`[Config] MODEL_PROVIDER overridden by header to: ${currentConfig.MODEL_PROVIDER}`); } // Check if the first path segment matches a MODEL_PROVIDER and switch if it does @@ -143,12 +155,12 @@ export function createRequestHandler(config, providerPoolManager) { const isValidProvider = Object.values(MODEL_PROVIDER).includes(firstSegment); if (firstSegment && isValidProvider) { currentConfig.MODEL_PROVIDER = firstSegment; - console.log(`[Config] MODEL_PROVIDER overridden by path segment to: ${currentConfig.MODEL_PROVIDER}`); + logger.info(`[Config] MODEL_PROVIDER overridden by path segment to: ${currentConfig.MODEL_PROVIDER}`); pathSegments.shift(); path = '/' + pathSegments.join('/'); requestUrl.pathname = path; } else if (firstSegment && !isValidProvider) { - console.log(`[Config] Ignoring invalid MODEL_PROVIDER in path segment: ${firstSegment}`); + logger.info(`[Config] Ignoring invalid MODEL_PROVIDER in path segment: ${firstSegment}`); } } @@ -200,7 +212,7 @@ export function createRequestHandler(config, providerPoolManager) { if (path.includes('/count_tokens') && method === 'POST') { try { const body = await parseRequestBody(req); - console.log(`[Server] Handling count_tokens request for model: ${body.model}`); + logger.info(`[Server] Handling count_tokens request for model: ${body.model}`); // Check if apiService has countTokens method if (apiService && typeof apiService.countTokens === 'function') { @@ -221,7 +233,7 @@ export function createRequestHandler(config, providerPoolManager) { } return true; } catch (error) { - console.error(`[Server] count_tokens error: ${error.message}`); + logger.error(`[Server] count_tokens error: ${error.message}`); handleError(res, { statusCode: 500, message: `Failed to count tokens: ${error.message}` }, currentConfig.MODEL_PROVIDER); return; } @@ -237,6 +249,9 @@ export function createRequestHandler(config, providerPoolManager) { res.end(JSON.stringify({ error: { message: 'Not Found' } })); } catch (error) { handleError(res, error, currentConfig.MODEL_PROVIDER); + } finally { + // Clear request context after request is complete + logger.clearRequestContext(requestId); } }; } diff --git a/src/plugins/api-potluck/api-routes.js b/src/plugins/api-potluck/api-routes.js index d9b924f..7703e69 100644 --- a/src/plugins/api-potluck/api-routes.js +++ b/src/plugins/api-potluck/api-routes.js @@ -33,6 +33,7 @@ import { getAllUserApiKeys } from './user-data-manager.js'; import path from 'path'; +import logger from '../../utils/logger.js'; import { existsSync } from 'fs'; import { promises as fs } from 'fs'; import multer from 'multer'; @@ -112,7 +113,7 @@ async function checkAdminAuth(req) { return true; } catch (error) { - console.error('[API Potluck] Auth check error:', error.message); + logger.error('[API Potluck] Auth check error:', error.message); return false; } } @@ -130,7 +131,7 @@ export async function handlePotluckApiRoutes(method, path, req, res) { if (!path.startsWith('/api/potluck')) { return false; } - console.log('[API Potluck] Handling request:', method, path); + logger.info('[API Potluck] Handling request:', method, path); // 验证管理员权限 const isAuthed = await checkAdminAuth(req); @@ -252,7 +253,7 @@ export async function handlePotluckApiRoutes(method, path, req, res) { totalBonusUpdated++; } } catch (error) { - console.warn(`[API Potluck] Failed to sync bonus for ${apiKey.substring(0, 12)}...:`, error.message); + logger.warn(`[API Potluck] Failed to sync bonus for ${apiKey.substring(0, 12)}...:`, error.message); } } @@ -412,7 +413,7 @@ export async function handlePotluckApiRoutes(method, path, req, res) { return true; } catch (error) { - console.error('[API Potluck] API error:', error); + logger.error('[API Potluck] API error:', error); sendJson(res, 500, { success: false, error: { message: error.message || 'Internal server error' } @@ -458,7 +459,7 @@ export async function handlePotluckUserApiRoutes(method, path, req, res) { if (!path.startsWith('/api/potluckuser')) { return false; } - console.log('[API Potluck User] Handling request:', method, path); + logger.info('[API Potluck User] Handling request:', method, path); try { // 从请求中提取 API Key @@ -662,7 +663,7 @@ export async function handlePotluckUserApiRoutes(method, path, req, res) { return true; } catch (error) { - console.error('[API Potluck] User API error:', error); + logger.error('[API Potluck] User API error:', error); sendJson(res, 500, { success: false, error: { message: error.message || 'Internal server error' } @@ -732,7 +733,7 @@ async function handleUserUpload(req, res, apiKey) { return new Promise((resolve) => { userUpload.single('file')(req, res, async (err) => { if (err) { - console.error('[API Potluck User] File upload error:', err.message); + logger.error('[API Potluck User] File upload error:', err.message); sendJson(res, 400, { success: false, error: err.message }); resolve(true); return; @@ -782,10 +783,10 @@ async function handleUserUpload(req, res, apiKey) { try { await autoLinkProviderConfigs(CONFIG); } catch (linkError) { - console.warn('[API Potluck User] Auto-link failed:', linkError.message); + logger.warn('[API Potluck User] Auto-link failed:', linkError.message); } - console.log(`[API Potluck User] File uploaded, linked and health checked: ${relativePath} (provider: ${providerType}, health: ${healthResult.message})`); + logger.info(`[API Potluck User] File uploaded, linked and health checked: ${relativePath} (provider: ${providerType}, health: ${healthResult.message})`); sendJson(res, 200, { success: true, @@ -798,7 +799,7 @@ async function handleUserUpload(req, res, apiKey) { resolve(true); } catch (error) { - console.error('[API Potluck User] File processing error:', error); + logger.error('[API Potluck User] File processing error:', error); sendJson(res, 500, { success: false, error: error.message }); resolve(true); } @@ -825,7 +826,7 @@ async function handleKiroBatchImportTokens(req, res, apiKey) { return true; } - console.log(`[API Potluck User] Starting batch import of ${refreshTokens.length} tokens (user: ${apiKey.substring(0, 12)}...)`); + logger.info(`[API Potluck User] Starting batch import of ${refreshTokens.length} tokens (user: ${apiKey.substring(0, 12)}...)`); // 设置 SSE 响应头 res.writeHead(200, { @@ -864,15 +865,15 @@ async function handleKiroBatchImportTokens(req, res, apiKey) { // 自动从主服务同步健康状态 await syncCredentialHealthFromPool(apiKey, credential); - console.log(`[API Potluck User] Credential linked and health synced: ${credentialInfo.path}`); + logger.info(`[API Potluck User] Credential linked and health synced: ${credentialInfo.path}`); } catch (linkError) { - console.warn('[API Potluck User] Failed to link/check credential:', linkError.message); + logger.warn('[API Potluck User] Failed to link/check credential:', linkError.message); } } } ); - console.log(`[API Potluck User] Completed: ${result.success} success, ${result.failed} failed`); + logger.info(`[API Potluck User] Completed: ${result.success} success, ${result.failed} failed`); // 发送完成事件 sendSSE('complete', { @@ -887,7 +888,7 @@ async function handleKiroBatchImportTokens(req, res, apiKey) { return true; } catch (error) { - console.error('[API Potluck User] Kiro Batch Import Error:', error); + logger.error('[API Potluck User] Kiro Batch Import Error:', error); if (res.headersSent) { res.write(`event: error\n`); res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); @@ -936,12 +937,12 @@ async function handleKiroImportAwsCredentials(req, res, apiKey) { return true; } - console.log(`[API Potluck User] Starting AWS credentials import (user: ${apiKey.substring(0, 12)}...)`); + logger.info(`[API Potluck User] Starting AWS credentials import (user: ${apiKey.substring(0, 12)}...)`); const result = await importAwsCredentials(credentials); if (result.success) { - console.log(`[API Potluck User] Successfully imported credentials to: ${result.path}`); + logger.info(`[API Potluck User] Successfully imported credentials to: ${result.path}`); // 将凭据路径关联到用户 const credentialInfo = { @@ -953,7 +954,7 @@ async function handleKiroImportAwsCredentials(req, res, apiKey) { // 自动从主服务同步健康状态 const healthResult = await syncCredentialHealthFromPool(apiKey, credential); - console.log(`[API Potluck User] Health sync result: ${healthResult.message}`); + logger.info(`[API Potluck User] Health sync result: ${healthResult.message}`); sendJson(res, 200, { success: true, @@ -972,7 +973,7 @@ async function handleKiroImportAwsCredentials(req, res, apiKey) { return true; } catch (error) { - console.error('[API Potluck User] Kiro AWS Import Error:', error); + logger.error('[API Potluck User] Kiro AWS Import Error:', error); sendJson(res, 500, { success: false, error: error.message @@ -1070,7 +1071,7 @@ async function handleCredentialHealthCheck(req, res, apiKey, credentialId) { return true; } - console.log(`[API Potluck User] Syncing health for credential: ${credential.path}`); + logger.info(`[API Potluck User] Syncing health for credential: ${credential.path}`); const result = await syncCredentialHealthFromPool(apiKey, credential); @@ -1081,7 +1082,7 @@ async function handleCredentialHealthCheck(req, res, apiKey, credentialId) { return true; } catch (error) { - console.error('[API Potluck User] Health check error:', error); + logger.error('[API Potluck User] Health check error:', error); sendJson(res, 500, { success: false, error: error.message @@ -1117,7 +1118,7 @@ async function checkAllCredentialsHealth() { } // isHealthy === null 表示未注册到服务,不计入健康/不健康 } catch (error) { - console.warn(`[API Potluck] Health sync failed for ${credential.path}:`, error.message); + logger.warn(`[API Potluck] Health sync failed for ${credential.path}:`, error.message); } } } @@ -1171,19 +1172,19 @@ export function startHealthCheckScheduler() { // 启动后延迟 30 秒执行第一次同步 setTimeout(async () => { - console.log('[API Potluck] Running initial health sync from pool...'); + logger.info('[API Potluck] Running initial health sync from pool...'); const result = await checkAllCredentialsHealth(); - console.log(`[API Potluck] Health sync complete: ${result.healthy}/${result.total} healthy`); + logger.info(`[API Potluck] Health sync complete: ${result.healthy}/${result.total} healthy`); }, 30000); // 定时同步 healthCheckTimer = setInterval(async () => { - console.log('[API Potluck] Running scheduled health sync from pool...'); + logger.info('[API Potluck] Running scheduled health sync from pool...'); const result = await checkAllCredentialsHealth(); - console.log(`[API Potluck] Health sync complete: ${result.healthy}/${result.total} healthy`); + logger.info(`[API Potluck] Health sync complete: ${result.healthy}/${result.total} healthy`); }, HEALTH_CHECK_INTERVAL); - console.log(`[API Potluck] Health sync scheduler started (interval: ${HEALTH_CHECK_INTERVAL / 1000}s)`); + logger.info(`[API Potluck] Health sync scheduler started (interval: ${HEALTH_CHECK_INTERVAL / 1000}s)`); } /** @@ -1193,7 +1194,7 @@ export function stopHealthCheckScheduler() { if (healthCheckTimer) { clearInterval(healthCheckTimer); healthCheckTimer = null; - console.log('[API Potluck] Health sync scheduler stopped'); + logger.info('[API Potluck] Health sync scheduler stopped'); } } diff --git a/src/plugins/api-potluck/index.js b/src/plugins/api-potluck/index.js index 35ae1cf..43c7ace 100644 --- a/src/plugins/api-potluck/index.js +++ b/src/plugins/api-potluck/index.js @@ -31,6 +31,7 @@ import { } from './middleware.js'; import { consumeBonus, getConfig } from './user-data-manager.js'; +import logger from '../../utils/logger.js'; import { handlePotluckApiRoutes, handlePotluckUserApiRoutes, startHealthCheckScheduler, stopHealthCheckScheduler } from './api-routes.js'; @@ -53,7 +54,7 @@ const apiPotluckPlugin = { * @param {Object} config - 服务器配置 */ async init(config) { - console.log('[API Potluck Plugin] Initializing...'); + logger.info('[API Potluck Plugin] Initializing...'); // 注入配置获取函数 setConfigGetter(getConfig); // 启动定时健康检查 @@ -64,7 +65,7 @@ const apiPotluckPlugin = { * 销毁钩子 */ async destroy() { - console.log('[API Potluck Plugin] Destroying...'); + logger.info('[API Potluck Plugin] Destroying...'); // 停止定时健康检查 stopHealthCheckScheduler(); }, @@ -137,7 +138,7 @@ const apiPotluckPlugin = { } // 认证成功,返回数据供后续使用 - console.log(`[API Potluck Plugin] Authorized with key: ${apiKey.substring(0, 12)}...`); + logger.info(`[API Potluck Plugin] Authorized with key: ${apiKey.substring(0, 12)}...`); return { handled: false, authorized: true, @@ -165,7 +166,7 @@ const apiPotluckPlugin = { }); } catch (e) { // 静默失败,不影响主流程 - console.error('[API Potluck Plugin] Failed to record usage:', e.message); + logger.error('[API Potluck Plugin] Failed to record usage:', e.message); } } } diff --git a/src/plugins/api-potluck/key-manager.js b/src/plugins/api-potluck/key-manager.js index c72af50..d91abe0 100644 --- a/src/plugins/api-potluck/key-manager.js +++ b/src/plugins/api-potluck/key-manager.js @@ -4,6 +4,7 @@ */ import { promises as fs } from 'fs'; +import logger from '../../utils/logger.js'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import path from 'path'; import crypto from 'crypto'; @@ -64,7 +65,7 @@ function ensureLoaded() { } } if (needsMigration) { - console.log('[API Potluck] Migrated legacy keys: added bonusRemaining field'); + logger.info('[API Potluck] Migrated legacy keys: added bonusRemaining field'); markDirty(); } } else { @@ -72,7 +73,7 @@ function ensureLoaded() { syncWriteToFile(); } } catch (error) { - console.error('[API Potluck] Failed to load key store:', error.message); + logger.error('[API Potluck] Failed to load key store:', error.message); keyStore = { keys: {} }; } @@ -101,7 +102,7 @@ function syncWriteToFile() { } writeFileSync(KEYS_STORE_FILE, JSON.stringify(keyStore, null, 2), 'utf8'); } catch (error) { - console.error('[API Potluck] Sync write failed:', error.message); + logger.error('[API Potluck] Sync write failed:', error.message); } } @@ -122,7 +123,7 @@ async function persistIfDirty() { await fs.rename(tempFile, KEYS_STORE_FILE); isDirty = false; } catch (error) { - console.error('[API Potluck] Persist failed:', error.message); + logger.error('[API Potluck] Persist failed:', error.message); } finally { isWriting = false; } @@ -206,7 +207,7 @@ export async function createKey(name = '', dailyLimit = null) { markDirty(); await persistIfDirty(); // 创建操作立即持久化 - console.log(`[API Potluck] Created key: ${apiKey.substring(0, 12)}...`); + logger.info(`[API Potluck] Created key: ${apiKey.substring(0, 12)}...`); return keyData; } @@ -245,7 +246,7 @@ export async function deleteKey(keyId) { delete keyStore.keys[keyId]; markDirty(); await persistIfDirty(); // 删除操作立即持久化 - console.log(`[API Potluck] Deleted key: ${keyId.substring(0, 12)}...`); + logger.info(`[API Potluck] Deleted key: ${keyId.substring(0, 12)}...`); return true; } @@ -322,7 +323,7 @@ export async function regenerateKey(oldKeyId) { markDirty(); await persistIfDirty(); // 立即持久化 - console.log(`[API Potluck] Regenerated key: ${oldKeyId.substring(0, 12)}... -> ${newKeyId.substring(0, 12)}...`); + logger.info(`[API Potluck] Regenerated key: ${oldKeyId.substring(0, 12)}... -> ${newKeyId.substring(0, 12)}...`); return { oldKey: oldKeyId, @@ -491,7 +492,7 @@ export async function applyDailyLimitToAllKeys(newLimit) { await persistIfDirty(); } - console.log(`[API Potluck] Applied daily limit ${newLimit} to ${updated}/${keys.length} keys`); + logger.info(`[API Potluck] Applied daily limit ${newLimit} to ${updated}/${keys.length} keys`); return { total: keys.length, updated }; } diff --git a/src/plugins/api-potluck/user-data-manager.js b/src/plugins/api-potluck/user-data-manager.js index 4d330ac..d2ec4fe 100644 --- a/src/plugins/api-potluck/user-data-manager.js +++ b/src/plugins/api-potluck/user-data-manager.js @@ -5,6 +5,7 @@ */ import { promises as fs } from 'fs'; +import logger from '../../utils/logger.js'; import { existsSync, readFileSync, writeFileSync, watch } from 'fs'; import path from 'path'; @@ -126,7 +127,7 @@ export async function updateConfig(newConfig) { await persistIfDirty(); const updatedConfig = getFullConfig(); - console.log(`[API Potluck UserData] Config updated:`, updatedConfig); + logger.info(`[API Potluck UserData] Config updated:`, updatedConfig); return updatedConfig; } @@ -140,7 +141,7 @@ function updatePersistTimer(newInterval) { if (persistTimer) { clearInterval(persistTimer); persistTimer = setInterval(persistIfDirty, currentPersistInterval); - console.log(`[API Potluck UserData] Persist interval updated to ${currentPersistInterval}ms`); + logger.info(`[API Potluck UserData] Persist interval updated to ${currentPersistInterval}ms`); } } @@ -179,7 +180,7 @@ function ensureLoaded() { syncWriteToFile(); } } catch (error) { - console.error('[API Potluck UserData] Failed to load user data:', error.message); + logger.error('[API Potluck UserData] Failed to load user data:', error.message); userDataStore = { config: {}, users: {} }; } @@ -206,7 +207,7 @@ function syncWriteToFile() { } writeFileSync(USER_DATA_FILE, JSON.stringify(userDataStore, null, 2), 'utf8'); } catch (error) { - console.error('[API Potluck UserData] Sync write failed:', error.message); + logger.error('[API Potluck UserData] Sync write failed:', error.message); } } @@ -226,7 +227,7 @@ async function persistIfDirty() { await fs.rename(tempFile, USER_DATA_FILE); isDirty = false; } catch (error) { - console.error('[API Potluck UserData] Persist failed:', error.message); + logger.error('[API Potluck UserData] Persist failed:', error.message); } finally { isWriting = false; } @@ -268,17 +269,17 @@ function startFileWatcher() { // 检查配置是否有变化 if (JSON.stringify(oldConfig) !== JSON.stringify(newConfig)) { userDataStore.config = newConfig; - console.log('[API Potluck UserData] Config hot-reloaded:', getBonusConfig()); + logger.info('[API Potluck UserData] Config hot-reloaded:', getBonusConfig()); } } } catch (error) { - console.error('[API Potluck UserData] Hot-reload failed:', error.message); + logger.error('[API Potluck UserData] Hot-reload failed:', error.message); } }); - console.log('[API Potluck UserData] File watcher started for config hot-reload'); + logger.info('[API Potluck UserData] File watcher started for config hot-reload'); } catch (error) { - console.error('[API Potluck UserData] Failed to start file watcher:', error.message); + logger.error('[API Potluck UserData] Failed to start file watcher:', error.message); } } @@ -451,7 +452,7 @@ export async function migrateUserCredentials(oldApiKey, newApiKey) { markDirty(); await persistIfDirty(); - console.log(`[API Potluck UserData] Migrated credentials from ${oldApiKey.substring(0, 12)}... to ${newApiKey.substring(0, 12)}...`); + logger.info(`[API Potluck UserData] Migrated credentials from ${oldApiKey.substring(0, 12)}... to ${newApiKey.substring(0, 12)}...`); return true; } @@ -521,7 +522,7 @@ export async function addCredentialBonus(apiKey, credentialId) { userData.credentialBonuses.push(bonus); markDirty(); - console.log(`[API Potluck UserData] Added bonus for credential: ${credentialId}`); + logger.info(`[API Potluck UserData] Added bonus for credential: ${credentialId}`); return bonus; } @@ -542,7 +543,7 @@ export async function removeCredentialBonus(apiKey, credentialId) { userData.credentialBonuses.splice(index, 1); markDirty(); - console.log(`[API Potluck UserData] Removed bonus for credential: ${credentialId}`); + logger.info(`[API Potluck UserData] Removed bonus for credential: ${credentialId}`); return true; } @@ -643,7 +644,7 @@ export async function syncCredentialBonuses(apiKey, credentialsWithHealth) { usedCount: 0 }); added++; - console.log(`[API Potluck UserData] Created bonus for credential ${cred.id}, grantedAt: ${grantedAt}`); + logger.info(`[API Potluck UserData] Created bonus for credential ${cred.id}, grantedAt: ${grantedAt}`); } } diff --git a/src/plugins/default-auth/index.js b/src/plugins/default-auth/index.js index d95109c..26bce81 100644 --- a/src/plugins/default-auth/index.js +++ b/src/plugins/default-auth/index.js @@ -9,6 +9,8 @@ * 4. URL query: ?key= */ +import logger from '../../utils/logger.js'; + /** * 检查请求是否已授权 * @param {http.IncomingMessage} req - HTTP 请求 @@ -79,11 +81,13 @@ const defaultAuthPlugin = { } // 认证失败,记录日志但不发送响应(由 request-handler 统一处理) - console.log(`[Default Auth] Unauthorized request. Headers: Authorization=${req.headers['authorization'] ? 'present' : 'N/A'}, x-api-key=${req.headers['x-api-key'] || 'N/A'}, x-goog-api-key=${req.headers['x-goog-api-key'] || 'N/A'}`); + logger.info(`[Default Auth] Unauthorized request. Headers: Authorization=${req.headers['authorization'] ? 'present' : 'N/A'}, x-api-key=${req.headers['x-api-key'] || 'N/A'}, x-goog-api-key=${req.headers['x-goog-api-key'] || 'N/A'}`); // 返回 null 表示此插件不授权,让其他插件或默认逻辑处理 return { handled: false, authorized: null }; } }; -export default defaultAuthPlugin; \ No newline at end of file +export default defaultAuthPlugin; + + diff --git a/src/providers/adapter.js b/src/providers/adapter.js index 33cbc60..ba972f1 100644 --- a/src/providers/adapter.js +++ b/src/providers/adapter.js @@ -1,14 +1,15 @@ -import { OpenAIResponsesApiService } from './openai/openai-responses-core.js'; // 导入OpenAIResponsesApiService -import { GeminiApiService } from './gemini/gemini-core.js'; // 导入geminiApiService -import { AntigravityApiService } from './gemini/antigravity-core.js'; // 导入AntigravityApiService -import { OpenAIApiService } from './openai/openai-core.js'; // 导入OpenAIApiService -import { ClaudeApiService } from './claude/claude-core.js'; // 导入ClaudeApiService -import { KiroApiService } from './claude/claude-kiro.js'; // 导入KiroApiService -import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService -import { IFlowApiService } from './openai/iflow-core.js'; // 导入IFlowApiService -import { CodexApiService } from './openai/codex-core.js'; // 导入CodexApiService -import { ForwardApiService } from './forward/forward-core.js'; // 导入ForwardApiService -import { MODEL_PROVIDER } from '../utils/common.js'; // 导入 MODEL_PROVIDER +import { OpenAIResponsesApiService } from './openai/openai-responses-core.js'; +import { GeminiApiService } from './gemini/gemini-core.js'; +import { AntigravityApiService } from './gemini/antigravity-core.js'; +import { OpenAIApiService } from './openai/openai-core.js'; +import { ClaudeApiService } from './claude/claude-core.js'; +import { KiroApiService } from './claude/claude-kiro.js'; +import { QwenApiService } from './openai/qwen-core.js'; +import { IFlowApiService } from './openai/iflow-core.js'; +import { CodexApiService } from './openai/codex-core.js'; +import { ForwardApiService } from './forward/forward-core.js'; +import { MODEL_PROVIDER } from '../utils/common.js'; +import logger from '../utils/logger.js'; // 定义AI服务适配器接口 // 所有的服务适配器都应该实现这些方法 @@ -78,13 +79,13 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter { super(); this.geminiApiService = new GeminiApiService(config); // this.geminiApiService.initialize().catch(error => { - // console.error("Failed to initialize geminiApiService:", error); + // logger.error("Failed to initialize geminiApiService:", error); // }); } async generateContent(model, requestBody) { if (!this.geminiApiService.isInitialized) { - console.warn("geminiApiService not initialized, attempting to re-initialize..."); + logger.warn("geminiApiService not initialized, attempting to re-initialize..."); await this.geminiApiService.initialize(); } return this.geminiApiService.generateContent(model, requestBody); @@ -92,7 +93,7 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter { async *generateContentStream(model, requestBody) { if (!this.geminiApiService.isInitialized) { - console.warn("geminiApiService not initialized, attempting to re-initialize..."); + logger.warn("geminiApiService not initialized, attempting to re-initialize..."); await this.geminiApiService.initialize(); } yield* this.geminiApiService.generateContentStream(model, requestBody); @@ -100,7 +101,7 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter { async listModels() { if (!this.geminiApiService.isInitialized) { - console.warn("geminiApiService not initialized, attempting to re-initialize..."); + logger.warn("geminiApiService not initialized, attempting to re-initialize..."); await this.geminiApiService.initialize(); } // Gemini Core API 的 listModels 已经返回符合 Gemini 格式的数据,所以不需要额外转换 @@ -112,7 +113,7 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter { await this.geminiApiService.initialize(); } if(this.isExpiryDateNear()===true){ - console.log(`[Gemini] Expiry date is near, refreshing token...`); + logger.info(`[Gemini] Expiry date is near, refreshing token...`); return this.geminiApiService.initializeAuth(true); } return Promise.resolve(); @@ -122,7 +123,7 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter { if (!this.geminiApiService.isInitialized) { await this.geminiApiService.initialize(); } - console.log(`[Gemini] Force refreshing token...`); + logger.info(`[Gemini] Force refreshing token...`); return this.geminiApiService.initializeAuth(true); } @@ -136,7 +137,7 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter { */ async getUsageLimits() { if (!this.geminiApiService.isInitialized) { - console.warn("geminiApiService not initialized, attempting to re-initialize..."); + logger.warn("geminiApiService not initialized, attempting to re-initialize..."); await this.geminiApiService.initialize(); } return this.geminiApiService.getUsageLimits(); @@ -152,7 +153,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter { async generateContent(model, requestBody) { if (!this.antigravityApiService.isInitialized) { - console.warn("antigravityApiService not initialized, attempting to re-initialize..."); + logger.warn("antigravityApiService not initialized, attempting to re-initialize..."); await this.antigravityApiService.initialize(); } return this.antigravityApiService.generateContent(model, requestBody); @@ -160,7 +161,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter { async *generateContentStream(model, requestBody) { if (!this.antigravityApiService.isInitialized) { - console.warn("antigravityApiService not initialized, attempting to re-initialize..."); + logger.warn("antigravityApiService not initialized, attempting to re-initialize..."); await this.antigravityApiService.initialize(); } yield* this.antigravityApiService.generateContentStream(model, requestBody); @@ -168,7 +169,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter { async listModels() { if (!this.antigravityApiService.isInitialized) { - console.warn("antigravityApiService not initialized, attempting to re-initialize..."); + logger.warn("antigravityApiService not initialized, attempting to re-initialize..."); await this.antigravityApiService.initialize(); } return this.antigravityApiService.listModels(); @@ -179,7 +180,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter { await this.antigravityApiService.initialize(); } if (this.isExpiryDateNear() === true) { - console.log(`[Antigravity] Expiry date is near, refreshing token...`); + logger.info(`[Antigravity] Expiry date is near, refreshing token...`); return this.antigravityApiService.initializeAuth(true); } return Promise.resolve(); @@ -189,7 +190,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter { if (!this.antigravityApiService.isInitialized) { await this.antigravityApiService.initialize(); } - console.log(`[Antigravity] Force refreshing token...`); + logger.info(`[Antigravity] Force refreshing token...`); return this.antigravityApiService.initializeAuth(true); } @@ -203,7 +204,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter { */ async getUsageLimits() { if (!this.antigravityApiService.isInitialized) { - console.warn("antigravityApiService not initialized, attempting to re-initialize..."); + logger.warn("antigravityApiService not initialized, attempting to re-initialize..."); await this.antigravityApiService.initialize(); } return this.antigravityApiService.getUsageLimits(); @@ -330,14 +331,14 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter { super(); this.kiroApiService = new KiroApiService(config); // this.kiroApiService.initialize().catch(error => { - // console.error("Failed to initialize kiroApiService:", error); + // logger.error("Failed to initialize kiroApiService:", error); // }); } async generateContent(model, requestBody) { // The adapter expects the requestBody to be in OpenAI format for Kiro API if (!this.kiroApiService.isInitialized) { - console.warn("kiroApiService not initialized, attempting to re-initialize..."); + logger.warn("kiroApiService not initialized, attempting to re-initialize..."); await this.kiroApiService.initialize(); } return this.kiroApiService.generateContent(model, requestBody); @@ -346,7 +347,7 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter { async *generateContentStream(model, requestBody) { // The adapter expects the requestBody to be in OpenAI format for Kiro API if (!this.kiroApiService.isInitialized) { - console.warn("kiroApiService not initialized, attempting to re-initialize..."); + logger.warn("kiroApiService not initialized, attempting to re-initialize..."); await this.kiroApiService.initialize(); } const stream = this.kiroApiService.generateContentStream(model, requestBody); @@ -356,7 +357,7 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter { async listModels() { // Returns the native model list from the Kiro service if (!this.kiroApiService.isInitialized) { - console.warn("kiroApiService not initialized, attempting to re-initialize..."); + logger.warn("kiroApiService not initialized, attempting to re-initialize..."); await this.kiroApiService.initialize(); } return this.kiroApiService.listModels(); @@ -367,7 +368,7 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter { await this.kiroApiService.initialize(); } if(this.isExpiryDateNear()===true){ - console.log(`[Kiro] Expiry date is near, refreshing token...`); + logger.info(`[Kiro] Expiry date is near, refreshing token...`); return this.kiroApiService.initializeAuth(true); } return Promise.resolve(); @@ -377,7 +378,7 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter { if (!this.kiroApiService.isInitialized) { await this.kiroApiService.initialize(); } - console.log(`[Kiro] Force refreshing token...`); + logger.info(`[Kiro] Force refreshing token...`); return this.kiroApiService.initializeAuth(true); } @@ -391,7 +392,7 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter { */ async getUsageLimits() { if (!this.kiroApiService.isInitialized) { - console.warn("kiroApiService not initialized, attempting to re-initialize..."); + logger.warn("kiroApiService not initialized, attempting to re-initialize..."); await this.kiroApiService.initialize(); } return this.kiroApiService.getUsageLimits(); @@ -416,7 +417,7 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter { async generateContent(model, requestBody) { if (!this.qwenApiService.isInitialized) { - console.warn("qwenApiService not initialized, attempting to re-initialize..."); + logger.warn("qwenApiService not initialized, attempting to re-initialize..."); await this.qwenApiService.initialize(); } return this.qwenApiService.generateContent(model, requestBody); @@ -424,7 +425,7 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter { async *generateContentStream(model, requestBody) { if (!this.qwenApiService.isInitialized) { - console.warn("qwenApiService not initialized, attempting to re-initialize..."); + logger.warn("qwenApiService not initialized, attempting to re-initialize..."); await this.qwenApiService.initialize(); } yield* this.qwenApiService.generateContentStream(model, requestBody); @@ -432,7 +433,7 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter { async listModels() { if (!this.qwenApiService.isInitialized) { - console.warn("qwenApiService not initialized, attempting to re-initialize..."); + logger.warn("qwenApiService not initialized, attempting to re-initialize..."); await this.qwenApiService.initialize(); } return this.qwenApiService.listModels(); @@ -443,7 +444,7 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter { await this.qwenApiService.initialize(); } if (this.isExpiryDateNear()) { - console.log(`[Qwen] Expiry date is near, refreshing token...`); + logger.info(`[Qwen] Expiry date is near, refreshing token...`); return this.qwenApiService._initializeAuth(true); } return Promise.resolve(); @@ -453,7 +454,7 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter { if (!this.qwenApiService.isInitialized) { await this.qwenApiService.initialize(); } - console.log(`[Qwen] Force refreshing token...`); + logger.info(`[Qwen] Force refreshing token...`); return this.qwenApiService._initializeAuth(true); } @@ -471,7 +472,7 @@ export class IFlowApiServiceAdapter extends ApiServiceAdapter { async generateContent(model, requestBody) { if (!this.iflowApiService.isInitialized) { - console.warn("iflowApiService not initialized, attempting to re-initialize..."); + logger.warn("iflowApiService not initialized, attempting to re-initialize..."); await this.iflowApiService.initialize(); } return this.iflowApiService.generateContent(model, requestBody); @@ -479,7 +480,7 @@ export class IFlowApiServiceAdapter extends ApiServiceAdapter { async *generateContentStream(model, requestBody) { if (!this.iflowApiService.isInitialized) { - console.warn("iflowApiService not initialized, attempting to re-initialize..."); + logger.warn("iflowApiService not initialized, attempting to re-initialize..."); await this.iflowApiService.initialize(); } yield* this.iflowApiService.generateContentStream(model, requestBody); @@ -487,7 +488,7 @@ export class IFlowApiServiceAdapter extends ApiServiceAdapter { async listModels() { if (!this.iflowApiService.isInitialized) { - console.warn("iflowApiService not initialized, attempting to re-initialize..."); + logger.warn("iflowApiService not initialized, attempting to re-initialize..."); await this.iflowApiService.initialize(); } return this.iflowApiService.listModels(); @@ -498,7 +499,7 @@ export class IFlowApiServiceAdapter extends ApiServiceAdapter { await this.iflowApiService.initialize(); } if (this.isExpiryDateNear()) { - console.log(`[iFlow] Expiry date is near, refreshing API key...`); + logger.info(`[iFlow] Expiry date is near, refreshing API key...`); await this.iflowApiService.initializeAuth(true); } return Promise.resolve(); @@ -508,7 +509,7 @@ export class IFlowApiServiceAdapter extends ApiServiceAdapter { if (!this.iflowApiService.isInitialized) { await this.iflowApiService.initialize(); } - console.log(`[iFlow] Force refreshing API key...`); + logger.info(`[iFlow] Force refreshing API key...`); return this.iflowApiService.initializeAuth(true); } @@ -527,7 +528,7 @@ export class CodexApiServiceAdapter extends ApiServiceAdapter { async generateContent(model, requestBody) { if (!this.codexApiService.isInitialized) { - console.warn("codexApiService not initialized, attempting to re-initialize..."); + logger.warn("codexApiService not initialized, attempting to re-initialize..."); await this.codexApiService.initialize(); } return this.codexApiService.generateContent(model, requestBody); @@ -535,7 +536,7 @@ export class CodexApiServiceAdapter extends ApiServiceAdapter { async *generateContentStream(model, requestBody) { if (!this.codexApiService.isInitialized) { - console.warn("codexApiService not initialized, attempting to re-initialize..."); + logger.warn("codexApiService not initialized, attempting to re-initialize..."); await this.codexApiService.initialize(); } yield* this.codexApiService.generateContentStream(model, requestBody); @@ -550,7 +551,7 @@ export class CodexApiServiceAdapter extends ApiServiceAdapter { await this.codexApiService.initialize(); } if (this.isExpiryDateNear()) { - console.log(`[Codex] Expiry date is near, refreshing token...`); + logger.info(`[Codex] Expiry date is near, refreshing token...`); await this.codexApiService.refreshAccessToken(); } return Promise.resolve(); @@ -560,7 +561,7 @@ export class CodexApiServiceAdapter extends ApiServiceAdapter { if (!this.codexApiService.isInitialized) { await this.codexApiService.initialize(); } - console.log(`[Codex] Force refreshing token...`); + logger.info(`[Codex] Force refreshing token...`); return this.codexApiService.refreshAccessToken(); } @@ -607,7 +608,7 @@ export const serviceInstances = {}; // 服务适配器工厂 export function getServiceAdapter(config) { const customNameDisplay = config.customName ? ` (${config.customName})` : ''; - console.log(`[Adapter] getServiceAdapter, provider: ${config.MODEL_PROVIDER}, uuid: ${config.uuid}${customNameDisplay}`); + logger.info(`[Adapter] getServiceAdapter, provider: ${config.MODEL_PROVIDER}, uuid: ${config.uuid}${customNameDisplay}`); const provider = config.MODEL_PROVIDER; const providerKey = config.uuid ? provider + config.uuid : provider; if (!serviceInstances[providerKey]) { @@ -648,3 +649,4 @@ export function getServiceAdapter(config) { } return serviceInstances[providerKey]; } + diff --git a/src/providers/claude/claude-core.js b/src/providers/claude/claude-core.js index 9826171..b5f7a72 100644 --- a/src/providers/claude/claude-core.js +++ b/src/providers/claude/claude-core.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; import { configureAxiosProxy } from '../../utils/proxy-utils.js'; @@ -22,7 +23,7 @@ export class ClaudeApiService { this.apiKey = config.CLAUDE_API_KEY; this.baseUrl = config.CLAUDE_BASE_URL; this.useSystemProxy = config?.USE_SYSTEM_PROXY_CLAUDE ?? false; - console.log(`[Claude] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); + logger.info(`[Claude] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); this.client = this.createClient(); } @@ -92,14 +93,14 @@ export class ClaudeApiService { // 对于 Claude API,401 通常意味着 API Key 无效,不进行重试 if (status === 401 || status === 403) { - console.error(`[Claude API] Received ${status}. API Key might be invalid or expired.`); + logger.error(`[Claude API] Received ${status}. API Key might be invalid or expired.`); throw error; } // 处理 429 (Too Many Requests) 与指数退避 if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Claude API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Claude API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } @@ -107,7 +108,7 @@ export class ClaudeApiService { // 处理其他可重试错误 (5xx 服务器错误) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Claude API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Claude API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } @@ -116,12 +117,12 @@ export class ClaudeApiService { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[Claude API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Claude API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } - console.error(`[Claude API] Error calling API (Status: ${status}, Code: ${errorCode}):`, error.response ? error.response.data : error.message); + logger.error(`[Claude API] Error calling API (Status: ${status}, Code: ${errorCode}):`, error.response ? error.response.data : error.message); throw error; } } @@ -166,7 +167,7 @@ export class ClaudeApiService { return; } } catch (e) { - console.warn("[ClaudeApiService] Failed to parse stream chunk JSON:", e.message, "Data:", data); + logger.warn("[ClaudeApiService] Failed to parse stream chunk JSON:", e.message, "Data:", data); } } } @@ -181,14 +182,14 @@ export class ClaudeApiService { // 对于 Claude API,401 通常意味着 API Key 无效,不进行重试 if (status === 401 || status === 403) { - console.error(`[Claude API] Received ${status} during stream. API Key might be invalid or expired.`); + logger.error(`[Claude API] Received ${status} during stream. API Key might be invalid or expired.`); throw error; } // 处理 429 (Too Many Requests) 与指数退避 if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Claude API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Claude API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; @@ -197,7 +198,7 @@ export class ClaudeApiService { // 处理其他可重试错误 (5xx 服务器错误) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Claude API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Claude API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; @@ -207,13 +208,13 @@ export class ClaudeApiService { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[Claude API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Claude API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; } - console.error(`[Claude API] Error generating content stream (Status: ${status}, Code: ${errorCode}):`, error.response ? error.response.data : error.message); + logger.error(`[Claude API] Error generating content stream (Status: ${status}, Code: ${errorCode}):`, error.response ? error.response.data : error.message); throw error; } } @@ -248,7 +249,7 @@ export class ClaudeApiService { * @returns {Promise} List of models. */ async listModels() { - console.log('[ClaudeApiService] Listing available models.'); + logger.info('[ClaudeApiService] Listing available models.'); // Claude API 没有直接的 /models 端点来列出所有模型。 // 通常,你需要根据 Anthropic 的文档硬编码你希望支持的模型。 // 这里我们返回一些常见的 Claude 模型作为示例。 @@ -266,3 +267,4 @@ export class ClaudeApiService { return { models: models.map(m => ({ name: m.name })) }; } } + diff --git a/src/providers/claude/claude-kiro.js b/src/providers/claude/claude-kiro.js index 4aa48b8..a2f982e 100644 --- a/src/providers/claude/claude-kiro.js +++ b/src/providers/claude/claude-kiro.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import logger from '../../utils/logger.js'; import { v4 as uuidv4 } from 'uuid'; import { promises as fs } from 'fs'; import * as path from 'path'; @@ -215,7 +216,7 @@ function extractCredentialsFromCorruptedJson(content) { // 至少需要 refreshToken 或 accessToken 才算有效 if (extracted.refreshToken || extracted.accessToken) { - console.info(`[Kiro Auth] Extracted ${Object.keys(extracted).length} fields from corrupted JSON: ${Object.keys(extracted).join(', ')}`); + logger.info(`[Kiro Auth] Extracted ${Object.keys(extracted).length} fields from corrupted JSON: ${Object.keys(extracted).join(', ')}`); return extracted; } @@ -270,7 +271,7 @@ function parseSingleToolCall(toolCallText) { } }; } catch (e) { - console.error(`Failed to parse tool call arguments: ${e.message}`, jsonCandidate); + logger.error(`Failed to parse tool call arguments: ${e.message}`, jsonCandidate); return null; } } @@ -335,7 +336,7 @@ function deduplicateToolCalls(toolCalls) { seen.add(key); uniqueToolCalls.push(tc); } else { - console.log(`Skipping duplicate tool call: ${tc.function.name}`); + logger.info(`Skipping duplicate tool call: ${tc.function.name}`); } } return uniqueToolCalls; @@ -349,7 +350,7 @@ export class KiroApiService { this.credsBase64 = config.KIRO_OAUTH_CREDS_BASE64; this.useSystemProxy = config?.USE_SYSTEM_PROXY_KIRO ?? false; this.uuid = config?.uuid; // 获取多节点配置的 uuid - console.log(`[Kiro] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); + logger.info(`[Kiro] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); // this.accessToken = config.KIRO_ACCESS_TOKEN; // this.refreshToken = config.KIRO_REFRESH_TOKEN; // this.clientId = config.KIRO_CLIENT_ID; @@ -366,9 +367,9 @@ export class KiroApiService { const parsedCreds = JSON.parse(decodedCreds); // Store parsedCreds to be merged in initializeAuth this.base64Creds = parsedCreds; - console.info('[Kiro] Successfully decoded Base64 credentials in constructor.'); + logger.info('[Kiro] Successfully decoded Base64 credentials in constructor.'); } catch (error) { - console.error(`[Kiro] Failed to parse Base64 credentials in constructor: ${error.message}`); + logger.error(`[Kiro] Failed to parse Base64 credentials in constructor: ${error.message}`); } } else if (config.KIRO_OAUTH_CREDS_FILE_PATH) { this.credsFilePath = config.KIRO_OAUTH_CREDS_FILE_PATH; @@ -381,7 +382,7 @@ export class KiroApiService { async initialize() { if (this.isInitialized) return; - console.log('[Kiro] Initializing Kiro API Service...'); + logger.info('[Kiro] Initializing Kiro API Service...'); // 注意:V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 仅执行基础的凭证加载 await this.loadCredentials(); @@ -454,29 +455,29 @@ async loadCredentials() { try { return JSON.parse(fileContent); } catch (parseError) { - console.warn('[Kiro Auth] JSON parse failed, attempting repair...'); + logger.warn('[Kiro Auth] JSON parse failed, attempting repair...'); try { const repaired = repairJson(fileContent); const result = JSON.parse(repaired); - console.info('[Kiro Auth] JSON repair successful'); + logger.info('[Kiro Auth] JSON repair successful'); return result; } catch (repairError) { - console.warn('[Kiro Auth] JSON repair failed, attempting field extraction...'); + logger.warn('[Kiro Auth] JSON repair failed, attempting field extraction...'); // 尝试从损坏的 JSON 中提取关键字段 const extracted = extractCredentialsFromCorruptedJson(fileContent); if (extracted) { - console.info('[Kiro Auth] Field extraction successful, credentials recovered'); + logger.info('[Kiro Auth] Field extraction successful, credentials recovered'); return extracted; } - console.error('[Kiro Auth] All recovery methods failed:', repairError.message); + logger.error('[Kiro Auth] All recovery methods failed:', repairError.message); return null; } } } catch (error) { if (error.code === 'ENOENT') { - console.debug(`[Kiro Auth] Credential file not found: ${filePath}`); + logger.debug(`[Kiro Auth] Credential file not found: ${filePath}`); } else { - console.warn(`[Kiro Auth] Failed to read credential file ${filePath}: ${error.message}`); + logger.warn(`[Kiro Auth] Failed to read credential file ${filePath}: ${error.message}`); } return null; } @@ -488,7 +489,7 @@ async loadCredentials() { // Priority 1: Load from Base64 credentials if available if (this.base64Creds) { Object.assign(mergedCredentials, this.base64Creds); - console.info('[Kiro Auth] Successfully loaded credentials from Base64 (constructor).'); + logger.info('[Kiro Auth] Successfully loaded credentials from Base64 (constructor).'); this.base64Creds = null; } @@ -497,13 +498,13 @@ async loadCredentials() { const dirPath = path.dirname(targetFilePath); const targetFileName = path.basename(targetFilePath); - console.debug(`[Kiro Auth] Loading credentials from directory: ${dirPath}`); + logger.debug(`[Kiro Auth] Loading credentials from directory: ${dirPath}`); try { const targetCredentials = await loadCredentialsFromFile(targetFilePath); if (targetCredentials) { Object.assign(mergedCredentials, targetCredentials); - console.info(`[Kiro Auth] Successfully loaded OAuth credentials from ${targetFilePath}`); + logger.info(`[Kiro Auth] Successfully loaded OAuth credentials from ${targetFilePath}`); } const files = await fs.readdir(dirPath); @@ -514,12 +515,12 @@ async loadCredentials() { if (credentials) { credentials.expiresAt = mergedCredentials.expiresAt; Object.assign(mergedCredentials, credentials); - console.debug(`[Kiro Auth] Loaded Client credentials from ${file}`); + logger.debug(`[Kiro Auth] Loaded Client credentials from ${file}`); } } } } catch (error) { - console.warn(`[Kiro Auth] Error loading credentials from directory ${dirPath}: ${error.message}`); + logger.warn(`[Kiro Auth] Error loading credentials from directory ${dirPath}: ${error.message}`); } // Apply loaded credentials @@ -534,7 +535,7 @@ async loadCredentials() { this.idcRegion = this.idcRegion || mergedCredentials.idcRegion; if (!this.region) { - console.warn('[Kiro Auth] Region not found in credentials. Using default region us-east-1 for URLs.'); + logger.warn('[Kiro Auth] Region not found in credentials. Using default region us-east-1 for URLs.'); this.region = 'us-east-1'; } @@ -547,13 +548,13 @@ async loadCredentials() { this.refreshIDCUrl = (this.config.KIRO_REFRESH_IDC_URL || KIRO_CONSTANTS.REFRESH_IDC_URL).replace("{{region}}", this.idcRegion); this.baseUrl = (this.config.KIRO_BASE_URL || KIRO_CONSTANTS.BASE_URL).replace("{{region}}", this.region); } catch (error) { - console.warn(`[Kiro Auth] Error during credential loading: ${error.message}`); + logger.warn(`[Kiro Auth] Error during credential loading: ${error.message}`); } } async initializeAuth(forceRefresh = false) { if (this.accessToken && !forceRefresh) { - console.debug('[Kiro Auth] Access token already available and not forced refresh.'); + logger.debug('[Kiro Auth] Access token already available and not forced refresh.'); return; } @@ -586,33 +587,33 @@ async saveCredentialsToFile(filePath, newData) { try { existingData = JSON.parse(fileContent); } catch (parseError) { - console.warn('[Kiro Auth] JSON parse failed, attempting repair...'); + logger.warn('[Kiro Auth] JSON parse failed, attempting repair...'); try { const repaired = repairJson(fileContent); existingData = JSON.parse(repaired); - console.info('[Kiro Auth] JSON repair successful'); + logger.info('[Kiro Auth] JSON repair successful'); } catch (repairError) { - console.warn('[Kiro Auth] JSON repair failed, attempting field extraction...'); + logger.warn('[Kiro Auth] JSON repair failed, attempting field extraction...'); const extracted = extractCredentialsFromCorruptedJson(fileContent); if (extracted) { existingData = extracted; - console.info('[Kiro Auth] Field extraction successful'); + logger.info('[Kiro Auth] Field extraction successful'); } else { - console.error('[Kiro Auth] All recovery methods failed:', repairError.message); + logger.error('[Kiro Auth] All recovery methods failed:', repairError.message); existingData = {}; } } } } catch (readError) { if (readError.code === 'ENOENT') { - console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`); + logger.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`); } else { - console.warn(`[Kiro Auth] Could not read existing token file ${filePath}: ${readError.message}`); + logger.warn(`[Kiro Auth] Could not read existing token file ${filePath}: ${readError.message}`); } } const mergedData = { ...existingData, ...newData }; await fs.writeFile(filePath, JSON.stringify(mergedData, null, 2), 'utf8'); - console.info(`[Kiro Auth] Updated token file: ${filePath}`); + logger.info(`[Kiro Auth] Updated token file: ${filePath}`); }; /** @@ -639,10 +640,10 @@ async saveCredentialsToFile(filePath, newData) { const refreshConfig = { timeout: KIRO_CONSTANTS.TOKEN_REFRESH_TIMEOUT }; if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) { response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody, refreshConfig); - console.log('[Kiro Auth] Token refresh social response: ok'); + logger.info('[Kiro Auth] Token refresh social response: ok'); } else { response = await this.axiosInstance.post(refreshUrl, requestBody, refreshConfig); - console.log('[Kiro Auth] Token refresh idc response: ok'); + logger.info('[Kiro Auth] Token refresh idc response: ok'); } if (response.data && response.data.accessToken) { @@ -652,7 +653,7 @@ async saveCredentialsToFile(filePath, newData) { const expiresIn = response.data.expiresIn; const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); this.expiresAt = expiresAt; - console.info('[Kiro Auth] Access token refreshed successfully'); + logger.info('[Kiro Auth] Access token refreshed successfully'); const updatedTokenData = { accessToken: this.accessToken, @@ -673,7 +674,7 @@ async saveCredentialsToFile(filePath, newData) { throw new Error('Invalid refresh response: Missing accessToken'); } } catch (error) { - console.error('[Kiro Auth] Token refresh failed:', error.message); + logger.error('[Kiro Auth] Token refresh failed:', error.message); throw new Error(`Token refresh failed: ${error.message}`); } } @@ -787,7 +788,7 @@ async saveCredentialsToFile(filePath, newData) { const lastMessage = processedMessages[processedMessages.length - 1]; if (processedMessages.length > 0 && lastMessage.role === 'assistant') { if (lastMessage.content[0].type === "text" && lastMessage.content[0].text === "{") { - console.log('[Kiro] Removing last assistant with "{" message from processedMessages'); + logger.info('[Kiro] Removing last assistant with "{" message from processedMessages'); processedMessages.pop(); } } @@ -818,7 +819,7 @@ async saveCredentialsToFile(filePath, newData) { // 上一条是字符串,当前是数组,转换为数组格式 lastMsg.content = [{ type: 'text', text: lastMsg.content }, ...currentMsg.content]; } - // console.log(`[Kiro] Merged adjacent ${currentMsg.role} messages`); + // logger.info(`[Kiro] Merged adjacent ${currentMsg.role} messages`); } else { mergedMessages.push(currentMsg); } @@ -839,14 +840,14 @@ async saveCredentialsToFile(filePath, newData) { const name = (tool.name || '').toLowerCase(); const shouldIgnore = name === 'web_search' || name === 'websearch'; if (shouldIgnore) { - console.log(`[Kiro] Ignoring tool: ${tool.name}`); + logger.info(`[Kiro] Ignoring tool: ${tool.name}`); } return !shouldIgnore; }); if (filteredTools.length === 0) { // 所有工具都被过滤掉了,不添加 tools 上下文 - console.log('[Kiro] All tools were filtered out'); + logger.info('[Kiro] All tools were filtered out'); } else { const MAX_DESCRIPTION_LENGTH = 9216; @@ -858,7 +859,7 @@ async saveCredentialsToFile(filePath, newData) { if (desc.length > MAX_DESCRIPTION_LENGTH) { desc = desc.substring(0, MAX_DESCRIPTION_LENGTH) + "..."; truncatedCount++; - console.log(`[Kiro] Truncated tool '${tool.name}' description: ${originalLength} -> ${desc.length} chars`); + logger.info(`[Kiro] Truncated tool '${tool.name}' description: ${originalLength} -> ${desc.length} chars`); } return { @@ -873,7 +874,7 @@ async saveCredentialsToFile(filePath, newData) { }); if (truncatedCount > 0) { - console.log(`[Kiro] Truncated ${truncatedCount} tool description(s) to max ${MAX_DESCRIPTION_LENGTH} chars`); + logger.info(`[Kiro] Truncated ${truncatedCount} tool description(s) to max ${MAX_DESCRIPTION_LENGTH} chars`); } toolsContext = { tools: kiroTools }; @@ -960,7 +961,7 @@ async saveCredentialsToFile(filePath, newData) { // 如果有保留的图片,添加到消息中 if (images.length > 0) { userInputMessage.images = images; - console.log(`[Kiro] Kept ${images.length} image(s) in recent history message (distance from end: ${distanceFromEnd})`); + logger.info(`[Kiro] Kept ${images.length} image(s) in recent history message (distance from end: ${distanceFromEnd})`); } // 如果有被替换的图片,添加占位符说明 @@ -969,7 +970,7 @@ async saveCredentialsToFile(filePath, newData) { userInputMessage.content = userInputMessage.content ? `${userInputMessage.content}\n${imagePlaceholder}` : imagePlaceholder; - console.log(`[Kiro] Replaced ${imageCount} image(s) with placeholder in old history message (distance from end: ${distanceFromEnd})`); + logger.info(`[Kiro] Replaced ${imageCount} image(s) with placeholder in old history message (distance from end: ${distanceFromEnd})`); } if (toolResults.length > 0) { @@ -1036,7 +1037,7 @@ async saveCredentialsToFile(filePath, newData) { // 如果最后一条消息是 assistant,需要将其加入 history,然后创建一个 user 类型的 currentMessage // 因为 CodeWhisperer API 的 currentMessage 必须是 userInputMessage 类型 if (currentMessage.role === 'assistant') { - console.log('[Kiro] Last message is assistant, moving it to history and creating user currentMessage'); + logger.info('[Kiro] Last message is assistant, moving it to history and creating user currentMessage'); // 构建 assistant 消息并加入 history let assistantResponseMessage = { @@ -1080,7 +1081,7 @@ async saveCredentialsToFile(filePath, newData) { const lastHistoryItem = history[history.length - 1]; if (!lastHistoryItem.assistantResponseMessage) { // 最后一个不是 assistantResponseMessage,需要补全一个空的 - console.log('[Kiro] History does not end with assistantResponseMessage, adding empty one'); + logger.info('[Kiro] History does not end with assistantResponseMessage, adding empty one'); history.push({ assistantResponseMessage: { content: 'Continue' @@ -1189,7 +1190,7 @@ async saveCredentialsToFile(filePath, newData) { let fullContent = ''; const toolCalls = []; let currentToolCallDict = null; - // console.log(`rawStr=${rawStr}`); + // logger.info(`rawStr=${rawStr}`); // 改进的 SSE 事件解析:匹配 :message-typeevent 后面的 JSON 数据 // 使用更精确的正则来匹配 SSE 格式的事件 @@ -1237,7 +1238,7 @@ async saveCredentialsToFile(filePath, newData) { const args = JSON.parse(currentToolCallDict.function.arguments); currentToolCallDict.function.arguments = JSON.stringify(args); } catch (e) { - console.warn(`[Kiro] Tool call arguments not valid JSON: ${currentToolCallDict.function.arguments}`); + logger.warn(`[Kiro] Tool call arguments not valid JSON: ${currentToolCallDict.function.arguments}`); } toolCalls.push(currentToolCallDict); currentToolCallDict = null; @@ -1329,12 +1330,12 @@ async saveCredentialsToFile(filePath, newData) { // Handle 401 (Unauthorized) - refresh UUID first, then try to refresh token if (status === 401 && !isRetry) { - console.log('[Kiro] Received 401. Refreshing UUID and triggering background refresh via PoolManager...'); + logger.info('[Kiro] Received 401. Refreshing UUID and triggering background refresh via PoolManager...'); // 1. 先刷新 UUID const newUuid = this._refreshUuid(); if (newUuid) { - console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); + logger.info(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); this.uuid = newUuid; } @@ -1353,20 +1354,20 @@ async saveCredentialsToFile(filePath, newData) { // Handle 403 (Forbidden) - mark as unhealthy immediately, no retry if (status === 403 && !isRetry) { - console.log('[Kiro] Received 403. Marking credential as need refresh...'); + logger.info('[Kiro] Received 403. Marking credential as need refresh...'); // 检查是否为 temporarily suspended 错误 const isSuspended = errorMessage && errorMessage.toLowerCase().includes('temporarily is suspended'); if (isSuspended) { // temporarily suspended 错误:直接标记为不健康,不刷新 UUID - console.log('[Kiro] Account temporarily suspended. Marking as unhealthy without UUID refresh...'); + logger.info('[Kiro] Account temporarily suspended. Marking as unhealthy without UUID refresh...'); this._markCredentialUnhealthy('403 Forbidden - Account temporarily suspended', error); } else { // 其他 403 错误:先刷新 UUID,然后标记需要刷新 // const newUuid = this._refreshUuid(); // if (newUuid) { - // console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); + // logger.info(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); // this.uuid = newUuid; // } this._markCredentialNeedRefresh('403 Forbidden', error); @@ -1380,7 +1381,7 @@ async saveCredentialsToFile(filePath, newData) { // Handle 429 (Too Many Requests) - wait baseDelay then switch credential if (status === 429) { - console.log(`[Kiro] Received 429 (Too Many Requests). Waiting ${baseDelay}ms before switching credential...`); + logger.info(`[Kiro] Received 429 (Too Many Requests). Waiting ${baseDelay}ms before switching credential...`); await new Promise(resolve => setTimeout(resolve, baseDelay)); // Mark error for credential switch without recording error count error.shouldSwitchCredential = true; @@ -1390,7 +1391,7 @@ async saveCredentialsToFile(filePath, newData) { // Handle 5xx server errors - wait baseDelay then switch credential if (status >= 500 && status < 600) { - console.log(`[Kiro] Received ${status} server error. Waiting ${baseDelay}ms before switching credential...`); + logger.info(`[Kiro] Received ${status} server error. Waiting ${baseDelay}ms before switching credential...`); await new Promise(resolve => setTimeout(resolve, baseDelay)); // Mark error for credential switch without recording error count error.shouldSwitchCredential = true; @@ -1402,12 +1403,12 @@ async saveCredentialsToFile(filePath, newData) { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[Kiro] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Kiro] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, model, body, isRetry, retryCount + 1); } - console.error(`[Kiro] API call failed (Status: ${status}, Code: ${errorCode}):`, error.message); + logger.error(`[Kiro] API call failed (Status: ${status}, Code: ${errorCode}):`, error.message); throw error; } } @@ -1426,7 +1427,7 @@ async saveCredentialsToFile(filePath, newData) { }); return newUuid; } else { - console.warn(`[Kiro] Cannot refresh UUID: poolManager=${!!poolManager}, uuid=${this.uuid}`); + logger.warn(`[Kiro] Cannot refresh UUID: poolManager=${!!poolManager}, uuid=${this.uuid}`); return null; } } @@ -1441,7 +1442,7 @@ async saveCredentialsToFile(filePath, newData) { _markCredentialNeedRefresh(reason, error = null) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Kiro] Marking credential ${this.uuid} as needs refresh. Reason: ${reason}`); + logger.info(`[Kiro] Marking credential ${this.uuid} as needs refresh. Reason: ${reason}`); // 使用新的 markProviderNeedRefresh 方法代替 markProviderUnhealthyImmediately poolManager.markProviderNeedRefresh(MODEL_PROVIDER.KIRO_API, { uuid: this.uuid @@ -1452,7 +1453,7 @@ async saveCredentialsToFile(filePath, newData) { } return true; } else { - console.warn(`[Kiro] Cannot mark credential as unhealthy: poolManager=${!!poolManager}, uuid=${this.uuid}`); + logger.warn(`[Kiro] Cannot mark credential as unhealthy: poolManager=${!!poolManager}, uuid=${this.uuid}`); return false; } } @@ -1467,7 +1468,7 @@ async saveCredentialsToFile(filePath, newData) { _markCredentialUnhealthy(reason, error = null) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Kiro] Marking credential ${this.uuid} as unhealthy. Reason: ${reason}`); + logger.info(`[Kiro] Marking credential ${this.uuid} as unhealthy. Reason: ${reason}`); poolManager.markProviderUnhealthyImmediately(MODEL_PROVIDER.KIRO_API, { uuid: this.uuid }, reason); @@ -1477,7 +1478,7 @@ async saveCredentialsToFile(filePath, newData) { } return true; } else { - console.warn(`[Kiro] Cannot mark credential as unhealthy: poolManager=${!!poolManager}, uuid=${this.uuid}`); + logger.warn(`[Kiro] Cannot mark credential as unhealthy: poolManager=${!!poolManager}, uuid=${this.uuid}`); return false; } } @@ -1494,7 +1495,7 @@ async saveCredentialsToFile(filePath, newData) { _markCredentialUnhealthyWithRecovery(reason, error = null, recoveryTime = null) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Kiro] Marking credential ${this.uuid} as unhealthy with recovery time. Reason: ${reason}, Recovery: ${recoveryTime?.toISOString()}`); + logger.info(`[Kiro] Marking credential ${this.uuid} as unhealthy with recovery time. Reason: ${reason}, Recovery: ${recoveryTime?.toISOString()}`); poolManager.markProviderUnhealthyWithRecoveryTime(MODEL_PROVIDER.KIRO_API, { uuid: this.uuid }, reason, recoveryTime); @@ -1504,7 +1505,7 @@ async saveCredentialsToFile(filePath, newData) { } return true; } else { - console.warn(`[Kiro] Cannot mark credential as unhealthy: poolManager=${!!poolManager}, uuid=${this.uuid}`); + logger.warn(`[Kiro] Cannot mark credential as unhealthy: poolManager=${!!poolManager}, uuid=${this.uuid}`); return false; } } @@ -1528,18 +1529,18 @@ async saveCredentialsToFile(filePath, newData) { * @private */ async _handle402Error(error, context = 'unknown') { - console.log(`[Kiro] Received 402 (Quota Exceeded) in ${context}. Verifying usage limits...`); + logger.info(`[Kiro] Received 402 (Quota Exceeded) in ${context}. Verifying usage limits...`); try { // Verify usage limits to confirm quota exhaustion const usageLimits = await this.getUsageLimits(); const isQuotaExhausted = usageLimits?.usedCount >= usageLimits?.limitCount; - console.log(`[Kiro] Quota confirmed exhausted: ${usageLimits?.usedCount}/${usageLimits?.limitCount}`); + logger.info(`[Kiro] Quota confirmed exhausted: ${usageLimits?.usedCount}/${usageLimits?.limitCount}`); // Calculate recovery time: 1st day of next month at 00:00:00 UTC const nextMonth = this._getNextMonthFirstDay(); this._markCredentialUnhealthyWithRecovery('402 Payment Required - Quota Exhausted', error, nextMonth); } catch (usageError) { - console.warn('[Kiro] Failed to verify usage limits:', usageError.message); + logger.warn('[Kiro] Failed to verify usage limits:', usageError.message); // If we can't verify, still mark as unhealthy with recovery time const nextMonth = this._getNextMonthFirstDay(); this._markCredentialUnhealthyWithRecovery('402 Payment Required - Quota Exceeded (unverified)', error, nextMonth); @@ -1552,27 +1553,27 @@ async saveCredentialsToFile(filePath, newData) { _processApiResponse(response) { const rawResponseText = Buffer.isBuffer(response.data) ? response.data.toString('utf8') : String(response.data); - //console.log(`[Kiro] Raw response length: ${rawResponseText.length}`); + //logger.info(`[Kiro] Raw response length: ${rawResponseText.length}`); if (rawResponseText.includes("[Called")) { - console.log("[Kiro] Raw response contains [Called marker."); + logger.info("[Kiro] Raw response contains [Called marker."); } // 1. Parse structured events and bracket calls from parsed content const parsedFromEvents = this.parseEventStreamChunk(rawResponseText); let fullResponseText = parsedFromEvents.content; let allToolCalls = [...parsedFromEvents.toolCalls]; // clone - //console.log(`[Kiro] Found ${allToolCalls.length} tool calls from event stream parsing.`); + //logger.info(`[Kiro] Found ${allToolCalls.length} tool calls from event stream parsing.`); // 2. Crucial fix from Python example: Parse bracket tool calls from the original raw response const rawBracketToolCalls = parseBracketToolCalls(rawResponseText); if (rawBracketToolCalls) { - //console.log(`[Kiro] Found ${rawBracketToolCalls.length} bracket tool calls in raw response.`); + //logger.info(`[Kiro] Found ${rawBracketToolCalls.length} bracket tool calls in raw response.`); allToolCalls.push(...rawBracketToolCalls); } // 3. Deduplicate all collected tool calls const uniqueToolCalls = deduplicateToolCalls(allToolCalls); - //console.log(`[Kiro] Total unique tool calls after deduplication: ${uniqueToolCalls.length}`); + //logger.info(`[Kiro] Total unique tool calls after deduplication: ${uniqueToolCalls.length}`); // 4. Clean up response text by removing all tool call syntax from the final text. // The text from parseEventStreamChunk is already partially cleaned. @@ -1587,8 +1588,8 @@ async saveCredentialsToFile(filePath, newData) { fullResponseText = fullResponseText.replace(/\s+/g, ' ').trim(); } - //console.log(`[Kiro] Final response text after tool call cleanup: ${fullResponseText}`); - //console.log(`[Kiro] Final tool calls after deduplication: ${JSON.stringify(uniqueToolCalls)}`); + //logger.info(`[Kiro] Final response text after tool call cleanup: ${fullResponseText}`); + //logger.info(`[Kiro] Final tool calls after deduplication: ${JSON.stringify(uniqueToolCalls)}`); return { responseText: fullResponseText, toolCalls: uniqueToolCalls }; } @@ -1597,12 +1598,12 @@ async saveCredentialsToFile(filePath, newData) { // 检查 token 是否即将过期,如果是则推送到刷新队列 if (this.isExpiryDateNear()) { - console.log('[Kiro] Token is near expiry, marking credential as need refresh...'); + logger.info('[Kiro] Token is near expiry, marking credential as need refresh...'); this._markCredentialNeedRefresh('Token near expiry in generateContent'); } const finalModel = MODEL_MAPPING[model] ? model : this.modelName; - console.log(`[Kiro] Calling generateContent with model: ${finalModel}`); + logger.info(`[Kiro] Calling generateContent with model: ${finalModel}`); // Estimate input tokens before making the API call const inputTokens = this.estimateInputTokens(requestBody); @@ -1613,7 +1614,7 @@ async saveCredentialsToFile(filePath, newData) { const { responseText, toolCalls } = this._processApiResponse(response); return this.buildClaudeResponse(responseText, false, 'assistant', model, toolCalls, inputTokens); } catch (error) { - console.error('[Kiro] Error in generateContent:', error); + logger.error('[Kiro] Error in generateContent:', error); throw new Error(`Error processing response: ${error.message}`); } } @@ -1850,12 +1851,12 @@ async saveCredentialsToFile(filePath, newData) { // Handle 401 (Unauthorized) - try to refresh token first if (status === 401 && !isRetry) { - console.log('[Kiro] Received 401 in stream. Triggering background refresh via PoolManager...'); + logger.info('[Kiro] Received 401 in stream. Triggering background refresh via PoolManager...'); // 1. 先刷新 UUID const newUuid = this._refreshUuid(); if (newUuid) { - console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); + logger.info(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); this.uuid = newUuid; } // 标记当前凭证为不健康(会自动进入刷新队列) @@ -1873,20 +1874,20 @@ async saveCredentialsToFile(filePath, newData) { // Handle 403 (Forbidden) - mark as unhealthy immediately, no retry if (status === 403 && !isRetry) { - console.log('[Kiro] Received 403 in stream. Marking credential as need refresh...'); + logger.info('[Kiro] Received 403 in stream. Marking credential as need refresh...'); // 检查是否为 temporarily suspended 错误 const isSuspended = errorMessage && errorMessage.toLowerCase().includes('temporarily is suspended'); if (isSuspended) { // temporarily suspended 错误:直接标记为不健康,不刷新 UUID - console.log('[Kiro] Account temporarily suspended in stream. Marking as unhealthy without UUID refresh...'); + logger.info('[Kiro] Account temporarily suspended in stream. Marking as unhealthy without UUID refresh...'); this._markCredentialUnhealthy('403 Forbidden - Account temporarily suspended', error); } else { // 其他 403 错误:先刷新 UUID,然后标记需要刷新 // const newUuid = this._refreshUuid(); // if (newUuid) { - // console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); + // logger.info(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); // this.uuid = newUuid; // } this._markCredentialNeedRefresh('403 Forbidden', error); @@ -1900,7 +1901,7 @@ async saveCredentialsToFile(filePath, newData) { // Handle 429 (Too Many Requests) - wait baseDelay then switch credential if (status === 429) { - console.log(`[Kiro] Received 429 (Too Many Requests) in stream. Waiting ${baseDelay}ms before switching credential...`); + logger.info(`[Kiro] Received 429 (Too Many Requests) in stream. Waiting ${baseDelay}ms before switching credential...`); await new Promise(resolve => setTimeout(resolve, baseDelay)); // Mark error for credential switch without recording error count error.shouldSwitchCredential = true; @@ -1910,7 +1911,7 @@ async saveCredentialsToFile(filePath, newData) { // Handle 5xx server errors - wait baseDelay then switch credential if (status >= 500 && status < 600) { - console.log(`[Kiro] Received ${status} server error in stream. Waiting ${baseDelay}ms before switching credential...`); + logger.info(`[Kiro] Received ${status} server error in stream. Waiting ${baseDelay}ms before switching credential...`); await new Promise(resolve => setTimeout(resolve, baseDelay)); // Mark error for credential switch without recording error count error.shouldSwitchCredential = true; @@ -1922,13 +1923,13 @@ async saveCredentialsToFile(filePath, newData) { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[Kiro] Network error (${errorIdentifier}) in stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Kiro] Network error (${errorIdentifier}) in stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApiReal(method, model, body, isRetry, retryCount + 1); return; } - console.error(`[Kiro] Stream API call failed (Status: ${status}, Code: ${errorCode}):`, error.message); + logger.error(`[Kiro] Stream API call failed (Status: ${status}, Code: ${errorCode}):`, error.message); throw error; } finally { // 确保流被关闭,释放资源 @@ -1943,7 +1944,7 @@ async saveCredentialsToFile(filePath, newData) { try { return await this.callApi(method, model, body, isRetry, retryCount); } catch (error) { - console.error('[Kiro] Error calling API:', error); + logger.error('[Kiro] Error calling API:', error); throw error; } } @@ -1954,12 +1955,12 @@ async saveCredentialsToFile(filePath, newData) { // 检查 token 是否即将过期,如果是则推送到刷新队列 if (this.isExpiryDateNear()) { - console.log('[Kiro] Token is near expiry, marking credential as need refresh...'); + logger.info('[Kiro] Token is near expiry, marking credential as need refresh...'); this._markCredentialNeedRefresh('Token near expiry in generateContentStream'); } const finalModel = MODEL_MAPPING[model] ? model : this.modelName; - console.log(`[Kiro] Calling generateContentStream with model: ${finalModel} (real streaming)`); + logger.info(`[Kiro] Calling generateContentStream with model: ${finalModel} (real streaming)`); let inputTokens = 0; let contextUsagePercentage = null; @@ -2214,7 +2215,7 @@ async saveCredentialsToFile(filePath, newData) { if (thinkingRequested && streamState.buffer) { if (streamState.inThinking) { - console.warn('[Kiro] Incomplete thinking tag at stream end'); + logger.warn('[Kiro] Incomplete thinking tag at stream end'); yield* pushEvents(createThinkingDeltaEvents(streamState.buffer)); streamState.buffer = ''; yield* pushEvents(createThinkingDeltaEvents("")); @@ -2293,9 +2294,9 @@ async saveCredentialsToFile(filePath, newData) { if (contextUsagePercentage !== null && contextUsagePercentage > 0) { const totalTokens = Math.round(KIRO_CONSTANTS.TOTAL_CONTEXT_TOKENS * contextUsagePercentage / 100); inputTokens = Math.max(0, totalTokens - outputTokens); - console.log(`[Kiro] Token calculation from contextUsagePercentage: total=${totalTokens}, output=${outputTokens}, input=${inputTokens}`); + logger.info(`[Kiro] Token calculation from contextUsagePercentage: total=${totalTokens}, output=${outputTokens}, input=${inputTokens}`); } else { - console.warn('[Kiro Stream] contextUsagePercentage not received, using estimation'); + logger.warn('[Kiro Stream] contextUsagePercentage not received, using estimation'); inputTokens = estimatedInputTokens; } @@ -2315,7 +2316,7 @@ async saveCredentialsToFile(filePath, newData) { yield { type: "message_stop" }; } catch (error) { - console.error('[Kiro] Error in streaming generation:', error); + logger.error('[Kiro] Error in streaming generation:', error); throw new Error(`Error processing response: ${error.message}`); } } @@ -2329,7 +2330,7 @@ async saveCredentialsToFile(filePath, newData) { return countTokens(text); } catch (error) { // Fallback to estimation if tokenizer fails - console.warn('[Kiro] Tokenizer error, falling back to estimation:', error.message); + logger.warn('[Kiro] Tokenizer error, falling back to estimation:', error.message); return Math.ceil((text || '').length / 4); } } @@ -2459,7 +2460,7 @@ async saveCredentialsToFile(filePath, newData) { const args = tc.function.arguments; inputObject = typeof args === 'string' ? JSON.parse(args) : args; } catch (e) { - console.warn(`[Kiro] Invalid JSON for tool call arguments. Wrapping in raw_arguments. Error: ${e.message}`, tc.function.arguments); + logger.warn(`[Kiro] Invalid JSON for tool call arguments. Wrapping in raw_arguments. Error: ${e.message}`, tc.function.arguments); // If parsing fails, wrap the raw string in an object as a fallback, // since Claude's `input` field expects an object. inputObject = { "raw_arguments": tc.function.arguments }; @@ -2527,7 +2528,7 @@ async saveCredentialsToFile(filePath, newData) { const args = tc.function.arguments; inputObject = typeof args === 'string' ? JSON.parse(args) : args; } catch (e) { - console.warn(`[Kiro] Invalid JSON for tool call arguments. Wrapping in raw_arguments. Error: ${e.message}`, tc.function.arguments); + logger.warn(`[Kiro] Invalid JSON for tool call arguments. Wrapping in raw_arguments. Error: ${e.message}`, tc.function.arguments); // If parsing fails, wrap the raw string in an object as a fallback, // since Claude's `input` field expects an object. inputObject = { "raw_arguments": tc.function.arguments }; @@ -2589,7 +2590,7 @@ async saveCredentialsToFile(filePath, newData) { const bufferMs = 30 * 1000; return expirationTime.getTime() <= (currentTime.getTime() + bufferMs); } catch (error) { - console.error(`[Kiro] Error checking token expiry: ${error.message}`); + logger.error(`[Kiro] Error checking token expiry: ${error.message}`); return true; // Treat as expired if parsing fails } } @@ -2603,10 +2604,10 @@ async saveCredentialsToFile(filePath, newData) { const expirationTime = new Date(this.expiresAt); const nearMinutes = 30; const { message, isNearExpiry } = formatExpiryLog('Kiro', expirationTime.getTime(), nearMinutes); - console.log(message); + logger.info(message); return isNearExpiry; } catch (error) { - console.error(`[Kiro] Error checking expiry date: ${this.expiresAt}, Error: ${error.message}`); + logger.error(`[Kiro] Error checking expiry date: ${this.expiresAt}, Error: ${error.message}`); return false; // Treat as expired if parsing fails } } @@ -2615,11 +2616,11 @@ async saveCredentialsToFile(filePath, newData) { * 后台异步刷新 token(不阻塞当前请求) */ triggerBackgroundRefresh() { - console.log('[Kiro] Background token refresh started...'); + logger.info('[Kiro] Background token refresh started...'); this.initializeAuth(true).then(() => { - console.log('[Kiro] Background token refresh completed successfully'); + logger.info('[Kiro] Background token refresh completed successfully'); }).catch((error) => { - console.error('[Kiro] Background token refresh failed:', error.message); + logger.error('[Kiro] Background token refresh failed:', error.message); // 后台刷新失败不抛出错误,下次请求会重试 }); } @@ -2702,10 +2703,10 @@ async saveCredentialsToFile(filePath, newData) { // 1. 已过期 → 必须等待刷新 // 2. 即将过期但还能用 → 后台异步刷新,不阻塞当前请求 // if (this.isTokenExpired()) { - // console.log('[Kiro] Token is expired, must refresh before getUsageLimits request...'); + // logger.info('[Kiro] Token is expired, must refresh before getUsageLimits request...'); // await this.initializeAuth(true); // } else if (this.isExpiryDateNear()) { - // console.log('[Kiro] Token is near expiry, triggering background refresh...'); + // logger.info('[Kiro] Token is near expiry, triggering background refresh...'); // this.triggerBackgroundRefresh(); // } @@ -2745,7 +2746,7 @@ async saveCredentialsToFile(filePath, newData) { try { const response = await this.axiosInstance.get(fullUrl, { headers }); - console.log('[Kiro] Usage limits fetched successfully'); + logger.info('[Kiro] Usage limits fetched successfully'); return response.data; } catch (error) { const status = error.response?.status; @@ -2771,20 +2772,20 @@ async saveCredentialsToFile(filePath, newData) { // 对于用量查询,401/403 错误直接标记凭证为不健康,不重试 if (status === 401) { - console.log('[Kiro] Received 401 on getUsageLimits. Marking credential as unhealthy (no retry)...'); + logger.info('[Kiro] Received 401 on getUsageLimits. Marking credential as unhealthy (no retry)...'); this._markCredentialNeedRefresh('401 Unauthorized on usage query', formattedError); throw formattedError; } if (status === 403) { - console.log('[Kiro] Received 403 on getUsageLimits. Marking credential as unhealthy (no retry)...'); + logger.info('[Kiro] Received 403 on getUsageLimits. Marking credential as unhealthy (no retry)...'); // 检查是否为 temporarily suspended 错误 const isSuspended = errorMessage && errorMessage.toLowerCase().includes('temporarily is suspended'); if (isSuspended) { // temporarily suspended 错误:直接标记为不健康,不刷新 UUID - console.log('[Kiro] Account temporarily suspended on usage query. Marking as unhealthy without UUID refresh...'); + logger.info('[Kiro] Account temporarily suspended on usage query. Marking as unhealthy without UUID refresh...'); this._markCredentialUnhealthy('403 Forbidden - Account temporarily suspended on usage query', formattedError); } else { // 其他 403 错误:标记需要刷新 @@ -2794,8 +2795,9 @@ async saveCredentialsToFile(filePath, newData) { throw formattedError; } - console.error('[Kiro] Failed to fetch usage limits:', formattedError.message, error); + logger.error('[Kiro] Failed to fetch usage limits:', formattedError.message, error); throw formattedError; } } } + diff --git a/src/providers/claude/claude-strategy.js b/src/providers/claude/claude-strategy.js index e9c4981..1de7d01 100644 --- a/src/providers/claude/claude-strategy.js +++ b/src/providers/claude/claude-strategy.js @@ -1,4 +1,5 @@ import { ProviderStrategy } from '../../utils/provider-strategy.js'; +import logger from '../../utils/logger.js'; import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; /** @@ -59,7 +60,7 @@ class ClaudeStrategy extends ProviderStrategy { : filePromptContent; requestBody.system = newSystemText; - console.log(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'claude'.`); + logger.info(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'claude'.`); return requestBody; } @@ -71,3 +72,4 @@ class ClaudeStrategy extends ProviderStrategy { } export { ClaudeStrategy }; + diff --git a/src/providers/forward/forward-core.js b/src/providers/forward/forward-core.js index 56463f9..b2405b8 100644 --- a/src/providers/forward/forward-core.js +++ b/src/providers/forward/forward-core.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; import { configureAxiosProxy } from '../../utils/proxy-utils.js'; @@ -24,7 +25,7 @@ export class ForwardApiService { this.headerName = config?.FORWARD_HEADER_NAME || 'Authorization'; this.headerValuePrefix = config?.FORWARD_HEADER_VALUE_PREFIX || 'Bearer '; - console.log(`[Forward] Base URL: ${this.baseUrl}, System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); + logger.info(`[Forward] Base URL: ${this.baseUrl}, System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); const httpAgent = new http.Agent({ keepAlive: true, @@ -75,18 +76,18 @@ export class ForwardApiService { const isNetworkError = isRetryableNetworkError(error); if (status === 401 || status === 403) { - console.error(`[Forward API] Received ${status}. API Key might be invalid or expired.`); + logger.error(`[Forward API] Received ${status}. API Key might be invalid or expired.`); throw error; } if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Forward API] Error ${status || errorCode}. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Forward API] Error ${status || errorCode}. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } - console.error(`[Forward API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message); + logger.error(`[Forward API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message); throw error; } } @@ -120,7 +121,7 @@ export class ForwardApiService { yield parsedChunk; } catch (e) { // If it's not JSON, it might be a different format, but for a forwarder we try to parse common SSE formats - console.warn("[ForwardApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData); + logger.warn("[ForwardApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData); } } } @@ -132,7 +133,7 @@ export class ForwardApiService { if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Forward API] Stream error ${status || errorCode}. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Forward API] Stream error ${status || errorCode}. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; @@ -158,8 +159,9 @@ export class ForwardApiService { const response = await this.axiosInstance.get('/models'); return response.data; } catch (error) { - console.error(`Error listing Forward models:`, error.message); + logger.error(`Error listing Forward models:`, error.message); return { data: [] }; } } } + diff --git a/src/providers/forward/forward-strategy.js b/src/providers/forward/forward-strategy.js index 92b7486..b118741 100644 --- a/src/providers/forward/forward-strategy.js +++ b/src/providers/forward/forward-strategy.js @@ -51,3 +51,4 @@ class ForwardStrategy extends ProviderStrategy { } export { ForwardStrategy }; + diff --git a/src/providers/gemini/antigravity-core.js b/src/providers/gemini/antigravity-core.js index 32b5d85..15dc14b 100644 --- a/src/providers/gemini/antigravity-core.js +++ b/src/providers/gemini/antigravity-core.js @@ -1,5 +1,6 @@ import { OAuth2Client } from 'google-auth-library'; +import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; import * as crypto from 'crypto'; @@ -716,7 +717,7 @@ export class AntigravityApiService { if (proxyConfig) { oauth2Options.transporterOptions = proxyConfig; - console.log('[Antigravity] Using proxy for OAuth2Client'); + logger.info('[Antigravity] Using proxy for OAuth2Client'); } else { oauth2Options.transporterOptions = { agent: httpsAgent, @@ -762,7 +763,7 @@ export class AntigravityApiService { async initialize() { if (this.isInitialized) return; - console.log('[Antigravity] Initializing Antigravity API Service...'); + logger.info('[Antigravity] Initializing Antigravity API Service...'); // 注意:V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 仅执行基础的凭证加载 await this.loadCredentials(); @@ -770,13 +771,13 @@ export class AntigravityApiService { if (!this.projectId) { this.projectId = await this.discoverProjectAndModels(); } else { - console.log(`[Antigravity] Using provided Project ID: ${this.projectId}`); + logger.info(`[Antigravity] Using provided Project ID: ${this.projectId}`); // 获取可用模型 await this.fetchAvailableModels(); } this.isInitialized = true; - console.log(`[Antigravity] Initialization complete. Project ID: ${this.projectId}`); + logger.info(`[Antigravity] Initialization complete. Project ID: ${this.projectId}`); } /** @@ -788,12 +789,12 @@ export class AntigravityApiService { const data = await fs.readFile(credPath, "utf8"); const credentials = JSON.parse(data); this.authClient.setCredentials(credentials); - console.log('[Antigravity Auth] Credentials loaded successfully from file.'); + logger.info('[Antigravity Auth] Credentials loaded successfully from file.'); } catch (error) { if (error.code === 'ENOENT') { - console.debug(`[Antigravity Auth] Credentials file not found: ${credPath}`); + logger.debug(`[Antigravity Auth] Credentials file not found: ${credPath}`); } else { - console.warn(`[Antigravity Auth] Failed to load credentials from file: ${error.message}`); + logger.warn(`[Antigravity Auth] Failed to load credentials from file: ${error.message}`); } } } @@ -817,11 +818,11 @@ export class AntigravityApiService { if (needsRefresh || !this.authClient.credentials.access_token) { try { if (this.authClient.credentials.refresh_token) { - console.log('[Antigravity Auth] Token expiring soon or force refresh requested. Refreshing token...'); + logger.info('[Antigravity Auth] Token expiring soon or force refresh requested. Refreshing token...'); const { credentials: newCredentials } = await this.authClient.refreshAccessToken(); this.authClient.setCredentials(newCredentials); await this._saveCredentialsToFile(credPath, newCredentials); - console.log(`[Antigravity Auth] Token refreshed and saved to ${credPath} successfully.`); + logger.info(`[Antigravity Auth] Token refreshed and saved to ${credPath} successfully.`); // 刷新成功,重置 PoolManager 中的刷新状态并标记为健康 const poolManager = getProviderPoolManager(); @@ -829,10 +830,10 @@ export class AntigravityApiService { poolManager.resetProviderRefreshStatus(MODEL_PROVIDER.ANTIGRAVITY, this.uuid); } } else { - console.log(`[Antigravity Auth] No access token or refresh token. Starting new authentication flow...`); + logger.info(`[Antigravity Auth] No access token or refresh token. Starting new authentication flow...`); const newTokens = await this.getNewToken(credPath); this.authClient.setCredentials(newTokens); - console.log('[Antigravity Auth] New token obtained and loaded into memory.'); + logger.info('[Antigravity Auth] New token obtained and loaded into memory.'); // 认证成功,重置状态 const poolManager = getProviderPoolManager(); @@ -841,7 +842,7 @@ export class AntigravityApiService { } } } catch (error) { - console.error('[Antigravity Auth] Failed to initialize authentication:', error); + logger.error('[Antigravity Auth] Failed to initialize authentication:', error); throw new Error(`Failed to load OAuth credentials.`); } } @@ -851,12 +852,12 @@ export class AntigravityApiService { // 使用统一的 OAuth 处理方法 const { authUrl, authInfo } = await handleGeminiAntigravityOAuth(this.config); - console.log('\n[Antigravity Auth] 正在自动打开浏览器进行授权...'); - console.log('[Antigravity Auth] 授权链接:', authUrl, '\n'); + logger.info('\n[Antigravity Auth] 正在自动打开浏览器进行授权...'); + logger.info('[Antigravity Auth] 授权链接:', authUrl, '\n'); // 自动打开浏览器 const showFallbackMessage = () => { - console.log('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); + logger.info('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); }; if (this.config) { @@ -880,7 +881,7 @@ export class AntigravityApiService { const credentials = JSON.parse(data); if (credentials.access_token) { clearInterval(checkInterval); - console.log('[Antigravity Auth] New token obtained successfully.'); + logger.info('[Antigravity Auth] New token obtained successfully.'); resolve(credentials); } } catch (error) { @@ -914,20 +915,20 @@ export class AntigravityApiService { async _saveCredentialsToFile(filePath, credentials) { try { await fs.writeFile(filePath, JSON.stringify(credentials, null, 2)); - console.log(`[Antigravity Auth] Credentials saved to ${filePath}`); + logger.info(`[Antigravity Auth] Credentials saved to ${filePath}`); } catch (error) { - console.error(`[Antigravity Auth] Failed to save credentials to ${filePath}: ${error.message}`); + logger.error(`[Antigravity Auth] Failed to save credentials to ${filePath}: ${error.message}`); throw error; } } async discoverProjectAndModels() { if (this.projectId) { - console.log(`[Antigravity] Using pre-configured Project ID: ${this.projectId}`); + logger.info(`[Antigravity] Using pre-configured Project ID: ${this.projectId}`); return this.projectId; } - console.log('[Antigravity] Discovering Project ID...'); + logger.info('[Antigravity] Discovering Project ID...'); try { const initialProjectId = ""; // Prepare client metadata @@ -948,7 +949,7 @@ export class AntigravityApiService { // Check if we already have a project ID from the response if (loadResponse.cloudaicompanionProject) { - console.log(`[Antigravity] Discovered existing Project ID: ${loadResponse.cloudaicompanionProject}`); + logger.info(`[Antigravity] Discovered existing Project ID: ${loadResponse.cloudaicompanionProject}`); // 获取可用模型 await this.fetchAvailableModels(); return loadResponse.cloudaicompanionProject; @@ -981,15 +982,15 @@ export class AntigravityApiService { } const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId; - console.log(`[Antigravity] Onboarded and discovered Project ID: ${discoveredProjectId}`); + logger.info(`[Antigravity] Onboarded and discovered Project ID: ${discoveredProjectId}`); // 获取可用模型 await this.fetchAvailableModels(); return discoveredProjectId; } catch (error) { - console.error('[Antigravity] Failed to discover Project ID:', error.response?.data || error.message); - console.log('[Antigravity] Falling back to generated Project ID as last resort...'); + logger.error('[Antigravity] Failed to discover Project ID:', error.response?.data || error.message); + logger.info('[Antigravity] Falling back to generated Project ID as last resort...'); const fallbackProjectId = generateProjectID(); - console.log(`[Antigravity] Generated fallback Project ID: ${fallbackProjectId}`); + logger.info(`[Antigravity] Generated fallback Project ID: ${fallbackProjectId}`); // 获取可用模型 await this.fetchAvailableModels(); return fallbackProjectId; @@ -997,7 +998,7 @@ export class AntigravityApiService { } async fetchAvailableModels() { - console.log('[Antigravity] Fetching available models...'); + logger.info('[Antigravity] Fetching available models...'); for (const baseURL of this.baseURLs) { try { @@ -1014,7 +1015,7 @@ export class AntigravityApiService { }; const res = await this.authClient.request(requestOptions); - // console.log(`[Antigravity] Raw response from ${baseURL}:`, Object.keys(res.data.models)); + // logger.info(`[Antigravity] Raw response from ${baseURL}:`, Object.keys(res.data.models)); if (res.data && res.data.models) { const models = Object.keys(res.data.models); this.availableModels = models @@ -1022,15 +1023,15 @@ export class AntigravityApiService { .filter(alias => alias !== undefined && alias !== '' && alias !== null) .filter(alias => ANTIGRAVITY_MODELS.includes(alias)); - console.log(`[Antigravity] Available models: [${this.availableModels.join(', ')}]`); + logger.info(`[Antigravity] Available models: [${this.availableModels.join(', ')}]`); return; } } catch (error) { - console.error(`[Antigravity] Failed to fetch models from ${baseURL}:`, error.message); + logger.error(`[Antigravity] Failed to fetch models from ${baseURL}:`, error.message); } } - console.warn('[Antigravity] Failed to fetch models from all endpoints. Using default models.'); + logger.warn('[Antigravity] Failed to fetch models from all endpoints. Using default models.'); this.availableModels = ANTIGRAVITY_MODELS; } @@ -1104,15 +1105,15 @@ export class AntigravityApiService { // 检查是否为可重试的网络错误 const isNetworkError = isRetryableNetworkError(error); - console.error(`[Antigravity API] Error calling ${method} on ${baseURL}:`, status, error.message); + logger.error(`[Antigravity API] Error calling ${method} on ${baseURL}:`, status, error.message); if ((status === 400 || status === 401) && !isRetry) { - console.log('[Antigravity API] Received 401/400. Triggering background refresh via PoolManager...'); + logger.info('[Antigravity API] Received 401/400. Triggering background refresh via PoolManager...'); // 标记当前凭证为不健康(会自动进入刷新队列) const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Antigravity] Marking credential ${this.uuid} as needs refresh. Reason: 401/400 Unauthorized`); + logger.info(`[Antigravity] Marking credential ${this.uuid} as needs refresh. Reason: 401/400 Unauthorized`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.ANTIGRAVITY, { uuid: this.uuid }); @@ -1127,11 +1128,11 @@ export class AntigravityApiService { if (status === 429) { if (baseURLIndex + 1 < this.baseURLs.length) { - console.log(`[Antigravity API] Rate limited on ${baseURL}. Trying next base URL...`); + logger.info(`[Antigravity API] Rate limited on ${baseURL}. Trying next base URL...`); return this.callApi(method, body, isRetry, retryCount, baseURLIndex + 1); } else if (retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Antigravity API] Rate limited. Retrying in ${delay}ms...`); + logger.info(`[Antigravity API] Rate limited. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1, 0); } @@ -1141,12 +1142,12 @@ export class AntigravityApiService { if (isNetworkError) { if (baseURLIndex + 1 < this.baseURLs.length) { const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[Antigravity API] Network error (${errorIdentifier}) on ${baseURL}. Trying next base URL...`); + logger.info(`[Antigravity API] Network error (${errorIdentifier}) on ${baseURL}. Trying next base URL...`); return this.callApi(method, body, isRetry, retryCount, baseURLIndex + 1); } else if (retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[Antigravity API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Antigravity API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1, 0); } @@ -1154,7 +1155,7 @@ export class AntigravityApiService { if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Antigravity API] Server error ${status}. Retrying in ${delay}ms...`); + logger.info(`[Antigravity API] Server error ${status}. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1, baseURLIndex); } @@ -1206,15 +1207,15 @@ export class AntigravityApiService { // 检查是否为可重试的网络错误 const isNetworkError = isRetryableNetworkError(error); - console.error(`[Antigravity API] Error during stream ${method} on ${baseURL}:`, status, error.message); + logger.error(`[Antigravity API] Error during stream ${method} on ${baseURL}:`, status, error.message); if ((status === 400 || status === 401) && !isRetry) { - console.log('[Antigravity API] Received 401/400 during stream. Triggering background refresh via PoolManager...'); + logger.info('[Antigravity API] Received 401/400 during stream. Triggering background refresh via PoolManager...'); // 标记当前凭证为不健康(会自动进入刷新队列) const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Antigravity] Marking credential ${this.uuid} as needs refresh. Reason: 401/400 Unauthorized in stream`); + logger.info(`[Antigravity] Marking credential ${this.uuid} as needs refresh. Reason: 401/400 Unauthorized in stream`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.ANTIGRAVITY, { uuid: this.uuid }); @@ -1229,12 +1230,12 @@ export class AntigravityApiService { if (status === 429) { if (baseURLIndex + 1 < this.baseURLs.length) { - console.log(`[Antigravity API] Rate limited on ${baseURL}. Trying next base URL...`); + logger.info(`[Antigravity API] Rate limited on ${baseURL}. Trying next base URL...`); yield* this.streamApi(method, body, isRetry, retryCount, baseURLIndex + 1); return; } else if (retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Antigravity API] Rate limited during stream. Retrying in ${delay}ms...`); + logger.info(`[Antigravity API] Rate limited during stream. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1, 0); return; @@ -1245,13 +1246,13 @@ export class AntigravityApiService { if (isNetworkError) { if (baseURLIndex + 1 < this.baseURLs.length) { const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[Antigravity API] Network error (${errorIdentifier}) on ${baseURL} during stream. Trying next base URL...`); + logger.info(`[Antigravity API] Network error (${errorIdentifier}) on ${baseURL} during stream. Trying next base URL...`); yield* this.streamApi(method, body, isRetry, retryCount, baseURLIndex + 1); return; } else if (retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[Antigravity API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Antigravity API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1, 0); return; @@ -1260,7 +1261,7 @@ export class AntigravityApiService { if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Antigravity API] Server error ${status} during stream. Retrying in ${delay}ms...`); + logger.info(`[Antigravity API] Server error ${status} during stream. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1, baseURLIndex); return; @@ -1286,7 +1287,7 @@ export class AntigravityApiService { try { yield JSON.parse(buffer.join('\n')); } catch (e) { - console.error('[Antigravity Stream] Failed to parse JSON chunk:', buffer.join('\n')); + logger.error('[Antigravity Stream] Failed to parse JSON chunk:', buffer.join('\n')); } buffer = []; } @@ -1296,19 +1297,19 @@ export class AntigravityApiService { try { yield JSON.parse(buffer.join('\n')); } catch (e) { - console.error('[Antigravity Stream] Failed to parse final JSON chunk:', buffer.join('\n')); + logger.error('[Antigravity Stream] Failed to parse final JSON chunk:', buffer.join('\n')); } } } async generateContent(model, requestBody) { - console.log(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); + logger.info(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); // 检查 token 是否即将过期,如果是则推送到刷新队列 if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Antigravity] Token is near expiry, marking credential ${this.uuid} for refresh`); + logger.info(`[Antigravity] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.ANTIGRAVITY, { uuid: this.uuid }); @@ -1317,7 +1318,7 @@ export class AntigravityApiService { let selectedModel = model; if (!this.availableModels.includes(model)) { - console.warn(`[Antigravity] Model '${model}' not found. Using default model: '${this.availableModels[0]}'`); + logger.warn(`[Antigravity] Model '${model}' not found. Using default model: '${this.availableModels[0]}'`); selectedModel = this.availableModels[0]; } @@ -1363,19 +1364,19 @@ export class AntigravityApiService { const nonStreamResponse = convertStreamToNonStream(streamData); return toGeminiApiResponse(nonStreamResponse.response); } catch (error) { - console.error('[Antigravity] Claude non-stream execution error:', error.message); + logger.error('[Antigravity] Claude non-stream execution error:', error.message); throw error; } } async * generateContentStream(model, requestBody) { - console.log(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); + logger.info(`[Antigravity Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); // 检查 token 是否即将过期,如果是则推送到刷新队列 if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Antigravity] Token is near expiry, marking credential ${this.uuid} for refresh`); + logger.info(`[Antigravity] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.ANTIGRAVITY, { uuid: this.uuid }); @@ -1384,7 +1385,7 @@ export class AntigravityApiService { let selectedModel = model; if (!this.availableModels.includes(model)) { - console.warn(`[Antigravity] Model '${model}' not found. Using default model: '${this.availableModels[0]}'`); + logger.warn(`[Antigravity] Model '${model}' not found. Using default model: '${this.availableModels[0]}'`); selectedModel = this.availableModels[0]; } @@ -1408,10 +1409,10 @@ export class AntigravityApiService { try { const nearMinutes = 20; const { message, isNearExpiry } = formatExpiryLog('Antigravity', this.authClient.credentials.expiry_date, nearMinutes); - console.log(message); + logger.info(message); return isNearExpiry; } catch (error) { - console.error(`[Antigravity] Error checking expiry date: ${error.message}`); + logger.error(`[Antigravity] Error checking expiry date: ${error.message}`); return false; } } @@ -1426,7 +1427,7 @@ export class AntigravityApiService { // 注意:V2 架构下不再在 getUsageLimits 中同步刷新 token // 如果 token 过期,PoolManager 后台会自动处理 // if (this.isExpiryDateNear()) { - // console.log('[Antigravity] Token is near expiry, refreshing before getUsageLimits request...'); + // logger.info('[Antigravity] Token is near expiry, refreshing before getUsageLimits request...'); // await this.initializeAuth(true); // } @@ -1434,7 +1435,7 @@ export class AntigravityApiService { const modelsWithQuotas = await this.getModelsWithQuotas(); return modelsWithQuotas; } catch (error) { - console.error('[Antigravity] Failed to get usage limits:', error.message); + logger.error('[Antigravity] Failed to get usage limits:', error.message); throw error; } } @@ -1467,7 +1468,7 @@ export class AntigravityApiService { }; const res = await this.authClient.request(requestOptions); - console.log(`[Antigravity] fetchAvailableModels success`); + logger.info(`[Antigravity] fetchAvailableModels success`); if (res.data && res.data.models) { const modelsData = res.data.models; @@ -1498,19 +1499,19 @@ export class AntigravityApiService { sortedModels[key] = result.models[key]; }); result.models = sortedModels; - console.log(`[Antigravity] Successfully fetched quotas for ${Object.keys(result.models).length} models`); + logger.info(`[Antigravity] Successfully fetched quotas for ${Object.keys(result.models).length} models`); break; // 成功获取后退出循环 } } catch (error) { - console.error(`[Antigravity] Failed to fetch models with quotas from ${baseURL}:`, error.message); + logger.error(`[Antigravity] Failed to fetch models with quotas from ${baseURL}:`, error.message); } } return result; } catch (error) { - console.error('[Antigravity] Failed to get models with quotas:', error.message); + logger.error('[Antigravity] Failed to get models with quotas:', error.message); throw error; } } -} \ No newline at end of file +} diff --git a/src/providers/gemini/gemini-core.js b/src/providers/gemini/gemini-core.js index 3fb3e1a..14a3d41 100644 --- a/src/providers/gemini/gemini-core.js +++ b/src/providers/gemini/gemini-core.js @@ -1,4 +1,5 @@ import { OAuth2Client } from 'google-auth-library'; +import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; import { promises as fs } from 'fs'; @@ -207,7 +208,7 @@ export class GeminiApiService { if (proxyConfig) { oauth2Options.transporterOptions = proxyConfig; - console.log('[Gemini] Using proxy for OAuth2Client'); + logger.info('[Gemini] Using proxy for OAuth2Client'); } else { oauth2Options.transporterOptions = { agent: httpsAgent, @@ -233,7 +234,7 @@ export class GeminiApiService { async initialize() { if (this.isInitialized) return; - console.log('[Gemini] Initializing Gemini API Service...'); + logger.info('[Gemini] Initializing Gemini API Service...'); // 注意:V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 仅执行基础的凭证加载 await this.loadCredentials(); @@ -241,15 +242,15 @@ export class GeminiApiService { if (!this.projectId) { this.projectId = await this.discoverProjectAndModels(); } else { - console.log(`[Gemini] Using provided Project ID: ${this.projectId}`); + logger.info(`[Gemini] Using provided Project ID: ${this.projectId}`); this.availableModels = GEMINI_MODELS; - console.log(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`); + logger.info(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`); } if (this.projectId === 'default') { throw new Error("Error: 'default' is not a valid project ID. Please provide a valid Google Cloud Project ID using the --project-id argument."); } this.isInitialized = true; - console.log(`[Gemini] Initialization complete. Project ID: ${this.projectId}`); + logger.info(`[Gemini] Initialization complete. Project ID: ${this.projectId}`); } /** @@ -261,10 +262,10 @@ export class GeminiApiService { const decoded = Buffer.from(this.oauthCredsBase64, 'base64').toString('utf8'); const credentials = JSON.parse(decoded); this.authClient.setCredentials(credentials); - console.log('[Gemini Auth] Credentials loaded successfully from base64 string.'); + logger.info('[Gemini Auth] Credentials loaded successfully from base64 string.'); return; } catch (error) { - console.error('[Gemini Auth] Failed to parse base64 OAuth credentials:', error); + logger.error('[Gemini Auth] Failed to parse base64 OAuth credentials:', error); } } @@ -273,12 +274,12 @@ export class GeminiApiService { const data = await fs.readFile(credPath, "utf8"); const credentials = JSON.parse(data); this.authClient.setCredentials(credentials); - console.log('[Gemini Auth] Credentials loaded successfully from file.'); + logger.info('[Gemini Auth] Credentials loaded successfully from file.'); } catch (error) { if (error.code === 'ENOENT') { - console.debug(`[Gemini Auth] Credentials file not found: ${credPath}`); + logger.debug(`[Gemini Auth] Credentials file not found: ${credPath}`); } else { - console.warn(`[Gemini Auth] Failed to load credentials from file: ${error.message}`); + logger.warn(`[Gemini Auth] Failed to load credentials from file: ${error.message}`); } } } @@ -301,16 +302,16 @@ export class GeminiApiService { const credPath = this.oauthCredsFilePath || path.join(os.homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE); try { if (this.authClient.credentials.refresh_token) { - console.log('[Gemini Auth] Token expiring soon or force refresh requested. Refreshing token...'); + logger.info('[Gemini Auth] Token expiring soon or force refresh requested. Refreshing token...'); const { credentials: newCredentials } = await this.authClient.refreshAccessToken(); this.authClient.setCredentials(newCredentials); // 如果不是从 base64 加载的,则保存到文件 if (!this.oauthCredsBase64) { await this._saveCredentialsToFile(credPath, newCredentials); - console.log('[Gemini Auth] Token refreshed and saved successfully.'); + logger.info('[Gemini Auth] Token refreshed and saved successfully.'); } else { - console.log('[Gemini Auth] Token refreshed successfully (Base64 source).'); + logger.info('[Gemini Auth] Token refreshed successfully (Base64 source).'); } // 刷新成功,重置 PoolManager 中的刷新状态并标记为健康 @@ -319,10 +320,10 @@ export class GeminiApiService { poolManager.resetProviderRefreshStatus(MODEL_PROVIDER.GEMINI_CLI, this.uuid); } } else { - console.log(`[Gemini Auth] No access token or refresh token. Starting new authentication flow...`); + logger.info(`[Gemini Auth] No access token or refresh token. Starting new authentication flow...`); const newTokens = await this.getNewToken(credPath); this.authClient.setCredentials(newTokens); - console.log('[Gemini Auth] New token obtained and loaded into memory.'); + logger.info('[Gemini Auth] New token obtained and loaded into memory.'); // 认证成功,重置状态 const poolManager = getProviderPoolManager(); @@ -331,7 +332,7 @@ export class GeminiApiService { } } } catch (error) { - console.error('[Gemini Auth] Failed to initialize authentication:', error); + logger.error('[Gemini Auth] Failed to initialize authentication:', error); throw new Error(`Failed to load OAuth credentials.`); } } @@ -341,12 +342,12 @@ export class GeminiApiService { // 使用统一的 OAuth 处理方法 const { authUrl, authInfo } = await handleGeminiCliOAuth(this.config); - console.log('\n[Gemini Auth] 正在自动打开浏览器进行授权...'); - console.log('[Gemini Auth] 授权链接:', authUrl, '\n'); + logger.info('\n[Gemini Auth] 正在自动打开浏览器进行授权...'); + logger.info('[Gemini Auth] 授权链接:', authUrl, '\n'); // 自动打开浏览器 const showFallbackMessage = () => { - console.log('[Gemini Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); + logger.info('[Gemini Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); }; if (this.config) { @@ -370,7 +371,7 @@ export class GeminiApiService { const credentials = JSON.parse(data); if (credentials.access_token) { clearInterval(checkInterval); - console.log('[Gemini Auth] New token obtained successfully.'); + logger.info('[Gemini Auth] New token obtained successfully.'); resolve(credentials); } } catch (error) { @@ -388,13 +389,13 @@ export class GeminiApiService { async discoverProjectAndModels() { if (this.projectId) { - console.log(`[Gemini] Using pre-configured Project ID: ${this.projectId}`); + logger.info(`[Gemini] Using pre-configured Project ID: ${this.projectId}`); return this.projectId; } - console.log('[Gemini] Discovering Project ID...'); + logger.info('[Gemini] Discovering Project ID...'); this.availableModels = GEMINI_MODELS; - console.log(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`); + logger.info(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`); try { const initialProjectId = "" // Prepare client metadata @@ -447,7 +448,7 @@ export class GeminiApiService { const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId; return discoveredProjectId; } catch (error) { - console.error('[Gemini] Failed to discover Project ID:', error.response?.data || error.message); + logger.error('[Gemini] Failed to discover Project ID:', error.response?.data || error.message); throw new Error('Could not discover a valid Google Cloud Project ID.'); } } @@ -488,16 +489,16 @@ export class GeminiApiService { // 检查是否为可重试的网络错误 const isNetworkError = isRetryableNetworkError(error); - console.error(`[Gemini API] Error calling ${method}:`, status, error.message); + logger.error(`[Gemini API] Error calling ${method}:`, status, error.message); // Handle 401 (Unauthorized) - refresh auth and retry once if ((status === 400 || status === 401) && !isRetry) { - console.log('[Gemini API] Received 401/400. Triggering background refresh via PoolManager...'); + logger.info('[Gemini API] Received 401/400. Triggering background refresh via PoolManager...'); // 标记当前凭证为不健康(会自动进入刷新队列) const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Gemini] Marking credential ${this.uuid} as needs refresh. Reason: 401/400 Unauthorized`); + logger.info(`[Gemini] Marking credential ${this.uuid} as needs refresh. Reason: 401/400 Unauthorized`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.GEMINI_CLI, { uuid: this.uuid }); @@ -513,7 +514,7 @@ export class GeminiApiService { // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Gemini API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Gemini API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1); } @@ -521,7 +522,7 @@ export class GeminiApiService { // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Gemini API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Gemini API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1); } @@ -530,7 +531,7 @@ export class GeminiApiService { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[Gemini API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Gemini API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(method, body, isRetry, retryCount + 1); } @@ -567,16 +568,16 @@ export class GeminiApiService { // 检查是否为可重试的网络错误 const isNetworkError = isRetryableNetworkError(error); - console.error(`[Gemini API] Error during stream ${method}:`, status, error.message); + logger.error(`[Gemini API] Error during stream ${method}:`, status, error.message); // Handle 401 (Unauthorized) - refresh auth and retry once if ((status === 400 || status === 401) && !isRetry) { - console.log('[Gemini API] Received 401/400 during stream. Triggering background refresh via PoolManager...'); + logger.info('[Gemini API] Received 401/400 during stream. Triggering background refresh via PoolManager...'); // 标记当前凭证为不健康(会自动进入刷新队列) const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Gemini] Marking credential ${this.uuid} as needs refresh. Reason: 401/400 Unauthorized in stream`); + logger.info(`[Gemini] Marking credential ${this.uuid} as needs refresh. Reason: 401/400 Unauthorized in stream`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.GEMINI_CLI, { uuid: this.uuid }); @@ -592,7 +593,7 @@ export class GeminiApiService { // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Gemini API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Gemini API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1); return; @@ -601,7 +602,7 @@ export class GeminiApiService { // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Gemini API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Gemini API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1); return; @@ -611,7 +612,7 @@ export class GeminiApiService { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[Gemini API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[Gemini API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(method, body, isRetry, retryCount + 1); return; @@ -627,23 +628,23 @@ export class GeminiApiService { for await (const line of rl) { if (line.startsWith("data: ")) buffer.push(line.slice(6)); else if (line === "" && buffer.length > 0) { - try { yield JSON.parse(buffer.join('\n')); } catch (e) { console.error("[Stream] Failed to parse JSON chunk:", buffer.join('\n')); } + try { yield JSON.parse(buffer.join('\n')); } catch (e) { logger.error("[Stream] Failed to parse JSON chunk:", buffer.join('\n')); } buffer = []; } } if (buffer.length > 0) { - try { yield JSON.parse(buffer.join('\n')); } catch (e) { console.error("[Stream] Failed to parse final JSON chunk:", buffer.join('\n')); } + try { yield JSON.parse(buffer.join('\n')); } catch (e) { logger.error("[Stream] Failed to parse final JSON chunk:", buffer.join('\n')); } } } async generateContent(model, requestBody) { - console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); + logger.info(`[Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); // 检查 token 是否即将过期,如果是则推送到刷新队列 if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Gemini] Token is near expiry, marking credential ${this.uuid} for refresh`); + logger.info(`[Gemini] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.GEMINI_CLI, { uuid: this.uuid }); @@ -652,7 +653,7 @@ export class GeminiApiService { let selectedModel = model; if (!GEMINI_MODELS.includes(model)) { - console.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`); + logger.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`); selectedModel = GEMINI_MODELS[0]; } const processedRequestBody = ensureRolesInContents(requestBody); @@ -662,13 +663,13 @@ export class GeminiApiService { } async * generateContentStream(model, requestBody) { - console.log(`[Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); + logger.info(`[Auth Token] Time until expiry: ${formatExpiryTime(this.authClient.credentials.expiry_date)}`); // 检查 token 是否即将过期,如果是则推送到刷新队列 if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Gemini] Token is near expiry, marking credential ${this.uuid} for refresh`); + logger.info(`[Gemini] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.GEMINI_CLI, { uuid: this.uuid }); @@ -686,7 +687,7 @@ export class GeminiApiService { // 正常流处理 let selectedModel = model; if (!GEMINI_MODELS.includes(model)) { - console.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`); + logger.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`); selectedModel = GEMINI_MODELS[0]; } const processedRequestBody = ensureRolesInContents(requestBody); @@ -706,10 +707,10 @@ export class GeminiApiService { try { const nearMinutes = 20; const { message, isNearExpiry } = formatExpiryLog('Gemini', this.authClient.credentials.expiry_date, nearMinutes); - console.log(message); + logger.info(message); return isNearExpiry; } catch (error) { - console.error(`[Gemini] Error checking expiry date: ${error.message}`); + logger.error(`[Gemini] Error checking expiry date: ${error.message}`); return false; } } @@ -722,9 +723,9 @@ export class GeminiApiService { async _saveCredentialsToFile(filePath, credentials) { try { await fs.writeFile(filePath, JSON.stringify(credentials, null, 2)); - console.log(`[Gemini Auth] Credentials saved to ${filePath}`); + logger.info(`[Gemini Auth] Credentials saved to ${filePath}`); } catch (error) { - console.error(`[Gemini Auth] Failed to save credentials to ${filePath}: ${error.message}`); + logger.error(`[Gemini Auth] Failed to save credentials to ${filePath}: ${error.message}`); throw error; } } @@ -739,7 +740,7 @@ export class GeminiApiService { // 注意:V2 架构下不再在 getUsageLimits 中同步刷新 token // 如果 token 过期,PoolManager 后台会自动处理 // if (this.isExpiryDateNear()) { - // console.log('[Gemini] Token is near expiry, refreshing before getUsageLimits request...'); + // logger.info('[Gemini] Token is near expiry, refreshing before getUsageLimits request...'); // await this.initializeAuth(true); // } @@ -747,7 +748,7 @@ export class GeminiApiService { const modelsWithQuotas = await this.getModelsWithQuotas(); return modelsWithQuotas; } catch (error) { - console.error('[Gemini] Failed to get usage limits:', error.message); + logger.error('[Gemini] Failed to get usage limits:', error.message); throw error; } } @@ -781,7 +782,7 @@ export class GeminiApiService { }; const res = await this.authClient.request(requestOptions); - // console.log(`[Gemini] retrieveUserQuota success`, JSON.stringify(res.data)); + // logger.info(`[Gemini] retrieveUserQuota success`, JSON.stringify(res.data)); if (res.data && res.data.buckets) { const buckets = res.data.buckets; @@ -807,11 +808,11 @@ export class GeminiApiService { sortedModels[key] = result.models[key]; }); result.models = sortedModels; - // console.log(`[Gemini] Sorted Models:`, sortedModels); - console.log(`[Gemini] Successfully fetched quotas for ${Object.keys(result.models).length} models`); + // logger.info(`[Gemini] Sorted Models:`, sortedModels); + logger.info(`[Gemini] Successfully fetched quotas for ${Object.keys(result.models).length} models`); } } catch (fetchError) { - console.error(`[Gemini] Failed to fetch user quota:`, fetchError.message); + logger.error(`[Gemini] Failed to fetch user quota:`, fetchError.message); // 如果 retrieveUserQuota 失败,回退到使用固定模型列表 for (const modelId of GEMINI_MODELS) { @@ -825,8 +826,9 @@ export class GeminiApiService { return result; } catch (error) { - console.error('[Gemini] Failed to get models with quotas:', error.message); + logger.error('[Gemini] Failed to get models with quotas:', error.message); throw error; } } } + diff --git a/src/providers/gemini/gemini-strategy.js b/src/providers/gemini/gemini-strategy.js index 4a175be..6b2071d 100644 --- a/src/providers/gemini/gemini-strategy.js +++ b/src/providers/gemini/gemini-strategy.js @@ -1,4 +1,5 @@ import { API_ACTIONS, extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; +import logger from '../../utils/logger.js'; import { ProviderStrategy } from '../../utils/provider-strategy.js'; /** @@ -55,7 +56,7 @@ class GeminiStrategy extends ProviderStrategy { if (requestBody.system_instruction) { delete requestBody.system_instruction; } - console.log(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'gemini'.`); + logger.info(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'gemini'.`); return requestBody; } @@ -67,3 +68,4 @@ class GeminiStrategy extends ProviderStrategy { } export { GeminiStrategy }; + diff --git a/src/providers/openai/codex-core.js b/src/providers/openai/codex-core.js index e69f740..75b8bd8 100644 --- a/src/providers/openai/codex-core.js +++ b/src/providers/openai/codex-core.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import logger from '../../utils/logger.js'; import crypto from 'crypto'; import { promises as fs } from 'fs'; import path from 'path'; @@ -32,13 +33,13 @@ export class CodexApiService { */ async initialize() { if (this.isInitialized) return; - console.log('[Codex] Initializing Codex API Service...'); + logger.info('[Codex] Initializing Codex API Service...'); // 注意:V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 仅执行基础的凭证加载 await this.loadCredentials(); this.isInitialized = true; - console.log(`[Codex] Initialization complete. Account: ${this.email || 'unknown'}`); + logger.info(`[Codex] Initialization complete. Account: ${this.email || 'unknown'}`); } /** @@ -84,14 +85,14 @@ export class CodexApiService { // 检查 token 是否需要刷新 if (this.isExpiryDateNear()) { - console.log('[Codex] Token expiring soon, refreshing...'); + logger.info('[Codex] Token expiring soon, refreshing...'); await this.refreshAccessToken(); } this.isInitialized = true; - console.log(`[Codex] Initialized with account: ${this.email}`); + logger.info(`[Codex] Initialized with account: ${this.email}`); } catch (error) { - console.warn(`[Codex Auth] Failed to load credentials: ${error.message}`); + logger.warn(`[Codex Auth] Failed to load credentials: ${error.message}`); } } @@ -114,7 +115,7 @@ export class CodexApiService { if (!this.refreshToken) { throw new Error('Codex credentials not found. Please authenticate first using OAuth.'); } - console.log('[Codex] Token expiring soon or refresh requested, refreshing...'); + logger.info('[Codex] Token expiring soon or refresh requested, refreshing...'); await this.refreshAccessToken(); // 刷新成功,重置 PoolManager 中的刷新状态并标记为健康 @@ -137,7 +138,7 @@ export class CodexApiService { if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Codex] Token is near expiry, marking credential ${this.uuid} for refresh`); + logger.info(`[Codex] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, { uuid: this.uuid }); @@ -157,12 +158,12 @@ export class CodexApiService { return this.parseNonStreamResponse(response.data); } catch (error) { if (error.response?.status === 401) { - console.log('[Codex] Received 401. Triggering background refresh via PoolManager...'); + logger.info('[Codex] Received 401. Triggering background refresh via PoolManager...'); // 标记当前凭证为不健康(会自动进入刷新队列) const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Codex] Marking credential ${this.uuid} as needs refresh. Reason: 401 Unauthorized`); + logger.info(`[Codex] Marking credential ${this.uuid} as needs refresh. Reason: 401 Unauthorized`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, { uuid: this.uuid }); @@ -190,7 +191,7 @@ export class CodexApiService { if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Codex] Token is near expiry, marking credential ${this.uuid} for refresh`); + logger.info(`[Codex] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, { uuid: this.uuid }); @@ -211,12 +212,12 @@ export class CodexApiService { yield* this.parseSSEStream(response.data); } catch (error) { if (error.response?.status === 401) { - console.log('[Codex] Received 401 during stream. Triggering background refresh via PoolManager...'); + logger.info('[Codex] Received 401 during stream. Triggering background refresh via PoolManager...'); // 标记当前凭证为不健康 const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Codex] Marking credential ${this.uuid} as needs refresh. Reason: 401 Unauthorized in stream`); + logger.info(`[Codex] Marking credential ${this.uuid} as needs refresh. Reason: 401 Unauthorized in stream`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, { uuid: this.uuid }); @@ -293,9 +294,9 @@ export class CodexApiService { // 保存更新的凭据 await this.saveCredentials(); - console.log('[Codex] Token refreshed successfully'); + logger.info('[Codex] Token refreshed successfully'); } catch (error) { - console.error('[Codex] Failed to refresh token:', error.message); + logger.error('[Codex] Failed to refresh token:', error.message); throw new Error('Failed to refresh Codex token. Please re-authenticate.'); } } @@ -308,7 +309,7 @@ export class CodexApiService { const expiry = this.expiresAt.getTime(); const nearMinutes = 20; const { message, isNearExpiry } = formatExpiryLog('Codex', expiry, nearMinutes); - console.log(message); + logger.info(message); return isNearExpiry; } @@ -379,7 +380,7 @@ export class CodexApiService { const parsed = JSON.parse(data); yield parsed; } catch (e) { - console.error('[Codex] Failed to parse SSE data:', e.message); + logger.error('[Codex] Failed to parse SSE data:', e.message); } } } @@ -395,7 +396,7 @@ export class CodexApiService { const parsed = JSON.parse(data); yield parsed; } catch (e) { - console.error('[Codex] Failed to parse final SSE data:', e.message); + logger.error('[Codex] Failed to parse final SSE data:', e.message); } } } @@ -469,3 +470,4 @@ export class CodexApiService { } } } + diff --git a/src/providers/openai/iflow-core.js b/src/providers/openai/iflow-core.js index 5a662cf..a4c0df3 100644 --- a/src/providers/openai/iflow-core.js +++ b/src/providers/openai/iflow-core.js @@ -17,6 +17,7 @@ */ import axios from 'axios'; +import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; import { promises as fs } from 'fs'; @@ -115,12 +116,12 @@ async function loadTokenFromFile(filePath) { // 记录加载的 token 信息 const refreshToken = json.refreshToken || json.refresh_token || ''; - console.log(`[iFlow] Token loaded from: ${filePath} (refresh_token: ${refreshToken ? refreshToken.substring(0, 8) + '...' : 'EMPTY'})`); + logger.info(`[iFlow] Token loaded from: ${filePath} (refresh_token: ${refreshToken ? refreshToken.substring(0, 8) + '...' : 'EMPTY'})`); return IFlowTokenStorage.fromJSON(json); } catch (error) { if (error.code === 'ENOENT') { - console.warn(`[iFlow] Token file not found: ${filePath}`); + logger.warn(`[iFlow] Token file not found: ${filePath}`); return null; } throw new Error(`[iFlow] Failed to load token from file: ${error.message}`); @@ -147,15 +148,15 @@ async function saveTokenToFile(filePath, tokenStorage, uuid = null) { // 验证关键字段是否存在 if (!json.refresh_token || json.refresh_token.trim() === '') { - console.error('[iFlow] WARNING: Attempting to save token file with empty refresh_token!'); + logger.error('[iFlow] WARNING: Attempting to save token file with empty refresh_token!'); } if (!json.apiKey || json.apiKey.trim() === '') { - console.error('[iFlow] WARNING: Attempting to save token file with empty apiKey!'); + logger.error('[iFlow] WARNING: Attempting to save token file with empty apiKey!'); } await fs.writeFile(absolutePath, JSON.stringify(json, null, 2), 'utf-8'); - console.log(`[iFlow] Token saved to: ${filePath} (refresh_token: ${json.refresh_token ? json.refresh_token.substring(0, 8) + '...' : 'EMPTY'})`); + logger.info(`[iFlow] Token saved to: ${filePath} (refresh_token: ${json.refresh_token ? json.refresh_token.substring(0, 8) + '...' : 'EMPTY'})`); } catch (error) { throw new Error(`[iFlow] Failed to save token to file: ${error.message}`); } @@ -174,7 +175,7 @@ async function refreshOAuthTokens(refreshToken, axiosInstance = null) { throw new Error('[iFlow] refresh_token is empty'); } - console.log('[iFlow] Refreshing OAuth tokens...'); + logger.info('[iFlow] Refreshing OAuth tokens...'); // 构建请求参数 const params = new URLSearchParams(); @@ -205,9 +206,9 @@ async function refreshOAuthTokens(refreshToken, axiosInstance = null) { const tokenResp = response.data; - // console.log('[iFlow] Token response:', JSON.stringify(tokenResp)); + // logger.info('[iFlow] Token response:', JSON.stringify(tokenResp)); if (!tokenResp.access_token) { - console.error('[iFlow] Token response:', JSON.stringify(tokenResp)); + logger.error('[iFlow] Token response:', JSON.stringify(tokenResp)); throw new Error('[iFlow] Missing access_token in response'); } @@ -223,7 +224,7 @@ async function refreshOAuthTokens(refreshToken, axiosInstance = null) { expiryDate: expireTimestamp // 毫秒级时间戳 }; - console.log('[iFlow] OAuth tokens refreshed successfully'); + logger.info('[iFlow] OAuth tokens refreshed successfully'); // 获取用户信息以获取 API Key const userInfo = await fetchUserInfo(tokenData.accessToken, axiosInstance); @@ -236,7 +237,7 @@ async function refreshOAuthTokens(refreshToken, axiosInstance = null) { } catch (error) { const status = error.response?.status; const data = error.response?.data; - console.error(`[iFlow] OAuth token refresh failed (Status: ${status}):`, data || error.message); + logger.error(`[iFlow] OAuth token refresh failed (Status: ${status}):`, data || error.message); throw error; } } @@ -269,7 +270,7 @@ async function fetchUserInfo(accessToken, axiosInstance = null) { : await axios.request(requestConfig); const result = response.data; - // console.log('[iFlow] User info response:', JSON.stringify(result)); + // logger.info('[iFlow] User info response:', JSON.stringify(result)); if (!result.success) { throw new Error('[iFlow] User info request not successful'); } @@ -286,7 +287,7 @@ async function fetchUserInfo(accessToken, axiosInstance = null) { } catch (error) { const status = error.response?.status; const data = error.response?.data; - console.error(`[iFlow] Fetch user info failed (Status: ${status}):`, data || error.message); + logger.error(`[iFlow] Fetch user info failed (Status: ${status}):`, data || error.message); throw error; } } @@ -382,7 +383,7 @@ function preserveReasoningContentInMessages(body, model) { ); if (hasReasoningContent) { - console.log(`[iFlow] reasoning_content found in message history for ${model}`); + logger.info(`[iFlow] reasoning_content found in message history for ${model}`); } return body; @@ -493,13 +494,13 @@ export class IFlowApiService { async initialize() { if (this.isInitialized) return; - console.log('[iFlow] Initializing iFlow API Service...'); + logger.info('[iFlow] Initializing iFlow API Service...'); // 注意:V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 仅执行基础的凭证加载 await this.loadCredentials(); this.isInitialized = true; - console.log('[iFlow] Initialization complete.'); + logger.info('[iFlow] Initialization complete.'); } /** @@ -513,10 +514,10 @@ export class IFlowApiService { this.apiKey = this.tokenStorage.apiKey; // 更新 axios 实例的 Authorization header this.axiosInstance.defaults.headers['Authorization'] = `Bearer ${this.apiKey}`; - console.log('[iFlow Auth] Credentials loaded successfully from file'); + logger.info('[iFlow Auth] Credentials loaded successfully from file'); } } catch (error) { - console.warn(`[iFlow Auth] Failed to load credentials from file: ${error.message}`); + logger.warn(`[iFlow Auth] Failed to load credentials from file: ${error.message}`); } } @@ -540,17 +541,17 @@ export class IFlowApiService { // 从文件加载 if (!this.tokenStorage) { this.tokenStorage = await loadTokenFromFile(this.tokenFilePath); - console.log('[iFlow Auth] Loaded credentials from file'); + logger.info('[iFlow Auth] Loaded credentials from file'); } if (this.tokenStorage && this.tokenStorage.apiKey) { this.apiKey = this.tokenStorage.apiKey; - console.log('[iFlow Auth] Authentication configured successfully from file.'); + logger.info('[iFlow Auth] Authentication configured successfully from file.'); if (forceRefresh) { - console.log('[iFlow Auth] Forcing token refresh...'); + logger.info('[iFlow Auth] Forcing token refresh...'); await this._refreshOAuthTokens(); - console.log('[iFlow Auth] Token refreshed and saved successfully.'); + logger.info('[iFlow Auth] Token refreshed and saved successfully.'); // 刷新成功,重置 PoolManager 中的刷新状态并标记为健康 const poolManager = getProviderPoolManager(); @@ -562,7 +563,7 @@ export class IFlowApiService { throw new Error('[iFlow] No refresh token available in credentials.'); } } catch (error) { - console.error('[iFlow Auth] Failed to initialize authentication:', error.message); + logger.error('[iFlow Auth] Failed to initialize authentication:', error.message); throw new Error(`[iFlow Auth] Failed to load OAuth credentials.`); } } @@ -578,23 +579,23 @@ export class IFlowApiService { // 检查是否有 refresh_token if (!this.tokenStorage.refreshToken || this.tokenStorage.refreshToken.trim() === '') { - console.log('[iFlow] No refresh_token available, skipping token refresh check'); + logger.info('[iFlow] No refresh_token available, skipping token refresh check'); return false; } // 使用 isExpiryDateNear 检查过期时间 // if (!this.isExpiryDateNear()) { - // console.log('[iFlow] Token is valid, no refresh needed'); + // logger.info('[iFlow] Token is valid, no refresh needed'); // return false; // } - console.log('[iFlow] Token is expiring soon, attempting refresh...'); + logger.info('[iFlow] Token is expiring soon, attempting refresh...'); try { await this._refreshOAuthTokens(); return true; } catch (error) { - console.error('[iFlow] Token refresh failed:', error.message); + logger.error('[iFlow] Token refresh failed:', error.message); // 刷新失败不抛出异常,继续使用现有 Token return false; } @@ -611,7 +612,7 @@ export class IFlowApiService { const oldAccessToken = this.tokenStorage.accessToken; if (oldAccessToken) { - console.log(`[iFlow] Refreshing access token, old: ${this._maskToken(oldAccessToken)}`); + logger.info(`[iFlow] Refreshing access token, old: ${this._maskToken(oldAccessToken)}`); } // 调用刷新函数 @@ -625,7 +626,7 @@ export class IFlowApiService { // 记录 refresh_token 是否发生变化 if (tokenData.refreshToken !== oldRefreshToken) { - console.log(`[iFlow] refresh_token has been rotated (old: ${this._maskToken(oldRefreshToken)}, new: ${this._maskToken(tokenData.refreshToken)})`); + logger.info(`[iFlow] refresh_token has been rotated (old: ${this._maskToken(oldRefreshToken)}, new: ${this._maskToken(tokenData.refreshToken)})`); } if (tokenData.apiKey) { this.tokenStorage.apiKey = tokenData.apiKey; @@ -644,7 +645,7 @@ export class IFlowApiService { // 保存到文件 await saveTokenToFile(this.tokenFilePath, this.tokenStorage, this.uuid); - console.log(`[iFlow] Token refresh successful, new: ${this._maskToken(tokenData.accessToken)}`); + logger.info(`[iFlow] Token refresh successful, new: ${this._maskToken(tokenData.accessToken)}`); } /** @@ -672,7 +673,7 @@ export class IFlowApiService { await this._refreshOAuthTokens(); return true; } catch (error) { - console.error('[iFlow] Manual token refresh failed:', error.message); + logger.error('[iFlow] Manual token refresh failed:', error.message); return false; } } @@ -709,21 +710,21 @@ export class IFlowApiService { expireTime = new Date(expireValue.replace(' ', 'T') + ':00').getTime(); } } else { - console.error(`[iFlow] Invalid expiry date type: ${typeof expireValue}`); + logger.error(`[iFlow] Invalid expiry date type: ${typeof expireValue}`); return false; } if (isNaN(expireTime)) { - console.error(`[iFlow] Error parsing expiry date: ${expireValue}`); + logger.error(`[iFlow] Error parsing expiry date: ${expireValue}`); return false; } const { message, isNearExpiry } = formatExpiryLog('iFlow', expireTime, cronNearMinutes); - console.log(message); + logger.info(message); return isNearExpiry; } catch (error) { - console.error(`[iFlow] Error checking expiry date: ${error.message}`); + logger.error(`[iFlow] Error checking expiry date: ${error.message}`); return false; } } @@ -775,12 +776,12 @@ export class IFlowApiService { // Handle 401/400 - refresh auth and retry once if ((status === 400 || status === 401) && !isRetry) { - console.log(`[iFlow] Received ${status}. Triggering background refresh via PoolManager...`); + logger.info(`[iFlow] Received ${status}. Triggering background refresh via PoolManager...`); // 标记当前凭证为不健康(会自动进入刷新队列) const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[iFlow] Marking credential ${this.uuid} as needs refresh. Reason: ${status} Unauthorized`); + logger.info(`[iFlow] Marking credential ${this.uuid} as needs refresh. Reason: ${status} Unauthorized`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.IFLOW_API, { uuid: this.uuid }); @@ -794,14 +795,14 @@ export class IFlowApiService { } if (status === 401 || status === 403) { - console.error(`[iFlow] Received ${status}. API Key might be invalid or expired.`); + logger.error(`[iFlow] Received ${status}. API Key might be invalid or expired.`); throw error; } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[iFlow] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[iFlow] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, model, isRetry, retryCount + 1); } @@ -809,7 +810,7 @@ export class IFlowApiService { // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[iFlow] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[iFlow] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, model, isRetry, retryCount + 1); } @@ -818,12 +819,12 @@ export class IFlowApiService { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[iFlow] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[iFlow] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, model, isRetry, retryCount + 1); } - console.error(`[iFlow] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message); + logger.error(`[iFlow] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message); throw error; } } @@ -895,7 +896,7 @@ export class IFlowApiService { yield parsedChunk; } catch (e) { // JSON 解析失败,记录警告但继续处理 - console.warn("[iFlow] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData.substring(0, 200)); + logger.warn("[iFlow] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData.substring(0, 200)); } } // 忽略其他 SSE 字段(如 event:, id:, retry: 等) @@ -917,7 +918,7 @@ export class IFlowApiService { const parsedChunk = JSON.parse(jsonData); yield parsedChunk; } catch (e) { - console.warn("[iFlow] Failed to parse final stream chunk JSON:", e.message); + logger.warn("[iFlow] Failed to parse final stream chunk JSON:", e.message); } } } @@ -933,12 +934,12 @@ export class IFlowApiService { // Handle 401/400 during stream - refresh auth and retry once if ((status === 400 || status === 401) && !isRetry) { - console.log(`[iFlow] Received ${status} during stream. Triggering background refresh via PoolManager...`); + logger.info(`[iFlow] Received ${status} during stream. Triggering background refresh via PoolManager...`); // 标记当前凭证为不健康(会自动进入刷新队列) const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[iFlow] Marking credential ${this.uuid} as needs refresh. Reason: ${status} Unauthorized in stream`); + logger.info(`[iFlow] Marking credential ${this.uuid} as needs refresh. Reason: ${status} Unauthorized in stream`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.IFLOW_API, { uuid: this.uuid }); @@ -952,14 +953,14 @@ export class IFlowApiService { } if (status === 401 || status === 403) { - console.error(`[iFlow] Received ${status} during stream. API Key might be invalid or expired.`); + logger.error(`[iFlow] Received ${status} during stream. API Key might be invalid or expired.`); throw error; } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[iFlow] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[iFlow] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, model, isRetry, retryCount + 1); return; @@ -968,7 +969,7 @@ export class IFlowApiService { // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[iFlow] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[iFlow] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, model, isRetry, retryCount + 1); return; @@ -978,13 +979,13 @@ export class IFlowApiService { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[iFlow] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[iFlow] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, model, isRetry, retryCount + 1); return; } - console.error(`[iFlow] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, data || error.message); + logger.error(`[iFlow] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, data || error.message); throw error; } } @@ -1001,7 +1002,7 @@ export class IFlowApiService { if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[iFlow] Token is near expiry, marking credential ${this.uuid} for refresh`); + logger.info(`[iFlow] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.IFLOW_API, { uuid: this.uuid }); @@ -1023,7 +1024,7 @@ export class IFlowApiService { if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[iFlow] Token is near expiry, marking credential ${this.uuid} for refresh`); + logger.info(`[iFlow] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.IFLOW_API, { uuid: this.uuid }); @@ -1058,13 +1059,13 @@ export class IFlowApiService { created: Math.floor(Date.now() / 1000), owned_by: 'iflow' }); - console.log('[iFlow] Added glm-4.7 to models list'); + logger.info('[iFlow] Added glm-4.7 to models list'); } } return modelsData; } catch (error) { - console.warn('[iFlow] Failed to fetch models from API, using default list:', error.message); + logger.warn('[iFlow] Failed to fetch models from API, using default list:', error.message); // 返回默认模型列表,确保包含 glm-4.7 const defaultModels = [...IFLOW_MODELS]; if (!defaultModels.includes('glm-4.7')) { @@ -1097,4 +1098,4 @@ export { preserveReasoningContentInMessages, ensureToolsArray, preprocessRequestBody, -}; \ No newline at end of file +}; diff --git a/src/providers/openai/openai-core.js b/src/providers/openai/openai-core.js index fa7dabc..fbf7afb 100644 --- a/src/providers/openai/openai-core.js +++ b/src/providers/openai/openai-core.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; import { configureAxiosProxy } from '../../utils/proxy-utils.js'; @@ -14,7 +15,7 @@ export class OpenAIApiService { this.apiKey = config.OPENAI_API_KEY; this.baseUrl = config.OPENAI_BASE_URL; this.useSystemProxy = config?.USE_SYSTEM_PROXY_OPENAI ?? false; - console.log(`[OpenAI] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); + logger.info(`[OpenAI] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 const httpAgent = new http.Agent({ @@ -68,14 +69,14 @@ export class OpenAIApiService { const isNetworkError = isRetryableNetworkError(error); if (status === 401 || status === 403) { - console.error(`[OpenAI API] Received ${status}. API Key might be invalid or expired.`); + logger.error(`[OpenAI API] Received ${status}. API Key might be invalid or expired.`); throw error; } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[OpenAI API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[OpenAI API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } @@ -83,7 +84,7 @@ export class OpenAIApiService { // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[OpenAI API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[OpenAI API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } @@ -92,12 +93,12 @@ export class OpenAIApiService { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[OpenAI API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[OpenAI API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } - console.error(`[OpenAI API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message); + logger.error(`[OpenAI API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message); throw error; } } @@ -133,7 +134,7 @@ export class OpenAIApiService { const parsedChunk = JSON.parse(jsonData); yield parsedChunk; } catch (e) { - console.warn("[OpenAIApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData); + logger.warn("[OpenAIApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData); } } else if (line === '') { // Empty line, end of an event @@ -150,14 +151,14 @@ export class OpenAIApiService { const isNetworkError = isRetryableNetworkError(error); if (status === 401 || status === 403) { - console.error(`[OpenAI API] Received ${status} during stream. API Key might be invalid or expired.`); + logger.error(`[OpenAI API] Received ${status} during stream. API Key might be invalid or expired.`); throw error; } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[OpenAI API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[OpenAI API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; @@ -166,7 +167,7 @@ export class OpenAIApiService { // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[OpenAI API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[OpenAI API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; @@ -176,13 +177,13 @@ export class OpenAIApiService { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[OpenAI API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[OpenAI API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; } - console.error(`[OpenAI API] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, data || error.message); + logger.error(`[OpenAI API] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, data || error.message); throw error; } } @@ -202,8 +203,9 @@ export class OpenAIApiService { } catch (error) { const status = error.response?.status; const data = error.response?.data; - console.error(`Error listing OpenAI models (Status: ${status}):`, data || error.message); + logger.error(`Error listing OpenAI models (Status: ${status}):`, data || error.message); throw error; } } } + diff --git a/src/providers/openai/openai-responses-core.js b/src/providers/openai/openai-responses-core.js index 0af2b77..cbec8f4 100644 --- a/src/providers/openai/openai-responses-core.js +++ b/src/providers/openai/openai-responses-core.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; import { configureAxiosProxy } from '../../utils/proxy-utils.js'; @@ -13,7 +14,7 @@ export class OpenAIResponsesApiService { this.apiKey = config.OPENAI_API_KEY; this.baseUrl = config.OPENAI_BASE_URL || 'https://api.openai.com/v1'; this.useSystemProxy = config?.USE_SYSTEM_PROXY_OPENAI ?? false; - console.log(`[OpenAIResponses] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); + logger.info(`[OpenAIResponses] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 const httpAgent = new http.Agent({ @@ -61,14 +62,14 @@ export class OpenAIResponsesApiService { const status = error.response?.status; const data = error.response?.data; if (status === 401 || status === 403) { - console.error(`[API] Received ${status}. API Key might be invalid or expired.`); + logger.error(`[API] Received ${status}. API Key might be invalid or expired.`); throw error; } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } @@ -76,12 +77,12 @@ export class OpenAIResponsesApiService { // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApi(endpoint, body, isRetry, retryCount + 1); } - console.error(`Error calling OpenAI Responses API (Status: ${status}):`, data || error.message); + logger.error(`Error calling OpenAI Responses API (Status: ${status}):`, data || error.message); throw error; } } @@ -117,7 +118,7 @@ export class OpenAIResponsesApiService { const parsedChunk = JSON.parse(jsonData); yield parsedChunk; } catch (e) { - console.warn("[OpenAIResponsesApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData); + logger.warn("[OpenAIResponsesApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData); } } else if (line === '') { // Empty line, end of an event @@ -128,14 +129,14 @@ export class OpenAIResponsesApiService { const status = error.response?.status; const data = error.response?.data; if (status === 401 || status === 403) { - console.error(`[API] Received ${status} during stream. API Key might be invalid or expired.`); + logger.error(`[API] Received ${status} during stream. API Key might be invalid or expired.`); throw error; } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; @@ -144,13 +145,13 @@ export class OpenAIResponsesApiService { // Handle other retryable errors (5xx server errors) if (status >= 500 && status < 600 && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); return; } - console.error(`Error calling OpenAI Responses streaming API (Status: ${status}):`, data || error.message); + logger.error(`Error calling OpenAI Responses streaming API (Status: ${status}):`, data || error.message); throw error; } } @@ -170,8 +171,8 @@ export class OpenAIResponsesApiService { } catch (error) { const status = error.response?.status; const data = error.response?.data; - console.error(`Error listing OpenAI Responses models (Status: ${status}):`, data || error.message); + logger.error(`Error listing OpenAI Responses models (Status: ${status}):`, data || error.message); throw error; } } -} \ No newline at end of file +} diff --git a/src/providers/openai/openai-responses-strategy.js b/src/providers/openai/openai-responses-strategy.js index 74a679e..27f9080 100644 --- a/src/providers/openai/openai-responses-strategy.js +++ b/src/providers/openai/openai-responses-strategy.js @@ -1,4 +1,5 @@ import { ProviderStrategy } from '../../utils/provider-strategy.js'; +import logger from '../../utils/logger.js'; import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; /** @@ -96,7 +97,7 @@ class ResponsesAPIStrategy extends ProviderStrategy { requestBody.instructions = filePromptContent; } - console.log(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'responses'.`); + logger.info(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'responses'.`); return requestBody; } @@ -120,4 +121,4 @@ class ResponsesAPIStrategy extends ProviderStrategy { } } -export { ResponsesAPIStrategy }; \ No newline at end of file +export { ResponsesAPIStrategy }; diff --git a/src/providers/openai/openai-strategy.js b/src/providers/openai/openai-strategy.js index b5231d9..801b3d4 100644 --- a/src/providers/openai/openai-strategy.js +++ b/src/providers/openai/openai-strategy.js @@ -1,4 +1,5 @@ import { ProviderStrategy } from '../../utils/provider-strategy.js'; +import logger from '../../utils/logger.js'; import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; /** @@ -69,16 +70,17 @@ class OpenAIStrategy extends ProviderStrategy { } else { requestBody.messages.unshift({ role: 'system', content: newSystemText }); } - console.log(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'openai'.`); + logger.info(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'openai'.`); return requestBody; } async manageSystemPrompt(requestBody) { - //console.log('[System Prompt] Managing system prompt for provider "openai".', requestBody); + //logger.info('[System Prompt] Managing system prompt for provider "openai".', requestBody); const incomingSystemText = extractSystemPromptFromRequestBody(requestBody, MODEL_PROTOCOL_PREFIX.OPENAI); await this._updateSystemPromptFile(incomingSystemText, MODEL_PROTOCOL_PREFIX.OPENAI); } } export { OpenAIStrategy }; + diff --git a/src/providers/openai/qwen-core.js b/src/providers/openai/qwen-core.js index c91c1e4..753abca 100644 --- a/src/providers/openai/qwen-core.js +++ b/src/providers/openai/qwen-core.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import logger from '../../utils/logger.js'; import crypto from 'crypto'; import path from 'node:path'; import { promises as fs, unlinkSync } from 'node:fs'; @@ -75,7 +76,7 @@ async function commonFetch(url, options = {}, useSystemProxy = false) { if (!useSystemProxy && typeof mergedOptions.agent === 'undefined') { // 对于 Node.js fetch,我们可以通过设置 dispatcher 来控制代理 // 但这需要 undici 支持,这里我们先记录日志 - console.debug('[Qwen] System proxy disabled for fetch request'); + logger.debug('[Qwen] System proxy disabled for fetch request'); } const response = await fetch(url, mergedOptions); @@ -188,13 +189,13 @@ export class QwenApiService { this.oauthDeviceCodeEndpoint = `${oauthBaseUrl}/api/v1/oauth2/device/code`; this.oauthTokenEndpoint = `${oauthBaseUrl}/api/v1/oauth2/token`; - console.log(`[Qwen] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); + logger.info(`[Qwen] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`); this.qwenClient = new QwenOAuth2Client(config, this.useSystemProxy); } async initialize() { if (this.isInitialized) return; - console.log('[Qwen] Initializing Qwen API Service...'); + logger.info('[Qwen] Initializing Qwen API Service...'); // 注意:V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 仅执行基础的凭证加载 await this.loadCredentials(); @@ -234,7 +235,7 @@ export class QwenApiService { this.currentAxiosInstance = axios.create(axiosConfig); this.isInitialized = true; - console.log('[Qwen] Initialization complete.'); + logger.info('[Qwen] Initialization complete.'); } /** @@ -246,12 +247,12 @@ export class QwenApiService { const creds = await fs.readFile(keyFile, 'utf-8'); const credentials = JSON.parse(creds); this.qwenClient.setCredentials(credentials); - console.log('[Qwen Auth] Credentials loaded successfully from file.'); + logger.info('[Qwen Auth] Credentials loaded successfully from file.'); } catch (error) { if (error.code === 'ENOENT') { - console.debug('[Qwen Auth] No cached credentials found.'); + logger.debug('[Qwen Auth] No cached credentials found.'); } else { - console.warn(`[Qwen Auth] Failed to load credentials from file: ${error.message}`); + logger.warn(`[Qwen Auth] Failed to load credentials from file: ${error.message}`); } } } @@ -266,7 +267,7 @@ export class QwenApiService { forceRefresh, this.tokenManagerOptions, ); - // console.log('credentials', credentials); + // logger.info('credentials', credentials); this.qwenClient.setCredentials(credentials); // 如果执行了刷新或认证,重置状态 @@ -277,27 +278,27 @@ export class QwenApiService { } } } catch (error) { - console.debug('Shared token manager failed, attempting device flow:', error); + logger.debug('Shared token manager failed, attempting device flow:', error); if (error instanceof TokenManagerError) { switch (error.type) { case TokenError.NO_REFRESH_TOKEN: - console.debug('No refresh token available, proceeding with device flow'); + logger.debug('No refresh token available, proceeding with device flow'); break; case TokenError.REFRESH_FAILED: - console.debug('Token refresh failed, proceeding with device flow'); + logger.debug('Token refresh failed, proceeding with device flow'); break; case TokenError.NETWORK_ERROR: - console.warn('Network error during token refresh, trying device flow'); + logger.warn('Network error during token refresh, trying device flow'); break; default: - console.warn('Token manager error:', error.message); + logger.warn('Token manager error:', error.message); } } // If cached credentials are present and still valid, use them directly. if (await this._loadCachedQwenCredentials(this.qwenClient)) { - console.log('[Qwen] Using cached OAuth credentials.'); + logger.info('[Qwen] Using cached OAuth credentials.'); return; } @@ -355,10 +356,10 @@ export class QwenApiService { }); const showFallbackMessage = () => { - console.log('\n=== Qwen OAuth Device Authorization ==='); - console.log('Please visit the following URL in your browser to authorize:'); - console.log(`\n${authUrl}\n`); - console.log('Waiting for authorization to complete...\n'); + logger.info('\n=== Qwen OAuth Device Authorization ==='); + logger.info('Please visit the following URL in your browser to authorize:'); + logger.info(`\n${authUrl}\n`); + logger.info('Waiting for authorization to complete...\n'); }; if (config) { @@ -375,7 +376,7 @@ export class QwenApiService { } qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'polling', 'Waiting for authorization...'); - console.debug('Waiting for authorization...\n'); + logger.debug('Waiting for authorization...\n'); // 等待 OAuth 回调完成并读取保存的凭据 const credPath = this._getQwenCachedCredentialPath(); @@ -386,7 +387,7 @@ export class QwenApiService { const creds = JSON.parse(data); if (creds.access_token) { clearInterval(checkInterval); - console.log('[Qwen Auth] New token obtained successfully.'); + logger.info('[Qwen Auth] New token obtained successfully.'); resolve(creds); } } catch (error) { @@ -405,7 +406,7 @@ export class QwenApiService { qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'success', 'Authentication successful! Access token obtained.'); return { success: true }; } catch (error) { - console.error('Device authorization flow failed:', error.message); + logger.error('Device authorization flow failed:', error.message); qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', error.message); return { success: false, reason: 'error' }; } @@ -439,9 +440,9 @@ export class QwenApiService { await fs.mkdir(path.dirname(filePath), { recursive: true }); const credString = JSON.stringify(credentials, null, 2); await fs.writeFile(filePath, credString); - console.log(`[Qwen Auth] Credentials cached to ${filePath}`); + logger.info(`[Qwen Auth] Credentials cached to ${filePath}`); } catch (error) { - console.error(`[Qwen Auth] Failed to cache credentials to ${filePath}: ${error.message}`); + logger.error(`[Qwen Auth] Failed to cache credentials to ${filePath}: ${error.message}`); } } @@ -490,7 +491,7 @@ export class QwenApiService { }; } catch (error) { if (this.isAuthError(error)) throw error; - console.warn('Failed to get token from shared manager:', error); + logger.warn('Failed to get token from shared manager:', error); throw new Error('Failed to obtain valid Qwen access token. Please re-authenticate.'); } } @@ -532,7 +533,7 @@ export class QwenApiService { const version = "0.2.1"; const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; - console.log(`[QwenApiService] User-Agent: ${userAgent}`); + logger.info(`[QwenApiService] User-Agent: ${userAgent}`); try { const { token, endpoint: qwenBaseUrl } = await this.getValidToken(); @@ -579,7 +580,7 @@ export class QwenApiService { // Check if model in body is in QWEN_MODEL_LIST, if not, use the first model's id if (processedBody.model && !QWEN_MODEL_LIST.some(model => model.id === processedBody.model)) { - console.warn(`[QwenApiService] Model '${processedBody.model}' not found. Using default model: '${QWEN_MODEL_LIST[0].id}'`); + logger.warn(`[QwenApiService] Model '${processedBody.model}' not found. Using default model: '${QWEN_MODEL_LIST[0].id}'`); processedBody.model = QWEN_MODEL_LIST[0].id; } @@ -610,12 +611,12 @@ export class QwenApiService { const isNetworkError = isRetryableNetworkError(error); if (this.isAuthError(error) && retryCount === 0) { - console.warn(`[QwenApiService] Auth error (${status}). Triggering background refresh via PoolManager...`); + logger.warn(`[QwenApiService] Auth error (${status}). Triggering background refresh via PoolManager...`); // 标记当前凭证为不健康(会自动进入刷新队列) const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Qwen] Marking credential ${this.uuid} as needs refresh. Reason: Auth Error ${status}`); + logger.info(`[Qwen] Marking credential ${this.uuid} as needs refresh. Reason: Auth Error ${status}`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.QWEN_API, { uuid: this.uuid }); @@ -630,7 +631,7 @@ export class QwenApiService { if ((status === 429 || (status >= 500 && status < 600)) && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[QwenApiService] Status ${status}. Retrying in ${delay}ms...`); + logger.info(`[QwenApiService] Status ${status}. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1); } @@ -639,12 +640,12 @@ export class QwenApiService { if (isNetworkError && retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); const errorIdentifier = errorCode || errorMessage.substring(0, 50); - console.log(`[QwenApiService] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); + logger.info(`[QwenApiService] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1); } - console.error(`[QwenApiService] Error calling API (Status: ${status}, Code: ${errorCode}):`, data); + logger.error(`[QwenApiService] Error calling API (Status: ${status}, Code: ${errorCode}):`, data); throw error; } } @@ -654,7 +655,7 @@ export class QwenApiService { if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Qwen] Token is near expiry, marking credential ${this.uuid} for refresh`); + logger.info(`[Qwen] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.QWEN_API, { uuid: this.uuid }); @@ -669,7 +670,7 @@ export class QwenApiService { if (this.isExpiryDateNear()) { const poolManager = getProviderPoolManager(); if (poolManager && this.uuid) { - console.log(`[Qwen] Token is near expiry, marking credential ${this.uuid} for refresh`); + logger.info(`[Qwen] Token is near expiry, marking credential ${this.uuid} for refresh`); poolManager.markProviderNeedRefresh(MODEL_PROVIDER.QWEN_API, { uuid: this.uuid }); @@ -691,7 +692,7 @@ export class QwenApiService { try { yield JSON.parse(jsonData); } catch (e) { - console.warn("[QwenApiService] Failed to parse stream chunk:", jsonData); + logger.warn("[QwenApiService] Failed to parse stream chunk:", jsonData); } } } @@ -713,10 +714,10 @@ export class QwenApiService { } const nearMinutes = 20; const { message, isNearExpiry } = formatExpiryLog('Qwen', credentials.expiry_date, nearMinutes); - console.log(message); + logger.info(message); return isNearExpiry; } catch (error) { - console.error(`[Qwen] Error checking expiry date: ${error.message}`); + logger.error(`[Qwen] Error checking expiry date: ${error.message}`); return false; } } @@ -890,7 +891,7 @@ class SharedTokenManager { context.memoryCache.credentials = newCredentials; qwenClient.setCredentials(newCredentials); await this.saveCredentialsToFile(context, newCredentials); - console.log('[Qwen Auth] Token refresh response: ok'); + logger.info('[Qwen Auth] Token refresh response: ok'); return newCredentials; } finally { await this.releaseLock(context); @@ -902,7 +903,7 @@ class SharedTokenManager { if (error instanceof CredentialsClearRequiredError) { try { await fs.unlink(context.credentialFilePath); - console.log('[Qwen Auth] Credentials cleared due to refresh token expiry'); + logger.info('[Qwen Auth] Credentials cleared due to refresh token expiry'); } catch (_) { /* ignore */ } throw error; // 重新抛出以便上层处理 } @@ -926,7 +927,7 @@ class SharedTokenManager { const stats = await fs.stat(context.credentialFilePath); context.memoryCache.fileModTime = stats.mtimeMs; } catch (error) { - console.error(`[Qwen Auth] Failed to save credentials to ${context.credentialFilePath}: ${error.message}`); + logger.error(`[Qwen Auth] Failed to save credentials to ${context.credentialFilePath}: ${error.message}`); } } @@ -967,7 +968,7 @@ class SharedTokenManager { await fs.unlink(context.lockFilePath); } catch (error) { if (error.code !== 'ENOENT') { - console.warn(`Failed to release lock: ${error.message}`); + logger.warn(`Failed to release lock: ${error.message}`); } } } @@ -1084,3 +1085,4 @@ class QwenOAuth2Client { } } } + diff --git a/src/providers/provider-models.js b/src/providers/provider-models.js index aae4335..5a35d33 100644 --- a/src/providers/provider-models.js +++ b/src/providers/provider-models.js @@ -91,4 +91,4 @@ export function getProviderModels(providerType) { */ export function getAllProviderModels() { return PROVIDER_MODELS; -} \ No newline at end of file +} diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js index 30b6965..93f3246 100644 --- a/src/providers/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -1,6 +1,7 @@ -import * as fs from 'fs'; // Import fs module -import * as crypto from 'crypto'; // Import crypto module for UUID generation +import * as fs from 'fs'; +import * as crypto from 'crypto'; import { getServiceAdapter } from './adapter.js'; +import logger from '../utils/logger.js'; import { MODEL_PROVIDER, getProtocolPrefix } from '../utils/common.js'; import { getProviderModels } from './provider-models.js'; import axios from 'axios'; @@ -104,7 +105,7 @@ export class ProviderPoolManager { configPath = config.CODEX_OAUTH_CREDS_FILE_PATH; } - // console.log(`Checking node ${providerStatus.uuid} (${providerType}) expiry date... configPath: ${configPath}`); + // logger.info(`Checking node ${providerStatus.uuid} (${providerType}) expiry date... configPath: ${configPath}`); // 排除不健康和禁用的节点 if (!config.isHealthy || config.isDisabled) continue; @@ -411,11 +412,10 @@ export class ProviderPoolManager { // 1. 基础健康分:不健康的排最后 if (!config.isHealthy || config.isDisabled) return 1e18; - // 2. 预热/刷新分:2分钟内刷新过且使用次数极少的节点视为“新鲜”,分数极低(最高优) - const isFresh = config.lastHealthCheckTime && - (now - new Date(config.lastHealthCheckTime).getTime() < 120000) && - (config.usageCount === 0); - if (isFresh) return -2e18; // 极其优先 + // 2. 预热/刷新分:60秒内刷新过且使用次数极少的节点视为“新鲜”,分数极低(最高优) + const lastHealthCheckTime = config.lastHealthCheckTime ? new Date(config.lastHealthCheckTime).getTime() : 0; + const isFresh = lastHealthCheckTime && (now - lastHealthCheckTime < 60000); + if (isFresh) return -2e18 + (config.usageCount || 0) * 10000 + (now - lastHealthCheckTime); // 极其优先 // 3. 权重计算逻辑: // 改进点:使用 lastUsedTime + usageCount 惩罚 + selectionSequence 惩罚 @@ -453,8 +453,7 @@ export class ProviderPoolManager { _log(level, message) { const levels = { debug: 0, info: 1, warn: 2, error: 3 }; if (levels[level] >= levels[this.logLevel]) { - const logMethod = level === 'debug' ? 'log' : level; - console[logMethod](`[ProviderPoolManager] ${message}`); + logger[level](`[ProviderPoolManager] ${message}`); } } @@ -903,9 +902,7 @@ export class ProviderPoolManager { if (provider.config.errorCount >= this.maxErrorCount) { provider.config.isHealthy = false; this._log('warn', `Marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Total errors: ${provider.config.errorCount}`); - } else { - this._log('warn', `Provider ${providerConfig.uuid} for type ${providerType} error count: ${provider.config.errorCount}/${this.maxErrorCount}. Still healthy.`); - } + } this._debouncedSave(providerType); } @@ -1002,8 +999,8 @@ export class ProviderPoolManager { provider.config.lastErrorMessage = null; // 更新健康检测信息 - provider.config.lastHealthCheckTime = new Date().toISOString(); if (healthCheckModel) { + provider.config.lastHealthCheckTime = new Date().toISOString(); provider.config.lastHealthCheckModel = healthCheckModel; } @@ -1456,3 +1453,4 @@ export class ProviderPoolManager { } } + diff --git a/src/services/api-manager.js b/src/services/api-manager.js index 513b35b..d9cb526 100644 --- a/src/services/api-manager.js +++ b/src/services/api-manager.js @@ -5,7 +5,7 @@ import { ENDPOINT_TYPE } from '../utils/common.js'; import { getProviderPoolManager } from './service-manager.js'; - +import logger from '../utils/logger.js'; /** * Handle API authentication and routing * @param {string} method - The HTTP method @@ -65,7 +65,7 @@ export async function handleAPIRequests(method, path, req, res, currentConfig, a export function initializeAPIManagement(services) { const providerPoolManager = getProviderPoolManager(); return async function heartbeatAndRefreshToken() { - console.log(`[Heartbeat] Server is running. Current time: ${new Date().toLocaleString()}`, Object.keys(services)); + logger.info(`[Heartbeat] Server is running. Current time: ${new Date().toLocaleString()}`, Object.keys(services)); // 循环遍历所有已初始化的服务适配器,并尝试刷新令牌 // if (getProviderPoolManager()) { // await getProviderPoolManager().performHealthChecks(); // 定期执行健康检查 @@ -83,9 +83,9 @@ export function initializeAPIManagement(services) { } else { await serviceAdapter.refreshToken(); } - // console.log(`[Token Refresh] Refreshed token for ${providerKey}`); + // logger.info(`[Token Refresh] Refreshed token for ${providerKey}`); } catch (error) { - console.error(`[Token Refresh Error] Failed to refresh token for ${providerKey}: ${error.message}`); + logger.error(`[Token Refresh Error] Failed to refresh token for ${providerKey}: ${error.message}`); // 如果是号池中的某个实例刷新失败,这里需要捕获并更新其状态 // 现有的 serviceInstances 存储的是每个配置对应的单例,而非池中的成员 // 这意味着如果一个池成员的 token 刷新失败,需要找到它并更新其在 poolManager 中的状态 diff --git a/src/services/api-server.js b/src/services/api-server.js index ebdbb85..d0d15df 100644 --- a/src/services/api-server.js +++ b/src/services/api-server.js @@ -1,3 +1,4 @@ +import logger from '../utils/logger.js'; import * as http from 'http'; import { initializeConfig, CONFIG } from '../core/config-manager.js'; import { initApiService, autoLinkProviderConfigs } from './service-manager.js'; @@ -141,11 +142,11 @@ function setupWorkerCommunication() { process.on('message', (message) => { if (!message || !message.type) return; - console.log('[Worker] Received message from master:', message.type); + logger.info('[Worker] Received message from master:', message.type); switch (message.type) { case 'shutdown': - console.log('[Worker] Shutdown requested by master'); + logger.info('[Worker] Shutdown requested by master'); gracefulShutdown(); break; case 'status': @@ -159,13 +160,13 @@ function setupWorkerCommunication() { }); break; default: - console.log('[Worker] Unknown message type:', message.type); + logger.info('[Worker] Unknown message type:', message.type); } }); // 监听断开连接 process.on('disconnect', () => { - console.log('[Worker] Disconnected from master, shutting down...'); + logger.info('[Worker] Disconnected from master, shutting down...'); gracefulShutdown(); }); } @@ -174,17 +175,17 @@ function setupWorkerCommunication() { * 优雅关闭服务器 */ async function gracefulShutdown() { - console.log('[Server] Initiating graceful shutdown...'); + logger.info('[Server] Initiating graceful shutdown...'); if (serverInstance) { serverInstance.close(() => { - console.log('[Server] HTTP server closed'); + logger.info('[Server] HTTP server closed'); process.exit(0); }); // 设置超时,防止无限等待 setTimeout(() => { - console.log('[Server] Shutdown timeout, forcing exit...'); + logger.info('[Server] Shutdown timeout, forcing exit...'); process.exit(1); }, 10000); } else { @@ -197,22 +198,22 @@ async function gracefulShutdown() { */ function setupSignalHandlers() { process.on('SIGTERM', () => { - console.log('[Server] Received SIGTERM'); + logger.info('[Server] Received SIGTERM'); gracefulShutdown(); }); process.on('SIGINT', () => { - console.log('[Server] Received SIGINT'); + logger.info('[Server] Received SIGINT'); gracefulShutdown(); }); process.on('uncaughtException', (error) => { - console.error('[Server] Uncaught exception:', error); + logger.error('[Server] Uncaught exception:', error); gracefulShutdown(); }); process.on('unhandledRejection', (reason, promise) => { - console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason); + logger.error('[Server] Unhandled rejection at:', promise, 'reason:', reason); }); } @@ -222,11 +223,11 @@ async function startServer() { await initializeConfig(process.argv.slice(2), 'configs/config.json'); // 自动关联 configs 目录中的配置文件到对应的提供商 - // console.log('[Initialization] Checking for unlinked provider configs...'); + // logger.info('[Initialization] Checking for unlinked provider configs...'); // await autoLinkProviderConfigs(CONFIG); // Initialize plugin system - console.log('[Initialization] Discovering and initializing plugins...'); + logger.info('[Initialization] Discovering and initializing plugins...'); await discoverPlugins(); const pluginManager = getPluginManager(); await pluginManager.initAll(CONFIG); @@ -234,10 +235,10 @@ async function startServer() { // Log loaded plugins const pluginList = pluginManager.getPluginList(); if (pluginList.length > 0) { - console.log(`[Plugins] Loaded ${pluginList.length} plugin(s):`); + logger.info(`[Plugins] Loaded ${pluginList.length} plugin(s):`); pluginList.forEach(p => { const status = p.enabled ? '✓' : '✗'; - console.log(` ${status} ${p.name} v${p.version} - ${p.description}`); + logger.info(` ${status} ${p.name} v${p.version} - ${p.description}`); }); } @@ -263,29 +264,29 @@ async function startServer() { // 设置服务器的最大连接数 serverInstance.maxConnections = 1000; serverInstance.listen(CONFIG.SERVER_PORT, CONFIG.HOST, async () => { - console.log(`--- Unified API Server Configuration ---`); + logger.info(`--- Unified API Server Configuration ---`); const configuredProviders = Array.isArray(CONFIG.DEFAULT_MODEL_PROVIDERS) && CONFIG.DEFAULT_MODEL_PROVIDERS.length > 0 ? CONFIG.DEFAULT_MODEL_PROVIDERS : [CONFIG.MODEL_PROVIDER]; const uniqueProviders = [...new Set(configuredProviders)]; - console.log(` Primary Model Provider: ${CONFIG.MODEL_PROVIDER}`); + logger.info(` Primary Model Provider: ${CONFIG.MODEL_PROVIDER}`); if (uniqueProviders.length > 1) { - console.log(` Additional Model Providers: ${uniqueProviders.slice(1).join(', ')}`); + logger.info(` Additional Model Providers: ${uniqueProviders.slice(1).join(', ')}`); } - console.log(` System Prompt File: ${CONFIG.SYSTEM_PROMPT_FILE_PATH || 'Default'}`); - console.log(` System Prompt Mode: ${CONFIG.SYSTEM_PROMPT_MODE}`); - console.log(` Host: ${CONFIG.HOST}`); - console.log(` Port: ${CONFIG.SERVER_PORT}`); - console.log(` Required API Key: ${CONFIG.REQUIRED_API_KEY}`); - console.log(` Prompt Logging: ${CONFIG.PROMPT_LOG_MODE}${CONFIG.PROMPT_LOG_FILENAME ? ` (to ${CONFIG.PROMPT_LOG_FILENAME})` : ''}`); - console.log(`------------------------------------------`); - console.log(`\nUnified API Server running on http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}`); - console.log(`Supports multiple API formats:`); - console.log(` • OpenAI-compatible: /v1/chat/completions, /v1/responses, /v1/models`); - console.log(` • Gemini-compatible: /v1beta/models, /v1beta/models/{model}:generateContent`); - console.log(` • Claude-compatible: /v1/messages`); - console.log(` • Health check: /health`); - console.log(` • UI Management Console: http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}/`); + logger.info(` System Prompt File: ${CONFIG.SYSTEM_PROMPT_FILE_PATH || 'Default'}`); + logger.info(` System Prompt Mode: ${CONFIG.SYSTEM_PROMPT_MODE}`); + logger.info(` Host: ${CONFIG.HOST}`); + logger.info(` Port: ${CONFIG.SERVER_PORT}`); + logger.info(` Required API Key: ${CONFIG.REQUIRED_API_KEY}`); + logger.info(` Prompt Logging: ${CONFIG.PROMPT_LOG_MODE}${CONFIG.PROMPT_LOG_FILENAME ? ` (to ${CONFIG.PROMPT_LOG_FILENAME})` : ''}`); + logger.info(`------------------------------------------`); + logger.info(`\nUnified API Server running on http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}`); + logger.info(`Supports multiple API formats:`); + logger.info(` • OpenAI-compatible: /v1/chat/completions, /v1/responses, /v1/models`); + logger.info(` • Gemini-compatible: /v1beta/models, /v1beta/models/{model}:generateContent`); + logger.info(` • Claude-compatible: /v1/messages`); + logger.info(` • Health check: /health`); + logger.info(` • UI Management Console: http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}/`); // Auto-open browser to UI (only if host is 0.0.0.0 or 127.0.0.1) // if (CONFIG.HOST === '0.0.0.0' || CONFIG.HOST === '127.0.0.1') { @@ -300,27 +301,27 @@ async function startServer() { } open(openUrl) .then(() => { - console.log('[UI] Opened login page in default browser'); + logger.info('[UI] Opened login page in default browser'); }) .catch(err => { - console.log('[UI] Please open manually: http://' + CONFIG.HOST + ':' + CONFIG.SERVER_PORT + '/login.html'); + logger.info('[UI] Please open manually: http://' + CONFIG.HOST + ':' + CONFIG.SERVER_PORT + '/login.html'); }); }, openDelay); } catch (err) { - console.log(`[UI] Login page available at: http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}/login.html`); + logger.info(`[UI] Login page available at: http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}/login.html`); } // } if (CONFIG.CRON_REFRESH_TOKEN) { - console.log(` • Cron Near Minutes: ${CONFIG.CRON_NEAR_MINUTES}`); - console.log(` • Cron Refresh Token: ${CONFIG.CRON_REFRESH_TOKEN}`); + logger.info(` • Cron Near Minutes: ${CONFIG.CRON_NEAR_MINUTES}`); + logger.info(` • Cron Refresh Token: ${CONFIG.CRON_REFRESH_TOKEN}`); // 每 CRON_NEAR_MINUTES 分钟执行一次心跳日志和令牌刷新 setInterval(heartbeatAndRefreshToken, CONFIG.CRON_NEAR_MINUTES * 60 * 1000); } // 服务器完全启动后,执行初始健康检查 const poolManager = getProviderPoolManager(); if (poolManager) { - console.log('[Initialization] Performing initial health checks for provider pools...'); + logger.info('[Initialization] Performing initial health checks for provider pools...'); poolManager.performHealthChecks(true); } @@ -339,7 +340,7 @@ setupSignalHandlers(); setupWorkerCommunication(); startServer().catch(err => { - console.error("[Server] Failed to start server:", err.message); + logger.error("[Server] Failed to start server:", err.message); process.exit(1); }); diff --git a/src/services/service-manager.js b/src/services/service-manager.js index c47c4b6..6d1e8a1 100644 --- a/src/services/service-manager.js +++ b/src/services/service-manager.js @@ -1,4 +1,5 @@ import { getServiceAdapter, serviceInstances } from '../providers/adapter.js'; +import logger from '../utils/logger.js'; import { ProviderPoolManager } from '../providers/provider-pool-manager.js'; import deepmerge from 'deepmerge'; import * as fs from 'fs'; @@ -75,24 +76,24 @@ export async function autoLinkProviderConfigs(config) { const filePath = config.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json'; try { await pfs.writeFile(filePath, JSON.stringify(config.providerPools, null, 2), 'utf8'); - console.log(`[Auto-Link] Added ${totalNewProviders} new config(s) to provider pools:`); + logger.info(`[Auto-Link] Added ${totalNewProviders} new config(s) to provider pools:`); for (const [displayName, providers] of Object.entries(allNewProviders)) { - console.log(` ${displayName}: ${providers.length} config(s)`); + logger.info(` ${displayName}: ${providers.length} config(s)`); providers.forEach(p => { // 获取凭据路径键(支持 _CREDS_FILE_PATH 和 _TOKEN_FILE_PATH 两种格式) const credKey = Object.keys(p).find(k => k.endsWith('_CREDS_FILE_PATH') || k.endsWith('_TOKEN_FILE_PATH') ); if (credKey) { - console.log(` - ${p[credKey]}`); + logger.info(` - ${p[credKey]}`); } }); } } catch (error) { - console.error(`[Auto-Link] Failed to save provider_pools.json: ${error.message}`); + logger.error(`[Auto-Link] Failed to save provider_pools.json: ${error.message}`); } } else { - console.log('[Auto-Link] No new configs to link'); + logger.info('[Auto-Link] No new configs to link'); } // Update provider pool manager if available @@ -154,7 +155,7 @@ async function scanProviderDirectory(dirPath, linkedPaths, newProviders, options } } } catch (error) { - console.warn(`[Auto-Link] Failed to scan directory ${dirPath}: ${error.message}`); + logger.warn(`[Auto-Link] Failed to scan directory ${dirPath}: ${error.message}`); } } @@ -173,24 +174,24 @@ export async function initApiService(config, isReady = false) { maxErrorCount: config.MAX_ERROR_COUNT ?? 3, providerFallbackChain: config.providerFallbackChain || {}, }); - console.log('[Initialization] ProviderPoolManager initialized with configured pools.'); + logger.info('[Initialization] ProviderPoolManager initialized with configured pools.'); if(isReady){ // --- V2: 触发系统预热 --- // 预热逻辑是异步的,不会阻塞服务器启动 providerPoolManager.warmupNodes().catch(err => { - console.error(`[Initialization] Warmup failed: ${err.message}`); + logger.error(`[Initialization] Warmup failed: ${err.message}`); }); // 检查并刷新即将过期的节点(异步调用,不阻塞启动) providerPoolManager.checkAndRefreshExpiringNodes().catch(err => { - console.error(`[Initialization] Check and refresh expiring nodes failed: ${err.message}`); + logger.error(`[Initialization] Check and refresh expiring nodes failed: ${err.message}`); }); } // 健康检查将在服务器完全启动后执行 } else { - console.log('[Initialization] No provider pools configured. Using single provider mode.'); + logger.info('[Initialization] No provider pools configured. Using single provider mode.'); } // Initialize all provider pool nodes at startup @@ -203,7 +204,7 @@ export async function initApiService(config, isReady = false) { // 验证提供商类型是否在 DEFAULT_MODEL_PROVIDERS 中 if (config.DEFAULT_MODEL_PROVIDERS && Array.isArray(config.DEFAULT_MODEL_PROVIDERS)) { if (!config.DEFAULT_MODEL_PROVIDERS.includes(providerType)) { - console.log(`[Initialization] Skipping provider type '${providerType}' (not in DEFAULT_MODEL_PROVIDERS).`); + logger.info(`[Initialization] Skipping provider type '${providerType}' (not in DEFAULT_MODEL_PROVIDERS).`); continue; } } @@ -212,7 +213,7 @@ export async function initApiService(config, isReady = false) { continue; } - console.log(`[Initialization] Initializing ${providerConfigs.length} node(s) for provider '${providerType}'...`); + logger.info(`[Initialization] Initializing ${providerConfigs.length} node(s) for provider '${providerType}'...`); // 初始化该提供商类型的所有节点 for (const providerConfig of providerConfigs) { @@ -234,18 +235,18 @@ export async function initApiService(config, isReady = false) { totalInitialized++; const identifier = providerConfig.customName || providerConfig.uuid || 'unknown'; - console.log(` ✓ Initialized node: ${identifier}`); + logger.info(` ✓ Initialized node: ${identifier}`); } catch (error) { totalFailed++; const identifier = providerConfig.customName || providerConfig.uuid || 'unknown'; - console.warn(` ✗ Failed to initialize node ${identifier}: ${error.message}`); + logger.warn(` ✗ Failed to initialize node ${identifier}: ${error.message}`); } } } - console.log(`[Initialization] Provider pool initialization complete: ${totalInitialized} succeeded, ${totalFailed} failed.`); + logger.info(`[Initialization] Provider pool initialization complete: ${totalInitialized} succeeded, ${totalFailed} failed.`); } else { - console.log('[Initialization] No provider pools configured. Skipping node initialization.'); + logger.info('[Initialization] No provider pools configured. Skipping node initialization.'); } return serviceInstances; // Return the collection of initialized service instances } @@ -271,10 +272,10 @@ export async function getApiService(config, requestedModel = null, options = {}) config.uuid = serviceConfig.uuid; config.customName = serviceConfig.customName; const customNameDisplay = serviceConfig.customName ? ` (${serviceConfig.customName})` : ''; - console.log(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}${customNameDisplay}${requestedModel ? ` (model: ${requestedModel})` : ''}`); + logger.info(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}${customNameDisplay}${requestedModel ? ` (model: ${requestedModel})` : ''}`); } else { const errorMsg = `[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}`; - console.error(errorMsg); + logger.error(errorMsg); throw new Error(errorMsg); } } @@ -321,7 +322,7 @@ export async function getApiServiceWithFallback(config, requestedModel = null, o } } else { const errorMsg = `[API Service] No healthy provider found in pool (including fallback) for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}`; - console.error(errorMsg); + logger.error(errorMsg); throw new Error(errorMsg); } } @@ -376,7 +377,7 @@ export async function getProviderStatus(config, options = {}) { providerPools = poolsData; } } catch (error) { - console.warn('[API Service] Failed to load provider pools:', error.message); + logger.warn('[API Service] Failed to load provider pools:', error.message); } // providerPoolsSlim 只保留顶级 key 及部分字段,过滤 isDisabled 为 true 的元素 diff --git a/src/services/ui-manager.js b/src/services/ui-manager.js index 6a5368a..d657d1e 100644 --- a/src/services/ui-manager.js +++ b/src/services/ui-manager.js @@ -108,6 +108,11 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo return await systemApi.handleGetSystem(req, res); } + // Download today's log file + if (method === 'GET' && pathParam === '/api/system/download-log') { + return await systemApi.handleDownloadTodayLog(req, res); + } + // Get provider pools summary if (method === 'GET' && pathParam === '/api/providers') { return await providerApi.handleGetProviders(req, res, currentConfig, providerPoolManager); diff --git a/src/ui-modules/auth.js b/src/ui-modules/auth.js index 9c031dc..f3cf9d0 100644 --- a/src/ui-modules/auth.js +++ b/src/ui-modules/auth.js @@ -1,4 +1,5 @@ import { existsSync } from 'fs'; +import logger from '../utils/logger.js'; import { promises as fs } from 'fs'; import path from 'path'; import crypto from 'crypto'; @@ -23,18 +24,18 @@ export async function readPasswordFile() { const trimmedPassword = password.trim(); // 如果密码文件为空,使用默认密码 if (!trimmedPassword) { - console.log('[Auth] Password file is empty, using default password: ' + DEFAULT_PASSWORD); + logger.info('[Auth] Password file is empty, using default password: ' + DEFAULT_PASSWORD); return DEFAULT_PASSWORD; } - console.log('[Auth] Successfully read password file'); + logger.info('[Auth] Successfully read password file'); return trimmedPassword; } catch (error) { // ENOENT means file does not exist, which is normal if (error.code === 'ENOENT') { - console.log('[Auth] Password file does not exist, using default password: ' + DEFAULT_PASSWORD); + logger.info('[Auth] Password file does not exist, using default password: ' + DEFAULT_PASSWORD); } else { - console.error('[Auth] Failed to read password file:', error.code || error.message); - console.log('[Auth] Using default password: ' + DEFAULT_PASSWORD); + logger.error('[Auth] Failed to read password file:', error.code || error.message); + logger.info('[Auth] Using default password: ' + DEFAULT_PASSWORD); } return DEFAULT_PASSWORD; } @@ -45,9 +46,9 @@ export async function readPasswordFile() { */ export async function validateCredentials(password) { const storedPassword = await readPasswordFile(); - console.log('[Auth] Validating password, stored password length:', storedPassword ? storedPassword.length : 0, ', input password length:', password ? password.length : 0); + logger.info('[Auth] Validating password, stored password length:', storedPassword ? storedPassword.length : 0, ', input password length:', password ? password.length : 0); const isValid = storedPassword && password === storedPassword; - console.log('[Auth] Password validation result:', isValid); + logger.info('[Auth] Password validation result:', isValid); return isValid; } @@ -105,7 +106,7 @@ async function readTokenStore() { return { tokens: {} }; } } catch (error) { - console.error('[Token Store] Failed to read token store file:', error); + logger.error('[Token Store] Failed to read token store file:', error); return { tokens: {} }; } } @@ -117,7 +118,7 @@ async function writeTokenStore(tokenStore) { try { await fs.writeFile(TOKEN_STORE_FILE, JSON.stringify(tokenStore, null, 2), 'utf8'); } catch (error) { - console.error('[Token Store] Failed to write token store file:', error); + logger.error('[Token Store] Failed to write token store file:', error); } } @@ -245,7 +246,7 @@ export async function handleLoginRequest(req, res) { })); } } catch (error) { - console.error('[Auth] Login processing error:', error); + logger.error('[Auth] Login processing error:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, @@ -256,4 +257,6 @@ export async function handleLoginRequest(req, res) { } // 定时清理过期token -setInterval(cleanupExpiredTokens, 5 * 60 * 1000); // 每5分钟清理一次 \ No newline at end of file +setInterval(cleanupExpiredTokens, 5 * 60 * 1000); // 每5分钟清理一次 + + diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js index eb14619..b0e2b37 100644 --- a/src/ui-modules/config-api.js +++ b/src/ui-modules/config-api.js @@ -1,4 +1,5 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; +import logger from '../utils/logger.js'; import { promises as fs } from 'fs'; import path from 'path'; import { CONFIG } from '../core/config-manager.js'; @@ -27,17 +28,17 @@ export async function reloadConfig(providerPoolManager) { // Update global CONFIG Object.assign(CONFIG, newConfig); - console.log('[UI API] Configuration reloaded:'); + logger.info('[UI API] Configuration reloaded:'); // Update initApiService - 清空并重新初始化服务实例 Object.keys(serviceInstances).forEach(key => delete serviceInstances[key]); initApiService(CONFIG); - console.log('[UI API] Configuration reloaded successfully'); + logger.info('[UI API] Configuration reloaded successfully'); return newConfig; } catch (error) { - console.error('[UI API] Failed to reload configuration:', error); + logger.error('[UI API] Failed to reload configuration:', error); throw error; } } @@ -52,7 +53,7 @@ export async function handleGetConfig(req, res, currentConfig) { try { systemPrompt = readFileSync(currentConfig.SYSTEM_PROMPT_FILE_PATH, 'utf-8'); } catch (e) { - console.warn('[UI API] Failed to read system prompt file:', e.message); + logger.warn('[UI API] Failed to read system prompt file:', e.message); } } @@ -97,6 +98,16 @@ export async function handleUpdateConfig(req, res, currentConfig) { if (newConfig.PROXY_URL !== undefined) currentConfig.PROXY_URL = newConfig.PROXY_URL; if (newConfig.PROXY_ENABLED_PROVIDERS !== undefined) currentConfig.PROXY_ENABLED_PROVIDERS = newConfig.PROXY_ENABLED_PROVIDERS; + // Log settings + if (newConfig.LOG_ENABLED !== undefined) currentConfig.LOG_ENABLED = newConfig.LOG_ENABLED; + if (newConfig.LOG_OUTPUT_MODE !== undefined) currentConfig.LOG_OUTPUT_MODE = newConfig.LOG_OUTPUT_MODE; + if (newConfig.LOG_LEVEL !== undefined) currentConfig.LOG_LEVEL = newConfig.LOG_LEVEL; + if (newConfig.LOG_DIR !== undefined) currentConfig.LOG_DIR = newConfig.LOG_DIR; + if (newConfig.LOG_INCLUDE_REQUEST_ID !== undefined) currentConfig.LOG_INCLUDE_REQUEST_ID = newConfig.LOG_INCLUDE_REQUEST_ID; + if (newConfig.LOG_INCLUDE_TIMESTAMP !== undefined) currentConfig.LOG_INCLUDE_TIMESTAMP = newConfig.LOG_INCLUDE_TIMESTAMP; + if (newConfig.LOG_MAX_FILE_SIZE !== undefined) currentConfig.LOG_MAX_FILE_SIZE = newConfig.LOG_MAX_FILE_SIZE; + if (newConfig.LOG_MAX_FILES !== undefined) currentConfig.LOG_MAX_FILES = newConfig.LOG_MAX_FILES; + // Handle system prompt update if (newConfig.systemPrompt !== undefined) { const promptPath = currentConfig.SYSTEM_PROMPT_FILE_PATH || 'configs/input_system_prompt.txt'; @@ -112,9 +123,9 @@ export async function handleUpdateConfig(req, res, currentConfig) { timestamp: new Date().toISOString() }); - console.log('[UI API] System prompt updated'); + logger.info('[UI API] System prompt updated'); } catch (e) { - console.warn('[UI API] Failed to write system prompt:', e.message); + logger.warn('[UI API] Failed to write system prompt:', e.message); } } @@ -139,17 +150,24 @@ export async function handleUpdateConfig(req, res, currentConfig) { CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN, PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH, MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT, - POOL_SIZE_LIMIT: currentConfig.POOL_SIZE_LIMIT, WARMUP_TARGET: currentConfig.WARMUP_TARGET, REFRESH_CONCURRENCY_PER_PROVIDER: currentConfig.REFRESH_CONCURRENCY_PER_PROVIDER, providerFallbackChain: currentConfig.providerFallbackChain, modelFallbackMapping: currentConfig.modelFallbackMapping, PROXY_URL: currentConfig.PROXY_URL, - PROXY_ENABLED_PROVIDERS: currentConfig.PROXY_ENABLED_PROVIDERS + PROXY_ENABLED_PROVIDERS: currentConfig.PROXY_ENABLED_PROVIDERS, + LOG_ENABLED: currentConfig.LOG_ENABLED, + LOG_OUTPUT_MODE: currentConfig.LOG_OUTPUT_MODE, + LOG_LEVEL: currentConfig.LOG_LEVEL, + LOG_DIR: currentConfig.LOG_DIR, + LOG_INCLUDE_REQUEST_ID: currentConfig.LOG_INCLUDE_REQUEST_ID, + LOG_INCLUDE_TIMESTAMP: currentConfig.LOG_INCLUDE_TIMESTAMP, + LOG_MAX_FILE_SIZE: currentConfig.LOG_MAX_FILE_SIZE, + LOG_MAX_FILES: currentConfig.LOG_MAX_FILES }; writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8'); - console.log('[UI API] Configuration saved to configs/config.json'); + logger.info('[UI API] Configuration saved to configs/config.json'); // 广播更新事件 broadcastEvent('config_update', { @@ -159,7 +177,7 @@ export async function handleUpdateConfig(req, res, currentConfig) { timestamp: new Date().toISOString() }); } catch (error) { - console.error('[UI API] Failed to save configuration to file:', error.message); + logger.error('[UI API] Failed to save configuration to file:', error.message); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -215,7 +233,7 @@ export async function handleReloadConfig(req, res, providerPoolManager) { })); return true; } catch (error) { - console.error('[UI API] Failed to reload config files:', error); + logger.error('[UI API] Failed to reload config files:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -248,7 +266,7 @@ export async function handleUpdateAdminPassword(req, res) { const pwdFilePath = path.join(process.cwd(), 'configs', 'pwd'); await fs.writeFile(pwdFilePath, password.trim(), 'utf-8'); - console.log('[UI API] Admin password updated successfully'); + logger.info('[UI API] Admin password updated successfully'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ @@ -257,7 +275,7 @@ export async function handleUpdateAdminPassword(req, res) { })); return true; } catch (error) { - console.error('[UI API] Failed to update admin password:', error); + logger.error('[UI API] Failed to update admin password:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { diff --git a/src/ui-modules/config-scanner.js b/src/ui-modules/config-scanner.js index 984b112..46f1734 100644 --- a/src/ui-modules/config-scanner.js +++ b/src/ui-modules/config-scanner.js @@ -1,4 +1,5 @@ import { existsSync } from 'fs'; +import logger from '../utils/logger.js'; import { promises as fs } from 'fs'; import path from 'path'; import { addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js'; @@ -16,7 +17,7 @@ export async function scanConfigFiles(currentConfig, providerPoolManager) { const configsPath = path.join(process.cwd(), 'configs'); if (!existsSync(configsPath)) { - // console.log('[Config Scanner] configs directory not found, creating empty result'); + // logger.info('[Config Scanner] configs directory not found, creating empty result'); return configFiles; } @@ -55,7 +56,7 @@ export async function scanConfigFiles(currentConfig, providerPoolManager) { const configsFiles = await scanOAuthDirectory(configsPath, usedPaths, currentConfig); configFiles.push(...configsFiles); } catch (error) { - console.warn(`[Config Scanner] Failed to scan configs directory:`, error.message); + logger.warn(`[Config Scanner] Failed to scan configs directory:`, error.message); } return configFiles; @@ -143,7 +144,7 @@ async function analyzeOAuthFile(filePath, usedPaths, currentConfig) { preview: content.substring(0, 100) + (content.length > 100 ? '...' : '') }; } catch (error) { - console.warn(`[OAuth Analyzer] Failed to analyze file ${filePath}:`, error.message); + logger.warn(`[OAuth Analyzer] Failed to analyze file ${filePath}:`, error.message); return null; } } @@ -361,7 +362,7 @@ async function scanOAuthDirectory(dirPath, usedPaths, currentConfig) { } } } catch (error) { - console.warn(`[OAuth Scanner] Failed to scan directory ${dirPath}:`, error.message); + logger.warn(`[OAuth Scanner] Failed to scan directory ${dirPath}:`, error.message); } return oauthFiles; diff --git a/src/ui-modules/event-broadcast.js b/src/ui-modules/event-broadcast.js index 1a1c91a..dd1420f 100644 --- a/src/ui-modules/event-broadcast.js +++ b/src/ui-modules/event-broadcast.js @@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'fs'; import { promises as fs } from 'fs'; import path from 'path'; import multer from 'multer'; +import logger from '../utils/logger.js'; // Token存储到本地文件中 const TOKEN_STORE_FILE = path.join(process.cwd(), 'configs', 'token-store.json'); @@ -72,21 +73,17 @@ export function initializeUIManagement() { const originalLog = console.log; console.log = function(...args) { originalLog.apply(console, args); - const message = args.map(arg => { - if (typeof arg === 'string') return arg; - try { - return JSON.stringify(arg); - } catch (e) { - if (arg instanceof Error) { - return `[Error: ${arg.message}] ${arg.stack || ''}`; - } - return `[Object: ${Object.prototype.toString.call(arg)}] (Circular or too complex to stringify)`; - } - }).join(' '); const logEntry = { timestamp: new Date().toISOString(), level: 'info', - message: message + message: args.map(arg => { + if (typeof arg === 'string') return arg; + try { + return JSON.stringify(arg); + } catch (e) { + return String(arg); + } + }).join(' ') }; global.logBuffer.push(logEntry); if (global.logBuffer.length > 100) { @@ -99,21 +96,17 @@ export function initializeUIManagement() { const originalError = console.error; console.error = function(...args) { originalError.apply(console, args); - const message = args.map(arg => { - if (typeof arg === 'string') return arg; - try { - return JSON.stringify(arg); - } catch (e) { - if (arg instanceof Error) { - return `[Error: ${arg.message}] ${arg.stack || ''}`; - } - return `[Object: ${Object.prototype.toString.call(arg)}] (Circular or too complex to stringify)`; - } - }).join(' '); const logEntry = { timestamp: new Date().toISOString(), level: 'error', - message: message + message: args.map(arg => { + if (typeof arg === 'string') return arg; + try { + return JSON.stringify(arg); + } catch (e) { + return String(arg); + } + }).join(' ') }; global.logBuffer.push(logEntry); if (global.logBuffer.length > 100) { @@ -185,7 +178,7 @@ export function handleUploadOAuthCredentials(req, res, options = {}) { return new Promise((resolve) => { uploadMiddleware(req, res, async (err) => { if (err) { - console.error(`${logPrefix} File upload error:`, err.message); + logger.error(`${logPrefix} File upload error:`, err.message); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -242,7 +235,7 @@ export function handleUploadOAuthCredentials(req, res, options = {}) { }); const userInfoStr = userInfo ? `, ${userInfo}` : ''; - console.log(`${logPrefix} OAuth credentials file uploaded: ${targetFilePath} (provider: ${provider}${userInfoStr})`); + logger.info(`${logPrefix} OAuth credentials file uploaded: ${targetFilePath} (provider: ${provider}${userInfoStr})`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ @@ -255,7 +248,7 @@ export function handleUploadOAuthCredentials(req, res, options = {}) { resolve(true); } catch (error) { - console.error(`${logPrefix} File upload processing error:`, error); + logger.error(`${logPrefix} File upload processing error:`, error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { diff --git a/src/ui-modules/oauth-api.js b/src/ui-modules/oauth-api.js index 1dd2c1a..a0c6a07 100644 --- a/src/ui-modules/oauth-api.js +++ b/src/ui-modules/oauth-api.js @@ -1,4 +1,5 @@ import { getRequestBody } from '../utils/common.js'; +import logger from '../utils/logger.js'; import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, @@ -74,7 +75,7 @@ export async function handleGenerateAuthUrl(req, res, currentConfig, providerTyp return true; } catch (error) { - console.error(`[UI API] Failed to generate auth URL for ${providerType}:`, error); + logger.error(`[UI API] Failed to generate auth URL for ${providerType}:`, error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -102,8 +103,8 @@ export async function handleManualOAuthCallback(req, res) { return true; } - console.log(`[OAuth Manual Callback] Processing manual callback for ${provider}`); - console.log(`[OAuth Manual Callback] Callback URL: ${callbackUrl}`); + logger.info(`[OAuth Manual Callback] Processing manual callback for ${provider}`); + logger.info(`[OAuth Manual Callback] Callback URL: ${callbackUrl}`); // 解析回调URL const url = new URL(callbackUrl); @@ -140,7 +141,7 @@ export async function handleManualOAuthCallback(req, res) { const response = await fetch(localUrl.href); if (response.ok) { - console.log(`[OAuth Manual Callback] Successfully processed callback for ${provider}`); + logger.info(`[OAuth Manual Callback] Successfully processed callback for ${provider}`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, @@ -148,7 +149,7 @@ export async function handleManualOAuthCallback(req, res) { })); } else { const errorText = await response.text(); - console.error(`[OAuth Manual Callback] Callback processing failed:`, errorText); + logger.error(`[OAuth Manual Callback] Callback processing failed:`, errorText); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, @@ -156,7 +157,7 @@ export async function handleManualOAuthCallback(req, res) { })); } } catch (fetchError) { - console.error(`[OAuth Manual Callback] Failed to process callback:`, fetchError); + logger.error(`[OAuth Manual Callback] Failed to process callback:`, fetchError); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, @@ -166,7 +167,7 @@ export async function handleManualOAuthCallback(req, res) { return true; } catch (error) { - console.error('[OAuth Manual Callback] Error:', error); + logger.error('[OAuth Manual Callback] Error:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, @@ -193,7 +194,7 @@ export async function handleBatchImportKiroTokens(req, res) { return true; } - console.log(`[Kiro Batch Import] Starting batch import of ${refreshTokens.length} tokens with SSE...`); + logger.info(`[Kiro Batch Import] Starting batch import of ${refreshTokens.length} tokens with SSE...`); // 设置 SSE 响应头 res.writeHead(200, { @@ -222,7 +223,7 @@ export async function handleBatchImportKiroTokens(req, res) { } ); - console.log(`[Kiro Batch Import] Completed: ${result.success} success, ${result.failed} failed`); + logger.info(`[Kiro Batch Import] Completed: ${result.success} success, ${result.failed} failed`); // 发送完成事件 sendSSE('complete', { @@ -237,7 +238,7 @@ export async function handleBatchImportKiroTokens(req, res) { return true; } catch (error) { - console.error('[Kiro Batch Import] Error:', error); + logger.error('[Kiro Batch Import] Error:', error); // 如果已经开始发送 SSE,则发送错误事件 if (res.headersSent) { res.write(`event: error\n`); @@ -287,12 +288,12 @@ export async function handleImportAwsCredentials(req, res) { return true; } - console.log('[Kiro AWS Import] Starting AWS credentials import...'); + logger.info('[Kiro AWS Import] Starting AWS credentials import...'); const result = await importAwsCredentials(credentials); if (result.success) { - console.log(`[Kiro AWS Import] Successfully imported credentials to: ${result.path}`); + logger.info(`[Kiro AWS Import] Successfully imported credentials to: ${result.path}`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, @@ -312,7 +313,7 @@ export async function handleImportAwsCredentials(req, res) { return true; } catch (error) { - console.error('[Kiro AWS Import] Error:', error); + logger.error('[Kiro AWS Import] Error:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, diff --git a/src/ui-modules/plugin-api.js b/src/ui-modules/plugin-api.js index 2d30b79..4ed0d1d 100644 --- a/src/ui-modules/plugin-api.js +++ b/src/ui-modules/plugin-api.js @@ -1,4 +1,5 @@ import { getPluginManager } from '../core/plugin-manager.js'; +import logger from '../utils/logger.js'; import { getRequestBody } from '../utils/common.js'; import { broadcastEvent } from './event-broadcast.js'; @@ -13,7 +14,7 @@ export async function handleGetPlugins(req, res) { res.end(JSON.stringify({ plugins })); return true; } catch (error) { - console.error('[UI API] Failed to get plugins:', error); + logger.error('[UI API] Failed to get plugins:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -64,7 +65,7 @@ export async function handleTogglePlugin(req, res, pluginName) { })); return true; } catch (error) { - console.error('[UI API] Failed to toggle plugin:', error); + logger.error('[UI API] Failed to toggle plugin:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js index 96ee31d..aa3952d 100644 --- a/src/ui-modules/provider-api.js +++ b/src/ui-modules/provider-api.js @@ -1,4 +1,5 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; +import logger from '../utils/logger.js'; import { getRequestBody } from '../utils/common.js'; import { getAllProviderModels, getProviderModels } from '../providers/provider-models.js'; import { generateUUID, createProviderConfig, formatSystemPath, detectProviderFromPath, addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js'; @@ -18,7 +19,7 @@ export async function handleGetProviders(req, res, currentConfig, providerPoolMa providerPools = poolsData; } } catch (error) { - console.warn('[UI API] Failed to load provider pools:', error.message); + logger.warn('[UI API] Failed to load provider pools:', error.message); } res.writeHead(200, { 'Content-Type': 'application/json' }); @@ -40,7 +41,7 @@ export async function handleGetProviderType(req, res, currentConfig, providerPoo providerPools = poolsData; } } catch (error) { - console.warn('[UI API] Failed to load provider pools:', error.message); + logger.warn('[UI API] Failed to load provider pools:', error.message); } const providers = providerPools[providerType] || []; @@ -112,7 +113,7 @@ export async function handleAddProvider(req, res, currentConfig, providerPoolMan const fileContent = readFileSync(filePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { - console.warn('[UI API] Failed to read existing provider pools:', readError.message); + logger.warn('[UI API] Failed to read existing provider pools:', readError.message); } } @@ -124,7 +125,7 @@ export async function handleAddProvider(req, res, currentConfig, providerPoolMan // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); - console.log(`[UI API] Added new provider to ${providerType}: ${providerConfig.uuid}`); + logger.info(`[UI API] Added new provider to ${providerType}: ${providerConfig.uuid}`); // Update provider pool manager if available if (providerPoolManager) { @@ -219,7 +220,7 @@ export async function handleUpdateProvider(req, res, currentConfig, providerPool // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); - console.log(`[UI API] Updated provider ${providerUuid} in ${providerType}`); + logger.info(`[UI API] Updated provider ${providerUuid} in ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { @@ -290,7 +291,7 @@ export async function handleDeleteProvider(req, res, currentConfig, providerPool // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); - console.log(`[UI API] Deleted provider ${providerUuid} from ${providerType}`); + logger.info(`[UI API] Deleted provider ${providerUuid} from ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { @@ -357,7 +358,7 @@ export async function handleDisableEnableProvider(req, res, currentConfig, provi // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); - console.log(`[UI API] ${action === 'disable' ? 'Disabled' : 'Enabled'} provider ${providerUuid} in ${providerType}`); + logger.info(`[UI API] ${action === 'disable' ? 'Disabled' : 'Enabled'} provider ${providerUuid} in ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { @@ -439,7 +440,7 @@ export async function handleResetProviderHealth(req, res, currentConfig, provide // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); - console.log(`[UI API] Reset health status for ${resetCount} providers in ${providerType}`); + logger.info(`[UI API] Reset health status for ${resetCount} providers in ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { @@ -524,7 +525,7 @@ export async function handleDeleteUnhealthyProviders(req, res, currentConfig, pr // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); - console.log(`[UI API] Deleted ${unhealthyProviders.length} unhealthy providers from ${providerType}`); + logger.info(`[UI API] Deleted ${unhealthyProviders.length} unhealthy providers from ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { @@ -615,7 +616,7 @@ export async function handleRefreshUnhealthyUuids(req, res, currentConfig, provi // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); - console.log(`[UI API] Refreshed UUIDs for ${refreshedProviders.length} unhealthy providers in ${providerType}`); + logger.info(`[UI API] Refreshed UUIDs for ${refreshedProviders.length} unhealthy providers in ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { @@ -684,7 +685,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan return true; } - console.log(`[UI API] Starting health check for ${unhealthyProviders.length} unhealthy providers in ${providerType} (total: ${providers.length})`); + logger.info(`[UI API] Starting health check for ${unhealthyProviders.length} unhealthy providers in ${providerType} (total: ${providers.length})`); // 执行健康检测(强制检查,忽略 checkHealth 配置) const results = []; @@ -693,7 +694,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan // 跳过已禁用的节点 if (providerConfig.isDisabled) { - console.log(`[UI API] Skipping health check for disabled provider: ${providerConfig.uuid}`); + logger.info(`[UI API] Skipping health check for disabled provider: ${providerConfig.uuid}`); continue; } @@ -726,7 +727,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan if (isAuthError) { providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage); - console.log(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`); + logger.info(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`); } else { providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage); } @@ -751,7 +752,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan if (isAuthError) { providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage); - console.log(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`); + logger.info(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`); } else { providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage); } @@ -778,7 +779,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan const successCount = results.filter(r => r.success === true).length; const failCount = results.filter(r => r.success === false).length; - console.log(`[UI API] Health check completed for ${providerType}: ${successCount} recovered, ${failCount} still unhealthy (checked ${unhealthyProviders.length} unhealthy nodes)`); + logger.info(`[UI API] Health check completed for ${providerType}: ${successCount} recovered, ${failCount} still unhealthy (checked ${unhealthyProviders.length} unhealthy nodes)`); // 广播更新事件 broadcastEvent('config_update', { @@ -800,7 +801,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan })); return true; } catch (error) { - console.error('[UI API] Health check error:', error); + logger.error('[UI API] Health check error:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); return true; @@ -846,7 +847,7 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP const fileContent = readFileSync(poolsFilePath, 'utf-8'); providerPools = JSON.parse(fileContent); } catch (readError) { - console.warn('[UI API] Failed to read existing provider pools:', readError.message); + logger.warn('[UI API] Failed to read existing provider pools:', readError.message); } } @@ -884,7 +885,7 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP // Save to file writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf-8'); - console.log(`[UI API] Quick linked config: ${filePath} -> ${providerType}`); + logger.info(`[UI API] Quick linked config: ${filePath} -> ${providerType}`); // Update provider pool manager if available if (providerPoolManager) { @@ -917,7 +918,7 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP })); return true; } catch (error) { - console.error('[UI API] Quick link failed:', error); + logger.error('[UI API] Quick link failed:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -967,7 +968,7 @@ export async function handleRefreshProviderUuid(req, res, currentConfig, provide // Save to file writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); - console.log(`[UI API] Refreshed UUID for provider in ${providerType}: ${oldUuid} -> ${newUuid}`); + logger.info(`[UI API] Refreshed UUID for provider in ${providerType}: ${oldUuid} -> ${newUuid}`); // Update provider pool manager if available if (providerPoolManager) { diff --git a/src/ui-modules/system-api.js b/src/ui-modules/system-api.js index 370d5dc..84a95bb 100644 --- a/src/ui-modules/system-api.js +++ b/src/ui-modules/system-api.js @@ -1,4 +1,5 @@ -import { existsSync, readFileSync } from 'fs'; +import { existsSync, readFileSync, createReadStream } from 'fs'; +import logger from '../utils/logger.js'; import path from 'path'; import { getCpuUsagePercent } from './system-monitor.js'; @@ -16,7 +17,7 @@ export async function handleGetSystem(req, res) { appVersion = readFileSync(versionFilePath, 'utf8').trim(); } } catch (error) { - console.warn('[UI API] Failed to read VERSION file:', error.message); + logger.warn('[UI API] Failed to read VERSION file:', error.message); } // 计算 CPU 使用率 @@ -45,6 +46,34 @@ export async function handleGetSystem(req, res) { return true; } +/** + * 下载当日日志 + */ +export async function handleDownloadTodayLog(req, res) { + try { + if (!logger.currentLogFile || !existsSync(logger.currentLogFile)) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: 'Today\'s log file not found' } })); + return true; + } + + const fileName = path.basename(logger.currentLogFile); + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Disposition': `attachment; filename="${fileName}"` + }); + + const readStream = createReadStream(logger.currentLogFile); + readStream.pipe(res); + return true; + } catch (error) { + logger.error('[UI API] Failed to download log:', error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: { message: 'Failed to download log: ' + error.message } })); + return true; + } +} + /** * 健康检查接口(用于前端token验证) */ @@ -84,7 +113,7 @@ export async function handleRestartService(req, res) { if (IS_WORKER_PROCESS && process.send) { // 作为子进程运行,通知主进程重启 - console.log('[UI API] Requesting restart from master process...'); + logger.info('[UI API] Requesting restart from master process...'); process.send({ type: 'restart_request' }); // 广播重启事件 @@ -107,7 +136,7 @@ export async function handleRestartService(req, res) { })); } else { // 独立运行模式,无法自动重启 - console.log('[UI API] Service is running in standalone mode, cannot auto-restart'); + logger.info('[UI API] Service is running in standalone mode, cannot auto-restart'); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ @@ -119,7 +148,7 @@ export async function handleRestartService(req, res) { } return true; } catch (error) { - console.error('[UI API] Failed to restart service:', error); + logger.error('[UI API] Failed to restart service:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { diff --git a/src/ui-modules/update-api.js b/src/ui-modules/update-api.js index f4753ae..b0c890f 100644 --- a/src/ui-modules/update-api.js +++ b/src/ui-modules/update-api.js @@ -1,4 +1,5 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; +import logger from '../utils/logger.js'; import { promises as fs } from 'fs'; import path from 'path'; import { exec } from 'child_process'; @@ -19,7 +20,7 @@ function getUpdateProxyConfig() { const proxyConfig = parseProxyUrl(CONFIG.PROXY_URL); if (proxyConfig) { - console.log(`[Update] Using ${proxyConfig.proxyType} proxy for update check: ${CONFIG.PROXY_URL}`); + logger.info(`[Update] Using ${proxyConfig.proxyType} proxy for update check: ${CONFIG.PROXY_URL}`); } return proxyConfig; } @@ -59,7 +60,7 @@ async function fetchWithProxy(url, options = {}) { }); } catch (importError) { // 如果 undici 不可用,回退到原生 fetch(不使用代理) - console.warn('[Update] undici not available, falling back to native fetch without proxy'); + logger.warn('[Update] undici not available, falling back to native fetch without proxy'); return await fetch(url, options); } } @@ -103,7 +104,7 @@ async function getLatestVersionFromGitHub() { const apiUrl = `https://gh-proxy.org/https://api.github.com/repos/${GITHUB_REPO}/tags`; try { - console.log('[Update] Fetching latest version from GitHub API...'); + logger.info('[Update] Fetching latest version from GitHub API...'); const response = await fetchWithProxy(apiUrl, { headers: { 'Accept': 'application/vnd.github.v3+json', @@ -136,7 +137,7 @@ async function getLatestVersionFromGitHub() { return versions[0]; } catch (error) { - console.warn('[Update] Failed to fetch from GitHub API:', error.message); + logger.warn('[Update] Failed to fetch from GitHub API:', error.message); return null; } } @@ -158,7 +159,7 @@ export async function checkForUpdates() { localVersion = readFileSync(versionFilePath, 'utf-8').trim(); } } catch (error) { - console.warn('[Update] Failed to read local VERSION file:', error.message); + logger.warn('[Update] Failed to read local VERSION file:', error.message); } // 检查是否在 git 仓库中 @@ -168,7 +169,7 @@ export async function checkForUpdates() { isGitRepo = true; } catch (error) { isGitRepo = false; - console.log('[Update] Not in a Git repository, will use GitHub API to check for updates'); + logger.info('[Update] Not in a Git repository, will use GitHub API to check for updates'); } let latestTag = null; @@ -180,10 +181,10 @@ export async function checkForUpdates() { // 获取远程 tags try { - console.log('[Update] Fetching remote tags...'); + logger.info('[Update] Fetching remote tags...'); await execAsync('git fetch --tags'); } catch (error) { - console.warn('[Update] Failed to fetch tags via git, falling back to GitHub API:', error.message); + logger.warn('[Update] Failed to fetch tags via git, falling back to GitHub API:', error.message); // 如果 git fetch 失败,回退到 GitHub API latestTag = await getLatestVersionFromGitHub(); updateMethod = 'github_api'; @@ -214,7 +215,7 @@ export async function checkForUpdates() { latestTag = tags[0]; } } catch (e) { - console.warn('[Update] Failed to get latest tag via git, falling back to GitHub API:', e.message); + logger.warn('[Update] Failed to get latest tag via git, falling back to GitHub API:', e.message); latestTag = await getLatestVersionFromGitHub(); updateMethod = 'github_api'; } @@ -240,7 +241,7 @@ export async function checkForUpdates() { const comparison = compareVersions(latestTag, localVersion); const hasUpdate = comparison > 0; - console.log(`[Update] Local version: ${localVersion}, Latest version: ${latestTag}, Has update: ${hasUpdate}, Method: ${updateMethod}`); + logger.info(`[Update] Local version: ${localVersion}, Latest version: ${latestTag}, Has update: ${hasUpdate}, Method: ${updateMethod}`); return { hasUpdate, @@ -278,30 +279,30 @@ export async function performUpdate() { // 检查更新方式 - 如果是通过 GitHub API 获取的版本信息,说明不在 Git 仓库中 if (updateInfo.updateMethod === 'github_api') { // Docker/非 Git 环境,通过下载 tarball 更新 - console.log('[Update] Running in Docker/non-Git environment, will download and extract tarball'); + logger.info('[Update] Running in Docker/non-Git environment, will download and extract tarball'); return await performTarballUpdate(updateInfo.localVersion, latestTag); } - console.log(`[Update] Starting update to ${latestTag}...`); + logger.info(`[Update] Starting update to ${latestTag}...`); // 检查是否有未提交的更改 try { const { stdout: statusOutput } = await execAsync('git status --porcelain'); if (statusOutput.trim()) { // 有未提交的更改,先 stash - console.log('[Update] Stashing local changes...'); + logger.info('[Update] Stashing local changes...'); await execAsync('git stash'); } } catch (error) { - console.warn('[Update] Failed to check git status:', error.message); + logger.warn('[Update] Failed to check git status:', error.message); } // 执行 checkout 到最新 tag try { - console.log(`[Update] Checking out to ${latestTag}...`); + logger.info(`[Update] Checking out to ${latestTag}...`); await execAsync(`git checkout ${latestTag}`); } catch (error) { - console.error('[Update] Failed to checkout:', error.message); + logger.error('[Update] Failed to checkout:', error.message); throw new Error('Failed to switch to new version: ' + error.message); } @@ -310,9 +311,9 @@ export async function performUpdate() { try { const newVersion = latestTag.replace(/^v/, ''); writeFileSync(versionFilePath, newVersion, 'utf-8'); - console.log(`[Update] VERSION file updated to ${newVersion}`); + logger.info(`[Update] VERSION file updated to ${newVersion}`); } catch (error) { - console.warn('[Update] Failed to update VERSION file:', error.message); + logger.warn('[Update] Failed to update VERSION file:', error.message); } // 检查是否需要安装依赖 @@ -322,15 +323,15 @@ export async function performUpdate() { const localVersionTag = updateInfo.localVersion.startsWith('v') ? updateInfo.localVersion : `v${updateInfo.localVersion}`; const { stdout: diffOutput } = await execAsync(`git diff ${localVersionTag}..${latestTag} --name-only`); if (diffOutput.includes('package.json') || diffOutput.includes('package-lock.json')) { - console.log('[Update] package.json changed, running npm install...'); + logger.info('[Update] package.json changed, running npm install...'); await execAsync('npm install'); needsRestart = true; } } catch (error) { - console.warn('[Update] Failed to check package changes:', error.message); + logger.warn('[Update] Failed to check package changes:', error.message); } - console.log(`[Update] Update completed successfully to ${latestTag}`); + logger.info(`[Update] Update completed successfully to ${latestTag}`); return { success: true, @@ -357,16 +358,16 @@ async function performTarballUpdate(localVersion, latestTag) { const tempDir = path.join(appDir, '.update_temp'); const tarballPath = path.join(tempDir, 'update.tar.gz'); - console.log(`[Update] Starting tarball update to ${latestTag}...`); - console.log(`[Update] Download URL: ${tarballUrl}`); + logger.info(`[Update] Starting tarball update to ${latestTag}...`); + logger.info(`[Update] Download URL: ${tarballUrl}`); try { // 1. 创建临时目录 await fs.mkdir(tempDir, { recursive: true }); - console.log('[Update] Created temp directory'); + logger.info('[Update] Created temp directory'); // 2. 下载 tarball - console.log('[Update] Downloading tarball...'); + logger.info('[Update] Downloading tarball...'); const response = await fetchWithProxy(tarballUrl, { headers: { 'User-Agent': 'AIClient2API-Updater' @@ -381,10 +382,10 @@ async function performTarballUpdate(localVersion, latestTag) { const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); await fs.writeFile(tarballPath, buffer); - console.log(`[Update] Downloaded tarball (${buffer.length} bytes)`); + logger.info(`[Update] Downloaded tarball (${buffer.length} bytes)`); // 3. 解压 tarball - console.log('[Update] Extracting tarball...'); + logger.info('[Update] Extracting tarball...'); await execAsync(`tar -xzf "${tarballPath}" -C "${tempDir}"`); // 4. 找到解压后的目录(格式通常是 repo-name-tag) @@ -398,7 +399,7 @@ async function performTarballUpdate(localVersion, latestTag) { } const sourcePath = path.join(tempDir, extractedDir); - console.log(`[Update] Extracted to: ${sourcePath}`); + logger.info(`[Update] Extracted to: ${sourcePath}`); // 5. 备份当前的 package.json 用于比较 const oldPackageJson = existsSync(path.join(appDir, 'package.json')) @@ -410,9 +411,9 @@ async function performTarballUpdate(localVersion, latestTag) { for (const dirName of dirsToClean) { const dirPath = path.join(appDir, dirName); if (existsSync(dirPath)) { - console.log(`[Update] Removing old ${dirName}/ directory before extraction...`); + logger.info(`[Update] Removing old ${dirName}/ directory before extraction...`); await fs.rm(dirPath, { recursive: true, force: true }); - console.log(`[Update] Old ${dirName}/ directory removed`); + logger.info(`[Update] Old ${dirName}/ directory removed`); } } @@ -425,13 +426,13 @@ async function performTarballUpdate(localVersion, latestTag) { ]; // 7. 复制新文件到应用目录 - console.log('[Update] Copying new files...'); + logger.info('[Update] Copying new files...'); const sourceItems = await fs.readdir(sourcePath); for (const item of sourceItems) { // 跳过需要保留的目录 if (preservePaths.includes(item)) { - console.log(`[Update] Skipping preserved path: ${item}`); + logger.info(`[Update] Skipping preserved path: ${item}`); continue; } @@ -450,7 +451,7 @@ async function performTarballUpdate(localVersion, latestTag) { // 复制新文件/目录 await copyRecursive(srcItemPath, destItemPath); - console.log(`[Update] Copied: ${item}`); + logger.info(`[Update] Copied: ${item}`); } // 8. 检查是否需要更新依赖 @@ -460,23 +461,23 @@ async function performTarballUpdate(localVersion, latestTag) { if (oldPackageJson) { const newPackageJson = readFileSync(path.join(appDir, 'package.json'), 'utf-8'); if (oldPackageJson !== newPackageJson) { - console.log('[Update] package.json changed, running npm install...'); + logger.info('[Update] package.json changed, running npm install...'); needsNpmInstall = true; try { await execAsync('npm install', { cwd: appDir }); - console.log('[Update] npm install completed'); + logger.info('[Update] npm install completed'); } catch (npmError) { - console.error('[Update] npm install failed:', npmError.message); + logger.error('[Update] npm install failed:', npmError.message); // 不抛出错误,继续更新流程 } } } // 9. 清理临时目录 - console.log('[Update] Cleaning up...'); + logger.info('[Update] Cleaning up...'); await fs.rm(tempDir, { recursive: true, force: true }); - console.log(`[Update] Tarball update completed successfully to ${latestTag}`); + logger.info(`[Update] Tarball update completed successfully to ${latestTag}`); return { success: true, @@ -497,10 +498,10 @@ async function performTarballUpdate(localVersion, latestTag) { await fs.rm(tempDir, { recursive: true, force: true }); } } catch (cleanupError) { - console.warn('[Update] Failed to cleanup temp directory:', cleanupError.message); + logger.warn('[Update] Failed to cleanup temp directory:', cleanupError.message); } - console.error('[Update] Tarball update failed:', error.message); + logger.error('[Update] Tarball update failed:', error.message); throw new Error(`Tarball update failed: ${error.message}`); } } @@ -534,7 +535,7 @@ export async function handleCheckUpdate(req, res) { res.end(JSON.stringify(updateInfo)); return true; } catch (error) { - console.error('[UI API] Failed to check for updates:', error); + logger.error('[UI API] Failed to check for updates:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -555,7 +556,7 @@ export async function handlePerformUpdate(req, res) { res.end(JSON.stringify(updateResult)); return true; } catch (error) { - console.error('[UI API] Failed to perform update:', error); + logger.error('[UI API] Failed to perform update:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { diff --git a/src/ui-modules/upload-config-api.js b/src/ui-modules/upload-config-api.js index 426c323..0ca8a0b 100644 --- a/src/ui-modules/upload-config-api.js +++ b/src/ui-modules/upload-config-api.js @@ -1,4 +1,5 @@ import { existsSync } from 'fs'; +import logger from '../utils/logger.js'; import { promises as fs } from 'fs'; import path from 'path'; import AdmZip from 'adm-zip'; @@ -15,7 +16,7 @@ export async function handleGetUploadConfigs(req, res, currentConfig, providerPo res.end(JSON.stringify(configFiles)); return true; } catch (error) { - console.error('[UI API] Failed to scan config files:', error); + logger.error('[UI API] Failed to scan config files:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -71,7 +72,7 @@ export async function handleViewConfigFile(req, res, filePath) { })); return true; } catch (error) { - console.error('[UI API] Failed to view config file:', error); + logger.error('[UI API] Failed to view config file:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -132,7 +133,7 @@ export async function handleDeleteConfigFile(req, res, filePath) { })); return true; } catch (error) { - console.error('[UI API] Failed to delete config file:', error); + logger.error('[UI API] Failed to delete config file:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -185,10 +186,10 @@ export async function handleDownloadAllConfigs(req, res) { }); res.end(zipBuffer); - console.log(`[UI API] All configs downloaded as zip: ${filename}`); + logger.info(`[UI API] All configs downloaded as zip: ${filename}`); return true; } catch (error) { - console.error('[UI API] Failed to download all configs:', error); + logger.error('[UI API] Failed to download all configs:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -298,7 +299,7 @@ export async function handleDeleteUnboundConfigs(req, res, currentConfig, provid })); return true; } catch (error) { - console.error('[UI API] Failed to delete unbound configs:', error); + logger.error('[UI API] Failed to delete unbound configs:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { diff --git a/src/ui-modules/usage-api.js b/src/ui-modules/usage-api.js index b7f73ec..09695bc 100644 --- a/src/ui-modules/usage-api.js +++ b/src/ui-modules/usage-api.js @@ -1,4 +1,5 @@ import { CONFIG } from '../core/config-manager.js'; +import logger from '../utils/logger.js'; import { serviceInstances, getServiceAdapter } from '../providers/adapter.js'; import { formatKiroUsage, formatGeminiUsage, formatAntigravityUsage } from '../services/usage-service.js'; import { readUsageCache, writeUsageCache, readProviderUsageCache, updateProviderUsageCache } from './usage-cache.js'; @@ -95,7 +96,7 @@ async function getProviderTypeUsage(providerType, currentConfig, providerPoolMan } else if (!adapter) { // Service instance not initialized, try auto-initialization try { - console.log(`[Usage API] Auto-initializing service adapter for ${providerType}: ${provider.uuid}`); + logger.info(`[Usage API] Auto-initializing service adapter for ${providerType}: ${provider.uuid}`); // Build configuration object const serviceConfig = { ...CONFIG, @@ -104,7 +105,7 @@ async function getProviderTypeUsage(providerType, currentConfig, providerPoolMan }; adapter = getServiceAdapter(serviceConfig); } catch (initError) { - console.error(`[Usage API] Failed to initialize adapter for ${providerType}: ${provider.uuid}:`, initError.message); + logger.error(`[Usage API] Failed to initialize adapter for ${providerType}: ${provider.uuid}:`, initError.message); instanceResult.error = `Service instance initialization failed: ${initError.message}`; result.errorCount++; } @@ -222,14 +223,14 @@ export async function handleGetUsage(req, res, currentConfig, providerPoolManage // 优先读取缓存 const cachedData = await readUsageCache(); if (cachedData) { - console.log('[Usage API] Returning cached usage data'); + logger.info('[Usage API] Returning cached usage data'); usageResults = { ...cachedData, fromCache: true }; } } if (!usageResults) { // 缓存不存在或需要刷新,重新查询 - console.log('[Usage API] Fetching fresh usage data'); + logger.info('[Usage API] Fetching fresh usage data'); usageResults = await getAllProvidersUsage(currentConfig, providerPoolManager); // 写入缓存 await writeUsageCache(usageResults); @@ -239,7 +240,7 @@ export async function handleGetUsage(req, res, currentConfig, providerPoolManage res.end(JSON.stringify(usageResults)); return true; } catch (error) { - console.error('[UI API] Failed to get usage:', error); + logger.error('[UI API] Failed to get usage:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { @@ -265,14 +266,14 @@ export async function handleGetProviderUsage(req, res, currentConfig, providerPo // Prefer reading from cache const cachedData = await readProviderUsageCache(providerType); if (cachedData) { - console.log(`[Usage API] Returning cached usage data for ${providerType}`); + logger.info(`[Usage API] Returning cached usage data for ${providerType}`); usageResults = cachedData; } } if (!usageResults) { // Cache does not exist or refresh required, re-query - console.log(`[Usage API] Fetching fresh usage data for ${providerType}`); + logger.info(`[Usage API] Fetching fresh usage data for ${providerType}`); usageResults = await getProviderTypeUsage(providerType, currentConfig, providerPoolManager); // 更新缓存 await updateProviderUsageCache(providerType, usageResults); @@ -282,7 +283,7 @@ export async function handleGetProviderUsage(req, res, currentConfig, providerPo res.end(JSON.stringify(usageResults)); return true; } catch (error) { - console.error(`[UI API] Failed to get usage for ${providerType}:`, error); + logger.error(`[UI API] Failed to get usage for ${providerType}:`, error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { diff --git a/src/ui-modules/usage-cache.js b/src/ui-modules/usage-cache.js index 0402be0..1de626b 100644 --- a/src/ui-modules/usage-cache.js +++ b/src/ui-modules/usage-cache.js @@ -1,4 +1,5 @@ import { existsSync } from 'fs'; +import logger from '../utils/logger.js'; import { promises as fs } from 'fs'; import path from 'path'; @@ -17,7 +18,7 @@ export async function readUsageCache() { } return null; } catch (error) { - console.warn('[Usage Cache] Failed to read usage cache:', error.message); + logger.warn('[Usage Cache] Failed to read usage cache:', error.message); return null; } } @@ -29,9 +30,9 @@ export async function readUsageCache() { export async function writeUsageCache(usageData) { try { await fs.writeFile(USAGE_CACHE_FILE, JSON.stringify(usageData, null, 2), 'utf8'); - console.log('[Usage Cache] Usage data cached to', USAGE_CACHE_FILE); + logger.info('[Usage Cache] Usage data cached to', USAGE_CACHE_FILE); } catch (error) { - console.error('[Usage Cache] Failed to write usage cache:', error.message); + logger.error('[Usage Cache] Failed to write usage cache:', error.message); } } diff --git a/src/utils/common.js b/src/utils/common.js index 547591d..3c4846b 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -2,6 +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 logger from './logger.js'; import { convertData, getOpenAIStreamChunkStop } from '../convert/convert.js'; import { ProviderStrategyFactory } from './provider-strategies.js'; import { getPluginManager } from '../core/plugin-manager.js'; @@ -200,13 +201,13 @@ export async function logConversation(type, content, logMode, logFilename) { const logEntry = `${timestamp} [${type.toUpperCase()}]:\n${content}\n--------------------------------------\n`; if (logMode === 'console') { - console.log(logEntry); + logger.info(logEntry); } else if (logMode === 'file') { try { // Append to the file await fs.appendFile(logFilename, logEntry); } catch (err) { - console.error(`[Error] Failed to write conversation log to ${logFilename}:`, err); + logger.error(`[Error] Failed to write conversation log to ${logFilename}:`, err); } } } @@ -247,7 +248,7 @@ export function isAuthorized(req, requestUrl, REQUIRED_API_KEY) { return true; } - console.log(`[Auth] Unauthorized request denied. Bearer: "${authHeader ? 'present' : 'N/A'}", Query Key: "${queryKey}", x-goog-api-key: "${googApiKey}", x-api-key: "${claudeApiKey}"`); + logger.info(`[Auth] Unauthorized request denied. Bearer: "${authHeader ? 'present' : 'N/A'}", Query Key: "${queryKey}", x-goog-api-key: "${googApiKey}", x-api-key: "${claudeApiKey}"`); return false; } @@ -323,35 +324,35 @@ export async function handleStreamRequest(res, service, model, requestBody, from // fullOldResponseJson += chunk.type+"\n"; // fullResponseJson += chunk.type+"\n"; res.write(`event: ${chunk.type}\n`); - // console.log(`event: ${chunk.type}\n`); + // logger.info(`event: ${chunk.type}\n`); } // fullOldResponseJson += JSON.stringify(chunk)+"\n"; // fullResponseJson += JSON.stringify(chunk)+"\n\n"; res.write(`data: ${JSON.stringify(chunk)}\n\n`); - // console.log(`data: ${JSON.stringify(chunk)}\n`); + // logger.info(`data: ${JSON.stringify(chunk)}\n`); } } if (openStop && needsConversion) { res.write(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n\n`); - // console.log(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n`); + // logger.info(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n`); } // 流式请求成功完成,统计使用次数,错误次数重置为0 if (providerPoolManager && pooluuid) { const customNameDisplay = customName ? `, ${customName}` : ''; - console.log(`[Provider Pool] Increasing usage count for ${toProvider} (${pooluuid}${customNameDisplay}) after successful stream request`); + logger.info(`[Provider Pool] Increasing usage count for ${toProvider} (${pooluuid}${customNameDisplay}) after successful stream request`); providerPoolManager.markProviderHealthy(toProvider, { uuid: pooluuid }); } } catch (error) { - console.error('\n[Server] Error during stream processing:', error.stack); + logger.error('\n[Server] Error during stream processing:', error.stack); // 如果已经发送了内容,不进行重试(避免响应数据损坏) if (fullResponseText.length > 0) { - console.log(`[Stream Retry] Cannot retry: ${fullResponseText.length} bytes already sent to client`); + logger.info(`[Stream Retry] Cannot retry: ${fullResponseText.length} bytes already sent to client`); // 直接发送错误并结束 const errorPayload = createStreamErrorResponse(error, fromProvider); res.write(errorPayload); @@ -375,9 +376,9 @@ export async function handleStreamRequest(res, service, model, requestBody, from if (!credentialMarkedUnhealthy && !skipErrorCount && providerPoolManager && pooluuid) { // 400 报错码通常是请求参数问题,不记录为提供商错误 if (status === 400) { - console.log(`[Provider Pool] Skipping unhealthy marking for ${toProvider} (${pooluuid}) due to status 400 (client error)`); + logger.info(`[Provider Pool] Skipping unhealthy marking for ${toProvider} (${pooluuid}) due to status 400 (client error)`); } else { - console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error (status: ${status || 'unknown'})`); + logger.info(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error (status: ${status || 'unknown'})`); // 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康 providerPoolManager.markProviderUnhealthy(toProvider, { uuid: pooluuid @@ -385,9 +386,9 @@ export async function handleStreamRequest(res, service, model, requestBody, from credentialMarkedUnhealthy = true; } } else if (credentialMarkedUnhealthy) { - console.log(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`); + logger.info(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`); } else if (skipErrorCount) { - console.log(`[Provider Pool] Skipping error count for ${toProvider} (${pooluuid}) - will switch credential without marking unhealthy`); + logger.info(`[Provider Pool] Skipping error count for ${toProvider} (${pooluuid}) - will switch credential without marking unhealthy`); } // 如果需要切换凭证(无论是否标记不健康),都设置标记以触发重试 @@ -400,7 +401,7 @@ export async function handleStreamRequest(res, service, model, requestBody, from if (credentialMarkedUnhealthy && currentRetry < maxRetries && providerPoolManager && CONFIG) { // 增加10秒内的随机等待时间,避免所有请求同时切换凭证 const randomDelay = Math.floor(Math.random() * 10000); // 0-10000毫秒 - console.log(`[Stream Retry] Credential marked unhealthy. Waiting ${randomDelay}ms before retry ${currentRetry + 1}/${maxRetries} with different credential...`); + logger.info(`[Stream Retry] Credential marked unhealthy. Waiting ${randomDelay}ms before retry ${currentRetry + 1}/${maxRetries} with different credential...`); await new Promise(resolve => setTimeout(resolve, randomDelay)); try { @@ -408,8 +409,8 @@ export async function handleStreamRequest(res, service, model, requestBody, from const { getApiServiceWithFallback } = await import('../services/service-manager.js'); const result = await getApiServiceWithFallback(CONFIG, model); - if (result && result.service && result.uuid !== pooluuid) { - console.log(`[Stream Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`); + if (result && result.service) { + logger.info(`[Stream Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`); // 使用新服务重试 const newRetryContext = { @@ -434,13 +435,11 @@ export async function handleStreamRequest(res, service, model, requestBody, from result.serviceConfig?.customName || customName, newRetryContext ); - } else if (result && result.uuid === pooluuid) { - console.log(`[Stream Retry] No different healthy credential available. Same credential selected.`); } else { - console.log(`[Stream Retry] No healthy credential available for retry.`); + logger.info(`[Stream Retry] No healthy credential available for retry.`); } } catch (retryError) { - console.error(`[Stream Retry] Failed to get alternative service:`, retryError.message); + logger.error(`[Stream Retry] Failed to get alternative service:`, retryError.message); } } @@ -478,11 +477,11 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP // Convert the response back to the client's format (fromProvider), if necessary. let clientResponse = nativeResponse; if (needsConversion) { - console.log(`[Response Convert] Converting response from ${toProvider} to ${fromProvider}`); + logger.info(`[Response Convert] Converting response from ${toProvider} to ${fromProvider}`); clientResponse = convertData(nativeResponse, 'response', toProvider, fromProvider, model); } - //console.log(`[Response] Sending response to client: ${JSON.stringify(clientResponse)}`); + //logger.info(`[Response] Sending response to client: ${JSON.stringify(clientResponse)}`); await handleUnifiedResponse(res, JSON.stringify(clientResponse), false); await logConversation('output', responseText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME); // fs.writeFile('oldResponse'+Date.now()+'.json', JSON.stringify(clientResponse)); @@ -490,13 +489,13 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP // 一元请求成功完成,统计使用次数,错误次数重置为0 if (providerPoolManager && pooluuid) { const customNameDisplay = customName ? `, ${customName}` : ''; - console.log(`[Provider Pool] Increasing usage count for ${toProvider} (${pooluuid}${customNameDisplay}) after successful unary request`); + logger.info(`[Provider Pool] Increasing usage count for ${toProvider} (${pooluuid}${customNameDisplay}) after successful unary request`); providerPoolManager.markProviderHealthy(toProvider, { uuid: pooluuid }); } } catch (error) { - console.error('\n[Server] Error during unary processing:', error.stack); + logger.error('\n[Server] Error during unary processing:', error.stack); // 获取状态码(用于日志记录,不再用于判断是否重试) const status = error.response?.status; @@ -513,9 +512,9 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP if (!credentialMarkedUnhealthy && !skipErrorCount && providerPoolManager && pooluuid) { // 400 报错码通常是请求参数问题,不记录为提供商错误 if (status === 400) { - console.log(`[Provider Pool] Skipping unhealthy marking for ${toProvider} (${pooluuid}) due to status 400 (client error)`); + logger.info(`[Provider Pool] Skipping unhealthy marking for ${toProvider} (${pooluuid}) due to status 400 (client error)`); } else { - console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to unary error (status: ${status || 'unknown'})`); + logger.info(`[Provider Pool] Marking ${toProvider} as unhealthy due to unary error (status: ${status || 'unknown'})`); // 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康 providerPoolManager.markProviderUnhealthy(toProvider, { uuid: pooluuid @@ -523,9 +522,9 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP credentialMarkedUnhealthy = true; } } else if (credentialMarkedUnhealthy) { - console.log(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`); + logger.info(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`); } else if (skipErrorCount) { - console.log(`[Provider Pool] Skipping error count for ${toProvider} (${pooluuid}) - will switch credential without marking unhealthy`); + logger.info(`[Provider Pool] Skipping error count for ${toProvider} (${pooluuid}) - will switch credential without marking unhealthy`); } // 如果需要切换凭证(无论是否标记不健康),都设置标记以触发重试 @@ -538,7 +537,7 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP if (credentialMarkedUnhealthy && currentRetry < maxRetries && providerPoolManager && CONFIG) { // 增加10秒内的随机等待时间,避免所有请求同时切换凭证 const randomDelay = Math.floor(Math.random() * 10000); // 0-10000毫秒 - console.log(`[Unary Retry] Credential marked unhealthy. Waiting ${randomDelay}ms before retry ${currentRetry + 1}/${maxRetries} with different credential...`); + logger.info(`[Unary Retry] Credential marked unhealthy. Waiting ${randomDelay}ms before retry ${currentRetry + 1}/${maxRetries} with different credential...`); await new Promise(resolve => setTimeout(resolve, randomDelay)); try { @@ -546,8 +545,8 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP const { getApiServiceWithFallback } = await import('../services/service-manager.js'); const result = await getApiServiceWithFallback(CONFIG, model); - if (result && result.service && result.uuid !== pooluuid) { - console.log(`[Unary Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`); + if (result && result.service) { + logger.info(`[Unary Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`); // 使用新服务重试 const newRetryContext = { @@ -572,13 +571,11 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP result.serviceConfig?.customName || customName, newRetryContext ); - } else if (result && result.uuid === pooluuid) { - console.log(`[Unary Retry] No different healthy credential available. Same credential selected.`); } else { - console.log(`[Unary Retry] No healthy credential available for retry.`); + logger.info(`[Unary Retry] No healthy credential available for retry.`); } } catch (retryError) { - console.error(`[Unary Retry] Failed to get alternative service:`, retryError.message); + logger.error(`[Unary Retry] Failed to get alternative service:`, retryError.message); } } @@ -618,17 +615,17 @@ export async function handleModelListRequest(req, res, service, endpointType, CO // 2. Convert the model list to the client's expected format, if necessary. let clientModelList = nativeModelList; if (!getProtocolPrefix(toProvider).includes(getProtocolPrefix(fromProvider))) { - console.log(`[ModelList Convert] Converting model list from ${toProvider} to ${fromProvider}`); + logger.info(`[ModelList Convert] Converting model list from ${toProvider} to ${fromProvider}`); clientModelList = convertData(nativeModelList, 'modelList', toProvider, fromProvider); } else { - console.log(`[ModelList Convert] Model list format matches. No conversion needed.`); + logger.info(`[ModelList Convert] Model list format matches. No conversion needed.`); } - console.log(`[ModelList Response] Sending model list to client: ${JSON.stringify(clientModelList)}`); + logger.info(`[ModelList Response] Sending model list to client: ${JSON.stringify(clientModelList)}`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(clientModelList)); } catch (error) { - console.error('\n[Server] Error during model list processing:', error.stack); + logger.error('\n[Server] Error during model list processing:', error.stack); if (providerPoolManager) { // 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康 providerPoolManager.markProviderUnhealthy(toProvider, { @@ -677,7 +674,7 @@ export async function handleContentGenerationRequest(req, res, service, endpoint if (!model) { throw new Error("Could not determine the model from the request."); } - console.log(`[Content Generation] Model: ${model}, Stream: ${isStream}`); + logger.info(`[Content Generation] Model: ${model}, Stream: ${isStream}`); let actualCustomName = CONFIG.customName; @@ -694,14 +691,14 @@ export async function handleContentGenerationRequest(req, res, service, endpoint // 如果发生了模型级别的 fallback,需要更新请求使用的模型 if (result.actualModel && result.actualModel !== model) { - console.log(`[Content Generation] Model Fallback: ${model} -> ${result.actualModel}`); + logger.info(`[Content Generation] Model Fallback: ${model} -> ${result.actualModel}`); model = result.actualModel; } if (result.isFallback) { - console.log(`[Content Generation] Fallback activated: ${CONFIG.MODEL_PROVIDER} -> ${toProvider} (uuid: ${actualUuid})`); + logger.info(`[Content Generation] Fallback activated: ${CONFIG.MODEL_PROVIDER} -> ${toProvider} (uuid: ${actualUuid})`); } else { - console.log(`[Content Generation] Re-selected service adapter based on model: ${model}`); + logger.info(`[Content Generation] Re-selected service adapter based on model: ${model}`); } } @@ -709,15 +706,15 @@ export async function handleContentGenerationRequest(req, res, service, endpoint let processedRequestBody = originalRequestBody; // fs.writeFile('originalRequestBody'+Date.now()+'.json', JSON.stringify(originalRequestBody)); if (getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider)) { - console.log(`[Request Convert] Converting request from ${fromProvider} to ${toProvider}`); + logger.info(`[Request Convert] Converting request from ${fromProvider} to ${toProvider}`); processedRequestBody = convertData(originalRequestBody, 'request', fromProvider, toProvider); } else { - console.log(`[Request Convert] Request format matches backend provider. No conversion needed.`); + logger.info(`[Request Convert] Request format matches backend provider. No conversion needed.`); } // 为 forward provider 添加原始请求路径作为 endpoint if (requestPath && toProvider === MODEL_PROVIDER.FORWARD_API) { - console.log(`[Forward API] Request path: ${requestPath}`); + logger.info(`[Forward API] Request path: ${requestPath}`); processedRequestBody.endpoint = requestPath; } @@ -828,14 +825,14 @@ export function handleError(res, error, provider = null) { } errorMessage = hasOriginalMessage ? error.message.trim() : errorMessage; - console.error(`\n[Server] Request failed (${statusCode}): ${errorMessage}`); + logger.error(`\n[Server] Request failed (${statusCode}): ${errorMessage}`); if (suggestions.length > 0) { - console.error('[Server] Suggestions:'); + logger.error('[Server] Suggestions:'); suggestions.forEach((suggestion, index) => { - console.error(` ${index + 1}. ${suggestion}`); + logger.error(` ${index + 1}. ${suggestion}`); }); } - console.error('[Server] Full error details:', error.stack); + logger.error('[Server] Full error details:', error.stack); if (!res.headersSent) { res.writeHead(statusCode, { 'Content-Type': 'application/json' }); @@ -1071,7 +1068,7 @@ export function extractSystemPromptFromRequestBody(requestBody, provider) { } break; default: - console.warn(`[System Prompt] Unknown provider: ${provider}`); + logger.warn(`[System Prompt] Unknown provider: ${provider}`); break; } return incomingSystemText; diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..fd1601e --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,353 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { randomUUID } from 'crypto'; + +/** + * 统一日志工具类 + * 支持控制台和文件输出,自动添加请求ID和时间戳 + */ +class Logger { + constructor() { + this.config = { + enabled: true, + outputMode: 'all', // 'console', 'file', 'all', 'none' + logDir: 'logs', + logLevel: 'info', // 'debug', 'info', 'warn', 'error' + includeRequestId: true, + includeTimestamp: true, + maxFileSize: 10 * 1024 * 1024, // 10MB + maxFiles: 10 + }; + this.currentLogFile = null; + this.logStream = null; + this.currentRequestId = null; // 当前请求ID + this.requestContext = new Map(); // 存储请求上下文 + this.levels = { + debug: 0, + info: 1, + warn: 2, + error: 3 + }; + } + + /** + * 初始化日志配置 + * @param {Object} config - 日志配置对象 + */ + initialize(config = {}) { + this.config = { ...this.config, ...config }; + + if (this.config.outputMode === 'none') { + this.config.enabled = false; + return; + } + + if (this.config.outputMode === 'file' || this.config.outputMode === 'all') { + this.initializeFileLogging(); + } + } + + /** + * 初始化文件日志 + */ + initializeFileLogging() { + try { + // 确保日志目录存在 + if (!fs.existsSync(this.config.logDir)) { + fs.mkdirSync(this.config.logDir, { recursive: true }); + } + + // 创建日志文件名(按本地日期) + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const dateStr = `${year}-${month}-${day}`; + this.currentLogFile = path.join(this.config.logDir, `app-${dateStr}.log`); + + // 创建写入流 + this.logStream = fs.createWriteStream(this.currentLogFile, { flags: 'a' }); + + // 监听错误 + this.logStream.on('error', (err) => { + console.error('[Logger] Failed to write to log file:', err.message); + }); + } catch (error) { + console.error('[Logger] Failed to initialize file logging:', error.message); + } + } + + /** + * 设置请求上下文 + * @param {string} requestId - 请求ID + * @param {Object} context - 上下文信息 + */ + setRequestContext(requestId, context = {}) { + if (!requestId) { + requestId = randomUUID().substring(0, 8); + } + this.currentRequestId = requestId; + this.requestContext.set(requestId, context); + return requestId; + } + + /** + * 获取当前请求ID + * @returns {string} 请求ID + */ + getCurrentRequestId() { + // 从上下文中获取当前请求ID + return this.currentRequestId; + } + + /** + * 获取当前请求上下文 + * @param {string} requestId - 请求ID + * @returns {Object} 上下文信息 + */ + getRequestContext(requestId) { + return this.requestContext.get(requestId) || {}; + } + + /** + * 清除请求上下文 + * @param {string} requestId - 请求ID + */ + clearRequestContext(requestId) { + if (requestId) { + this.requestContext.delete(requestId); + } + this.currentRequestId = null; + } + + /** + * 格式化日志消息 + * @param {string} level - 日志级别 + * @param {Array} args - 日志参数 + * @param {string} requestId - 请求ID + * @returns {string} 格式化后的日志 + */ + formatMessage(level, args, requestId) { + const parts = []; + + // 添加本地时间戳 + if (this.config.includeTimestamp) { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + const ms = String(now.getMilliseconds()).padStart(3, '0'); + const timestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`; + parts.push(`[${timestamp}]`); + } + + // 添加请求ID + if (this.config.includeRequestId && requestId) { + parts.push(`[Req:${requestId}]`); + } + + // 添加日志级别 + parts.push(`[${level.toUpperCase()}]`); + + // 添加消息内容 + const message = args.map(arg => { + if (typeof arg === 'object') { + try { + return JSON.stringify(arg, null, 2); + } catch (e) { + return String(arg); + } + } + return String(arg); + }).join(' '); + + parts.push(message); + + return parts.join(' '); + } + + /** + * 检查是否应该输出该级别的日志 + * @param {string} level - 日志级别 + * @returns {boolean} + */ + shouldLog(level) { + if (!this.config.enabled) return false; + const currentLevel = this.levels[this.config.logLevel] ?? 1; + const targetLevel = this.levels[level] ?? 1; + return targetLevel >= currentLevel; + } + + /** + * 检查并轮转日志文件 + */ + checkAndRotateLogFile() { + try { + if (!this.currentLogFile || !fs.existsSync(this.currentLogFile)) { + return; + } + + const stats = fs.statSync(this.currentLogFile); + if (stats.size >= this.config.maxFileSize) { + // 关闭当前日志流 + if (this.logStream && !this.logStream.destroyed) { + this.logStream.end(); + } + + // 重命名当前日志文件,添加时间戳 + const timestamp = new Date().getTime(); + const ext = path.extname(this.currentLogFile); + const basename = path.basename(this.currentLogFile, ext); + const newName = path.join(this.config.logDir, `${basename}-${timestamp}${ext}`); + fs.renameSync(this.currentLogFile, newName); + + // 重新创建日志流 + this.logStream = fs.createWriteStream(this.currentLogFile, { flags: 'a' }); + this.logStream.on('error', (err) => { + console.error('[Logger] Failed to write to log file:', err.message); + }); + + // 清理旧日志文件 + this.cleanupOldLogs(); + } + } catch (error) { + console.error('[Logger] Failed to rotate log file:', error.message); + } + } + + /** + * 输出日志 + * @param {string} level - 日志级别 + * @param {Array} args - 日志参数 + * @param {string} requestId - 请求ID + */ + log(level, args, requestId = null) { + if (!this.shouldLog(level)) return; + + const message = this.formatMessage(level, args, requestId); + + // 输出到控制台 + if (this.config.outputMode === 'console' || this.config.outputMode === 'all') { + const consoleMethod = level === 'error' ? console.error : + level === 'warn' ? console.warn : + level === 'debug' ? console.debug : console.log; + consoleMethod(message); + } + + // 输出到文件 + if (this.config.outputMode === 'file' || this.config.outputMode === 'all') { + if (this.logStream && !this.logStream.destroyed) { + // 检查文件大小并轮转 + this.checkAndRotateLogFile(); + this.logStream.write(message + '\n'); + } + } + } + + /** + * Debug 级别日志 + * @param {...any} args - 日志参数 + */ + debug(...args) { + const requestId = this.getCurrentRequestId(); + this.log('debug', args, requestId); + } + + /** + * Info 级别日志 + * @param {...any} args - 日志参数 + */ + info(...args) { + const requestId = this.getCurrentRequestId(); + this.log('info', args, requestId); + } + + /** + * Warn 级别日志 + * @param {...any} args - 日志参数 + */ + warn(...args) { + const requestId = this.getCurrentRequestId(); + this.log('warn', args, requestId); + } + + /** + * Error 级别日志 + * @param {...any} args - 日志参数 + */ + error(...args) { + const requestId = this.getCurrentRequestId(); + this.log('error', args, requestId); + } + + /** + * 创建带请求ID的日志记录器 + * @param {string} requestId - 请求ID + * @returns {Object} 带请求上下文的日志方法 + */ + withRequest(requestId) { + if (!requestId) { + requestId = this.getCurrentRequestId(); + } + + return { + debug: (...args) => this.log('debug', args, requestId), + info: (...args) => this.log('info', args, requestId), + warn: (...args) => this.log('warn', args, requestId), + error: (...args) => this.log('error', args, requestId) + }; + } + + /** + * 关闭日志流 + */ + close() { + if (this.logStream && !this.logStream.destroyed) { + this.logStream.end(); + this.logStream = null; + } + } + + /** + * 清理旧日志文件 + */ + cleanupOldLogs() { + try { + if (!fs.existsSync(this.config.logDir)) { + return; + } + + const files = fs.readdirSync(this.config.logDir) + .filter(file => file.startsWith('app-') && file.endsWith('.log')) + .map(file => ({ + name: file, + path: path.join(this.config.logDir, file), + time: fs.statSync(path.join(this.config.logDir, file)).mtime.getTime() + })) + .sort((a, b) => b.time - a.time); + + // 保留最新的 maxFiles 个文件,删除其他的 + if (files.length > this.config.maxFiles) { + for (let i = this.config.maxFiles; i < files.length; i++) { + try { + fs.unlinkSync(files[i].path); + } catch (err) { + console.error('[Logger] Failed to delete old log file:', files[i].name, err.message); + } + } + } + } catch (error) { + console.error('[Logger] Failed to cleanup old logs:', error.message); + } + } +} + +// 创建单例实例 +const logger = new Logger(); + +// 导出实例和类 +export default logger; +export { Logger }; diff --git a/src/utils/provider-strategy.js b/src/utils/provider-strategy.js index e2c2725..f408f96 100644 --- a/src/utils/provider-strategy.js +++ b/src/utils/provider-strategy.js @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import logger from './logger.js'; import { FETCH_SYSTEM_PROMPT_FILE } from '../utils/common.js'; /** @@ -64,20 +65,20 @@ export class ProviderStrategy { currentSystemText = await fs.readFile(FETCH_SYSTEM_PROMPT_FILE, 'utf8'); } catch (error) { if (error.code !== 'ENOENT') { - console.error(`[System Prompt Manager] Error reading system prompt file: ${error.message}`); + logger.error(`[System Prompt Manager] Error reading system prompt file: ${error.message}`); } } try { if (incomingSystemText && incomingSystemText !== currentSystemText) { await fs.writeFile(FETCH_SYSTEM_PROMPT_FILE, incomingSystemText); - console.log(`[System Prompt Manager] System prompt updated in file for provider '${providerName}'.`); + logger.info(`[System Prompt Manager] System prompt updated in file for provider '${providerName}'.`); } else if (!incomingSystemText && currentSystemText) { await fs.writeFile(FETCH_SYSTEM_PROMPT_FILE, ''); - console.log('[System Prompt Manager] System prompt cleared from file.'); + logger.info('[System Prompt Manager] System prompt cleared from file.'); } } catch (error) { - console.error(`[System Prompt Manager] Failed to manage system prompt file: ${error.message}`); + logger.error(`[System Prompt Manager] Failed to manage system prompt file: ${error.message}`); } } } diff --git a/src/utils/provider-utils.js b/src/utils/provider-utils.js index d02eafc..23313c2 100644 --- a/src/utils/provider-utils.js +++ b/src/utils/provider-utils.js @@ -4,6 +4,7 @@ */ import * as path from 'path'; +import logger from './logger.js'; import { promises as fs } from 'fs'; /** @@ -163,7 +164,7 @@ export function pathsEqual(path1, path2) { return false; } catch (error) { - console.warn(`[Path Comparison] Error comparing paths: ${path1} vs ${path2}`, error.message); + logger.warn(`[Path Comparison] Error comparing paths: ${path1} vs ${path2}`, error.message); return false; } } diff --git a/src/utils/proxy-utils.js b/src/utils/proxy-utils.js index d1fb27c..3051bc4 100644 --- a/src/utils/proxy-utils.js +++ b/src/utils/proxy-utils.js @@ -4,6 +4,7 @@ */ import { HttpsProxyAgent } from 'https-proxy-agent'; +import logger from './logger.js'; import { HttpProxyAgent } from 'http-proxy-agent'; import { SocksProxyAgent } from 'socks-proxy-agent'; @@ -42,11 +43,11 @@ export function parseProxyUrl(proxyUrl) { proxyType: 'http' }; } else { - console.warn(`[Proxy] Unsupported proxy protocol: ${protocol}`); + logger.warn(`[Proxy] Unsupported proxy protocol: ${protocol}`); return null; } } catch (error) { - console.error(`[Proxy] Failed to parse proxy URL: ${error.message}`); + logger.error(`[Proxy] Failed to parse proxy URL: ${error.message}`); return null; } } @@ -83,7 +84,7 @@ export function getProxyConfigForProvider(config, providerType) { const proxyConfig = parseProxyUrl(config.PROXY_URL); if (proxyConfig) { - console.log(`[Proxy] Using ${proxyConfig.proxyType} proxy for ${providerType}: ${config.PROXY_URL}`); + logger.info(`[Proxy] Using ${proxyConfig.proxyType} proxy for ${providerType}: ${config.PROXY_URL}`); } return proxyConfig; } diff --git a/static/app/config-manager.js b/static/app/config-manager.js index 4776925..2597034 100644 --- a/static/app/config-manager.js +++ b/static/app/config-manager.js @@ -155,6 +155,25 @@ async function loadConfiguration() { }); } + // 加载日志配置 + const logEnabledEl = document.getElementById('logEnabled'); + const logOutputModeEl = document.getElementById('logOutputMode'); + const logLevelEl = document.getElementById('logLevel'); + const logDirEl = document.getElementById('logDir'); + const logIncludeRequestIdEl = document.getElementById('logIncludeRequestId'); + const logIncludeTimestampEl = document.getElementById('logIncludeTimestamp'); + const logMaxFileSizeEl = document.getElementById('logMaxFileSize'); + const logMaxFilesEl = document.getElementById('logMaxFiles'); + + if (logEnabledEl) logEnabledEl.checked = data.LOG_ENABLED !== false; + if (logOutputModeEl) logOutputModeEl.value = data.LOG_OUTPUT_MODE || 'all'; + if (logLevelEl) logLevelEl.value = data.LOG_LEVEL || 'info'; + if (logDirEl) logDirEl.value = data.LOG_DIR || 'logs'; + if (logIncludeRequestIdEl) logIncludeRequestIdEl.checked = data.LOG_INCLUDE_REQUEST_ID !== false; + if (logIncludeTimestampEl) logIncludeTimestampEl.checked = data.LOG_INCLUDE_TIMESTAMP !== false; + if (logMaxFileSizeEl) logMaxFileSizeEl.value = data.LOG_MAX_FILE_SIZE || 10485760; + if (logMaxFilesEl) logMaxFilesEl.value = data.LOG_MAX_FILES || 10; + } catch (error) { console.error('Failed to load configuration:', error); } @@ -201,7 +220,6 @@ async function saveConfiguration() { config.CRON_REFRESH_TOKEN = document.getElementById('cronRefreshToken')?.checked || false; config.PROVIDER_POOLS_FILE_PATH = document.getElementById('providerPoolsFilePath')?.value || ''; config.MAX_ERROR_COUNT = parseInt(document.getElementById('maxErrorCount')?.value || 10); - config.POOL_SIZE_LIMIT = parseInt(document.getElementById('poolSizeLimit')?.value || 0); config.WARMUP_TARGET = parseInt(document.getElementById('warmupTarget')?.value || 0); config.REFRESH_CONCURRENCY_PER_PROVIDER = parseInt(document.getElementById('refreshConcurrencyPerProvider')?.value || 1); @@ -242,6 +260,16 @@ async function saveConfiguration() { } else { config.PROXY_ENABLED_PROVIDERS = []; } + + // 保存日志配置 + config.LOG_ENABLED = document.getElementById('logEnabled')?.checked !== false; + config.LOG_OUTPUT_MODE = document.getElementById('logOutputMode')?.value || 'all'; + config.LOG_LEVEL = document.getElementById('logLevel')?.value || 'info'; + config.LOG_DIR = document.getElementById('logDir')?.value || 'logs'; + config.LOG_INCLUDE_REQUEST_ID = document.getElementById('logIncludeRequestId')?.checked !== false; + config.LOG_INCLUDE_TIMESTAMP = document.getElementById('logIncludeTimestamp')?.checked !== false; + config.LOG_MAX_FILE_SIZE = parseInt(document.getElementById('logMaxFileSize')?.value || 10485760); + config.LOG_MAX_FILES = parseInt(document.getElementById('logMaxFiles')?.value || 10); try { await window.apiClient.post('/config', config); diff --git a/static/app/constants.js b/static/app/constants.js index eeb300d..3f08aa9 100644 --- a/static/app/constants.js +++ b/static/app/constants.js @@ -22,6 +22,7 @@ const elements = { get navItems() { return document.querySelectorAll('.nav-item'); }, get logsContainer() { return document.getElementById('logsContainer'); }, get clearLogsBtn() { return document.getElementById('clearLogs'); }, + get downloadLogsBtn() { return document.getElementById('downloadLogs'); }, get toggleAutoScrollBtn() { return document.getElementById('toggleAutoScroll'); }, get saveConfigBtn() { return document.getElementById('saveConfig'); }, get resetConfigBtn() { return document.getElementById('resetConfig'); }, diff --git a/static/app/event-handlers.js b/static/app/event-handlers.js index 1d572b8..3d5e240 100644 --- a/static/app/event-handlers.js +++ b/static/app/event-handlers.js @@ -26,6 +26,67 @@ function initEventListeners() { }); } + // 下载日志 + if (elements.downloadLogsBtn) { + elements.downloadLogsBtn.addEventListener('click', async () => { + try { + const token = window.authManager.getToken(); + if (!token) { + showToast(t('common.error'), '请先登录', 'error'); + return; + } + + // 使用带认证的方式下载文件 + const url = `${window.location.origin}/api/system/download-log`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.status === 401) { + showToast(t('common.error'), '认证失败,请重新登录', 'error'); + window.authManager.clearToken(); + window.location.href = '/login.html'; + return; + } + + if (!response.ok) { + const errorData = await response.json(); + showToast(t('common.error'), errorData.error?.message || '下载失败', 'error'); + return; + } + + // 获取文件名 + const contentDisposition = response.headers.get('Content-Disposition'); + let filename = 'app.log'; + if (contentDisposition) { + const matches = /filename="?([^"]+)"?/.exec(contentDisposition); + if (matches && matches[1]) { + filename = matches[1]; + } + } + + // 下载文件 + const blob = await response.blob(); + const downloadUrl = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = downloadUrl; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(downloadUrl); + + showToast(t('common.success'), '日志下载成功', 'success'); + } catch (error) { + console.error('下载日志失败:', error); + showToast(t('common.error'), '下载失败: ' + error.message, 'error'); + } + }); + } + // 自动滚动切换 if (elements.toggleAutoScrollBtn) { elements.toggleAutoScrollBtn.addEventListener('click', () => { diff --git a/static/app/event-stream.js b/static/app/event-stream.js index b436968..cbd969c 100644 --- a/static/app/event-stream.js +++ b/static/app/event-stream.js @@ -68,8 +68,6 @@ function addLogEntry(logData) { const levelClass = `log-level-${logData.level}`; logEntry.innerHTML = ` - [${time}] - [${logData.level.toUpperCase()}] ${escapeHtml(logData.message)} `; diff --git a/static/app/i18n.js b/static/app/i18n.js index 10baa93..1acb9d2 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -196,6 +196,9 @@ const translations = { 'config.hostPlaceholder': '例如: 127.0.0.1', 'config.port': '端口', 'config.portPlaceholder': '3000', + 'config.basic.title': '基础设置', + 'config.governance.title': '服务治理', + 'config.oauth.title': 'OAuth & 令牌', 'config.modelProvider': '模型提供商', 'config.modelProviderHelp': '勾选启动时初始化的模型提供商 (必须至少勾选一个)', 'config.modelProviderRequired': '必须至少勾选一个模型提供商', @@ -253,7 +256,7 @@ const translations = { 'config.advanced.promptLogMode.none': '无 (none)', 'config.advanced.promptLogMode.console': '控制台 (console)', 'config.advanced.promptLogMode.file': '文件 (file)', - 'config.advanced.maxRetries': '最大重试次数', + 'config.advanced.maxRetries': '提供商内最大重试次数', 'config.advanced.baseDelay': '重试基础延迟(毫秒)', 'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数', 'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次', @@ -293,6 +296,26 @@ const translations = { 'config.proxy.urlNote': '支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理', 'config.proxy.enabledProviders': '启用代理的提供商', 'config.proxy.enabledProvidersNote': '选择需要通过代理访问的提供商,未选中的提供商将直接连接', + 'config.log.title': '日志设置', + 'config.log.enabled': '启用日志', + 'config.log.outputMode': '日志输出模式', + 'config.log.outputMode.all': '全部 (控制台+文件)', + 'config.log.outputMode.console': '仅控制台', + 'config.log.outputMode.file': '仅文件', + 'config.log.outputMode.none': '禁用', + 'config.log.level': '日志级别', + 'config.log.level.debug': '调试 (debug)', + 'config.log.level.info': '信息 (info)', + 'config.log.level.warn': '警告 (warn)', + 'config.log.level.error': '错误 (error)', + 'config.log.dir': '日志目录', + 'config.log.dirPlaceholder': '例如: logs', + 'config.log.includeRequestId': '包含请求ID', + 'config.log.includeTimestamp': '包含时间戳', + 'config.log.maxFileSize': '最大文件大小(字节)', + 'config.log.maxFileSizeNote': '默认 10MB (10485760 字节)', + 'config.log.maxFiles': '最大保留文件数', + 'config.log.maxFilesNote': '保留最近的日志文件数量', 'config.save': '保存配置', 'config.reset': '重置', 'config.placeholder.nodeName': '例如: 我的节点1', @@ -513,6 +536,7 @@ const translations = { // Logs 'logs.title': '实时日志', 'logs.clear': '清空日志', + 'logs.download': '下载日志', 'logs.autoScroll': '自动滚动', 'logs.autoScroll.on': '自动滚动: 开', 'logs.autoScroll.off': '自动滚动: 关', @@ -631,9 +655,13 @@ const translations = { 'tutorial.main.prompt.file': '系统提示文件路径', 'tutorial.main.prompt.mode': '系统提示模式:overwrite(覆盖) 或 append(追加)', 'tutorial.main.retry.title': '重试配置', - 'tutorial.main.retry.max': '最大重试次数', + 'tutorial.main.retry.max': '提供商内最大重试次数', 'tutorial.main.retry.delay': '重试基础延迟(毫秒)', + 'tutorial.main.retry.credentialSwitch': '坏凭证切换最大重试次数', 'tutorial.main.retry.error': '提供商最大错误次数,超过后标记为不健康', + 'tutorial.main.governance.warmup': '系统启动时自动刷新的节点数量', + 'tutorial.main.governance.concurrency': '每个提供商内部最大并行刷新任务数', + 'tutorial.main.governance.poolLimit': '每个提供商类型参与轮询的最大健康凭证数量', 'tutorial.main.example.title': '配置示例', 'tutorial.pool.title': '提供商池配置 (provider_pools.json)', 'tutorial.pool.desc': '提供商池用于配置多个账号,实现负载均衡和故障转移。每个提供商类型可以配置多个账号节点。', @@ -903,6 +931,9 @@ const translations = { 'config.hostPlaceholder': 'e.g.: 127.0.0.1', 'config.port': 'Port', 'config.portPlaceholder': '3000', + 'config.basic.title': 'Basic Settings', + 'config.governance.title': 'Service Governance', + 'config.oauth.title': 'OAuth & Tokens', 'config.modelProvider': 'Model Provider', 'config.modelProviderHelp': 'Check model providers to initialize on startup (must select at least one)', 'config.modelProviderRequired': 'At least one model provider must be selected', @@ -960,7 +991,7 @@ const translations = { 'config.advanced.promptLogMode.none': 'None', 'config.advanced.promptLogMode.console': 'Console', 'config.advanced.promptLogMode.file': 'File', - 'config.advanced.maxRetries': 'Max Retries', + 'config.advanced.maxRetries': 'Provider Max Retries', 'config.advanced.baseDelay': 'Base Retry Delay (ms)', 'config.advanced.credentialSwitchMaxRetries': 'Credential Switch Max Retries', 'config.advanced.credentialSwitchMaxRetriesNote': 'Max retry count for switching credentials after auth errors (401/403), default 5', @@ -1000,6 +1031,26 @@ const translations = { 'config.proxy.urlNote': 'Supports HTTP, HTTPS and SOCKS5 proxies. Leave empty to disable proxy', 'config.proxy.enabledProviders': 'Providers Using Proxy', 'config.proxy.enabledProvidersNote': 'Select providers that should use the proxy. Unselected providers will connect directly', + 'config.log.title': 'Log Settings', + 'config.log.enabled': 'Enable Logging', + 'config.log.outputMode': 'Log Output Mode', + 'config.log.outputMode.all': 'All (Console + File)', + 'config.log.outputMode.console': 'Console Only', + 'config.log.outputMode.file': 'File Only', + 'config.log.outputMode.none': 'Disabled', + 'config.log.level': 'Log Level', + 'config.log.level.debug': 'Debug', + 'config.log.level.info': 'Info', + 'config.log.level.warn': 'Warning', + 'config.log.level.error': 'Error', + 'config.log.dir': 'Log Directory', + 'config.log.dirPlaceholder': 'e.g.: logs', + 'config.log.includeRequestId': 'Include Request ID', + 'config.log.includeTimestamp': 'Include Timestamp', + 'config.log.maxFileSize': 'Max File Size (bytes)', + 'config.log.maxFileSizeNote': 'Default 10MB (10485760 bytes)', + 'config.log.maxFiles': 'Max Files to Keep', + 'config.log.maxFilesNote': 'Number of recent log files to keep', 'config.save': 'Save Configuration', 'config.reset': 'Reset', 'config.placeholder.nodeName': 'e.g.: My Node 1', @@ -1220,6 +1271,7 @@ const translations = { // Logs 'logs.title': 'Real-time Logs', 'logs.clear': 'Clear Logs', + 'logs.download': 'Download Logs', 'logs.autoScroll': 'Auto Scroll', 'logs.autoScroll.on': 'Auto Scroll: On', 'logs.autoScroll.off': 'Auto Scroll: Off', @@ -1338,9 +1390,13 @@ const translations = { 'tutorial.main.prompt.file': 'System prompt file path', 'tutorial.main.prompt.mode': 'System prompt mode: overwrite or append', 'tutorial.main.retry.title': 'Retry Configuration', - 'tutorial.main.retry.max': 'Maximum retry count', + 'tutorial.main.retry.max': 'Provider max retry count', 'tutorial.main.retry.delay': 'Base retry delay (milliseconds)', + 'tutorial.main.retry.credentialSwitch': 'Max retries for switching bad credentials', 'tutorial.main.retry.error': 'Max provider error count before marking unhealthy', + 'tutorial.main.governance.warmup': 'Number of nodes to auto-refresh on startup', + 'tutorial.main.governance.concurrency': 'Maximum parallel refresh tasks per provider', + 'tutorial.main.governance.poolLimit': 'Maximum number of healthy credentials per provider type for rotation', 'tutorial.main.example.title': 'Configuration Example', 'tutorial.pool.title': 'Provider Pool Config (provider_pools.json)', 'tutorial.pool.desc': 'Provider pool configures multiple accounts for load balancing and failover. Each provider type can have multiple account nodes.', diff --git a/static/app/navigation.js b/static/app/navigation.js index 2a3b3f9..142b80a 100644 --- a/static/app/navigation.js +++ b/static/app/navigation.js @@ -25,6 +25,16 @@ function initNavigation() { section.classList.remove('active'); if (section.id === sectionId) { section.classList.add('active'); + + // 如果是日志页面,默认滚动到底部 + if (sectionId === 'logs') { + setTimeout(() => { + const logsContainer = document.getElementById('logsContainer'); + if (logsContainer) { + logsContainer.scrollTop = logsContainer.scrollHeight; + } + }, 100); + } } }); @@ -52,6 +62,16 @@ function switchToSection(sectionId) { section.classList.remove('active'); if (section.id === sectionId) { section.classList.add('active'); + + // 如果是日志页面,默认滚动到底部 + if (sectionId === 'logs') { + setTimeout(() => { + const logsContainer = document.getElementById('logsContainer'); + if (logsContainer) { + logsContainer.scrollTop = logsContainer.scrollHeight; + } + }, 100); + } } }); diff --git a/static/components/section-config.css b/static/components/section-config.css index cd63a37..26abaf3 100644 --- a/static/components/section-config.css +++ b/static/components/section-config.css @@ -369,15 +369,59 @@ input:checked + .toggle-slider:before { } +/* 组合配置区块样式 */ +.config-group-section { + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: 1.5rem; + margin-bottom: 2rem; + background: var(--bg-primary); + transition: var(--transition); +} + +.config-group-section:hover { + box-shadow: var(--shadow-md); + border-color: var(--primary-color); +} + +.config-group-section h3 { + color: var(--text-primary); + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 1.5rem; + padding-bottom: 0.75rem; + border-bottom: 2px solid var(--bg-tertiary); + display: flex; + align-items: center; + gap: 0.75rem; +} + +.config-group-section h3 i { + color: var(--primary-color); + width: 1.25rem; + text-align: center; +} + +.config-group-section hr { + border: none; + border-top: 1px solid var(--border-color); + margin: 1.5rem 0; + opacity: 0.5; +} + /* 响应式调整 */ @media (max-width: 768px) { - .oauth-refresh-toggle { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; + .form-row, .config-row { + grid-template-columns: 1fr; + gap: 0; } - .restart-required-modal .restart-modal-content { - max-width: 95%; + + .config-panel { + padding: 1rem; + } + + .config-group-section { + padding: 1rem; } } diff --git a/static/components/section-config.html b/static/components/section-config.html index cc831c2..77fdeb9 100644 --- a/static/components/section-config.html +++ b/static/components/section-config.html @@ -4,128 +4,275 @@

配置管理

-
- -
- - + +
+

基础设置

+
+ +
+ + +
-
-
-
- - -
-
- - -
-
-
- -
- - - - - - - - - -
- 点击选择启动时初始化的模型提供商 (必须至少选择一个) -
- - -
-

高级配置

- - -
-

代理设置

+
- - - 支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理 + +
-
- -
- - - - - - - - - -
- 点击选择需要通过代理访问的提供商,未选中的提供商将直接连接 +
+ +
- +
+ +
+ + + + + + + + + +
+ 点击选择启动时初始化的模型提供商 (必须至少选择一个) +
+
+ + +
+

代理设置

+
+ + + 支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理 +
+
+ +
+ + + + + + + + + +
+ 点击选择需要通过代理访问的提供商,未选中的提供商将直接连接 +
+
+ + +
+

服务治理

+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+

OAuth & 令牌

+
+
+ + +
+
+ + +
+
+
+ + +
+

日志设置

+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+

系统与高级

+
+ + +
+
- +
@@ -135,121 +282,22 @@
- -
-
- - -
-
- - -
+
+ +
- -
-
- - -
-
- - -
-
- -
-
- - - 认证错误(401/403)后切换凭证的最大重试次数,默认 5 次 -
-
- -
-
- - - 系统启动时自动刷新的节点数量,默认为 0 -
-
- - - 每个提供商内部最大并行刷新任务数,默认为 1 -
-
- -
-
- - -
-
- - -
-
- -
- - - 使用默认路径配置需添加一个空节点 -
- -
- - - 提供商连续错误达到此次数后将被标记为不健康,默认为 10 次 -
- -
- - - 当某一 Provider Type 所有账号都不健康时,自动切换到配置的 Fallback 类型。JSON 格式,键为主类型,值为 Fallback 类型数组(按优先级排序) -
- -
- - - 当主 Provider 不可用时,根据模型名映射到其他协议的 Provider 和模型。JSON 格式。 -
- - -
- - -
- - -
+
- -
- 用于保护管理控制台的访问,修改后需要重新登录 + 修改后需要重新登录
- +
+
- \ No newline at end of file + diff --git a/static/components/section-logs.html b/static/components/section-logs.html index f9b2de1..b8bb791 100644 --- a/static/components/section-logs.html +++ b/static/components/section-logs.html @@ -6,6 +6,9 @@ + diff --git a/static/components/section-tutorial.html b/static/components/section-tutorial.html index 0c7e190..3cc77f5 100644 --- a/static/components/section-tutorial.html +++ b/static/components/section-tutorial.html @@ -50,8 +50,9 @@

主配置详解 (config.json)

+
-

基础配置

+

基础设置

@@ -92,38 +93,37 @@ +
-

系统提示配置

+

代理设置

- - + - - + - - - - + + +
参数 类型默认值 说明
SYSTEM_PROMPT_FILE_PATHPROXY_URL string-系统提示文件路径代理地址,支持 HTTP、HTTPS、SOCKS5
SYSTEM_PROMPT_MODEstringoverwrite系统提示模式:overwrite(覆盖) 或 append(追加)PROXY_ENABLED_PROVIDERSarray启用代理的提供商列表
+
-

重试配置

+

服务治理

@@ -147,12 +147,63 @@ + + + + + + + + + + + + + + + + + + + +
1000 重试基础延迟(毫秒)
CREDENTIAL_SWITCH_MAX_RETRIESnumber5坏凭证切换最大重试次数
MAX_ERROR_COUNT number 10 提供商最大错误次数,超过后标记为不健康
WARMUP_TARGETnumber0系统启动时自动刷新的节点数量
REFRESH_CONCURRENCY_PER_PROVIDERnumber1提供商内刷新并发数
+
+
+ + +
+

日志设置

+
+ + + + + + + + + + + + + + + + + + + + + + + +
参数类型说明
LOG_ENABLEDboolean启用日志
LOG_OUTPUT_MODEstring日志输出模式 (all/console/file/none)
PROMPT_LOG_MODEstring提示词日志模式:none(关闭)、console(控制台)、file(文件)
@@ -164,13 +215,14 @@ "REQUIRED_API_KEY": "your-api-key", "SERVER_PORT": 3000, "HOST": "0.0.0.0", - "MODEL_PROVIDER": "gemini-cli-oauth", - "SYSTEM_PROMPT_FILE_PATH": "configs/input_system_prompt.txt", - "SYSTEM_PROMPT_MODE": "overwrite", + "MODEL_PROVIDER": "gemini-cli-oauth,claude-kiro-oauth", + "PROXY_URL": "http://127.0.0.1:7890", + "PROXY_ENABLED_PROVIDERS": ["gemini-cli-oauth", "claude-kiro-oauth"], "REQUEST_MAX_RETRIES": 3, - "REQUEST_BASE_DELAY": 1000, "MAX_ERROR_COUNT": 10, - "PROVIDER_POOLS_FILE_PATH": "configs/provider_pools.json" + "WARMUP_TARGET": 5, + "LOG_ENABLED": true, + "LOG_OUTPUT_MODE": "all" }
@@ -244,13 +296,6 @@ "oauthCredsFilePath": "configs/gemini/oauth_creds_1.json", "checkHealth": true, "checkModel": "gemini-2.0-flash-exp" - }, - { - "uuid": "gemini-account-2", - "name": "Gemini 账号 2", - "oauthCredsFilePath": "configs/gemini/oauth_creds_2.json", - "checkHealth": true, - "notSupportedModels": ["gemini-3.0-pro"] } ], "claude-kiro-oauth": [ @@ -258,8 +303,7 @@ "uuid": "kiro-account-1", "name": "Kiro 账号 1", "oauthCredsFilePath": "configs/kiro/kiro-auth-token.json", - "checkHealth": true, - "checkModel": "claude-sonnet-4-5" + "checkHealth": true } ] } @@ -294,10 +338,6 @@ "gemini-claude-opus-4-5-thinking": { "targetProviderType": "claude-kiro-oauth", "targetModel": "claude-opus-4-5" - }, - "claude-opus-4-5": { - "targetProviderType": "gemini-antigravity", - "targetModel": "gemini-claude-opus-4-5-thinking" } } } @@ -305,91 +345,54 @@
- +
-

代理配置

+

OAuth 授权路径

-

支持为特定提供商配置代理,用于网络受限环境。

- -
-

代理配置参数

-
- - - - - - - - - - - - - - - - - - - - -
参数类型说明
PROXY_URLstring代理地址,支持 HTTP、HTTPS、SOCKS5
PROXY_ENABLED_PROVIDERSarray启用代理的提供商列表
-
-
- -
-

配置示例

-
{
-    "PROXY_URL": "http://127.0.0.1:7890",
-    "PROXY_ENABLED_PROVIDERS": [
-        "gemini-cli-oauth",
-        "gemini-antigravity"
-    ]
-}
-
- -
- - 支持的代理类型:HTTP (http://)、HTTPS (https://)、SOCKS5 (socks5://) -
-
-
- - -
-

OAuth 授权配置

-
-

各提供商的 OAuth 凭据文件默认存储位置:

+

各提供商的 OAuth 凭据文件默认存储位置(建议保持在 configs/ 目录下以便统一管理):

- - Gemini + + Gemini CLI OAuth
- ~/.gemini/oauth_creds.json + configs/gemini/
- Antigravity + Gemini Antigravity
- ~/.antigravity/oauth_creds.json + configs/antigravity/
- - Kiro + + Claude Kiro OAuth
- ~/.aws/sso/cache/kiro-auth-token.json + configs/kiro/ +
+
+
+ + Qwen OAuth +
+ configs/qwen/ +
+
+
+ + iFlow OAuth +
+ configs/iflow/
- Qwen + OpenAI Codex OAuth
- ~/.qwen/oauth_creds.json + configs/codex/
@@ -399,45 +402,4 @@
- - -
-

日志配置

-
-
-

提示日志配置

-
- - - - - - - - - - - - - - - - - - - - -
参数类型说明
PROMPT_LOG_MODEstring日志模式:none(关闭)、console(控制台)、file(文件)
PROMPT_LOG_BASE_NAMEstring日志文件基础名称
-
-
- -
-

配置示例

-
{
-    "PROMPT_LOG_MODE": "file",
-    "PROMPT_LOG_BASE_NAME": "prompt_log"
-}
-
-
-
- \ No newline at end of file +