refactor: 重构服务器部分并整理目录和注释

This commit is contained in:
foxhui
2025-12-13 03:37:51 +08:00
Unverified
parent 063a05a499
commit 5d8dc7c0a5
27 changed files with 367 additions and 378 deletions
+128
View File
@@ -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;
+111
View File
@@ -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
};
+167
View File
@@ -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;
}