mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 支持多窗口并行且支持账号数据隔离
This commit is contained in:
+181
-44
@@ -3,7 +3,7 @@
|
||||
* @description 负责读取/解析 `config.yaml`,并提供 API Key 生成能力(供脚本使用)。
|
||||
*
|
||||
* 约定:
|
||||
* - 该模块只负责“读取 + 校验 + 默认值补全”,不负责创建/写入配置文件。
|
||||
* - 该模块只负责"读取 + 校验 + 默认值补全",不负责创建/写入配置文件。
|
||||
* - 初始化/拷贝配置请使用 `config.example.yaml` + `scripts/config-init.js`。
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,129 @@ const EXAMPLE_CONFIG_PATH = path.join(process.cwd(), 'config.example.yaml');
|
||||
// 模块级缓存:确保配置只从磁盘读取一次
|
||||
let cachedConfig = null;
|
||||
|
||||
// 有效的适配器类型
|
||||
const VALID_ADAPTER_TYPES = ['lmarena', 'gemini', 'gemini_biz', 'nanobananafree_ai', 'zai_is', 'merge'];
|
||||
|
||||
/**
|
||||
* 解析用户数据目录路径
|
||||
* @param {string|undefined} userDataMark - 用户数据标记
|
||||
* @returns {string} 完整的用户数据目录路径
|
||||
*/
|
||||
function resolveUserDataDir(userDataMark) {
|
||||
const baseDir = path.join(process.cwd(), 'data');
|
||||
if (!userDataMark) {
|
||||
return path.join(baseDir, 'camoufoxUserData');
|
||||
}
|
||||
return path.join(baseDir, `camoufoxUserData_${userDataMark}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析代理配置(Instance 级优先于全局)
|
||||
* @param {object|undefined} globalProxy - 全局代理配置
|
||||
* @param {object|undefined} instanceProxy - Instance 级代理配置
|
||||
* @returns {object|null} 最终代理配置,null 表示直连
|
||||
*/
|
||||
function resolveProxyConfig(globalProxy, instanceProxy) {
|
||||
// Instance 级显式禁用代理 -> 直连
|
||||
if (instanceProxy && instanceProxy.enable === false) {
|
||||
return null;
|
||||
}
|
||||
// Instance 级有配置且启用 -> 使用 Instance 配置
|
||||
if (instanceProxy && instanceProxy.enable === true) {
|
||||
return instanceProxy;
|
||||
}
|
||||
// 回退到全局配置
|
||||
if (globalProxy && globalProxy.enable === true) {
|
||||
return globalProxy;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Instance 配置
|
||||
* @param {object} instance - Instance 配置
|
||||
* @param {number} index - Instance 索引
|
||||
*/
|
||||
function validateInstance(instance, index) {
|
||||
if (!instance.name) {
|
||||
throw new Error(`instances[${index}] 缺少必需字段: name`);
|
||||
}
|
||||
if (!instance.workers || !Array.isArray(instance.workers) || instance.workers.length === 0) {
|
||||
throw new Error(`instances[${index}] (${instance.name}) 缺少有效的 workers 数组`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Worker 配置
|
||||
* @param {object} worker - Worker 配置
|
||||
* @param {string} instanceName - 所属 Instance 名称
|
||||
* @param {number} index - Worker 索引
|
||||
*/
|
||||
function validateWorker(worker, instanceName, index) {
|
||||
if (!worker.name) {
|
||||
throw new Error(`instances[${instanceName}].workers[${index}] 缺少必需字段: name`);
|
||||
}
|
||||
if (!worker.type) {
|
||||
throw new Error(`instances[${instanceName}].workers[${index}] (${worker.name}) 缺少必需字段: type`);
|
||||
}
|
||||
if (!VALID_ADAPTER_TYPES.includes(worker.type)) {
|
||||
throw new Error(`Worker "${worker.name}" 的 type "${worker.type}" 无效。有效值: ${VALID_ADAPTER_TYPES.join(', ')}`);
|
||||
}
|
||||
if (worker.type === 'merge') {
|
||||
if (!worker.mergeTypes || !Array.isArray(worker.mergeTypes) || worker.mergeTypes.length === 0) {
|
||||
throw new Error(`Worker "${worker.name}" 类型为 merge,但缺少有效的 mergeTypes 数组`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开 instances 配置为扁平化的 workers 数组
|
||||
* @param {object[]} instances - instances 配置数组
|
||||
* @param {object} globalProxy - 全局代理配置
|
||||
* @returns {object[]} 扁平化的 worker 配置数组
|
||||
*/
|
||||
function flattenInstancesToWorkers(instances, globalProxy) {
|
||||
const workers = [];
|
||||
const workerNames = new Set();
|
||||
|
||||
for (let i = 0; i < instances.length; i++) {
|
||||
const instance = instances[i];
|
||||
validateInstance(instance, i);
|
||||
|
||||
// 解析 Instance 级配置
|
||||
const userDataDir = resolveUserDataDir(instance.userDataMark);
|
||||
const resolvedProxy = resolveProxyConfig(globalProxy, instance.proxy);
|
||||
|
||||
for (let j = 0; j < instance.workers.length; j++) {
|
||||
const worker = instance.workers[j];
|
||||
validateWorker(worker, instance.name, j);
|
||||
|
||||
// 检查 Worker 名称全局唯一性
|
||||
if (workerNames.has(worker.name)) {
|
||||
throw new Error(`Worker 名称 "${worker.name}" 重复。Worker 名称必须全局唯一。`);
|
||||
}
|
||||
workerNames.add(worker.name);
|
||||
|
||||
// 构建扁平化的 Worker 配置
|
||||
workers.push({
|
||||
// Worker 自身属性
|
||||
name: worker.name,
|
||||
type: worker.type,
|
||||
mergeTypes: worker.mergeTypes || [],
|
||||
mergeMonitor: worker.mergeMonitor || null,
|
||||
|
||||
// 从 Instance 继承的属性
|
||||
instanceName: instance.name,
|
||||
userDataMark: instance.userDataMark || null,
|
||||
userDataDir,
|
||||
resolvedProxy
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return workers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载并校验配置(只读)
|
||||
* @returns {object} 配置对象
|
||||
@@ -35,7 +158,7 @@ export function loadConfig() {
|
||||
}
|
||||
|
||||
const configFile = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||
const config = yaml.parse(configFile);
|
||||
let config = yaml.parse(configFile);
|
||||
if (!config || typeof config !== 'object') {
|
||||
throw new Error(`配置文件解析失败: ${CONFIG_PATH}`);
|
||||
}
|
||||
@@ -56,61 +179,68 @@ export function loadConfig() {
|
||||
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)
|
||||
// 设置 keepalive 配置默认值
|
||||
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: '' }
|
||||
// 设置 Pool 配置默认值
|
||||
if (!config.backend) config.backend = {};
|
||||
if (!config.backend.pool) config.backend.pool = {};
|
||||
|
||||
if (!config.backend.pool.strategy) {
|
||||
config.backend.pool.strategy = 'least_busy';
|
||||
}
|
||||
if (!['least_busy', 'round_robin', 'random'].includes(config.backend.pool.strategy)) {
|
||||
logger.warn('配置器', `无效的 pool.strategy: ${config.backend.pool.strategy},使用默认值 least_busy`);
|
||||
config.backend.pool.strategy = 'least_busy';
|
||||
}
|
||||
|
||||
// 校验 instances 配置
|
||||
if (!config.backend.pool.instances || !Array.isArray(config.backend.pool.instances)) {
|
||||
throw new Error('配置文件缺少必需字段: backend.pool.instances');
|
||||
}
|
||||
if (config.backend.pool.instances.length === 0) {
|
||||
throw new Error('backend.pool.instances 不能为空数组');
|
||||
}
|
||||
|
||||
// 展开 instances 为扁平化的 workers 数组
|
||||
config.backend.pool.workers = flattenInstancesToWorkers(
|
||||
config.backend.pool.instances,
|
||||
config.browser?.proxy
|
||||
);
|
||||
|
||||
// 设置队列配置默认值
|
||||
if (!config.queue) {
|
||||
config.queue = {
|
||||
queueBuffer: 2,
|
||||
imageLimit: 5
|
||||
};
|
||||
} else {
|
||||
if (config.queue.queueBuffer === undefined) config.queue.queueBuffer = 2;
|
||||
if (config.queue.imageLimit === undefined) config.queue.imageLimit = 5;
|
||||
}
|
||||
|
||||
// 新增:Merge 配置初始化
|
||||
if (!config.backend.merge) {
|
||||
config.backend.merge = {
|
||||
enable: false,
|
||||
type: ['zai_is', 'lmarena'],
|
||||
monitor: null
|
||||
};
|
||||
// maxConcurrent 动态计算:等于 Workers 数量
|
||||
config.queue.maxConcurrent = config.backend.pool.workers.length;
|
||||
|
||||
// 初始化 adapter 配置容器
|
||||
if (!config.backend.adapter) {
|
||||
config.backend.adapter = {};
|
||||
}
|
||||
|
||||
// 校验 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}`);
|
||||
// 校验 gemini_biz 配置(如果有 Worker 使用)
|
||||
const hasGeminiBizWorker = config.backend.pool.workers.some(
|
||||
w => w.type === 'gemini_biz' || (w.type === 'merge' && w.mergeTypes?.includes('gemini_biz'))
|
||||
);
|
||||
if (hasGeminiBizWorker && !config.backend.adapter.gemini_biz?.entryUrl) {
|
||||
throw new Error('存在 gemini_biz 类型的 Worker,但 backend.adapter.gemini_biz.entryUrl 未配置');
|
||||
}
|
||||
|
||||
// 设置日志级别
|
||||
@@ -118,12 +248,19 @@ export function loadConfig() {
|
||||
logger.setLevel(config.logLevel);
|
||||
}
|
||||
|
||||
// 日志输出
|
||||
logger.debug('配置器', '已加载 config.yaml');
|
||||
logger.debug('配置器', `Instances: ${config.backend.pool.instances.length}, Workers: ${config.backend.pool.workers.length}`);
|
||||
logger.debug('配置器', `调度策略: ${config.backend.pool.strategy}`);
|
||||
logger.debug('配置器', `流式心跳模式: ${config.server.keepalive.mode}`);
|
||||
|
||||
// 缓存配置
|
||||
cachedConfig = config;
|
||||
return config;
|
||||
}
|
||||
|
||||
// 导出辅助函数供其他模块使用
|
||||
export { resolveUserDataDir, resolveProxyConfig };
|
||||
|
||||
// 默认导出为函数
|
||||
export default loadConfig;
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user