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_ENABLED_PROVIDERS": [
"gemini-cli-oauth",
"gemini-antigravity"
]
}
"gemini-cli-oauth",
"gemini-antigravity"
],
"LOG_ENABLED": true,
"LOG_OUTPUT_MODE": "all",
"LOG_LEVEL": "info",
"LOG_DIR": "logs",
"LOG_INCLUDE_REQUEST_ID": true,
"LOG_INCLUDE_TIMESTAMP": true,
"LOG_MAX_FILE_SIZE": 10485760,
"LOG_MAX_FILES": 10
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@
*/
import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js';
import logger from '../utils/logger.js';
/**
* 转换器工厂单例模式 + 工厂模式
@ -125,7 +126,7 @@ export class ContentProcessorFactory {
static createProcessor(sourceFormat, targetFormat) {
// 这里返回null实际使用时需要导入具体的处理器类
// 为了避免循环依赖,处理器类应该在使用时动态导入
console.warn(`Content processor for ${sourceFormat} to ${targetFormat} not yet implemented`);
logger.warn(`Content processor for ${sourceFormat} to ${targetFormat} not yet implemented`);
return null;
}
@ -166,7 +167,7 @@ export class ToolProcessorFactory {
* @returns {ToolProcessor} 工具处理器实例
*/
static createProcessor(sourceFormat, targetFormat) {
console.warn(`Tool processor for ${sourceFormat} to ${targetFormat} not yet implemented`);
logger.warn(`Tool processor for ${sourceFormat} to ${targetFormat} not yet implemented`);
return null;
}

View file

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

View file

