mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
refactor: 重构服务器部分并整理目录和注释
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @fileoverview 配置加载模块
|
||||
* @description 负责读取/解析 `config.yaml`,并提供 API Key 生成能力(供脚本使用)。
|
||||
*
|
||||
* 约定:
|
||||
* - 该模块只负责“读取 + 校验 + 默认值补全”,不负责创建/写入配置文件。
|
||||
* - 初始化/拷贝配置请使用 `config.example.yaml` + `scripts/config-init.js`。
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import yaml from 'yaml';
|
||||
|
||||
import { logger } from './logger.js';
|
||||
|
||||
const CONFIG_PATH = path.join(process.cwd(), 'config.yaml');
|
||||
const EXAMPLE_CONFIG_PATH = path.join(process.cwd(), 'config.example.yaml');
|
||||
|
||||
// 模块级缓存:确保配置只从磁盘读取一次
|
||||
let cachedConfig = null;
|
||||
|
||||
/**
|
||||
* 加载并校验配置(只读)
|
||||
* @returns {object} 配置对象
|
||||
*/
|
||||
export function loadConfig() {
|
||||
// 如果已有缓存,直接返回
|
||||
if (cachedConfig) return cachedConfig;
|
||||
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
const hint = fs.existsSync(EXAMPLE_CONFIG_PATH)
|
||||
? `请复制 ${EXAMPLE_CONFIG_PATH} 为 ${CONFIG_PATH}`
|
||||
: `请创建 ${CONFIG_PATH}(仓库根目录通常提供 config.example.yaml 作为模板)`;
|
||||
throw new Error(`未找到配置文件: ${CONFIG_PATH}。${hint}`);
|
||||
}
|
||||
|
||||
const configFile = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||
const config = yaml.parse(configFile);
|
||||
if (!config || typeof config !== 'object') {
|
||||
throw new Error(`配置文件解析失败: ${CONFIG_PATH}`);
|
||||
}
|
||||
|
||||
// Docker 路径兼容处理
|
||||
if ((!config.browser?.path || !fs.existsSync(config.browser.path)) &&
|
||||
fs.existsSync('/app/camoufox/camoufox')) {
|
||||
logger.info('配置器', '检测到容器环境,自动修正浏览器路径为 /app/camoufox/camoufox');
|
||||
if (!config.browser) config.browser = {};
|
||||
config.browser.path = '/app/camoufox/camoufox';
|
||||
}
|
||||
|
||||
// 基础配置校验
|
||||
if (!config.server || !config.server.port) {
|
||||
throw new Error('配置文件缺少必需字段: server.port');
|
||||
}
|
||||
if (!config.server.auth) {
|
||||
throw new Error('配置文件缺少必需字段: server.auth');
|
||||
}
|
||||
|
||||
// 设置队列配置默认值
|
||||
if (!config.queue) {
|
||||
config.queue = {
|
||||
maxConcurrent: 1,
|
||||
maxQueueSize: 2,
|
||||
imageLimit: 5
|
||||
};
|
||||
} else {
|
||||
// 强制 maxConcurrent 为 1
|
||||
config.queue.maxConcurrent = 1;
|
||||
if (config.queue.maxQueueSize === undefined) config.queue.maxQueueSize = 2;
|
||||
if (config.queue.imageLimit === undefined) config.queue.imageLimit = 5;
|
||||
}
|
||||
|
||||
// 设置 keepalive 配置默认值(兼容旧字段:keepalive.enable)
|
||||
if (!config.server.keepalive) {
|
||||
config.server.keepalive = { mode: 'comment' };
|
||||
} else {
|
||||
if (config.server.keepalive.mode === undefined) config.server.keepalive.mode = 'comment';
|
||||
// 验证 mode 值
|
||||
if (!['comment', 'content'].includes(config.server.keepalive.mode)) {
|
||||
logger.warn('配置器', `无效的 keepalive.mode: ${config.server.keepalive.mode},使用默认值 comment`);
|
||||
config.server.keepalive.mode = 'comment';
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 backend 配置默认值
|
||||
if (!config.backend) {
|
||||
config.backend = {
|
||||
type: 'lmarena',
|
||||
geminiBiz: { entryUrl: '' }
|
||||
};
|
||||
}
|
||||
|
||||
// 新增:Merge 配置初始化
|
||||
if (!config.backend.merge) {
|
||||
config.backend.merge = {
|
||||
enable: false,
|
||||
type: ['zai_is', 'lmarena']
|
||||
};
|
||||
}
|
||||
|
||||
// 校验 GeminiBiz 配置
|
||||
if (config.backend.type === 'gemini_biz') {
|
||||
if (!config.backend.geminiBiz || !config.backend.geminiBiz.entryUrl) {
|
||||
throw new Error('backend.type = gemini_biz requires backend.geminiBiz.entryUrl');
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('配置器', '已加载 config.yaml');
|
||||
logger.debug('配置器', '后端类型:', config.backend.type);
|
||||
logger.debug('配置器', '流式心跳模式:', config.server.keepalive.mode);
|
||||
if (config.backend.type === 'gemini_biz') {
|
||||
logger.debug('配置器', `GeminiBiz 入口: ${config.backend.geminiBiz.entryUrl}`);
|
||||
}
|
||||
|
||||
// 设置日志级别
|
||||
if (config.logLevel) {
|
||||
logger.setLevel(config.logLevel);
|
||||
}
|
||||
|
||||
// 缓存配置
|
||||
cachedConfig = config;
|
||||
return config;
|
||||
}
|
||||
|
||||
// 默认导出为函数
|
||||
export default loadConfig;
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @fileoverview 控制台日志模块
|
||||
* @description 提供带时间戳/级别/模块名的彩色日志输出,并支持通过环境变量控制日志等级。
|
||||
*
|
||||
* - 环境变量:LOG_LEVEL=debug|info|warn|error
|
||||
* - 输出格式:YYYY-MM-DD HH:mm:ss.SSS [LEVEL] [模块] 消息 | k=v ...
|
||||
*/
|
||||
|
||||
import process from 'process';
|
||||
|
||||
const LEVELS = ['debug', 'info', 'warn', 'error'];
|
||||
|
||||
// ANSI 颜色代码
|
||||
const COLORS = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
white: '\x1b[37m'
|
||||
};
|
||||
|
||||
// 根据日志级别获取颜色
|
||||
function getColor(level) {
|
||||
switch (level.toLowerCase()) {
|
||||
case 'error':
|
||||
return COLORS.red;
|
||||
case 'warn':
|
||||
return COLORS.yellow;
|
||||
case 'info':
|
||||
return COLORS.white;
|
||||
case 'debug':
|
||||
return COLORS.blue;
|
||||
default:
|
||||
return COLORS.reset;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(date = new Date()) {
|
||||
const pad = (n, len = 2) => n.toString().padStart(len, '0');
|
||||
const yyyy = date.getFullYear();
|
||||
const MM = pad(date.getMonth() + 1);
|
||||
const dd = pad(date.getDate());
|
||||
const HH = pad(date.getHours());
|
||||
const mm = pad(date.getMinutes());
|
||||
const ss = pad(date.getSeconds());
|
||||
const SSS = pad(date.getMilliseconds(), 3);
|
||||
return `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}.${SSS}`;
|
||||
}
|
||||
|
||||
let currentLogLevel = (process.env.LOG_LEVEL || 'info').toLowerCase();
|
||||
|
||||
export function setLogLevel(level) {
|
||||
if (level && LEVELS.includes(level.toLowerCase())) {
|
||||
currentLogLevel = level.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
function shouldLog(level) {
|
||||
const targetLevel = level.toLowerCase();
|
||||
const envIndex = LEVELS.indexOf(currentLogLevel);
|
||||
const targetIndex = LEVELS.indexOf(targetLevel);
|
||||
|
||||
// If env level is invalid, default to info (index 1)
|
||||
const effectiveEnvIndex = envIndex === -1 ? 1 : envIndex;
|
||||
|
||||
return targetIndex >= effectiveEnvIndex;
|
||||
}
|
||||
|
||||
export function log(level, mod, msg, meta = {}) {
|
||||
if (!shouldLog(level)) return;
|
||||
|
||||
const ts = formatTime();
|
||||
const levelTag = level.toUpperCase();
|
||||
const base = `${ts} [${levelTag}] [${mod}] ${msg}`;
|
||||
|
||||
const metaStr = Object.keys(meta).length
|
||||
? ' | ' + Object.entries(meta).map(([k, v]) => {
|
||||
if (v instanceof Error) {
|
||||
return `${k}=${v.message}`;
|
||||
}
|
||||
if (typeof v === 'object' && v !== null) {
|
||||
try {
|
||||
return `${k}=${JSON.stringify(v)}`;
|
||||
} catch (e) {
|
||||
return `${k}=[Circular]`;
|
||||
}
|
||||
}
|
||||
return `${k}=${v}`;
|
||||
}).join(' ')
|
||||
: '';
|
||||
|
||||
const line = base + metaStr;
|
||||
const color = getColor(level);
|
||||
const coloredLine = `${color}${line}${COLORS.reset}`;
|
||||
|
||||
if (level === 'error') {
|
||||
console.error(coloredLine);
|
||||
} else if (level === 'warn') {
|
||||
console.warn(coloredLine);
|
||||
} else {
|
||||
console.log(coloredLine);
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
debug: (mod, msg, meta) => log('debug', mod, msg, meta),
|
||||
info: (mod, msg, meta) => log('info', mod, msg, meta),
|
||||
warn: (mod, msg, meta) => log('warn', mod, msg, meta),
|
||||
error: (mod, msg, meta) => log('error', mod, msg, meta),
|
||||
setLevel: setLogLevel
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @fileoverview 代理适配模块
|
||||
* @description 将配置中的 HTTP/SOCKS5 代理转换为 Playwright 可用的代理配置,并在需要时通过 proxy-chain 搭建本地 HTTP 代理桥接。
|
||||
*/
|
||||
|
||||
import { anonymizeProxy, closeAnonymizedProxy } from 'proxy-chain';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
// 全局代理状态:用于清理 proxy-chain 创建的本地代理资源
|
||||
const proxyState = {
|
||||
anonymizedProxyUrl: null, // 转换后的 HTTP 代理地址
|
||||
originalProxyUrl: null // 原始代理地址
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建代理 URL
|
||||
* @param {object} proxyConfig - 代理配置对象
|
||||
* @param {string} proxyConfig.type - 代理类型 ('http' 或 'socks5')
|
||||
* @param {string} proxyConfig.host - 代理主机地址
|
||||
* @param {number} proxyConfig.port - 代理端口
|
||||
* @param {string} [proxyConfig.user] - 可选的用户名
|
||||
* @param {string} [proxyConfig.passwd] - 可选的密码
|
||||
* @returns {string} - 代理 URL
|
||||
*/
|
||||
export function buildProxyUrl(proxyConfig) {
|
||||
const { type, host, port, user, passwd } = proxyConfig;
|
||||
|
||||
// 构建带认证的代理 URL
|
||||
if (user && passwd) {
|
||||
return `${type}://${user}:${passwd}@${host}:${port}`;
|
||||
}
|
||||
|
||||
// 构建不带认证的代理 URL
|
||||
if (type === 'socks5') {
|
||||
return `socks5://${host}:${port}`;
|
||||
}
|
||||
|
||||
return `http://${host}:${port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将代理转换为 HTTP 代理
|
||||
* - HTTP 代理:直接返回
|
||||
* - SOCKS5 代理:使用 proxy-chain 转换为本地 HTTP 代理
|
||||
* @param {object} proxyConfig - 代理配置对象
|
||||
* @returns {Promise<string|null>} - 转换后的 HTTP 代理 URL,如果无需代理则返回 null
|
||||
*/
|
||||
export async function getHttpProxy(proxyConfig) {
|
||||
if (!proxyConfig || !proxyConfig.enable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { type, host, port } = proxyConfig;
|
||||
const originalUrl = buildProxyUrl(proxyConfig);
|
||||
|
||||
// 如果是 HTTP 代理,直接返回
|
||||
if (type === 'http') {
|
||||
logger.debug('代理器', `使用 HTTP 代理: ${host}:${port}`);
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
// 如果是 SOCKS5 代理,需要转换为 HTTP 代理
|
||||
if (type === 'socks5') {
|
||||
try {
|
||||
logger.info('代理器', `检测到 SOCKS5 代理,正在转换为 HTTP 代理: ${host}:${port}`);
|
||||
const httpProxyUrl = await anonymizeProxy(originalUrl);
|
||||
|
||||
// 保存状态用于后续清理
|
||||
proxyState.anonymizedProxyUrl = httpProxyUrl;
|
||||
proxyState.originalProxyUrl = originalUrl;
|
||||
|
||||
logger.info('代理器', `SOCKS5 代理已转换为 HTTP 代理: ${httpProxyUrl}`);
|
||||
return httpProxyUrl;
|
||||
} catch (error) {
|
||||
logger.error('代理器', `SOCKS5 代理转换失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn('代理器', `不支持的代理类型: ${type}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用于浏览器的代理配置
|
||||
* 返回 Playwright 可以使用的代理对象
|
||||
* @param {object} proxyConfig - 代理配置对象
|
||||
* @returns {Promise<object|null>} - Playwright 代理配置对象
|
||||
*/
|
||||
export async function getBrowserProxy(proxyConfig) {
|
||||
if (!proxyConfig || !proxyConfig.enable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { type, host, port, user, passwd } = proxyConfig;
|
||||
|
||||
// 对于 SOCKS5 + 认证,需要转换为 HTTP 代理
|
||||
if (type === 'socks5' && user && passwd) {
|
||||
try {
|
||||
const originalUrl = buildProxyUrl(proxyConfig);
|
||||
logger.info('代理器', `检测到需鉴权的 SOCKS5 代理,正在创建本地代理桥接: ${host}:${port}`);
|
||||
|
||||
const httpProxyUrl = await anonymizeProxy(originalUrl);
|
||||
|
||||
// 保存状态用于后续清理
|
||||
proxyState.anonymizedProxyUrl = httpProxyUrl;
|
||||
proxyState.originalProxyUrl = originalUrl;
|
||||
|
||||
logger.info('代理器', `本地代理桥接已建立: ${httpProxyUrl} -> ${host}:${port}`);
|
||||
|
||||
return {
|
||||
server: httpProxyUrl
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('代理器', `本地代理桥接创建失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 对于其他情况(HTTP 代理、不带认证的 SOCKS5)
|
||||
const proxyUrl = type === 'socks5' ? `socks5://${host}:${port}` : `${host}:${port}`;
|
||||
|
||||
const proxyObject = {
|
||||
server: proxyUrl
|
||||
};
|
||||
|
||||
// 如果有认证信息,添加到代理对象
|
||||
if (user && passwd) {
|
||||
proxyObject.username = user;
|
||||
proxyObject.password = passwd;
|
||||
}
|
||||
|
||||
logger.info('代理器', `代理配置: ${type}://${host}:${port}`);
|
||||
return proxyObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理代理资源
|
||||
* 关闭由 proxy-chain 创建的本地代理服务器
|
||||
*/
|
||||
export async function cleanupProxy() {
|
||||
if (proxyState.anonymizedProxyUrl) {
|
||||
try {
|
||||
logger.debug('代理器', '正在关闭本地代理桥接...');
|
||||
await closeAnonymizedProxy(proxyState.anonymizedProxyUrl, true);
|
||||
logger.debug('代理器', '本地代理桥接已关闭');
|
||||
|
||||
// 清理状态
|
||||
proxyState.anonymizedProxyUrl = null;
|
||||
proxyState.originalProxyUrl = null;
|
||||
} catch (error) {
|
||||
logger.error('代理器', `关闭本地代理桥接失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从配置文件读取代理配置
|
||||
* @param {object} config - 配置对象
|
||||
* @returns {object|null} - 代理配置对象或 null
|
||||
*/
|
||||
export function getProxyConfig(config) {
|
||||
if (config?.browser?.proxy?.enable) {
|
||||
return config.browser.proxy;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user