feat(logging): 添加日志系统配置和下载功能

- 新增日志系统配置选项,支持日志级别、输出模式、文件大小等设置
- 添加当日日志文件下载功能,可通过Web界面直接下载
- 将console.log/error替换为结构化logger,提升日志可管理性
- 在日志页面添加自动滚动到底部功能
- 更新配置示例文件,包含完整的日志配置参数
This commit is contained in:
hex2077 2026-01-25 17:24:39 +08:00
parent e8353e0e2e
commit 245583b96a
73 changed files with 2168 additions and 1436 deletions

View file

@ -43,7 +43,15 @@
}, },
"PROXY_URL": "http://127.0.0.1:1089", "PROXY_URL": "http://127.0.0.1:1089",
"PROXY_ENABLED_PROVIDERS": [ "PROXY_ENABLED_PROVIDERS": [
"gemini-cli-oauth", "gemini-cli-oauth",
"gemini-antigravity" "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
}

View file

@ -80,7 +80,7 @@ echo [成功] 项目文件检查完成
:: 启动应用程序 :: 启动应用程序
echo. echo.
echo ======================================== echo ========================================
echo 启动AI Client 2 API服务器... echo 启动AIClient2API服务器...
echo ======================================== echo ========================================
echo. echo.
echo 服务器将在 http://localhost:3000 启动 echo 服务器将在 http://localhost:3000 启动

View file

@ -86,7 +86,7 @@ echo "[成功] 项目文件检查完成"
# 启动应用程序 # 启动应用程序
echo echo
echo "========================================" echo "========================================"
echo " 启动AI Client 2 API服务器..." echo " 启动AIClient2API服务器..."
echo "========================================" echo "========================================"
echo echo
echo "服务器将在 http://localhost:3000 启动" echo "服务器将在 http://localhost:3000 启动"

View file

@ -1,4 +1,5 @@
import http from 'http'; import http from 'http';
import logger from '../utils/logger.js';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import crypto from 'crypto'; import crypto from 'crypto';
@ -36,7 +37,7 @@ class CodexAuth {
if (proxyConfig) { if (proxyConfig) {
axiosConfig.httpAgent = proxyConfig.httpAgent; axiosConfig.httpAgent = proxyConfig.httpAgent;
axiosConfig.httpsAgent = proxyConfig.httpsAgent; 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); this.httpClient = axios.create(axiosConfig);
@ -68,16 +69,16 @@ class CodexAuth {
const pkce = this.generatePKCECodes(); const pkce = this.generatePKCECodes();
const state = crypto.randomBytes(16).toString('hex'); 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) { if (this.server) {
console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Closing existing callback server...`); logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Closing existing callback server...`);
try { try {
this.server.close(); this.server.close();
this.server = null; this.server = null;
} catch (error) { } 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 credPath = saveResult.credsPath;
const relativePath = saveResult.relativePath; const relativePath = saveResult.relativePath;
console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`); logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`);
console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`); logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`);
console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`); logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`);
// 关闭服务器 // 关闭服务器
if (this.server) { if (this.server) {
@ -168,7 +169,7 @@ class CodexAuth {
const pkce = this.generatePKCECodes(); const pkce = this.generatePKCECodes();
const state = crypto.randomBytes(16).toString('hex'); 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(); const server = await this.startCallbackServer();
@ -186,13 +187,13 @@ class CodexAuth {
authUrl.searchParams.set('id_token_add_organizations', 'true'); authUrl.searchParams.set('id_token_add_organizations', 'true');
authUrl.searchParams.set('codex_cli_simplified_flow', 'true'); authUrl.searchParams.set('codex_cli_simplified_flow', 'true');
console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Opening browser for authentication...`); logger.info(`${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} If browser doesn't open, visit: ${authUrl.toString()}`);
try { try {
await open(authUrl.toString()); await open(authUrl.toString());
} catch (error) { } 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); await this.saveCredentials(credentials);
console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`); logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`);
console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`); logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`);
console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`); logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`);
return credentials; return credentials;
} }
@ -302,7 +303,7 @@ class CodexAuth {
}); });
server.listen(CODEX_OAUTH_CONFIG.port, () => { 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); resolve(server);
}); });
@ -355,7 +356,7 @@ class CodexAuth {
* @returns {Promise<Object>} * @returns {Promise<Object>}
*/ */
async exchangeCodeForTokens(code, codeVerifier) { 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 { try {
const response = await this.httpClient.post( const response = await this.httpClient.post(
@ -377,7 +378,7 @@ class CodexAuth {
return response.data; return response.data;
} catch (error) { } 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}`); throw new Error(`Failed to exchange code for tokens: ${error.response?.data?.error_description || error.message}`);
} }
} }
@ -388,7 +389,7 @@ class CodexAuth {
* @returns {Promise<Object>} * @returns {Promise<Object>}
*/ */
async refreshTokens(refreshToken) { async refreshTokens(refreshToken) {
console.log(`${CODEX_OAUTH_CONFIG.logPrefix} Refreshing access token...`); logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Refreshing access token...`);
try { try {
const response = await this.httpClient.post( const response = await this.httpClient.post(
@ -420,7 +421,7 @@ class CodexAuth {
expired: new Date(Date.now() + (tokens.expires_in || 3600) * 1000).toISOString() expired: new Date(Date.now() + (tokens.expires_in || 3600) * 1000).toISOString()
}; };
} catch (error) { } 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}`); 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'); const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
return JSON.parse(payload); return JSON.parse(payload);
} catch (error) { } 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}`); 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 }); await fs.promises.writeFile(credsPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
const relativePath = path.relative(process.cwd(), credsPath); 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 }; return { credsPath, relativePath };
} catch (error) { } 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}`); 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); return await auth.refreshTokens(refreshToken);
} catch (error) { } catch (error) {
lastError = 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) { if (i < maxRetries - 1) {
// 指数退避 // 指数退避
@ -606,11 +607,11 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
const auth = new CodexAuth(currentConfig); const auth = new CodexAuth(currentConfig);
try { try {
console.log('[Codex Auth] Generating OAuth URL...'); logger.info('[Codex Auth] Generating OAuth URL...');
// 清理所有旧的会话和服务器 // 清理所有旧的会话和服务器
if (global.codexOAuthSessions && global.codexOAuthSessions.size > 0) { 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()) { for (const [sessionId, session] of global.codexOAuthSessions.entries()) {
try { try {
// 清理定时器 // 清理定时器
@ -623,7 +624,7 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
} }
global.codexOAuthSessions.delete(sessionId); global.codexOAuthSessions.delete(sessionId);
} catch (error) { } 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 和启动回调服务器 // 生成授权 URL 和启动回调服务器
const { authUrl, state, pkce, server } = await auth.generateAuthUrl(); 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 会话信息,供后续回调使用 // 存储 OAuth 会话信息,供后续回调使用
if (!global.codexOAuthSessions) { if (!global.codexOAuthSessions) {
@ -663,13 +664,13 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
pollTimer = setInterval(() => { pollTimer = setInterval(() => {
pollCount++; pollCount++;
if (pollCount <= maxPollCount && !isCompleted) { 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) { if (pollCount >= maxPollCount && !isCompleted) {
clearInterval(pollTimer); clearInterval(pollTimer);
const totalSeconds = (maxPollCount * pollInterval) / 1000; 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)) { if (global.codexOAuthSessions.has(sessionId)) {
@ -693,11 +694,11 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
} }
try { 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); const session = global.codexOAuthSessions.get(sessionId);
if (!session) { if (!session) {
console.error('[Codex Auth] Session not found'); logger.error('[Codex Auth] Session not found');
return; return;
} }
@ -720,9 +721,9 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG);
console.log('[Codex Auth] OAuth flow completed successfully'); logger.info('[Codex Auth] OAuth flow completed successfully');
} catch (error) { } 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', { broadcastEvent('oauth_error', {
@ -739,7 +740,7 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
clearInterval(pollTimer); clearInterval(pollTimer);
} }
console.error('[Codex Auth] Auth error:', error.message); logger.error('[Codex Auth] Auth error:', error.message);
global.codexOAuthSessions.delete(sessionId); global.codexOAuthSessions.delete(sessionId);
broadcastEvent('oauth_error', { broadcastEvent('oauth_error', {
@ -767,7 +768,7 @@ export async function handleCodexOAuth(currentConfig, options = {}) {
} }
}; };
} catch (error) { } 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 { return {
success: false, success: false,
@ -801,7 +802,7 @@ export async function handleCodexOAuthCallback(code, state) {
const session = global.codexOAuthSessions.get(state); const session = global.codexOAuthSessions.get(state);
const { auth, state: expectedState, pkce } = session; const { auth, state: expectedState, pkce } = session;
console.log('[Codex Auth] Processing OAuth callback...'); logger.info('[Codex Auth] Processing OAuth callback...');
// 完成 OAuth 流程 // 完成 OAuth 流程
const result = await auth.completeOAuthFlow(code, state, expectedState, pkce); const result = await auth.completeOAuthFlow(code, state, expectedState, pkce);
@ -822,7 +823,7 @@ export async function handleCodexOAuthCallback(code, state) {
// 自动关联新生成的凭据到 Pools // 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG);
console.log('[Codex Auth] OAuth callback processed successfully'); logger.info('[Codex Auth] OAuth callback processed successfully');
return { return {
success: true, success: true,
@ -834,7 +835,7 @@ export async function handleCodexOAuthCallback(code, state) {
relativePath: result.relativePath relativePath: result.relativePath
}; };
} catch (error) { } catch (error) {
console.error('[Codex Auth] OAuth callback failed:', error.message); logger.error('[Codex Auth] OAuth callback failed:', error.message);
// 广播认证失败事件 // 广播认证失败事件
broadcastEvent({ broadcastEvent({

View file

@ -1,4 +1,5 @@
import { OAuth2Client } from 'google-auth-library'; import { OAuth2Client } from 'google-auth-library';
import logger from '../utils/logger.js';
import http from 'http'; import http from 'http';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
@ -74,7 +75,7 @@ async function closeActiveServer(provider, port = null) {
await new Promise((resolve) => { await new Promise((resolve) => {
existing.server.close(() => { existing.server.close(() => {
activeServers.delete(provider); activeServers.delete(provider);
console.log(`[OAuth] 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); logger.info(`[OAuth] 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
resolve(); resolve();
}); });
}); });
@ -87,7 +88,7 @@ async function closeActiveServer(provider, port = null) {
await new Promise((resolve) => { await new Promise((resolve) => {
info.server.close(() => { info.server.close(() => {
activeServers.delete(p); activeServers.delete(p);
console.log(`[OAuth] 已关闭端口 ${port} 上被占用(提供商: ${p})的旧服务器`); logger.info(`[OAuth] 已关闭端口 ${port} 上被占用(提供商: ${p})的旧服务器`);
resolve(); resolve();
}); });
}); });
@ -118,7 +119,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
const errorParam = url.searchParams.get('error'); const errorParam = url.searchParams.get('error');
if (code) { if (code) {
console.log(`${config.logPrefix} 收到来自 Google 的成功回调: ${req.url}`); logger.info(`${config.logPrefix} 收到来自 Google 的成功回调: ${req.url}`);
try { try {
const { tokens } = await authClient.getToken(code); 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.mkdir(path.dirname(finalCredPath), { recursive: true });
await fs.promises.writeFile(finalCredPath, JSON.stringify(tokens, null, 2)); 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); 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.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, '您可以关闭此页面')); res.end(generateResponsePage(true, '您可以关闭此页面'));
} catch (tokenError) { } catch (tokenError) {
console.error(`${config.logPrefix} 获取令牌失败:`, tokenError); logger.error(`${config.logPrefix} 获取令牌失败:`, tokenError);
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`)); res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`));
} finally { } finally {
@ -164,7 +165,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
} }
} else if (errorParam) { } else if (errorParam) {
const errorMessage = `授权失败。Google 返回错误: ${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.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, errorMessage)); res.end(generateResponsePage(false, errorMessage));
@ -172,12 +173,12 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
activeServers.delete(provider); activeServers.delete(provider);
}); });
} else { } else {
console.log(`${config.logPrefix} 忽略无关请求: ${req.url}`); logger.info(`${config.logPrefix} 忽略无关请求: ${req.url}`);
res.writeHead(204); res.writeHead(204);
res.end(); res.end();
} }
} catch (error) { } catch (error) {
console.error(`${config.logPrefix} 处理回调时出错:`, error); logger.error(`${config.logPrefix} 处理回调时出错:`, error);
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `服务器错误: ${error.message}`)); res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
@ -191,17 +192,17 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
server.on('error', (err) => { server.on('error', (err) => {
if (err.code === 'EADDRINUSE') { if (err.code === 'EADDRINUSE') {
console.error(`${config.logPrefix} 端口 ${port} 已被占用`); logger.error(`${config.logPrefix} 端口 ${port} 已被占用`);
reject(new Error(`端口 ${port} 已被占用`)); reject(new Error(`端口 ${port} 已被占用`));
} else { } else {
console.error(`${config.logPrefix} 服务器错误:`, err); logger.error(`${config.logPrefix} 服务器错误:`, err);
reject(err); reject(err);
} }
}); });
const host = '0.0.0.0'; const host = '0.0.0.0';
server.listen(port, host, () => { server.listen(port, host, () => {
console.log(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`); logger.info(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
activeServers.set(provider, { server, port }); activeServers.set(provider, { server, port });
resolve(server); resolve(server);
}); });
@ -236,7 +237,7 @@ async function handleGoogleOAuth(providerKey, currentConfig, options = {}) {
if (proxyConfig) { if (proxyConfig) {
oauth2Options.transporterOptions = 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); const authClient = new OAuth2Client(oauth2Options);

View file

@ -1,4 +1,5 @@
import http from 'http'; import http from 'http';
import logger from '../utils/logger.js';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import os from 'os'; import os from 'os';
@ -67,7 +68,7 @@ async function fetchWithProxy(url, options = {}, providerType) {
axiosConfig.httpAgent = proxyConfig.httpAgent; axiosConfig.httpAgent = proxyConfig.httpAgent;
axiosConfig.httpsAgent = proxyConfig.httpsAgent; axiosConfig.httpsAgent = proxyConfig.httpsAgent;
axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent 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 { try {
@ -255,7 +256,7 @@ async function closeIFlowServer(provider, port = null) {
await new Promise((resolve) => { await new Promise((resolve) => {
existing.server.close(() => { existing.server.close(() => {
activeIFlowServers.delete(provider); activeIFlowServers.delete(provider);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
resolve(); resolve();
}); });
}); });
@ -267,7 +268,7 @@ async function closeIFlowServer(provider, port = null) {
await new Promise((resolve) => { await new Promise((resolve) => {
info.server.close(() => { info.server.close(() => {
activeIFlowServers.delete(p); activeIFlowServers.delete(p);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`);
resolve(); resolve();
}); });
}); });
@ -296,7 +297,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = {
const errorParam = url.searchParams.get('error'); const errorParam = url.searchParams.get('error');
if (errorParam) { 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.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `授权失败: ${errorParam}`)); res.end(generateResponsePage(false, `授权失败: ${errorParam}`));
server.close(() => { server.close(() => {
@ -306,7 +307,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = {
} }
if (state !== expectedState) { 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.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, 'State 验证失败')); res.end(generateResponsePage(false, 'State 验证失败'));
server.close(() => { server.close(() => {
@ -316,7 +317,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = {
} }
if (!code) { 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.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, '缺少授权码')); res.end(generateResponsePage(false, '缺少授权码'));
server.close(() => { server.close(() => {
@ -325,16 +326,16 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = {
return; return;
} }
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 收到授权回调,正在交换令牌...`); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 收到授权回调,正在交换令牌...`);
try { try {
// 1. 交换授权码获取令牌 // 1. 交换授权码获取令牌
const tokenData = await exchangeIFlowCodeForTokens(code, redirectUri); const tokenData = await exchangeIFlowCodeForTokens(code, redirectUri);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌交换成功`); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌交换成功`);
// 2. 获取用户信息(包含 API Key // 2. 获取用户信息(包含 API Key
const userInfo = await fetchIFlowUserInfo(tokenData.accessToken); const userInfo = await fetchIFlowUserInfo(tokenData.accessToken);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 用户信息获取成功: ${userInfo.email}`); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 用户信息获取成功: ${userInfo.email}`);
// 3. 组合完整的凭据数据 // 3. 组合完整的凭据数据
const credentialsData = { const credentialsData = {
@ -360,7 +361,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = {
await fs.promises.mkdir(path.dirname(credPath), { recursive: true }); await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2)); 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); const relativePath = path.relative(process.cwd(), credPath);
@ -380,7 +381,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = {
res.end(generateResponsePage(true, `授权成功!账户: ${userInfo.email},您可以关闭此页面`)); res.end(generateResponsePage(true, `授权成功!账户: ${userInfo.email},您可以关闭此页面`));
} catch (tokenError) { } 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.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `令牌处理失败: ${tokenError.message}`)); res.end(generateResponsePage(false, `令牌处理失败: ${tokenError.message}`));
} finally { } finally {
@ -394,7 +395,7 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = {
res.end(); res.end();
} }
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `服务器错误: ${error.message}`)); res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
@ -408,24 +409,24 @@ function createIFlowCallbackServer(port, redirectUri, expectedState, options = {
server.on('error', (err) => { server.on('error', (err) => {
if (err.code === 'EADDRINUSE') { if (err.code === 'EADDRINUSE') {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 端口 ${port} 已被占用`); logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 端口 ${port} 已被占用`);
reject(new Error(`端口 ${port} 已被占用`)); reject(new Error(`端口 ${port} 已被占用`));
} else { } else {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 服务器错误:`, err); logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 服务器错误:`, err);
reject(err); reject(err);
} }
}); });
const host = '0.0.0.0'; const host = '0.0.0.0';
server.listen(port, host, () => { server.listen(port, host, () => {
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
resolve(server); resolve(server);
}); });
// 10 分钟超时自动关闭 // 10 分钟超时自动关闭
setTimeout(() => { setTimeout(() => {
if (server.listening) { if (server.listening) {
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 回调服务器超时,自动关闭`); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 回调服务器超时,自动关闭`);
server.close(() => { server.close(() => {
activeIFlowServers.delete('openai-iflow'); activeIFlowServers.delete('openai-iflow');
}); });
@ -453,7 +454,7 @@ export async function handleIFlowOAuth(currentConfig, options = {}) {
// 生成授权链接 // 生成授权链接
const { authUrl, redirectUri } = generateIFlowAuthorizationURL(state, port); const { authUrl, redirectUri } = generateIFlowAuthorizationURL(state, port);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 生成授权链接: ${authUrl}`); logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 生成授权链接: ${authUrl}`);
// 关闭之前可能存在的服务器 // 关闭之前可能存在的服务器
await closeIFlowServer(providerKey, port); await closeIFlowServer(providerKey, port);

View file

@ -1,4 +1,5 @@
import http from 'http'; import http from 'http';
import logger from '../utils/logger.js';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import crypto from 'crypto'; import crypto from 'crypto';
@ -85,7 +86,7 @@ async function fetchWithProxy(url, options = {}, providerType) {
axiosConfig.httpAgent = proxyConfig.httpAgent; axiosConfig.httpAgent = proxyConfig.httpAgent;
axiosConfig.httpsAgent = proxyConfig.httpsAgent; axiosConfig.httpsAgent = proxyConfig.httpsAgent;
axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent 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 { try {
@ -174,7 +175,7 @@ function generateCodeChallenge(codeVerifier) {
export async function handleKiroOAuth(currentConfig, options = {}) { export async function handleKiroOAuth(currentConfig, options = {}) {
const method = options.method || options.authMethod || 'google'; // 默认使用 Google同时支持 authMethod 参数 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) { switch (method) {
case 'google': case 'google':
@ -249,7 +250,7 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) {
// 获取 Builder ID Start URL优先使用前端传入的值否则使用默认值 // 获取 Builder ID Start URL优先使用前端传入的值否则使用默认值
const builderIDStartURL = options.builderIDStartURL || KIRO_OAUTH_CONFIG.builderIDStartURL; 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 客户端 // 1. 注册 OIDC 客户端
const region = options.region || 'us-east-1'; const region = options.region || 'us-east-1';
@ -308,7 +309,7 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) {
taskId, taskId,
{ ...options, region } { ...options, region }
).catch(error => { ).catch(error => {
console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error); logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error);
broadcastEvent('oauth_error', { broadcastEvent('oauth_error', {
provider: 'claude-kiro-oauth', provider: 'claude-kiro-oauth',
error: error.message, error: error.message,
@ -343,7 +344,7 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv
const taskControl = { shouldStop: false }; const taskControl = { shouldStop: false };
activeKiroPollingTasks.set(taskId, taskControl); activeKiroPollingTasks.set(taskId, taskControl);
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}]`); logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}]`);
const poll = async () => { const poll = async () => {
if (taskControl.shouldStop) { if (taskControl.shouldStop) {
@ -377,7 +378,7 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv
const data = await response.json(); const data = await response.json();
if (response.ok && data.accessToken) { if (response.ok && data.accessToken) {
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`); logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`);
// 保存令牌(符合现有规范) // 保存令牌(符合现有规范)
if (options.saveToConfigs) { if (options.saveToConfigs) {
@ -419,7 +420,7 @@ async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interv
// 检查错误类型 // 检查错误类型
if (data.error === 'authorization_pending') { 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)); await new Promise(resolve => setTimeout(resolve, interval * 1000));
return poll(); return poll();
} else if (data.error === 'slow_down') { } else if (data.error === 'slow_down') {
@ -449,7 +450,7 @@ function stopKiroPollingTask(taskId) {
if (task) { if (task) {
task.shouldStop = true; task.shouldStop = true;
activeKiroPollingTasks.delete(taskId); 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 { try {
const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options); const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options);
activeKiroServers.set('claude-kiro-oauth', { server, port }); activeKiroServers.set('claude-kiro-oauth', { server, port });
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`); logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`);
return port; return port;
} catch (err) { } 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) => { await new Promise((resolve) => {
existing.server.close(() => { existing.server.close(() => {
activeKiroServers.delete(provider); activeKiroServers.delete(provider);
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
resolve(); resolve();
}); });
}); });
@ -498,7 +499,7 @@ async function closeKiroServer(provider, port = null) {
await new Promise((resolve) => { await new Promise((resolve) => {
info.server.close(() => { info.server.close(() => {
activeKiroServers.delete(p); activeKiroServers.delete(p);
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`);
resolve(); resolve();
}); });
}); });
@ -551,7 +552,7 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options
if (!tokenResponse.ok) { if (!tokenResponse.ok) {
const errorText = await tokenResponse.text(); 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.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `获取令牌失败: ${tokenResponse.status}`)); res.end(generateResponsePage(false, `获取令牌失败: ${tokenResponse.status}`));
return; return;
@ -582,7 +583,7 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options
await fs.promises.mkdir(path.dirname(credPath), { recursive: true }); await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
await fs.promises.writeFile(credPath, JSON.stringify(saveData, null, 2)); 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', { broadcastEvent('oauth_success', {
@ -608,7 +609,7 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options
res.end(); res.end();
} }
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `服务器错误: ${error.message}`)); res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
} }
@ -733,7 +734,7 @@ export async function checkKiroCredentialsDuplicate(refreshToken, provider = 'cl
// 检查 refreshToken 是否匹配 // 检查 refreshToken 是否匹配
if (credentials.refreshToken && credentials.refreshToken === refreshToken) { if (credentials.refreshToken && credentials.refreshToken === refreshToken) {
const relativePath = path.relative(process.cwd(), fullPath); 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 { return {
isDuplicate: true, isDuplicate: true,
existingPath: relativePath existingPath: relativePath
@ -751,7 +752,7 @@ export async function checkKiroCredentialsDuplicate(refreshToken, provider = 'cl
return await scanDirectory(kiroDir); return await scanDirectory(kiroDir);
} catch (error) { } 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 }; return { isDuplicate: false };
} }
} }
@ -800,7 +801,7 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_
} }
try { 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); const tokenData = await refreshKiroToken(refreshToken, region);
@ -815,7 +816,7 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_
const relativePath = path.relative(process.cwd(), credPath); 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({ results.details.push({
index: i + 1, index: i + 1,
@ -826,7 +827,7 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_
results.success++; results.success++;
} catch (error) { } 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({ results.details.push({
index: i + 1, index: i + 1,
@ -922,7 +923,7 @@ export async function batchImportKiroRefreshTokensStream(refreshTokens, region =
} }
try { 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); const tokenData = await refreshKiroToken(refreshToken, region);
@ -937,7 +938,7 @@ export async function batchImportKiroRefreshTokensStream(refreshTokens, region =
const relativePath = path.relative(process.cwd(), credPath); 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 = { progressData.current = {
index: i + 1, index: i + 1,
@ -949,7 +950,7 @@ export async function batchImportKiroRefreshTokensStream(refreshTokens, region =
results.success++; results.success++;
} catch (error) { } 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 = { progressData.current = {
index: i + 1, 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 = { const credentialsData = {
@ -1046,7 +1047,7 @@ export async function importAwsCredentials(credentials, skipDuplicateCheck = fal
// 尝试刷新获取最新的 accessToken // 尝试刷新获取最新的 accessToken
try { 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 refreshRegion = credentials.idcRegion || KIRO_REFRESH_CONSTANTS.IDC_REGION;
const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_IDC_URL.replace('{{region}}', refreshRegion); 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; credentialsData.refreshToken = tokenData.refreshToken;
const expiresIn = tokenData.expiresIn || 3600; const expiresIn = tokenData.expiresIn || 3600;
credentialsData.expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); 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 { } 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) { } 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); 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', { broadcastEvent('oauth_success', {
@ -1108,10 +1109,12 @@ export async function importAwsCredentials(credentials, skipDuplicateCheck = fal
}; };
} catch (error) { } 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 { return {
success: false, success: false,
error: error.message error: error.message
}; };
} }
} }

View file

@ -1,4 +1,5 @@
import fs from 'fs'; import fs from 'fs';
import logger from '../utils/logger.js';
import path from 'path'; import path from 'path';
import os from 'os'; import os from 'os';
import crypto from 'crypto'; import crypto from 'crypto';
@ -55,7 +56,7 @@ async function fetchWithProxy(url, options = {}, providerType) {
axiosConfig.httpAgent = proxyConfig.httpAgent; axiosConfig.httpAgent = proxyConfig.httpAgent;
axiosConfig.httpsAgent = proxyConfig.httpsAgent; axiosConfig.httpsAgent = proxyConfig.httpsAgent;
axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent 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 { try {
@ -117,7 +118,7 @@ function stopPollingTask(taskId) {
if (task) { if (task) {
task.shouldStop = true; task.shouldStop = true;
activePollingTasks.delete(taskId); 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 }; const taskControl = { shouldStop: false };
activePollingTasks.set(taskId, taskControl); 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 () => { const poll = async () => {
// 检查是否需要停止 // 检查是否需要停止
if (taskControl.shouldStop) { if (taskControl.shouldStop) {
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询任务 [${taskId}] 已被停止`); logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询任务 [${taskId}] 已被停止`);
throw new Error('轮询任务已被取消'); throw new Error('轮询任务已被取消');
} }
@ -181,7 +182,7 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn =
if (response.ok && data.access_token) { if (response.ok && data.access_token) {
// 成功获取令牌 // 成功获取令牌
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`); logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`);
// 如果指定了保存到 configs 目录 // 如果指定了保存到 configs 目录
if (options.saveToConfigs) { 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.mkdir(path.dirname(credPath), { recursive: true });
await fs.promises.writeFile(credPath, JSON.stringify(data, null, 2)); 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); const relativePath = path.relative(process.cwd(), credPath);
@ -219,12 +220,12 @@ async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn =
// 检查错误类型 // 检查错误类型
if (data.error === 'authorization_pending') { 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)); await new Promise(resolve => setTimeout(resolve, interval * 1000));
return poll(); return poll();
} else if (data.error === 'slow_down') { } 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)); await new Promise(resolve => setTimeout(resolve, (interval + 5) * 1000));
return poll(); return poll();
} else if (data.error === 'expired_token') { } 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('拒绝')) { if (error.message.includes('授权') || error.message.includes('过期') || error.message.includes('拒绝')) {
throw error; throw error;
} }
console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询出错:`, error); logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询出错:`, error);
// 网络错误,继续重试 // 网络错误,继续重试
await new Promise(resolve => setTimeout(resolve, interval * 1000)); await new Promise(resolve => setTimeout(resolve, interval * 1000));
return poll(); return poll();
@ -310,7 +311,7 @@ export async function handleQwenOAuth(currentConfig, options = {}) {
// 不等待轮询完成,立即返回授权信息 // 不等待轮询完成,立即返回授权信息
pollQwenToken(deviceAuth.device_code, codeVerifier, interval, expiresIn, taskId, options) pollQwenToken(deviceAuth.device_code, codeVerifier, interval, expiresIn, taskId, options)
.catch(error => { .catch(error => {
console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error); logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error);
// 广播授权失败事件 // 广播授权失败事件
broadcastEvent('oauth_error', { broadcastEvent('oauth_error', {
provider: 'openai-qwen-oauth', provider: 'openai-qwen-oauth',
@ -333,7 +334,7 @@ export async function handleQwenOAuth(currentConfig, options = {}) {
} }
}; };
} catch (error) { } catch (error) {
console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 请求失败:`, error); logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 请求失败:`, error);
throw new Error(`Qwen OAuth 授权失败: ${error.message}`); throw new Error(`Qwen OAuth 授权失败: ${error.message}`);
} }
} }

View file

@ -1,4 +1,5 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import logger from '../utils/logger.js';
import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js'; import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js';
import { import {
streamStateManager, streamStateManager,
@ -113,7 +114,7 @@ function _cleanJsonSchemaProperties(schema) {
function _determineReasoningEffortFromBudget(budgetTokens) { function _determineReasoningEffortFromBudget(budgetTokens) {
// 如果没有提供budget_tokens默认为high // 如果没有提供budget_tokens默认为high
if (budgetTokens === null || budgetTokens === undefined) { 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"; return "high";
} }
@ -121,7 +122,7 @@ function _determineReasoningEffortFromBudget(budgetTokens) {
const LOW_THRESHOLD = 50; // 低推理努力的阈值 const LOW_THRESHOLD = 50; // 低推理努力的阈值
const HIGH_THRESHOLD = 200; // 高推理努力的阈值 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; let effort;
if (budgetTokens <= LOW_THRESHOLD) { if (budgetTokens <= LOW_THRESHOLD) {
@ -132,7 +133,7 @@ function _determineReasoningEffortFromBudget(budgetTokens) {
effort = "high"; 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; 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}`); 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') { if (type === 'response' || type === 'streamChunk' || type === 'modelList') {
return conversionFunction(data, model); return conversionFunction(data, model);
} else { } else {
@ -856,16 +857,16 @@ export function toOpenAIRequestFromClaude(claudeRequest) {
if (claudeRequest.max_tokens !== undefined) { if (claudeRequest.max_tokens !== undefined) {
maxCompletionTokens = claudeRequest.max_tokens; maxCompletionTokens = claudeRequest.max_tokens;
delete openaiRequest.max_tokens; // 移除max_tokens使用max_completion_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 { } else {
// 优先级2环境变量OPENAI_REASONING_MAX_TOKENS // 优先级2环境变量OPENAI_REASONING_MAX_TOKENS
const envMaxTokens = process.env.OPENAI_REASONING_MAX_TOKENS; const envMaxTokens = process.env.OPENAI_REASONING_MAX_TOKENS;
if (envMaxTokens) { if (envMaxTokens) {
try { try {
maxCompletionTokens = parseInt(envMaxTokens, 10); 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) { } 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; 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) { 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 => { functionDeclarations: openaiRequest.tools.map(t => {
// Ensure tool is a valid object and has function property // Ensure tool is a valid object and has function property
if (!t || typeof t !== 'object' || !t.function) { 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 return null; // Return null for invalid tools, filter out later
} }
@ -1057,7 +1058,7 @@ export function toGeminiRequestFromOpenAI(openaiRequest) {
// Validation // Validation
if (geminiRequest.contents[0]?.role !== 'user') { 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; return geminiRequest;
@ -1618,7 +1619,7 @@ export function extractTextFromMessageContent(content) {
export function toGeminiRequestFromClaude(claudeRequest) { export function toGeminiRequestFromClaude(claudeRequest) {
// Ensure claudeRequest is a valid object // Ensure claudeRequest is a valid object
if (!claudeRequest || typeof claudeRequest !== 'object') { if (!claudeRequest || typeof claudeRequest !== 'object') {
console.warn("Invalid claudeRequest provided to toGeminiRequestFromClaude."); logger.warn("Invalid claudeRequest provided to toGeminiRequestFromClaude.");
return { contents: [] }; return { contents: [] };
} }
@ -1654,7 +1655,7 @@ export function toGeminiRequestFromClaude(claudeRequest) {
claudeRequest.messages.forEach(message => { claudeRequest.messages.forEach(message => {
// Ensure message is a valid object and has a role and content // Ensure message is a valid object and has a role and content
if (!message || typeof message !== 'object' || !message.role || !message.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; return;
} }
@ -1698,13 +1699,13 @@ export function toGeminiRequestFromClaude(claudeRequest) {
functionDeclarations: claudeRequest.tools.map(tool => { functionDeclarations: claudeRequest.tools.map(tool => {
// Ensure tool is a valid object and has a name // Ensure tool is a valid object and has a name
if (!tool || typeof tool !== 'object' || !tool.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 return null; // Return null for invalid tools, filter out later
} }
// Filter out TodoWrite tool // Filter out TodoWrite tool
// if (tool.name === 'TodoWrite') { // if (tool.name === 'TodoWrite') {
// console.log("Filtering out TodoWrite tool"); // logger.info("Filtering out TodoWrite tool");
// return null; // return null;
// } // }
@ -1737,7 +1738,7 @@ export function toGeminiRequestFromClaude(claudeRequest) {
*/ */
function buildGeminiToolConfigFromClaude(claudeToolChoice) { function buildGeminiToolConfigFromClaude(claudeToolChoice) {
if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) { if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) {
console.warn("Invalid claudeToolChoice provided to buildGeminiToolConfigFromClaude."); logger.warn("Invalid claudeToolChoice provided to buildGeminiToolConfigFromClaude.");
return undefined; return undefined;
} }
@ -1750,10 +1751,10 @@ function buildGeminiToolConfigFromClaude(claudeToolChoice) {
if (claudeToolChoice.name && typeof claudeToolChoice.name === 'string') { if (claudeToolChoice.name && typeof claudeToolChoice.name === 'string') {
return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [claudeToolChoice.name] } }; 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; return undefined;
default: default:
console.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`); logger.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`);
return undefined; return undefined;
} }
} }
@ -1778,7 +1779,7 @@ function processClaudeContentToGeminiParts(content) {
content.forEach(block => { content.forEach(block => {
// Ensure block is a valid object and has a type // Ensure block is a valid object and has a type
if (!block || typeof block !== 'object' || !block.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; return;
} }
@ -1787,7 +1788,7 @@ function processClaudeContentToGeminiParts(content) {
if (typeof block.text === 'string') { if (typeof block.text === 'string') {
parts.push({ text: block.text }); parts.push({ text: block.text });
} else { } else {
console.warn("Invalid text content in Claude text block."); logger.warn("Invalid text content in Claude text block.");
} }
break; break;
@ -1801,7 +1802,7 @@ function processClaudeContentToGeminiParts(content) {
} }
}); });
} else { } else {
console.warn("Invalid image source in Claude image block."); logger.warn("Invalid image source in Claude image block.");
} }
break; break;
@ -1809,7 +1810,7 @@ function processClaudeContentToGeminiParts(content) {
if (typeof block.name === 'string' && block.input && typeof block.input === 'object') { if (typeof block.name === 'string' && block.input && typeof block.input === 'object') {
// Filter out TodoWrite tool use // Filter out TodoWrite tool use
// if (block.name === 'TodoWrite') { // 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 // break; // Skip adding this tool to parts
// } // }
parts.push({ parts.push({
@ -1819,7 +1820,7 @@ function processClaudeContentToGeminiParts(content) {
} }
}); });
} else { } else {
console.warn("Invalid tool_use block in Claude content."); logger.warn("Invalid tool_use block in Claude content.");
} }
break; break;
@ -1837,7 +1838,7 @@ function processClaudeContentToGeminiParts(content) {
} }
}); });
} else { } 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; break;
@ -1846,7 +1847,7 @@ function processClaudeContentToGeminiParts(content) {
if (typeof block.text === 'string') { if (typeof block.text === 'string') {
parts.push({ text: block.text }); parts.push({ text: block.text });
} else { } 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) { for (const candidate of geminiResponse.candidates) {
// 检查完成原因是否为错误类型 // 检查完成原因是否为错误类型
if (candidate.finishReason && candidate.finishReason !== 'STOP') { if (candidate.finishReason && candidate.finishReason !== 'STOP') {
// console.log('Gemini response finishReason:', JSON.stringify(candidate)); // logger.info('Gemini response finishReason:', JSON.stringify(candidate));
// console.warn('Gemini response contains malformed function call:', candidate.finishMessage || 'No finish message'); // logger.warn('Gemini response contains malformed function call:', candidate.finishMessage || 'No finish message');
// 将错误信息作为文本内容返回 // 将错误信息作为文本内容返回
if (candidate.finishMessage) { if (candidate.finishMessage) {
@ -1946,7 +1947,7 @@ function processGeminiResponseToClaudeContent(geminiResponse) {
text: `Error: ${candidate.finishMessage}` text: `Error: ${candidate.finishMessage}`
}); });
} }
// console.log("Processed content:", content); // logger.info("Processed content:", content);
continue; // 跳过当前候选的进一步处理 continue; // 跳过当前候选的进一步处理
} }
@ -2568,4 +2569,6 @@ export function getOpenAIResponsesStreamChunkEnd(id){
generateOutputItemDone(id), generateOutputItemDone(id),
generateResponseCompleted(id) generateResponseCompleted(id)
]; ];
} }

View file

@ -7,6 +7,7 @@
*/ */
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import logger from '../utils/logger.js';
import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js'; import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js';
import { ConverterFactory } from '../converters/ConverterFactory.js'; import { ConverterFactory } from '../converters/ConverterFactory.js';
import { import {
@ -46,7 +47,7 @@ export function convertData(data, type, fromProvider, toProvider, model) {
// 如果目标协议为 forward直接返回原始数据无需转换 // 如果目标协议为 forward直接返回原始数据无需转换
if (toProtocol === MODEL_PROTOCOL_PREFIX.FORWARD || fromProtocol === MODEL_PROTOCOL_PREFIX.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; return data;
} }
@ -75,7 +76,7 @@ export function convertData(data, type, fromProvider, toProvider, model) {
throw new Error(`Unsupported conversion type: ${type}`); throw new Error(`Unsupported conversion type: ${type}`);
} }
} catch (error) { } catch (error) {
console.error(`Conversion error: ${error.message}`); logger.error(`Conversion error: ${error.message}`);
throw error; throw error;
} }
} }
@ -386,4 +387,6 @@ export default {
toOpenAIStreamChunkFromOpenAIResponses, toOpenAIStreamChunkFromOpenAIResponses,
toClaudeRequestFromOpenAIResponses, toClaudeRequestFromOpenAIResponses,
toGeminiRequestFromOpenAIResponses, toGeminiRequestFromOpenAIResponses,
}; };

View file

@ -4,6 +4,7 @@
*/ */
import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js'; 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) { static createProcessor(sourceFormat, targetFormat) {
// 这里返回null实际使用时需要导入具体的处理器类 // 这里返回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; return null;
} }
@ -166,7 +167,7 @@ export class ToolProcessorFactory {
* @returns {ToolProcessor} 工具处理器实例 * @returns {ToolProcessor} 工具处理器实例
*/ */
static createProcessor(sourceFormat, targetFormat) { 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; return null;
} }

View file

@ -4,6 +4,7 @@
*/ */
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import logger from '../../utils/logger.js';
import { BaseConverter } from '../BaseConverter.js'; import { BaseConverter } from '../BaseConverter.js';
import { import {
checkAndAssignOrDefault, checkAndAssignOrDefault,
@ -249,7 +250,7 @@ export class ClaudeConverter extends BaseConverter {
try { try {
maxCompletionTokens = parseInt(envMaxTokens, 10); maxCompletionTokens = parseInt(envMaxTokens, 10);
} catch (e) { } catch (e) {
console.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}'`); logger.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}'`);
} }
} }
if (!envMaxTokens) { if (!envMaxTokens) {
@ -754,7 +755,7 @@ export class ClaudeConverter extends BaseConverter {
*/ */
toGeminiRequest(claudeRequest) { toGeminiRequest(claudeRequest) {
if (!claudeRequest || typeof claudeRequest !== 'object') { if (!claudeRequest || typeof claudeRequest !== 'object') {
console.warn("Invalid claudeRequest provided to toGeminiRequest."); logger.warn("Invalid claudeRequest provided to toGeminiRequest.");
return { contents: [] }; return { contents: [] };
} }
@ -795,7 +796,7 @@ export class ClaudeConverter extends BaseConverter {
if (Array.isArray(claudeRequest.messages)) { if (Array.isArray(claudeRequest.messages)) {
claudeRequest.messages.forEach(message => { claudeRequest.messages.forEach(message => {
if (!message || typeof message !== 'object' || !message.role) { if (!message || typeof message !== 'object' || !message.role) {
console.warn("Skipping invalid message in claudeRequest.messages."); logger.warn("Skipping invalid message in claudeRequest.messages.");
return; return;
} }
@ -992,7 +993,7 @@ export class ClaudeConverter extends BaseConverter {
claudeRequest.tools.forEach(tool => { claudeRequest.tools.forEach(tool => {
if (!tool || typeof tool !== 'object' || !tool.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; return;
} }
@ -1334,7 +1335,7 @@ export class ClaudeConverter extends BaseConverter {
content.forEach(block => { content.forEach(block => {
if (!block || typeof block !== 'object' || !block.type) { if (!block || typeof block !== 'object' || !block.type) {
console.warn("Skipping invalid content block."); logger.warn("Skipping invalid content block.");
return; return;
} }
@ -1400,7 +1401,7 @@ export class ClaudeConverter extends BaseConverter {
*/ */
buildGeminiToolConfigFromClaude(claudeToolChoice) { buildGeminiToolConfigFromClaude(claudeToolChoice) {
if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) { if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) {
console.warn("Invalid claudeToolChoice provided."); logger.warn("Invalid claudeToolChoice provided.");
return undefined; 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; return undefined;
default: default:
console.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`); logger.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`);
return undefined; return undefined;
} }
} }

View file

@ -4,6 +4,7 @@
*/ */
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import logger from '../../utils/logger.js';
import { BaseConverter } from '../BaseConverter.js'; import { BaseConverter } from '../BaseConverter.js';
import { CodexConverter } from './CodexConverter.js'; import { CodexConverter } from './CodexConverter.js';
import { import {
@ -1172,10 +1173,10 @@ export class OpenAIConverter extends BaseConverter {
// But this parameter cannot be added when using tools (causes 400 error) // But this parameter cannot be added when using tools (causes 400 error)
const hasTools = tools && Array.isArray(tools) && tools.length > 0; 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'))) { 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"]; config.responseModalities = ["TEXT"];
} else if (hasTools && model && (model.includes('2.5') || model.includes('thinking') || model.includes('2.0-flash-thinking'))) { } 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; return config;

View file

@ -4,6 +4,7 @@
*/ */
import { v4 as uuidv4 } from 'uuid'; 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) { export function determineReasoningEffortFromBudget(budgetTokens) {
if (budgetTokens === null || budgetTokens === undefined) { 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"; return "high";
} }
const LOW_THRESHOLD = 50; const LOW_THRESHOLD = 50;
const HIGH_THRESHOLD = 200; 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; let effort;
if (budgetTokens <= LOW_THRESHOLD) { if (budgetTokens <= LOW_THRESHOLD) {
@ -345,7 +346,7 @@ export function determineReasoningEffortFromBudget(budgetTokens) {
effort = "high"; 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; return effort;
} }

View file

@ -1,6 +1,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { promises as pfs } from 'fs'; import { promises as pfs } from 'fs';
import { INPUT_SYSTEM_PROMPT_FILE, MODEL_PROVIDER } from '../utils/common.js'; 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 CONFIG = {}; // Make CONFIG exportable
export let PROMPT_LOG_FILENAME = ''; // Make PROMPT_LOG_FILENAME 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()); const matched = ALL_MODEL_PROVIDERS.find((provider) => provider.toLowerCase() === trimmed.toLowerCase());
if (!matched) { 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; return;
} }
if (!dedupedProviders.includes(matched)) { if (!dedupedProviders.includes(matched)) {
@ -58,9 +59,9 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
try { try {
const configData = fs.readFileSync(configFilePath, 'utf8'); const configData = fs.readFileSync(configFilePath, 'utf8');
currentConfig = JSON.parse(configData); currentConfig = JSON.parse(configData);
console.log('[Config] Loaded configuration from configs/config.json'); logger.info('[Config] Loaded configuration from configs/config.json');
} catch (error) { } 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 // Fallback to default values if config.json is not found or invalid
currentConfig = { currentConfig = {
REQUIRED_API_KEY: "123456", REQUIRED_API_KEY: "123456",
@ -80,9 +81,17 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
CRON_REFRESH_TOKEN: false, CRON_REFRESH_TOKEN: false,
PROVIDER_POOLS_FILE_PATH: null, // 新增号池配置文件路径 PROVIDER_POOLS_FILE_PATH: null, // 新增号池配置文件路径
MAX_ERROR_COUNT: 10, // 提供商最大错误次数 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 // 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]; currentConfig.REQUIRED_API_KEY = args[i + 1];
i++; i++;
} else { } 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') { } else if (args[i] === '--log-prompts') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
@ -100,32 +109,32 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
if (mode === 'console' || mode === 'file') { if (mode === 'console' || mode === 'file') {
currentConfig.PROMPT_LOG_MODE = mode; currentConfig.PROMPT_LOG_MODE = mode;
} else { } 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++; i++;
} else { } 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') { } else if (args[i] === '--port') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
currentConfig.SERVER_PORT = parseInt(args[i + 1], 10); currentConfig.SERVER_PORT = parseInt(args[i + 1], 10);
i++; i++;
} else { } 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') { } else if (args[i] === '--model-provider') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
currentConfig.MODEL_PROVIDER = args[i + 1]; currentConfig.MODEL_PROVIDER = args[i + 1];
i++; i++;
} else { } 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') { } else if (args[i] === '--system-prompt-file') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
currentConfig.SYSTEM_PROMPT_FILE_PATH = args[i + 1]; currentConfig.SYSTEM_PROMPT_FILE_PATH = args[i + 1];
i++; i++;
} else { } 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') { } else if (args[i] === '--system-prompt-mode') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
@ -133,53 +142,53 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
if (mode === 'overwrite' || mode === 'append') { if (mode === 'overwrite' || mode === 'append') {
currentConfig.SYSTEM_PROMPT_MODE = mode; currentConfig.SYSTEM_PROMPT_MODE = mode;
} else { } 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++; i++;
} else { } 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') { } else if (args[i] === '--host') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
currentConfig.HOST = args[i + 1]; currentConfig.HOST = args[i + 1];
i++; i++;
} else { } 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') { } else if (args[i] === '--prompt-log-base-name') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
currentConfig.PROMPT_LOG_BASE_NAME = args[i + 1]; currentConfig.PROMPT_LOG_BASE_NAME = args[i + 1];
i++; i++;
} else { } 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') { } else if (args[i] === '--cron-near-minutes') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
currentConfig.CRON_NEAR_MINUTES = parseInt(args[i + 1], 10); currentConfig.CRON_NEAR_MINUTES = parseInt(args[i + 1], 10);
i++; i++;
} else { } 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') { } else if (args[i] === '--cron-refresh-token') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
currentConfig.CRON_REFRESH_TOKEN = args[i + 1].toLowerCase() === 'true'; currentConfig.CRON_REFRESH_TOKEN = args[i + 1].toLowerCase() === 'true';
i++; i++;
} else { } 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') { } else if (args[i] === '--provider-pools-file') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
currentConfig.PROVIDER_POOLS_FILE_PATH = args[i + 1]; currentConfig.PROVIDER_POOLS_FILE_PATH = args[i + 1];
i++; i++;
} else { } 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') { } else if (args[i] === '--max-error-count') {
if (i + 1 < args.length) { if (i + 1 < args.length) {
currentConfig.MAX_ERROR_COUNT = parseInt(args[i + 1], 10); currentConfig.MAX_ERROR_COUNT = parseInt(args[i + 1], 10);
i++; i++;
} else { } 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 { try {
const poolsData = await pfs.readFile(currentConfig.PROVIDER_POOLS_FILE_PATH, 'utf8'); const poolsData = await pfs.readFile(currentConfig.PROVIDER_POOLS_FILE_PATH, 'utf8');
currentConfig.providerPools = JSON.parse(poolsData); 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) { } 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 = {}; currentConfig.providerPools = {};
} }
} else { } else {
@ -220,6 +229,22 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
// Assign to the exported CONFIG // Assign to the exported CONFIG
Object.assign(CONFIG, currentConfig); 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; return CONFIG;
} }
@ -233,9 +258,9 @@ export async function getSystemPromptFileContent(filePath) {
await pfs.access(filePath, pfs.constants.F_OK); await pfs.access(filePath, pfs.constants.F_OK);
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { 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 { } 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; return null;
} }
@ -245,10 +270,10 @@ export async function getSystemPromptFileContent(filePath) {
if (!content.trim()) { if (!content.trim()) {
return null; return null;
} }
console.log(`[System Prompt] Loaded system prompt from ${filePath}`); logger.info(`[System Prompt] Loaded system prompt from ${filePath}`);
return content; return content;
} catch (error) { } 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; return null;
} }
} }

View file

@ -12,6 +12,7 @@
*/ */
import { fork } from 'child_process'; import { fork } from 'child_process';
import logger from '../utils/logger.js';
import * as http from 'http'; import * as http from 'http';
import * as path from 'path'; import * as path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@ -45,13 +46,13 @@ const config = {
*/ */
function startWorker() { function startWorker() {
if (workerProcess) { if (workerProcess) {
console.log('[Master] Worker process already running, PID:', workerProcess.pid); logger.info('[Master] Worker process already running, PID:', workerProcess.pid);
return; return;
} }
console.log('[Master] Starting worker process...'); logger.info('[Master] Starting worker process...');
console.log('[Master] Worker script:', config.workerScript); logger.info('[Master] Worker script:', config.workerScript);
console.log('[Master] Worker args:', config.args.join(' ')); logger.info('[Master] Worker args:', config.args.join(' '));
workerProcess = fork(config.workerScript, config.args, { workerProcess = fork(config.workerScript, config.args, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'], stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
@ -64,30 +65,30 @@ function startWorker() {
workerStatus.pid = workerProcess.pid; workerStatus.pid = workerProcess.pid;
workerStatus.startTime = new Date().toISOString(); 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) => { workerProcess.on('message', (message) => {
console.log('[Master] Received message from worker:', message); logger.info('[Master] Received message from worker:', message);
handleWorkerMessage(message); handleWorkerMessage(message);
}); });
// 监听子进程退出 // 监听子进程退出
workerProcess.on('exit', (code, signal) => { 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; workerProcess = null;
workerStatus.pid = null; workerStatus.pid = null;
// 如果不是主动重启导致的退出,尝试自动重启 // 如果不是主动重启导致的退出,尝试自动重启
if (!workerStatus.isRestarting && code !== 0) { if (!workerStatus.isRestarting && code !== 0) {
console.log('[Master] Worker crashed, attempting auto-restart...'); logger.info('[Master] Worker crashed, attempting auto-restart...');
scheduleRestart(); scheduleRestart();
} }
}); });
// 监听子进程错误 // 监听子进程错误
workerProcess.on('error', (error) => { 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) { function stopWorker(graceful = true) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!workerProcess) { if (!workerProcess) {
console.log('[Master] No worker process to stop'); logger.info('[Master] No worker process to stop');
resolve(); resolve();
return; return;
} }
console.log('[Master] Stopping worker process, PID:', workerProcess.pid); logger.info('[Master] Stopping worker process, PID:', workerProcess.pid);
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (workerProcess) { if (workerProcess) {
console.log('[Master] Force killing worker process...'); logger.info('[Master] Force killing worker process...');
workerProcess.kill('SIGKILL'); workerProcess.kill('SIGKILL');
} }
resolve(); resolve();
@ -118,7 +119,7 @@ function stopWorker(graceful = true) {
clearTimeout(timeout); clearTimeout(timeout);
workerProcess = null; workerProcess = null;
workerStatus.pid = null; workerStatus.pid = null;
console.log('[Master] Worker process stopped'); logger.info('[Master] Worker process stopped');
resolve(); resolve();
}); });
@ -138,7 +139,7 @@ function stopWorker(graceful = true) {
*/ */
async function restartWorker() { async function restartWorker() {
if (workerStatus.isRestarting) { 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' }; return { success: false, message: 'Restart already in progress' };
} }
@ -146,7 +147,7 @@ async function restartWorker() {
workerStatus.restartCount++; workerStatus.restartCount++;
workerStatus.lastRestartTime = new Date().toISOString(); workerStatus.lastRestartTime = new Date().toISOString();
console.log('[Master] Restarting worker process...'); logger.info('[Master] Restarting worker process...');
try { try {
await stopWorker(true); await stopWorker(true);
@ -165,7 +166,7 @@ async function restartWorker() {
}; };
} catch (error) { } catch (error) {
workerStatus.isRestarting = false; workerStatus.isRestarting = false;
console.error('[Master] Failed to restart worker:', error.message); logger.error('[Master] Failed to restart worker:', error.message);
return { return {
success: false, success: false,
message: 'Failed to restart worker: ' + error.message message: 'Failed to restart worker: ' + error.message
@ -178,12 +179,12 @@ async function restartWorker() {
*/ */
function scheduleRestart() { function scheduleRestart() {
if (workerStatus.restartCount >= config.maxRestartAttempts) { if (workerStatus.restartCount >= config.maxRestartAttempts) {
console.error('[Master] Max restart attempts reached, giving up'); logger.error('[Master] Max restart attempts reached, giving up');
return; return;
} }
const delay = Math.min(config.restartDelay * Math.pow(2, workerStatus.restartCount), 30000); 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(() => { setTimeout(() => {
restartWorker(); restartWorker();
@ -199,17 +200,17 @@ function handleWorkerMessage(message) {
switch (message.type) { switch (message.type) {
case 'ready': case 'ready':
console.log('[Master] Worker is ready'); logger.info('[Master] Worker is ready');
break; break;
case 'restart_request': case 'restart_request':
console.log('[Master] Worker requested restart'); logger.info('[Master] Worker requested restart');
restartWorker(); restartWorker();
break; break;
case 'status': case 'status':
console.log('[Master] Worker status:', message.data); logger.info('[Master] Worker status:', message.data);
break; break;
default: 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') { if (method === 'POST' && path === '/master/restart') {
console.log('[Master] Restart requested via API'); logger.info('[Master] Restart requested via API');
const result = await restartWorker(); const result = await restartWorker();
res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' }); res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result)); res.end(JSON.stringify(result));
@ -273,7 +274,7 @@ function createMasterServer() {
// 停止端点 // 停止端点
if (method === 'POST' && path === '/master/stop') { if (method === 'POST' && path === '/master/stop') {
console.log('[Master] Stop requested via API'); logger.info('[Master] Stop requested via API');
await stopWorker(true); await stopWorker(true);
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, message: 'Worker stopped' })); res.end(JSON.stringify({ success: true, message: 'Worker stopped' }));
@ -282,7 +283,7 @@ function createMasterServer() {
// 启动端点 // 启动端点
if (method === 'POST' && path === '/master/start') { if (method === 'POST' && path === '/master/start') {
console.log('[Master] Start requested via API'); logger.info('[Master] Start requested via API');
if (workerProcess) { if (workerProcess) {
res.writeHead(400, { 'Content-Type': 'application/json' }); res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'Worker already running' })); res.end(JSON.stringify({ success: false, message: 'Worker already running' }));
@ -311,13 +312,13 @@ function createMasterServer() {
}); });
server.listen(config.masterPort, () => { server.listen(config.masterPort, () => {
console.log(`[Master] Management server listening on port ${config.masterPort}`); logger.info(`[Master] Management server listening on port ${config.masterPort}`);
console.log(`[Master] Available endpoints:`); logger.info(`[Master] Available endpoints:`);
console.log(` GET /master/status - Get master and worker status`); logger.info(` GET /master/status - Get master and worker status`);
console.log(` GET /master/health - Health check`); logger.info(` GET /master/health - Health check`);
console.log(` POST /master/restart - Restart worker process`); logger.info(` POST /master/restart - Restart worker process`);
console.log(` POST /master/stop - Stop worker process`); logger.info(` POST /master/stop - Stop worker process`);
console.log(` POST /master/start - Start worker process`); logger.info(` POST /master/start - Start worker process`);
}); });
return server; return server;
@ -329,24 +330,24 @@ function createMasterServer() {
function setupSignalHandlers() { function setupSignalHandlers() {
// 优雅关闭 // 优雅关闭
process.on('SIGTERM', async () => { process.on('SIGTERM', async () => {
console.log('[Master] Received SIGTERM, shutting down...'); logger.info('[Master] Received SIGTERM, shutting down...');
await stopWorker(true); await stopWorker(true);
process.exit(0); process.exit(0);
}); });
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
console.log('[Master] Received SIGINT, shutting down...'); logger.info('[Master] Received SIGINT, shutting down...');
await stopWorker(true); await stopWorker(true);
process.exit(0); process.exit(0);
}); });
// 未捕获的异常 // 未捕获的异常
process.on('uncaughtException', (error) => { process.on('uncaughtException', (error) => {
console.error('[Master] Uncaught exception:', error); logger.error('[Master] Uncaught exception:', error);
}); });
process.on('unhandledRejection', (reason, promise) => { 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() { async function main() {
console.log('='.repeat(50)); logger.info('='.repeat(50));
console.log('[Master] AIClient2API Master Process'); logger.info('[Master] AIClient2API Master Process');
console.log('[Master] PID:', process.pid); logger.info('[Master] PID:', process.pid);
console.log('[Master] Node version:', process.version); logger.info('[Master] Node version:', process.version);
console.log('[Master] Working directory:', process.cwd()); logger.info('[Master] Working directory:', process.cwd());
console.log('='.repeat(50)); logger.info('='.repeat(50));
// 设置信号处理 // 设置信号处理
setupSignalHandlers(); setupSignalHandlers();
@ -373,6 +374,6 @@ async function main() {
// 启动主进程 // 启动主进程
main().catch(error => { main().catch(error => {
console.error('[Master] Failed to start:', error); logger.error('[Master] Failed to start:', error);
process.exit(1); process.exit(1);
}); });

View file

@ -9,6 +9,7 @@
*/ */
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import logger from '../utils/logger.js';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import path from 'path'; import path from 'path';
@ -72,7 +73,7 @@ class PluginManager {
await this.saveConfig(); await this.saveConfig();
} }
} catch (error) { } catch (error) {
console.error('[PluginManager] Failed to load config:', error.message); logger.error('[PluginManager] Failed to load config:', error.message);
this.pluginsConfig = { plugins: {} }; this.pluginsConfig = { plugins: {} };
} }
} }
@ -108,7 +109,7 @@ class PluginManager {
enabled: true, enabled: true,
description: plugin.description || '' 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) { } catch (importError) {
// 如果导入失败,使用目录名作为插件名 // 如果导入失败,使用目录名作为插件名
@ -116,11 +117,11 @@ class PluginManager {
enabled: true, enabled: true,
description: '' 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) { } catch (error) {
console.error('[PluginManager] Failed to scan plugins directory:', error.message); logger.error('[PluginManager] Failed to scan plugins directory:', error.message);
} }
return defaultConfig; return defaultConfig;
@ -137,7 +138,7 @@ class PluginManager {
} }
await fs.writeFile(PLUGINS_CONFIG_FILE, JSON.stringify(this.pluginsConfig, null, 2), 'utf8'); await fs.writeFile(PLUGINS_CONFIG_FILE, JSON.stringify(this.pluginsConfig, null, 2), 'utf8');
} catch (error) { } 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'); throw new Error('Plugin must have a name');
} }
if (this.plugins.has(plugin.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; return;
} }
this.plugins.set(plugin.name, plugin); 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; // 默认启用 const enabled = pluginConfig.enabled !== false; // 默认启用
if (!enabled) { if (!enabled) {
console.log(`[PluginManager] Plugin "${name}" is disabled, skipping init`); logger.info(`[PluginManager] Plugin "${name}" is disabled, skipping init`);
continue; continue;
} }
try { try {
if (typeof plugin.init === 'function') { if (typeof plugin.init === 'function') {
await plugin.init(config); await plugin.init(config);
console.log(`[PluginManager] Initialized plugin: ${name}`); logger.info(`[PluginManager] Initialized plugin: ${name}`);
} }
plugin._enabled = true; plugin._enabled = true;
} catch (error) { } 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; plugin._enabled = false;
} }
} }
@ -198,10 +199,10 @@ class PluginManager {
try { try {
if (typeof plugin.destroy === 'function') { if (typeof plugin.destroy === 'function') {
await plugin.destroy(); await plugin.destroy();
console.log(`[PluginManager] Destroyed plugin: ${name}`); logger.info(`[PluginManager] Destroyed plugin: ${name}`);
} }
} catch (error) { } 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; this.initialized = false;
@ -303,7 +304,7 @@ class PluginManager {
// authorized === null 表示此插件不处理,继续下一个 // authorized === null 表示此插件不处理,继续下一个
} catch (error) { } 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); Object.assign(config, result.data);
} }
} catch (error) { } 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); const handled = await route.handler(method, path, req, res);
if (handled) return true; if (handled) return true;
} catch (error) { } 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 { try {
await plugin.hooks[hookName](...args); await plugin.hooks[hookName](...args);
} catch (error) { } 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 { try {
if (!existsSync(pluginsDir)) { if (!existsSync(pluginsDir)) {
await fs.mkdir(pluginsDir, { recursive: true }); 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 }); const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
@ -502,11 +503,11 @@ export async function discoverPlugins() {
pluginManager.register(plugin); pluginManager.register(plugin);
} }
} catch (error) { } 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) { } catch (error) {
console.error('[PluginManager] Failed to discover plugins:', error.message); logger.error('[PluginManager] Failed to discover plugins:', error.message);
} }
} }

View file

@ -4,10 +4,10 @@
*/ */
import { getRequestBody, handleError, MODEL_PROTOCOL_PREFIX, MODEL_PROVIDER, getProtocolPrefix } from '../utils/common.js'; 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 { convertData } from '../convert/convert.js';
import { ConverterFactory } from '../converters/ConverterFactory.js'; import { ConverterFactory } from '../converters/ConverterFactory.js';
import { getProviderModels } from '../providers/provider-models.js'; import { getProviderModels } from '../providers/provider-models.js';
// Ollama版本号 // Ollama版本号
/** /**
* Model name prefix mapping for different providers * 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 // First, check if model name has a prefix that directly indicates the provider
const providerFromPrefix = getProviderFromPrefix(modelName); const providerFromPrefix = getProviderFromPrefix(modelName);
if (providerFromPrefix) { if (providerFromPrefix) {
console.log(`[Provider Selection] Provider determined from prefix: ${providerFromPrefix}`); logger.info(`[Provider Selection] Provider determined from prefix: ${providerFromPrefix}`);
return providerFromPrefix; return providerFromPrefix;
} }
@ -222,17 +222,17 @@ export function getProviderForModel(modelName, defaultProvider) {
// Remove prefix for further analysis // Remove prefix for further analysis
const cleanModelName = removeModelPrefix(modelName); 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 // 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 // This handles cases where different providers have the same model name
const providerType = findProviderByModelName(cleanModelName); 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) { if (providerType) {
return 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 // Default to the provided default provider
return defaultProvider; return defaultProvider;
} }
@ -385,7 +385,7 @@ export async function handleOllamaEndpointsAfterAuth(method, path, req, res, api
*/ */
export async function handleOllamaTags(req, res, apiService, currentConfig, providerPoolManager) { export async function handleOllamaTags(req, res, apiService, currentConfig, providerPoolManager) {
try { try {
console.log('[Ollama] Handling /api/tags request'); logger.info('[Ollama] Handling /api/tags request');
const ollamaConverter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OLLAMA); const ollamaConverter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OLLAMA);
const { getServiceAdapter } = await import('../providers/adapter.js'); const { getServiceAdapter } = await import('../providers/adapter.js');
@ -402,7 +402,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov
} }
return []; return [];
} catch (error) { } catch (error) {
console.error(`[Ollama] Error from ${providerType}:`, error.message); logger.error(`[Ollama] Error from ${providerType}:`, error.message);
return []; return [];
} }
}; };
@ -435,7 +435,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov
// If no providers available, return empty list // If no providers available, return empty list
if (fetchPromises.length === 0) { 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: [] }; const response = { models: [] };
res.writeHead(200, { res.writeHead(200, {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -450,7 +450,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov
const results = await Promise.all(fetchPromises); const results = await Promise.all(fetchPromises);
const allModels = results.flat(); 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 }; const response = { models: allModels };
@ -461,7 +461,7 @@ export async function handleOllamaTags(req, res, apiService, currentConfig, prov
}); });
res.end(JSON.stringify(response)); res.end(JSON.stringify(response));
} catch (error) { } catch (error) {
console.error('[Ollama Tags Error]', error); logger.error('[Ollama Tags Error]', error);
handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA); 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) { export async function handleOllamaShow(req, res) {
try { try {
// console.log('[Ollama] Handling /api/show request'); // logger.info('[Ollama] Handling /api/show request');
const body = await getRequestBody(req); const body = await getRequestBody(req);
const modelName = body.name || body.model || 'unknown'; const modelName = body.name || body.model || 'unknown';
@ -486,7 +486,7 @@ export async function handleOllamaShow(req, res) {
}); });
res.end(JSON.stringify(showResponse)); res.end(JSON.stringify(showResponse));
} catch (error) { } catch (error) {
console.error('[Ollama Show Error]', error); logger.error('[Ollama Show Error]', error);
handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA); handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA);
} }
} }
@ -505,7 +505,7 @@ export function handleOllamaVersion(res) {
}); });
res.end(JSON.stringify(response)); res.end(JSON.stringify(response));
} catch (error) { } catch (error) {
console.error('[Ollama Version Error]', error); logger.error('[Ollama Version Error]', error);
handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA); handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA);
} }
} }
@ -516,7 +516,7 @@ export function handleOllamaVersion(res) {
*/ */
export async function handleOllamaChat(req, res, apiService, currentConfig, providerPoolManager) { export async function handleOllamaChat(req, res, apiService, currentConfig, providerPoolManager) {
try { try {
console.log('[Ollama] Handling /api/chat request'); logger.info('[Ollama] Handling /api/chat request');
const ollamaRequest = await getRequestBody(req); const ollamaRequest = await getRequestBody(req);
const { getServiceAdapter } = await import('../providers/adapter.js'); 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 ollamaRequest.model = modelName; // Use clean model name
const detectedProvider = getProviderForModel(rawModelName, currentConfig.MODEL_PROVIDER); 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 // Get the appropriate service based on detected provider
let actualApiService = apiService; let actualApiService = apiService;
@ -545,10 +545,10 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov
MODEL_PROVIDER: detectedProvider MODEL_PROVIDER: detectedProvider
}; };
actualApiService = getServiceAdapter(actualConfig); actualApiService = getServiceAdapter(actualConfig);
console.log(`[Ollama] Using provider from pool: ${detectedProvider}`); logger.info(`[Ollama] Using provider from pool: ${detectedProvider}`);
} else { } else {
// No healthy provider in pool, try to create service directly // 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) { if (!apiService) {
throw new Error(`No healthy provider available for ${detectedProvider}`); 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 // No pool manager and no apiService, try to create service directly
actualConfig = { ...currentConfig, MODEL_PROVIDER: detectedProvider }; actualConfig = { ...currentConfig, MODEL_PROVIDER: detectedProvider };
actualApiService = getServiceAdapter(actualConfig); 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); const ollamaChunk = ollamaConverter.convertStreamChunk(chunk, sourceProtocol, ollamaRequest.model, false);
res.write(JSON.stringify(ollamaChunk) + '\n'); res.write(JSON.stringify(ollamaChunk) + '\n');
} catch (chunkError) { } 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)); res.end(JSON.stringify(ollamaResponse));
} }
} catch (error) { } catch (error) {
console.error('[Ollama Chat Error]', error); logger.error('[Ollama Chat Error]', error);
handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA); 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) { export async function handleOllamaGenerate(req, res, apiService, currentConfig, providerPoolManager) {
try { try {
console.log('[Ollama] Handling /api/generate request'); logger.info('[Ollama] Handling /api/generate request');
const ollamaRequest = await getRequestBody(req); const ollamaRequest = await getRequestBody(req);
const { getServiceAdapter } = await import('../providers/adapter.js'); 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 ollamaRequest.model = modelName; // Use clean model name
const detectedProvider = getProviderForModel(rawModelName, currentConfig.MODEL_PROVIDER); 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 // Get the appropriate service based on detected provider
let actualApiService = apiService; let actualApiService = apiService;
@ -652,10 +652,10 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig,
MODEL_PROVIDER: detectedProvider MODEL_PROVIDER: detectedProvider
}; };
actualApiService = getServiceAdapter(actualConfig); actualApiService = getServiceAdapter(actualConfig);
console.log(`[Ollama] Using provider from pool: ${detectedProvider}`); logger.info(`[Ollama] Using provider from pool: ${detectedProvider}`);
} else { } else {
// No healthy provider in pool, try to create service directly // 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) { if (!apiService) {
throw new Error(`No healthy provider available for ${detectedProvider}`); 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 // No pool manager and no apiService, try to create service directly
actualConfig = { ...currentConfig, MODEL_PROVIDER: detectedProvider }; actualConfig = { ...currentConfig, MODEL_PROVIDER: detectedProvider };
actualApiService = getServiceAdapter(actualConfig); 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); const ollamaChunk = ollamaConverter.toOllamaGenerateStreamChunk(chunk, ollamaRequest.model, false);
res.write(JSON.stringify(ollamaChunk) + '\n'); res.write(JSON.stringify(ollamaChunk) + '\n');
} catch (chunkError) { } 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)); res.end(JSON.stringify(ollamaResponse));
} }
} catch (error) { } catch (error) {
console.error('[Ollama Generate Error]', error); logger.error('[Ollama Generate Error]', error);
handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA); handleError(res, error, MODEL_PROTOCOL_PREFIX.OLLAMA);
} }
} }

View file

@ -1,4 +1,5 @@
import deepmerge from 'deepmerge'; import deepmerge from 'deepmerge';
import logger from '../utils/logger.js';
import { handleError } from '../utils/common.js'; import { handleError } from '../utils/common.js';
import { handleUIApiRequests, serveStaticFiles } from '../services/ui-manager.js'; import { handleUIApiRequests, serveStaticFiles } from '../services/ui-manager.js';
import { handleAPIRequests } from '../services/api-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 { PROMPT_LOG_FILENAME } from '../core/config-manager.js';
import { handleOllamaRequest, handleOllamaShow } from './ollama-handler.js'; import { handleOllamaRequest, handleOllamaShow } from './ollama-handler.js';
import { getPluginManager } from '../core/plugin-manager.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 * Parse request body as JSON
*/ */
@ -36,6 +44,10 @@ function parseRequestBody(req) {
*/ */
export function createRequestHandler(config, providerPoolManager) { export function createRequestHandler(config, providerPoolManager) {
return async function requestHandler(req, res) { 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 // Deep copy the config for each request to allow dynamic modification
const currentConfig = deepmerge({}, config); const currentConfig = deepmerge({}, config);
const requestUrl = new URL(req.url, `http://${req.headers.host}`); const requestUrl = new URL(req.url, `http://${req.headers.host}`);
@ -77,8 +89,8 @@ export function createRequestHandler(config, providerPoolManager) {
return true; return true;
} }
console.log(`\n${new Date().toLocaleString()}`); // logger.info(`\n${new Date().toLocaleString()}`);
console.log(`[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`); logger.info(`[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`);
// Health check endpoint // Health check endpoint
if (method === 'GET' && path === '/health') { if (method === 'GET' && path === '/health') {
@ -118,7 +130,7 @@ export function createRequestHandler(config, providerPoolManager) {
})); }));
return true; return true;
} catch (error) { } 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); handleError(res, { statusCode: 500, message: `Failed to get providers health: ${error.message}` }, currentConfig.MODEL_PROVIDER);
return; return;
} }
@ -130,7 +142,7 @@ export function createRequestHandler(config, providerPoolManager) {
const modelProviderHeader = req.headers['model-provider']; const modelProviderHeader = req.headers['model-provider'];
if (modelProviderHeader) { if (modelProviderHeader) {
currentConfig.MODEL_PROVIDER = 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 // 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); const isValidProvider = Object.values(MODEL_PROVIDER).includes(firstSegment);
if (firstSegment && isValidProvider) { if (firstSegment && isValidProvider) {
currentConfig.MODEL_PROVIDER = firstSegment; 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(); pathSegments.shift();
path = '/' + pathSegments.join('/'); path = '/' + pathSegments.join('/');
requestUrl.pathname = path; requestUrl.pathname = path;
} else if (firstSegment && !isValidProvider) { } 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') { if (path.includes('/count_tokens') && method === 'POST') {
try { try {
const body = await parseRequestBody(req); 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 // Check if apiService has countTokens method
if (apiService && typeof apiService.countTokens === 'function') { if (apiService && typeof apiService.countTokens === 'function') {
@ -221,7 +233,7 @@ export function createRequestHandler(config, providerPoolManager) {
} }
return true; return true;
} catch (error) { } 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); handleError(res, { statusCode: 500, message: `Failed to count tokens: ${error.message}` }, currentConfig.MODEL_PROVIDER);
return; return;
} }
@ -237,6 +249,9 @@ export function createRequestHandler(config, providerPoolManager) {
res.end(JSON.stringify({ error: { message: 'Not Found' } })); res.end(JSON.stringify({ error: { message: 'Not Found' } }));
} catch (error) { } catch (error) {
handleError(res, error, currentConfig.MODEL_PROVIDER); handleError(res, error, currentConfig.MODEL_PROVIDER);
} finally {
// Clear request context after request is complete
logger.clearRequestContext(requestId);
} }
}; };
} }

View file

@ -33,6 +33,7 @@ import {
getAllUserApiKeys getAllUserApiKeys
} from './user-data-manager.js'; } from './user-data-manager.js';
import path from 'path'; import path from 'path';
import logger from '../../utils/logger.js';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import multer from 'multer'; import multer from 'multer';
@ -112,7 +113,7 @@ async function checkAdminAuth(req) {
return true; return true;
} catch (error) { } catch (error) {
console.error('[API Potluck] Auth check error:', error.message); logger.error('[API Potluck] Auth check error:', error.message);
return false; return false;
} }
} }
@ -130,7 +131,7 @@ export async function handlePotluckApiRoutes(method, path, req, res) {
if (!path.startsWith('/api/potluck')) { if (!path.startsWith('/api/potluck')) {
return false; return false;
} }
console.log('[API Potluck] Handling request:', method, path); logger.info('[API Potluck] Handling request:', method, path);
// 验证管理员权限 // 验证管理员权限
const isAuthed = await checkAdminAuth(req); const isAuthed = await checkAdminAuth(req);
@ -252,7 +253,7 @@ export async function handlePotluckApiRoutes(method, path, req, res) {
totalBonusUpdated++; totalBonusUpdated++;
} }
} catch (error) { } 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; return true;
} catch (error) { } catch (error) {
console.error('[API Potluck] API error:', error); logger.error('[API Potluck] API error:', error);
sendJson(res, 500, { sendJson(res, 500, {
success: false, success: false,
error: { message: error.message || 'Internal server error' } error: { message: error.message || 'Internal server error' }
@ -458,7 +459,7 @@ export async function handlePotluckUserApiRoutes(method, path, req, res) {
if (!path.startsWith('/api/potluckuser')) { if (!path.startsWith('/api/potluckuser')) {
return false; return false;
} }
console.log('[API Potluck User] Handling request:', method, path); logger.info('[API Potluck User] Handling request:', method, path);
try { try {
// 从请求中提取 API Key // 从请求中提取 API Key
@ -662,7 +663,7 @@ export async function handlePotluckUserApiRoutes(method, path, req, res) {
return true; return true;
} catch (error) { } catch (error) {
console.error('[API Potluck] User API error:', error); logger.error('[API Potluck] User API error:', error);
sendJson(res, 500, { sendJson(res, 500, {
success: false, success: false,
error: { message: error.message || 'Internal server error' } error: { message: error.message || 'Internal server error' }
@ -732,7 +733,7 @@ async function handleUserUpload(req, res, apiKey) {
return new Promise((resolve) => { return new Promise((resolve) => {
userUpload.single('file')(req, res, async (err) => { userUpload.single('file')(req, res, async (err) => {
if (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 }); sendJson(res, 400, { success: false, error: err.message });
resolve(true); resolve(true);
return; return;
@ -782,10 +783,10 @@ async function handleUserUpload(req, res, apiKey) {
try { try {
await autoLinkProviderConfigs(CONFIG); await autoLinkProviderConfigs(CONFIG);
} catch (linkError) { } 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, { sendJson(res, 200, {
success: true, success: true,
@ -798,7 +799,7 @@ async function handleUserUpload(req, res, apiKey) {
resolve(true); resolve(true);
} catch (error) { } 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 }); sendJson(res, 500, { success: false, error: error.message });
resolve(true); resolve(true);
} }
@ -825,7 +826,7 @@ async function handleKiroBatchImportTokens(req, res, apiKey) {
return true; 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 响应头 // 设置 SSE 响应头
res.writeHead(200, { res.writeHead(200, {
@ -864,15 +865,15 @@ async function handleKiroBatchImportTokens(req, res, apiKey) {
// 自动从主服务同步健康状态 // 自动从主服务同步健康状态
await syncCredentialHealthFromPool(apiKey, credential); 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) { } 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', { sendSSE('complete', {
@ -887,7 +888,7 @@ async function handleKiroBatchImportTokens(req, res, apiKey) {
return true; return true;
} catch (error) { } 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) { if (res.headersSent) {
res.write(`event: error\n`); res.write(`event: error\n`);
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
@ -936,12 +937,12 @@ async function handleKiroImportAwsCredentials(req, res, apiKey) {
return true; 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); const result = await importAwsCredentials(credentials);
if (result.success) { 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 = { const credentialInfo = {
@ -953,7 +954,7 @@ async function handleKiroImportAwsCredentials(req, res, apiKey) {
// 自动从主服务同步健康状态 // 自动从主服务同步健康状态
const healthResult = await syncCredentialHealthFromPool(apiKey, credential); 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, { sendJson(res, 200, {
success: true, success: true,
@ -972,7 +973,7 @@ async function handleKiroImportAwsCredentials(req, res, apiKey) {
return true; return true;
} catch (error) { } 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, { sendJson(res, 500, {
success: false, success: false,
error: error.message error: error.message
@ -1070,7 +1071,7 @@ async function handleCredentialHealthCheck(req, res, apiKey, credentialId) {
return true; 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); const result = await syncCredentialHealthFromPool(apiKey, credential);
@ -1081,7 +1082,7 @@ async function handleCredentialHealthCheck(req, res, apiKey, credentialId) {
return true; return true;
} catch (error) { } catch (error) {
console.error('[API Potluck User] Health check error:', error); logger.error('[API Potluck User] Health check error:', error);
sendJson(res, 500, { sendJson(res, 500, {
success: false, success: false,
error: error.message error: error.message
@ -1117,7 +1118,7 @@ async function checkAllCredentialsHealth() {
} }
// isHealthy === null 表示未注册到服务,不计入健康/不健康 // isHealthy === null 表示未注册到服务,不计入健康/不健康
} catch (error) { } 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 秒执行第一次同步 // 启动后延迟 30 秒执行第一次同步
setTimeout(async () => { 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(); 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); }, 30000);
// 定时同步 // 定时同步
healthCheckTimer = setInterval(async () => { 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(); 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); }, 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) { if (healthCheckTimer) {
clearInterval(healthCheckTimer); clearInterval(healthCheckTimer);
healthCheckTimer = null; healthCheckTimer = null;
console.log('[API Potluck] Health sync scheduler stopped'); logger.info('[API Potluck] Health sync scheduler stopped');
} }
} }

View file

@ -31,6 +31,7 @@ import {
} from './middleware.js'; } from './middleware.js';
import { consumeBonus, getConfig } from './user-data-manager.js'; import { consumeBonus, getConfig } from './user-data-manager.js';
import logger from '../../utils/logger.js';
import { handlePotluckApiRoutes, handlePotluckUserApiRoutes, startHealthCheckScheduler, stopHealthCheckScheduler } from './api-routes.js'; import { handlePotluckApiRoutes, handlePotluckUserApiRoutes, startHealthCheckScheduler, stopHealthCheckScheduler } from './api-routes.js';
@ -53,7 +54,7 @@ const apiPotluckPlugin = {
* @param {Object} config - 服务器配置 * @param {Object} config - 服务器配置
*/ */
async init(config) { async init(config) {
console.log('[API Potluck Plugin] Initializing...'); logger.info('[API Potluck Plugin] Initializing...');
// 注入配置获取函数 // 注入配置获取函数
setConfigGetter(getConfig); setConfigGetter(getConfig);
// 启动定时健康检查 // 启动定时健康检查
@ -64,7 +65,7 @@ const apiPotluckPlugin = {
* 销毁钩子 * 销毁钩子
*/ */
async destroy() { async destroy() {
console.log('[API Potluck Plugin] Destroying...'); logger.info('[API Potluck Plugin] Destroying...');
// 停止定时健康检查 // 停止定时健康检查
stopHealthCheckScheduler(); 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 { return {
handled: false, handled: false,
authorized: true, authorized: true,
@ -165,7 +166,7 @@ const apiPotluckPlugin = {
}); });
} catch (e) { } catch (e) {
// 静默失败,不影响主流程 // 静默失败,不影响主流程
console.error('[API Potluck Plugin] Failed to record usage:', e.message); logger.error('[API Potluck Plugin] Failed to record usage:', e.message);
} }
} }
} }

View file

@ -4,6 +4,7 @@
*/ */
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import logger from '../../utils/logger.js';
import { existsSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, readFileSync, writeFileSync } from 'fs';
import path from 'path'; import path from 'path';
import crypto from 'crypto'; import crypto from 'crypto';
@ -64,7 +65,7 @@ function ensureLoaded() {
} }
} }
if (needsMigration) { if (needsMigration) {
console.log('[API Potluck] Migrated legacy keys: added bonusRemaining field'); logger.info('[API Potluck] Migrated legacy keys: added bonusRemaining field');
markDirty(); markDirty();
} }
} else { } else {
@ -72,7 +73,7 @@ function ensureLoaded() {
syncWriteToFile(); syncWriteToFile();
} }
} catch (error) { } 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: {} }; keyStore = { keys: {} };
} }
@ -101,7 +102,7 @@ function syncWriteToFile() {
} }
writeFileSync(KEYS_STORE_FILE, JSON.stringify(keyStore, null, 2), 'utf8'); writeFileSync(KEYS_STORE_FILE, JSON.stringify(keyStore, null, 2), 'utf8');
} catch (error) { } 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); await fs.rename(tempFile, KEYS_STORE_FILE);
isDirty = false; isDirty = false;
} catch (error) { } catch (error) {
console.error('[API Potluck] Persist failed:', error.message); logger.error('[API Potluck] Persist failed:', error.message);
} finally { } finally {
isWriting = false; isWriting = false;
} }
@ -206,7 +207,7 @@ export async function createKey(name = '', dailyLimit = null) {
markDirty(); markDirty();
await persistIfDirty(); // 创建操作立即持久化 await persistIfDirty(); // 创建操作立即持久化
console.log(`[API Potluck] Created key: ${apiKey.substring(0, 12)}...`); logger.info(`[API Potluck] Created key: ${apiKey.substring(0, 12)}...`);
return keyData; return keyData;
} }
@ -245,7 +246,7 @@ export async function deleteKey(keyId) {
delete keyStore.keys[keyId]; delete keyStore.keys[keyId];
markDirty(); markDirty();
await persistIfDirty(); // 删除操作立即持久化 await persistIfDirty(); // 删除操作立即持久化
console.log(`[API Potluck] Deleted key: ${keyId.substring(0, 12)}...`); logger.info(`[API Potluck] Deleted key: ${keyId.substring(0, 12)}...`);
return true; return true;
} }
@ -322,7 +323,7 @@ export async function regenerateKey(oldKeyId) {
markDirty(); markDirty();
await persistIfDirty(); // 立即持久化 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 { return {
oldKey: oldKeyId, oldKey: oldKeyId,
@ -491,7 +492,7 @@ export async function applyDailyLimitToAllKeys(newLimit) {
await persistIfDirty(); 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 }; return { total: keys.length, updated };
} }

View file

@ -5,6 +5,7 @@
*/ */
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import logger from '../../utils/logger.js';
import { existsSync, readFileSync, writeFileSync, watch } from 'fs'; import { existsSync, readFileSync, writeFileSync, watch } from 'fs';
import path from 'path'; import path from 'path';
@ -126,7 +127,7 @@ export async function updateConfig(newConfig) {
await persistIfDirty(); await persistIfDirty();
const updatedConfig = getFullConfig(); const updatedConfig = getFullConfig();
console.log(`[API Potluck UserData] Config updated:`, updatedConfig); logger.info(`[API Potluck UserData] Config updated:`, updatedConfig);
return updatedConfig; return updatedConfig;
} }
@ -140,7 +141,7 @@ function updatePersistTimer(newInterval) {
if (persistTimer) { if (persistTimer) {
clearInterval(persistTimer); clearInterval(persistTimer);
persistTimer = setInterval(persistIfDirty, currentPersistInterval); 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(); syncWriteToFile();
} }
} catch (error) { } 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: {} }; userDataStore = { config: {}, users: {} };
} }
@ -206,7 +207,7 @@ function syncWriteToFile() {
} }
writeFileSync(USER_DATA_FILE, JSON.stringify(userDataStore, null, 2), 'utf8'); writeFileSync(USER_DATA_FILE, JSON.stringify(userDataStore, null, 2), 'utf8');
} catch (error) { } 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); await fs.rename(tempFile, USER_DATA_FILE);
isDirty = false; isDirty = false;
} catch (error) { } catch (error) {
console.error('[API Potluck UserData] Persist failed:', error.message); logger.error('[API Potluck UserData] Persist failed:', error.message);
} finally { } finally {
isWriting = false; isWriting = false;
} }
@ -268,17 +269,17 @@ function startFileWatcher() {
// 检查配置是否有变化 // 检查配置是否有变化
if (JSON.stringify(oldConfig) !== JSON.stringify(newConfig)) { if (JSON.stringify(oldConfig) !== JSON.stringify(newConfig)) {
userDataStore.config = newConfig; userDataStore.config = newConfig;
console.log('[API Potluck UserData] Config hot-reloaded:', getBonusConfig()); logger.info('[API Potluck UserData] Config hot-reloaded:', getBonusConfig());
} }
} }
} catch (error) { } 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) { } 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(); markDirty();
await persistIfDirty(); 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; return true;
} }
@ -521,7 +522,7 @@ export async function addCredentialBonus(apiKey, credentialId) {
userData.credentialBonuses.push(bonus); userData.credentialBonuses.push(bonus);
markDirty(); markDirty();
console.log(`[API Potluck UserData] Added bonus for credential: ${credentialId}`); logger.info(`[API Potluck UserData] Added bonus for credential: ${credentialId}`);
return bonus; return bonus;
} }
@ -542,7 +543,7 @@ export async function removeCredentialBonus(apiKey, credentialId) {
userData.credentialBonuses.splice(index, 1); userData.credentialBonuses.splice(index, 1);
markDirty(); markDirty();
console.log(`[API Potluck UserData] Removed bonus for credential: ${credentialId}`); logger.info(`[API Potluck UserData] Removed bonus for credential: ${credentialId}`);
return true; return true;
} }
@ -643,7 +644,7 @@ export async function syncCredentialBonuses(apiKey, credentialsWithHealth) {
usedCount: 0 usedCount: 0
}); });
added++; 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}`);
} }
} }

View file

@ -9,6 +9,8 @@
* 4. URL query: ?key=<key> * 4. URL query: ?key=<key>
*/ */
import logger from '../../utils/logger.js';
/** /**
* 检查请求是否已授权 * 检查请求是否已授权
* @param {http.IncomingMessage} req - HTTP 请求 * @param {http.IncomingMessage} req - HTTP 请求
@ -79,11 +81,13 @@ const defaultAuthPlugin = {
} }
// 认证失败,记录日志但不发送响应(由 request-handler 统一处理) // 认证失败,记录日志但不发送响应(由 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 表示此插件不授权,让其他插件或默认逻辑处理 // 返回 null 表示此插件不授权,让其他插件或默认逻辑处理
return { handled: false, authorized: null }; return { handled: false, authorized: null };
} }
}; };
export default defaultAuthPlugin; export default defaultAuthPlugin;

View file

@ -1,14 +1,15 @@
import { OpenAIResponsesApiService } from './openai/openai-responses-core.js'; // 导入OpenAIResponsesApiService import { OpenAIResponsesApiService } from './openai/openai-responses-core.js';
import { GeminiApiService } from './gemini/gemini-core.js'; // 导入geminiApiService import { GeminiApiService } from './gemini/gemini-core.js';
import { AntigravityApiService } from './gemini/antigravity-core.js'; // 导入AntigravityApiService import { AntigravityApiService } from './gemini/antigravity-core.js';
import { OpenAIApiService } from './openai/openai-core.js'; // 导入OpenAIApiService import { OpenAIApiService } from './openai/openai-core.js';
import { ClaudeApiService } from './claude/claude-core.js'; // 导入ClaudeApiService import { ClaudeApiService } from './claude/claude-core.js';
import { KiroApiService } from './claude/claude-kiro.js'; // 导入KiroApiService import { KiroApiService } from './claude/claude-kiro.js';
import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService import { QwenApiService } from './openai/qwen-core.js';
import { IFlowApiService } from './openai/iflow-core.js'; // 导入IFlowApiService import { IFlowApiService } from './openai/iflow-core.js';
import { CodexApiService } from './openai/codex-core.js'; // 导入CodexApiService import { CodexApiService } from './openai/codex-core.js';
import { ForwardApiService } from './forward/forward-core.js'; // 导入ForwardApiService import { ForwardApiService } from './forward/forward-core.js';
import { MODEL_PROVIDER } from '../utils/common.js'; // 导入 MODEL_PROVIDER import { MODEL_PROVIDER } from '../utils/common.js';
import logger from '../utils/logger.js';
// 定义AI服务适配器接口 // 定义AI服务适配器接口
// 所有的服务适配器都应该实现这些方法 // 所有的服务适配器都应该实现这些方法
@ -78,13 +79,13 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter {
super(); super();
this.geminiApiService = new GeminiApiService(config); this.geminiApiService = new GeminiApiService(config);
// this.geminiApiService.initialize().catch(error => { // this.geminiApiService.initialize().catch(error => {
// console.error("Failed to initialize geminiApiService:", error); // logger.error("Failed to initialize geminiApiService:", error);
// }); // });
} }
async generateContent(model, requestBody) { async generateContent(model, requestBody) {
if (!this.geminiApiService.isInitialized) { 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(); await this.geminiApiService.initialize();
} }
return this.geminiApiService.generateContent(model, requestBody); return this.geminiApiService.generateContent(model, requestBody);
@ -92,7 +93,7 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter {
async *generateContentStream(model, requestBody) { async *generateContentStream(model, requestBody) {
if (!this.geminiApiService.isInitialized) { 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(); await this.geminiApiService.initialize();
} }
yield* this.geminiApiService.generateContentStream(model, requestBody); yield* this.geminiApiService.generateContentStream(model, requestBody);
@ -100,7 +101,7 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter {
async listModels() { async listModels() {
if (!this.geminiApiService.isInitialized) { 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(); await this.geminiApiService.initialize();
} }
// Gemini Core API 的 listModels 已经返回符合 Gemini 格式的数据,所以不需要额外转换 // Gemini Core API 的 listModels 已经返回符合 Gemini 格式的数据,所以不需要额外转换
@ -112,7 +113,7 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter {
await this.geminiApiService.initialize(); await this.geminiApiService.initialize();
} }
if(this.isExpiryDateNear()===true){ 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 this.geminiApiService.initializeAuth(true);
} }
return Promise.resolve(); return Promise.resolve();
@ -122,7 +123,7 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter {
if (!this.geminiApiService.isInitialized) { if (!this.geminiApiService.isInitialized) {
await this.geminiApiService.initialize(); await this.geminiApiService.initialize();
} }
console.log(`[Gemini] Force refreshing token...`); logger.info(`[Gemini] Force refreshing token...`);
return this.geminiApiService.initializeAuth(true); return this.geminiApiService.initializeAuth(true);
} }
@ -136,7 +137,7 @@ export class GeminiApiServiceAdapter extends ApiServiceAdapter {
*/ */
async getUsageLimits() { async getUsageLimits() {
if (!this.geminiApiService.isInitialized) { 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(); await this.geminiApiService.initialize();
} }
return this.geminiApiService.getUsageLimits(); return this.geminiApiService.getUsageLimits();
@ -152,7 +153,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter {
async generateContent(model, requestBody) { async generateContent(model, requestBody) {
if (!this.antigravityApiService.isInitialized) { 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(); await this.antigravityApiService.initialize();
} }
return this.antigravityApiService.generateContent(model, requestBody); return this.antigravityApiService.generateContent(model, requestBody);
@ -160,7 +161,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter {
async *generateContentStream(model, requestBody) { async *generateContentStream(model, requestBody) {
if (!this.antigravityApiService.isInitialized) { 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(); await this.antigravityApiService.initialize();
} }
yield* this.antigravityApiService.generateContentStream(model, requestBody); yield* this.antigravityApiService.generateContentStream(model, requestBody);
@ -168,7 +169,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter {
async listModels() { async listModels() {
if (!this.antigravityApiService.isInitialized) { 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(); await this.antigravityApiService.initialize();
} }
return this.antigravityApiService.listModels(); return this.antigravityApiService.listModels();
@ -179,7 +180,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter {
await this.antigravityApiService.initialize(); await this.antigravityApiService.initialize();
} }
if (this.isExpiryDateNear() === true) { 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 this.antigravityApiService.initializeAuth(true);
} }
return Promise.resolve(); return Promise.resolve();
@ -189,7 +190,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter {
if (!this.antigravityApiService.isInitialized) { if (!this.antigravityApiService.isInitialized) {
await this.antigravityApiService.initialize(); await this.antigravityApiService.initialize();
} }
console.log(`[Antigravity] Force refreshing token...`); logger.info(`[Antigravity] Force refreshing token...`);
return this.antigravityApiService.initializeAuth(true); return this.antigravityApiService.initializeAuth(true);
} }
@ -203,7 +204,7 @@ export class AntigravityApiServiceAdapter extends ApiServiceAdapter {
*/ */
async getUsageLimits() { async getUsageLimits() {
if (!this.antigravityApiService.isInitialized) { 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(); await this.antigravityApiService.initialize();
} }
return this.antigravityApiService.getUsageLimits(); return this.antigravityApiService.getUsageLimits();
@ -330,14 +331,14 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter {
super(); super();
this.kiroApiService = new KiroApiService(config); this.kiroApiService = new KiroApiService(config);
// this.kiroApiService.initialize().catch(error => { // this.kiroApiService.initialize().catch(error => {
// console.error("Failed to initialize kiroApiService:", error); // logger.error("Failed to initialize kiroApiService:", error);
// }); // });
} }
async generateContent(model, requestBody) { async generateContent(model, requestBody) {
// The adapter expects the requestBody to be in OpenAI format for Kiro API // The adapter expects the requestBody to be in OpenAI format for Kiro API
if (!this.kiroApiService.isInitialized) { 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(); await this.kiroApiService.initialize();
} }
return this.kiroApiService.generateContent(model, requestBody); return this.kiroApiService.generateContent(model, requestBody);
@ -346,7 +347,7 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter {
async *generateContentStream(model, requestBody) { async *generateContentStream(model, requestBody) {
// The adapter expects the requestBody to be in OpenAI format for Kiro API // The adapter expects the requestBody to be in OpenAI format for Kiro API
if (!this.kiroApiService.isInitialized) { 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(); await this.kiroApiService.initialize();
} }
const stream = this.kiroApiService.generateContentStream(model, requestBody); const stream = this.kiroApiService.generateContentStream(model, requestBody);
@ -356,7 +357,7 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter {
async listModels() { async listModels() {
// Returns the native model list from the Kiro service // Returns the native model list from the Kiro service
if (!this.kiroApiService.isInitialized) { 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(); await this.kiroApiService.initialize();
} }
return this.kiroApiService.listModels(); return this.kiroApiService.listModels();
@ -367,7 +368,7 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter {
await this.kiroApiService.initialize(); await this.kiroApiService.initialize();
} }
if(this.isExpiryDateNear()===true){ 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 this.kiroApiService.initializeAuth(true);
} }
return Promise.resolve(); return Promise.resolve();
@ -377,7 +378,7 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter {
if (!this.kiroApiService.isInitialized) { if (!this.kiroApiService.isInitialized) {
await this.kiroApiService.initialize(); await this.kiroApiService.initialize();
} }
console.log(`[Kiro] Force refreshing token...`); logger.info(`[Kiro] Force refreshing token...`);
return this.kiroApiService.initializeAuth(true); return this.kiroApiService.initializeAuth(true);
} }
@ -391,7 +392,7 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter {
*/ */
async getUsageLimits() { async getUsageLimits() {
if (!this.kiroApiService.isInitialized) { 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(); await this.kiroApiService.initialize();
} }
return this.kiroApiService.getUsageLimits(); return this.kiroApiService.getUsageLimits();
@ -416,7 +417,7 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter {
async generateContent(model, requestBody) { async generateContent(model, requestBody) {
if (!this.qwenApiService.isInitialized) { 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(); await this.qwenApiService.initialize();
} }
return this.qwenApiService.generateContent(model, requestBody); return this.qwenApiService.generateContent(model, requestBody);
@ -424,7 +425,7 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter {
async *generateContentStream(model, requestBody) { async *generateContentStream(model, requestBody) {
if (!this.qwenApiService.isInitialized) { 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(); await this.qwenApiService.initialize();
} }
yield* this.qwenApiService.generateContentStream(model, requestBody); yield* this.qwenApiService.generateContentStream(model, requestBody);
@ -432,7 +433,7 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter {
async listModels() { async listModels() {
if (!this.qwenApiService.isInitialized) { 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(); await this.qwenApiService.initialize();
} }
return this.qwenApiService.listModels(); return this.qwenApiService.listModels();
@ -443,7 +444,7 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter {
await this.qwenApiService.initialize(); await this.qwenApiService.initialize();
} }
if (this.isExpiryDateNear()) { 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 this.qwenApiService._initializeAuth(true);
} }
return Promise.resolve(); return Promise.resolve();
@ -453,7 +454,7 @@ export class QwenApiServiceAdapter extends ApiServiceAdapter {
if (!this.qwenApiService.isInitialized) { if (!this.qwenApiService.isInitialized) {
await this.qwenApiService.initialize(); await this.qwenApiService.initialize();
} }
console.log(`[Qwen] Force refreshing token...`); logger.info(`[Qwen] Force refreshing token...`);
return this.qwenApiService._initializeAuth(true); return this.qwenApiService._initializeAuth(true);
} }
@ -471,7 +472,7 @@ export class IFlowApiServiceAdapter extends ApiServiceAdapter {
async generateContent(model, requestBody) { async generateContent(model, requestBody) {
if (!this.iflowApiService.isInitialized) { 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(); await this.iflowApiService.initialize();
} }
return this.iflowApiService.generateContent(model, requestBody); return this.iflowApiService.generateContent(model, requestBody);
@ -479,7 +480,7 @@ export class IFlowApiServiceAdapter extends ApiServiceAdapter {
async *generateContentStream(model, requestBody) { async *generateContentStream(model, requestBody) {
if (!this.iflowApiService.isInitialized) { 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(); await this.iflowApiService.initialize();
} }
yield* this.iflowApiService.generateContentStream(model, requestBody); yield* this.iflowApiService.generateContentStream(model, requestBody);
@ -487,7 +488,7 @@ export class IFlowApiServiceAdapter extends ApiServiceAdapter {
async listModels() { async listModels() {
if (!this.iflowApiService.isInitialized) { 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(); await this.iflowApiService.initialize();
} }
return this.iflowApiService.listModels(); return this.iflowApiService.listModels();
@ -498,7 +499,7 @@ export class IFlowApiServiceAdapter extends ApiServiceAdapter {
await this.iflowApiService.initialize(); await this.iflowApiService.initialize();
} }
if (this.isExpiryDateNear()) { 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); await this.iflowApiService.initializeAuth(true);
} }
return Promise.resolve(); return Promise.resolve();
@ -508,7 +509,7 @@ export class IFlowApiServiceAdapter extends ApiServiceAdapter {
if (!this.iflowApiService.isInitialized) { if (!this.iflowApiService.isInitialized) {
await this.iflowApiService.initialize(); await this.iflowApiService.initialize();
} }
console.log(`[iFlow] Force refreshing API key...`); logger.info(`[iFlow] Force refreshing API key...`);
return this.iflowApiService.initializeAuth(true); return this.iflowApiService.initializeAuth(true);
} }
@ -527,7 +528,7 @@ export class CodexApiServiceAdapter extends ApiServiceAdapter {
async generateContent(model, requestBody) { async generateContent(model, requestBody) {
if (!this.codexApiService.isInitialized) { 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(); await this.codexApiService.initialize();
} }
return this.codexApiService.generateContent(model, requestBody); return this.codexApiService.generateContent(model, requestBody);
@ -535,7 +536,7 @@ export class CodexApiServiceAdapter extends ApiServiceAdapter {
async *generateContentStream(model, requestBody) { async *generateContentStream(model, requestBody) {
if (!this.codexApiService.isInitialized) { 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(); await this.codexApiService.initialize();
} }
yield* this.codexApiService.generateContentStream(model, requestBody); yield* this.codexApiService.generateContentStream(model, requestBody);
@ -550,7 +551,7 @@ export class CodexApiServiceAdapter extends ApiServiceAdapter {
await this.codexApiService.initialize(); await this.codexApiService.initialize();
} }
if (this.isExpiryDateNear()) { 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(); await this.codexApiService.refreshAccessToken();
} }
return Promise.resolve(); return Promise.resolve();
@ -560,7 +561,7 @@ export class CodexApiServiceAdapter extends ApiServiceAdapter {
if (!this.codexApiService.isInitialized) { if (!this.codexApiService.isInitialized) {
await this.codexApiService.initialize(); await this.codexApiService.initialize();
} }
console.log(`[Codex] Force refreshing token...`); logger.info(`[Codex] Force refreshing token...`);
return this.codexApiService.refreshAccessToken(); return this.codexApiService.refreshAccessToken();
} }
@ -607,7 +608,7 @@ export const serviceInstances = {};
// 服务适配器工厂 // 服务适配器工厂
export function getServiceAdapter(config) { export function getServiceAdapter(config) {
const customNameDisplay = config.customName ? ` (${config.customName})` : ''; 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 provider = config.MODEL_PROVIDER;
const providerKey = config.uuid ? provider + config.uuid : provider; const providerKey = config.uuid ? provider + config.uuid : provider;
if (!serviceInstances[providerKey]) { if (!serviceInstances[providerKey]) {
@ -648,3 +649,4 @@ export function getServiceAdapter(config) {
} }
return serviceInstances[providerKey]; return serviceInstances[providerKey];
} }

View file

@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js'; import { configureAxiosProxy } from '../../utils/proxy-utils.js';
@ -22,7 +23,7 @@ export class ClaudeApiService {
this.apiKey = config.CLAUDE_API_KEY; this.apiKey = config.CLAUDE_API_KEY;
this.baseUrl = config.CLAUDE_BASE_URL; this.baseUrl = config.CLAUDE_BASE_URL;
this.useSystemProxy = config?.USE_SYSTEM_PROXY_CLAUDE ?? false; 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(); this.client = this.createClient();
} }
@ -92,14 +93,14 @@ export class ClaudeApiService {
// 对于 Claude API401 通常意味着 API Key 无效,不进行重试 // 对于 Claude API401 通常意味着 API Key 无效,不进行重试
if (status === 401 || status === 403) { 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; throw error;
} }
// 处理 429 (Too Many Requests) 与指数退避 // 处理 429 (Too Many Requests) 与指数退避
if (status === 429 && retryCount < maxRetries) { if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1); return this.callApi(endpoint, body, isRetry, retryCount + 1);
} }
@ -107,7 +108,7 @@ export class ClaudeApiService {
// 处理其他可重试错误 (5xx 服务器错误) // 处理其他可重试错误 (5xx 服务器错误)
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1); return this.callApi(endpoint, body, isRetry, retryCount + 1);
} }
@ -116,12 +117,12 @@ export class ClaudeApiService {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1); 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; throw error;
} }
} }
@ -166,7 +167,7 @@ export class ClaudeApiService {
return; return;
} }
} catch (e) { } 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 API401 通常意味着 API Key 无效,不进行重试 // 对于 Claude API401 通常意味着 API Key 无效,不进行重试
if (status === 401 || status === 403) { 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; throw error;
} }
// 处理 429 (Too Many Requests) 与指数退避 // 处理 429 (Too Many Requests) 与指数退避
if (status === 429 && retryCount < maxRetries) { if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return; return;
@ -197,7 +198,7 @@ export class ClaudeApiService {
// 处理其他可重试错误 (5xx 服务器错误) // 处理其他可重试错误 (5xx 服务器错误)
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return; return;
@ -207,13 +208,13 @@ export class ClaudeApiService {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return; 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; throw error;
} }
} }
@ -248,7 +249,7 @@ export class ClaudeApiService {
* @returns {Promise<object>} List of models. * @returns {Promise<object>} List of models.
*/ */
async listModels() { async listModels() {
console.log('[ClaudeApiService] Listing available models.'); logger.info('[ClaudeApiService] Listing available models.');
// Claude API 没有直接的 /models 端点来列出所有模型。 // Claude API 没有直接的 /models 端点来列出所有模型。
// 通常,你需要根据 Anthropic 的文档硬编码你希望支持的模型。 // 通常,你需要根据 Anthropic 的文档硬编码你希望支持的模型。
// 这里我们返回一些常见的 Claude 模型作为示例。 // 这里我们返回一些常见的 Claude 模型作为示例。
@ -266,3 +267,4 @@ export class ClaudeApiService {
return { models: models.map(m => ({ name: m.name })) }; return { models: models.map(m => ({ name: m.name })) };
} }
} }

View file

@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import logger from '../../utils/logger.js';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as path from 'path'; import * as path from 'path';
@ -215,7 +216,7 @@ function extractCredentialsFromCorruptedJson(content) {
// 至少需要 refreshToken 或 accessToken 才算有效 // 至少需要 refreshToken 或 accessToken 才算有效
if (extracted.refreshToken || extracted.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; return extracted;
} }
@ -270,7 +271,7 @@ function parseSingleToolCall(toolCallText) {
} }
}; };
} catch (e) { } 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; return null;
} }
} }
@ -335,7 +336,7 @@ function deduplicateToolCalls(toolCalls) {
seen.add(key); seen.add(key);
uniqueToolCalls.push(tc); uniqueToolCalls.push(tc);
} else { } else {
console.log(`Skipping duplicate tool call: ${tc.function.name}`); logger.info(`Skipping duplicate tool call: ${tc.function.name}`);
} }
} }
return uniqueToolCalls; return uniqueToolCalls;
@ -349,7 +350,7 @@ export class KiroApiService {
this.credsBase64 = config.KIRO_OAUTH_CREDS_BASE64; this.credsBase64 = config.KIRO_OAUTH_CREDS_BASE64;
this.useSystemProxy = config?.USE_SYSTEM_PROXY_KIRO ?? false; this.useSystemProxy = config?.USE_SYSTEM_PROXY_KIRO ?? false;
this.uuid = config?.uuid; // 获取多节点配置的 uuid 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.accessToken = config.KIRO_ACCESS_TOKEN;
// this.refreshToken = config.KIRO_REFRESH_TOKEN; // this.refreshToken = config.KIRO_REFRESH_TOKEN;
// this.clientId = config.KIRO_CLIENT_ID; // this.clientId = config.KIRO_CLIENT_ID;
@ -366,9 +367,9 @@ export class KiroApiService {
const parsedCreds = JSON.parse(decodedCreds); const parsedCreds = JSON.parse(decodedCreds);
// Store parsedCreds to be merged in initializeAuth // Store parsedCreds to be merged in initializeAuth
this.base64Creds = parsedCreds; this.base64Creds = parsedCreds;
console.info('[Kiro] Successfully decoded Base64 credentials in constructor.'); logger.info('[Kiro] Successfully decoded Base64 credentials in constructor.');
} catch (error) { } 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) { } else if (config.KIRO_OAUTH_CREDS_FILE_PATH) {
this.credsFilePath = config.KIRO_OAUTH_CREDS_FILE_PATH; this.credsFilePath = config.KIRO_OAUTH_CREDS_FILE_PATH;
@ -381,7 +382,7 @@ export class KiroApiService {
async initialize() { async initialize() {
if (this.isInitialized) return; if (this.isInitialized) return;
console.log('[Kiro] Initializing Kiro API Service...'); logger.info('[Kiro] Initializing Kiro API Service...');
// 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑
// 仅执行基础的凭证加载 // 仅执行基础的凭证加载
await this.loadCredentials(); await this.loadCredentials();
@ -454,29 +455,29 @@ async loadCredentials() {
try { try {
return JSON.parse(fileContent); return JSON.parse(fileContent);
} catch (parseError) { } catch (parseError) {
console.warn('[Kiro Auth] JSON parse failed, attempting repair...'); logger.warn('[Kiro Auth] JSON parse failed, attempting repair...');
try { try {
const repaired = repairJson(fileContent); const repaired = repairJson(fileContent);
const result = JSON.parse(repaired); const result = JSON.parse(repaired);
console.info('[Kiro Auth] JSON repair successful'); logger.info('[Kiro Auth] JSON repair successful');
return result; return result;
} catch (repairError) { } catch (repairError) {
console.warn('[Kiro Auth] JSON repair failed, attempting field extraction...'); logger.warn('[Kiro Auth] JSON repair failed, attempting field extraction...');
// 尝试从损坏的 JSON 中提取关键字段 // 尝试从损坏的 JSON 中提取关键字段
const extracted = extractCredentialsFromCorruptedJson(fileContent); const extracted = extractCredentialsFromCorruptedJson(fileContent);
if (extracted) { if (extracted) {
console.info('[Kiro Auth] Field extraction successful, credentials recovered'); logger.info('[Kiro Auth] Field extraction successful, credentials recovered');
return extracted; return extracted;
} }
console.error('[Kiro Auth] All recovery methods failed:', repairError.message); logger.error('[Kiro Auth] All recovery methods failed:', repairError.message);
return null; return null;
} }
} }
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
console.debug(`[Kiro Auth] Credential file not found: ${filePath}`); logger.debug(`[Kiro Auth] Credential file not found: ${filePath}`);
} else { } 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; return null;
} }
@ -488,7 +489,7 @@ async loadCredentials() {
// Priority 1: Load from Base64 credentials if available // Priority 1: Load from Base64 credentials if available
if (this.base64Creds) { if (this.base64Creds) {
Object.assign(mergedCredentials, 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; this.base64Creds = null;
} }
@ -497,13 +498,13 @@ async loadCredentials() {
const dirPath = path.dirname(targetFilePath); const dirPath = path.dirname(targetFilePath);
const targetFileName = path.basename(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 { try {
const targetCredentials = await loadCredentialsFromFile(targetFilePath); const targetCredentials = await loadCredentialsFromFile(targetFilePath);
if (targetCredentials) { if (targetCredentials) {
Object.assign(mergedCredentials, 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); const files = await fs.readdir(dirPath);
@ -514,12 +515,12 @@ async loadCredentials() {
if (credentials) { if (credentials) {
credentials.expiresAt = mergedCredentials.expiresAt; credentials.expiresAt = mergedCredentials.expiresAt;
Object.assign(mergedCredentials, credentials); Object.assign(mergedCredentials, credentials);
console.debug(`[Kiro Auth] Loaded Client credentials from ${file}`); logger.debug(`[Kiro Auth] Loaded Client credentials from ${file}`);
} }
} }
} }
} catch (error) { } 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 // Apply loaded credentials
@ -534,7 +535,7 @@ async loadCredentials() {
this.idcRegion = this.idcRegion || mergedCredentials.idcRegion; this.idcRegion = this.idcRegion || mergedCredentials.idcRegion;
if (!this.region) { 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'; 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.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); this.baseUrl = (this.config.KIRO_BASE_URL || KIRO_CONSTANTS.BASE_URL).replace("{{region}}", this.region);
} catch (error) { } 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) { async initializeAuth(forceRefresh = false) {
if (this.accessToken && !forceRefresh) { 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; return;
} }
@ -586,33 +587,33 @@ async saveCredentialsToFile(filePath, newData) {
try { try {
existingData = JSON.parse(fileContent); existingData = JSON.parse(fileContent);
} catch (parseError) { } catch (parseError) {
console.warn('[Kiro Auth] JSON parse failed, attempting repair...'); logger.warn('[Kiro Auth] JSON parse failed, attempting repair...');
try { try {
const repaired = repairJson(fileContent); const repaired = repairJson(fileContent);
existingData = JSON.parse(repaired); existingData = JSON.parse(repaired);
console.info('[Kiro Auth] JSON repair successful'); logger.info('[Kiro Auth] JSON repair successful');
} catch (repairError) { } 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); const extracted = extractCredentialsFromCorruptedJson(fileContent);
if (extracted) { if (extracted) {
existingData = extracted; existingData = extracted;
console.info('[Kiro Auth] Field extraction successful'); logger.info('[Kiro Auth] Field extraction successful');
} else { } else {
console.error('[Kiro Auth] All recovery methods failed:', repairError.message); logger.error('[Kiro Auth] All recovery methods failed:', repairError.message);
existingData = {}; existingData = {};
} }
} }
} }
} catch (readError) { } catch (readError) {
if (readError.code === 'ENOENT') { 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 { } 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 }; const mergedData = { ...existingData, ...newData };
await fs.writeFile(filePath, JSON.stringify(mergedData, null, 2), 'utf8'); 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 }; const refreshConfig = { timeout: KIRO_CONSTANTS.TOKEN_REFRESH_TIMEOUT };
if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) { if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) {
response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody, refreshConfig); 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 { } else {
response = await this.axiosInstance.post(refreshUrl, requestBody, refreshConfig); 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) { if (response.data && response.data.accessToken) {
@ -652,7 +653,7 @@ async saveCredentialsToFile(filePath, newData) {
const expiresIn = response.data.expiresIn; const expiresIn = response.data.expiresIn;
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
this.expiresAt = expiresAt; this.expiresAt = expiresAt;
console.info('[Kiro Auth] Access token refreshed successfully'); logger.info('[Kiro Auth] Access token refreshed successfully');
const updatedTokenData = { const updatedTokenData = {
accessToken: this.accessToken, accessToken: this.accessToken,
@ -673,7 +674,7 @@ async saveCredentialsToFile(filePath, newData) {
throw new Error('Invalid refresh response: Missing accessToken'); throw new Error('Invalid refresh response: Missing accessToken');
} }
} catch (error) { } 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}`); throw new Error(`Token refresh failed: ${error.message}`);
} }
} }
@ -787,7 +788,7 @@ async saveCredentialsToFile(filePath, newData) {
const lastMessage = processedMessages[processedMessages.length - 1]; const lastMessage = processedMessages[processedMessages.length - 1];
if (processedMessages.length > 0 && lastMessage.role === 'assistant') { if (processedMessages.length > 0 && lastMessage.role === 'assistant') {
if (lastMessage.content[0].type === "text" && lastMessage.content[0].text === "{") { 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(); processedMessages.pop();
} }
} }
@ -818,7 +819,7 @@ async saveCredentialsToFile(filePath, newData) {
// 上一条是字符串,当前是数组,转换为数组格式 // 上一条是字符串,当前是数组,转换为数组格式
lastMsg.content = [{ type: 'text', text: lastMsg.content }, ...currentMsg.content]; 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 { } else {
mergedMessages.push(currentMsg); mergedMessages.push(currentMsg);
} }
@ -839,14 +840,14 @@ async saveCredentialsToFile(filePath, newData) {
const name = (tool.name || '').toLowerCase(); const name = (tool.name || '').toLowerCase();
const shouldIgnore = name === 'web_search' || name === 'websearch'; const shouldIgnore = name === 'web_search' || name === 'websearch';
if (shouldIgnore) { if (shouldIgnore) {
console.log(`[Kiro] Ignoring tool: ${tool.name}`); logger.info(`[Kiro] Ignoring tool: ${tool.name}`);
} }
return !shouldIgnore; return !shouldIgnore;
}); });
if (filteredTools.length === 0) { if (filteredTools.length === 0) {
// 所有工具都被过滤掉了,不添加 tools 上下文 // 所有工具都被过滤掉了,不添加 tools 上下文
console.log('[Kiro] All tools were filtered out'); logger.info('[Kiro] All tools were filtered out');
} else { } else {
const MAX_DESCRIPTION_LENGTH = 9216; const MAX_DESCRIPTION_LENGTH = 9216;
@ -858,7 +859,7 @@ async saveCredentialsToFile(filePath, newData) {
if (desc.length > MAX_DESCRIPTION_LENGTH) { if (desc.length > MAX_DESCRIPTION_LENGTH) {
desc = desc.substring(0, MAX_DESCRIPTION_LENGTH) + "..."; desc = desc.substring(0, MAX_DESCRIPTION_LENGTH) + "...";
truncatedCount++; 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 { return {
@ -873,7 +874,7 @@ async saveCredentialsToFile(filePath, newData) {
}); });
if (truncatedCount > 0) { 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 }; toolsContext = { tools: kiroTools };
@ -960,7 +961,7 @@ async saveCredentialsToFile(filePath, newData) {
// 如果有保留的图片,添加到消息中 // 如果有保留的图片,添加到消息中
if (images.length > 0) { if (images.length > 0) {
userInputMessage.images = images; 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 = userInputMessage.content
? `${userInputMessage.content}\n${imagePlaceholder}` ? `${userInputMessage.content}\n${imagePlaceholder}`
: 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) { if (toolResults.length > 0) {
@ -1036,7 +1037,7 @@ async saveCredentialsToFile(filePath, newData) {
// 如果最后一条消息是 assistant需要将其加入 history然后创建一个 user 类型的 currentMessage // 如果最后一条消息是 assistant需要将其加入 history然后创建一个 user 类型的 currentMessage
// 因为 CodeWhisperer API 的 currentMessage 必须是 userInputMessage 类型 // 因为 CodeWhisperer API 的 currentMessage 必须是 userInputMessage 类型
if (currentMessage.role === 'assistant') { 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 // 构建 assistant 消息并加入 history
let assistantResponseMessage = { let assistantResponseMessage = {
@ -1080,7 +1081,7 @@ async saveCredentialsToFile(filePath, newData) {
const lastHistoryItem = history[history.length - 1]; const lastHistoryItem = history[history.length - 1];
if (!lastHistoryItem.assistantResponseMessage) { if (!lastHistoryItem.assistantResponseMessage) {
// 最后一个不是 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({ history.push({
assistantResponseMessage: { assistantResponseMessage: {
content: 'Continue' content: 'Continue'
@ -1189,7 +1190,7 @@ async saveCredentialsToFile(filePath, newData) {
let fullContent = ''; let fullContent = '';
const toolCalls = []; const toolCalls = [];
let currentToolCallDict = null; let currentToolCallDict = null;
// console.log(`rawStr=${rawStr}`); // logger.info(`rawStr=${rawStr}`);
// 改进的 SSE 事件解析:匹配 :message-typeevent 后面的 JSON 数据 // 改进的 SSE 事件解析:匹配 :message-typeevent 后面的 JSON 数据
// 使用更精确的正则来匹配 SSE 格式的事件 // 使用更精确的正则来匹配 SSE 格式的事件
@ -1237,7 +1238,7 @@ async saveCredentialsToFile(filePath, newData) {
const args = JSON.parse(currentToolCallDict.function.arguments); const args = JSON.parse(currentToolCallDict.function.arguments);
currentToolCallDict.function.arguments = JSON.stringify(args); currentToolCallDict.function.arguments = JSON.stringify(args);
} catch (e) { } 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); toolCalls.push(currentToolCallDict);
currentToolCallDict = null; currentToolCallDict = null;
@ -1329,12 +1330,12 @@ async saveCredentialsToFile(filePath, newData) {
// Handle 401 (Unauthorized) - refresh UUID first, then try to refresh token // Handle 401 (Unauthorized) - refresh UUID first, then try to refresh token
if (status === 401 && !isRetry) { 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 // 1. 先刷新 UUID
const newUuid = this._refreshUuid(); const newUuid = this._refreshUuid();
if (newUuid) { if (newUuid) {
console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); logger.info(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`);
this.uuid = newUuid; this.uuid = newUuid;
} }
@ -1353,20 +1354,20 @@ async saveCredentialsToFile(filePath, newData) {
// Handle 403 (Forbidden) - mark as unhealthy immediately, no retry // Handle 403 (Forbidden) - mark as unhealthy immediately, no retry
if (status === 403 && !isRetry) { 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 错误 // 检查是否为 temporarily suspended 错误
const isSuspended = errorMessage && errorMessage.toLowerCase().includes('temporarily is suspended'); const isSuspended = errorMessage && errorMessage.toLowerCase().includes('temporarily is suspended');
if (isSuspended) { if (isSuspended) {
// temporarily suspended 错误:直接标记为不健康,不刷新 UUID // 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); this._markCredentialUnhealthy('403 Forbidden - Account temporarily suspended', error);
} else { } else {
// 其他 403 错误:先刷新 UUID然后标记需要刷新 // 其他 403 错误:先刷新 UUID然后标记需要刷新
// const newUuid = this._refreshUuid(); // const newUuid = this._refreshUuid();
// if (newUuid) { // if (newUuid) {
// console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); // logger.info(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`);
// this.uuid = newUuid; // this.uuid = newUuid;
// } // }
this._markCredentialNeedRefresh('403 Forbidden', error); this._markCredentialNeedRefresh('403 Forbidden', error);
@ -1380,7 +1381,7 @@ async saveCredentialsToFile(filePath, newData) {
// Handle 429 (Too Many Requests) - wait baseDelay then switch credential // Handle 429 (Too Many Requests) - wait baseDelay then switch credential
if (status === 429) { 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)); await new Promise(resolve => setTimeout(resolve, baseDelay));
// Mark error for credential switch without recording error count // Mark error for credential switch without recording error count
error.shouldSwitchCredential = true; error.shouldSwitchCredential = true;
@ -1390,7 +1391,7 @@ async saveCredentialsToFile(filePath, newData) {
// Handle 5xx server errors - wait baseDelay then switch credential // Handle 5xx server errors - wait baseDelay then switch credential
if (status >= 500 && status < 600) { 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)); await new Promise(resolve => setTimeout(resolve, baseDelay));
// Mark error for credential switch without recording error count // Mark error for credential switch without recording error count
error.shouldSwitchCredential = true; error.shouldSwitchCredential = true;
@ -1402,12 +1403,12 @@ async saveCredentialsToFile(filePath, newData) {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(method, model, body, isRetry, retryCount + 1); 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; throw error;
} }
} }
@ -1426,7 +1427,7 @@ async saveCredentialsToFile(filePath, newData) {
}); });
return newUuid; return newUuid;
} else { } 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; return null;
} }
} }
@ -1441,7 +1442,7 @@ async saveCredentialsToFile(filePath, newData) {
_markCredentialNeedRefresh(reason, error = null) { _markCredentialNeedRefresh(reason, error = null) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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 // 使用新的 markProviderNeedRefresh 方法代替 markProviderUnhealthyImmediately
poolManager.markProviderNeedRefresh(MODEL_PROVIDER.KIRO_API, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.KIRO_API, {
uuid: this.uuid uuid: this.uuid
@ -1452,7 +1453,7 @@ async saveCredentialsToFile(filePath, newData) {
} }
return true; return true;
} else { } 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; return false;
} }
} }
@ -1467,7 +1468,7 @@ async saveCredentialsToFile(filePath, newData) {
_markCredentialUnhealthy(reason, error = null) { _markCredentialUnhealthy(reason, error = null) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderUnhealthyImmediately(MODEL_PROVIDER.KIRO_API, {
uuid: this.uuid uuid: this.uuid
}, reason); }, reason);
@ -1477,7 +1478,7 @@ async saveCredentialsToFile(filePath, newData) {
} }
return true; return true;
} else { } 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; return false;
} }
} }
@ -1494,7 +1495,7 @@ async saveCredentialsToFile(filePath, newData) {
_markCredentialUnhealthyWithRecovery(reason, error = null, recoveryTime = null) { _markCredentialUnhealthyWithRecovery(reason, error = null, recoveryTime = null) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderUnhealthyWithRecoveryTime(MODEL_PROVIDER.KIRO_API, {
uuid: this.uuid uuid: this.uuid
}, reason, recoveryTime); }, reason, recoveryTime);
@ -1504,7 +1505,7 @@ async saveCredentialsToFile(filePath, newData) {
} }
return true; return true;
} else { } 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; return false;
} }
} }
@ -1528,18 +1529,18 @@ async saveCredentialsToFile(filePath, newData) {
* @private * @private
*/ */
async _handle402Error(error, context = 'unknown') { 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 { try {
// Verify usage limits to confirm quota exhaustion // Verify usage limits to confirm quota exhaustion
const usageLimits = await this.getUsageLimits(); const usageLimits = await this.getUsageLimits();
const isQuotaExhausted = usageLimits?.usedCount >= usageLimits?.limitCount; 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 // Calculate recovery time: 1st day of next month at 00:00:00 UTC
const nextMonth = this._getNextMonthFirstDay(); const nextMonth = this._getNextMonthFirstDay();
this._markCredentialUnhealthyWithRecovery('402 Payment Required - Quota Exhausted', error, nextMonth); this._markCredentialUnhealthyWithRecovery('402 Payment Required - Quota Exhausted', error, nextMonth);
} catch (usageError) { } 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 // If we can't verify, still mark as unhealthy with recovery time
const nextMonth = this._getNextMonthFirstDay(); const nextMonth = this._getNextMonthFirstDay();
this._markCredentialUnhealthyWithRecovery('402 Payment Required - Quota Exceeded (unverified)', error, nextMonth); this._markCredentialUnhealthyWithRecovery('402 Payment Required - Quota Exceeded (unverified)', error, nextMonth);
@ -1552,27 +1553,27 @@ async saveCredentialsToFile(filePath, newData) {
_processApiResponse(response) { _processApiResponse(response) {
const rawResponseText = Buffer.isBuffer(response.data) ? response.data.toString('utf8') : String(response.data); 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")) { 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 // 1. Parse structured events and bracket calls from parsed content
const parsedFromEvents = this.parseEventStreamChunk(rawResponseText); const parsedFromEvents = this.parseEventStreamChunk(rawResponseText);
let fullResponseText = parsedFromEvents.content; let fullResponseText = parsedFromEvents.content;
let allToolCalls = [...parsedFromEvents.toolCalls]; // clone 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 // 2. Crucial fix from Python example: Parse bracket tool calls from the original raw response
const rawBracketToolCalls = parseBracketToolCalls(rawResponseText); const rawBracketToolCalls = parseBracketToolCalls(rawResponseText);
if (rawBracketToolCalls) { 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); allToolCalls.push(...rawBracketToolCalls);
} }
// 3. Deduplicate all collected tool calls // 3. Deduplicate all collected tool calls
const uniqueToolCalls = deduplicateToolCalls(allToolCalls); 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. // 4. Clean up response text by removing all tool call syntax from the final text.
// The text from parseEventStreamChunk is already partially cleaned. // The text from parseEventStreamChunk is already partially cleaned.
@ -1587,8 +1588,8 @@ async saveCredentialsToFile(filePath, newData) {
fullResponseText = fullResponseText.replace(/\s+/g, ' ').trim(); fullResponseText = fullResponseText.replace(/\s+/g, ' ').trim();
} }
//console.log(`[Kiro] Final response text after tool call cleanup: ${fullResponseText}`); //logger.info(`[Kiro] Final response text after tool call cleanup: ${fullResponseText}`);
//console.log(`[Kiro] Final tool calls after deduplication: ${JSON.stringify(uniqueToolCalls)}`); //logger.info(`[Kiro] Final tool calls after deduplication: ${JSON.stringify(uniqueToolCalls)}`);
return { responseText: fullResponseText, toolCalls: uniqueToolCalls }; return { responseText: fullResponseText, toolCalls: uniqueToolCalls };
} }
@ -1597,12 +1598,12 @@ async saveCredentialsToFile(filePath, newData) {
// 检查 token 是否即将过期,如果是则推送到刷新队列 // 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) { 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'); this._markCredentialNeedRefresh('Token near expiry in generateContent');
} }
const finalModel = MODEL_MAPPING[model] ? model : this.modelName; 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 // Estimate input tokens before making the API call
const inputTokens = this.estimateInputTokens(requestBody); const inputTokens = this.estimateInputTokens(requestBody);
@ -1613,7 +1614,7 @@ async saveCredentialsToFile(filePath, newData) {
const { responseText, toolCalls } = this._processApiResponse(response); const { responseText, toolCalls } = this._processApiResponse(response);
return this.buildClaudeResponse(responseText, false, 'assistant', model, toolCalls, inputTokens); return this.buildClaudeResponse(responseText, false, 'assistant', model, toolCalls, inputTokens);
} catch (error) { } catch (error) {
console.error('[Kiro] Error in generateContent:', error); logger.error('[Kiro] Error in generateContent:', error);
throw new Error(`Error processing response: ${error.message}`); throw new Error(`Error processing response: ${error.message}`);
} }
} }
@ -1850,12 +1851,12 @@ async saveCredentialsToFile(filePath, newData) {
// Handle 401 (Unauthorized) - try to refresh token first // Handle 401 (Unauthorized) - try to refresh token first
if (status === 401 && !isRetry) { 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 // 1. 先刷新 UUID
const newUuid = this._refreshUuid(); const newUuid = this._refreshUuid();
if (newUuid) { if (newUuid) {
console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); logger.info(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`);
this.uuid = newUuid; this.uuid = newUuid;
} }
// 标记当前凭证为不健康(会自动进入刷新队列) // 标记当前凭证为不健康(会自动进入刷新队列)
@ -1873,20 +1874,20 @@ async saveCredentialsToFile(filePath, newData) {
// Handle 403 (Forbidden) - mark as unhealthy immediately, no retry // Handle 403 (Forbidden) - mark as unhealthy immediately, no retry
if (status === 403 && !isRetry) { 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 错误 // 检查是否为 temporarily suspended 错误
const isSuspended = errorMessage && errorMessage.toLowerCase().includes('temporarily is suspended'); const isSuspended = errorMessage && errorMessage.toLowerCase().includes('temporarily is suspended');
if (isSuspended) { if (isSuspended) {
// temporarily suspended 错误:直接标记为不健康,不刷新 UUID // 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); this._markCredentialUnhealthy('403 Forbidden - Account temporarily suspended', error);
} else { } else {
// 其他 403 错误:先刷新 UUID然后标记需要刷新 // 其他 403 错误:先刷新 UUID然后标记需要刷新
// const newUuid = this._refreshUuid(); // const newUuid = this._refreshUuid();
// if (newUuid) { // if (newUuid) {
// console.log(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`); // logger.info(`[Kiro] UUID refreshed: ${this.uuid} -> ${newUuid}`);
// this.uuid = newUuid; // this.uuid = newUuid;
// } // }
this._markCredentialNeedRefresh('403 Forbidden', error); this._markCredentialNeedRefresh('403 Forbidden', error);
@ -1900,7 +1901,7 @@ async saveCredentialsToFile(filePath, newData) {
// Handle 429 (Too Many Requests) - wait baseDelay then switch credential // Handle 429 (Too Many Requests) - wait baseDelay then switch credential
if (status === 429) { 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)); await new Promise(resolve => setTimeout(resolve, baseDelay));
// Mark error for credential switch without recording error count // Mark error for credential switch without recording error count
error.shouldSwitchCredential = true; error.shouldSwitchCredential = true;
@ -1910,7 +1911,7 @@ async saveCredentialsToFile(filePath, newData) {
// Handle 5xx server errors - wait baseDelay then switch credential // Handle 5xx server errors - wait baseDelay then switch credential
if (status >= 500 && status < 600) { 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)); await new Promise(resolve => setTimeout(resolve, baseDelay));
// Mark error for credential switch without recording error count // Mark error for credential switch without recording error count
error.shouldSwitchCredential = true; error.shouldSwitchCredential = true;
@ -1922,13 +1923,13 @@ async saveCredentialsToFile(filePath, newData) {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApiReal(method, model, body, isRetry, retryCount + 1); yield* this.streamApiReal(method, model, body, isRetry, retryCount + 1);
return; 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; throw error;
} finally { } finally {
// 确保流被关闭,释放资源 // 确保流被关闭,释放资源
@ -1943,7 +1944,7 @@ async saveCredentialsToFile(filePath, newData) {
try { try {
return await this.callApi(method, model, body, isRetry, retryCount); return await this.callApi(method, model, body, isRetry, retryCount);
} catch (error) { } catch (error) {
console.error('[Kiro] Error calling API:', error); logger.error('[Kiro] Error calling API:', error);
throw error; throw error;
} }
} }
@ -1954,12 +1955,12 @@ async saveCredentialsToFile(filePath, newData) {
// 检查 token 是否即将过期,如果是则推送到刷新队列 // 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) { 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'); this._markCredentialNeedRefresh('Token near expiry in generateContentStream');
} }
const finalModel = MODEL_MAPPING[model] ? model : this.modelName; 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 inputTokens = 0;
let contextUsagePercentage = null; let contextUsagePercentage = null;
@ -2214,7 +2215,7 @@ async saveCredentialsToFile(filePath, newData) {
if (thinkingRequested && streamState.buffer) { if (thinkingRequested && streamState.buffer) {
if (streamState.inThinking) { 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)); yield* pushEvents(createThinkingDeltaEvents(streamState.buffer));
streamState.buffer = ''; streamState.buffer = '';
yield* pushEvents(createThinkingDeltaEvents("")); yield* pushEvents(createThinkingDeltaEvents(""));
@ -2293,9 +2294,9 @@ async saveCredentialsToFile(filePath, newData) {
if (contextUsagePercentage !== null && contextUsagePercentage > 0) { if (contextUsagePercentage !== null && contextUsagePercentage > 0) {
const totalTokens = Math.round(KIRO_CONSTANTS.TOTAL_CONTEXT_TOKENS * contextUsagePercentage / 100); const totalTokens = Math.round(KIRO_CONSTANTS.TOTAL_CONTEXT_TOKENS * contextUsagePercentage / 100);
inputTokens = Math.max(0, totalTokens - outputTokens); 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 { } else {
console.warn('[Kiro Stream] contextUsagePercentage not received, using estimation'); logger.warn('[Kiro Stream] contextUsagePercentage not received, using estimation');
inputTokens = estimatedInputTokens; inputTokens = estimatedInputTokens;
} }
@ -2315,7 +2316,7 @@ async saveCredentialsToFile(filePath, newData) {
yield { type: "message_stop" }; yield { type: "message_stop" };
} catch (error) { } 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}`); throw new Error(`Error processing response: ${error.message}`);
} }
} }
@ -2329,7 +2330,7 @@ async saveCredentialsToFile(filePath, newData) {
return countTokens(text); return countTokens(text);
} catch (error) { } catch (error) {
// Fallback to estimation if tokenizer fails // 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); return Math.ceil((text || '').length / 4);
} }
} }
@ -2459,7 +2460,7 @@ async saveCredentialsToFile(filePath, newData) {
const args = tc.function.arguments; const args = tc.function.arguments;
inputObject = typeof args === 'string' ? JSON.parse(args) : args; inputObject = typeof args === 'string' ? JSON.parse(args) : args;
} catch (e) { } 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, // If parsing fails, wrap the raw string in an object as a fallback,
// since Claude's `input` field expects an object. // since Claude's `input` field expects an object.
inputObject = { "raw_arguments": tc.function.arguments }; inputObject = { "raw_arguments": tc.function.arguments };
@ -2527,7 +2528,7 @@ async saveCredentialsToFile(filePath, newData) {
const args = tc.function.arguments; const args = tc.function.arguments;
inputObject = typeof args === 'string' ? JSON.parse(args) : args; inputObject = typeof args === 'string' ? JSON.parse(args) : args;
} catch (e) { } 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, // If parsing fails, wrap the raw string in an object as a fallback,
// since Claude's `input` field expects an object. // since Claude's `input` field expects an object.
inputObject = { "raw_arguments": tc.function.arguments }; inputObject = { "raw_arguments": tc.function.arguments };
@ -2589,7 +2590,7 @@ async saveCredentialsToFile(filePath, newData) {
const bufferMs = 30 * 1000; const bufferMs = 30 * 1000;
return expirationTime.getTime() <= (currentTime.getTime() + bufferMs); return expirationTime.getTime() <= (currentTime.getTime() + bufferMs);
} catch (error) { } 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 return true; // Treat as expired if parsing fails
} }
} }
@ -2603,10 +2604,10 @@ async saveCredentialsToFile(filePath, newData) {
const expirationTime = new Date(this.expiresAt); const expirationTime = new Date(this.expiresAt);
const nearMinutes = 30; const nearMinutes = 30;
const { message, isNearExpiry } = formatExpiryLog('Kiro', expirationTime.getTime(), nearMinutes); const { message, isNearExpiry } = formatExpiryLog('Kiro', expirationTime.getTime(), nearMinutes);
console.log(message); logger.info(message);
return isNearExpiry; return isNearExpiry;
} catch (error) { } 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 return false; // Treat as expired if parsing fails
} }
} }
@ -2615,11 +2616,11 @@ async saveCredentialsToFile(filePath, newData) {
* 后台异步刷新 token不阻塞当前请求 * 后台异步刷新 token不阻塞当前请求
*/ */
triggerBackgroundRefresh() { triggerBackgroundRefresh() {
console.log('[Kiro] Background token refresh started...'); logger.info('[Kiro] Background token refresh started...');
this.initializeAuth(true).then(() => { this.initializeAuth(true).then(() => {
console.log('[Kiro] Background token refresh completed successfully'); logger.info('[Kiro] Background token refresh completed successfully');
}).catch((error) => { }).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. 已过期 → 必须等待刷新 // 1. 已过期 → 必须等待刷新
// 2. 即将过期但还能用 → 后台异步刷新,不阻塞当前请求 // 2. 即将过期但还能用 → 后台异步刷新,不阻塞当前请求
// if (this.isTokenExpired()) { // 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); // await this.initializeAuth(true);
// } else if (this.isExpiryDateNear()) { // } 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(); // this.triggerBackgroundRefresh();
// } // }
@ -2745,7 +2746,7 @@ async saveCredentialsToFile(filePath, newData) {
try { try {
const response = await this.axiosInstance.get(fullUrl, { headers }); 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; return response.data;
} catch (error) { } catch (error) {
const status = error.response?.status; const status = error.response?.status;
@ -2771,20 +2772,20 @@ async saveCredentialsToFile(filePath, newData) {
// 对于用量查询401/403 错误直接标记凭证为不健康,不重试 // 对于用量查询401/403 错误直接标记凭证为不健康,不重试
if (status === 401) { 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); this._markCredentialNeedRefresh('401 Unauthorized on usage query', formattedError);
throw formattedError; throw formattedError;
} }
if (status === 403) { 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 错误 // 检查是否为 temporarily suspended 错误
const isSuspended = errorMessage && errorMessage.toLowerCase().includes('temporarily is suspended'); const isSuspended = errorMessage && errorMessage.toLowerCase().includes('temporarily is suspended');
if (isSuspended) { if (isSuspended) {
// temporarily suspended 错误:直接标记为不健康,不刷新 UUID // 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); this._markCredentialUnhealthy('403 Forbidden - Account temporarily suspended on usage query', formattedError);
} else { } else {
// 其他 403 错误:标记需要刷新 // 其他 403 错误:标记需要刷新
@ -2794,8 +2795,9 @@ async saveCredentialsToFile(filePath, newData) {
throw formattedError; 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; throw formattedError;
} }
} }
} }

View file

@ -1,4 +1,5 @@
import { ProviderStrategy } from '../../utils/provider-strategy.js'; import { ProviderStrategy } from '../../utils/provider-strategy.js';
import logger from '../../utils/logger.js';
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
/** /**
@ -59,7 +60,7 @@ class ClaudeStrategy extends ProviderStrategy {
: filePromptContent; : filePromptContent;
requestBody.system = newSystemText; 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; return requestBody;
} }
@ -71,3 +72,4 @@ class ClaudeStrategy extends ProviderStrategy {
} }
export { ClaudeStrategy }; export { ClaudeStrategy };

View file

@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js'; import { configureAxiosProxy } from '../../utils/proxy-utils.js';
@ -24,7 +25,7 @@ export class ForwardApiService {
this.headerName = config?.FORWARD_HEADER_NAME || 'Authorization'; this.headerName = config?.FORWARD_HEADER_NAME || 'Authorization';
this.headerValuePrefix = config?.FORWARD_HEADER_VALUE_PREFIX || 'Bearer '; 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({ const httpAgent = new http.Agent({
keepAlive: true, keepAlive: true,
@ -75,18 +76,18 @@ export class ForwardApiService {
const isNetworkError = isRetryableNetworkError(error); const isNetworkError = isRetryableNetworkError(error);
if (status === 401 || status === 403) { 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; throw error;
} }
if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) { if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1); 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; throw error;
} }
} }
@ -120,7 +121,7 @@ export class ForwardApiService {
yield parsedChunk; yield parsedChunk;
} catch (e) { } catch (e) {
// If it's not JSON, it might be a different format, but for a forwarder we try to parse common SSE formats // 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) { if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return; return;
@ -158,8 +159,9 @@ export class ForwardApiService {
const response = await this.axiosInstance.get('/models'); const response = await this.axiosInstance.get('/models');
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error(`Error listing Forward models:`, error.message); logger.error(`Error listing Forward models:`, error.message);
return { data: [] }; return { data: [] };
} }
} }
} }

View file

@ -51,3 +51,4 @@ class ForwardStrategy extends ProviderStrategy {
} }
export { ForwardStrategy }; export { ForwardStrategy };

View file

@ -1,5 +1,6 @@
import { OAuth2Client } from 'google-auth-library'; import { OAuth2Client } from 'google-auth-library';
import logger from '../../utils/logger.js';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
@ -716,7 +717,7 @@ export class AntigravityApiService {
if (proxyConfig) { if (proxyConfig) {
oauth2Options.transporterOptions = proxyConfig; oauth2Options.transporterOptions = proxyConfig;
console.log('[Antigravity] Using proxy for OAuth2Client'); logger.info('[Antigravity] Using proxy for OAuth2Client');
} else { } else {
oauth2Options.transporterOptions = { oauth2Options.transporterOptions = {
agent: httpsAgent, agent: httpsAgent,
@ -762,7 +763,7 @@ export class AntigravityApiService {
async initialize() { async initialize() {
if (this.isInitialized) return; if (this.isInitialized) return;
console.log('[Antigravity] Initializing Antigravity API Service...'); logger.info('[Antigravity] Initializing Antigravity API Service...');
// 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑
// 仅执行基础的凭证加载 // 仅执行基础的凭证加载
await this.loadCredentials(); await this.loadCredentials();
@ -770,13 +771,13 @@ export class AntigravityApiService {
if (!this.projectId) { if (!this.projectId) {
this.projectId = await this.discoverProjectAndModels(); this.projectId = await this.discoverProjectAndModels();
} else { } else {
console.log(`[Antigravity] Using provided Project ID: ${this.projectId}`); logger.info(`[Antigravity] Using provided Project ID: ${this.projectId}`);
// 获取可用模型 // 获取可用模型
await this.fetchAvailableModels(); await this.fetchAvailableModels();
} }
this.isInitialized = true; 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 data = await fs.readFile(credPath, "utf8");
const credentials = JSON.parse(data); const credentials = JSON.parse(data);
this.authClient.setCredentials(credentials); this.authClient.setCredentials(credentials);
console.log('[Antigravity Auth] Credentials loaded successfully from file.'); logger.info('[Antigravity Auth] Credentials loaded successfully from file.');
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
console.debug(`[Antigravity Auth] Credentials file not found: ${credPath}`); logger.debug(`[Antigravity Auth] Credentials file not found: ${credPath}`);
} else { } 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) { if (needsRefresh || !this.authClient.credentials.access_token) {
try { try {
if (this.authClient.credentials.refresh_token) { 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(); const { credentials: newCredentials } = await this.authClient.refreshAccessToken();
this.authClient.setCredentials(newCredentials); this.authClient.setCredentials(newCredentials);
await this._saveCredentialsToFile(credPath, 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 中的刷新状态并标记为健康 // 刷新成功,重置 PoolManager 中的刷新状态并标记为健康
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
@ -829,10 +830,10 @@ export class AntigravityApiService {
poolManager.resetProviderRefreshStatus(MODEL_PROVIDER.ANTIGRAVITY, this.uuid); poolManager.resetProviderRefreshStatus(MODEL_PROVIDER.ANTIGRAVITY, this.uuid);
} }
} else { } 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); const newTokens = await this.getNewToken(credPath);
this.authClient.setCredentials(newTokens); 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(); const poolManager = getProviderPoolManager();
@ -841,7 +842,7 @@ export class AntigravityApiService {
} }
} }
} catch (error) { } 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.`); throw new Error(`Failed to load OAuth credentials.`);
} }
} }
@ -851,12 +852,12 @@ export class AntigravityApiService {
// 使用统一的 OAuth 处理方法 // 使用统一的 OAuth 处理方法
const { authUrl, authInfo } = await handleGeminiAntigravityOAuth(this.config); const { authUrl, authInfo } = await handleGeminiAntigravityOAuth(this.config);
console.log('\n[Antigravity Auth] 正在自动打开浏览器进行授权...'); logger.info('\n[Antigravity Auth] 正在自动打开浏览器进行授权...');
console.log('[Antigravity Auth] 授权链接:', authUrl, '\n'); logger.info('[Antigravity Auth] 授权链接:', authUrl, '\n');
// 自动打开浏览器 // 自动打开浏览器
const showFallbackMessage = () => { const showFallbackMessage = () => {
console.log('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); logger.info('[Antigravity Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开');
}; };
if (this.config) { if (this.config) {
@ -880,7 +881,7 @@ export class AntigravityApiService {
const credentials = JSON.parse(data); const credentials = JSON.parse(data);
if (credentials.access_token) { if (credentials.access_token) {
clearInterval(checkInterval); clearInterval(checkInterval);
console.log('[Antigravity Auth] New token obtained successfully.'); logger.info('[Antigravity Auth] New token obtained successfully.');
resolve(credentials); resolve(credentials);
} }
} catch (error) { } catch (error) {
@ -914,20 +915,20 @@ export class AntigravityApiService {
async _saveCredentialsToFile(filePath, credentials) { async _saveCredentialsToFile(filePath, credentials) {
try { try {
await fs.writeFile(filePath, JSON.stringify(credentials, null, 2)); 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) { } 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; throw error;
} }
} }
async discoverProjectAndModels() { async discoverProjectAndModels() {
if (this.projectId) { 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; return this.projectId;
} }
console.log('[Antigravity] Discovering Project ID...'); logger.info('[Antigravity] Discovering Project ID...');
try { try {
const initialProjectId = ""; const initialProjectId = "";
// Prepare client metadata // Prepare client metadata
@ -948,7 +949,7 @@ export class AntigravityApiService {
// Check if we already have a project ID from the response // Check if we already have a project ID from the response
if (loadResponse.cloudaicompanionProject) { if (loadResponse.cloudaicompanionProject) {
console.log(`[Antigravity] Discovered existing Project ID: ${loadResponse.cloudaicompanionProject}`); logger.info(`[Antigravity] Discovered existing Project ID: ${loadResponse.cloudaicompanionProject}`);
// 获取可用模型 // 获取可用模型
await this.fetchAvailableModels(); await this.fetchAvailableModels();
return loadResponse.cloudaicompanionProject; return loadResponse.cloudaicompanionProject;
@ -981,15 +982,15 @@ export class AntigravityApiService {
} }
const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId; 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(); await this.fetchAvailableModels();
return discoveredProjectId; return discoveredProjectId;
} catch (error) { } catch (error) {
console.error('[Antigravity] Failed to discover Project ID:', error.response?.data || error.message); logger.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.info('[Antigravity] Falling back to generated Project ID as last resort...');
const fallbackProjectId = generateProjectID(); const fallbackProjectId = generateProjectID();
console.log(`[Antigravity] Generated fallback Project ID: ${fallbackProjectId}`); logger.info(`[Antigravity] Generated fallback Project ID: ${fallbackProjectId}`);
// 获取可用模型 // 获取可用模型
await this.fetchAvailableModels(); await this.fetchAvailableModels();
return fallbackProjectId; return fallbackProjectId;
@ -997,7 +998,7 @@ export class AntigravityApiService {
} }
async fetchAvailableModels() { async fetchAvailableModels() {
console.log('[Antigravity] Fetching available models...'); logger.info('[Antigravity] Fetching available models...');
for (const baseURL of this.baseURLs) { for (const baseURL of this.baseURLs) {
try { try {
@ -1014,7 +1015,7 @@ export class AntigravityApiService {
}; };
const res = await this.authClient.request(requestOptions); 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) { if (res.data && res.data.models) {
const models = Object.keys(res.data.models); const models = Object.keys(res.data.models);
this.availableModels = models this.availableModels = models
@ -1022,15 +1023,15 @@ export class AntigravityApiService {
.filter(alias => alias !== undefined && alias !== '' && alias !== null) .filter(alias => alias !== undefined && alias !== '' && alias !== null)
.filter(alias => ANTIGRAVITY_MODELS.includes(alias)); .filter(alias => ANTIGRAVITY_MODELS.includes(alias));
console.log(`[Antigravity] Available models: [${this.availableModels.join(', ')}]`); logger.info(`[Antigravity] Available models: [${this.availableModels.join(', ')}]`);
return; return;
} }
} catch (error) { } 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; this.availableModels = ANTIGRAVITY_MODELS;
} }
@ -1104,15 +1105,15 @@ export class AntigravityApiService {
// 检查是否为可重试的网络错误 // 检查是否为可重试的网络错误
const isNetworkError = isRetryableNetworkError(error); 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) { 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(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.ANTIGRAVITY, {
uuid: this.uuid uuid: this.uuid
}); });
@ -1127,11 +1128,11 @@ export class AntigravityApiService {
if (status === 429) { if (status === 429) {
if (baseURLIndex + 1 < this.baseURLs.length) { 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); return this.callApi(method, body, isRetry, retryCount, baseURLIndex + 1);
} else if (retryCount < maxRetries) { } else if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(method, body, isRetry, retryCount + 1, 0); return this.callApi(method, body, isRetry, retryCount + 1, 0);
} }
@ -1141,12 +1142,12 @@ export class AntigravityApiService {
if (isNetworkError) { if (isNetworkError) {
if (baseURLIndex + 1 < this.baseURLs.length) { if (baseURLIndex + 1 < this.baseURLs.length) {
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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); return this.callApi(method, body, isRetry, retryCount, baseURLIndex + 1);
} else if (retryCount < maxRetries) { } else if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(method, body, isRetry, retryCount + 1, 0); return this.callApi(method, body, isRetry, retryCount + 1, 0);
} }
@ -1154,7 +1155,7 @@ export class AntigravityApiService {
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(method, body, isRetry, retryCount + 1, baseURLIndex); return this.callApi(method, body, isRetry, retryCount + 1, baseURLIndex);
} }
@ -1206,15 +1207,15 @@ export class AntigravityApiService {
// 检查是否为可重试的网络错误 // 检查是否为可重试的网络错误
const isNetworkError = isRetryableNetworkError(error); 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) { 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(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.ANTIGRAVITY, {
uuid: this.uuid uuid: this.uuid
}); });
@ -1229,12 +1230,12 @@ export class AntigravityApiService {
if (status === 429) { if (status === 429) {
if (baseURLIndex + 1 < this.baseURLs.length) { 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); yield* this.streamApi(method, body, isRetry, retryCount, baseURLIndex + 1);
return; return;
} else if (retryCount < maxRetries) { } else if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(method, body, isRetry, retryCount + 1, 0); yield* this.streamApi(method, body, isRetry, retryCount + 1, 0);
return; return;
@ -1245,13 +1246,13 @@ export class AntigravityApiService {
if (isNetworkError) { if (isNetworkError) {
if (baseURLIndex + 1 < this.baseURLs.length) { if (baseURLIndex + 1 < this.baseURLs.length) {
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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); yield* this.streamApi(method, body, isRetry, retryCount, baseURLIndex + 1);
return; return;
} else if (retryCount < maxRetries) { } else if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(method, body, isRetry, retryCount + 1, 0); yield* this.streamApi(method, body, isRetry, retryCount + 1, 0);
return; return;
@ -1260,7 +1261,7 @@ export class AntigravityApiService {
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(method, body, isRetry, retryCount + 1, baseURLIndex); yield* this.streamApi(method, body, isRetry, retryCount + 1, baseURLIndex);
return; return;
@ -1286,7 +1287,7 @@ export class AntigravityApiService {
try { try {
yield JSON.parse(buffer.join('\n')); yield JSON.parse(buffer.join('\n'));
} catch (e) { } 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 = []; buffer = [];
} }
@ -1296,19 +1297,19 @@ export class AntigravityApiService {
try { try {
yield JSON.parse(buffer.join('\n')); yield JSON.parse(buffer.join('\n'));
} catch (e) { } 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) { 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 是否即将过期,如果是则推送到刷新队列 // 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.ANTIGRAVITY, {
uuid: this.uuid uuid: this.uuid
}); });
@ -1317,7 +1318,7 @@ export class AntigravityApiService {
let selectedModel = model; let selectedModel = model;
if (!this.availableModels.includes(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]; selectedModel = this.availableModels[0];
} }
@ -1363,19 +1364,19 @@ export class AntigravityApiService {
const nonStreamResponse = convertStreamToNonStream(streamData); const nonStreamResponse = convertStreamToNonStream(streamData);
return toGeminiApiResponse(nonStreamResponse.response); return toGeminiApiResponse(nonStreamResponse.response);
} catch (error) { } catch (error) {
console.error('[Antigravity] Claude non-stream execution error:', error.message); logger.error('[Antigravity] Claude non-stream execution error:', error.message);
throw error; throw error;
} }
} }
async * generateContentStream(model, requestBody) { 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 是否即将过期,如果是则推送到刷新队列 // 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.ANTIGRAVITY, {
uuid: this.uuid uuid: this.uuid
}); });
@ -1384,7 +1385,7 @@ export class AntigravityApiService {
let selectedModel = model; let selectedModel = model;
if (!this.availableModels.includes(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]; selectedModel = this.availableModels[0];
} }
@ -1408,10 +1409,10 @@ export class AntigravityApiService {
try { try {
const nearMinutes = 20; const nearMinutes = 20;
const { message, isNearExpiry } = formatExpiryLog('Antigravity', this.authClient.credentials.expiry_date, nearMinutes); const { message, isNearExpiry } = formatExpiryLog('Antigravity', this.authClient.credentials.expiry_date, nearMinutes);
console.log(message); logger.info(message);
return isNearExpiry; return isNearExpiry;
} catch (error) { } catch (error) {
console.error(`[Antigravity] Error checking expiry date: ${error.message}`); logger.error(`[Antigravity] Error checking expiry date: ${error.message}`);
return false; return false;
} }
} }
@ -1426,7 +1427,7 @@ export class AntigravityApiService {
// 注意V2 架构下不再在 getUsageLimits 中同步刷新 token // 注意V2 架构下不再在 getUsageLimits 中同步刷新 token
// 如果 token 过期PoolManager 后台会自动处理 // 如果 token 过期PoolManager 后台会自动处理
// if (this.isExpiryDateNear()) { // 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); // await this.initializeAuth(true);
// } // }
@ -1434,7 +1435,7 @@ export class AntigravityApiService {
const modelsWithQuotas = await this.getModelsWithQuotas(); const modelsWithQuotas = await this.getModelsWithQuotas();
return modelsWithQuotas; return modelsWithQuotas;
} catch (error) { } catch (error) {
console.error('[Antigravity] Failed to get usage limits:', error.message); logger.error('[Antigravity] Failed to get usage limits:', error.message);
throw error; throw error;
} }
} }
@ -1467,7 +1468,7 @@ export class AntigravityApiService {
}; };
const res = await this.authClient.request(requestOptions); const res = await this.authClient.request(requestOptions);
console.log(`[Antigravity] fetchAvailableModels success`); logger.info(`[Antigravity] fetchAvailableModels success`);
if (res.data && res.data.models) { if (res.data && res.data.models) {
const modelsData = res.data.models; const modelsData = res.data.models;
@ -1498,19 +1499,19 @@ export class AntigravityApiService {
sortedModels[key] = result.models[key]; sortedModels[key] = result.models[key];
}); });
result.models = sortedModels; 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; // 成功获取后退出循环 break; // 成功获取后退出循环
} }
} catch (error) { } 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; return result;
} catch (error) { } 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; throw error;
} }
} }
} }

View file

@ -1,4 +1,5 @@
import { OAuth2Client } from 'google-auth-library'; import { OAuth2Client } from 'google-auth-library';
import logger from '../../utils/logger.js';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
@ -207,7 +208,7 @@ export class GeminiApiService {
if (proxyConfig) { if (proxyConfig) {
oauth2Options.transporterOptions = proxyConfig; oauth2Options.transporterOptions = proxyConfig;
console.log('[Gemini] Using proxy for OAuth2Client'); logger.info('[Gemini] Using proxy for OAuth2Client');
} else { } else {
oauth2Options.transporterOptions = { oauth2Options.transporterOptions = {
agent: httpsAgent, agent: httpsAgent,
@ -233,7 +234,7 @@ export class GeminiApiService {
async initialize() { async initialize() {
if (this.isInitialized) return; if (this.isInitialized) return;
console.log('[Gemini] Initializing Gemini API Service...'); logger.info('[Gemini] Initializing Gemini API Service...');
// 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑
// 仅执行基础的凭证加载 // 仅执行基础的凭证加载
await this.loadCredentials(); await this.loadCredentials();
@ -241,15 +242,15 @@ export class GeminiApiService {
if (!this.projectId) { if (!this.projectId) {
this.projectId = await this.discoverProjectAndModels(); this.projectId = await this.discoverProjectAndModels();
} else { } else {
console.log(`[Gemini] Using provided Project ID: ${this.projectId}`); logger.info(`[Gemini] Using provided Project ID: ${this.projectId}`);
this.availableModels = GEMINI_MODELS; 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') { 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."); 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; 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 decoded = Buffer.from(this.oauthCredsBase64, 'base64').toString('utf8');
const credentials = JSON.parse(decoded); const credentials = JSON.parse(decoded);
this.authClient.setCredentials(credentials); 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; return;
} catch (error) { } 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 data = await fs.readFile(credPath, "utf8");
const credentials = JSON.parse(data); const credentials = JSON.parse(data);
this.authClient.setCredentials(credentials); this.authClient.setCredentials(credentials);
console.log('[Gemini Auth] Credentials loaded successfully from file.'); logger.info('[Gemini Auth] Credentials loaded successfully from file.');
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
console.debug(`[Gemini Auth] Credentials file not found: ${credPath}`); logger.debug(`[Gemini Auth] Credentials file not found: ${credPath}`);
} else { } 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); const credPath = this.oauthCredsFilePath || path.join(os.homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE);
try { try {
if (this.authClient.credentials.refresh_token) { 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(); const { credentials: newCredentials } = await this.authClient.refreshAccessToken();
this.authClient.setCredentials(newCredentials); this.authClient.setCredentials(newCredentials);
// 如果不是从 base64 加载的,则保存到文件 // 如果不是从 base64 加载的,则保存到文件
if (!this.oauthCredsBase64) { if (!this.oauthCredsBase64) {
await this._saveCredentialsToFile(credPath, newCredentials); await this._saveCredentialsToFile(credPath, newCredentials);
console.log('[Gemini Auth] Token refreshed and saved successfully.'); logger.info('[Gemini Auth] Token refreshed and saved successfully.');
} else { } else {
console.log('[Gemini Auth] Token refreshed successfully (Base64 source).'); logger.info('[Gemini Auth] Token refreshed successfully (Base64 source).');
} }
// 刷新成功,重置 PoolManager 中的刷新状态并标记为健康 // 刷新成功,重置 PoolManager 中的刷新状态并标记为健康
@ -319,10 +320,10 @@ export class GeminiApiService {
poolManager.resetProviderRefreshStatus(MODEL_PROVIDER.GEMINI_CLI, this.uuid); poolManager.resetProviderRefreshStatus(MODEL_PROVIDER.GEMINI_CLI, this.uuid);
} }
} else { } 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); const newTokens = await this.getNewToken(credPath);
this.authClient.setCredentials(newTokens); 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(); const poolManager = getProviderPoolManager();
@ -331,7 +332,7 @@ export class GeminiApiService {
} }
} }
} catch (error) { } 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.`); throw new Error(`Failed to load OAuth credentials.`);
} }
} }
@ -341,12 +342,12 @@ export class GeminiApiService {
// 使用统一的 OAuth 处理方法 // 使用统一的 OAuth 处理方法
const { authUrl, authInfo } = await handleGeminiCliOAuth(this.config); const { authUrl, authInfo } = await handleGeminiCliOAuth(this.config);
console.log('\n[Gemini Auth] 正在自动打开浏览器进行授权...'); logger.info('\n[Gemini Auth] 正在自动打开浏览器进行授权...');
console.log('[Gemini Auth] 授权链接:', authUrl, '\n'); logger.info('[Gemini Auth] 授权链接:', authUrl, '\n');
// 自动打开浏览器 // 自动打开浏览器
const showFallbackMessage = () => { const showFallbackMessage = () => {
console.log('[Gemini Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开'); logger.info('[Gemini Auth] 无法自动打开浏览器,请手动复制上面的链接到浏览器中打开');
}; };
if (this.config) { if (this.config) {
@ -370,7 +371,7 @@ export class GeminiApiService {
const credentials = JSON.parse(data); const credentials = JSON.parse(data);
if (credentials.access_token) { if (credentials.access_token) {
clearInterval(checkInterval); clearInterval(checkInterval);
console.log('[Gemini Auth] New token obtained successfully.'); logger.info('[Gemini Auth] New token obtained successfully.');
resolve(credentials); resolve(credentials);
} }
} catch (error) { } catch (error) {
@ -388,13 +389,13 @@ export class GeminiApiService {
async discoverProjectAndModels() { async discoverProjectAndModels() {
if (this.projectId) { 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; return this.projectId;
} }
console.log('[Gemini] Discovering Project ID...'); logger.info('[Gemini] Discovering Project ID...');
this.availableModels = GEMINI_MODELS; this.availableModels = GEMINI_MODELS;
console.log(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`); logger.info(`[Gemini] Using fixed models: [${this.availableModels.join(', ')}]`);
try { try {
const initialProjectId = "" const initialProjectId = ""
// Prepare client metadata // Prepare client metadata
@ -447,7 +448,7 @@ export class GeminiApiService {
const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId; const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId;
return discoveredProjectId; return discoveredProjectId;
} catch (error) { } 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.'); throw new Error('Could not discover a valid Google Cloud Project ID.');
} }
} }
@ -488,16 +489,16 @@ export class GeminiApiService {
// 检查是否为可重试的网络错误 // 检查是否为可重试的网络错误
const isNetworkError = isRetryableNetworkError(error); 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 // Handle 401 (Unauthorized) - refresh auth and retry once
if ((status === 400 || status === 401) && !isRetry) { 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(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.GEMINI_CLI, {
uuid: this.uuid uuid: this.uuid
}); });
@ -513,7 +514,7 @@ export class GeminiApiService {
// Handle 429 (Too Many Requests) with exponential backoff // Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) { if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(method, body, isRetry, retryCount + 1); return this.callApi(method, body, isRetry, retryCount + 1);
} }
@ -521,7 +522,7 @@ export class GeminiApiService {
// Handle other retryable errors (5xx server errors) // Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(method, body, isRetry, retryCount + 1); return this.callApi(method, body, isRetry, retryCount + 1);
} }
@ -530,7 +531,7 @@ export class GeminiApiService {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(method, body, isRetry, retryCount + 1); return this.callApi(method, body, isRetry, retryCount + 1);
} }
@ -567,16 +568,16 @@ export class GeminiApiService {
// 检查是否为可重试的网络错误 // 检查是否为可重试的网络错误
const isNetworkError = isRetryableNetworkError(error); 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 // Handle 401 (Unauthorized) - refresh auth and retry once
if ((status === 400 || status === 401) && !isRetry) { 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(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.GEMINI_CLI, {
uuid: this.uuid uuid: this.uuid
}); });
@ -592,7 +593,7 @@ export class GeminiApiService {
// Handle 429 (Too Many Requests) with exponential backoff // Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) { if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(method, body, isRetry, retryCount + 1); yield* this.streamApi(method, body, isRetry, retryCount + 1);
return; return;
@ -601,7 +602,7 @@ export class GeminiApiService {
// Handle other retryable errors (5xx server errors) // Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(method, body, isRetry, retryCount + 1); yield* this.streamApi(method, body, isRetry, retryCount + 1);
return; return;
@ -611,7 +612,7 @@ export class GeminiApiService {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(method, body, isRetry, retryCount + 1); yield* this.streamApi(method, body, isRetry, retryCount + 1);
return; return;
@ -627,23 +628,23 @@ export class GeminiApiService {
for await (const line of rl) { for await (const line of rl) {
if (line.startsWith("data: ")) buffer.push(line.slice(6)); if (line.startsWith("data: ")) buffer.push(line.slice(6));
else if (line === "" && buffer.length > 0) { 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 = []; buffer = [];
} }
} }
if (buffer.length > 0) { 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) { 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 是否即将过期,如果是则推送到刷新队列 // 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.GEMINI_CLI, {
uuid: this.uuid uuid: this.uuid
}); });
@ -652,7 +653,7 @@ export class GeminiApiService {
let selectedModel = model; let selectedModel = model;
if (!GEMINI_MODELS.includes(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]; selectedModel = GEMINI_MODELS[0];
} }
const processedRequestBody = ensureRolesInContents(requestBody); const processedRequestBody = ensureRolesInContents(requestBody);
@ -662,13 +663,13 @@ export class GeminiApiService {
} }
async * generateContentStream(model, requestBody) { 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 是否即将过期,如果是则推送到刷新队列 // 检查 token 是否即将过期,如果是则推送到刷新队列
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.GEMINI_CLI, {
uuid: this.uuid uuid: this.uuid
}); });
@ -686,7 +687,7 @@ export class GeminiApiService {
// 正常流处理 // 正常流处理
let selectedModel = model; let selectedModel = model;
if (!GEMINI_MODELS.includes(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]; selectedModel = GEMINI_MODELS[0];
} }
const processedRequestBody = ensureRolesInContents(requestBody); const processedRequestBody = ensureRolesInContents(requestBody);
@ -706,10 +707,10 @@ export class GeminiApiService {
try { try {
const nearMinutes = 20; const nearMinutes = 20;
const { message, isNearExpiry } = formatExpiryLog('Gemini', this.authClient.credentials.expiry_date, nearMinutes); const { message, isNearExpiry } = formatExpiryLog('Gemini', this.authClient.credentials.expiry_date, nearMinutes);
console.log(message); logger.info(message);
return isNearExpiry; return isNearExpiry;
} catch (error) { } catch (error) {
console.error(`[Gemini] Error checking expiry date: ${error.message}`); logger.error(`[Gemini] Error checking expiry date: ${error.message}`);
return false; return false;
} }
} }
@ -722,9 +723,9 @@ export class GeminiApiService {
async _saveCredentialsToFile(filePath, credentials) { async _saveCredentialsToFile(filePath, credentials) {
try { try {
await fs.writeFile(filePath, JSON.stringify(credentials, null, 2)); 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) { } 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; throw error;
} }
} }
@ -739,7 +740,7 @@ export class GeminiApiService {
// 注意V2 架构下不再在 getUsageLimits 中同步刷新 token // 注意V2 架构下不再在 getUsageLimits 中同步刷新 token
// 如果 token 过期PoolManager 后台会自动处理 // 如果 token 过期PoolManager 后台会自动处理
// if (this.isExpiryDateNear()) { // 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); // await this.initializeAuth(true);
// } // }
@ -747,7 +748,7 @@ export class GeminiApiService {
const modelsWithQuotas = await this.getModelsWithQuotas(); const modelsWithQuotas = await this.getModelsWithQuotas();
return modelsWithQuotas; return modelsWithQuotas;
} catch (error) { } catch (error) {
console.error('[Gemini] Failed to get usage limits:', error.message); logger.error('[Gemini] Failed to get usage limits:', error.message);
throw error; throw error;
} }
} }
@ -781,7 +782,7 @@ export class GeminiApiService {
}; };
const res = await this.authClient.request(requestOptions); 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) { if (res.data && res.data.buckets) {
const buckets = res.data.buckets; const buckets = res.data.buckets;
@ -807,11 +808,11 @@ export class GeminiApiService {
sortedModels[key] = result.models[key]; sortedModels[key] = result.models[key];
}); });
result.models = sortedModels; result.models = sortedModels;
// console.log(`[Gemini] Sorted Models:`, sortedModels); // logger.info(`[Gemini] Sorted Models:`, sortedModels);
console.log(`[Gemini] Successfully fetched quotas for ${Object.keys(result.models).length} models`); logger.info(`[Gemini] Successfully fetched quotas for ${Object.keys(result.models).length} models`);
} }
} catch (fetchError) { } catch (fetchError) {
console.error(`[Gemini] Failed to fetch user quota:`, fetchError.message); logger.error(`[Gemini] Failed to fetch user quota:`, fetchError.message);
// 如果 retrieveUserQuota 失败,回退到使用固定模型列表 // 如果 retrieveUserQuota 失败,回退到使用固定模型列表
for (const modelId of GEMINI_MODELS) { for (const modelId of GEMINI_MODELS) {
@ -825,8 +826,9 @@ export class GeminiApiService {
return result; return result;
} catch (error) { } 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; throw error;
} }
} }
} }

View file

@ -1,4 +1,5 @@
import { API_ACTIONS, extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; import { API_ACTIONS, extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import logger from '../../utils/logger.js';
import { ProviderStrategy } from '../../utils/provider-strategy.js'; import { ProviderStrategy } from '../../utils/provider-strategy.js';
/** /**
@ -55,7 +56,7 @@ class GeminiStrategy extends ProviderStrategy {
if (requestBody.system_instruction) { if (requestBody.system_instruction) {
delete 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; return requestBody;
} }
@ -67,3 +68,4 @@ class GeminiStrategy extends ProviderStrategy {
} }
export { GeminiStrategy }; export { GeminiStrategy };

View file

@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import logger from '../../utils/logger.js';
import crypto from 'crypto'; import crypto from 'crypto';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
@ -32,13 +33,13 @@ export class CodexApiService {
*/ */
async initialize() { async initialize() {
if (this.isInitialized) return; if (this.isInitialized) return;
console.log('[Codex] Initializing Codex API Service...'); logger.info('[Codex] Initializing Codex API Service...');
// 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑
// 仅执行基础的凭证加载 // 仅执行基础的凭证加载
await this.loadCredentials(); await this.loadCredentials();
this.isInitialized = true; 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 是否需要刷新 // 检查 token 是否需要刷新
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
console.log('[Codex] Token expiring soon, refreshing...'); logger.info('[Codex] Token expiring soon, refreshing...');
await this.refreshAccessToken(); await this.refreshAccessToken();
} }
this.isInitialized = true; this.isInitialized = true;
console.log(`[Codex] Initialized with account: ${this.email}`); logger.info(`[Codex] Initialized with account: ${this.email}`);
} catch (error) { } 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) { if (!this.refreshToken) {
throw new Error('Codex credentials not found. Please authenticate first using OAuth.'); 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(); await this.refreshAccessToken();
// 刷新成功,重置 PoolManager 中的刷新状态并标记为健康 // 刷新成功,重置 PoolManager 中的刷新状态并标记为健康
@ -137,7 +138,7 @@ export class CodexApiService {
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -157,12 +158,12 @@ export class CodexApiService {
return this.parseNonStreamResponse(response.data); return this.parseNonStreamResponse(response.data);
} catch (error) { } catch (error) {
if (error.response?.status === 401) { 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(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -190,7 +191,7 @@ export class CodexApiService {
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -211,12 +212,12 @@ export class CodexApiService {
yield* this.parseSSEStream(response.data); yield* this.parseSSEStream(response.data);
} catch (error) { } catch (error) {
if (error.response?.status === 401) { 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(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -293,9 +294,9 @@ export class CodexApiService {
// 保存更新的凭据 // 保存更新的凭据
await this.saveCredentials(); await this.saveCredentials();
console.log('[Codex] Token refreshed successfully'); logger.info('[Codex] Token refreshed successfully');
} catch (error) { } 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.'); throw new Error('Failed to refresh Codex token. Please re-authenticate.');
} }
} }
@ -308,7 +309,7 @@ export class CodexApiService {
const expiry = this.expiresAt.getTime(); const expiry = this.expiresAt.getTime();
const nearMinutes = 20; const nearMinutes = 20;
const { message, isNearExpiry } = formatExpiryLog('Codex', expiry, nearMinutes); const { message, isNearExpiry } = formatExpiryLog('Codex', expiry, nearMinutes);
console.log(message); logger.info(message);
return isNearExpiry; return isNearExpiry;
} }
@ -379,7 +380,7 @@ export class CodexApiService {
const parsed = JSON.parse(data); const parsed = JSON.parse(data);
yield parsed; yield parsed;
} catch (e) { } 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); const parsed = JSON.parse(data);
yield parsed; yield parsed;
} catch (e) { } 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 {
} }
} }
} }

View file

@ -17,6 +17,7 @@
*/ */
import axios from 'axios'; import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
@ -115,12 +116,12 @@ async function loadTokenFromFile(filePath) {
// 记录加载的 token 信息 // 记录加载的 token 信息
const refreshToken = json.refreshToken || json.refresh_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); return IFlowTokenStorage.fromJSON(json);
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
console.warn(`[iFlow] Token file not found: ${filePath}`); logger.warn(`[iFlow] Token file not found: ${filePath}`);
return null; return null;
} }
throw new Error(`[iFlow] Failed to load token from file: ${error.message}`); 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() === '') { 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() === '') { 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'); 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) { } catch (error) {
throw new Error(`[iFlow] Failed to save token to file: ${error.message}`); 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'); throw new Error('[iFlow] refresh_token is empty');
} }
console.log('[iFlow] Refreshing OAuth tokens...'); logger.info('[iFlow] Refreshing OAuth tokens...');
// 构建请求参数 // 构建请求参数
const params = new URLSearchParams(); const params = new URLSearchParams();
@ -205,9 +206,9 @@ async function refreshOAuthTokens(refreshToken, axiosInstance = null) {
const tokenResp = response.data; const tokenResp = response.data;
// console.log('[iFlow] Token response:', JSON.stringify(tokenResp)); // logger.info('[iFlow] Token response:', JSON.stringify(tokenResp));
if (!tokenResp.access_token) { 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'); throw new Error('[iFlow] Missing access_token in response');
} }
@ -223,7 +224,7 @@ async function refreshOAuthTokens(refreshToken, axiosInstance = null) {
expiryDate: expireTimestamp // 毫秒级时间戳 expiryDate: expireTimestamp // 毫秒级时间戳
}; };
console.log('[iFlow] OAuth tokens refreshed successfully'); logger.info('[iFlow] OAuth tokens refreshed successfully');
// 获取用户信息以获取 API Key // 获取用户信息以获取 API Key
const userInfo = await fetchUserInfo(tokenData.accessToken, axiosInstance); const userInfo = await fetchUserInfo(tokenData.accessToken, axiosInstance);
@ -236,7 +237,7 @@ async function refreshOAuthTokens(refreshToken, axiosInstance = null) {
} catch (error) { } catch (error) {
const status = error.response?.status; const status = error.response?.status;
const data = error.response?.data; 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; throw error;
} }
} }
@ -269,7 +270,7 @@ async function fetchUserInfo(accessToken, axiosInstance = null) {
: await axios.request(requestConfig); : await axios.request(requestConfig);
const result = response.data; 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) { if (!result.success) {
throw new Error('[iFlow] User info request not successful'); throw new Error('[iFlow] User info request not successful');
} }
@ -286,7 +287,7 @@ async function fetchUserInfo(accessToken, axiosInstance = null) {
} catch (error) { } catch (error) {
const status = error.response?.status; const status = error.response?.status;
const data = error.response?.data; 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; throw error;
} }
} }
@ -382,7 +383,7 @@ function preserveReasoningContentInMessages(body, model) {
); );
if (hasReasoningContent) { 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; return body;
@ -493,13 +494,13 @@ export class IFlowApiService {
async initialize() { async initialize() {
if (this.isInitialized) return; if (this.isInitialized) return;
console.log('[iFlow] Initializing iFlow API Service...'); logger.info('[iFlow] Initializing iFlow API Service...');
// 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑
// 仅执行基础的凭证加载 // 仅执行基础的凭证加载
await this.loadCredentials(); await this.loadCredentials();
this.isInitialized = true; 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; this.apiKey = this.tokenStorage.apiKey;
// 更新 axios 实例的 Authorization header // 更新 axios 实例的 Authorization header
this.axiosInstance.defaults.headers['Authorization'] = `Bearer ${this.apiKey}`; 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) { } 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) { if (!this.tokenStorage) {
this.tokenStorage = await loadTokenFromFile(this.tokenFilePath); 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) { if (this.tokenStorage && this.tokenStorage.apiKey) {
this.apiKey = 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) { if (forceRefresh) {
console.log('[iFlow Auth] Forcing token refresh...'); logger.info('[iFlow Auth] Forcing token refresh...');
await this._refreshOAuthTokens(); await this._refreshOAuthTokens();
console.log('[iFlow Auth] Token refreshed and saved successfully.'); logger.info('[iFlow Auth] Token refreshed and saved successfully.');
// 刷新成功,重置 PoolManager 中的刷新状态并标记为健康 // 刷新成功,重置 PoolManager 中的刷新状态并标记为健康
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
@ -562,7 +563,7 @@ export class IFlowApiService {
throw new Error('[iFlow] No refresh token available in credentials.'); throw new Error('[iFlow] No refresh token available in credentials.');
} }
} catch (error) { } 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.`); throw new Error(`[iFlow Auth] Failed to load OAuth credentials.`);
} }
} }
@ -578,23 +579,23 @@ export class IFlowApiService {
// 检查是否有 refresh_token // 检查是否有 refresh_token
if (!this.tokenStorage.refreshToken || this.tokenStorage.refreshToken.trim() === '') { 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; return false;
} }
// 使用 isExpiryDateNear 检查过期时间 // 使用 isExpiryDateNear 检查过期时间
// if (!this.isExpiryDateNear()) { // if (!this.isExpiryDateNear()) {
// console.log('[iFlow] Token is valid, no refresh needed'); // logger.info('[iFlow] Token is valid, no refresh needed');
// return false; // return false;
// } // }
console.log('[iFlow] Token is expiring soon, attempting refresh...'); logger.info('[iFlow] Token is expiring soon, attempting refresh...');
try { try {
await this._refreshOAuthTokens(); await this._refreshOAuthTokens();
return true; return true;
} catch (error) { } catch (error) {
console.error('[iFlow] Token refresh failed:', error.message); logger.error('[iFlow] Token refresh failed:', error.message);
// 刷新失败不抛出异常,继续使用现有 Token // 刷新失败不抛出异常,继续使用现有 Token
return false; return false;
} }
@ -611,7 +612,7 @@ export class IFlowApiService {
const oldAccessToken = this.tokenStorage.accessToken; const oldAccessToken = this.tokenStorage.accessToken;
if (oldAccessToken) { 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 是否发生变化 // 记录 refresh_token 是否发生变化
if (tokenData.refreshToken !== oldRefreshToken) { 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) { if (tokenData.apiKey) {
this.tokenStorage.apiKey = tokenData.apiKey; this.tokenStorage.apiKey = tokenData.apiKey;
@ -644,7 +645,7 @@ export class IFlowApiService {
// 保存到文件 // 保存到文件
await saveTokenToFile(this.tokenFilePath, this.tokenStorage, this.uuid); 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(); await this._refreshOAuthTokens();
return true; return true;
} catch (error) { } catch (error) {
console.error('[iFlow] Manual token refresh failed:', error.message); logger.error('[iFlow] Manual token refresh failed:', error.message);
return false; return false;
} }
} }
@ -709,21 +710,21 @@ export class IFlowApiService {
expireTime = new Date(expireValue.replace(' ', 'T') + ':00').getTime(); expireTime = new Date(expireValue.replace(' ', 'T') + ':00').getTime();
} }
} else { } else {
console.error(`[iFlow] Invalid expiry date type: ${typeof expireValue}`); logger.error(`[iFlow] Invalid expiry date type: ${typeof expireValue}`);
return false; return false;
} }
if (isNaN(expireTime)) { if (isNaN(expireTime)) {
console.error(`[iFlow] Error parsing expiry date: ${expireValue}`); logger.error(`[iFlow] Error parsing expiry date: ${expireValue}`);
return false; return false;
} }
const { message, isNearExpiry } = formatExpiryLog('iFlow', expireTime, cronNearMinutes); const { message, isNearExpiry } = formatExpiryLog('iFlow', expireTime, cronNearMinutes);
console.log(message); logger.info(message);
return isNearExpiry; return isNearExpiry;
} catch (error) { } catch (error) {
console.error(`[iFlow] Error checking expiry date: ${error.message}`); logger.error(`[iFlow] Error checking expiry date: ${error.message}`);
return false; return false;
} }
} }
@ -775,12 +776,12 @@ export class IFlowApiService {
// Handle 401/400 - refresh auth and retry once // Handle 401/400 - refresh auth and retry once
if ((status === 400 || status === 401) && !isRetry) { 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(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.IFLOW_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -794,14 +795,14 @@ export class IFlowApiService {
} }
if (status === 401 || status === 403) { 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; throw error;
} }
// Handle 429 (Too Many Requests) with exponential backoff // Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) { if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, model, isRetry, retryCount + 1); return this.callApi(endpoint, body, model, isRetry, retryCount + 1);
} }
@ -809,7 +810,7 @@ export class IFlowApiService {
// Handle other retryable errors (5xx server errors) // Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, model, isRetry, retryCount + 1); return this.callApi(endpoint, body, model, isRetry, retryCount + 1);
} }
@ -818,12 +819,12 @@ export class IFlowApiService {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, model, isRetry, retryCount + 1); 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; throw error;
} }
} }
@ -895,7 +896,7 @@ export class IFlowApiService {
yield parsedChunk; yield parsedChunk;
} catch (e) { } catch (e) {
// JSON 解析失败,记录警告但继续处理 // 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: 等) // 忽略其他 SSE 字段(如 event:, id:, retry: 等)
@ -917,7 +918,7 @@ export class IFlowApiService {
const parsedChunk = JSON.parse(jsonData); const parsedChunk = JSON.parse(jsonData);
yield parsedChunk; yield parsedChunk;
} catch (e) { } 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 // Handle 401/400 during stream - refresh auth and retry once
if ((status === 400 || status === 401) && !isRetry) { 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(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.IFLOW_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -952,14 +953,14 @@ export class IFlowApiService {
} }
if (status === 401 || status === 403) { 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; throw error;
} }
// Handle 429 (Too Many Requests) with exponential backoff // Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) { if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, model, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, model, isRetry, retryCount + 1);
return; return;
@ -968,7 +969,7 @@ export class IFlowApiService {
// Handle other retryable errors (5xx server errors) // Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, model, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, model, isRetry, retryCount + 1);
return; return;
@ -978,13 +979,13 @@ export class IFlowApiService {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, model, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, model, isRetry, retryCount + 1);
return; 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; throw error;
} }
} }
@ -1001,7 +1002,7 @@ export class IFlowApiService {
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.IFLOW_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -1023,7 +1024,7 @@ export class IFlowApiService {
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.IFLOW_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -1058,13 +1059,13 @@ export class IFlowApiService {
created: Math.floor(Date.now() / 1000), created: Math.floor(Date.now() / 1000),
owned_by: 'iflow' 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; return modelsData;
} catch (error) { } 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 // 返回默认模型列表,确保包含 glm-4.7
const defaultModels = [...IFLOW_MODELS]; const defaultModels = [...IFLOW_MODELS];
if (!defaultModels.includes('glm-4.7')) { if (!defaultModels.includes('glm-4.7')) {
@ -1097,4 +1098,4 @@ export {
preserveReasoningContentInMessages, preserveReasoningContentInMessages,
ensureToolsArray, ensureToolsArray,
preprocessRequestBody, preprocessRequestBody,
}; };

View file

@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js'; import { configureAxiosProxy } from '../../utils/proxy-utils.js';
@ -14,7 +15,7 @@ export class OpenAIApiService {
this.apiKey = config.OPENAI_API_KEY; this.apiKey = config.OPENAI_API_KEY;
this.baseUrl = config.OPENAI_BASE_URL; this.baseUrl = config.OPENAI_BASE_URL;
this.useSystemProxy = config?.USE_SYSTEM_PROXY_OPENAI ?? false; 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 限制连接池大小,避免资源泄漏 // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({ const httpAgent = new http.Agent({
@ -68,14 +69,14 @@ export class OpenAIApiService {
const isNetworkError = isRetryableNetworkError(error); const isNetworkError = isRetryableNetworkError(error);
if (status === 401 || status === 403) { 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; throw error;
} }
// Handle 429 (Too Many Requests) with exponential backoff // Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) { if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1); return this.callApi(endpoint, body, isRetry, retryCount + 1);
} }
@ -83,7 +84,7 @@ export class OpenAIApiService {
// Handle other retryable errors (5xx server errors) // Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1); return this.callApi(endpoint, body, isRetry, retryCount + 1);
} }
@ -92,12 +93,12 @@ export class OpenAIApiService {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1); 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; throw error;
} }
} }
@ -133,7 +134,7 @@ export class OpenAIApiService {
const parsedChunk = JSON.parse(jsonData); const parsedChunk = JSON.parse(jsonData);
yield parsedChunk; yield parsedChunk;
} catch (e) { } 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 === '') { } else if (line === '') {
// Empty line, end of an event // Empty line, end of an event
@ -150,14 +151,14 @@ export class OpenAIApiService {
const isNetworkError = isRetryableNetworkError(error); const isNetworkError = isRetryableNetworkError(error);
if (status === 401 || status === 403) { 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; throw error;
} }
// Handle 429 (Too Many Requests) with exponential backoff // Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) { if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return; return;
@ -166,7 +167,7 @@ export class OpenAIApiService {
// Handle other retryable errors (5xx server errors) // Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return; return;
@ -176,13 +177,13 @@ export class OpenAIApiService {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return; 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; throw error;
} }
} }
@ -202,8 +203,9 @@ export class OpenAIApiService {
} catch (error) { } catch (error) {
const status = error.response?.status; const status = error.response?.status;
const data = error.response?.data; 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; throw error;
} }
} }
} }

View file

@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js'; import { configureAxiosProxy } from '../../utils/proxy-utils.js';
@ -13,7 +14,7 @@ export class OpenAIResponsesApiService {
this.apiKey = config.OPENAI_API_KEY; this.apiKey = config.OPENAI_API_KEY;
this.baseUrl = config.OPENAI_BASE_URL || 'https://api.openai.com/v1'; this.baseUrl = config.OPENAI_BASE_URL || 'https://api.openai.com/v1';
this.useSystemProxy = config?.USE_SYSTEM_PROXY_OPENAI ?? false; 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 限制连接池大小,避免资源泄漏 // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({ const httpAgent = new http.Agent({
@ -61,14 +62,14 @@ export class OpenAIResponsesApiService {
const status = error.response?.status; const status = error.response?.status;
const data = error.response?.data; const data = error.response?.data;
if (status === 401 || status === 403) { 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; throw error;
} }
// Handle 429 (Too Many Requests) with exponential backoff // Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) { if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1); return this.callApi(endpoint, body, isRetry, retryCount + 1);
} }
@ -76,12 +77,12 @@ export class OpenAIResponsesApiService {
// Handle other retryable errors (5xx server errors) // Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1); 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; throw error;
} }
} }
@ -117,7 +118,7 @@ export class OpenAIResponsesApiService {
const parsedChunk = JSON.parse(jsonData); const parsedChunk = JSON.parse(jsonData);
yield parsedChunk; yield parsedChunk;
} catch (e) { } 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 === '') { } else if (line === '') {
// Empty line, end of an event // Empty line, end of an event
@ -128,14 +129,14 @@ export class OpenAIResponsesApiService {
const status = error.response?.status; const status = error.response?.status;
const data = error.response?.data; const data = error.response?.data;
if (status === 401 || status === 403) { 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; throw error;
} }
// Handle 429 (Too Many Requests) with exponential backoff // Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) { if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return; return;
@ -144,13 +145,13 @@ export class OpenAIResponsesApiService {
// Handle other retryable errors (5xx server errors) // Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) { if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return; 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; throw error;
} }
} }
@ -170,8 +171,8 @@ export class OpenAIResponsesApiService {
} catch (error) { } catch (error) {
const status = error.response?.status; const status = error.response?.status;
const data = error.response?.data; 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; throw error;
} }
} }
} }

View file

@ -1,4 +1,5 @@
import { ProviderStrategy } from '../../utils/provider-strategy.js'; import { ProviderStrategy } from '../../utils/provider-strategy.js';
import logger from '../../utils/logger.js';
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
/** /**
@ -96,7 +97,7 @@ class ResponsesAPIStrategy extends ProviderStrategy {
requestBody.instructions = filePromptContent; 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; return requestBody;
} }
@ -120,4 +121,4 @@ class ResponsesAPIStrategy extends ProviderStrategy {
} }
} }
export { ResponsesAPIStrategy }; export { ResponsesAPIStrategy };

View file

@ -1,4 +1,5 @@
import { ProviderStrategy } from '../../utils/provider-strategy.js'; import { ProviderStrategy } from '../../utils/provider-strategy.js';
import logger from '../../utils/logger.js';
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
/** /**
@ -69,16 +70,17 @@ class OpenAIStrategy extends ProviderStrategy {
} else { } else {
requestBody.messages.unshift({ role: 'system', content: newSystemText }); 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; return requestBody;
} }
async manageSystemPrompt(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); const incomingSystemText = extractSystemPromptFromRequestBody(requestBody, MODEL_PROTOCOL_PREFIX.OPENAI);
await this._updateSystemPromptFile(incomingSystemText, MODEL_PROTOCOL_PREFIX.OPENAI); await this._updateSystemPromptFile(incomingSystemText, MODEL_PROTOCOL_PREFIX.OPENAI);
} }
} }
export { OpenAIStrategy }; export { OpenAIStrategy };

View file

@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import logger from '../../utils/logger.js';
import crypto from 'crypto'; import crypto from 'crypto';
import path from 'node:path'; import path from 'node:path';
import { promises as fs, unlinkSync } from 'node:fs'; 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') { if (!useSystemProxy && typeof mergedOptions.agent === 'undefined') {
// 对于 Node.js fetch,我们可以通过设置 dispatcher 来控制代理 // 对于 Node.js fetch,我们可以通过设置 dispatcher 来控制代理
// 但这需要 undici 支持,这里我们先记录日志 // 但这需要 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); const response = await fetch(url, mergedOptions);
@ -188,13 +189,13 @@ export class QwenApiService {
this.oauthDeviceCodeEndpoint = `${oauthBaseUrl}/api/v1/oauth2/device/code`; this.oauthDeviceCodeEndpoint = `${oauthBaseUrl}/api/v1/oauth2/device/code`;
this.oauthTokenEndpoint = `${oauthBaseUrl}/api/v1/oauth2/token`; 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); this.qwenClient = new QwenOAuth2Client(config, this.useSystemProxy);
} }
async initialize() { async initialize() {
if (this.isInitialized) return; if (this.isInitialized) return;
console.log('[Qwen] Initializing Qwen API Service...'); logger.info('[Qwen] Initializing Qwen API Service...');
// 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑 // 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑
// 仅执行基础的凭证加载 // 仅执行基础的凭证加载
await this.loadCredentials(); await this.loadCredentials();
@ -234,7 +235,7 @@ export class QwenApiService {
this.currentAxiosInstance = axios.create(axiosConfig); this.currentAxiosInstance = axios.create(axiosConfig);
this.isInitialized = true; 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 creds = await fs.readFile(keyFile, 'utf-8');
const credentials = JSON.parse(creds); const credentials = JSON.parse(creds);
this.qwenClient.setCredentials(credentials); this.qwenClient.setCredentials(credentials);
console.log('[Qwen Auth] Credentials loaded successfully from file.'); logger.info('[Qwen Auth] Credentials loaded successfully from file.');
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
console.debug('[Qwen Auth] No cached credentials found.'); logger.debug('[Qwen Auth] No cached credentials found.');
} else { } 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, forceRefresh,
this.tokenManagerOptions, this.tokenManagerOptions,
); );
// console.log('credentials', credentials); // logger.info('credentials', credentials);
this.qwenClient.setCredentials(credentials); this.qwenClient.setCredentials(credentials);
// 如果执行了刷新或认证,重置状态 // 如果执行了刷新或认证,重置状态
@ -277,27 +278,27 @@ export class QwenApiService {
} }
} }
} catch (error) { } 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) { if (error instanceof TokenManagerError) {
switch (error.type) { switch (error.type) {
case TokenError.NO_REFRESH_TOKEN: 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; break;
case TokenError.REFRESH_FAILED: case TokenError.REFRESH_FAILED:
console.debug('Token refresh failed, proceeding with device flow'); logger.debug('Token refresh failed, proceeding with device flow');
break; break;
case TokenError.NETWORK_ERROR: 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; break;
default: 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 cached credentials are present and still valid, use them directly.
if (await this._loadCachedQwenCredentials(this.qwenClient)) { if (await this._loadCachedQwenCredentials(this.qwenClient)) {
console.log('[Qwen] Using cached OAuth credentials.'); logger.info('[Qwen] Using cached OAuth credentials.');
return; return;
} }
@ -355,10 +356,10 @@ export class QwenApiService {
}); });
const showFallbackMessage = () => { const showFallbackMessage = () => {
console.log('\n=== Qwen OAuth Device Authorization ==='); logger.info('\n=== Qwen OAuth Device Authorization ===');
console.log('Please visit the following URL in your browser to authorize:'); logger.info('Please visit the following URL in your browser to authorize:');
console.log(`\n${authUrl}\n`); logger.info(`\n${authUrl}\n`);
console.log('Waiting for authorization to complete...\n'); logger.info('Waiting for authorization to complete...\n');
}; };
if (config) { if (config) {
@ -375,7 +376,7 @@ export class QwenApiService {
} }
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'polling', 'Waiting for authorization...'); qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'polling', 'Waiting for authorization...');
console.debug('Waiting for authorization...\n'); logger.debug('Waiting for authorization...\n');
// 等待 OAuth 回调完成并读取保存的凭据 // 等待 OAuth 回调完成并读取保存的凭据
const credPath = this._getQwenCachedCredentialPath(); const credPath = this._getQwenCachedCredentialPath();
@ -386,7 +387,7 @@ export class QwenApiService {
const creds = JSON.parse(data); const creds = JSON.parse(data);
if (creds.access_token) { if (creds.access_token) {
clearInterval(checkInterval); clearInterval(checkInterval);
console.log('[Qwen Auth] New token obtained successfully.'); logger.info('[Qwen Auth] New token obtained successfully.');
resolve(creds); resolve(creds);
} }
} catch (error) { } catch (error) {
@ -405,7 +406,7 @@ export class QwenApiService {
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'success', 'Authentication successful! Access token obtained.'); qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'success', 'Authentication successful! Access token obtained.');
return { success: true }; return { success: true };
} catch (error) { } 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); qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', error.message);
return { success: false, reason: 'error' }; return { success: false, reason: 'error' };
} }
@ -439,9 +440,9 @@ export class QwenApiService {
await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.mkdir(path.dirname(filePath), { recursive: true });
const credString = JSON.stringify(credentials, null, 2); const credString = JSON.stringify(credentials, null, 2);
await fs.writeFile(filePath, credString); await fs.writeFile(filePath, credString);
console.log(`[Qwen Auth] Credentials cached to ${filePath}`); logger.info(`[Qwen Auth] Credentials cached to ${filePath}`);
} catch (error) { } 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) { } catch (error) {
if (this.isAuthError(error)) throw 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.'); 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 version = "0.2.1";
const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`;
console.log(`[QwenApiService] User-Agent: ${userAgent}`); logger.info(`[QwenApiService] User-Agent: ${userAgent}`);
try { try {
const { token, endpoint: qwenBaseUrl } = await this.getValidToken(); 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 // 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)) { 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; processedBody.model = QWEN_MODEL_LIST[0].id;
} }
@ -610,12 +611,12 @@ export class QwenApiService {
const isNetworkError = isRetryableNetworkError(error); const isNetworkError = isRetryableNetworkError(error);
if (this.isAuthError(error) && retryCount === 0) { 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(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.QWEN_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -630,7 +631,7 @@ export class QwenApiService {
if ((status === 429 || (status >= 500 && status < 600)) && retryCount < maxRetries) { if ((status === 429 || (status >= 500 && status < 600)) && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1); return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1);
} }
@ -639,12 +640,12 @@ export class QwenApiService {
if (isNetworkError && retryCount < maxRetries) { if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50); 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)); await new Promise(resolve => setTimeout(resolve, delay));
return this.callApiWithAuthAndRetry(endpoint, body, isStream, retryCount + 1); 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; throw error;
} }
} }
@ -654,7 +655,7 @@ export class QwenApiService {
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.QWEN_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -669,7 +670,7 @@ export class QwenApiService {
if (this.isExpiryDateNear()) { if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) { 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, { poolManager.markProviderNeedRefresh(MODEL_PROVIDER.QWEN_API, {
uuid: this.uuid uuid: this.uuid
}); });
@ -691,7 +692,7 @@ export class QwenApiService {
try { try {
yield JSON.parse(jsonData); yield JSON.parse(jsonData);
} catch (e) { } 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 nearMinutes = 20;
const { message, isNearExpiry } = formatExpiryLog('Qwen', credentials.expiry_date, nearMinutes); const { message, isNearExpiry } = formatExpiryLog('Qwen', credentials.expiry_date, nearMinutes);
console.log(message); logger.info(message);
return isNearExpiry; return isNearExpiry;
} catch (error) { } catch (error) {
console.error(`[Qwen] Error checking expiry date: ${error.message}`); logger.error(`[Qwen] Error checking expiry date: ${error.message}`);
return false; return false;
} }
} }
@ -890,7 +891,7 @@ class SharedTokenManager {
context.memoryCache.credentials = newCredentials; context.memoryCache.credentials = newCredentials;
qwenClient.setCredentials(newCredentials); qwenClient.setCredentials(newCredentials);
await this.saveCredentialsToFile(context, newCredentials); await this.saveCredentialsToFile(context, newCredentials);
console.log('[Qwen Auth] Token refresh response: ok'); logger.info('[Qwen Auth] Token refresh response: ok');
return newCredentials; return newCredentials;
} finally { } finally {
await this.releaseLock(context); await this.releaseLock(context);
@ -902,7 +903,7 @@ class SharedTokenManager {
if (error instanceof CredentialsClearRequiredError) { if (error instanceof CredentialsClearRequiredError) {
try { try {
await fs.unlink(context.credentialFilePath); 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 */ } } catch (_) { /* ignore */ }
throw error; // 重新抛出以便上层处理 throw error; // 重新抛出以便上层处理
} }
@ -926,7 +927,7 @@ class SharedTokenManager {
const stats = await fs.stat(context.credentialFilePath); const stats = await fs.stat(context.credentialFilePath);
context.memoryCache.fileModTime = stats.mtimeMs; context.memoryCache.fileModTime = stats.mtimeMs;
} catch (error) { } 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); await fs.unlink(context.lockFilePath);
} catch (error) { } catch (error) {
if (error.code !== 'ENOENT') { 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 {
} }
} }
} }

View file

@ -91,4 +91,4 @@ export function getProviderModels(providerType) {
*/ */
export function getAllProviderModels() { export function getAllProviderModels() {
return PROVIDER_MODELS; return PROVIDER_MODELS;
} }

View file

@ -1,6 +1,7 @@
import * as fs from 'fs'; // Import fs module import * as fs from 'fs';
import * as crypto from 'crypto'; // Import crypto module for UUID generation import * as crypto from 'crypto';
import { getServiceAdapter } from './adapter.js'; import { getServiceAdapter } from './adapter.js';
import logger from '../utils/logger.js';
import { MODEL_PROVIDER, getProtocolPrefix } from '../utils/common.js'; import { MODEL_PROVIDER, getProtocolPrefix } from '../utils/common.js';
import { getProviderModels } from './provider-models.js'; import { getProviderModels } from './provider-models.js';
import axios from 'axios'; import axios from 'axios';
@ -104,7 +105,7 @@ export class ProviderPoolManager {
configPath = config.CODEX_OAUTH_CREDS_FILE_PATH; 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; if (!config.isHealthy || config.isDisabled) continue;
@ -411,11 +412,10 @@ export class ProviderPoolManager {
// 1. 基础健康分:不健康的排最后 // 1. 基础健康分:不健康的排最后
if (!config.isHealthy || config.isDisabled) return 1e18; if (!config.isHealthy || config.isDisabled) return 1e18;
// 2. 预热/刷新分2分钟内刷新过且使用次数极少的节点视为“新鲜”分数极低最高优 // 2. 预热/刷新分60秒内刷新过且使用次数极少的节点视为“新鲜”分数极低最高优
const isFresh = config.lastHealthCheckTime && const lastHealthCheckTime = config.lastHealthCheckTime ? new Date(config.lastHealthCheckTime).getTime() : 0;
(now - new Date(config.lastHealthCheckTime).getTime() < 120000) && const isFresh = lastHealthCheckTime && (now - lastHealthCheckTime < 60000);
(config.usageCount === 0); if (isFresh) return -2e18 + (config.usageCount || 0) * 10000 + (now - lastHealthCheckTime); // 极其优先
if (isFresh) return -2e18; // 极其优先
// 3. 权重计算逻辑: // 3. 权重计算逻辑:
// 改进点:使用 lastUsedTime + usageCount 惩罚 + selectionSequence 惩罚 // 改进点:使用 lastUsedTime + usageCount 惩罚 + selectionSequence 惩罚
@ -453,8 +453,7 @@ export class ProviderPoolManager {
_log(level, message) { _log(level, message) {
const levels = { debug: 0, info: 1, warn: 2, error: 3 }; const levels = { debug: 0, info: 1, warn: 2, error: 3 };
if (levels[level] >= levels[this.logLevel]) { if (levels[level] >= levels[this.logLevel]) {
const logMethod = level === 'debug' ? 'log' : level; logger[level](`[ProviderPoolManager] ${message}`);
console[logMethod](`[ProviderPoolManager] ${message}`);
} }
} }
@ -903,9 +902,7 @@ export class ProviderPoolManager {
if (provider.config.errorCount >= this.maxErrorCount) { if (provider.config.errorCount >= this.maxErrorCount) {
provider.config.isHealthy = false; provider.config.isHealthy = false;
this._log('warn', `Marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Total errors: ${provider.config.errorCount}`); 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); this._debouncedSave(providerType);
} }
@ -1002,8 +999,8 @@ export class ProviderPoolManager {
provider.config.lastErrorMessage = null; provider.config.lastErrorMessage = null;
// 更新健康检测信息 // 更新健康检测信息
provider.config.lastHealthCheckTime = new Date().toISOString();
if (healthCheckModel) { if (healthCheckModel) {
provider.config.lastHealthCheckTime = new Date().toISOString();
provider.config.lastHealthCheckModel = healthCheckModel; provider.config.lastHealthCheckModel = healthCheckModel;
} }
@ -1456,3 +1453,4 @@ export class ProviderPoolManager {
} }
} }

View file

@ -5,7 +5,7 @@ import {
ENDPOINT_TYPE ENDPOINT_TYPE
} from '../utils/common.js'; } from '../utils/common.js';
import { getProviderPoolManager } from './service-manager.js'; import { getProviderPoolManager } from './service-manager.js';
import logger from '../utils/logger.js';
/** /**
* Handle API authentication and routing * Handle API authentication and routing
* @param {string} method - The HTTP method * @param {string} method - The HTTP method
@ -65,7 +65,7 @@ export async function handleAPIRequests(method, path, req, res, currentConfig, a
export function initializeAPIManagement(services) { export function initializeAPIManagement(services) {
const providerPoolManager = getProviderPoolManager(); const providerPoolManager = getProviderPoolManager();
return async function heartbeatAndRefreshToken() { 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()) { // if (getProviderPoolManager()) {
// await getProviderPoolManager().performHealthChecks(); // 定期执行健康检查 // await getProviderPoolManager().performHealthChecks(); // 定期执行健康检查
@ -83,9 +83,9 @@ export function initializeAPIManagement(services) {
} else { } else {
await serviceAdapter.refreshToken(); await serviceAdapter.refreshToken();
} }
// console.log(`[Token Refresh] Refreshed token for ${providerKey}`); // logger.info(`[Token Refresh] Refreshed token for ${providerKey}`);
} catch (error) { } 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 存储的是每个配置对应的单例,而非池中的成员 // 现有的 serviceInstances 存储的是每个配置对应的单例,而非池中的成员
// 这意味着如果一个池成员的 token 刷新失败,需要找到它并更新其在 poolManager 中的状态 // 这意味着如果一个池成员的 token 刷新失败,需要找到它并更新其在 poolManager 中的状态

View file

@ -1,3 +1,4 @@
import logger from '../utils/logger.js';
import * as http from 'http'; import * as http from 'http';
import { initializeConfig, CONFIG } from '../core/config-manager.js'; import { initializeConfig, CONFIG } from '../core/config-manager.js';
import { initApiService, autoLinkProviderConfigs } from './service-manager.js'; import { initApiService, autoLinkProviderConfigs } from './service-manager.js';
@ -141,11 +142,11 @@ function setupWorkerCommunication() {
process.on('message', (message) => { process.on('message', (message) => {
if (!message || !message.type) return; 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) { switch (message.type) {
case 'shutdown': case 'shutdown':
console.log('[Worker] Shutdown requested by master'); logger.info('[Worker] Shutdown requested by master');
gracefulShutdown(); gracefulShutdown();
break; break;
case 'status': case 'status':
@ -159,13 +160,13 @@ function setupWorkerCommunication() {
}); });
break; break;
default: default:
console.log('[Worker] Unknown message type:', message.type); logger.info('[Worker] Unknown message type:', message.type);
} }
}); });
// 监听断开连接 // 监听断开连接
process.on('disconnect', () => { process.on('disconnect', () => {
console.log('[Worker] Disconnected from master, shutting down...'); logger.info('[Worker] Disconnected from master, shutting down...');
gracefulShutdown(); gracefulShutdown();
}); });
} }
@ -174,17 +175,17 @@ function setupWorkerCommunication() {
* 优雅关闭服务器 * 优雅关闭服务器
*/ */
async function gracefulShutdown() { async function gracefulShutdown() {
console.log('[Server] Initiating graceful shutdown...'); logger.info('[Server] Initiating graceful shutdown...');
if (serverInstance) { if (serverInstance) {
serverInstance.close(() => { serverInstance.close(() => {
console.log('[Server] HTTP server closed'); logger.info('[Server] HTTP server closed');
process.exit(0); process.exit(0);
}); });
// 设置超时,防止无限等待 // 设置超时,防止无限等待
setTimeout(() => { setTimeout(() => {
console.log('[Server] Shutdown timeout, forcing exit...'); logger.info('[Server] Shutdown timeout, forcing exit...');
process.exit(1); process.exit(1);
}, 10000); }, 10000);
} else { } else {
@ -197,22 +198,22 @@ async function gracefulShutdown() {
*/ */
function setupSignalHandlers() { function setupSignalHandlers() {
process.on('SIGTERM', () => { process.on('SIGTERM', () => {
console.log('[Server] Received SIGTERM'); logger.info('[Server] Received SIGTERM');
gracefulShutdown(); gracefulShutdown();
}); });
process.on('SIGINT', () => { process.on('SIGINT', () => {
console.log('[Server] Received SIGINT'); logger.info('[Server] Received SIGINT');
gracefulShutdown(); gracefulShutdown();
}); });
process.on('uncaughtException', (error) => { process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error); logger.error('[Server] Uncaught exception:', error);
gracefulShutdown(); gracefulShutdown();
}); });
process.on('unhandledRejection', (reason, promise) => { 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'); await initializeConfig(process.argv.slice(2), 'configs/config.json');
// 自动关联 configs 目录中的配置文件到对应的提供商 // 自动关联 configs 目录中的配置文件到对应的提供商
// console.log('[Initialization] Checking for unlinked provider configs...'); // logger.info('[Initialization] Checking for unlinked provider configs...');
// await autoLinkProviderConfigs(CONFIG); // await autoLinkProviderConfigs(CONFIG);
// Initialize plugin system // Initialize plugin system
console.log('[Initialization] Discovering and initializing plugins...'); logger.info('[Initialization] Discovering and initializing plugins...');
await discoverPlugins(); await discoverPlugins();
const pluginManager = getPluginManager(); const pluginManager = getPluginManager();
await pluginManager.initAll(CONFIG); await pluginManager.initAll(CONFIG);
@ -234,10 +235,10 @@ async function startServer() {
// Log loaded plugins // Log loaded plugins
const pluginList = pluginManager.getPluginList(); const pluginList = pluginManager.getPluginList();
if (pluginList.length > 0) { if (pluginList.length > 0) {
console.log(`[Plugins] Loaded ${pluginList.length} plugin(s):`); logger.info(`[Plugins] Loaded ${pluginList.length} plugin(s):`);
pluginList.forEach(p => { pluginList.forEach(p => {
const status = p.enabled ? '✓' : '✗'; 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.maxConnections = 1000;
serverInstance.listen(CONFIG.SERVER_PORT, CONFIG.HOST, async () => { 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 const configuredProviders = Array.isArray(CONFIG.DEFAULT_MODEL_PROVIDERS) && CONFIG.DEFAULT_MODEL_PROVIDERS.length > 0
? CONFIG.DEFAULT_MODEL_PROVIDERS ? CONFIG.DEFAULT_MODEL_PROVIDERS
: [CONFIG.MODEL_PROVIDER]; : [CONFIG.MODEL_PROVIDER];
const uniqueProviders = [...new Set(configuredProviders)]; 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) { 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'}`); logger.info(` System Prompt File: ${CONFIG.SYSTEM_PROMPT_FILE_PATH || 'Default'}`);
console.log(` System Prompt Mode: ${CONFIG.SYSTEM_PROMPT_MODE}`); logger.info(` System Prompt Mode: ${CONFIG.SYSTEM_PROMPT_MODE}`);
console.log(` Host: ${CONFIG.HOST}`); logger.info(` Host: ${CONFIG.HOST}`);
console.log(` Port: ${CONFIG.SERVER_PORT}`); logger.info(` Port: ${CONFIG.SERVER_PORT}`);
console.log(` Required API Key: ${CONFIG.REQUIRED_API_KEY}`); logger.info(` Required API Key: ${CONFIG.REQUIRED_API_KEY}`);
console.log(` Prompt Logging: ${CONFIG.PROMPT_LOG_MODE}${CONFIG.PROMPT_LOG_FILENAME ? ` (to ${CONFIG.PROMPT_LOG_FILENAME})` : ''}`); logger.info(` Prompt Logging: ${CONFIG.PROMPT_LOG_MODE}${CONFIG.PROMPT_LOG_FILENAME ? ` (to ${CONFIG.PROMPT_LOG_FILENAME})` : ''}`);
console.log(`------------------------------------------`); logger.info(`------------------------------------------`);
console.log(`\nUnified API Server running on http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}`); logger.info(`\nUnified API Server running on http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}`);
console.log(`Supports multiple API formats:`); logger.info(`Supports multiple API formats:`);
console.log(` • OpenAI-compatible: /v1/chat/completions, /v1/responses, /v1/models`); logger.info(` • OpenAI-compatible: /v1/chat/completions, /v1/responses, /v1/models`);
console.log(` • Gemini-compatible: /v1beta/models, /v1beta/models/{model}:generateContent`); logger.info(` • Gemini-compatible: /v1beta/models, /v1beta/models/{model}:generateContent`);
console.log(` • Claude-compatible: /v1/messages`); logger.info(` • Claude-compatible: /v1/messages`);
console.log(` • Health check: /health`); logger.info(` • Health check: /health`);
console.log(` • UI Management Console: http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}/`); 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) // 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') { // if (CONFIG.HOST === '0.0.0.0' || CONFIG.HOST === '127.0.0.1') {
@ -300,27 +301,27 @@ async function startServer() {
} }
open(openUrl) open(openUrl)
.then(() => { .then(() => {
console.log('[UI] Opened login page in default browser'); logger.info('[UI] Opened login page in default browser');
}) })
.catch(err => { .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); }, openDelay);
} catch (err) { } 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) { if (CONFIG.CRON_REFRESH_TOKEN) {
console.log(` • Cron Near Minutes: ${CONFIG.CRON_NEAR_MINUTES}`); logger.info(` • Cron Near Minutes: ${CONFIG.CRON_NEAR_MINUTES}`);
console.log(` • Cron Refresh Token: ${CONFIG.CRON_REFRESH_TOKEN}`); logger.info(` • Cron Refresh Token: ${CONFIG.CRON_REFRESH_TOKEN}`);
// 每 CRON_NEAR_MINUTES 分钟执行一次心跳日志和令牌刷新 // 每 CRON_NEAR_MINUTES 分钟执行一次心跳日志和令牌刷新
setInterval(heartbeatAndRefreshToken, CONFIG.CRON_NEAR_MINUTES * 60 * 1000); setInterval(heartbeatAndRefreshToken, CONFIG.CRON_NEAR_MINUTES * 60 * 1000);
} }
// 服务器完全启动后,执行初始健康检查 // 服务器完全启动后,执行初始健康检查
const poolManager = getProviderPoolManager(); const poolManager = getProviderPoolManager();
if (poolManager) { 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); poolManager.performHealthChecks(true);
} }
@ -339,7 +340,7 @@ setupSignalHandlers();
setupWorkerCommunication(); setupWorkerCommunication();
startServer().catch(err => { startServer().catch(err => {
console.error("[Server] Failed to start server:", err.message); logger.error("[Server] Failed to start server:", err.message);
process.exit(1); process.exit(1);
}); });

View file

@ -1,4 +1,5 @@
import { getServiceAdapter, serviceInstances } from '../providers/adapter.js'; import { getServiceAdapter, serviceInstances } from '../providers/adapter.js';
import logger from '../utils/logger.js';
import { ProviderPoolManager } from '../providers/provider-pool-manager.js'; import { ProviderPoolManager } from '../providers/provider-pool-manager.js';
import deepmerge from 'deepmerge'; import deepmerge from 'deepmerge';
import * as fs from 'fs'; 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'; const filePath = config.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json';
try { try {
await pfs.writeFile(filePath, JSON.stringify(config.providerPools, null, 2), 'utf8'); 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)) { 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 => { providers.forEach(p => {
// 获取凭据路径键(支持 _CREDS_FILE_PATH 和 _TOKEN_FILE_PATH 两种格式) // 获取凭据路径键(支持 _CREDS_FILE_PATH 和 _TOKEN_FILE_PATH 两种格式)
const credKey = Object.keys(p).find(k => const credKey = Object.keys(p).find(k =>
k.endsWith('_CREDS_FILE_PATH') || k.endsWith('_TOKEN_FILE_PATH') k.endsWith('_CREDS_FILE_PATH') || k.endsWith('_TOKEN_FILE_PATH')
); );
if (credKey) { if (credKey) {
console.log(` - ${p[credKey]}`); logger.info(` - ${p[credKey]}`);
} }
}); });
} }
} catch (error) { } 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 { } 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 // Update provider pool manager if available
@ -154,7 +155,7 @@ async function scanProviderDirectory(dirPath, linkedPaths, newProviders, options
} }
} }
} catch (error) { } 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, maxErrorCount: config.MAX_ERROR_COUNT ?? 3,
providerFallbackChain: config.providerFallbackChain || {}, providerFallbackChain: config.providerFallbackChain || {},
}); });
console.log('[Initialization] ProviderPoolManager initialized with configured pools.'); logger.info('[Initialization] ProviderPoolManager initialized with configured pools.');
if(isReady){ if(isReady){
// --- V2: 触发系统预热 --- // --- V2: 触发系统预热 ---
// 预热逻辑是异步的,不会阻塞服务器启动 // 预热逻辑是异步的,不会阻塞服务器启动
providerPoolManager.warmupNodes().catch(err => { providerPoolManager.warmupNodes().catch(err => {
console.error(`[Initialization] Warmup failed: ${err.message}`); logger.error(`[Initialization] Warmup failed: ${err.message}`);
}); });
// 检查并刷新即将过期的节点(异步调用,不阻塞启动) // 检查并刷新即将过期的节点(异步调用,不阻塞启动)
providerPoolManager.checkAndRefreshExpiringNodes().catch(err => { 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 { } 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 // Initialize all provider pool nodes at startup
@ -203,7 +204,7 @@ export async function initApiService(config, isReady = false) {
// 验证提供商类型是否在 DEFAULT_MODEL_PROVIDERS 中 // 验证提供商类型是否在 DEFAULT_MODEL_PROVIDERS 中
if (config.DEFAULT_MODEL_PROVIDERS && Array.isArray(config.DEFAULT_MODEL_PROVIDERS)) { if (config.DEFAULT_MODEL_PROVIDERS && Array.isArray(config.DEFAULT_MODEL_PROVIDERS)) {
if (!config.DEFAULT_MODEL_PROVIDERS.includes(providerType)) { 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; continue;
} }
} }
@ -212,7 +213,7 @@ export async function initApiService(config, isReady = false) {
continue; 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) { for (const providerConfig of providerConfigs) {
@ -234,18 +235,18 @@ export async function initApiService(config, isReady = false) {
totalInitialized++; totalInitialized++;
const identifier = providerConfig.customName || providerConfig.uuid || 'unknown'; const identifier = providerConfig.customName || providerConfig.uuid || 'unknown';
console.log(` ✓ Initialized node: ${identifier}`); logger.info(` ✓ Initialized node: ${identifier}`);
} catch (error) { } catch (error) {
totalFailed++; totalFailed++;
const identifier = providerConfig.customName || providerConfig.uuid || 'unknown'; 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 { } 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 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.uuid = serviceConfig.uuid;
config.customName = serviceConfig.customName; config.customName = serviceConfig.customName;
const customNameDisplay = serviceConfig.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 { } else {
const errorMsg = `[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}`; 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); throw new Error(errorMsg);
} }
} }
@ -321,7 +322,7 @@ export async function getApiServiceWithFallback(config, requestedModel = null, o
} }
} else { } else {
const errorMsg = `[API Service] No healthy provider found in pool (including fallback) for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}`; 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); throw new Error(errorMsg);
} }
} }
@ -376,7 +377,7 @@ export async function getProviderStatus(config, options = {}) {
providerPools = poolsData; providerPools = poolsData;
} }
} catch (error) { } 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 的元素 // providerPoolsSlim 只保留顶级 key 及部分字段,过滤 isDisabled 为 true 的元素

View file

@ -108,6 +108,11 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
return await systemApi.handleGetSystem(req, res); 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 // Get provider pools summary
if (method === 'GET' && pathParam === '/api/providers') { if (method === 'GET' && pathParam === '/api/providers') {
return await providerApi.handleGetProviders(req, res, currentConfig, providerPoolManager); return await providerApi.handleGetProviders(req, res, currentConfig, providerPoolManager);

View file

@ -1,4 +1,5 @@
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import logger from '../utils/logger.js';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import crypto from 'crypto'; import crypto from 'crypto';
@ -23,18 +24,18 @@ export async function readPasswordFile() {
const trimmedPassword = password.trim(); const trimmedPassword = password.trim();
// 如果密码文件为空,使用默认密码 // 如果密码文件为空,使用默认密码
if (!trimmedPassword) { 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; return DEFAULT_PASSWORD;
} }
console.log('[Auth] Successfully read password file'); logger.info('[Auth] Successfully read password file');
return trimmedPassword; return trimmedPassword;
} catch (error) { } catch (error) {
// ENOENT means file does not exist, which is normal // ENOENT means file does not exist, which is normal
if (error.code === 'ENOENT') { 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 { } else {
console.error('[Auth] Failed to read password file:', error.code || error.message); logger.error('[Auth] Failed to read password file:', error.code || error.message);
console.log('[Auth] Using default password: ' + DEFAULT_PASSWORD); logger.info('[Auth] Using default password: ' + DEFAULT_PASSWORD);
} }
return DEFAULT_PASSWORD; return DEFAULT_PASSWORD;
} }
@ -45,9 +46,9 @@ export async function readPasswordFile() {
*/ */
export async function validateCredentials(password) { export async function validateCredentials(password) {
const storedPassword = await readPasswordFile(); 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; const isValid = storedPassword && password === storedPassword;
console.log('[Auth] Password validation result:', isValid); logger.info('[Auth] Password validation result:', isValid);
return isValid; return isValid;
} }
@ -105,7 +106,7 @@ async function readTokenStore() {
return { tokens: {} }; return { tokens: {} };
} }
} catch (error) { } 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: {} }; return { tokens: {} };
} }
} }
@ -117,7 +118,7 @@ async function writeTokenStore(tokenStore) {
try { try {
await fs.writeFile(TOKEN_STORE_FILE, JSON.stringify(tokenStore, null, 2), 'utf8'); await fs.writeFile(TOKEN_STORE_FILE, JSON.stringify(tokenStore, null, 2), 'utf8');
} catch (error) { } 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) { } catch (error) {
console.error('[Auth] Login processing error:', error); logger.error('[Auth] Login processing error:', error);
res.writeHead(500, { 'Content-Type': 'application/json' }); res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
success: false, success: false,
@ -256,4 +257,6 @@ export async function handleLoginRequest(req, res) {
} }
// 定时清理过期token // 定时清理过期token
setInterval(cleanupExpiredTokens, 5 * 60 * 1000); // 每5分钟清理一次 setInterval(cleanupExpiredTokens, 5 * 60 * 1000); // 每5分钟清理一次

View file

@ -1,4 +1,5 @@
import { existsSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, readFileSync, writeFileSync } from 'fs';
import logger from '../utils/logger.js';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { CONFIG } from '../core/config-manager.js'; import { CONFIG } from '../core/config-manager.js';
@ -27,17 +28,17 @@ export async function reloadConfig(providerPoolManager) {
// Update global CONFIG // Update global CONFIG
Object.assign(CONFIG, newConfig); Object.assign(CONFIG, newConfig);
console.log('[UI API] Configuration reloaded:'); logger.info('[UI API] Configuration reloaded:');
// Update initApiService - 清空并重新初始化服务实例 // Update initApiService - 清空并重新初始化服务实例
Object.keys(serviceInstances).forEach(key => delete serviceInstances[key]); Object.keys(serviceInstances).forEach(key => delete serviceInstances[key]);
initApiService(CONFIG); initApiService(CONFIG);
console.log('[UI API] Configuration reloaded successfully'); logger.info('[UI API] Configuration reloaded successfully');
return newConfig; return newConfig;
} catch (error) { } catch (error) {
console.error('[UI API] Failed to reload configuration:', error); logger.error('[UI API] Failed to reload configuration:', error);
throw error; throw error;
} }
} }
@ -52,7 +53,7 @@ export async function handleGetConfig(req, res, currentConfig) {
try { try {
systemPrompt = readFileSync(currentConfig.SYSTEM_PROMPT_FILE_PATH, 'utf-8'); systemPrompt = readFileSync(currentConfig.SYSTEM_PROMPT_FILE_PATH, 'utf-8');
} catch (e) { } 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_URL !== undefined) currentConfig.PROXY_URL = newConfig.PROXY_URL;
if (newConfig.PROXY_ENABLED_PROVIDERS !== undefined) currentConfig.PROXY_ENABLED_PROVIDERS = newConfig.PROXY_ENABLED_PROVIDERS; 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 // Handle system prompt update
if (newConfig.systemPrompt !== undefined) { if (newConfig.systemPrompt !== undefined) {
const promptPath = currentConfig.SYSTEM_PROMPT_FILE_PATH || 'configs/input_system_prompt.txt'; 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() timestamp: new Date().toISOString()
}); });
console.log('[UI API] System prompt updated'); logger.info('[UI API] System prompt updated');
} catch (e) { } 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, CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN,
PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH, PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH,
MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT, MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT,
POOL_SIZE_LIMIT: currentConfig.POOL_SIZE_LIMIT,
WARMUP_TARGET: currentConfig.WARMUP_TARGET, WARMUP_TARGET: currentConfig.WARMUP_TARGET,
REFRESH_CONCURRENCY_PER_PROVIDER: currentConfig.REFRESH_CONCURRENCY_PER_PROVIDER, REFRESH_CONCURRENCY_PER_PROVIDER: currentConfig.REFRESH_CONCURRENCY_PER_PROVIDER,
providerFallbackChain: currentConfig.providerFallbackChain, providerFallbackChain: currentConfig.providerFallbackChain,
modelFallbackMapping: currentConfig.modelFallbackMapping, modelFallbackMapping: currentConfig.modelFallbackMapping,
PROXY_URL: currentConfig.PROXY_URL, 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'); 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', { broadcastEvent('config_update', {
@ -159,7 +177,7 @@ export async function handleUpdateConfig(req, res, currentConfig) {
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -215,7 +233,7 @@ export async function handleReloadConfig(req, res, providerPoolManager) {
})); }));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -248,7 +266,7 @@ export async function handleUpdateAdminPassword(req, res) {
const pwdFilePath = path.join(process.cwd(), 'configs', 'pwd'); const pwdFilePath = path.join(process.cwd(), 'configs', 'pwd');
await fs.writeFile(pwdFilePath, password.trim(), 'utf-8'); 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.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
@ -257,7 +275,7 @@ export async function handleUpdateAdminPassword(req, res) {
})); }));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {

View file

@ -1,4 +1,5 @@
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import logger from '../utils/logger.js';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js'; 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'); const configsPath = path.join(process.cwd(), 'configs');
if (!existsSync(configsPath)) { 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; return configFiles;
} }
@ -55,7 +56,7 @@ export async function scanConfigFiles(currentConfig, providerPoolManager) {
const configsFiles = await scanOAuthDirectory(configsPath, usedPaths, currentConfig); const configsFiles = await scanOAuthDirectory(configsPath, usedPaths, currentConfig);
configFiles.push(...configsFiles); configFiles.push(...configsFiles);
} catch (error) { } 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; return configFiles;
@ -143,7 +144,7 @@ async function analyzeOAuthFile(filePath, usedPaths, currentConfig) {
preview: content.substring(0, 100) + (content.length > 100 ? '...' : '') preview: content.substring(0, 100) + (content.length > 100 ? '...' : '')
}; };
} catch (error) { } 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; return null;
} }
} }
@ -361,7 +362,7 @@ async function scanOAuthDirectory(dirPath, usedPaths, currentConfig) {
} }
} }
} catch (error) { } 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; return oauthFiles;

View file

@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'fs';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import multer from 'multer'; import multer from 'multer';
import logger from '../utils/logger.js';
// Token存储到本地文件中 // Token存储到本地文件中
const TOKEN_STORE_FILE = path.join(process.cwd(), 'configs', 'token-store.json'); const TOKEN_STORE_FILE = path.join(process.cwd(), 'configs', 'token-store.json');
@ -72,21 +73,17 @@ export function initializeUIManagement() {
const originalLog = console.log; const originalLog = console.log;
console.log = function(...args) { console.log = function(...args) {
originalLog.apply(console, 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 = { const logEntry = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
level: 'info', 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); global.logBuffer.push(logEntry);
if (global.logBuffer.length > 100) { if (global.logBuffer.length > 100) {
@ -99,21 +96,17 @@ export function initializeUIManagement() {
const originalError = console.error; const originalError = console.error;
console.error = function(...args) { console.error = function(...args) {
originalError.apply(console, 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 = { const logEntry = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
level: 'error', 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); global.logBuffer.push(logEntry);
if (global.logBuffer.length > 100) { if (global.logBuffer.length > 100) {
@ -185,7 +178,7 @@ export function handleUploadOAuthCredentials(req, res, options = {}) {
return new Promise((resolve) => { return new Promise((resolve) => {
uploadMiddleware(req, res, async (err) => { uploadMiddleware(req, res, async (err) => {
if (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.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -242,7 +235,7 @@ export function handleUploadOAuthCredentials(req, res, options = {}) {
}); });
const userInfoStr = userInfo ? `, ${userInfo}` : ''; 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.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
@ -255,7 +248,7 @@ export function handleUploadOAuthCredentials(req, res, options = {}) {
resolve(true); resolve(true);
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {

View file

@ -1,4 +1,5 @@
import { getRequestBody } from '../utils/common.js'; import { getRequestBody } from '../utils/common.js';
import logger from '../utils/logger.js';
import { import {
handleGeminiCliOAuth, handleGeminiCliOAuth,
handleGeminiAntigravityOAuth, handleGeminiAntigravityOAuth,
@ -74,7 +75,7 @@ export async function handleGenerateAuthUrl(req, res, currentConfig, providerTyp
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -102,8 +103,8 @@ export async function handleManualOAuthCallback(req, res) {
return true; return true;
} }
console.log(`[OAuth Manual Callback] Processing manual callback for ${provider}`); logger.info(`[OAuth Manual Callback] Processing manual callback for ${provider}`);
console.log(`[OAuth Manual Callback] Callback URL: ${callbackUrl}`); logger.info(`[OAuth Manual Callback] Callback URL: ${callbackUrl}`);
// 解析回调URL // 解析回调URL
const url = new URL(callbackUrl); const url = new URL(callbackUrl);
@ -140,7 +141,7 @@ export async function handleManualOAuthCallback(req, res) {
const response = await fetch(localUrl.href); const response = await fetch(localUrl.href);
if (response.ok) { 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.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
success: true, success: true,
@ -148,7 +149,7 @@ export async function handleManualOAuthCallback(req, res) {
})); }));
} else { } else {
const errorText = await response.text(); 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
success: false, success: false,
@ -156,7 +157,7 @@ export async function handleManualOAuthCallback(req, res) {
})); }));
} }
} catch (fetchError) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
success: false, success: false,
@ -166,7 +167,7 @@ export async function handleManualOAuthCallback(req, res) {
return true; return true;
} catch (error) { } catch (error) {
console.error('[OAuth Manual Callback] Error:', error); logger.error('[OAuth Manual Callback] Error:', error);
res.writeHead(500, { 'Content-Type': 'application/json' }); res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
success: false, success: false,
@ -193,7 +194,7 @@ export async function handleBatchImportKiroTokens(req, res) {
return true; 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 响应头 // 设置 SSE 响应头
res.writeHead(200, { 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', { sendSSE('complete', {
@ -237,7 +238,7 @@ export async function handleBatchImportKiroTokens(req, res) {
return true; return true;
} catch (error) { } catch (error) {
console.error('[Kiro Batch Import] Error:', error); logger.error('[Kiro Batch Import] Error:', error);
// 如果已经开始发送 SSE则发送错误事件 // 如果已经开始发送 SSE则发送错误事件
if (res.headersSent) { if (res.headersSent) {
res.write(`event: error\n`); res.write(`event: error\n`);
@ -287,12 +288,12 @@ export async function handleImportAwsCredentials(req, res) {
return true; 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); const result = await importAwsCredentials(credentials);
if (result.success) { 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.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
success: true, success: true,
@ -312,7 +313,7 @@ export async function handleImportAwsCredentials(req, res) {
return true; return true;
} catch (error) { } catch (error) {
console.error('[Kiro AWS Import] Error:', error); logger.error('[Kiro AWS Import] Error:', error);
res.writeHead(500, { 'Content-Type': 'application/json' }); res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
success: false, success: false,

View file

@ -1,4 +1,5 @@
import { getPluginManager } from '../core/plugin-manager.js'; import { getPluginManager } from '../core/plugin-manager.js';
import logger from '../utils/logger.js';
import { getRequestBody } from '../utils/common.js'; import { getRequestBody } from '../utils/common.js';
import { broadcastEvent } from './event-broadcast.js'; import { broadcastEvent } from './event-broadcast.js';
@ -13,7 +14,7 @@ export async function handleGetPlugins(req, res) {
res.end(JSON.stringify({ plugins })); res.end(JSON.stringify({ plugins }));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -64,7 +65,7 @@ export async function handleTogglePlugin(req, res, pluginName) {
})); }));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {

View file

@ -1,4 +1,5 @@
import { existsSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, readFileSync, writeFileSync } from 'fs';
import logger from '../utils/logger.js';
import { getRequestBody } from '../utils/common.js'; import { getRequestBody } from '../utils/common.js';
import { getAllProviderModels, getProviderModels } from '../providers/provider-models.js'; import { getAllProviderModels, getProviderModels } from '../providers/provider-models.js';
import { generateUUID, createProviderConfig, formatSystemPath, detectProviderFromPath, addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.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; providerPools = poolsData;
} }
} catch (error) { } 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' }); res.writeHead(200, { 'Content-Type': 'application/json' });
@ -40,7 +41,7 @@ export async function handleGetProviderType(req, res, currentConfig, providerPoo
providerPools = poolsData; providerPools = poolsData;
} }
} catch (error) { } 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] || []; const providers = providerPools[providerType] || [];
@ -112,7 +113,7 @@ export async function handleAddProvider(req, res, currentConfig, providerPoolMan
const fileContent = readFileSync(filePath, 'utf-8'); const fileContent = readFileSync(filePath, 'utf-8');
providerPools = JSON.parse(fileContent); providerPools = JSON.parse(fileContent);
} catch (readError) { } 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 // Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); 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 // Update provider pool manager if available
if (providerPoolManager) { if (providerPoolManager) {
@ -219,7 +220,7 @@ export async function handleUpdateProvider(req, res, currentConfig, providerPool
// Save to file // Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); 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 // Update provider pool manager if available
if (providerPoolManager) { if (providerPoolManager) {
@ -290,7 +291,7 @@ export async function handleDeleteProvider(req, res, currentConfig, providerPool
// Save to file // Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); 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 // Update provider pool manager if available
if (providerPoolManager) { if (providerPoolManager) {
@ -357,7 +358,7 @@ export async function handleDisableEnableProvider(req, res, currentConfig, provi
// Save to file // Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); 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 // Update provider pool manager if available
if (providerPoolManager) { if (providerPoolManager) {
@ -439,7 +440,7 @@ export async function handleResetProviderHealth(req, res, currentConfig, provide
// Save to file // Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); 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 // Update provider pool manager if available
if (providerPoolManager) { if (providerPoolManager) {
@ -524,7 +525,7 @@ export async function handleDeleteUnhealthyProviders(req, res, currentConfig, pr
// Save to file // Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); 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 // Update provider pool manager if available
if (providerPoolManager) { if (providerPoolManager) {
@ -615,7 +616,7 @@ export async function handleRefreshUnhealthyUuids(req, res, currentConfig, provi
// Save to file // Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); 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 // Update provider pool manager if available
if (providerPoolManager) { if (providerPoolManager) {
@ -684,7 +685,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
return true; 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 配置) // 执行健康检测(强制检查,忽略 checkHealth 配置)
const results = []; const results = [];
@ -693,7 +694,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
// 跳过已禁用的节点 // 跳过已禁用的节点
if (providerConfig.isDisabled) { 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; continue;
} }
@ -726,7 +727,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
if (isAuthError) { if (isAuthError) {
providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage); 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 { } else {
providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage); providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage);
} }
@ -751,7 +752,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
if (isAuthError) { if (isAuthError) {
providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage); 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 { } else {
providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage); 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 successCount = results.filter(r => r.success === true).length;
const failCount = results.filter(r => r.success === false).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', { broadcastEvent('config_update', {
@ -800,7 +801,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
})); }));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: error.message } })); res.end(JSON.stringify({ error: { message: error.message } }));
return true; return true;
@ -846,7 +847,7 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP
const fileContent = readFileSync(poolsFilePath, 'utf-8'); const fileContent = readFileSync(poolsFilePath, 'utf-8');
providerPools = JSON.parse(fileContent); providerPools = JSON.parse(fileContent);
} catch (readError) { } 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 // Save to file
writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf-8'); 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 // Update provider pool manager if available
if (providerPoolManager) { if (providerPoolManager) {
@ -917,7 +918,7 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP
})); }));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -967,7 +968,7 @@ export async function handleRefreshProviderUuid(req, res, currentConfig, provide
// Save to file // Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8'); 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 // Update provider pool manager if available
if (providerPoolManager) { if (providerPoolManager) {

View file

@ -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 path from 'path';
import { getCpuUsagePercent } from './system-monitor.js'; import { getCpuUsagePercent } from './system-monitor.js';
@ -16,7 +17,7 @@ export async function handleGetSystem(req, res) {
appVersion = readFileSync(versionFilePath, 'utf8').trim(); appVersion = readFileSync(versionFilePath, 'utf8').trim();
} }
} catch (error) { } 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 使用率 // 计算 CPU 使用率
@ -45,6 +46,34 @@ export async function handleGetSystem(req, res) {
return true; 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验证 * 健康检查接口用于前端token验证
*/ */
@ -84,7 +113,7 @@ export async function handleRestartService(req, res) {
if (IS_WORKER_PROCESS && process.send) { 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' }); process.send({ type: 'restart_request' });
// 广播重启事件 // 广播重启事件
@ -107,7 +136,7 @@ export async function handleRestartService(req, res) {
})); }));
} else { } 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.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
@ -119,7 +148,7 @@ export async function handleRestartService(req, res) {
} }
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {

View file

@ -1,4 +1,5 @@
import { existsSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, readFileSync, writeFileSync } from 'fs';
import logger from '../utils/logger.js';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { exec } from 'child_process'; import { exec } from 'child_process';
@ -19,7 +20,7 @@ function getUpdateProxyConfig() {
const proxyConfig = parseProxyUrl(CONFIG.PROXY_URL); const proxyConfig = parseProxyUrl(CONFIG.PROXY_URL);
if (proxyConfig) { 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; return proxyConfig;
} }
@ -59,7 +60,7 @@ async function fetchWithProxy(url, options = {}) {
}); });
} catch (importError) { } catch (importError) {
// 如果 undici 不可用,回退到原生 fetch不使用代理 // 如果 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); 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`; const apiUrl = `https://gh-proxy.org/https://api.github.com/repos/${GITHUB_REPO}/tags`;
try { try {
console.log('[Update] Fetching latest version from GitHub API...'); logger.info('[Update] Fetching latest version from GitHub API...');
const response = await fetchWithProxy(apiUrl, { const response = await fetchWithProxy(apiUrl, {
headers: { headers: {
'Accept': 'application/vnd.github.v3+json', 'Accept': 'application/vnd.github.v3+json',
@ -136,7 +137,7 @@ async function getLatestVersionFromGitHub() {
return versions[0]; return versions[0];
} catch (error) { } 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; return null;
} }
} }
@ -158,7 +159,7 @@ export async function checkForUpdates() {
localVersion = readFileSync(versionFilePath, 'utf-8').trim(); localVersion = readFileSync(versionFilePath, 'utf-8').trim();
} }
} catch (error) { } 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 仓库中 // 检查是否在 git 仓库中
@ -168,7 +169,7 @@ export async function checkForUpdates() {
isGitRepo = true; isGitRepo = true;
} catch (error) { } catch (error) {
isGitRepo = false; 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; let latestTag = null;
@ -180,10 +181,10 @@ export async function checkForUpdates() {
// 获取远程 tags // 获取远程 tags
try { try {
console.log('[Update] Fetching remote tags...'); logger.info('[Update] Fetching remote tags...');
await execAsync('git fetch --tags'); await execAsync('git fetch --tags');
} catch (error) { } 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 // 如果 git fetch 失败,回退到 GitHub API
latestTag = await getLatestVersionFromGitHub(); latestTag = await getLatestVersionFromGitHub();
updateMethod = 'github_api'; updateMethod = 'github_api';
@ -214,7 +215,7 @@ export async function checkForUpdates() {
latestTag = tags[0]; latestTag = tags[0];
} }
} catch (e) { } 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(); latestTag = await getLatestVersionFromGitHub();
updateMethod = 'github_api'; updateMethod = 'github_api';
} }
@ -240,7 +241,7 @@ export async function checkForUpdates() {
const comparison = compareVersions(latestTag, localVersion); const comparison = compareVersions(latestTag, localVersion);
const hasUpdate = comparison > 0; 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 { return {
hasUpdate, hasUpdate,
@ -278,30 +279,30 @@ export async function performUpdate() {
// 检查更新方式 - 如果是通过 GitHub API 获取的版本信息,说明不在 Git 仓库中 // 检查更新方式 - 如果是通过 GitHub API 获取的版本信息,说明不在 Git 仓库中
if (updateInfo.updateMethod === 'github_api') { if (updateInfo.updateMethod === 'github_api') {
// Docker/非 Git 环境,通过下载 tarball 更新 // 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); return await performTarballUpdate(updateInfo.localVersion, latestTag);
} }
console.log(`[Update] Starting update to ${latestTag}...`); logger.info(`[Update] Starting update to ${latestTag}...`);
// 检查是否有未提交的更改 // 检查是否有未提交的更改
try { try {
const { stdout: statusOutput } = await execAsync('git status --porcelain'); const { stdout: statusOutput } = await execAsync('git status --porcelain');
if (statusOutput.trim()) { if (statusOutput.trim()) {
// 有未提交的更改,先 stash // 有未提交的更改,先 stash
console.log('[Update] Stashing local changes...'); logger.info('[Update] Stashing local changes...');
await execAsync('git stash'); await execAsync('git stash');
} }
} catch (error) { } catch (error) {
console.warn('[Update] Failed to check git status:', error.message); logger.warn('[Update] Failed to check git status:', error.message);
} }
// 执行 checkout 到最新 tag // 执行 checkout 到最新 tag
try { try {
console.log(`[Update] Checking out to ${latestTag}...`); logger.info(`[Update] Checking out to ${latestTag}...`);
await execAsync(`git checkout ${latestTag}`); await execAsync(`git checkout ${latestTag}`);
} catch (error) { } 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); throw new Error('Failed to switch to new version: ' + error.message);
} }
@ -310,9 +311,9 @@ export async function performUpdate() {
try { try {
const newVersion = latestTag.replace(/^v/, ''); const newVersion = latestTag.replace(/^v/, '');
writeFileSync(versionFilePath, newVersion, 'utf-8'); writeFileSync(versionFilePath, newVersion, 'utf-8');
console.log(`[Update] VERSION file updated to ${newVersion}`); logger.info(`[Update] VERSION file updated to ${newVersion}`);
} catch (error) { } 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 localVersionTag = updateInfo.localVersion.startsWith('v') ? updateInfo.localVersion : `v${updateInfo.localVersion}`;
const { stdout: diffOutput } = await execAsync(`git diff ${localVersionTag}..${latestTag} --name-only`); const { stdout: diffOutput } = await execAsync(`git diff ${localVersionTag}..${latestTag} --name-only`);
if (diffOutput.includes('package.json') || diffOutput.includes('package-lock.json')) { 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'); await execAsync('npm install');
needsRestart = true; needsRestart = true;
} }
} catch (error) { } 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 { return {
success: true, success: true,
@ -357,16 +358,16 @@ async function performTarballUpdate(localVersion, latestTag) {
const tempDir = path.join(appDir, '.update_temp'); const tempDir = path.join(appDir, '.update_temp');
const tarballPath = path.join(tempDir, 'update.tar.gz'); const tarballPath = path.join(tempDir, 'update.tar.gz');
console.log(`[Update] Starting tarball update to ${latestTag}...`); logger.info(`[Update] Starting tarball update to ${latestTag}...`);
console.log(`[Update] Download URL: ${tarballUrl}`); logger.info(`[Update] Download URL: ${tarballUrl}`);
try { try {
// 1. 创建临时目录 // 1. 创建临时目录
await fs.mkdir(tempDir, { recursive: true }); await fs.mkdir(tempDir, { recursive: true });
console.log('[Update] Created temp directory'); logger.info('[Update] Created temp directory');
// 2. 下载 tarball // 2. 下载 tarball
console.log('[Update] Downloading tarball...'); logger.info('[Update] Downloading tarball...');
const response = await fetchWithProxy(tarballUrl, { const response = await fetchWithProxy(tarballUrl, {
headers: { headers: {
'User-Agent': 'AIClient2API-Updater' 'User-Agent': 'AIClient2API-Updater'
@ -381,10 +382,10 @@ async function performTarballUpdate(localVersion, latestTag) {
const arrayBuffer = await response.arrayBuffer(); const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer); const buffer = Buffer.from(arrayBuffer);
await fs.writeFile(tarballPath, buffer); await fs.writeFile(tarballPath, buffer);
console.log(`[Update] Downloaded tarball (${buffer.length} bytes)`); logger.info(`[Update] Downloaded tarball (${buffer.length} bytes)`);
// 3. 解压 tarball // 3. 解压 tarball
console.log('[Update] Extracting tarball...'); logger.info('[Update] Extracting tarball...');
await execAsync(`tar -xzf "${tarballPath}" -C "${tempDir}"`); await execAsync(`tar -xzf "${tarballPath}" -C "${tempDir}"`);
// 4. 找到解压后的目录(格式通常是 repo-name-tag // 4. 找到解压后的目录(格式通常是 repo-name-tag
@ -398,7 +399,7 @@ async function performTarballUpdate(localVersion, latestTag) {
} }
const sourcePath = path.join(tempDir, extractedDir); const sourcePath = path.join(tempDir, extractedDir);
console.log(`[Update] Extracted to: ${sourcePath}`); logger.info(`[Update] Extracted to: ${sourcePath}`);
// 5. 备份当前的 package.json 用于比较 // 5. 备份当前的 package.json 用于比较
const oldPackageJson = existsSync(path.join(appDir, 'package.json')) const oldPackageJson = existsSync(path.join(appDir, 'package.json'))
@ -410,9 +411,9 @@ async function performTarballUpdate(localVersion, latestTag) {
for (const dirName of dirsToClean) { for (const dirName of dirsToClean) {
const dirPath = path.join(appDir, dirName); const dirPath = path.join(appDir, dirName);
if (existsSync(dirPath)) { 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 }); 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. 复制新文件到应用目录 // 7. 复制新文件到应用目录
console.log('[Update] Copying new files...'); logger.info('[Update] Copying new files...');
const sourceItems = await fs.readdir(sourcePath); const sourceItems = await fs.readdir(sourcePath);
for (const item of sourceItems) { for (const item of sourceItems) {
// 跳过需要保留的目录 // 跳过需要保留的目录
if (preservePaths.includes(item)) { if (preservePaths.includes(item)) {
console.log(`[Update] Skipping preserved path: ${item}`); logger.info(`[Update] Skipping preserved path: ${item}`);
continue; continue;
} }
@ -450,7 +451,7 @@ async function performTarballUpdate(localVersion, latestTag) {
// 复制新文件/目录 // 复制新文件/目录
await copyRecursive(srcItemPath, destItemPath); await copyRecursive(srcItemPath, destItemPath);
console.log(`[Update] Copied: ${item}`); logger.info(`[Update] Copied: ${item}`);
} }
// 8. 检查是否需要更新依赖 // 8. 检查是否需要更新依赖
@ -460,23 +461,23 @@ async function performTarballUpdate(localVersion, latestTag) {
if (oldPackageJson) { if (oldPackageJson) {
const newPackageJson = readFileSync(path.join(appDir, 'package.json'), 'utf-8'); const newPackageJson = readFileSync(path.join(appDir, 'package.json'), 'utf-8');
if (oldPackageJson !== newPackageJson) { if (oldPackageJson !== newPackageJson) {
console.log('[Update] package.json changed, running npm install...'); logger.info('[Update] package.json changed, running npm install...');
needsNpmInstall = true; needsNpmInstall = true;
try { try {
await execAsync('npm install', { cwd: appDir }); await execAsync('npm install', { cwd: appDir });
console.log('[Update] npm install completed'); logger.info('[Update] npm install completed');
} catch (npmError) { } catch (npmError) {
console.error('[Update] npm install failed:', npmError.message); logger.error('[Update] npm install failed:', npmError.message);
// 不抛出错误,继续更新流程 // 不抛出错误,继续更新流程
} }
} }
} }
// 9. 清理临时目录 // 9. 清理临时目录
console.log('[Update] Cleaning up...'); logger.info('[Update] Cleaning up...');
await fs.rm(tempDir, { recursive: true, force: true }); 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 { return {
success: true, success: true,
@ -497,10 +498,10 @@ async function performTarballUpdate(localVersion, latestTag) {
await fs.rm(tempDir, { recursive: true, force: true }); await fs.rm(tempDir, { recursive: true, force: true });
} }
} catch (cleanupError) { } 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}`); throw new Error(`Tarball update failed: ${error.message}`);
} }
} }
@ -534,7 +535,7 @@ export async function handleCheckUpdate(req, res) {
res.end(JSON.stringify(updateInfo)); res.end(JSON.stringify(updateInfo));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -555,7 +556,7 @@ export async function handlePerformUpdate(req, res) {
res.end(JSON.stringify(updateResult)); res.end(JSON.stringify(updateResult));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {

View file

@ -1,4 +1,5 @@
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import logger from '../utils/logger.js';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import AdmZip from 'adm-zip'; import AdmZip from 'adm-zip';
@ -15,7 +16,7 @@ export async function handleGetUploadConfigs(req, res, currentConfig, providerPo
res.end(JSON.stringify(configFiles)); res.end(JSON.stringify(configFiles));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -71,7 +72,7 @@ export async function handleViewConfigFile(req, res, filePath) {
})); }));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -132,7 +133,7 @@ export async function handleDeleteConfigFile(req, res, filePath) {
})); }));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -185,10 +186,10 @@ export async function handleDownloadAllConfigs(req, res) {
}); });
res.end(zipBuffer); 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; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -298,7 +299,7 @@ export async function handleDeleteUnboundConfigs(req, res, currentConfig, provid
})); }));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {

View file

@ -1,4 +1,5 @@
import { CONFIG } from '../core/config-manager.js'; import { CONFIG } from '../core/config-manager.js';
import logger from '../utils/logger.js';
import { serviceInstances, getServiceAdapter } from '../providers/adapter.js'; import { serviceInstances, getServiceAdapter } from '../providers/adapter.js';
import { formatKiroUsage, formatGeminiUsage, formatAntigravityUsage } from '../services/usage-service.js'; import { formatKiroUsage, formatGeminiUsage, formatAntigravityUsage } from '../services/usage-service.js';
import { readUsageCache, writeUsageCache, readProviderUsageCache, updateProviderUsageCache } from './usage-cache.js'; import { readUsageCache, writeUsageCache, readProviderUsageCache, updateProviderUsageCache } from './usage-cache.js';
@ -95,7 +96,7 @@ async function getProviderTypeUsage(providerType, currentConfig, providerPoolMan
} else if (!adapter) { } else if (!adapter) {
// Service instance not initialized, try auto-initialization // Service instance not initialized, try auto-initialization
try { 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 // Build configuration object
const serviceConfig = { const serviceConfig = {
...CONFIG, ...CONFIG,
@ -104,7 +105,7 @@ async function getProviderTypeUsage(providerType, currentConfig, providerPoolMan
}; };
adapter = getServiceAdapter(serviceConfig); adapter = getServiceAdapter(serviceConfig);
} catch (initError) { } 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}`; instanceResult.error = `Service instance initialization failed: ${initError.message}`;
result.errorCount++; result.errorCount++;
} }
@ -222,14 +223,14 @@ export async function handleGetUsage(req, res, currentConfig, providerPoolManage
// 优先读取缓存 // 优先读取缓存
const cachedData = await readUsageCache(); const cachedData = await readUsageCache();
if (cachedData) { if (cachedData) {
console.log('[Usage API] Returning cached usage data'); logger.info('[Usage API] Returning cached usage data');
usageResults = { ...cachedData, fromCache: true }; usageResults = { ...cachedData, fromCache: true };
} }
} }
if (!usageResults) { if (!usageResults) {
// 缓存不存在或需要刷新,重新查询 // 缓存不存在或需要刷新,重新查询
console.log('[Usage API] Fetching fresh usage data'); logger.info('[Usage API] Fetching fresh usage data');
usageResults = await getAllProvidersUsage(currentConfig, providerPoolManager); usageResults = await getAllProvidersUsage(currentConfig, providerPoolManager);
// 写入缓存 // 写入缓存
await writeUsageCache(usageResults); await writeUsageCache(usageResults);
@ -239,7 +240,7 @@ export async function handleGetUsage(req, res, currentConfig, providerPoolManage
res.end(JSON.stringify(usageResults)); res.end(JSON.stringify(usageResults));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {
@ -265,14 +266,14 @@ export async function handleGetProviderUsage(req, res, currentConfig, providerPo
// Prefer reading from cache // Prefer reading from cache
const cachedData = await readProviderUsageCache(providerType); const cachedData = await readProviderUsageCache(providerType);
if (cachedData) { if (cachedData) {
console.log(`[Usage API] Returning cached usage data for ${providerType}`); logger.info(`[Usage API] Returning cached usage data for ${providerType}`);
usageResults = cachedData; usageResults = cachedData;
} }
} }
if (!usageResults) { if (!usageResults) {
// Cache does not exist or refresh required, re-query // 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); usageResults = await getProviderTypeUsage(providerType, currentConfig, providerPoolManager);
// 更新缓存 // 更新缓存
await updateProviderUsageCache(providerType, usageResults); await updateProviderUsageCache(providerType, usageResults);
@ -282,7 +283,7 @@ export async function handleGetProviderUsage(req, res, currentConfig, providerPo
res.end(JSON.stringify(usageResults)); res.end(JSON.stringify(usageResults));
return true; return true;
} catch (error) { } 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.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
error: { error: {

View file

@ -1,4 +1,5 @@
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import logger from '../utils/logger.js';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
@ -17,7 +18,7 @@ export async function readUsageCache() {
} }
return null; return null;
} catch (error) { } 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; return null;
} }
} }
@ -29,9 +30,9 @@ export async function readUsageCache() {
export async function writeUsageCache(usageData) { export async function writeUsageCache(usageData) {
try { try {
await fs.writeFile(USAGE_CACHE_FILE, JSON.stringify(usageData, null, 2), 'utf8'); 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) { } catch (error) {
console.error('[Usage Cache] Failed to write usage cache:', error.message); logger.error('[Usage Cache] Failed to write usage cache:', error.message);
} }
} }

View file

@ -2,6 +2,7 @@ import { promises as fs } from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as http from 'http'; // Add http for IncomingMessage and ServerResponse types import * as http from 'http'; // Add http for IncomingMessage and ServerResponse types
import * as crypto from 'crypto'; // Import crypto for MD5 hashing import * as crypto from 'crypto'; // Import crypto for MD5 hashing
import logger from './logger.js';
import { convertData, getOpenAIStreamChunkStop } from '../convert/convert.js'; import { convertData, getOpenAIStreamChunkStop } from '../convert/convert.js';
import { ProviderStrategyFactory } from './provider-strategies.js'; import { ProviderStrategyFactory } from './provider-strategies.js';
import { getPluginManager } from '../core/plugin-manager.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`; const logEntry = `${timestamp} [${type.toUpperCase()}]:\n${content}\n--------------------------------------\n`;
if (logMode === 'console') { if (logMode === 'console') {
console.log(logEntry); logger.info(logEntry);
} else if (logMode === 'file') { } else if (logMode === 'file') {
try { try {
// Append to the file // Append to the file
await fs.appendFile(logFilename, logEntry); await fs.appendFile(logFilename, logEntry);
} catch (err) { } 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; 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; return false;
} }
@ -323,35 +324,35 @@ export async function handleStreamRequest(res, service, model, requestBody, from
// fullOldResponseJson += chunk.type+"\n"; // fullOldResponseJson += chunk.type+"\n";
// fullResponseJson += chunk.type+"\n"; // fullResponseJson += chunk.type+"\n";
res.write(`event: ${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"; // fullOldResponseJson += JSON.stringify(chunk)+"\n";
// fullResponseJson += JSON.stringify(chunk)+"\n\n"; // fullResponseJson += JSON.stringify(chunk)+"\n\n";
res.write(`data: ${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) { if (openStop && needsConversion) {
res.write(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n\n`); 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 // 流式请求成功完成统计使用次数错误次数重置为0
if (providerPoolManager && pooluuid) { if (providerPoolManager && pooluuid) {
const customNameDisplay = customName ? `, ${customName}` : ''; 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, { providerPoolManager.markProviderHealthy(toProvider, {
uuid: pooluuid uuid: pooluuid
}); });
} }
} catch (error) { } 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) { 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); const errorPayload = createStreamErrorResponse(error, fromProvider);
res.write(errorPayload); res.write(errorPayload);
@ -375,9 +376,9 @@ export async function handleStreamRequest(res, service, model, requestBody, from
if (!credentialMarkedUnhealthy && !skipErrorCount && providerPoolManager && pooluuid) { if (!credentialMarkedUnhealthy && !skipErrorCount && providerPoolManager && pooluuid) {
// 400 报错码通常是请求参数问题,不记录为提供商错误 // 400 报错码通常是请求参数问题,不记录为提供商错误
if (status === 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 { } 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, { providerPoolManager.markProviderUnhealthy(toProvider, {
uuid: pooluuid uuid: pooluuid
@ -385,9 +386,9 @@ export async function handleStreamRequest(res, service, model, requestBody, from
credentialMarkedUnhealthy = true; credentialMarkedUnhealthy = true;
} }
} else if (credentialMarkedUnhealthy) { } 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) { } 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) { if (credentialMarkedUnhealthy && currentRetry < maxRetries && providerPoolManager && CONFIG) {
// 增加10秒内的随机等待时间避免所有请求同时切换凭证 // 增加10秒内的随机等待时间避免所有请求同时切换凭证
const randomDelay = Math.floor(Math.random() * 10000); // 0-10000毫秒 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)); await new Promise(resolve => setTimeout(resolve, randomDelay));
try { try {
@ -408,8 +409,8 @@ export async function handleStreamRequest(res, service, model, requestBody, from
const { getApiServiceWithFallback } = await import('../services/service-manager.js'); const { getApiServiceWithFallback } = await import('../services/service-manager.js');
const result = await getApiServiceWithFallback(CONFIG, model); const result = await getApiServiceWithFallback(CONFIG, model);
if (result && result.service && result.uuid !== pooluuid) { if (result && result.service) {
console.log(`[Stream Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`); logger.info(`[Stream Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`);
// 使用新服务重试 // 使用新服务重试
const newRetryContext = { const newRetryContext = {
@ -434,13 +435,11 @@ export async function handleStreamRequest(res, service, model, requestBody, from
result.serviceConfig?.customName || customName, result.serviceConfig?.customName || customName,
newRetryContext newRetryContext
); );
} else if (result && result.uuid === pooluuid) {
console.log(`[Stream Retry] No different healthy credential available. Same credential selected.`);
} else { } else {
console.log(`[Stream Retry] No healthy credential available for retry.`); logger.info(`[Stream Retry] No healthy credential available for retry.`);
} }
} catch (retryError) { } 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. // Convert the response back to the client's format (fromProvider), if necessary.
let clientResponse = nativeResponse; let clientResponse = nativeResponse;
if (needsConversion) { 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); 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 handleUnifiedResponse(res, JSON.stringify(clientResponse), false);
await logConversation('output', responseText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME); await logConversation('output', responseText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME);
// fs.writeFile('oldResponse'+Date.now()+'.json', JSON.stringify(clientResponse)); // fs.writeFile('oldResponse'+Date.now()+'.json', JSON.stringify(clientResponse));
@ -490,13 +489,13 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
// 一元请求成功完成统计使用次数错误次数重置为0 // 一元请求成功完成统计使用次数错误次数重置为0
if (providerPoolManager && pooluuid) { if (providerPoolManager && pooluuid) {
const customNameDisplay = customName ? `, ${customName}` : ''; 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, { providerPoolManager.markProviderHealthy(toProvider, {
uuid: pooluuid uuid: pooluuid
}); });
} }
} catch (error) { } 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; const status = error.response?.status;
@ -513,9 +512,9 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
if (!credentialMarkedUnhealthy && !skipErrorCount && providerPoolManager && pooluuid) { if (!credentialMarkedUnhealthy && !skipErrorCount && providerPoolManager && pooluuid) {
// 400 报错码通常是请求参数问题,不记录为提供商错误 // 400 报错码通常是请求参数问题,不记录为提供商错误
if (status === 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 { } 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, { providerPoolManager.markProviderUnhealthy(toProvider, {
uuid: pooluuid uuid: pooluuid
@ -523,9 +522,9 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
credentialMarkedUnhealthy = true; credentialMarkedUnhealthy = true;
} }
} else if (credentialMarkedUnhealthy) { } 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) { } 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) { if (credentialMarkedUnhealthy && currentRetry < maxRetries && providerPoolManager && CONFIG) {
// 增加10秒内的随机等待时间避免所有请求同时切换凭证 // 增加10秒内的随机等待时间避免所有请求同时切换凭证
const randomDelay = Math.floor(Math.random() * 10000); // 0-10000毫秒 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)); await new Promise(resolve => setTimeout(resolve, randomDelay));
try { try {
@ -546,8 +545,8 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
const { getApiServiceWithFallback } = await import('../services/service-manager.js'); const { getApiServiceWithFallback } = await import('../services/service-manager.js');
const result = await getApiServiceWithFallback(CONFIG, model); const result = await getApiServiceWithFallback(CONFIG, model);
if (result && result.service && result.uuid !== pooluuid) { if (result && result.service) {
console.log(`[Unary Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`); logger.info(`[Unary Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`);
// 使用新服务重试 // 使用新服务重试
const newRetryContext = { const newRetryContext = {
@ -572,13 +571,11 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
result.serviceConfig?.customName || customName, result.serviceConfig?.customName || customName,
newRetryContext newRetryContext
); );
} else if (result && result.uuid === pooluuid) {
console.log(`[Unary Retry] No different healthy credential available. Same credential selected.`);
} else { } else {
console.log(`[Unary Retry] No healthy credential available for retry.`); logger.info(`[Unary Retry] No healthy credential available for retry.`);
} }
} catch (retryError) { } 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. // 2. Convert the model list to the client's expected format, if necessary.
let clientModelList = nativeModelList; let clientModelList = nativeModelList;
if (!getProtocolPrefix(toProvider).includes(getProtocolPrefix(fromProvider))) { 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); clientModelList = convertData(nativeModelList, 'modelList', toProvider, fromProvider);
} else { } 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.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(clientModelList)); res.end(JSON.stringify(clientModelList));
} catch (error) { } 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) { if (providerPoolManager) {
// 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康 // 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康
providerPoolManager.markProviderUnhealthy(toProvider, { providerPoolManager.markProviderUnhealthy(toProvider, {
@ -677,7 +674,7 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
if (!model) { if (!model) {
throw new Error("Could not determine the model from the request."); 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; let actualCustomName = CONFIG.customName;
@ -694,14 +691,14 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
// 如果发生了模型级别的 fallback需要更新请求使用的模型 // 如果发生了模型级别的 fallback需要更新请求使用的模型
if (result.actualModel && result.actualModel !== model) { 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; model = result.actualModel;
} }
if (result.isFallback) { 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 { } 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; let processedRequestBody = originalRequestBody;
// fs.writeFile('originalRequestBody'+Date.now()+'.json', JSON.stringify(originalRequestBody)); // fs.writeFile('originalRequestBody'+Date.now()+'.json', JSON.stringify(originalRequestBody));
if (getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider)) { 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); processedRequestBody = convertData(originalRequestBody, 'request', fromProvider, toProvider);
} else { } 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 // 为 forward provider 添加原始请求路径作为 endpoint
if (requestPath && toProvider === MODEL_PROVIDER.FORWARD_API) { if (requestPath && toProvider === MODEL_PROVIDER.FORWARD_API) {
console.log(`[Forward API] Request path: ${requestPath}`); logger.info(`[Forward API] Request path: ${requestPath}`);
processedRequestBody.endpoint = requestPath; processedRequestBody.endpoint = requestPath;
} }
@ -828,14 +825,14 @@ export function handleError(res, error, provider = null) {
} }
errorMessage = hasOriginalMessage ? error.message.trim() : errorMessage; 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) { if (suggestions.length > 0) {
console.error('[Server] Suggestions:'); logger.error('[Server] Suggestions:');
suggestions.forEach((suggestion, index) => { 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) { if (!res.headersSent) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.writeHead(statusCode, { 'Content-Type': 'application/json' });
@ -1071,7 +1068,7 @@ export function extractSystemPromptFromRequestBody(requestBody, provider) {
} }
break; break;
default: default:
console.warn(`[System Prompt] Unknown provider: ${provider}`); logger.warn(`[System Prompt] Unknown provider: ${provider}`);
break; break;
} }
return incomingSystemText; return incomingSystemText;

353
src/utils/logger.js Normal file
View file

@ -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 };

View file

@ -1,4 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import logger from './logger.js';
import { FETCH_SYSTEM_PROMPT_FILE } from '../utils/common.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'); currentSystemText = await fs.readFile(FETCH_SYSTEM_PROMPT_FILE, 'utf8');
} catch (error) { } catch (error) {
if (error.code !== 'ENOENT') { 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 { try {
if (incomingSystemText && incomingSystemText !== currentSystemText) { if (incomingSystemText && incomingSystemText !== currentSystemText) {
await fs.writeFile(FETCH_SYSTEM_PROMPT_FILE, incomingSystemText); 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) { } else if (!incomingSystemText && currentSystemText) {
await fs.writeFile(FETCH_SYSTEM_PROMPT_FILE, ''); 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) { } 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}`);
} }
} }
} }

View file

@ -4,6 +4,7 @@
*/ */
import * as path from 'path'; import * as path from 'path';
import logger from './logger.js';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
/** /**
@ -163,7 +164,7 @@ export function pathsEqual(path1, path2) {
return false; return false;
} catch (error) { } 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; return false;
} }
} }

View file

@ -4,6 +4,7 @@
*/ */
import { HttpsProxyAgent } from 'https-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent';
import logger from './logger.js';
import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpProxyAgent } from 'http-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent'; import { SocksProxyAgent } from 'socks-proxy-agent';
@ -42,11 +43,11 @@ export function parseProxyUrl(proxyUrl) {
proxyType: 'http' proxyType: 'http'
}; };
} else { } else {
console.warn(`[Proxy] Unsupported proxy protocol: ${protocol}`); logger.warn(`[Proxy] Unsupported proxy protocol: ${protocol}`);
return null; return null;
} }
} catch (error) { } catch (error) {
console.error(`[Proxy] Failed to parse proxy URL: ${error.message}`); logger.error(`[Proxy] Failed to parse proxy URL: ${error.message}`);
return null; return null;
} }
} }
@ -83,7 +84,7 @@ export function getProxyConfigForProvider(config, providerType) {
const proxyConfig = parseProxyUrl(config.PROXY_URL); const proxyConfig = parseProxyUrl(config.PROXY_URL);
if (proxyConfig) { 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; return proxyConfig;
} }

View file

@ -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) { } catch (error) {
console.error('Failed to load configuration:', 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.CRON_REFRESH_TOKEN = document.getElementById('cronRefreshToken')?.checked || false;
config.PROVIDER_POOLS_FILE_PATH = document.getElementById('providerPoolsFilePath')?.value || ''; config.PROVIDER_POOLS_FILE_PATH = document.getElementById('providerPoolsFilePath')?.value || '';
config.MAX_ERROR_COUNT = parseInt(document.getElementById('maxErrorCount')?.value || 10); 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.WARMUP_TARGET = parseInt(document.getElementById('warmupTarget')?.value || 0);
config.REFRESH_CONCURRENCY_PER_PROVIDER = parseInt(document.getElementById('refreshConcurrencyPerProvider')?.value || 1); config.REFRESH_CONCURRENCY_PER_PROVIDER = parseInt(document.getElementById('refreshConcurrencyPerProvider')?.value || 1);
@ -242,6 +260,16 @@ async function saveConfiguration() {
} else { } else {
config.PROXY_ENABLED_PROVIDERS = []; 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 { try {
await window.apiClient.post('/config', config); await window.apiClient.post('/config', config);

View file

@ -22,6 +22,7 @@ const elements = {
get navItems() { return document.querySelectorAll('.nav-item'); }, get navItems() { return document.querySelectorAll('.nav-item'); },
get logsContainer() { return document.getElementById('logsContainer'); }, get logsContainer() { return document.getElementById('logsContainer'); },
get clearLogsBtn() { return document.getElementById('clearLogs'); }, get clearLogsBtn() { return document.getElementById('clearLogs'); },
get downloadLogsBtn() { return document.getElementById('downloadLogs'); },
get toggleAutoScrollBtn() { return document.getElementById('toggleAutoScroll'); }, get toggleAutoScrollBtn() { return document.getElementById('toggleAutoScroll'); },
get saveConfigBtn() { return document.getElementById('saveConfig'); }, get saveConfigBtn() { return document.getElementById('saveConfig'); },
get resetConfigBtn() { return document.getElementById('resetConfig'); }, get resetConfigBtn() { return document.getElementById('resetConfig'); },

View file

@ -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) { if (elements.toggleAutoScrollBtn) {
elements.toggleAutoScrollBtn.addEventListener('click', () => { elements.toggleAutoScrollBtn.addEventListener('click', () => {

View file

@ -68,8 +68,6 @@ function addLogEntry(logData) {
const levelClass = `log-level-${logData.level}`; const levelClass = `log-level-${logData.level}`;
logEntry.innerHTML = ` logEntry.innerHTML = `
<span class="log-time">[${time}]</span>
<span class="${levelClass}">[${logData.level.toUpperCase()}]</span>
<span class="log-message">${escapeHtml(logData.message)}</span> <span class="log-message">${escapeHtml(logData.message)}</span>
`; `;

View file

@ -196,6 +196,9 @@ const translations = {
'config.hostPlaceholder': '例如: 127.0.0.1', 'config.hostPlaceholder': '例如: 127.0.0.1',
'config.port': '端口', 'config.port': '端口',
'config.portPlaceholder': '3000', 'config.portPlaceholder': '3000',
'config.basic.title': '基础设置',
'config.governance.title': '服务治理',
'config.oauth.title': 'OAuth & 令牌',
'config.modelProvider': '模型提供商', 'config.modelProvider': '模型提供商',
'config.modelProviderHelp': '勾选启动时初始化的模型提供商 (必须至少勾选一个)', 'config.modelProviderHelp': '勾选启动时初始化的模型提供商 (必须至少勾选一个)',
'config.modelProviderRequired': '必须至少勾选一个模型提供商', 'config.modelProviderRequired': '必须至少勾选一个模型提供商',
@ -253,7 +256,7 @@ const translations = {
'config.advanced.promptLogMode.none': '无 (none)', 'config.advanced.promptLogMode.none': '无 (none)',
'config.advanced.promptLogMode.console': '控制台 (console)', 'config.advanced.promptLogMode.console': '控制台 (console)',
'config.advanced.promptLogMode.file': '文件 (file)', 'config.advanced.promptLogMode.file': '文件 (file)',
'config.advanced.maxRetries': '最大重试次数', 'config.advanced.maxRetries': '提供商内最大重试次数',
'config.advanced.baseDelay': '重试基础延迟(毫秒)', 'config.advanced.baseDelay': '重试基础延迟(毫秒)',
'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数', 'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数',
'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次', 'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次',
@ -293,6 +296,26 @@ const translations = {
'config.proxy.urlNote': '支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理', 'config.proxy.urlNote': '支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理',
'config.proxy.enabledProviders': '启用代理的提供商', 'config.proxy.enabledProviders': '启用代理的提供商',
'config.proxy.enabledProvidersNote': '选择需要通过代理访问的提供商,未选中的提供商将直接连接', '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.save': '保存配置',
'config.reset': '重置', 'config.reset': '重置',
'config.placeholder.nodeName': '例如: 我的节点1', 'config.placeholder.nodeName': '例如: 我的节点1',
@ -513,6 +536,7 @@ const translations = {
// Logs // Logs
'logs.title': '实时日志', 'logs.title': '实时日志',
'logs.clear': '清空日志', 'logs.clear': '清空日志',
'logs.download': '下载日志',
'logs.autoScroll': '自动滚动', 'logs.autoScroll': '自动滚动',
'logs.autoScroll.on': '自动滚动: 开', 'logs.autoScroll.on': '自动滚动: 开',
'logs.autoScroll.off': '自动滚动: 关', 'logs.autoScroll.off': '自动滚动: 关',
@ -631,9 +655,13 @@ const translations = {
'tutorial.main.prompt.file': '系统提示文件路径', 'tutorial.main.prompt.file': '系统提示文件路径',
'tutorial.main.prompt.mode': '系统提示模式overwrite(覆盖) 或 append(追加)', 'tutorial.main.prompt.mode': '系统提示模式overwrite(覆盖) 或 append(追加)',
'tutorial.main.retry.title': '重试配置', 'tutorial.main.retry.title': '重试配置',
'tutorial.main.retry.max': '最大重试次数', 'tutorial.main.retry.max': '提供商内最大重试次数',
'tutorial.main.retry.delay': '重试基础延迟(毫秒)', 'tutorial.main.retry.delay': '重试基础延迟(毫秒)',
'tutorial.main.retry.credentialSwitch': '坏凭证切换最大重试次数',
'tutorial.main.retry.error': '提供商最大错误次数,超过后标记为不健康', 'tutorial.main.retry.error': '提供商最大错误次数,超过后标记为不健康',
'tutorial.main.governance.warmup': '系统启动时自动刷新的节点数量',
'tutorial.main.governance.concurrency': '每个提供商内部最大并行刷新任务数',
'tutorial.main.governance.poolLimit': '每个提供商类型参与轮询的最大健康凭证数量',
'tutorial.main.example.title': '配置示例', 'tutorial.main.example.title': '配置示例',
'tutorial.pool.title': '提供商池配置 (provider_pools.json)', 'tutorial.pool.title': '提供商池配置 (provider_pools.json)',
'tutorial.pool.desc': '提供商池用于配置多个账号,实现负载均衡和故障转移。每个提供商类型可以配置多个账号节点。', 'tutorial.pool.desc': '提供商池用于配置多个账号,实现负载均衡和故障转移。每个提供商类型可以配置多个账号节点。',
@ -903,6 +931,9 @@ const translations = {
'config.hostPlaceholder': 'e.g.: 127.0.0.1', 'config.hostPlaceholder': 'e.g.: 127.0.0.1',
'config.port': 'Port', 'config.port': 'Port',
'config.portPlaceholder': '3000', 'config.portPlaceholder': '3000',
'config.basic.title': 'Basic Settings',
'config.governance.title': 'Service Governance',
'config.oauth.title': 'OAuth & Tokens',
'config.modelProvider': 'Model Provider', 'config.modelProvider': 'Model Provider',
'config.modelProviderHelp': 'Check model providers to initialize on startup (must select at least one)', 'config.modelProviderHelp': 'Check model providers to initialize on startup (must select at least one)',
'config.modelProviderRequired': 'At least one model provider must be selected', 'config.modelProviderRequired': 'At least one model provider must be selected',
@ -960,7 +991,7 @@ const translations = {
'config.advanced.promptLogMode.none': 'None', 'config.advanced.promptLogMode.none': 'None',
'config.advanced.promptLogMode.console': 'Console', 'config.advanced.promptLogMode.console': 'Console',
'config.advanced.promptLogMode.file': 'File', '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.baseDelay': 'Base Retry Delay (ms)',
'config.advanced.credentialSwitchMaxRetries': 'Credential Switch Max Retries', 'config.advanced.credentialSwitchMaxRetries': 'Credential Switch Max Retries',
'config.advanced.credentialSwitchMaxRetriesNote': 'Max retry count for switching credentials after auth errors (401/403), default 5', '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.urlNote': 'Supports HTTP, HTTPS and SOCKS5 proxies. Leave empty to disable proxy',
'config.proxy.enabledProviders': 'Providers Using Proxy', 'config.proxy.enabledProviders': 'Providers Using Proxy',
'config.proxy.enabledProvidersNote': 'Select providers that should use the proxy. Unselected providers will connect directly', '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.save': 'Save Configuration',
'config.reset': 'Reset', 'config.reset': 'Reset',
'config.placeholder.nodeName': 'e.g.: My Node 1', 'config.placeholder.nodeName': 'e.g.: My Node 1',
@ -1220,6 +1271,7 @@ const translations = {
// Logs // Logs
'logs.title': 'Real-time Logs', 'logs.title': 'Real-time Logs',
'logs.clear': 'Clear Logs', 'logs.clear': 'Clear Logs',
'logs.download': 'Download Logs',
'logs.autoScroll': 'Auto Scroll', 'logs.autoScroll': 'Auto Scroll',
'logs.autoScroll.on': 'Auto Scroll: On', 'logs.autoScroll.on': 'Auto Scroll: On',
'logs.autoScroll.off': 'Auto Scroll: Off', 'logs.autoScroll.off': 'Auto Scroll: Off',
@ -1338,9 +1390,13 @@ const translations = {
'tutorial.main.prompt.file': 'System prompt file path', 'tutorial.main.prompt.file': 'System prompt file path',
'tutorial.main.prompt.mode': 'System prompt mode: overwrite or append', 'tutorial.main.prompt.mode': 'System prompt mode: overwrite or append',
'tutorial.main.retry.title': 'Retry Configuration', '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.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.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.main.example.title': 'Configuration Example',
'tutorial.pool.title': 'Provider Pool Config (provider_pools.json)', '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.', 'tutorial.pool.desc': 'Provider pool configures multiple accounts for load balancing and failover. Each provider type can have multiple account nodes.',

View file

@ -25,6 +25,16 @@ function initNavigation() {
section.classList.remove('active'); section.classList.remove('active');
if (section.id === sectionId) { if (section.id === sectionId) {
section.classList.add('active'); 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'); section.classList.remove('active');
if (section.id === sectionId) { if (section.id === sectionId) {
section.classList.add('active'); section.classList.add('active');
// 如果是日志页面,默认滚动到底部
if (sectionId === 'logs') {
setTimeout(() => {
const logsContainer = document.getElementById('logsContainer');
if (logsContainer) {
logsContainer.scrollTop = logsContainer.scrollHeight;
}
}, 100);
}
} }
}); });

View file

@ -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) { @media (max-width: 768px) {
.oauth-refresh-toggle { .form-row, .config-row {
flex-direction: column; grid-template-columns: 1fr;
align-items: flex-start; gap: 0;
gap: 0.5rem;
} }
.restart-required-modal .restart-modal-content {
max-width: 95%; .config-panel {
padding: 1rem;
}
.config-group-section {
padding: 1rem;
} }
} }

View file

@ -4,128 +4,275 @@
<h2 id="config-title" data-i18n="config.title">配置管理</h2> <h2 id="config-title" data-i18n="config.title">配置管理</h2>
<div class="config-panel"> <div class="config-panel">
<div class="config-form"> <div class="config-form">
<div class="form-group password-input-group"> <!-- 基础设置 -->
<label for="apiKey" data-i18n="config.apiKey">API密钥</label> <div class="config-group-section">
<div class="password-input-wrapper"> <h3 data-i18n="config.basic.title"><i class="fas fa-cog"></i> 基础设置</h3>
<input type="password" id="apiKey" class="form-control" data-i18n="config.apiKeyPlaceholder" placeholder="请输入API密钥" autocomplete="off"> <div class="form-group password-input-group">
<button type="button" class="password-toggle" data-target="apiKey" aria-label="显示/隐藏密码"> <label for="apiKey" data-i18n="config.apiKey">API密钥</label>
<i class="fas fa-eye" aria-hidden="true"></i> <div class="password-input-wrapper">
</button> <input type="password" id="apiKey" class="form-control" data-i18n="config.apiKeyPlaceholder" placeholder="请输入API密钥" autocomplete="off">
<button type="button" class="password-toggle" data-target="apiKey" aria-label="显示/隐藏密码">
<i class="fas fa-eye" aria-hidden="true"></i>
</button>
</div>
</div> </div>
</div> <div class="form-row">
<div class="form-row">
<div class="form-group">
<label for="host" data-i18n="config.host">监听地址</label>
<input type="text" id="host" class="form-control" value="127.0.0.1" data-i18n-placeholder="config.hostPlaceholder" placeholder="例如: 127.0.0.1">
</div>
<div class="form-group">
<label for="port" data-i18n="config.port">端口</label>
<input type="number" id="port" class="form-control" value="3000" data-i18n-placeholder="config.portPlaceholder" placeholder="3000">
</div>
</div>
<div class="form-group pool-section">
<label data-i18n="config.modelProvider">模型提供商 (可多选)</label>
<div id="modelProvider" class="provider-tags">
<button type="button" class="provider-tag" data-value="gemini-cli-oauth">
<i class="fas fa-robot"></i>
<span>Gemini CLI OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="gemini-antigravity">
<i class="fas fa-rocket"></i>
<span>Gemini Antigravity</span>
</button>
<button type="button" class="provider-tag" data-value="openai-custom">
<i class="fas fa-brain"></i>
<span>OpenAI Custom</span>
</button>
<button type="button" class="provider-tag" data-value="claude-custom">
<i class="fas fa-comment-dots"></i>
<span>Claude Custom</span>
</button>
<button type="button" class="provider-tag" data-value="claude-kiro-oauth">
<i class="fas fa-key"></i>
<span>Claude Kiro OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openai-qwen-oauth">
<i class="fas fa-cloud"></i>
<span>Qwen OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openaiResponses-custom">
<i class="fas fa-reply"></i>
<span>OpenAI Responses</span>
</button>
<button type="button" class="provider-tag" data-value="openai-iflow">
<i class="fas fa-stream"></i>
<span>iFlow OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openai-codex-oauth">
<i class="fas fa-code"></i>
<span>OpenAI Codex OAuth</span>
</button>
</div>
<small class="form-text" data-i18n="config.modelProviderHelp">点击选择启动时初始化的模型提供商 (必须至少选择一个)</small>
</div>
<!-- 高级配置区域 -->
<div class="advanced-config-section">
<h3 data-i18n="config.advanced.title"><i class="fas fa-cogs"></i> 高级配置</h3>
<!-- 代理配置 -->
<div class="proxy-config-section">
<h4 data-i18n="config.proxy.title"><i class="fas fa-globe"></i> 代理设置</h4>
<div class="form-group"> <div class="form-group">
<label for="proxyUrl" data-i18n="config.proxy.url">代理地址</label> <label for="host" data-i18n="config.host">监听地址</label>
<input type="text" id="proxyUrl" class="form-control" data-i18n-placeholder="config.proxy.urlPlaceholder" placeholder="例如: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080"> <input type="text" id="host" class="form-control" value="127.0.0.1" data-i18n-placeholder="config.hostPlaceholder" placeholder="例如: 127.0.0.1">
<small class="form-text" data-i18n="config.proxy.urlNote">支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理</small>
</div> </div>
<div class="form-group pool-section"> <div class="form-group">
<label data-i18n="config.proxy.enabledProviders">启用代理的提供商</label> <label for="port" data-i18n="config.port">端口</label>
<div id="proxyProviders" class="provider-tags"> <input type="number" id="port" class="form-control" value="3000" data-i18n-placeholder="config.portPlaceholder" placeholder="3000">
<button type="button" class="provider-tag" data-value="gemini-cli-oauth">
<i class="fas fa-robot"></i>
<span>Gemini CLI OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="gemini-antigravity">
<i class="fas fa-rocket"></i>
<span>Gemini Antigravity</span>
</button>
<button type="button" class="provider-tag" data-value="openai-custom">
<i class="fas fa-brain"></i>
<span>OpenAI Custom</span>
</button>
<button type="button" class="provider-tag" data-value="claude-custom">
<i class="fas fa-comment-dots"></i>
<span>Claude Custom</span>
</button>
<button type="button" class="provider-tag" data-value="claude-kiro-oauth">
<i class="fas fa-key"></i>
<span>Claude Kiro OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openai-qwen-oauth">
<i class="fas fa-cloud"></i>
<span>Qwen OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openaiResponses-custom">
<i class="fas fa-reply"></i>
<span>OpenAI Responses</span>
</button>
<button type="button" class="provider-tag" data-value="openai-iflow">
<i class="fas fa-stream"></i>
<span>iFlow OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openai-codex-oauth">
<i class="fas fa-code"></i>
<span>OpenAI Codex OAuth</span>
</button>
</div>
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
</div> </div>
</div> </div>
<div class="form-group pool-section">
<label data-i18n="config.modelProvider">模型提供商 (可多选)</label>
<div id="modelProvider" class="provider-tags">
<button type="button" class="provider-tag" data-value="gemini-cli-oauth">
<i class="fas fa-robot"></i>
<span>Gemini CLI OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="gemini-antigravity">
<i class="fas fa-rocket"></i>
<span>Gemini Antigravity</span>
</button>
<button type="button" class="provider-tag" data-value="openai-custom">
<i class="fas fa-brain"></i>
<span>OpenAI Custom</span>
</button>
<button type="button" class="provider-tag" data-value="claude-custom">
<i class="fas fa-comment-dots"></i>
<span>Claude Custom</span>
</button>
<button type="button" class="provider-tag" data-value="claude-kiro-oauth">
<i class="fas fa-key"></i>
<span>Claude Kiro OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openai-qwen-oauth">
<i class="fas fa-cloud"></i>
<span>Qwen OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openaiResponses-custom">
<i class="fas fa-reply"></i>
<span>OpenAI Responses</span>
</button>
<button type="button" class="provider-tag" data-value="openai-iflow">
<i class="fas fa-stream"></i>
<span>iFlow OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openai-codex-oauth">
<i class="fas fa-code"></i>
<span>OpenAI Codex OAuth</span>
</button>
</div>
<small class="form-text" data-i18n="config.modelProviderHelp">点击选择启动时初始化的模型提供商 (必须至少选择一个)</small>
</div>
</div>
<!-- 代理与网络 -->
<div class="config-group-section">
<h3 data-i18n="config.proxy.title"><i class="fas fa-globe"></i> 代理设置</h3>
<div class="form-group">
<label for="proxyUrl" data-i18n="config.proxy.url">代理地址</label>
<input type="text" id="proxyUrl" class="form-control" data-i18n-placeholder="config.proxy.urlPlaceholder" placeholder="例如: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080">
<small class="form-text" data-i18n="config.proxy.urlNote">支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理</small>
</div>
<div class="form-group pool-section">
<label data-i18n="config.proxy.enabledProviders">启用代理的提供商</label>
<div id="proxyProviders" class="provider-tags">
<button type="button" class="provider-tag" data-value="gemini-cli-oauth">
<i class="fas fa-robot"></i>
<span>Gemini CLI OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="gemini-antigravity">
<i class="fas fa-rocket"></i>
<span>Gemini Antigravity</span>
</button>
<button type="button" class="provider-tag" data-value="openai-custom">
<i class="fas fa-brain"></i>
<span>OpenAI Custom</span>
</button>
<button type="button" class="provider-tag" data-value="claude-custom">
<i class="fas fa-comment-dots"></i>
<span>Claude Custom</span>
</button>
<button type="button" class="provider-tag" data-value="claude-kiro-oauth">
<i class="fas fa-key"></i>
<span>Claude Kiro OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openai-qwen-oauth">
<i class="fas fa-cloud"></i>
<span>Qwen OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openaiResponses-custom">
<i class="fas fa-reply"></i>
<span>OpenAI Responses</span>
</button>
<button type="button" class="provider-tag" data-value="openai-iflow">
<i class="fas fa-stream"></i>
<span>iFlow OAuth</span>
</button>
<button type="button" class="provider-tag" data-value="openai-codex-oauth">
<i class="fas fa-code"></i>
<span>OpenAI Codex OAuth</span>
</button>
</div>
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
</div>
</div>
<!-- 服务治理与高可用 -->
<div class="config-group-section">
<h3 data-i18n="config.governance.title"><i class="fas fa-shield-alt"></i> 服务治理</h3>
<div class="config-row"> <div class="config-row">
<div class="form-group">
<label for="requestMaxRetries" data-i18n="config.advanced.maxRetries">请求最大重试次数</label>
<input type="number" id="requestMaxRetries" class="form-control" min="0" max="10" value="3">
</div>
<div class="form-group">
<label for="requestBaseDelay" data-i18n="config.advanced.baseDelay">重试基础延迟(毫秒)</label>
<input type="number" id="requestBaseDelay" class="form-control" min="0" step="100" value="1000">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="credentialSwitchMaxRetries" data-i18n="config.advanced.credentialSwitchMaxRetries">坏凭证切换最大重试次数</label>
<input type="number" id="credentialSwitchMaxRetries" class="form-control" min="1" max="50" value="5">
</div>
<div class="form-group">
<label for="maxErrorCount" data-i18n="config.advanced.maxErrorCount">节点最大错误阈值</label>
<input type="number" id="maxErrorCount" class="form-control" value="10" min="1" max="20">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="warmupTarget" data-i18n="config.advanced.warmupTarget">系统预热节点数</label>
<input type="number" id="warmupTarget" class="form-control" min="0" max="100" value="0">
</div>
<div class="form-group">
<label for="refreshConcurrencyPerProvider" data-i18n="config.advanced.refreshConcurrencyPerProvider">提供商刷新并发数</label>
<input type="number" id="refreshConcurrencyPerProvider" class="form-control" min="1" max="10" value="1">
</div>
</div>
<div class="form-group pool-section">
<label for="providerFallbackChain" data-i18n="config.advanced.fallbackChain">跨类型 Fallback 链配置 (JSON)</label>
<textarea id="providerFallbackChain" class="form-control" rows="4" data-i18n-placeholder="config.advanced.fallbackChainPlaceholder"></textarea>
</div>
<div class="form-group pool-section">
<label for="modelFallbackMapping" data-i18n="config.advanced.modelFallbackMapping">跨协议模型 Fallback 映射 (JSON)</label>
<textarea id="modelFallbackMapping" class="form-control" rows="4" data-i18n-placeholder="config.advanced.modelFallbackMappingPlaceholder"></textarea>
</div>
</div>
<!-- OAuth 与令牌 -->
<div class="config-group-section">
<h3 data-i18n="config.oauth.title"><i class="fas fa-key"></i> OAuth & 令牌</h3>
<div class="config-row">
<div class="form-group">
<label for="cronNearMinutes" data-i18n="config.advanced.cronInterval">令牌刷新间隔(分钟)</label>
<input type="number" id="cronNearMinutes" class="form-control" min="1" max="60" value="1">
</div>
<div class="form-group">
<label data-i18n="config.advanced.cronEnabled">启用自动刷新 (需重启)</label>
<label class="toggle-switch">
<input type="checkbox" id="cronRefreshToken">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- 日志管理 -->
<div class="config-group-section">
<h3 data-i18n="config.log.title"><i class="fas fa-file-alt"></i> 日志设置</h3>
<div class="config-row">
<div class="form-group">
<label data-i18n="config.log.enabled">启用日志</label>
<label class="toggle-switch">
<input type="checkbox" id="logEnabled" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="form-group">
<label for="logOutputMode" data-i18n="config.log.outputMode">日志输出模式</label>
<select id="logOutputMode" class="form-control">
<option value="all" selected data-i18n="config.log.outputMode.all">全部 (控制台+文件)</option>
<option value="console" data-i18n="config.log.outputMode.console">仅控制台</option>
<option value="file" data-i18n="config.log.outputMode.file">仅文件</option>
<option value="none" data-i18n="config.log.outputMode.none">禁用</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="logLevel" data-i18n="config.log.level">日志级别</label>
<select id="logLevel" class="form-control">
<option value="debug" data-i18n="config.log.level.debug">调试 (debug)</option>
<option value="info" selected data-i18n="config.log.level.info">信息 (info)</option>
<option value="warn" data-i18n="config.log.level.warn">警告 (warn)</option>
<option value="error" data-i18n="config.log.level.error">错误 (error)</option>
</select>
</div>
<div class="form-group">
<label for="logDir" data-i18n="config.log.dir">日志目录</label>
<input type="text" id="logDir" class="form-control" value="logs">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="logMaxFileSize" data-i18n="config.log.maxFileSize">最大文件大小(字节)</label>
<input type="number" id="logMaxFileSize" class="form-control" value="10485760" step="1048576">
</div>
<div class="form-group">
<label for="logMaxFiles" data-i18n="config.log.maxFiles">最大保留文件数</label>
<input type="number" id="logMaxFiles" class="form-control" value="10" min="1">
</div>
</div>
<div class="config-row">
<div class="form-group">
<label data-i18n="config.log.includeRequestId">包含请求ID</label>
<label class="toggle-switch">
<input type="checkbox" id="logIncludeRequestId" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="form-group">
<label data-i18n="config.log.includeTimestamp">包含时间戳</label>
<label class="toggle-switch">
<input type="checkbox" id="logIncludeTimestamp" checked>
<span class="toggle-slider"></span>
</label>
</div>
</div>
<hr>
<div class="form-row">
<div class="form-group">
<label for="promptLogMode" data-i18n="config.advanced.promptLogMode">提示日志模式</label>
<select id="promptLogMode" class="form-control">
<option value="none" data-i18n="config.advanced.promptLogMode.none">无 (none)</option>
<option value="console" data-i18n="config.advanced.promptLogMode.console">控制台 (console)</option>
<option value="file" data-i18n="config.advanced.promptLogMode.file">文件 (file)</option>
</select>
</div>
<div class="form-group">
<label for="promptLogBaseName" data-i18n="config.advanced.promptLogBaseName">提示日志基础名称</label>
<input type="text" id="promptLogBaseName" class="form-control">
</div>
</div>
</div>
<!-- 系统与高级 -->
<div class="config-group-section">
<h3 data-i18n="config.advanced.title"><i class="fas fa-cogs"></i> 系统与高级</h3>
<div class="form-group">
<label for="providerPoolsFilePath" data-i18n="config.advanced.poolFilePath">提供商池配置文件路径</label>
<input type="text" id="providerPoolsFilePath" class="form-control">
</div>
<div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="systemPromptFilePath" data-i18n="config.advanced.systemPromptFile">系统提示文件路径</label> <label for="systemPromptFilePath" data-i18n="config.advanced.systemPromptFile">系统提示文件路径</label>
<input type="text" id="systemPromptFilePath" class="form-control" value="configs/input_system_prompt.txt" data-i18n-placeholder="config.advanced.systemPromptFilePlaceholder" placeholder="例如: configs/input_system_prompt.txt"> <input type="text" id="systemPromptFilePath" class="form-control">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="systemPromptMode" data-i18n="config.advanced.systemPromptMode">系统提示模式</label> <label for="systemPromptMode" data-i18n="config.advanced.systemPromptMode">系统提示模式</label>
@ -135,121 +282,22 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group">
<div class="config-row"> <label for="systemPrompt" data-i18n="config.advanced.systemPrompt">系统提示内容</label>
<div class="form-group"> <textarea id="systemPrompt" class="form-control" rows="4"></textarea>
<label for="promptLogBaseName" data-i18n="config.advanced.promptLogBaseName">提示日志基础名称</label>
<input type="text" id="promptLogBaseName" class="form-control" data-i18n-placeholder="config.advanced.promptLogBaseNamePlaceholder" placeholder="例如: prompt_log">
</div>
<div class="form-group">
<label for="promptLogMode" data-i18n="config.advanced.promptLogMode">提示日志模式</label>
<select id="promptLogMode" class="form-control">
<option value="none" data-i18n="config.advanced.promptLogMode.none">无 (none)</option>
<option value="console" data-i18n="config.advanced.promptLogMode.console">控制台 (console)</option>
<option value="file" data-i18n="config.advanced.promptLogMode.file">文件 (file)</option>
</select>
</div>
</div> </div>
<div class="form-group">
<div class="config-row">
<div class="form-group">
<label for="requestMaxRetries" data-i18n="config.advanced.maxRetries">最大重试次数</label>
<input type="number" id="requestMaxRetries" class="form-control" min="0" max="10" value="3">
</div>
<div class="form-group">
<label for="requestBaseDelay" data-i18n="config.advanced.baseDelay">重试基础延迟(毫秒)</label>
<input type="number" id="requestBaseDelay" class="form-control" min="0" step="100" value="1000">
</div>
</div>
<div class="config-row">
<div class="form-group pool-section">
<label for="credentialSwitchMaxRetries" data-i18n="config.advanced.credentialSwitchMaxRetries">坏凭证切换最大重试次数</label>
<input type="number" id="credentialSwitchMaxRetries" class="form-control" min="1" max="50" value="5">
<small class="form-text" data-i18n="config.advanced.credentialSwitchMaxRetriesNote">认证错误(401/403)后切换凭证的最大重试次数,默认 5 次</small>
</div>
</div>
<div class="config-row">
<div class="form-group pool-section">
<label for="warmupTarget" data-i18n="config.advanced.warmupTarget">系统预热节点数</label>
<input type="number" id="warmupTarget" class="form-control" min="0" max="100" value="0">
<small class="form-text" data-i18n="config.advanced.warmupTargetNote">系统启动时自动刷新的节点数量,默认为 0</small>
</div>
<div class="form-group pool-section">
<label for="refreshConcurrencyPerProvider" data-i18n="config.advanced.refreshConcurrencyPerProvider">提供商内刷新并发数</label>
<input type="number" id="refreshConcurrencyPerProvider" class="form-control" min="1" max="10" value="1">
<small class="form-text" data-i18n="config.advanced.refreshConcurrencyPerProviderNote">每个提供商内部最大并行刷新任务数,默认为 1</small>
</div>
</div>
<div class="config-row">
<div class="form-group">
<label for="cronNearMinutes" data-i18n="config.advanced.cronInterval">OAuth令牌刷新间隔(分钟)</label>
<input type="number" id="cronNearMinutes" class="form-control" min="1" max="60" value="1">
</div>
<div class="form-group">
<label for="cronNearMinutes" data-i18n="config.advanced.cronEnabled">启用OAuth令牌自动刷新(需重启服务)</label>
<label class="toggle-switch">
<input type="checkbox" id="cronRefreshToken">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="form-group pool-section">
<label for="providerPoolsFilePath" data-i18n="config.advanced.poolFilePath">提供商池配置文件路径(不能为空)</label>
<input type="text" id="providerPoolsFilePath" class="form-control" value="" data-i18n-placeholder="config.advanced.poolFilePathPlaceholder" placeholder="默认: configs/provider_pools.json">
<small class="form-text" data-i18n="config.advanced.poolNote">使用默认路径配置需添加一个空节点</small>
</div>
<div class="form-group pool-section">
<label for="maxErrorCount" data-i18n="config.advanced.maxErrorCount">提供商最大错误次数</label>
<input type="number" id="maxErrorCount" class="form-control" value="10" min="1" max="20" data-i18n-placeholder="config.advanced.maxErrorCountPlaceholder" placeholder="默认: 10">
<small class="form-text" data-i18n="config.advanced.maxErrorCountNote">提供商连续错误达到此次数后将被标记为不健康,默认为 10 次</small>
</div>
<div class="form-group pool-section">
<label for="providerFallbackChain" data-i18n="config.advanced.fallbackChain">跨类型 Fallback 链配置</label>
<textarea id="providerFallbackChain" class="form-control" rows="6" data-i18n-placeholder="config.advanced.fallbackChainPlaceholder" placeholder='例如:
{
"gemini-cli-oauth": ["gemini-antigravity"],
"gemini-antigravity": ["gemini-cli-oauth"],
"claude-kiro-oauth": ["claude-custom"]
}'></textarea>
<small class="form-text" data-i18n="config.advanced.fallbackChainNote">当某一 Provider Type 所有账号都不健康时,自动切换到配置的 Fallback 类型。JSON 格式,键为主类型,值为 Fallback 类型数组(按优先级排序)</small>
</div>
<div class="form-group pool-section">
<label for="modelFallbackMapping" data-i18n="config.advanced.modelFallbackMapping">跨协议模型 Fallback 映射</label>
<textarea id="modelFallbackMapping" class="form-control" rows="6" data-i18n-placeholder="config.advanced.modelFallbackMappingPlaceholder" placeholder='例如:
{
"gemini-claude-opus-4-5-thinking": {
"targetProviderType": "claude-kiro-oauth",
"targetModel": "claude-opus-4-5"
}
}'></textarea>
<small class="form-text" data-i18n="config.advanced.modelFallbackMappingNote">当主 Provider 不可用时,根据模型名映射到其他协议的 Provider 和模型。JSON 格式。</small>
</div>
<!-- 系统提示配置移到最下面 -->
<div class="form-group system-prompt-section">
<label for="systemPrompt" data-i18n="config.advanced.systemPrompt">系统提示</label>
<textarea id="systemPrompt" class="form-control" rows="4" data-i18n-placeholder="config.advanced.systemPromptPlaceholder" placeholder="输入系统提示..."></textarea>
</div>
<!-- 后台登录密码配置 -->
<div class="form-group pool-section">
<label for="adminPassword" data-i18n="config.advanced.adminPassword">后台登录密码</label> <label for="adminPassword" data-i18n="config.advanced.adminPassword">后台登录密码</label>
<div class="password-input-wrapper"> <div class="password-input-wrapper">
<input type="password" id="adminPassword" class="form-control" data-i18n-placeholder="config.advanced.adminPasswordPlaceholder" placeholder="设置后台登录密码(留空则不修改)" autocomplete="new-password"> <input type="password" id="adminPassword" class="form-control" autocomplete="new-password">
<button type="button" class="password-toggle" data-target="adminPassword" aria-label="显示/隐藏密码" data-i18n-aria-label="common.togglePassword"> <button type="button" class="password-toggle" data-target="adminPassword" aria-label="显示/隐藏密码">
<i class="fas fa-eye" aria-hidden="true"></i> <i class="fas fa-eye" aria-hidden="true"></i>
</button> </button>
</div> </div>
<small class="form-text" data-i18n="config.advanced.adminPasswordNote">用于保护管理控制台的访问,修改后需要重新登录</small> <small class="form-text" data-i18n="config.advanced.adminPasswordNote">修改后需要重新登录</small>
</div> </div>
</div>
<div class="form-actions"> <div class="form-actions">
<button class="btn btn-success" id="saveConfig"> <button class="btn btn-success" id="saveConfig">
<i class="fas fa-save"></i> <span data-i18n="config.save">保存配置</span> <i class="fas fa-save"></i> <span data-i18n="config.save">保存配置</span>
@ -260,4 +308,4 @@
</div> </div>
</div> </div>
</div> </div>
</section> </section>

View file

@ -6,6 +6,9 @@
<button class="btn btn-danger" id="clearLogs" aria-label="Clear All Logs" data-i18n-aria-label="logs.clear"> <button class="btn btn-danger" id="clearLogs" aria-label="Clear All Logs" data-i18n-aria-label="logs.clear">
<i class="fas fa-trash" aria-hidden="true"></i> <span data-i18n="logs.clear">清空日志</span> <i class="fas fa-trash" aria-hidden="true"></i> <span data-i18n="logs.clear">清空日志</span>
</button> </button>
<button class="btn btn-secondary" id="downloadLogs" aria-label="Download Today Logs" data-i18n-aria-label="logs.download">
<i class="fas fa-download" aria-hidden="true"></i> <span data-i18n="logs.download">下载日志</span>
</button>
<button class="btn btn-primary" id="toggleAutoScroll" data-enabled="true" aria-label="Toggle Auto-scroll" data-i18n-aria-label="logs.autoScroll"> <button class="btn btn-primary" id="toggleAutoScroll" data-enabled="true" aria-label="Toggle Auto-scroll" data-i18n-aria-label="logs.autoScroll">
<i class="fas fa-arrow-down" aria-hidden="true"></i> <span data-i18n="logs.autoScroll.on">自动滚动: 开</span> <i class="fas fa-arrow-down" aria-hidden="true"></i> <span data-i18n="logs.autoScroll.on">自动滚动: 开</span>
</button> </button>

View file

@ -50,8 +50,9 @@
<div class="tutorial-panel"> <div class="tutorial-panel">
<h3><i class="fas fa-cogs"></i> <span data-i18n="tutorial.main.title">主配置详解 (config.json)</span></h3> <h3><i class="fas fa-cogs"></i> <span data-i18n="tutorial.main.title">主配置详解 (config.json)</span></h3>
<div class="tutorial-content"> <div class="tutorial-content">
<!-- 基础设置 -->
<div class="config-section"> <div class="config-section">
<h4 data-i18n="tutorial.main.basic.title">基础配</h4> <h4 data-i18n="config.basic.title">基础设</h4>
<div class="config-table"> <div class="config-table">
<table> <table>
<thead> <thead>
@ -92,38 +93,37 @@
</div> </div>
</div> </div>
<!-- 代理设置 -->
<div class="config-section"> <div class="config-section">
<h4 data-i18n="tutorial.main.prompt.title">系统提示配</h4> <h4 data-i18n="config.proxy.title">代理设</h4>
<div class="config-table"> <div class="config-table">
<table> <table>
<thead> <thead>
<tr> <tr>
<th data-i18n="tutorial.main.table.param">参数</th> <th data-i18n="tutorial.main.table.param">参数</th>
<th data-i18n="tutorial.main.table.type">类型</th> <th data-i18n="tutorial.main.table.type">类型</th>
<th data-i18n="tutorial.main.table.default">默认值</th>
<th data-i18n="tutorial.main.table.desc">说明</th> <th data-i18n="tutorial.main.table.desc">说明</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><code>SYSTEM_PROMPT_FILE_PATH</code></td> <td><code>PROXY_URL</code></td>
<td>string</td> <td>string</td>
<td>-</td> <td data-i18n="tutorial.proxy.url">代理地址,支持 HTTP、HTTPS、SOCKS5</td>
<td data-i18n="tutorial.main.prompt.file">系统提示文件路径</td>
</tr> </tr>
<tr> <tr>
<td><code>SYSTEM_PROMPT_MODE</code></td> <td><code>PROXY_ENABLED_PROVIDERS</code></td>
<td>string</td> <td>array</td>
<td>overwrite</td> <td data-i18n="tutorial.proxy.providers">启用代理的提供商列表</td>
<td data-i18n="tutorial.main.prompt.mode">系统提示模式overwrite(覆盖) 或 append(追加)</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<!-- 服务治理 -->
<div class="config-section"> <div class="config-section">
<h4 data-i18n="tutorial.main.retry.title">重试配置</h4> <h4 data-i18n="config.governance.title">服务治理</h4>
<div class="config-table"> <div class="config-table">
<table> <table>
<thead> <thead>
@ -147,12 +147,63 @@
<td>1000</td> <td>1000</td>
<td data-i18n="tutorial.main.retry.delay">重试基础延迟(毫秒)</td> <td data-i18n="tutorial.main.retry.delay">重试基础延迟(毫秒)</td>
</tr> </tr>
<tr>
<td><code>CREDENTIAL_SWITCH_MAX_RETRIES</code></td>
<td>number</td>
<td>5</td>
<td data-i18n="tutorial.main.retry.credentialSwitch">坏凭证切换最大重试次数</td>
</tr>
<tr> <tr>
<td><code>MAX_ERROR_COUNT</code></td> <td><code>MAX_ERROR_COUNT</code></td>
<td>number</td> <td>number</td>
<td>10</td> <td>10</td>
<td data-i18n="tutorial.main.retry.error">提供商最大错误次数,超过后标记为不健康</td> <td data-i18n="tutorial.main.retry.error">提供商最大错误次数,超过后标记为不健康</td>
</tr> </tr>
<tr>
<td><code>WARMUP_TARGET</code></td>
<td>number</td>
<td>0</td>
<td data-i18n="tutorial.main.governance.warmup">系统启动时自动刷新的节点数量</td>
</tr>
<tr>
<td><code>REFRESH_CONCURRENCY_PER_PROVIDER</code></td>
<td>number</td>
<td>1</td>
<td data-i18n="tutorial.main.governance.concurrency">提供商内刷新并发数</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 日志设置 -->
<div class="config-section">
<h4 data-i18n="config.log.title">日志设置</h4>
<div class="config-table">
<table>
<thead>
<tr>
<th data-i18n="tutorial.main.table.param">参数</th>
<th data-i18n="tutorial.main.table.type">类型</th>
<th data-i18n="tutorial.main.table.desc">说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>LOG_ENABLED</code></td>
<td>boolean</td>
<td data-i18n="config.log.enabled">启用日志</td>
</tr>
<tr>
<td><code>LOG_OUTPUT_MODE</code></td>
<td>string</td>
<td data-i18n="config.log.outputMode">日志输出模式 (all/console/file/none)</td>
</tr>
<tr>
<td><code>PROMPT_LOG_MODE</code></td>
<td>string</td>
<td data-i18n="tutorial.log.mode">提示词日志模式none(关闭)、console(控制台)、file(文件)</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -164,13 +215,14 @@
"REQUIRED_API_KEY": "your-api-key", "REQUIRED_API_KEY": "your-api-key",
"SERVER_PORT": 3000, "SERVER_PORT": 3000,
"HOST": "0.0.0.0", "HOST": "0.0.0.0",
"MODEL_PROVIDER": "gemini-cli-oauth", "MODEL_PROVIDER": "gemini-cli-oauth,claude-kiro-oauth",
"SYSTEM_PROMPT_FILE_PATH": "configs/input_system_prompt.txt", "PROXY_URL": "http://127.0.0.1:7890",
"SYSTEM_PROMPT_MODE": "overwrite", "PROXY_ENABLED_PROVIDERS": ["gemini-cli-oauth", "claude-kiro-oauth"],
"REQUEST_MAX_RETRIES": 3, "REQUEST_MAX_RETRIES": 3,
"REQUEST_BASE_DELAY": 1000,
"MAX_ERROR_COUNT": 10, "MAX_ERROR_COUNT": 10,
"PROVIDER_POOLS_FILE_PATH": "configs/provider_pools.json" "WARMUP_TARGET": 5,
"LOG_ENABLED": true,
"LOG_OUTPUT_MODE": "all"
}</code></pre> }</code></pre>
</div> </div>
</div> </div>
@ -244,13 +296,6 @@
"oauthCredsFilePath": "configs/gemini/oauth_creds_1.json", "oauthCredsFilePath": "configs/gemini/oauth_creds_1.json",
"checkHealth": true, "checkHealth": true,
"checkModel": "gemini-2.0-flash-exp" "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": [ "claude-kiro-oauth": [
@ -258,8 +303,7 @@
"uuid": "kiro-account-1", "uuid": "kiro-account-1",
"name": "Kiro 账号 1", "name": "Kiro 账号 1",
"oauthCredsFilePath": "configs/kiro/kiro-auth-token.json", "oauthCredsFilePath": "configs/kiro/kiro-auth-token.json",
"checkHealth": true, "checkHealth": true
"checkModel": "claude-sonnet-4-5"
} }
] ]
}</code></pre> }</code></pre>
@ -294,10 +338,6 @@
"gemini-claude-opus-4-5-thinking": { "gemini-claude-opus-4-5-thinking": {
"targetProviderType": "claude-kiro-oauth", "targetProviderType": "claude-kiro-oauth",
"targetModel": "claude-opus-4-5" "targetModel": "claude-opus-4-5"
},
"claude-opus-4-5": {
"targetProviderType": "gemini-antigravity",
"targetModel": "gemini-claude-opus-4-5-thinking"
} }
} }
}</code></pre> }</code></pre>
@ -305,91 +345,54 @@
</div> </div>
</div> </div>
<!-- 代理配置 --> <!-- OAuth 授权路径 -->
<div class="tutorial-panel"> <div class="tutorial-panel">
<h3><i class="fas fa-globe"></i> <span data-i18n="tutorial.proxy.title">代理配置</span></h3> <h3><i class="fas fa-key"></i> <span data-i18n="tutorial.oauth.title">OAuth 授权路径</span></h3>
<div class="tutorial-content"> <div class="tutorial-content">
<p data-i18n="tutorial.proxy.desc">支持为特定提供商配置代理,用于网络受限环境。</p> <p data-i18n="tutorial.oauth.desc">各提供商的 OAuth 凭据文件默认存储位置(建议保持在 <code>configs/</code> 目录下以便统一管理):</p>
<div class="config-section">
<h4 data-i18n="tutorial.proxy.config.title">代理配置参数</h4>
<div class="config-table">
<table>
<thead>
<tr>
<th data-i18n="tutorial.main.table.param">参数</th>
<th data-i18n="tutorial.main.table.type">类型</th>
<th data-i18n="tutorial.main.table.desc">说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>PROXY_URL</code></td>
<td>string</td>
<td data-i18n="tutorial.proxy.url">代理地址,支持 HTTP、HTTPS、SOCKS5</td>
</tr>
<tr>
<td><code>PROXY_ENABLED_PROVIDERS</code></td>
<td>array</td>
<td data-i18n="tutorial.proxy.providers">启用代理的提供商列表</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="config-example">
<h4 data-i18n="tutorial.proxy.example.title">配置示例</h4>
<pre><code>{
"PROXY_URL": "http://127.0.0.1:7890",
"PROXY_ENABLED_PROVIDERS": [
"gemini-cli-oauth",
"gemini-antigravity"
]
}</code></pre>
</div>
<div class="tutorial-note">
<i class="fas fa-info-circle"></i>
<span data-i18n="tutorial.proxy.note">支持的代理类型HTTP (http://)、HTTPS (https://)、SOCKS5 (socks5://)</span>
</div>
</div>
</div>
<!-- OAuth 授权配置 -->
<div class="tutorial-panel">
<h3><i class="fas fa-key"></i> <span data-i18n="tutorial.oauth.title">OAuth 授权配置</span></h3>
<div class="tutorial-content">
<p data-i18n="tutorial.oauth.desc">各提供商的 OAuth 凭据文件默认存储位置:</p>
<div class="oauth-path-list"> <div class="oauth-path-list">
<div class="oauth-path-item"> <div class="oauth-path-item">
<div class="path-header"> <div class="path-header">
<i class="fas fa-gem"></i> <i class="fas fa-robot"></i>
<span class="path-provider">Gemini</span> <span class="path-provider">Gemini CLI OAuth</span>
</div> </div>
<code class="path-value">~/.gemini/oauth_creds.json</code> <code class="path-value">configs/gemini/</code>
</div> </div>
<div class="oauth-path-item"> <div class="oauth-path-item">
<div class="path-header"> <div class="path-header">
<i class="fas fa-rocket"></i> <i class="fas fa-rocket"></i>
<span class="path-provider">Antigravity</span> <span class="path-provider">Gemini Antigravity</span>
</div> </div>
<code class="path-value">~/.antigravity/oauth_creds.json</code> <code class="path-value">configs/antigravity/</code>
</div> </div>
<div class="oauth-path-item"> <div class="oauth-path-item">
<div class="path-header"> <div class="path-header">
<i class="fas fa-robot"></i> <i class="fas fa-key"></i>
<span class="path-provider">Kiro</span> <span class="path-provider">Claude Kiro OAuth</span>
</div> </div>
<code class="path-value">~/.aws/sso/cache/kiro-auth-token.json</code> <code class="path-value">configs/kiro/</code>
</div>
<div class="oauth-path-item">
<div class="path-header">
<i class="fas fa-cloud"></i>
<span class="path-provider">Qwen OAuth</span>
</div>
<code class="path-value">configs/qwen/</code>
</div>
<div class="oauth-path-item">
<div class="path-header">
<i class="fas fa-stream"></i>
<span class="path-provider">iFlow OAuth</span>
</div>
<code class="path-value">configs/iflow/</code>
</div> </div>
<div class="oauth-path-item"> <div class="oauth-path-item">
<div class="path-header"> <div class="path-header">
<i class="fas fa-code"></i> <i class="fas fa-code"></i>
<span class="path-provider">Qwen</span> <span class="path-provider">OpenAI Codex OAuth</span>
</div> </div>
<code class="path-value">~/.qwen/oauth_creds.json</code> <code class="path-value">configs/codex/</code>
</div> </div>
</div> </div>
@ -399,45 +402,4 @@
</div> </div>
</div> </div>
</div> </div>
</section>
<!-- 日志配置 -->
<div class="tutorial-panel">
<h3><i class="fas fa-file-alt"></i> <span data-i18n="tutorial.log.title">日志配置</span></h3>
<div class="tutorial-content">
<div class="config-section">
<h4 data-i18n="tutorial.log.prompt.title">提示日志配置</h4>
<div class="config-table">
<table>
<thead>
<tr>
<th data-i18n="tutorial.main.table.param">参数</th>
<th data-i18n="tutorial.main.table.type">类型</th>
<th data-i18n="tutorial.main.table.desc">说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>PROMPT_LOG_MODE</code></td>
<td>string</td>
<td data-i18n="tutorial.log.mode">日志模式none(关闭)、console(控制台)、file(文件)</td>
</tr>
<tr>
<td><code>PROMPT_LOG_BASE_NAME</code></td>
<td>string</td>
<td data-i18n="tutorial.log.basename">日志文件基础名称</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="config-example">
<h4 data-i18n="tutorial.log.example.title">配置示例</h4>
<pre><code>{
"PROMPT_LOG_MODE": "file",
"PROMPT_LOG_BASE_NAME": "prompt_log"
}</code></pre>
</div>
</div>
</div>
</section>