@ -4,6 +4,7 @@
*/
import { v4 as uuidv4 } from 'uuid';
import logger from '../../utils/logger.js';
import { BaseConverter } from '../BaseConverter.js';
import { CodexConverter } from './CodexConverter.js';
import {
@ -1172,10 +1173,10 @@ export class OpenAIConverter extends BaseConverter {
// But this parameter cannot be added when using tools (causes 400 error)
const hasTools = tools && Array.isArray(tools) && tools.length > 0;
if (!hasTools && model && (model.includes('2.5') || model.includes('thinking') || model.includes('2.0-flash-thinking'))) {
console.log(`[OpenAI->Gemini] Adding responseModalities: ["TEXT"] for model: ${model}`);
logger.info(`[OpenAI->Gemini] Adding responseModalities: ["TEXT"] for model: ${model}`);
config.responseModalities = ["TEXT"];
} else if (hasTools && model && (model.includes('2.5') || model.includes('thinking') || model.includes('2.0-flash-thinking'))) {
console.log(`[OpenAI->Gemini] Skipping responseModalities for model ${model} because tools are present`);
logger.info(`[OpenAI->Gemini] Skipping responseModalities for model ${model} because tools are present`);
}
return config;

View file

@ -4,6 +4,7 @@
*/
import { v4 as uuidv4 } from 'uuid';
import logger from '../utils/logger.js';
// =============================================================================
// 常量定义
@ -327,14 +328,14 @@ export function mapFinishReason(reason, sourceFormat, targetFormat) {
*/
export function determineReasoningEffortFromBudget(budgetTokens) {
if (budgetTokens === null || budgetTokens === undefined) {
console.info("No budget_tokens provided, defaulting to reasoning_effort='high'");
logger.info("No budget_tokens provided, defaulting to reasoning_effort='high'");
return "high";
}
const LOW_THRESHOLD = 50;
const HIGH_THRESHOLD = 200;
console.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`);
logger.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`);
let effort;
if (budgetTokens <= LOW_THRESHOLD) {
@ -345,7 +346,7 @@ export function determineReasoningEffortFromBudget(budgetTokens) {
effort = "high";
}
console.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`);
logger.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`);
return effort;
}

View file

@ -1,6 +1,7 @@
import * as fs from 'fs';
import { promises as pfs } from 'fs';
import { INPUT_SYSTEM_PROMPT_FILE, MODEL_PROVIDER } from '../utils/common.js';
import logger from '../utils/logger.js';
export let CONFIG = {}; // Make CONFIG exportable
export let PROMPT_LOG_FILENAME = ''; // Make PROMPT_LOG_FILENAME exportable
@ -21,7 +22,7 @@ function normalizeConfiguredProviders(config) {
}
const matched = ALL_MODEL_PROVIDERS.find((provider) => provider.toLowerCase() === trimmed.toLowerCase());
if (!matched) {
console.warn(`[Config Warning] Unknown model provider '${trimmed}'. This entry will be ignored.`);
logger.warn(`[Config Warning] Unknown model provider '${trimmed}'. This entry will be ignored.`);
return;
}
if (!dedupedProviders.includes(matched)) {
@ -58,9 +59,9 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
try {
const configData = fs.readFileSync(configFilePath, 'utf8');
currentConfig = JSON.parse(configData);
console.log('[Config] Loaded configuration from configs/config.json');
logger.info('[Config] Loaded configuration from configs/config.json');
} catch (error) {
console.error('[Config Error] Failed to load configs/config.json:', error.message);
logger.error('[Config Error] Failed to load configs/config.json:', error.message);
// Fallback to default values if config.json is not found or invalid
currentConfig = {
REQUIRED_API_KEY: "123456",
@ -80,9 +81,17 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
CRON_REFRESH_TOKEN: false,
PROVIDER_POOLS_FILE_PATH: null, // 新增号池配置文件路径
MAX_ERROR_COUNT: 10, // 提供商最大错误次数
providerFallbackChain: {} // 跨类型 Fallback 链配置
providerFallbackChain: {}, // 跨类型 Fallback 链配置
LOG_ENABLED: true,
LOG_OUTPUT_MODE: "all",
LOG_LEVEL: "info",
LOG_DIR: "logs",
LOG_INCLUDE_REQUEST_ID: true,
LOG_INCLUDE_TIMESTAMP: true,
LOG_MAX_FILE_SIZE: 10485760,
LOG_MAX_FILES: 10
};
console.log('[Config] Using default configuration.');
logger.info('[Config] Using default configuration.');
}
// Parse command-line arguments
@ -92,7 +101,7 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
currentConfig.REQUIRED_API_KEY = args[i + 1];
i++;
} else {
console.warn(`[Config Warning] --api-key flag requires a value.`);
logger.warn(`[Config Warning] --api-key flag requires a value.`);
}
} else if (args[i] === '--log-prompts') {
if (i + 1 < args.length) {
@ -100,32 +109,32 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
if (mode === 'console' || mode === 'file') {
currentConfig.PROMPT_LOG_MODE = mode;
} else {
console.warn(`[Config Warning] Invalid mode for --log-prompts. Expected 'console' or 'file'. Prompt logging is disabled.`);
logger.warn(`[Config Warning] Invalid mode for --log-prompts. Expected 'console' or 'file'. Prompt logging is disabled.`);
}
i++;
} else {
console.warn(`[Config Warning] --log-prompts flag requires a value.`);
logger.warn(`[Config Warning] --log-prompts flag requires a value.`);
}
} else if (args[i] === '--port') {
if (i + 1 < args.length) {
currentConfig.SERVER_PORT = parseInt(args[i + 1], 10);
i++;
} else {
console.warn(`[Config Warning] --port flag requires a value.`);
logger.warn(`[Config Warning] --port flag requires a value.`);
}
} else if (args[i] === '--model-provider') {
if (i + 1 < args.length) {
currentConfig.MODEL_PROVIDER = args[i + 1];
i++;
} else {
console.warn(`[Config Warning] --model-provider flag requires a value.`);
logger.warn(`[Config Warning] --model-provider flag requires a value.`);
}
} else if (args[i] === '--system-prompt-file') {
if (i + 1 < args.length) {
currentConfig.SYSTEM_PROMPT_FILE_PATH = args[i + 1];
i++;
} else {
console.warn(`[Config Warning] --system-prompt-file flag requires a value.`);
logger.warn(`[Config Warning] --system-prompt-file flag requires a value.`);
}
} else if (args[i] === '--system-prompt-mode') {
if (i + 1 < args.length) {
@ -133,53 +142,53 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
if (mode === 'overwrite' || mode === 'append') {
currentConfig.SYSTEM_PROMPT_MODE = mode;
} else {
console.warn(`[Config Warning] Invalid mode for --system-prompt-mode. Expected 'overwrite' or 'append'. Using default 'overwrite'.`);
logger.warn(`[Config Warning] Invalid mode for --system-prompt-mode. Expected 'overwrite' or 'append'. Using default 'overwrite'.`);
}
i++;
} else {
console.warn(`[Config Warning] --system-prompt-mode flag requires a value.`);
logger.warn(`[Config Warning] --system-prompt-mode flag requires a value.`);
}
} else if (args[i] === '--host') {
if (i + 1 < args.length) {
currentConfig.HOST = args[i + 1];
i++;
} else {
console.warn(`[Config Warning] --host flag requires a value.`);
logger.warn(`[Config Warning] --host flag requires a value.`);
}
} else if (args[i] === '--prompt-log-base-name') {
if (i + 1 < args.length) {
currentConfig.PROMPT_LOG_BASE_NAME = args[i + 1];
i++;
} else {
console.warn(`[Config Warning] --prompt-log-base-name flag requires a value.`);
logger.warn(`[Config Warning] --prompt-log-base-name flag requires a value.`);
}
} else if (args[i] === '--cron-near-minutes') {
if (i + 1 < args.length) {
currentConfig.CRON_NEAR_MINUTES = parseInt(args[i + 1], 10);
i++;
} else {
console.warn(`[Config Warning] --cron-near-minutes flag requires a value.`);
logger.warn(`[Config Warning] --cron-near-minutes flag requires a value.`);
}
} else if (args[i] === '--cron-refresh-token') {
if (i + 1 < args.length) {
currentConfig.CRON_REFRESH_TOKEN = args[i + 1].toLowerCase() === 'true';
i++;
} else {
console.warn(`[Config Warning] --cron-refresh-token flag requires a value.`);
logger.warn(`[Config Warning] --cron-refresh-token flag requires a value.`);
}
} else if (args[i] === '--provider-pools-file') {
if (i + 1 < args.length) {
currentConfig.PROVIDER_POOLS_FILE_PATH = args[i + 1];
i++;
} else {
console.warn(`[Config Warning] --provider-pools-file flag requires a value.`);
logger.warn(`[Config Warning] --provider-pools-file flag requires a value.`);
}
} else if (args[i] === '--max-error-count') {
if (i + 1 < args.length) {
currentConfig.MAX_ERROR_COUNT = parseInt(args[i + 1], 10);
i++;
} else {
console.warn(`[Config Warning] --max-error-count flag requires a value.`);
logger.warn(`[Config Warning] --max-error-count flag requires a value.`);
}
}
}
@ -199,9 +208,9 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
try {
const poolsData = await pfs.readFile(currentConfig.PROVIDER_POOLS_FILE_PATH, 'utf8');
currentConfig.providerPools = JSON.parse(poolsData);
console.log(`[Config] Loaded provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}`);
logger.info(`[Config] Loaded provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}`);
} catch (error) {
console.error(`[Config Error] Failed to load provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}: ${error.message}`);
logger.error(`[Config Error] Failed to load provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}: ${error.message}`);
currentConfig.providerPools = {};
}
} else {
@ -220,6 +229,22 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
// Assign to the exported CONFIG
Object.assign(CONFIG, currentConfig);
// Initialize logger
logger.initialize({
enabled: CONFIG.LOG_ENABLED,
outputMode: CONFIG.LOG_OUTPUT_MODE,
logLevel: CONFIG.LOG_LEVEL,
logDir: CONFIG.LOG_DIR,
includeRequestId: CONFIG.LOG_INCLUDE_REQUEST_ID,
includeTimestamp: CONFIG.LOG_INCLUDE_TIMESTAMP,
maxFileSize: CONFIG.LOG_MAX_FILE_SIZE,
maxFiles: CONFIG.LOG_MAX_FILES
});
// Cleanup old logs periodically
logger.cleanupOldLogs();
return CONFIG;
}
@ -233,9 +258,9 @@ export async function getSystemPromptFileContent(filePath) {
await pfs.access(filePath, pfs.constants.F_OK);
} catch (error) {
if (error.code === 'ENOENT') {
console.warn(`[System Prompt] Specified system prompt file not found: ${filePath}`);
logger.warn(`[System Prompt] Specified system prompt file not found: ${filePath}`);
} else {
console.error(`[System Prompt] Error accessing system prompt file ${filePath}: ${error.message}`);
logger.error(`[System Prompt] Error accessing system prompt file ${filePath}: ${error.message}`);
}
return null;
}
@ -245,10 +270,10 @@ export async function getSystemPromptFileContent(filePath) {
if (!content.trim()) {
return null;
}
console.log(`[System Prompt] Loaded system prompt from ${filePath}`);
logger.info(`[System Prompt] Loaded system prompt from ${filePath}`);
return content;
} catch (error) {
console.error(`[System Prompt] Error reading system prompt file ${filePath}: ${error.message}`);
logger.error(`[System Prompt] Error reading system prompt file ${filePath}: ${error.message}`);
return null;
}
}

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import deepmerge from 'deepmerge';
import logger from '../utils/logger.js';
import { handleError } from '../utils/common.js';
import { handleUIApiRequests, serveStaticFiles } from '../services/ui-manager.js';
import { handleAPIRequests } from '../services/api-manager.js';
@ -8,7 +9,14 @@ import { MODEL_PROVIDER } from '../utils/common.js';
import { PROMPT_LOG_FILENAME } from '../core/config-manager.js';
import { handleOllamaRequest, handleOllamaShow } from './ollama-handler.js';
import { getPluginManager } from '../core/plugin-manager.js';
import { randomUUID } from 'crypto';
/**
* Generate a short unique request ID (8 characters)
*/
function generateRequestId() {
return randomUUID().slice(0, 8);
}
/**
* Parse request body as JSON
*/
@ -36,6 +44,10 @@ function parseRequestBody(req) {
*/
export function createRequestHandler(config, providerPoolManager) {
return async function requestHandler(req, res) {
// Generate unique request ID and set it in logger context
const requestId = generateRequestId();
logger.setRequestContext(requestId);
// Deep copy the config for each request to allow dynamic modification
const currentConfig = deepmerge({}, config);
const requestUrl = new URL(req.url, `http://${req.headers.host}`);
@ -77,8 +89,8 @@ export function createRequestHandler(config, providerPoolManager) {
return true;
}
console.log(`\n${new Date().toLocaleString()}`);
console.log(`[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`);
// logger.info(`\n${new Date().toLocaleString()}`);
logger.info(`[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`);
// Health check endpoint
if (method === 'GET' && path === '/health') {
@ -118,7 +130,7 @@ export function createRequestHandler(config, providerPoolManager) {
}));
return true;
} catch (error) {
console.log(`[Server] req provider_health error: ${error.message}`);
logger.info(`[Server] req provider_health error: ${error.message}`);
handleError(res, { statusCode: 500, message: `Failed to get providers health: ${error.message}` }, currentConfig.MODEL_PROVIDER);
return;
}
@ -130,7 +142,7 @@ export function createRequestHandler(config, providerPoolManager) {
const modelProviderHeader = req.headers['model-provider'];
if (modelProviderHeader) {
currentConfig.MODEL_PROVIDER = modelProviderHeader;
console.log(`[Config] MODEL_PROVIDER overridden by header to: ${currentConfig.MODEL_PROVIDER}`);
logger.info(`[Config] MODEL_PROVIDER overridden by header to: ${currentConfig.MODEL_PROVIDER}`);
}
// Check if the first path segment matches a MODEL_PROVIDER and switch if it does
@ -143,12 +155,12 @@ export function createRequestHandler(config, providerPoolManager) {
const isValidProvider = Object.values(MODEL_PROVIDER).includes(firstSegment);
if (firstSegment && isValidProvider) {
currentConfig.MODEL_PROVIDER = firstSegment;
console.log(`[Config] MODEL_PROVIDER overridden by path segment to: ${currentConfig.MODEL_PROVIDER}`);
logger.info(`[Config] MODEL_PROVIDER overridden by path segment to: ${currentConfig.MODEL_PROVIDER}`);
pathSegments.shift();
path = '/' + pathSegments.join('/');
requestUrl.pathname = path;
} else if (firstSegment && !isValidProvider) {
console.log(`[Config] Ignoring invalid MODEL_PROVIDER in path segment: ${firstSegment}`);
logger.info(`[Config] Ignoring invalid MODEL_PROVIDER in path segment: ${firstSegment}`);
}
}
@ -200,7 +212,7 @@ export function createRequestHandler(config, providerPoolManager) {
if (path.includes('/count_tokens') && method === 'POST') {
try {
const body = await parseRequestBody(req);
console.log(`[Server] Handling count_tokens request for model: ${body.model}`);
logger.info(`[Server] Handling count_tokens request for model: ${body.model}`);
// Check if apiService has countTokens method
if (apiService && typeof apiService.countTokens === 'function') {
@ -221,7 +233,7 @@ export function createRequestHandler(config, providerPoolManager) {
}
return true;
} catch (error) {
console.error(`[Server] count_tokens error: ${error.message}`);
logger.error(`[Server] count_tokens error: ${error.message}`);
handleError(res, { statusCode: 500, message: `Failed to count tokens: ${error.message}` }, currentConfig.MODEL_PROVIDER);
return;
}
@ -237,6 +249,9 @@ export function createRequestHandler(config, providerPoolManager) {
res.end(JSON.stringify({ error: { message: 'Not Found' } }));
} catch (error) {
handleError(res, error, currentConfig.MODEL_PROVIDER);
} finally {
// Clear request context after request is complete
logger.clearRequestContext(requestId);
}
};
}

View file

@ -33,6 +33,7 @@ import {
getAllUserApiKeys
} from './user-data-manager.js';
import path from 'path';
import logger from '../../utils/logger.js';
import { existsSync } from 'fs';
import { promises as fs } from 'fs';
import multer from 'multer';
@ -112,7 +113,7 @@ async function checkAdminAuth(req) {
return true;
} catch (error) {
console.error('[API Potluck] Auth check error:', error.message);
logger.error('[API Potluck] Auth check error:', error.message);
return false;
}
}
@ -130,7 +131,7 @@ export async function handlePotluckApiRoutes(method, path, req, res) {
if (!path.startsWith('/api/potluck')) {
return false;
}
console.log('[API Potluck] Handling request:', method, path);
logger.info('[API Potluck] Handling request:', method, path);
// 验证管理员权限
const isAuthed = await checkAdminAuth(req);
@ -252,7 +253,7 @@ export async function handlePotluckApiRoutes(method, path, req, res) {
totalBonusUpdated++;
}
} catch (error) {
console.warn(`[API Potluck] Failed to sync bonus for ${apiKey.substring(0, 12)}...:`, error.message);
logger.warn(`[API Potluck] Failed to sync bonus for ${apiKey.substring(0, 12)}...:`, error.message);
}
}
@ -412,7 +413,7 @@ export async function handlePotluckApiRoutes(method, path, req, res) {
return true;
} catch (error) {
console.error('[API Potluck] API error:', error);
logger.error('[API Potluck] API error:', error);
sendJson(res, 500, {
success: false,
error: { message: error.message || 'Internal server error' }
@ -458,7 +459,7 @@ export async function handlePotluckUserApiRoutes(method, path, req, res) {
if (!path.startsWith('/api/potluckuser')) {
return false;
}
console.log('[API Potluck User] Handling request:', method, path);
logger.info('[API Potluck User] Handling request:', method, path);
try {
// 从请求中提取 API Key
@ -662,7 +663,7 @@ export async function handlePotluckUserApiRoutes(method, path, req, res) {
return true;
} catch (error) {
console.error('[API Potluck] User API error:', error);
logger.error('[API Potluck] User API error:', error);
sendJson(res, 500, {
success: false,
error: { message: error.message || 'Internal server error' }
@ -732,7 +733,7 @@ async function handleUserUpload(req, res, apiKey) {
return new Promise((resolve) => {
userUpload.single('file')(req, res, async (err) => {
if (err) {
console.error('[API Potluck User] File upload error:', err.message);
logger.error('[API Potluck User] File upload error:', err.message);
sendJson(res, 400, { success: false, error: err.message });
resolve(true);
return;
@ -782,10 +783,10 @@ async function handleUserUpload(req, res, apiKey) {
try {
await autoLinkProviderConfigs(CONFIG);
} catch (linkError) {
console.warn('[API Potluck User] Auto-link failed:', linkError.message);
logger.warn('[API Potluck User] Auto-link failed:', linkError.message);
}
console.log(`[API Potluck User] File uploaded, linked and health checked: ${relativePath} (provider: ${providerType}, health: ${healthResult.message})`);
logger.info(`[API Potluck User] File uploaded, linked and health checked: ${relativePath} (provider: ${providerType}, health: ${healthResult.message})`);
sendJson(res, 200, {
success: true,
@ -798,7 +799,7 @@ async function handleUserUpload(req, res, apiKey) {
resolve(true);
} catch (error) {
console.error('[API Potluck User] File processing error:', error);
logger.error('[API Potluck User] File processing error:', error);
sendJson(res, 500, { success: false, error: error.message });
resolve(true);
}
@ -825,7 +826,7 @@ async function handleKiroBatchImportTokens(req, res, apiKey) {
return true;
}
console.log(`[API Potluck User] Starting batch import of ${refreshTokens.length} tokens (user: ${apiKey.substring(0, 12)}...)`);
logger.info(`[API Potluck User] Starting batch import of ${refreshTokens.length} tokens (user: ${apiKey.substring(0, 12)}...)`);
// 设置 SSE 响应头
res.writeHead(200, {
@ -864,15 +865,15 @@ async function handleKiroBatchImportTokens(req, res, apiKey) {
// 自动从主服务同步健康状态
await syncCredentialHealthFromPool(apiKey, credential);
console.log(`[API Potluck User] Credential linked and health synced: ${credentialInfo.path}`);
logger.info(`[API Potluck User] Credential linked and health synced: ${credentialInfo.path}`);
} catch (linkError) {
console.warn('[API Potluck User] Failed to link/check credential:', linkError.message);
logger.warn('[API Potluck User] Failed to link/check credential:', linkError.message);
}
}
}
);
console.log(`[API Potluck User] Completed: ${result.success} success, ${result.failed} failed`);
logger.info(`[API Potluck User] Completed: ${result.success} success, ${result.failed} failed`);
// 发送完成事件
sendSSE('complete', {
@ -887,7 +888,7 @@ async function handleKiroBatchImportTokens(req, res, apiKey) {
return true;
} catch (error) {
console.error('[API Potluck User] Kiro Batch Import Error:', error);
logger.error('[API Potluck User] Kiro Batch Import Error:', error);
if (res.headersSent) {
res.write(`event: error\n`);
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
@ -936,12 +937,12 @@ async function handleKiroImportAwsCredentials(req, res, apiKey) {
return true;
}
console.log(`[API Potluck User] Starting AWS credentials import (user: ${apiKey.substring(0, 12)}...)`);
logger.info(`[API Potluck User] Starting AWS credentials import (user: ${apiKey.substring(0, 12)}...)`);
const result = await importAwsCredentials(credentials);
if (result.success) {
console.log(`[API Potluck User] Successfully imported credentials to: ${result.path}`);
logger.info(`[API Potluck User] Successfully imported credentials to: ${result.path}`);
// 将凭据路径关联到用户
const credentialInfo = {
@ -953,7 +954,7 @@ async function handleKiroImportAwsCredentials(req, res, apiKey) {
// 自动从主服务同步健康状态
const healthResult = await syncCredentialHealthFromPool(apiKey, credential);
console.log(`[API Potluck User] Health sync result: ${healthResult.message}`);
logger.info(`[API Potluck User] Health sync result: ${healthResult.message}`);
sendJson(res, 200, {
success: true,
@ -972,7 +973,7 @@ async function handleKiroImportAwsCredentials(req, res, apiKey) {
return true;
} catch (error) {
console.error('[API Potluck User] Kiro AWS Import Error:', error);
logger.error('[API Potluck User] Kiro AWS Import Error:', error);
sendJson(res, 500, {
success: false,
error: error.message
@ -1070,7 +1071,7 @@ async function handleCredentialHealthCheck(req, res, apiKey, credentialId) {
return true;
}
console.log(`[API Potluck User] Syncing health for credential: ${credential.path}`);
logger.info(`[API Potluck User] Syncing health for credential: ${credential.path}`);
const result = await syncCredentialHealthFromPool(apiKey, credential);
@ -1081,7 +1082,7 @@ async function handleCredentialHealthCheck(req, res, apiKey, credentialId) {
return true;
} catch (error) {
console.error('[API Potluck User] Health check error:', error);
logger.error('[API Potluck User] Health check error:', error);
sendJson(res, 500, {
success: false,
error: error.message
@ -1117,7 +1118,7 @@ async function checkAllCredentialsHealth() {
}
// isHealthy === null 表示未注册到服务,不计入健康/不健康
} catch (error) {
console.warn(`[API Potluck] Health sync failed for ${credential.path}:`, error.message);
logger.warn(`[API Potluck] Health sync failed for ${credential.path}:`, error.message);
}
}
}
@ -1171,19 +1172,19 @@ export function startHealthCheckScheduler() {
// 启动后延迟 30 秒执行第一次同步
setTimeout(async () => {
console.log('[API Potluck] Running initial health sync from pool...');
logger.info('[API Potluck] Running initial health sync from pool...');
const result = await checkAllCredentialsHealth();
console.log(`[API Potluck] Health sync complete: ${result.healthy}/${result.total} healthy`);
logger.info(`[API Potluck] Health sync complete: ${result.healthy}/${result.total} healthy`);
}, 30000);
// 定时同步
healthCheckTimer = setInterval(async () => {
console.log('[API Potluck] Running scheduled health sync from pool...');
logger.info('[API Potluck] Running scheduled health sync from pool...');
const result = await checkAllCredentialsHealth();
console.log(`[API Potluck] Health sync complete: ${result.healthy}/${result.total} healthy`);
logger.info(`[API Potluck] Health sync complete: ${result.healthy}/${result.total} healthy`);
}, HEALTH_CHECK_INTERVAL);
console.log(`[API Potluck] Health sync scheduler started (interval: ${HEALTH_CHECK_INTERVAL / 1000}s)`);
logger.info(`[API Potluck] Health sync scheduler started (interval: ${HEALTH_CHECK_INTERVAL / 1000}s)`);
}
/**
@ -1193,7 +1194,7 @@ export function stopHealthCheckScheduler() {
if (healthCheckTimer) {
clearInterval(healthCheckTimer);
healthCheckTimer = null;
console.log('[API Potluck] Health sync scheduler stopped');
logger.info('[API Potluck] Health sync scheduler stopped');
}
}

View file

@ -31,6 +31,7 @@ import {
} from './middleware.js';
import { consumeBonus, getConfig } from './user-data-manager.js';
import logger from '../../utils/logger.js';
import { handlePotluckApiRoutes, handlePotluckUserApiRoutes, startHealthCheckScheduler, stopHealthCheckScheduler } from './api-routes.js';
@ -53,7 +54,7 @@ const apiPotluckPlugin = {
* @param {Object} config - 服务器配置
*/
async init(config) {
console.log('[API Potluck Plugin] Initializing...');
logger.info('[API Potluck Plugin] Initializing...');
// 注入配置获取函数
setConfigGetter(getConfig);
// 启动定时健康检查
@ -64,7 +65,7 @@ const apiPotluckPlugin = {
* 销毁钩子
*/
async destroy() {
console.log('[API Potluck Plugin] Destroying...');
logger.info('[API Potluck Plugin] Destroying...');
// 停止定时健康检查
stopHealthCheckScheduler();
},
@ -137,7 +138,7 @@ const apiPotluckPlugin = {
}
// 认证成功,返回数据供后续使用
console.log(`[API Potluck Plugin] Authorized with key: ${apiKey.substring(0, 12)}...`);
logger.info(`[API Potluck Plugin] Authorized with key: ${apiKey.substring(0, 12)}...`);
return {
handled: false,
authorized: true,
@ -165,7 +166,7 @@ const apiPotluckPlugin = {
});
} catch (e) {
// 静默失败,不影响主流程
console.error('[API Potluck Plugin] Failed to record usage:', e.message);
logger.error('[API Potluck Plugin] Failed to record usage:', e.message);
}
}
}

View file

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

View file

@ -5,6 +5,7 @@
*/
import { promises as fs } from 'fs';
import logger from '../../utils/logger.js';
import { existsSync, readFileSync, writeFileSync, watch } from 'fs';
import path from 'path';
@ -126,7 +127,7 @@ export async function updateConfig(newConfig) {
await persistIfDirty();
const updatedConfig = getFullConfig();
console.log(`[API Potluck UserData] Config updated:`, updatedConfig);
logger.info(`[API Potluck UserData] Config updated:`, updatedConfig);
return updatedConfig;
}
@ -140,7 +141,7 @@ function updatePersistTimer(newInterval) {
if (persistTimer) {
clearInterval(persistTimer);
persistTimer = setInterval(persistIfDirty, currentPersistInterval);
console.log(`[API Potluck UserData] Persist interval updated to ${currentPersistInterval}ms`);
logger.info(`[API Potluck UserData] Persist interval updated to ${currentPersistInterval}ms`);
}
}
@ -179,7 +180,7 @@ function ensureLoaded() {
syncWriteToFile();
}
} catch (error) {
console.error('[API Potluck UserData] Failed to load user data:', error.message);
logger.error('[API Potluck UserData] Failed to load user data:', error.message);
userDataStore = { config: {}, users: {} };
}
@ -206,7 +207,7 @@ function syncWriteToFile() {
}
writeFileSync(USER_DATA_FILE, JSON.stringify(userDataStore, null, 2), 'utf8');
} catch (error) {
console.error('[API Potluck UserData] Sync write failed:', error.message);
logger.error('[API Potluck UserData] Sync write failed:', error.message);
}
}
@ -226,7 +227,7 @@ async function persistIfDirty() {
await fs.rename(tempFile, USER_DATA_FILE);
isDirty = false;
} catch (error) {
console.error('[API Potluck UserData] Persist failed:', error.message);
logger.error('[API Potluck UserData] Persist failed:', error.message);
} finally {
isWriting = false;
}
@ -268,17 +269,17 @@ function startFileWatcher() {
// 检查配置是否有变化
if (JSON.stringify(oldConfig) !== JSON.stringify(newConfig)) {
userDataStore.config = newConfig;
console.log('[API Potluck UserData] Config hot-reloaded:', getBonusConfig());
logger.info('[API Potluck UserData] Config hot-reloaded:', getBonusConfig());
}
}
} catch (error) {
console.error('[API Potluck UserData] Hot-reload failed:', error.message);
logger.error('[API Potluck UserData] Hot-reload failed:', error.message);
}
});
console.log('[API Potluck UserData] File watcher started for config hot-reload');
logger.info('[API Potluck UserData] File watcher started for config hot-reload');
} catch (error) {
console.error('[API Potluck UserData] Failed to start file watcher:', error.message);
logger.error('[API Potluck UserData] Failed to start file watcher:', error.message);
}
}
@ -451,7 +452,7 @@ export async function migrateUserCredentials(oldApiKey, newApiKey) {
markDirty();
await persistIfDirty();
console.log(`[API Potluck UserData] Migrated credentials from ${oldApiKey.substring(0, 12)}... to ${newApiKey.substring(0, 12)}...`);
logger.info(`[API Potluck UserData] Migrated credentials from ${oldApiKey.substring(0, 12)}... to ${newApiKey.substring(0, 12)}...`);
return true;
}
@ -521,7 +522,7 @@ export async function addCredentialBonus(apiKey, credentialId) {
userData.credentialBonuses.push(bonus);
markDirty();
console.log(`[API Potluck UserData] Added bonus for credential: ${credentialId}`);
logger.info(`[API Potluck UserData] Added bonus for credential: ${credentialId}`);
return bonus;
}
@ -542,7 +543,7 @@ export async function removeCredentialBonus(apiKey, credentialId) {
userData.credentialBonuses.splice(index, 1);
markDirty();
console.log(`[API Potluck UserData] Removed bonus for credential: ${credentialId}`);
logger.info(`[API Potluck UserData] Removed bonus for credential: ${credentialId}`);
return true;
}
@ -643,7 +644,7 @@ export async function syncCredentialBonuses(apiKey, credentialsWithHealth) {
usedCount: 0
});
added++;
console.log(`[API Potluck UserData] Created bonus for credential ${cred.id}, grantedAt: ${grantedAt}`);
logger.info(`[API Potluck UserData] Created bonus for credential ${cred.id}, grantedAt: ${grantedAt}`);
}
}

View file

@ -9,6 +9,8 @@
* 4. URL query: ?key=<key>
*/
import logger from '../../utils/logger.js';
/**
* 检查请求是否已授权
* @param {http.IncomingMessage} req - HTTP 请求
@ -79,11 +81,13 @@ const defaultAuthPlugin = {
}
// 认证失败,记录日志但不发送响应(由 request-handler 统一处理)
console.log(`[Default Auth] Unauthorized request. Headers: Authorization=${req.headers['authorization'] ? 'present' : 'N/A'}, x-api-key=${req.headers['x-api-key'] || 'N/A'}, x-goog-api-key=${req.headers['x-goog-api-key'] || 'N/A'}`);
logger.info(`[Default Auth] Unauthorized request. Headers: Authorization=${req.headers['authorization'] ? 'present' : 'N/A'}, x-api-key=${req.headers['x-api-key'] || 'N/A'}, x-goog-api-key=${req.headers['x-goog-api-key'] || 'N/A'}`);
// 返回 null 表示此插件不授权,让其他插件或默认逻辑处理
return { handled: false, authorized: null };
}
};
export default defaultAuthPlugin;
export default defaultAuthPlugin;

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import { ProviderStrategy } from '../../utils/provider-strategy.js';
import logger from '../../utils/logger.js';
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
/**
@ -59,7 +60,7 @@ class ClaudeStrategy extends ProviderStrategy {
: filePromptContent;
requestBody.system = newSystemText;
console.log(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'claude'.`);
logger.info(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'claude'.`);
return requestBody;
}
@ -71,3 +72,4 @@ class ClaudeStrategy extends ProviderStrategy {
}
export { ClaudeStrategy };

View file

@ -1,4 +1,5 @@
import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http';
import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
@ -24,7 +25,7 @@ export class ForwardApiService {
this.headerName = config?.FORWARD_HEADER_NAME || 'Authorization';
this.headerValuePrefix = config?.FORWARD_HEADER_VALUE_PREFIX || 'Bearer ';
console.log(`[Forward] Base URL: ${this.baseUrl}, System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`);
logger.info(`[Forward] Base URL: ${this.baseUrl}, System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`);
const httpAgent = new http.Agent({
keepAlive: true,
@ -75,18 +76,18 @@ export class ForwardApiService {
const isNetworkError = isRetryableNetworkError(error);
if (status === 401 || status === 403) {
console.error(`[Forward API] Received ${status}. API Key might be invalid or expired.`);
logger.error(`[Forward API] Received ${status}. API Key might be invalid or expired.`);
throw error;
}
if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[Forward API] Error ${status || errorCode}. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[Forward API] Error ${status || errorCode}. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
console.error(`[Forward API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
logger.error(`[Forward API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
throw error;
}
}
@ -120,7 +121,7 @@ export class ForwardApiService {
yield parsedChunk;
} catch (e) {
// If it's not JSON, it might be a different format, but for a forwarder we try to parse common SSE formats
console.warn("[ForwardApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData);
logger.warn("[ForwardApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData);
}
}
}
@ -132,7 +133,7 @@ export class ForwardApiService {
if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[Forward API] Stream error ${status || errorCode}. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[Forward API] Stream error ${status || errorCode}. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return;
@ -158,8 +159,9 @@ export class ForwardApiService {
const response = await this.axiosInstance.get('/models');
return response.data;
} catch (error) {
console.error(`Error listing Forward models:`, error.message);
logger.error(`Error listing Forward models:`, error.message);
return { data: [] };
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import { API_ACTIONS, extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import logger from '../../utils/logger.js';
import { ProviderStrategy } from '../../utils/provider-strategy.js';
/**
@ -55,7 +56,7 @@ class GeminiStrategy extends ProviderStrategy {
if (requestBody.system_instruction) {
delete requestBody.system_instruction;
}
console.log(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'gemini'.`);
logger.info(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'gemini'.`);
return requestBody;
}
@ -67,3 +68,4 @@ class GeminiStrategy extends ProviderStrategy {
}
export { GeminiStrategy };

View file

@ -1,4 +1,5 @@
import axios from 'axios';
import logger from '../../utils/logger.js';
import crypto from 'crypto';
import { promises as fs } from 'fs';
import path from 'path';
@ -32,13 +33,13 @@ export class CodexApiService {
*/
async initialize() {
if (this.isInitialized) return;
console.log('[Codex] Initializing Codex API Service...');
logger.info('[Codex] Initializing Codex API Service...');
// 注意V2 读写分离架构下,初始化不再执行同步认证/刷新逻辑
// 仅执行基础的凭证加载
await this.loadCredentials();
this.isInitialized = true;
console.log(`[Codex] Initialization complete. Account: ${this.email || 'unknown'}`);
logger.info(`[Codex] Initialization complete. Account: ${this.email || 'unknown'}`);
}
/**
@ -84,14 +85,14 @@ export class CodexApiService {
// 检查 token 是否需要刷新
if (this.isExpiryDateNear()) {
console.log('[Codex] Token expiring soon, refreshing...');
logger.info('[Codex] Token expiring soon, refreshing...');
await this.refreshAccessToken();
}
this.isInitialized = true;
console.log(`[Codex] Initialized with account: ${this.email}`);
logger.info(`[Codex] Initialized with account: ${this.email}`);
} catch (error) {
console.warn(`[Codex Auth] Failed to load credentials: ${error.message}`);
logger.warn(`[Codex Auth] Failed to load credentials: ${error.message}`);
}
}
@ -114,7 +115,7 @@ export class CodexApiService {
if (!this.refreshToken) {
throw new Error('Codex credentials not found. Please authenticate first using OAuth.');
}
console.log('[Codex] Token expiring soon or refresh requested, refreshing...');
logger.info('[Codex] Token expiring soon or refresh requested, refreshing...');
await this.refreshAccessToken();
// 刷新成功,重置 PoolManager 中的刷新状态并标记为健康
@ -137,7 +138,7 @@ export class CodexApiService {
if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) {
console.log(`[Codex] Token is near expiry, marking credential ${this.uuid} for refresh`);
logger.info(`[Codex] Token is near expiry, marking credential ${this.uuid} for refresh`);
poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, {
uuid: this.uuid
});
@ -157,12 +158,12 @@ export class CodexApiService {
return this.parseNonStreamResponse(response.data);
} catch (error) {
if (error.response?.status === 401) {
console.log('[Codex] Received 401. Triggering background refresh via PoolManager...');
logger.info('[Codex] Received 401. Triggering background refresh via PoolManager...');
// 标记当前凭证为不健康(会自动进入刷新队列)
const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) {
console.log(`[Codex] Marking credential ${this.uuid} as needs refresh. Reason: 401 Unauthorized`);
logger.info(`[Codex] Marking credential ${this.uuid} as needs refresh. Reason: 401 Unauthorized`);
poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, {
uuid: this.uuid
});
@ -190,7 +191,7 @@ export class CodexApiService {
if (this.isExpiryDateNear()) {
const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) {
console.log(`[Codex] Token is near expiry, marking credential ${this.uuid} for refresh`);
logger.info(`[Codex] Token is near expiry, marking credential ${this.uuid} for refresh`);
poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, {
uuid: this.uuid
});
@ -211,12 +212,12 @@ export class CodexApiService {
yield* this.parseSSEStream(response.data);
} catch (error) {
if (error.response?.status === 401) {
console.log('[Codex] Received 401 during stream. Triggering background refresh via PoolManager...');
logger.info('[Codex] Received 401 during stream. Triggering background refresh via PoolManager...');
// 标记当前凭证为不健康
const poolManager = getProviderPoolManager();
if (poolManager && this.uuid) {
console.log(`[Codex] Marking credential ${this.uuid} as needs refresh. Reason: 401 Unauthorized in stream`);
logger.info(`[Codex] Marking credential ${this.uuid} as needs refresh. Reason: 401 Unauthorized in stream`);
poolManager.markProviderNeedRefresh(MODEL_PROVIDER.CODEX_API, {
uuid: this.uuid
});
@ -293,9 +294,9 @@ export class CodexApiService {
// 保存更新的凭据
await this.saveCredentials();
console.log('[Codex] Token refreshed successfully');
logger.info('[Codex] Token refreshed successfully');
} catch (error) {
console.error('[Codex] Failed to refresh token:', error.message);
logger.error('[Codex] Failed to refresh token:', error.message);
throw new Error('Failed to refresh Codex token. Please re-authenticate.');
}
}
@ -308,7 +309,7 @@ export class CodexApiService {
const expiry = this.expiresAt.getTime();
const nearMinutes = 20;
const { message, isNearExpiry } = formatExpiryLog('Codex', expiry, nearMinutes);
console.log(message);
logger.info(message);
return isNearExpiry;
}
@ -379,7 +380,7 @@ export class CodexApiService {
const parsed = JSON.parse(data);
yield parsed;
} catch (e) {
console.error('[Codex] Failed to parse SSE data:', e.message);
logger.error('[Codex] Failed to parse SSE data:', e.message);
}
}
}
@ -395,7 +396,7 @@ export class CodexApiService {
const parsed = JSON.parse(data);
yield parsed;
} catch (e) {
console.error('[Codex] Failed to parse final SSE data:', e.message);
logger.error('[Codex] Failed to parse final SSE data:', e.message);
}
}
}
@ -469,3 +470,4 @@ export class CodexApiService {
}
}
}

View file

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

View file

@ -1,4 +1,5 @@
import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http';
import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
@ -14,7 +15,7 @@ export class OpenAIApiService {
this.apiKey = config.OPENAI_API_KEY;
this.baseUrl = config.OPENAI_BASE_URL;
this.useSystemProxy = config?.USE_SYSTEM_PROXY_OPENAI ?? false;
console.log(`[OpenAI] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`);
logger.info(`[OpenAI] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`);
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({
@ -68,14 +69,14 @@ export class OpenAIApiService {
const isNetworkError = isRetryableNetworkError(error);
if (status === 401 || status === 403) {
console.error(`[OpenAI API] Received ${status}. API Key might be invalid or expired.`);
logger.error(`[OpenAI API] Received ${status}. API Key might be invalid or expired.`);
throw error;
}
// Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[OpenAI API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[OpenAI API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
@ -83,7 +84,7 @@ export class OpenAIApiService {
// Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[OpenAI API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[OpenAI API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
@ -92,12 +93,12 @@ export class OpenAIApiService {
if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50);
console.log(`[OpenAI API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[OpenAI API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
console.error(`[OpenAI API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
logger.error(`[OpenAI API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
throw error;
}
}
@ -133,7 +134,7 @@ export class OpenAIApiService {
const parsedChunk = JSON.parse(jsonData);
yield parsedChunk;
} catch (e) {
console.warn("[OpenAIApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData);
logger.warn("[OpenAIApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData);
}
} else if (line === '') {
// Empty line, end of an event
@ -150,14 +151,14 @@ export class OpenAIApiService {
const isNetworkError = isRetryableNetworkError(error);
if (status === 401 || status === 403) {
console.error(`[OpenAI API] Received ${status} during stream. API Key might be invalid or expired.`);
logger.error(`[OpenAI API] Received ${status} during stream. API Key might be invalid or expired.`);
throw error;
}
// Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[OpenAI API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[OpenAI API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return;
@ -166,7 +167,7 @@ export class OpenAIApiService {
// Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[OpenAI API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[OpenAI API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return;
@ -176,13 +177,13 @@ export class OpenAIApiService {
if (isNetworkError && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
const errorIdentifier = errorCode || errorMessage.substring(0, 50);
console.log(`[OpenAI API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[OpenAI API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return;
}
console.error(`[OpenAI API] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
logger.error(`[OpenAI API] Error calling streaming API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
throw error;
}
}
@ -202,8 +203,9 @@ export class OpenAIApiService {
} catch (error) {
const status = error.response?.status;
const data = error.response?.data;
console.error(`Error listing OpenAI models (Status: ${status}):`, data || error.message);
logger.error(`Error listing OpenAI models (Status: ${status}):`, data || error.message);
throw error;
}
}
}

View file

@ -1,4 +1,5 @@
import axios from 'axios';
import logger from '../../utils/logger.js';
import * as http from 'http';
import * as https from 'https';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
@ -13,7 +14,7 @@ export class OpenAIResponsesApiService {
this.apiKey = config.OPENAI_API_KEY;
this.baseUrl = config.OPENAI_BASE_URL || 'https://api.openai.com/v1';
this.useSystemProxy = config?.USE_SYSTEM_PROXY_OPENAI ?? false;
console.log(`[OpenAIResponses] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`);
logger.info(`[OpenAIResponses] System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`);
// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏
const httpAgent = new http.Agent({
@ -61,14 +62,14 @@ export class OpenAIResponsesApiService {
const status = error.response?.status;
const data = error.response?.data;
if (status === 401 || status === 403) {
console.error(`[API] Received ${status}. API Key might be invalid or expired.`);
logger.error(`[API] Received ${status}. API Key might be invalid or expired.`);
throw error;
}
// Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
@ -76,12 +77,12 @@ export class OpenAIResponsesApiService {
// Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.callApi(endpoint, body, isRetry, retryCount + 1);
}
console.error(`Error calling OpenAI Responses API (Status: ${status}):`, data || error.message);
logger.error(`Error calling OpenAI Responses API (Status: ${status}):`, data || error.message);
throw error;
}
}
@ -117,7 +118,7 @@ export class OpenAIResponsesApiService {
const parsedChunk = JSON.parse(jsonData);
yield parsedChunk;
} catch (e) {
console.warn("[OpenAIResponsesApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData);
logger.warn("[OpenAIResponsesApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData);
}
} else if (line === '') {
// Empty line, end of an event
@ -128,14 +129,14 @@ export class OpenAIResponsesApiService {
const status = error.response?.status;
const data = error.response?.data;
if (status === 401 || status === 403) {
console.error(`[API] Received ${status} during stream. API Key might be invalid or expired.`);
logger.error(`[API] Received ${status} during stream. API Key might be invalid or expired.`);
throw error;
}
// Handle 429 (Too Many Requests) with exponential backoff
if (status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return;
@ -144,13 +145,13 @@ export class OpenAIResponsesApiService {
// Handle other retryable errors (5xx server errors)
if (status >= 500 && status < 600 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`[API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
logger.info(`[API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
return;
}
console.error(`Error calling OpenAI Responses streaming API (Status: ${status}):`, data || error.message);
logger.error(`Error calling OpenAI Responses streaming API (Status: ${status}):`, data || error.message);
throw error;
}
}
@ -170,8 +171,8 @@ export class OpenAIResponsesApiService {
} catch (error) {
const status = error.response?.status;
const data = error.response?.data;
console.error(`Error listing OpenAI Responses models (Status: ${status}):`, data || error.message);
logger.error(`Error listing OpenAI Responses models (Status: ${status}):`, data || error.message);
throw error;
}
}
}
}

View file

@ -1,4 +1,5 @@
import { ProviderStrategy } from '../../utils/provider-strategy.js';
import logger from '../../utils/logger.js';
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
/**
@ -96,7 +97,7 @@ class ResponsesAPIStrategy extends ProviderStrategy {
requestBody.instructions = filePromptContent;
}
console.log(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'responses'.`);
logger.info(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'responses'.`);
return requestBody;
}
@ -120,4 +121,4 @@ class ResponsesAPIStrategy extends ProviderStrategy {
}
}
export { ResponsesAPIStrategy };
export { ResponsesAPIStrategy };

View file

@ -1,4 +1,5 @@
import { ProviderStrategy } from '../../utils/provider-strategy.js';
import logger from '../../utils/logger.js';
import { extractSystemPromptFromRequestBody, MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
/**
@ -69,16 +70,17 @@ class OpenAIStrategy extends ProviderStrategy {
} else {
requestBody.messages.unshift({ role: 'system', content: newSystemText });
}
console.log(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'openai'.`);
logger.info(`[System Prompt] Applied system prompt from ${config.SYSTEM_PROMPT_FILE_PATH} in '${config.SYSTEM_PROMPT_MODE}' mode for provider 'openai'.`);
return requestBody;
}
async manageSystemPrompt(requestBody) {
//console.log('[System Prompt] Managing system prompt for provider "openai".', requestBody);
//logger.info('[System Prompt] Managing system prompt for provider "openai".', requestBody);
const incomingSystemText = extractSystemPromptFromRequestBody(requestBody, MODEL_PROTOCOL_PREFIX.OPENAI);
await this._updateSystemPromptFile(incomingSystemText, MODEL_PROTOCOL_PREFIX.OPENAI);
}
}
export { OpenAIStrategy };

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import {
ENDPOINT_TYPE
} from '../utils/common.js';
import { getProviderPoolManager } from './service-manager.js';
import logger from '../utils/logger.js';
/**
* Handle API authentication and routing
* @param {string} method - The HTTP method
@ -65,7 +65,7 @@ export async function handleAPIRequests(method, path, req, res, currentConfig, a
export function initializeAPIManagement(services) {
const providerPoolManager = getProviderPoolManager();
return async function heartbeatAndRefreshToken() {
console.log(`[Heartbeat] Server is running. Current time: ${new Date().toLocaleString()}`, Object.keys(services));
logger.info(`[Heartbeat] Server is running. Current time: ${new Date().toLocaleString()}`, Object.keys(services));
// 循环遍历所有已初始化的服务适配器,并尝试刷新令牌
// if (getProviderPoolManager()) {
// await getProviderPoolManager().performHealthChecks(); // 定期执行健康检查
@ -83,9 +83,9 @@ export function initializeAPIManagement(services) {
} else {
await serviceAdapter.refreshToken();
}
// console.log(`[Token Refresh] Refreshed token for ${providerKey}`);
// logger.info(`[Token Refresh] Refreshed token for ${providerKey}`);
} catch (error) {
console.error(`[Token Refresh Error] Failed to refresh token for ${providerKey}: ${error.message}`);
logger.error(`[Token Refresh Error] Failed to refresh token for ${providerKey}: ${error.message}`);
// 如果是号池中的某个实例刷新失败,这里需要捕获并更新其状态
// 现有的 serviceInstances 存储的是每个配置对应的单例,而非池中的成员
// 这意味着如果一个池成员的 token 刷新失败,需要找到它并更新其在 poolManager 中的状态

View file

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

View file

@ -1,4 +1,5 @@
import { getServiceAdapter, serviceInstances } from '../providers/adapter.js';
import logger from '../utils/logger.js';
import { ProviderPoolManager } from '../providers/provider-pool-manager.js';
import deepmerge from 'deepmerge';
import * as fs from 'fs';
@ -75,24 +76,24 @@ export async function autoLinkProviderConfigs(config) {
const filePath = config.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json';
try {
await pfs.writeFile(filePath, JSON.stringify(config.providerPools, null, 2), 'utf8');
console.log(`[Auto-Link] Added ${totalNewProviders} new config(s) to provider pools:`);
logger.info(`[Auto-Link] Added ${totalNewProviders} new config(s) to provider pools:`);
for (const [displayName, providers] of Object.entries(allNewProviders)) {
console.log(` ${displayName}: ${providers.length} config(s)`);
logger.info(` ${displayName}: ${providers.length} config(s)`);
providers.forEach(p => {
// 获取凭据路径键(支持 _CREDS_FILE_PATH 和 _TOKEN_FILE_PATH 两种格式)
const credKey = Object.keys(p).find(k =>
k.endsWith('_CREDS_FILE_PATH') || k.endsWith('_TOKEN_FILE_PATH')
);
if (credKey) {
console.log(` - ${p[credKey]}`);
logger.info(` - ${p[credKey]}`);
}
});
}
} catch (error) {
console.error(`[Auto-Link] Failed to save provider_pools.json: ${error.message}`);
logger.error(`[Auto-Link] Failed to save provider_pools.json: ${error.message}`);
}
} else {
console.log('[Auto-Link] No new configs to link');
logger.info('[Auto-Link] No new configs to link');
}
// Update provider pool manager if available
@ -154,7 +155,7 @@ async function scanProviderDirectory(dirPath, linkedPaths, newProviders, options
}
}
} catch (error) {
console.warn(`[Auto-Link] Failed to scan directory ${dirPath}: ${error.message}`);
logger.warn(`[Auto-Link] Failed to scan directory ${dirPath}: ${error.message}`);
}
}
@ -173,24 +174,24 @@ export async function initApiService(config, isReady = false) {
maxErrorCount: config.MAX_ERROR_COUNT ?? 3,
providerFallbackChain: config.providerFallbackChain || {},
});
console.log('[Initialization] ProviderPoolManager initialized with configured pools.');
logger.info('[Initialization] ProviderPoolManager initialized with configured pools.');
if(isReady){
// --- V2: 触发系统预热 ---
// 预热逻辑是异步的,不会阻塞服务器启动
providerPoolManager.warmupNodes().catch(err => {
console.error(`[Initialization] Warmup failed: ${err.message}`);
logger.error(`[Initialization] Warmup failed: ${err.message}`);
});
// 检查并刷新即将过期的节点(异步调用,不阻塞启动)
providerPoolManager.checkAndRefreshExpiringNodes().catch(err => {
console.error(`[Initialization] Check and refresh expiring nodes failed: ${err.message}`);
logger.error(`[Initialization] Check and refresh expiring nodes failed: ${err.message}`);
});
}
// 健康检查将在服务器完全启动后执行
} else {
console.log('[Initialization] No provider pools configured. Using single provider mode.');
logger.info('[Initialization] No provider pools configured. Using single provider mode.');
}
// Initialize all provider pool nodes at startup
@ -203,7 +204,7 @@ export async function initApiService(config, isReady = false) {
// 验证提供商类型是否在 DEFAULT_MODEL_PROVIDERS 中
if (config.DEFAULT_MODEL_PROVIDERS && Array.isArray(config.DEFAULT_MODEL_PROVIDERS)) {
if (!config.DEFAULT_MODEL_PROVIDERS.includes(providerType)) {
console.log(`[Initialization] Skipping provider type '${providerType}' (not in DEFAULT_MODEL_PROVIDERS).`);
logger.info(`[Initialization] Skipping provider type '${providerType}' (not in DEFAULT_MODEL_PROVIDERS).`);
continue;
}
}
@ -212,7 +213,7 @@ export async function initApiService(config, isReady = false) {
continue;
}
console.log(`[Initialization] Initializing ${providerConfigs.length} node(s) for provider '${providerType}'...`);
logger.info(`[Initialization] Initializing ${providerConfigs.length} node(s) for provider '${providerType}'...`);
// 初始化该提供商类型的所有节点
for (const providerConfig of providerConfigs) {
@ -234,18 +235,18 @@ export async function initApiService(config, isReady = false) {
totalInitialized++;
const identifier = providerConfig.customName || providerConfig.uuid || 'unknown';
console.log(` ✓ Initialized node: ${identifier}`);
logger.info(` ✓ Initialized node: ${identifier}`);
} catch (error) {
totalFailed++;
const identifier = providerConfig.customName || providerConfig.uuid || 'unknown';
console.warn(` ✗ Failed to initialize node ${identifier}: ${error.message}`);
logger.warn(` ✗ Failed to initialize node ${identifier}: ${error.message}`);
}
}
}
console.log(`[Initialization] Provider pool initialization complete: ${totalInitialized} succeeded, ${totalFailed} failed.`);
logger.info(`[Initialization] Provider pool initialization complete: ${totalInitialized} succeeded, ${totalFailed} failed.`);
} else {
console.log('[Initialization] No provider pools configured. Skipping node initialization.');
logger.info('[Initialization] No provider pools configured. Skipping node initialization.');
}
return serviceInstances; // Return the collection of initialized service instances
}
@ -271,10 +272,10 @@ export async function getApiService(config, requestedModel = null, options = {})
config.uuid = serviceConfig.uuid;
config.customName = serviceConfig.customName;
const customNameDisplay = serviceConfig.customName ? ` (${serviceConfig.customName})` : '';
console.log(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}${customNameDisplay}${requestedModel ? ` (model: ${requestedModel})` : ''}`);
logger.info(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}${customNameDisplay}${requestedModel ? ` (model: ${requestedModel})` : ''}`);
} else {
const errorMsg = `[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}`;
console.error(errorMsg);
logger.error(errorMsg);
throw new Error(errorMsg);
}
}
@ -321,7 +322,7 @@ export async function getApiServiceWithFallback(config, requestedModel = null, o
}
} else {
const errorMsg = `[API Service] No healthy provider found in pool (including fallback) for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}`;
console.error(errorMsg);
logger.error(errorMsg);
throw new Error(errorMsg);
}
}
@ -376,7 +377,7 @@ export async function getProviderStatus(config, options = {}) {
providerPools = poolsData;
}
} catch (error) {
console.warn('[API Service] Failed to load provider pools:', error.message);
logger.warn('[API Service] Failed to load provider pools:', error.message);
}
// providerPoolsSlim 只保留顶级 key 及部分字段,过滤 isDisabled 为 true 的元素

View file

@ -108,6 +108,11 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
return await systemApi.handleGetSystem(req, res);
}
// Download today's log file
if (method === 'GET' && pathParam === '/api/system/download-log') {
return await systemApi.handleDownloadTodayLog(req, res);
}
// Get provider pools summary
if (method === 'GET' && pathParam === '/api/providers') {
return await providerApi.handleGetProviders(req, res, currentConfig, providerPoolManager);

View file

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

View file

@ -1,4 +1,5 @@
import { existsSync, readFileSync, writeFileSync } from 'fs';
import logger from '../utils/logger.js';
import { promises as fs } from 'fs';
import path from 'path';
import { CONFIG } from '../core/config-manager.js';
@ -27,17 +28,17 @@ export async function reloadConfig(providerPoolManager) {
// Update global CONFIG
Object.assign(CONFIG, newConfig);
console.log('[UI API] Configuration reloaded:');
logger.info('[UI API] Configuration reloaded:');
// Update initApiService - 清空并重新初始化服务实例
Object.keys(serviceInstances).forEach(key => delete serviceInstances[key]);
initApiService(CONFIG);
console.log('[UI API] Configuration reloaded successfully');
logger.info('[UI API] Configuration reloaded successfully');
return newConfig;
} catch (error) {
console.error('[UI API] Failed to reload configuration:', error);
logger.error('[UI API] Failed to reload configuration:', error);
throw error;
}
}
@ -52,7 +53,7 @@ export async function handleGetConfig(req, res, currentConfig) {
try {
systemPrompt = readFileSync(currentConfig.SYSTEM_PROMPT_FILE_PATH, 'utf-8');
} catch (e) {
console.warn('[UI API] Failed to read system prompt file:', e.message);
logger.warn('[UI API] Failed to read system prompt file:', e.message);
}
}
@ -97,6 +98,16 @@ export async function handleUpdateConfig(req, res, currentConfig) {
if (newConfig.PROXY_URL !== undefined) currentConfig.PROXY_URL = newConfig.PROXY_URL;
if (newConfig.PROXY_ENABLED_PROVIDERS !== undefined) currentConfig.PROXY_ENABLED_PROVIDERS = newConfig.PROXY_ENABLED_PROVIDERS;
// Log settings
if (newConfig.LOG_ENABLED !== undefined) currentConfig.LOG_ENABLED = newConfig.LOG_ENABLED;
if (newConfig.LOG_OUTPUT_MODE !== undefined) currentConfig.LOG_OUTPUT_MODE = newConfig.LOG_OUTPUT_MODE;
if (newConfig.LOG_LEVEL !== undefined) currentConfig.LOG_LEVEL = newConfig.LOG_LEVEL;
if (newConfig.LOG_DIR !== undefined) currentConfig.LOG_DIR = newConfig.LOG_DIR;
if (newConfig.LOG_INCLUDE_REQUEST_ID !== undefined) currentConfig.LOG_INCLUDE_REQUEST_ID = newConfig.LOG_INCLUDE_REQUEST_ID;
if (newConfig.LOG_INCLUDE_TIMESTAMP !== undefined) currentConfig.LOG_INCLUDE_TIMESTAMP = newConfig.LOG_INCLUDE_TIMESTAMP;
if (newConfig.LOG_MAX_FILE_SIZE !== undefined) currentConfig.LOG_MAX_FILE_SIZE = newConfig.LOG_MAX_FILE_SIZE;
if (newConfig.LOG_MAX_FILES !== undefined) currentConfig.LOG_MAX_FILES = newConfig.LOG_MAX_FILES;
// Handle system prompt update
if (newConfig.systemPrompt !== undefined) {
const promptPath = currentConfig.SYSTEM_PROMPT_FILE_PATH || 'configs/input_system_prompt.txt';
@ -112,9 +123,9 @@ export async function handleUpdateConfig(req, res, currentConfig) {
timestamp: new Date().toISOString()
});
console.log('[UI API] System prompt updated');
logger.info('[UI API] System prompt updated');
} catch (e) {
console.warn('[UI API] Failed to write system prompt:', e.message);
logger.warn('[UI API] Failed to write system prompt:', e.message);
}
}
@ -139,17 +150,24 @@ export async function handleUpdateConfig(req, res, currentConfig) {
CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN,
PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH,
MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT,
POOL_SIZE_LIMIT: currentConfig.POOL_SIZE_LIMIT,
WARMUP_TARGET: currentConfig.WARMUP_TARGET,
REFRESH_CONCURRENCY_PER_PROVIDER: currentConfig.REFRESH_CONCURRENCY_PER_PROVIDER,
providerFallbackChain: currentConfig.providerFallbackChain,
modelFallbackMapping: currentConfig.modelFallbackMapping,
PROXY_URL: currentConfig.PROXY_URL,
PROXY_ENABLED_PROVIDERS: currentConfig.PROXY_ENABLED_PROVIDERS
PROXY_ENABLED_PROVIDERS: currentConfig.PROXY_ENABLED_PROVIDERS,
LOG_ENABLED: currentConfig.LOG_ENABLED,
LOG_OUTPUT_MODE: currentConfig.LOG_OUTPUT_MODE,
LOG_LEVEL: currentConfig.LOG_LEVEL,
LOG_DIR: currentConfig.LOG_DIR,
LOG_INCLUDE_REQUEST_ID: currentConfig.LOG_INCLUDE_REQUEST_ID,
LOG_INCLUDE_TIMESTAMP: currentConfig.LOG_INCLUDE_TIMESTAMP,
LOG_MAX_FILE_SIZE: currentConfig.LOG_MAX_FILE_SIZE,
LOG_MAX_FILES: currentConfig.LOG_MAX_FILES
};
writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
console.log('[UI API] Configuration saved to configs/config.json');
logger.info('[UI API] Configuration saved to configs/config.json');
// 广播更新事件
broadcastEvent('config_update', {
@ -159,7 +177,7 @@ export async function handleUpdateConfig(req, res, currentConfig) {
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('[UI API] Failed to save configuration to file:', error.message);
logger.error('[UI API] Failed to save configuration to file:', error.message);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
@ -215,7 +233,7 @@ export async function handleReloadConfig(req, res, providerPoolManager) {
}));
return true;
} catch (error) {
console.error('[UI API] Failed to reload config files:', error);
logger.error('[UI API] Failed to reload config files:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
@ -248,7 +266,7 @@ export async function handleUpdateAdminPassword(req, res) {
const pwdFilePath = path.join(process.cwd(), 'configs', 'pwd');
await fs.writeFile(pwdFilePath, password.trim(), 'utf-8');
console.log('[UI API] Admin password updated successfully');
logger.info('[UI API] Admin password updated successfully');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
@ -257,7 +275,7 @@ export async function handleUpdateAdminPassword(req, res) {
}));
return true;
} catch (error) {
console.error('[UI API] Failed to update admin password:', error);
logger.error('[UI API] Failed to update admin password:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {

View file

@ -1,4 +1,5 @@
import { existsSync } from 'fs';
import logger from '../utils/logger.js';
import { promises as fs } from 'fs';
import path from 'path';
import { addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js';
@ -16,7 +17,7 @@ export async function scanConfigFiles(currentConfig, providerPoolManager) {
const configsPath = path.join(process.cwd(), 'configs');
if (!existsSync(configsPath)) {
// console.log('[Config Scanner] configs directory not found, creating empty result');
// logger.info('[Config Scanner] configs directory not found, creating empty result');
return configFiles;
}
@ -55,7 +56,7 @@ export async function scanConfigFiles(currentConfig, providerPoolManager) {
const configsFiles = await scanOAuthDirectory(configsPath, usedPaths, currentConfig);
configFiles.push(...configsFiles);
} catch (error) {
console.warn(`[Config Scanner] Failed to scan configs directory:`, error.message);
logger.warn(`[Config Scanner] Failed to scan configs directory:`, error.message);
}
return configFiles;
@ -143,7 +144,7 @@ async function analyzeOAuthFile(filePath, usedPaths, currentConfig) {
preview: content.substring(0, 100) + (content.length > 100 ? '...' : '')
};
} catch (error) {
console.warn(`[OAuth Analyzer] Failed to analyze file ${filePath}:`, error.message);
logger.warn(`[OAuth Analyzer] Failed to analyze file ${filePath}:`, error.message);
return null;
}
}
@ -361,7 +362,7 @@ async function scanOAuthDirectory(dirPath, usedPaths, currentConfig) {
}
}
} catch (error) {
console.warn(`[OAuth Scanner] Failed to scan directory ${dirPath}:`, error.message);
logger.warn(`[OAuth Scanner] Failed to scan directory ${dirPath}:`, error.message);
}
return oauthFiles;

View file

@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'fs';
import { promises as fs } from 'fs';
import path from 'path';
import multer from 'multer';
import logger from '../utils/logger.js';
// Token存储到本地文件中
const TOKEN_STORE_FILE = path.join(process.cwd(), 'configs', 'token-store.json');
@ -72,21 +73,17 @@ export function initializeUIManagement() {
const originalLog = console.log;
console.log = function(...args) {
originalLog.apply(console, args);
const message = args.map(arg => {
if (typeof arg === 'string') return arg;
try {
return JSON.stringify(arg);
} catch (e) {
if (arg instanceof Error) {
return `[Error: ${arg.message}] ${arg.stack || ''}`;
}
return `[Object: ${Object.prototype.toString.call(arg)}] (Circular or too complex to stringify)`;
}
}).join(' ');
const logEntry = {
timestamp: new Date().toISOString(),
level: 'info',
message: message
message: args.map(arg => {
if (typeof arg === 'string') return arg;
try {
return JSON.stringify(arg);
} catch (e) {
return String(arg);
}
}).join(' ')
};
global.logBuffer.push(logEntry);
if (global.logBuffer.length > 100) {
@ -99,21 +96,17 @@ export function initializeUIManagement() {
const originalError = console.error;
console.error = function(...args) {
originalError.apply(console, args);
const message = args.map(arg => {
if (typeof arg === 'string') return arg;
try {
return JSON.stringify(arg);
} catch (e) {
if (arg instanceof Error) {
return `[Error: ${arg.message}] ${arg.stack || ''}`;
}
return `[Object: ${Object.prototype.toString.call(arg)}] (Circular or too complex to stringify)`;
}
}).join(' ');
const logEntry = {
timestamp: new Date().toISOString(),
level: 'error',
message: message
message: args.map(arg => {
if (typeof arg === 'string') return arg;
try {
return JSON.stringify(arg);
} catch (e) {
return String(arg);
}
}).join(' ')
};
global.logBuffer.push(logEntry);
if (global.logBuffer.length > 100) {
@ -185,7 +178,7 @@ export function handleUploadOAuthCredentials(req, res, options = {}) {
return new Promise((resolve) => {
uploadMiddleware(req, res, async (err) => {
if (err) {
console.error(`${logPrefix} File upload error:`, err.message);
logger.error(`${logPrefix} File upload error:`, err.message);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
@ -242,7 +235,7 @@ export function handleUploadOAuthCredentials(req, res, options = {}) {
});
const userInfoStr = userInfo ? `, ${userInfo}` : '';
console.log(`${logPrefix} OAuth credentials file uploaded: ${targetFilePath} (provider: ${provider}${userInfoStr})`);
logger.info(`${logPrefix} OAuth credentials file uploaded: ${targetFilePath} (provider: ${provider}${userInfoStr})`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
@ -255,7 +248,7 @@ export function handleUploadOAuthCredentials(req, res, options = {}) {
resolve(true);
} catch (error) {
console.error(`${logPrefix} File upload processing error:`, error);
logger.error(`${logPrefix} File upload processing error:`, error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {

View file

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

View file

@ -1,4 +1,5 @@
import { getPluginManager } from '../core/plugin-manager.js';
import logger from '../utils/logger.js';
import { getRequestBody } from '../utils/common.js';
import { broadcastEvent } from './event-broadcast.js';
@ -13,7 +14,7 @@ export async function handleGetPlugins(req, res) {
res.end(JSON.stringify({ plugins }));
return true;
} catch (error) {
console.error('[UI API] Failed to get plugins:', error);
logger.error('[UI API] Failed to get plugins:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
@ -64,7 +65,7 @@ export async function handleTogglePlugin(req, res, pluginName) {
}));
return true;
} catch (error) {
console.error('[UI API] Failed to toggle plugin:', error);
logger.error('[UI API] Failed to toggle plugin:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {

View file

@ -1,4 +1,5 @@
import { existsSync, readFileSync, writeFileSync } from 'fs';
import logger from '../utils/logger.js';
import { getRequestBody } from '../utils/common.js';
import { getAllProviderModels, getProviderModels } from '../providers/provider-models.js';
import { generateUUID, createProviderConfig, formatSystemPath, detectProviderFromPath, addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js';
@ -18,7 +19,7 @@ export async function handleGetProviders(req, res, currentConfig, providerPoolMa
providerPools = poolsData;
}
} catch (error) {
console.warn('[UI API] Failed to load provider pools:', error.message);
logger.warn('[UI API] Failed to load provider pools:', error.message);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
@ -40,7 +41,7 @@ export async function handleGetProviderType(req, res, currentConfig, providerPoo
providerPools = poolsData;
}
} catch (error) {
console.warn('[UI API] Failed to load provider pools:', error.message);
logger.warn('[UI API] Failed to load provider pools:', error.message);
}
const providers = providerPools[providerType] || [];
@ -112,7 +113,7 @@ export async function handleAddProvider(req, res, currentConfig, providerPoolMan
const fileContent = readFileSync(filePath, 'utf-8');
providerPools = JSON.parse(fileContent);
} catch (readError) {
console.warn('[UI API] Failed to read existing provider pools:', readError.message);
logger.warn('[UI API] Failed to read existing provider pools:', readError.message);
}
}
@ -124,7 +125,7 @@ export async function handleAddProvider(req, res, currentConfig, providerPoolMan
// Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8');
console.log(`[UI API] Added new provider to ${providerType}: ${providerConfig.uuid}`);
logger.info(`[UI API] Added new provider to ${providerType}: ${providerConfig.uuid}`);
// Update provider pool manager if available
if (providerPoolManager) {
@ -219,7 +220,7 @@ export async function handleUpdateProvider(req, res, currentConfig, providerPool
// Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8');
console.log(`[UI API] Updated provider ${providerUuid} in ${providerType}`);
logger.info(`[UI API] Updated provider ${providerUuid} in ${providerType}`);
// Update provider pool manager if available
if (providerPoolManager) {
@ -290,7 +291,7 @@ export async function handleDeleteProvider(req, res, currentConfig, providerPool
// Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8');
console.log(`[UI API] Deleted provider ${providerUuid} from ${providerType}`);
logger.info(`[UI API] Deleted provider ${providerUuid} from ${providerType}`);
// Update provider pool manager if available
if (providerPoolManager) {
@ -357,7 +358,7 @@ export async function handleDisableEnableProvider(req, res, currentConfig, provi
// Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8');
console.log(`[UI API] ${action === 'disable' ? 'Disabled' : 'Enabled'} provider ${providerUuid} in ${providerType}`);
logger.info(`[UI API] ${action === 'disable' ? 'Disabled' : 'Enabled'} provider ${providerUuid} in ${providerType}`);
// Update provider pool manager if available
if (providerPoolManager) {
@ -439,7 +440,7 @@ export async function handleResetProviderHealth(req, res, currentConfig, provide
// Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8');
console.log(`[UI API] Reset health status for ${resetCount} providers in ${providerType}`);
logger.info(`[UI API] Reset health status for ${resetCount} providers in ${providerType}`);
// Update provider pool manager if available
if (providerPoolManager) {
@ -524,7 +525,7 @@ export async function handleDeleteUnhealthyProviders(req, res, currentConfig, pr
// Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8');
console.log(`[UI API] Deleted ${unhealthyProviders.length} unhealthy providers from ${providerType}`);
logger.info(`[UI API] Deleted ${unhealthyProviders.length} unhealthy providers from ${providerType}`);
// Update provider pool manager if available
if (providerPoolManager) {
@ -615,7 +616,7 @@ export async function handleRefreshUnhealthyUuids(req, res, currentConfig, provi
// Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8');
console.log(`[UI API] Refreshed UUIDs for ${refreshedProviders.length} unhealthy providers in ${providerType}`);
logger.info(`[UI API] Refreshed UUIDs for ${refreshedProviders.length} unhealthy providers in ${providerType}`);
// Update provider pool manager if available
if (providerPoolManager) {
@ -684,7 +685,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
return true;
}
console.log(`[UI API] Starting health check for ${unhealthyProviders.length} unhealthy providers in ${providerType} (total: ${providers.length})`);
logger.info(`[UI API] Starting health check for ${unhealthyProviders.length} unhealthy providers in ${providerType} (total: ${providers.length})`);
// 执行健康检测(强制检查,忽略 checkHealth 配置)
const results = [];
@ -693,7 +694,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
// 跳过已禁用的节点
if (providerConfig.isDisabled) {
console.log(`[UI API] Skipping health check for disabled provider: ${providerConfig.uuid}`);
logger.info(`[UI API] Skipping health check for disabled provider: ${providerConfig.uuid}`);
continue;
}
@ -726,7 +727,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
if (isAuthError) {
providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage);
console.log(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`);
logger.info(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`);
} else {
providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage);
}
@ -751,7 +752,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
if (isAuthError) {
providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage);
console.log(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`);
logger.info(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`);
} else {
providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage);
}
@ -778,7 +779,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
const successCount = results.filter(r => r.success === true).length;
const failCount = results.filter(r => r.success === false).length;
console.log(`[UI API] Health check completed for ${providerType}: ${successCount} recovered, ${failCount} still unhealthy (checked ${unhealthyProviders.length} unhealthy nodes)`);
logger.info(`[UI API] Health check completed for ${providerType}: ${successCount} recovered, ${failCount} still unhealthy (checked ${unhealthyProviders.length} unhealthy nodes)`);
// 广播更新事件
broadcastEvent('config_update', {
@ -800,7 +801,7 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
}));
return true;
} catch (error) {
console.error('[UI API] Health check error:', error);
logger.error('[UI API] Health check error:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: error.message } }));
return true;
@ -846,7 +847,7 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP
const fileContent = readFileSync(poolsFilePath, 'utf-8');
providerPools = JSON.parse(fileContent);
} catch (readError) {
console.warn('[UI API] Failed to read existing provider pools:', readError.message);
logger.warn('[UI API] Failed to read existing provider pools:', readError.message);
}
}
@ -884,7 +885,7 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP
// Save to file
writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf-8');
console.log(`[UI API] Quick linked config: ${filePath} -> ${providerType}`);
logger.info(`[UI API] Quick linked config: ${filePath} -> ${providerType}`);
// Update provider pool manager if available
if (providerPoolManager) {
@ -917,7 +918,7 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP
}));
return true;
} catch (error) {
console.error('[UI API] Quick link failed:', error);
logger.error('[UI API] Quick link failed:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
@ -967,7 +968,7 @@ export async function handleRefreshProviderUuid(req, res, currentConfig, provide
// Save to file
writeFileSync(filePath, JSON.stringify(providerPools, null, 2), 'utf-8');
console.log(`[UI API] Refreshed UUID for provider in ${providerType}: ${oldUuid} -> ${newUuid}`);
logger.info(`[UI API] Refreshed UUID for provider in ${providerType}: ${oldUuid} -> ${newUuid}`);
// Update provider pool manager if available
if (providerPoolManager) {

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 { getCpuUsagePercent } from './system-monitor.js';
@ -16,7 +17,7 @@ export async function handleGetSystem(req, res) {
appVersion = readFileSync(versionFilePath, 'utf8').trim();
}
} catch (error) {
console.warn('[UI API] Failed to read VERSION file:', error.message);
logger.warn('[UI API] Failed to read VERSION file:', error.message);
}
// 计算 CPU 使用率
@ -45,6 +46,34 @@ export async function handleGetSystem(req, res) {
return true;
}
/**
* 下载当日日志
*/
export async function handleDownloadTodayLog(req, res) {
try {
if (!logger.currentLogFile || !existsSync(logger.currentLogFile)) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'Today\'s log file not found' } }));
return true;
}
const fileName = path.basename(logger.currentLogFile);
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Disposition': `attachment; filename="${fileName}"`
});
const readStream = createReadStream(logger.currentLogFile);
readStream.pipe(res);
return true;
} catch (error) {
logger.error('[UI API] Failed to download log:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'Failed to download log: ' + error.message } }));
return true;
}
}
/**
* 健康检查接口用于前端token验证
*/
@ -84,7 +113,7 @@ export async function handleRestartService(req, res) {
if (IS_WORKER_PROCESS && process.send) {
// 作为子进程运行,通知主进程重启
console.log('[UI API] Requesting restart from master process...');
logger.info('[UI API] Requesting restart from master process...');
process.send({ type: 'restart_request' });
// 广播重启事件
@ -107,7 +136,7 @@ export async function handleRestartService(req, res) {
}));
} else {
// 独立运行模式,无法自动重启
console.log('[UI API] Service is running in standalone mode, cannot auto-restart');
logger.info('[UI API] Service is running in standalone mode, cannot auto-restart');
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
@ -119,7 +148,7 @@ export async function handleRestartService(req, res) {
}
return true;
} catch (error) {
console.error('[UI API] Failed to restart service:', error);
logger.error('[UI API] Failed to restart service:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {

View file

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

View file

@ -1,4 +1,5 @@
import { existsSync } from 'fs';
import logger from '../utils/logger.js';
import { promises as fs } from 'fs';
import path from 'path';
import AdmZip from 'adm-zip';
@ -15,7 +16,7 @@ export async function handleGetUploadConfigs(req, res, currentConfig, providerPo
res.end(JSON.stringify(configFiles));
return true;
} catch (error) {
console.error('[UI API] Failed to scan config files:', error);
logger.error('[UI API] Failed to scan config files:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
@ -71,7 +72,7 @@ export async function handleViewConfigFile(req, res, filePath) {
}));
return true;
} catch (error) {
console.error('[UI API] Failed to view config file:', error);
logger.error('[UI API] Failed to view config file:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
@ -132,7 +133,7 @@ export async function handleDeleteConfigFile(req, res, filePath) {
}));
return true;
} catch (error) {
console.error('[UI API] Failed to delete config file:', error);
logger.error('[UI API] Failed to delete config file:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
@ -185,10 +186,10 @@ export async function handleDownloadAllConfigs(req, res) {
});
res.end(zipBuffer);
console.log(`[UI API] All configs downloaded as zip: ${filename}`);
logger.info(`[UI API] All configs downloaded as zip: ${filename}`);
return true;
} catch (error) {
console.error('[UI API] Failed to download all configs:', error);
logger.error('[UI API] Failed to download all configs:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
@ -298,7 +299,7 @@ export async function handleDeleteUnboundConfigs(req, res, currentConfig, provid
}));
return true;
} catch (error) {
console.error('[UI API] Failed to delete unbound configs:', error);
logger.error('[UI API] Failed to delete unbound configs:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import { promises as fs } from 'fs';
import * as path from 'path';
import * as http from 'http'; // Add http for IncomingMessage and ServerResponse types
import * as crypto from 'crypto'; // Import crypto for MD5 hashing
import logger from './logger.js';
import { convertData, getOpenAIStreamChunkStop } from '../convert/convert.js';
import { ProviderStrategyFactory } from './provider-strategies.js';
import { getPluginManager } from '../core/plugin-manager.js';
@ -200,13 +201,13 @@ export async function logConversation(type, content, logMode, logFilename) {
const logEntry = `${timestamp} [${type.toUpperCase()}]:\n${content}\n--------------------------------------\n`;
if (logMode === 'console') {
console.log(logEntry);
logger.info(logEntry);
} else if (logMode === 'file') {
try {
// Append to the file
await fs.appendFile(logFilename, logEntry);
} catch (err) {
console.error(`[Error] Failed to write conversation log to ${logFilename}:`, err);
logger.error(`[Error] Failed to write conversation log to ${logFilename}:`, err);
}
}
}
@ -247,7 +248,7 @@ export function isAuthorized(req, requestUrl, REQUIRED_API_KEY) {
return true;
}
console.log(`[Auth] Unauthorized request denied. Bearer: "${authHeader ? 'present' : 'N/A'}", Query Key: "${queryKey}", x-goog-api-key: "${googApiKey}", x-api-key: "${claudeApiKey}"`);
logger.info(`[Auth] Unauthorized request denied. Bearer: "${authHeader ? 'present' : 'N/A'}", Query Key: "${queryKey}", x-goog-api-key: "${googApiKey}", x-api-key: "${claudeApiKey}"`);
return false;
}
@ -323,35 +324,35 @@ export async function handleStreamRequest(res, service, model, requestBody, from
// fullOldResponseJson += chunk.type+"\n";
// fullResponseJson += chunk.type+"\n";
res.write(`event: ${chunk.type}\n`);
// console.log(`event: ${chunk.type}\n`);
// logger.info(`event: ${chunk.type}\n`);
}
// fullOldResponseJson += JSON.stringify(chunk)+"\n";
// fullResponseJson += JSON.stringify(chunk)+"\n\n";
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
// console.log(`data: ${JSON.stringify(chunk)}\n`);
// logger.info(`data: ${JSON.stringify(chunk)}\n`);
}
}
if (openStop && needsConversion) {
res.write(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n\n`);
// console.log(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n`);
// logger.info(`data: ${JSON.stringify(getOpenAIStreamChunkStop(model))}\n`);
}
// 流式请求成功完成统计使用次数错误次数重置为0
if (providerPoolManager && pooluuid) {
const customNameDisplay = customName ? `, ${customName}` : '';
console.log(`[Provider Pool] Increasing usage count for ${toProvider} (${pooluuid}${customNameDisplay}) after successful stream request`);
logger.info(`[Provider Pool] Increasing usage count for ${toProvider} (${pooluuid}${customNameDisplay}) after successful stream request`);
providerPoolManager.markProviderHealthy(toProvider, {
uuid: pooluuid
});
}
} catch (error) {
console.error('\n[Server] Error during stream processing:', error.stack);
logger.error('\n[Server] Error during stream processing:', error.stack);
// 如果已经发送了内容,不进行重试(避免响应数据损坏)
if (fullResponseText.length > 0) {
console.log(`[Stream Retry] Cannot retry: ${fullResponseText.length} bytes already sent to client`);
logger.info(`[Stream Retry] Cannot retry: ${fullResponseText.length} bytes already sent to client`);
// 直接发送错误并结束
const errorPayload = createStreamErrorResponse(error, fromProvider);
res.write(errorPayload);
@ -375,9 +376,9 @@ export async function handleStreamRequest(res, service, model, requestBody, from
if (!credentialMarkedUnhealthy && !skipErrorCount && providerPoolManager && pooluuid) {
// 400 报错码通常是请求参数问题,不记录为提供商错误
if (status === 400) {
console.log(`[Provider Pool] Skipping unhealthy marking for ${toProvider} (${pooluuid}) due to status 400 (client error)`);
logger.info(`[Provider Pool] Skipping unhealthy marking for ${toProvider} (${pooluuid}) due to status 400 (client error)`);
} else {
console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error (status: ${status || 'unknown'})`);
logger.info(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error (status: ${status || 'unknown'})`);
// 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康
providerPoolManager.markProviderUnhealthy(toProvider, {
uuid: pooluuid
@ -385,9 +386,9 @@ export async function handleStreamRequest(res, service, model, requestBody, from
credentialMarkedUnhealthy = true;
}
} else if (credentialMarkedUnhealthy) {
console.log(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`);
logger.info(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`);
} else if (skipErrorCount) {
console.log(`[Provider Pool] Skipping error count for ${toProvider} (${pooluuid}) - will switch credential without marking unhealthy`);
logger.info(`[Provider Pool] Skipping error count for ${toProvider} (${pooluuid}) - will switch credential without marking unhealthy`);
}
// 如果需要切换凭证(无论是否标记不健康),都设置标记以触发重试
@ -400,7 +401,7 @@ export async function handleStreamRequest(res, service, model, requestBody, from
if (credentialMarkedUnhealthy && currentRetry < maxRetries && providerPoolManager && CONFIG) {
// 增加10秒内的随机等待时间避免所有请求同时切换凭证
const randomDelay = Math.floor(Math.random() * 10000); // 0-10000毫秒
console.log(`[Stream Retry] Credential marked unhealthy. Waiting ${randomDelay}ms before retry ${currentRetry + 1}/${maxRetries} with different credential...`);
logger.info(`[Stream Retry] Credential marked unhealthy. Waiting ${randomDelay}ms before retry ${currentRetry + 1}/${maxRetries} with different credential...`);
await new Promise(resolve => setTimeout(resolve, randomDelay));
try {
@ -408,8 +409,8 @@ export async function handleStreamRequest(res, service, model, requestBody, from
const { getApiServiceWithFallback } = await import('../services/service-manager.js');
const result = await getApiServiceWithFallback(CONFIG, model);
if (result && result.service && result.uuid !== pooluuid) {
console.log(`[Stream Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`);
if (result && result.service) {
logger.info(`[Stream Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`);
// 使用新服务重试
const newRetryContext = {
@ -434,13 +435,11 @@ export async function handleStreamRequest(res, service, model, requestBody, from
result.serviceConfig?.customName || customName,
newRetryContext
);
} else if (result && result.uuid === pooluuid) {
console.log(`[Stream Retry] No different healthy credential available. Same credential selected.`);
} else {
console.log(`[Stream Retry] No healthy credential available for retry.`);
logger.info(`[Stream Retry] No healthy credential available for retry.`);
}
} catch (retryError) {
console.error(`[Stream Retry] Failed to get alternative service:`, retryError.message);
logger.error(`[Stream Retry] Failed to get alternative service:`, retryError.message);
}
}
@ -478,11 +477,11 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
// Convert the response back to the client's format (fromProvider), if necessary.
let clientResponse = nativeResponse;
if (needsConversion) {
console.log(`[Response Convert] Converting response from ${toProvider} to ${fromProvider}`);
logger.info(`[Response Convert] Converting response from ${toProvider} to ${fromProvider}`);
clientResponse = convertData(nativeResponse, 'response', toProvider, fromProvider, model);
}
//console.log(`[Response] Sending response to client: ${JSON.stringify(clientResponse)}`);
//logger.info(`[Response] Sending response to client: ${JSON.stringify(clientResponse)}`);
await handleUnifiedResponse(res, JSON.stringify(clientResponse), false);
await logConversation('output', responseText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME);
// fs.writeFile('oldResponse'+Date.now()+'.json', JSON.stringify(clientResponse));
@ -490,13 +489,13 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
// 一元请求成功完成统计使用次数错误次数重置为0
if (providerPoolManager && pooluuid) {
const customNameDisplay = customName ? `, ${customName}` : '';
console.log(`[Provider Pool] Increasing usage count for ${toProvider} (${pooluuid}${customNameDisplay}) after successful unary request`);
logger.info(`[Provider Pool] Increasing usage count for ${toProvider} (${pooluuid}${customNameDisplay}) after successful unary request`);
providerPoolManager.markProviderHealthy(toProvider, {
uuid: pooluuid
});
}
} catch (error) {
console.error('\n[Server] Error during unary processing:', error.stack);
logger.error('\n[Server] Error during unary processing:', error.stack);
// 获取状态码(用于日志记录,不再用于判断是否重试)
const status = error.response?.status;
@ -513,9 +512,9 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
if (!credentialMarkedUnhealthy && !skipErrorCount && providerPoolManager && pooluuid) {
// 400 报错码通常是请求参数问题,不记录为提供商错误
if (status === 400) {
console.log(`[Provider Pool] Skipping unhealthy marking for ${toProvider} (${pooluuid}) due to status 400 (client error)`);
logger.info(`[Provider Pool] Skipping unhealthy marking for ${toProvider} (${pooluuid}) due to status 400 (client error)`);
} else {
console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to unary error (status: ${status || 'unknown'})`);
logger.info(`[Provider Pool] Marking ${toProvider} as unhealthy due to unary error (status: ${status || 'unknown'})`);
// 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康
providerPoolManager.markProviderUnhealthy(toProvider, {
uuid: pooluuid
@ -523,9 +522,9 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
credentialMarkedUnhealthy = true;
}
} else if (credentialMarkedUnhealthy) {
console.log(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`);
logger.info(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`);
} else if (skipErrorCount) {
console.log(`[Provider Pool] Skipping error count for ${toProvider} (${pooluuid}) - will switch credential without marking unhealthy`);
logger.info(`[Provider Pool] Skipping error count for ${toProvider} (${pooluuid}) - will switch credential without marking unhealthy`);
}
// 如果需要切换凭证(无论是否标记不健康),都设置标记以触发重试
@ -538,7 +537,7 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
if (credentialMarkedUnhealthy && currentRetry < maxRetries && providerPoolManager && CONFIG) {
// 增加10秒内的随机等待时间避免所有请求同时切换凭证
const randomDelay = Math.floor(Math.random() * 10000); // 0-10000毫秒
console.log(`[Unary Retry] Credential marked unhealthy. Waiting ${randomDelay}ms before retry ${currentRetry + 1}/${maxRetries} with different credential...`);
logger.info(`[Unary Retry] Credential marked unhealthy. Waiting ${randomDelay}ms before retry ${currentRetry + 1}/${maxRetries} with different credential...`);
await new Promise(resolve => setTimeout(resolve, randomDelay));
try {
@ -546,8 +545,8 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
const { getApiServiceWithFallback } = await import('../services/service-manager.js');
const result = await getApiServiceWithFallback(CONFIG, model);
if (result && result.service && result.uuid !== pooluuid) {
console.log(`[Unary Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`);
if (result && result.service) {
logger.info(`[Unary Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`);
// 使用新服务重试
const newRetryContext = {
@ -572,13 +571,11 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
result.serviceConfig?.customName || customName,
newRetryContext
);
} else if (result && result.uuid === pooluuid) {
console.log(`[Unary Retry] No different healthy credential available. Same credential selected.`);
} else {
console.log(`[Unary Retry] No healthy credential available for retry.`);
logger.info(`[Unary Retry] No healthy credential available for retry.`);
}
} catch (retryError) {
console.error(`[Unary Retry] Failed to get alternative service:`, retryError.message);
logger.error(`[Unary Retry] Failed to get alternative service:`, retryError.message);
}
}
@ -618,17 +615,17 @@ export async function handleModelListRequest(req, res, service, endpointType, CO
// 2. Convert the model list to the client's expected format, if necessary.
let clientModelList = nativeModelList;
if (!getProtocolPrefix(toProvider).includes(getProtocolPrefix(fromProvider))) {
console.log(`[ModelList Convert] Converting model list from ${toProvider} to ${fromProvider}`);
logger.info(`[ModelList Convert] Converting model list from ${toProvider} to ${fromProvider}`);
clientModelList = convertData(nativeModelList, 'modelList', toProvider, fromProvider);
} else {
console.log(`[ModelList Convert] Model list format matches. No conversion needed.`);
logger.info(`[ModelList Convert] Model list format matches. No conversion needed.`);
}
console.log(`[ModelList Response] Sending model list to client: ${JSON.stringify(clientModelList)}`);
logger.info(`[ModelList Response] Sending model list to client: ${JSON.stringify(clientModelList)}`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(clientModelList));
} catch (error) {
console.error('\n[Server] Error during model list processing:', error.stack);
logger.error('\n[Server] Error during model list processing:', error.stack);
if (providerPoolManager) {
// 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康
providerPoolManager.markProviderUnhealthy(toProvider, {
@ -677,7 +674,7 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
if (!model) {
throw new Error("Could not determine the model from the request.");
}
console.log(`[Content Generation] Model: ${model}, Stream: ${isStream}`);
logger.info(`[Content Generation] Model: ${model}, Stream: ${isStream}`);
let actualCustomName = CONFIG.customName;
@ -694,14 +691,14 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
// 如果发生了模型级别的 fallback需要更新请求使用的模型
if (result.actualModel && result.actualModel !== model) {
console.log(`[Content Generation] Model Fallback: ${model} -> ${result.actualModel}`);
logger.info(`[Content Generation] Model Fallback: ${model} -> ${result.actualModel}`);
model = result.actualModel;
}
if (result.isFallback) {
console.log(`[Content Generation] Fallback activated: ${CONFIG.MODEL_PROVIDER} -> ${toProvider} (uuid: ${actualUuid})`);
logger.info(`[Content Generation] Fallback activated: ${CONFIG.MODEL_PROVIDER} -> ${toProvider} (uuid: ${actualUuid})`);
} else {
console.log(`[Content Generation] Re-selected service adapter based on model: ${model}`);
logger.info(`[Content Generation] Re-selected service adapter based on model: ${model}`);
}
}
@ -709,15 +706,15 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
let processedRequestBody = originalRequestBody;
// fs.writeFile('originalRequestBody'+Date.now()+'.json', JSON.stringify(originalRequestBody));
if (getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider)) {
console.log(`[Request Convert] Converting request from ${fromProvider} to ${toProvider}`);
logger.info(`[Request Convert] Converting request from ${fromProvider} to ${toProvider}`);
processedRequestBody = convertData(originalRequestBody, 'request', fromProvider, toProvider);
} else {
console.log(`[Request Convert] Request format matches backend provider. No conversion needed.`);
logger.info(`[Request Convert] Request format matches backend provider. No conversion needed.`);
}
// 为 forward provider 添加原始请求路径作为 endpoint
if (requestPath && toProvider === MODEL_PROVIDER.FORWARD_API) {
console.log(`[Forward API] Request path: ${requestPath}`);
logger.info(`[Forward API] Request path: ${requestPath}`);
processedRequestBody.endpoint = requestPath;
}
@ -828,14 +825,14 @@ export function handleError(res, error, provider = null) {
}
errorMessage = hasOriginalMessage ? error.message.trim() : errorMessage;
console.error(`\n[Server] Request failed (${statusCode}): ${errorMessage}`);
logger.error(`\n[Server] Request failed (${statusCode}): ${errorMessage}`);
if (suggestions.length > 0) {
console.error('[Server] Suggestions:');
logger.error('[Server] Suggestions:');
suggestions.forEach((suggestion, index) => {
console.error(` ${index + 1}. ${suggestion}`);
logger.error(` ${index + 1}. ${suggestion}`);
});
}
console.error('[Server] Full error details:', error.stack);
logger.error('[Server] Full error details:', error.stack);
if (!res.headersSent) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
@ -1071,7 +1068,7 @@ export function extractSystemPromptFromRequestBody(requestBody, provider) {
}
break;
default:
console.warn(`[System Prompt] Unknown provider: ${provider}`);
logger.warn(`[System Prompt] Unknown provider: ${provider}`);
break;
}
return incomingSystemText;

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 logger from './logger.js';
import { FETCH_SYSTEM_PROMPT_FILE } from '../utils/common.js';
/**
@ -64,20 +65,20 @@ export class ProviderStrategy {
currentSystemText = await fs.readFile(FETCH_SYSTEM_PROMPT_FILE, 'utf8');
} catch (error) {
if (error.code !== 'ENOENT') {
console.error(`[System Prompt Manager] Error reading system prompt file: ${error.message}`);
logger.error(`[System Prompt Manager] Error reading system prompt file: ${error.message}`);
}
}
try {
if (incomingSystemText && incomingSystemText !== currentSystemText) {
await fs.writeFile(FETCH_SYSTEM_PROMPT_FILE, incomingSystemText);
console.log(`[System Prompt Manager] System prompt updated in file for provider '${providerName}'.`);
logger.info(`[System Prompt Manager] System prompt updated in file for provider '${providerName}'.`);
} else if (!incomingSystemText && currentSystemText) {
await fs.writeFile(FETCH_SYSTEM_PROMPT_FILE, '');
console.log('[System Prompt Manager] System prompt cleared from file.');
logger.info('[System Prompt Manager] System prompt cleared from file.');
}
} catch (error) {
console.error(`[System Prompt Manager] Failed to manage system prompt file: ${error.message}`);
logger.error(`[System Prompt Manager] Failed to manage system prompt file: ${error.message}`);
}
}
}

View file

@ -4,6 +4,7 @@
*/
import * as path from 'path';
import logger from './logger.js';
import { promises as fs } from 'fs';
/**
@ -163,7 +164,7 @@ export function pathsEqual(path1, path2) {
return false;
} catch (error) {
console.warn(`[Path Comparison] Error comparing paths: ${path1} vs ${path2}`, error.message);
logger.warn(`[Path Comparison] Error comparing paths: ${path1} vs ${path2}`, error.message);
return false;
}
}

View file

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

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) {
console.error('Failed to load configuration:', error);
}
@ -201,7 +220,6 @@ async function saveConfiguration() {
config.CRON_REFRESH_TOKEN = document.getElementById('cronRefreshToken')?.checked || false;
config.PROVIDER_POOLS_FILE_PATH = document.getElementById('providerPoolsFilePath')?.value || '';
config.MAX_ERROR_COUNT = parseInt(document.getElementById('maxErrorCount')?.value || 10);
config.POOL_SIZE_LIMIT = parseInt(document.getElementById('poolSizeLimit')?.value || 0);
config.WARMUP_TARGET = parseInt(document.getElementById('warmupTarget')?.value || 0);
config.REFRESH_CONCURRENCY_PER_PROVIDER = parseInt(document.getElementById('refreshConcurrencyPerProvider')?.value || 1);
@ -242,6 +260,16 @@ async function saveConfiguration() {
} else {
config.PROXY_ENABLED_PROVIDERS = [];
}
// 保存日志配置
config.LOG_ENABLED = document.getElementById('logEnabled')?.checked !== false;
config.LOG_OUTPUT_MODE = document.getElementById('logOutputMode')?.value || 'all';
config.LOG_LEVEL = document.getElementById('logLevel')?.value || 'info';
config.LOG_DIR = document.getElementById('logDir')?.value || 'logs';
config.LOG_INCLUDE_REQUEST_ID = document.getElementById('logIncludeRequestId')?.checked !== false;
config.LOG_INCLUDE_TIMESTAMP = document.getElementById('logIncludeTimestamp')?.checked !== false;
config.LOG_MAX_FILE_SIZE = parseInt(document.getElementById('logMaxFileSize')?.value || 10485760);
config.LOG_MAX_FILES = parseInt(document.getElementById('logMaxFiles')?.value || 10);
try {
await window.apiClient.post('/config', config);

View file

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

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

View file

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

View file

@ -196,6 +196,9 @@ const translations = {
'config.hostPlaceholder': '例如: 127.0.0.1',
'config.port': '端口',
'config.portPlaceholder': '3000',
'config.basic.title': '基础设置',
'config.governance.title': '服务治理',
'config.oauth.title': 'OAuth & 令牌',
'config.modelProvider': '模型提供商',
'config.modelProviderHelp': '勾选启动时初始化的模型提供商 (必须至少勾选一个)',
'config.modelProviderRequired': '必须至少勾选一个模型提供商',
@ -253,7 +256,7 @@ const translations = {
'config.advanced.promptLogMode.none': '无 (none)',
'config.advanced.promptLogMode.console': '控制台 (console)',
'config.advanced.promptLogMode.file': '文件 (file)',
'config.advanced.maxRetries': '最大重试次数',
'config.advanced.maxRetries': '提供商内最大重试次数',
'config.advanced.baseDelay': '重试基础延迟(毫秒)',
'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数',
'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次',
@ -293,6 +296,26 @@ const translations = {
'config.proxy.urlNote': '支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理',
'config.proxy.enabledProviders': '启用代理的提供商',
'config.proxy.enabledProvidersNote': '选择需要通过代理访问的提供商,未选中的提供商将直接连接',
'config.log.title': '日志设置',
'config.log.enabled': '启用日志',
'config.log.outputMode': '日志输出模式',
'config.log.outputMode.all': '全部 (控制台+文件)',
'config.log.outputMode.console': '仅控制台',
'config.log.outputMode.file': '仅文件',
'config.log.outputMode.none': '禁用',
'config.log.level': '日志级别',
'config.log.level.debug': '调试 (debug)',
'config.log.level.info': '信息 (info)',
'config.log.level.warn': '警告 (warn)',
'config.log.level.error': '错误 (error)',
'config.log.dir': '日志目录',
'config.log.dirPlaceholder': '例如: logs',
'config.log.includeRequestId': '包含请求ID',
'config.log.includeTimestamp': '包含时间戳',
'config.log.maxFileSize': '最大文件大小(字节)',
'config.log.maxFileSizeNote': '默认 10MB (10485760 字节)',
'config.log.maxFiles': '最大保留文件数',
'config.log.maxFilesNote': '保留最近的日志文件数量',
'config.save': '保存配置',
'config.reset': '重置',
'config.placeholder.nodeName': '例如: 我的节点1',
@ -513,6 +536,7 @@ const translations = {
// Logs
'logs.title': '实时日志',
'logs.clear': '清空日志',
'logs.download': '下载日志',
'logs.autoScroll': '自动滚动',
'logs.autoScroll.on': '自动滚动: 开',
'logs.autoScroll.off': '自动滚动: 关',
@ -631,9 +655,13 @@ const translations = {
'tutorial.main.prompt.file': '系统提示文件路径',
'tutorial.main.prompt.mode': '系统提示模式overwrite(覆盖) 或 append(追加)',
'tutorial.main.retry.title': '重试配置',
'tutorial.main.retry.max': '最大重试次数',
'tutorial.main.retry.max': '提供商内最大重试次数',
'tutorial.main.retry.delay': '重试基础延迟(毫秒)',
'tutorial.main.retry.credentialSwitch': '坏凭证切换最大重试次数',
'tutorial.main.retry.error': '提供商最大错误次数,超过后标记为不健康',
'tutorial.main.governance.warmup': '系统启动时自动刷新的节点数量',
'tutorial.main.governance.concurrency': '每个提供商内部最大并行刷新任务数',
'tutorial.main.governance.poolLimit': '每个提供商类型参与轮询的最大健康凭证数量',
'tutorial.main.example.title': '配置示例',
'tutorial.pool.title': '提供商池配置 (provider_pools.json)',
'tutorial.pool.desc': '提供商池用于配置多个账号,实现负载均衡和故障转移。每个提供商类型可以配置多个账号节点。',
@ -903,6 +931,9 @@ const translations = {
'config.hostPlaceholder': 'e.g.: 127.0.0.1',
'config.port': 'Port',
'config.portPlaceholder': '3000',
'config.basic.title': 'Basic Settings',
'config.governance.title': 'Service Governance',
'config.oauth.title': 'OAuth & Tokens',
'config.modelProvider': 'Model Provider',
'config.modelProviderHelp': 'Check model providers to initialize on startup (must select at least one)',
'config.modelProviderRequired': 'At least one model provider must be selected',
@ -960,7 +991,7 @@ const translations = {
'config.advanced.promptLogMode.none': 'None',
'config.advanced.promptLogMode.console': 'Console',
'config.advanced.promptLogMode.file': 'File',
'config.advanced.maxRetries': 'Max Retries',
'config.advanced.maxRetries': 'Provider Max Retries',
'config.advanced.baseDelay': 'Base Retry Delay (ms)',
'config.advanced.credentialSwitchMaxRetries': 'Credential Switch Max Retries',
'config.advanced.credentialSwitchMaxRetriesNote': 'Max retry count for switching credentials after auth errors (401/403), default 5',
@ -1000,6 +1031,26 @@ const translations = {
'config.proxy.urlNote': 'Supports HTTP, HTTPS and SOCKS5 proxies. Leave empty to disable proxy',
'config.proxy.enabledProviders': 'Providers Using Proxy',
'config.proxy.enabledProvidersNote': 'Select providers that should use the proxy. Unselected providers will connect directly',
'config.log.title': 'Log Settings',
'config.log.enabled': 'Enable Logging',
'config.log.outputMode': 'Log Output Mode',
'config.log.outputMode.all': 'All (Console + File)',
'config.log.outputMode.console': 'Console Only',
'config.log.outputMode.file': 'File Only',
'config.log.outputMode.none': 'Disabled',
'config.log.level': 'Log Level',
'config.log.level.debug': 'Debug',
'config.log.level.info': 'Info',
'config.log.level.warn': 'Warning',
'config.log.level.error': 'Error',
'config.log.dir': 'Log Directory',
'config.log.dirPlaceholder': 'e.g.: logs',
'config.log.includeRequestId': 'Include Request ID',
'config.log.includeTimestamp': 'Include Timestamp',
'config.log.maxFileSize': 'Max File Size (bytes)',
'config.log.maxFileSizeNote': 'Default 10MB (10485760 bytes)',
'config.log.maxFiles': 'Max Files to Keep',
'config.log.maxFilesNote': 'Number of recent log files to keep',
'config.save': 'Save Configuration',
'config.reset': 'Reset',
'config.placeholder.nodeName': 'e.g.: My Node 1',
@ -1220,6 +1271,7 @@ const translations = {
// Logs
'logs.title': 'Real-time Logs',
'logs.clear': 'Clear Logs',
'logs.download': 'Download Logs',
'logs.autoScroll': 'Auto Scroll',
'logs.autoScroll.on': 'Auto Scroll: On',
'logs.autoScroll.off': 'Auto Scroll: Off',
@ -1338,9 +1390,13 @@ const translations = {
'tutorial.main.prompt.file': 'System prompt file path',
'tutorial.main.prompt.mode': 'System prompt mode: overwrite or append',
'tutorial.main.retry.title': 'Retry Configuration',
'tutorial.main.retry.max': 'Maximum retry count',
'tutorial.main.retry.max': 'Provider max retry count',
'tutorial.main.retry.delay': 'Base retry delay (milliseconds)',
'tutorial.main.retry.credentialSwitch': 'Max retries for switching bad credentials',
'tutorial.main.retry.error': 'Max provider error count before marking unhealthy',
'tutorial.main.governance.warmup': 'Number of nodes to auto-refresh on startup',
'tutorial.main.governance.concurrency': 'Maximum parallel refresh tasks per provider',
'tutorial.main.governance.poolLimit': 'Maximum number of healthy credentials per provider type for rotation',
'tutorial.main.example.title': 'Configuration Example',
'tutorial.pool.title': 'Provider Pool Config (provider_pools.json)',
'tutorial.pool.desc': 'Provider pool configures multiple accounts for load balancing and failover. Each provider type can have multiple account nodes.',

View file

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

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

View file

@ -4,128 +4,275 @@
<h2 id="config-title" data-i18n="config.title">配置管理</h2>
<div class="config-panel">
<div class="config-form">
<div class="form-group password-input-group">
<label for="apiKey" data-i18n="config.apiKey">API密钥</label>
<div class="password-input-wrapper">
<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 class="config-group-section">
<h3 data-i18n="config.basic.title"><i class="fas fa-cog"></i> 基础设置</h3>
<div class="form-group password-input-group">
<label for="apiKey" data-i18n="config.apiKey">API密钥</label>
<div class="password-input-wrapper">
<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 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-row">
<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>
<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 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 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>
<!-- 代理与网络 -->
<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="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">
<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 class="form-group">
<label for="systemPromptMode" data-i18n="config.advanced.systemPromptMode">系统提示模式</label>
@ -135,121 +282,22 @@
</select>
</div>
</div>
<div class="config-row">
<div class="form-group">
<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 class="form-group">
<label for="systemPrompt" data-i18n="config.advanced.systemPrompt">系统提示内容</label>
<textarea id="systemPrompt" class="form-control" rows="4"></textarea>
</div>
<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">
<div class="form-group">
<label for="adminPassword" data-i18n="config.advanced.adminPassword">后台登录密码</label>
<div class="password-input-wrapper">
<input type="password" id="adminPassword" class="form-control" data-i18n-placeholder="config.advanced.adminPasswordPlaceholder" placeholder="设置后台登录密码(留空则不修改)" autocomplete="new-password">
<button type="button" class="password-toggle" data-target="adminPassword" aria-label="显示/隐藏密码" data-i18n-aria-label="common.togglePassword">
<input type="password" id="adminPassword" class="form-control" autocomplete="new-password">
<button type="button" class="password-toggle" data-target="adminPassword" aria-label="显示/隐藏密码">
<i class="fas fa-eye" aria-hidden="true"></i>
</button>
</div>
<small class="form-text" data-i18n="config.advanced.adminPasswordNote">用于保护管理控制台的访问,修改后需要重新登录</small>
<small class="form-text" data-i18n="config.advanced.adminPasswordNote">修改后需要重新登录</small>
</div>
</div>
<div class="form-actions">
<button class="btn btn-success" id="saveConfig">
<i class="fas fa-save"></i> <span data-i18n="config.save">保存配置</span>
@ -260,4 +308,4 @@
</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">
<i class="fas fa-trash" aria-hidden="true"></i> <span data-i18n="logs.clear">清空日志</span>
</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">
<i class="fas fa-arrow-down" aria-hidden="true"></i> <span data-i18n="logs.autoScroll.on">自动滚动: 开</span>
</button>

View file

@ -50,8 +50,9 @@
<div class="tutorial-panel">
<h3><i class="fas fa-cogs"></i> <span data-i18n="tutorial.main.title">主配置详解 (config.json)</span></h3>
<div class="tutorial-content">
<!-- 基础设置 -->
<div class="config-section">
<h4 data-i18n="tutorial.main.basic.title">基础配</h4>
<h4 data-i18n="config.basic.title">基础设</h4>
<div class="config-table">
<table>
<thead>
@ -92,38 +93,37 @@
</div>
</div>
<!-- 代理设置 -->
<div class="config-section">
<h4 data-i18n="tutorial.main.prompt.title">系统提示配</h4>
<h4 data-i18n="config.proxy.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.default">默认值</th>
<th data-i18n="tutorial.main.table.desc">说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>SYSTEM_PROMPT_FILE_PATH</code></td>
<td><code>PROXY_URL</code></td>
<td>string</td>
<td>-</td>
<td data-i18n="tutorial.main.prompt.file">系统提示文件路径</td>
<td data-i18n="tutorial.proxy.url">代理地址,支持 HTTP、HTTPS、SOCKS5</td>
</tr>
<tr>
<td><code>SYSTEM_PROMPT_MODE</code></td>
<td>string</td>
<td>overwrite</td>
<td data-i18n="tutorial.main.prompt.mode">系统提示模式overwrite(覆盖) 或 append(追加)</td>
<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-section">
<h4 data-i18n="tutorial.main.retry.title">重试配置</h4>
<h4 data-i18n="config.governance.title">服务治理</h4>
<div class="config-table">
<table>
<thead>
@ -147,12 +147,63 @@
<td>1000</td>
<td data-i18n="tutorial.main.retry.delay">重试基础延迟(毫秒)</td>
</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>
<td><code>MAX_ERROR_COUNT</code></td>
<td>number</td>
<td>10</td>
<td data-i18n="tutorial.main.retry.error">提供商最大错误次数,超过后标记为不健康</td>
</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>
</table>
</div>
@ -164,13 +215,14 @@
"REQUIRED_API_KEY": "your-api-key",
"SERVER_PORT": 3000,
"HOST": "0.0.0.0",
"MODEL_PROVIDER": "gemini-cli-oauth",
"SYSTEM_PROMPT_FILE_PATH": "configs/input_system_prompt.txt",
"SYSTEM_PROMPT_MODE": "overwrite",
"MODEL_PROVIDER": "gemini-cli-oauth,claude-kiro-oauth",
"PROXY_URL": "http://127.0.0.1:7890",
"PROXY_ENABLED_PROVIDERS": ["gemini-cli-oauth", "claude-kiro-oauth"],
"REQUEST_MAX_RETRIES": 3,
"REQUEST_BASE_DELAY": 1000,
"MAX_ERROR_COUNT": 10,
"PROVIDER_POOLS_FILE_PATH": "configs/provider_pools.json"
"WARMUP_TARGET": 5,
"LOG_ENABLED": true,
"LOG_OUTPUT_MODE": "all"
}</code></pre>
</div>
</div>
@ -244,13 +296,6 @@
"oauthCredsFilePath": "configs/gemini/oauth_creds_1.json",
"checkHealth": true,
"checkModel": "gemini-2.0-flash-exp"
},
{
"uuid": "gemini-account-2",
"name": "Gemini 账号 2",
"oauthCredsFilePath": "configs/gemini/oauth_creds_2.json",
"checkHealth": true,
"notSupportedModels": ["gemini-3.0-pro"]
}
],
"claude-kiro-oauth": [
@ -258,8 +303,7 @@
"uuid": "kiro-account-1",
"name": "Kiro 账号 1",
"oauthCredsFilePath": "configs/kiro/kiro-auth-token.json",
"checkHealth": true,
"checkModel": "claude-sonnet-4-5"
"checkHealth": true
}
]
}</code></pre>
@ -294,10 +338,6 @@
"gemini-claude-opus-4-5-thinking": {
"targetProviderType": "claude-kiro-oauth",
"targetModel": "claude-opus-4-5"
},
"claude-opus-4-5": {
"targetProviderType": "gemini-antigravity",
"targetModel": "gemini-claude-opus-4-5-thinking"
}
}
}</code></pre>
@ -305,91 +345,54 @@
</div>
</div>
<!-- 代理配置 -->
<!-- OAuth 授权路径 -->
<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">
<p data-i18n="tutorial.proxy.desc">支持为特定提供商配置代理,用于网络受限环境。</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>
<p data-i18n="tutorial.oauth.desc">各提供商的 OAuth 凭据文件默认存储位置(建议保持在 <code>configs/</code> 目录下以便统一管理):</p>
<div class="oauth-path-list">
<div class="oauth-path-item">
<div class="path-header">
<i class="fas fa-gem"></i>
<span class="path-provider">Gemini</span>
<i class="fas fa-robot"></i>
<span class="path-provider">Gemini CLI OAuth</span>
</div>
<code class="path-value">~/.gemini/oauth_creds.json</code>
<code class="path-value">configs/gemini/</code>
</div>
<div class="oauth-path-item">
<div class="path-header">
<i class="fas fa-rocket"></i>
<span class="path-provider">Antigravity</span>
<span class="path-provider">Gemini Antigravity</span>
</div>
<code class="path-value">~/.antigravity/oauth_creds.json</code>
<code class="path-value">configs/antigravity/</code>
</div>
<div class="oauth-path-item">
<div class="path-header">
<i class="fas fa-robot"></i>
<span class="path-provider">Kiro</span>
<i class="fas fa-key"></i>
<span class="path-provider">Claude Kiro OAuth</span>
</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 class="oauth-path-item">
<div class="path-header">
<i class="fas fa-code"></i>
<span class="path-provider">Qwen</span>
<span class="path-provider">OpenAI Codex OAuth</span>
</div>
<code class="path-value">~/.qwen/oauth_creds.json</code>
<code class="path-value">configs/codex/</code>
</div>
</div>
@ -399,45 +402,4 @@
</div>
</div>
</div>
<!-- 日志配置 -->
<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>
</section>