mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
refactor: 代码解耦合,完善WebUI功能,添加网页VNC
This commit is contained in:
@@ -1,282 +0,0 @@
|
||||
/**
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 解析用户数据目录路径
|
||||
* @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`);
|
||||
}
|
||||
// 移除对 type 的硬编码校验,允许动态加载新适配器
|
||||
// if (!VALID_ADAPTER_TYPES.includes(worker.type)) { ... }
|
||||
|
||||
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} 配置对象
|
||||
*/
|
||||
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');
|
||||
let 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');
|
||||
}
|
||||
// 端口类型和范围校验
|
||||
const port = config.server.port;
|
||||
if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error(`server.port 必须是 1-65535 范围内的整数,当前值: ${port}`);
|
||||
}
|
||||
if (!config.server.auth) {
|
||||
throw new Error('配置文件缺少必需字段: server.auth');
|
||||
}
|
||||
if (typeof config.server.auth !== 'string' || config.server.auth.length < 10) {
|
||||
throw new Error('server.auth 必须是至少 10 个字符的字符串 (建议使用 npm run genkey 生成)');
|
||||
}
|
||||
|
||||
// 设置 keepalive 配置默认值
|
||||
if (!config.server.keepalive) {
|
||||
config.server.keepalive = { mode: 'comment' };
|
||||
} else {
|
||||
if (config.server.keepalive.mode === undefined) config.server.keepalive.mode = 'comment';
|
||||
if (!['comment', 'content'].includes(config.server.keepalive.mode)) {
|
||||
logger.warn('配置器', `无效的 keepalive.mode: ${config.server.keepalive.mode},使用默认值 comment`);
|
||||
config.server.keepalive.mode = 'comment';
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 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';
|
||||
}
|
||||
|
||||
// 故障转移配置默认值
|
||||
if (!config.backend.pool.failover) {
|
||||
config.backend.pool.failover = {};
|
||||
}
|
||||
if (config.backend.pool.failover.enabled === undefined) {
|
||||
config.backend.pool.failover.enabled = true;
|
||||
}
|
||||
if (config.backend.pool.failover.maxRetries === undefined) {
|
||||
config.backend.pool.failover.maxRetries = 2;
|
||||
}
|
||||
|
||||
// 校验 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;
|
||||
}
|
||||
|
||||
// maxConcurrent 动态计算:等于 Workers 数量
|
||||
config.queue.maxConcurrent = config.backend.pool.workers.length;
|
||||
|
||||
// 初始化 adapter 配置容器
|
||||
if (!config.backend.adapter) {
|
||||
config.backend.adapter = {};
|
||||
}
|
||||
|
||||
// 校验 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 未配置');
|
||||
}
|
||||
|
||||
// 设置日志级别
|
||||
if (config.logLevel) {
|
||||
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;
|
||||
@@ -1,295 +0,0 @@
|
||||
/**
|
||||
* @fileoverview 配置管理模块
|
||||
* @description 提供配置读取和写入能力,支持分段更新
|
||||
*/
|
||||
|
||||
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');
|
||||
|
||||
/**
|
||||
* 读取原始配置(不带缓存,直接从磁盘读取)
|
||||
* @returns {object} 原始配置对象
|
||||
*/
|
||||
function readRawConfig() {
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
throw new Error('配置文件不存在');
|
||||
}
|
||||
const content = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||
return yaml.parse(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入配置到文件
|
||||
* @param {object} config - 完整配置对象
|
||||
*/
|
||||
function writeConfig(config) {
|
||||
// 使用 yaml 库的默认序列化(会丢失注释,但结构正确)
|
||||
const content = yaml.stringify(config, {
|
||||
indent: 2,
|
||||
lineWidth: 0 // 不自动换行
|
||||
});
|
||||
fs.writeFileSync(CONFIG_PATH, content, 'utf8');
|
||||
logger.info('管理器', '配置已保存到 config.yaml');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器配置
|
||||
* @returns {object}
|
||||
*/
|
||||
export function getServerConfig() {
|
||||
const config = readRawConfig();
|
||||
return {
|
||||
port: config.server?.port || 3000,
|
||||
authToken: config.server?.auth || '',
|
||||
keepaliveMode: config.server?.keepalive?.mode || 'comment'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存服务器配置
|
||||
* @param {object} data - 服务器配置
|
||||
*/
|
||||
export function saveServerConfig(data) {
|
||||
const config = readRawConfig();
|
||||
|
||||
if (!config.server) config.server = {};
|
||||
|
||||
if (data.port !== undefined) config.server.port = data.port;
|
||||
if (data.authToken !== undefined) config.server.auth = data.authToken;
|
||||
if (data.keepaliveMode !== undefined) {
|
||||
if (!config.server.keepalive) config.server.keepalive = {};
|
||||
config.server.keepalive.mode = data.keepaliveMode;
|
||||
}
|
||||
|
||||
writeConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器配置
|
||||
* @returns {object}
|
||||
*/
|
||||
export function getBrowserConfig() {
|
||||
const config = readRawConfig();
|
||||
const browser = config.browser || {};
|
||||
const proxy = browser.proxy || {};
|
||||
|
||||
return {
|
||||
path: browser.path || '',
|
||||
headless: browser.headless || false,
|
||||
proxy: {
|
||||
enable: proxy.enable || false,
|
||||
type: proxy.type || 'http',
|
||||
host: proxy.host || '',
|
||||
port: proxy.port || 0,
|
||||
auth: !!(proxy.user || proxy.passwd),
|
||||
username: proxy.user || '',
|
||||
password: proxy.passwd || ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存浏览器配置
|
||||
* @param {object} data - 浏览器配置
|
||||
*/
|
||||
export function saveBrowserConfig(data) {
|
||||
const config = readRawConfig();
|
||||
|
||||
if (!config.browser) config.browser = {};
|
||||
|
||||
if (data.path !== undefined) config.browser.path = data.path;
|
||||
if (data.headless !== undefined) config.browser.headless = data.headless;
|
||||
|
||||
if (data.proxy) {
|
||||
if (!config.browser.proxy) config.browser.proxy = {};
|
||||
const p = data.proxy;
|
||||
|
||||
if (p.enable !== undefined) config.browser.proxy.enable = p.enable;
|
||||
if (p.type !== undefined) config.browser.proxy.type = p.type;
|
||||
if (p.host !== undefined) config.browser.proxy.host = p.host;
|
||||
if (p.port !== undefined) config.browser.proxy.port = p.port;
|
||||
if (p.username !== undefined) config.browser.proxy.user = p.username;
|
||||
if (p.password !== undefined) config.browser.proxy.passwd = p.password;
|
||||
}
|
||||
|
||||
writeConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列配置
|
||||
* @returns {object}
|
||||
*/
|
||||
export function getQueueConfig() {
|
||||
const config = readRawConfig();
|
||||
return {
|
||||
queueBuffer: config.queue?.queueBuffer ?? 2,
|
||||
imageLimit: config.queue?.imageLimit ?? 5
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存队列配置
|
||||
* @param {object} data - 队列配置
|
||||
*/
|
||||
export function saveQueueConfig(data) {
|
||||
const config = readRawConfig();
|
||||
|
||||
if (!config.queue) config.queue = {};
|
||||
|
||||
if (data.queueBuffer !== undefined) config.queue.queueBuffer = data.queueBuffer;
|
||||
if (data.imageLimit !== undefined) config.queue.imageLimit = data.imageLimit;
|
||||
|
||||
writeConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实例配置
|
||||
* @returns {object[]}
|
||||
*/
|
||||
export function getInstancesConfig() {
|
||||
const config = readRawConfig();
|
||||
const instances = config.backend?.pool?.instances || [];
|
||||
|
||||
return instances.map(inst => ({
|
||||
name: inst.name,
|
||||
userDataMark: inst.userDataMark || null,
|
||||
proxy: inst.proxy ? {
|
||||
enable: inst.proxy.enable || false,
|
||||
type: inst.proxy.type || 'http',
|
||||
host: inst.proxy.host || '',
|
||||
port: inst.proxy.port || 0
|
||||
} : null,
|
||||
workers: (inst.workers || []).map(w => ({
|
||||
name: w.name,
|
||||
type: w.type,
|
||||
mergeTypes: w.mergeTypes || [],
|
||||
mergeMonitor: w.mergeMonitor || null
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存实例配置
|
||||
* @param {object[]} data - 实例配置列表
|
||||
*/
|
||||
export function saveInstancesConfig(data) {
|
||||
const config = readRawConfig();
|
||||
|
||||
if (!config.backend) config.backend = {};
|
||||
if (!config.backend.pool) config.backend.pool = {};
|
||||
|
||||
// 转换为 YAML 格式
|
||||
config.backend.pool.instances = data.map(inst => {
|
||||
const result = {
|
||||
name: inst.name
|
||||
};
|
||||
|
||||
if (inst.userDataMark) {
|
||||
result.userDataMark = inst.userDataMark;
|
||||
}
|
||||
|
||||
if (inst.proxy && inst.proxy.enable) {
|
||||
result.proxy = {
|
||||
enable: true,
|
||||
type: inst.proxy.type || 'http',
|
||||
host: inst.proxy.host,
|
||||
port: inst.proxy.port
|
||||
};
|
||||
if (inst.proxy.username) result.proxy.user = inst.proxy.username;
|
||||
if (inst.proxy.password) result.proxy.passwd = inst.proxy.password;
|
||||
}
|
||||
|
||||
result.workers = (inst.workers || []).map(w => {
|
||||
const worker = {
|
||||
name: w.name,
|
||||
type: w.type
|
||||
};
|
||||
if (w.type === 'merge' && w.mergeTypes) {
|
||||
worker.mergeTypes = w.mergeTypes;
|
||||
if (w.mergeMonitor) worker.mergeMonitor = w.mergeMonitor;
|
||||
}
|
||||
return worker;
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
writeConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器配置
|
||||
* @returns {object}
|
||||
*/
|
||||
export function getAdaptersConfig() {
|
||||
const config = readRawConfig();
|
||||
return config.backend?.adapter || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存适配器配置
|
||||
* @param {object} data - 适配器配置(键值对)
|
||||
*/
|
||||
export function saveAdaptersConfig(data) {
|
||||
const config = readRawConfig();
|
||||
|
||||
if (!config.backend) config.backend = {};
|
||||
|
||||
// 合并而非覆盖,保留其他适配器配置
|
||||
config.backend.adapter = {
|
||||
...(config.backend.adapter || {}),
|
||||
...data
|
||||
};
|
||||
|
||||
writeConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Pool 配置(负载均衡和故障转移)
|
||||
* @returns {object}
|
||||
*/
|
||||
export function getPoolConfig() {
|
||||
const config = readRawConfig();
|
||||
const pool = config.backend?.pool || {};
|
||||
const failover = pool.failover || {};
|
||||
|
||||
return {
|
||||
strategy: pool.strategy || 'least_busy',
|
||||
failover: {
|
||||
enabled: failover.enabled !== false, // 默认 true
|
||||
maxRetries: failover.maxRetries ?? 2
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Pool 配置
|
||||
* @param {object} data - Pool 配置
|
||||
*/
|
||||
export function savePoolConfig(data) {
|
||||
const config = readRawConfig();
|
||||
|
||||
if (!config.backend) config.backend = {};
|
||||
if (!config.backend.pool) config.backend.pool = {};
|
||||
|
||||
if (data.strategy !== undefined) {
|
||||
config.backend.pool.strategy = data.strategy;
|
||||
}
|
||||
|
||||
if (data.failover) {
|
||||
if (!config.backend.pool.failover) config.backend.pool.failover = {};
|
||||
if (data.failover.enabled !== undefined) {
|
||||
config.backend.pool.failover.enabled = data.failover.enabled;
|
||||
}
|
||||
if (data.failover.maxRetries !== undefined) {
|
||||
config.backend.pool.failover.maxRetries = data.failover.maxRetries;
|
||||
}
|
||||
}
|
||||
|
||||
writeConfig(config);
|
||||
}
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
/**
|
||||
* @fileoverview 配置校验模块
|
||||
* @description 对前端提交的配置进行严格校验
|
||||
*/
|
||||
|
||||
import { registry } from '../backend/registry.js';
|
||||
|
||||
/**
|
||||
* 校验 Server 配置
|
||||
* @param {object} data - 服务器配置
|
||||
* @returns {{valid: boolean, errors: string[]}}
|
||||
*/
|
||||
export function validateServerConfig(data) {
|
||||
const errors = [];
|
||||
|
||||
// 端口校验
|
||||
if (data.port !== undefined) {
|
||||
if (typeof data.port !== 'number' || !Number.isInteger(data.port)) {
|
||||
errors.push('port 必须是整数');
|
||||
} else if (data.port < 1 || data.port > 65535) {
|
||||
errors.push('port 必须在 1-65535 范围内');
|
||||
}
|
||||
}
|
||||
|
||||
// Auth Token 校验
|
||||
if (data.authToken !== undefined) {
|
||||
if (typeof data.authToken !== 'string') {
|
||||
errors.push('authToken 必须是字符串');
|
||||
} else if (data.authToken.length < 10) {
|
||||
errors.push('authToken 必须至少 10 个字符');
|
||||
}
|
||||
}
|
||||
|
||||
// Keepalive Mode 校验
|
||||
if (data.keepaliveMode !== undefined) {
|
||||
if (!['comment', 'content'].includes(data.keepaliveMode)) {
|
||||
errors.push('keepaliveMode 必须是 comment 或 content');
|
||||
}
|
||||
}
|
||||
|
||||
// Queue Buffer 校验
|
||||
if (data.queueBuffer !== undefined) {
|
||||
if (typeof data.queueBuffer !== 'number' || !Number.isInteger(data.queueBuffer)) {
|
||||
errors.push('queueBuffer 必须是整数');
|
||||
} else if (data.queueBuffer < 0) {
|
||||
errors.push('queueBuffer 不能为负数');
|
||||
}
|
||||
}
|
||||
|
||||
// Image Limit 校验
|
||||
if (data.imageLimit !== undefined) {
|
||||
if (typeof data.imageLimit !== 'number' || !Number.isInteger(data.imageLimit)) {
|
||||
errors.push('imageLimit 必须是整数');
|
||||
} else if (data.imageLimit < 1 || data.imageLimit > 10) {
|
||||
errors.push('imageLimit 必须在 1-10 范围内');
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Browser 配置
|
||||
* @param {object} data - 浏览器配置
|
||||
* @returns {{valid: boolean, errors: string[]}}
|
||||
*/
|
||||
export function validateBrowserConfig(data) {
|
||||
const errors = [];
|
||||
|
||||
// Path 校验(可选,字符串)
|
||||
if (data.path !== undefined && typeof data.path !== 'string') {
|
||||
errors.push('path 必须是字符串');
|
||||
}
|
||||
|
||||
// Headless 校验
|
||||
if (data.headless !== undefined && typeof data.headless !== 'boolean') {
|
||||
errors.push('headless 必须是布尔值');
|
||||
}
|
||||
|
||||
// Proxy 校验
|
||||
if (data.proxy) {
|
||||
if (data.proxy.enable !== undefined && typeof data.proxy.enable !== 'boolean') {
|
||||
errors.push('proxy.enable 必须是布尔值');
|
||||
}
|
||||
if (data.proxy.type !== undefined && !['http', 'socks5'].includes(data.proxy.type)) {
|
||||
errors.push('proxy.type 必须是 http 或 socks5');
|
||||
}
|
||||
if (data.proxy.port !== undefined) {
|
||||
if (typeof data.proxy.port !== 'number' || !Number.isInteger(data.proxy.port)) {
|
||||
errors.push('proxy.port 必须是整数');
|
||||
} else if (data.proxy.port < 1 || data.proxy.port > 65535) {
|
||||
errors.push('proxy.port 必须在 1-65535 范围内');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Instances 配置
|
||||
* @param {object[]} data - 实例配置列表
|
||||
* @returns {{valid: boolean, errors: string[]}}
|
||||
*/
|
||||
export function validateInstancesConfig(data) {
|
||||
const errors = [];
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
return { valid: false, errors: ['instances 必须是数组'] };
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return { valid: false, errors: ['instances 不能为空'] };
|
||||
}
|
||||
|
||||
const instanceNames = new Set();
|
||||
const workerNames = new Set();
|
||||
|
||||
// 获取有效的适配器类型列表
|
||||
const validAdapterTypes = new Set(registry.getAdapterIds());
|
||||
validAdapterTypes.add('merge'); // merge 是特殊类型
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const inst = data[i];
|
||||
const prefix = `instances[${i}]`;
|
||||
|
||||
// Instance 名称校验
|
||||
if (!inst.name || typeof inst.name !== 'string') {
|
||||
errors.push(`${prefix}: name 是必填字段且必须是字符串`);
|
||||
} else if (inst.name.trim() === '') {
|
||||
errors.push(`${prefix}: name 不能为空`);
|
||||
} else if (instanceNames.has(inst.name)) {
|
||||
errors.push(`${prefix}: Instance 名称 "${inst.name}" 重复`);
|
||||
} else {
|
||||
instanceNames.add(inst.name);
|
||||
}
|
||||
|
||||
// userDataMark 校验(可选,可为空)
|
||||
if (inst.userDataMark !== undefined && inst.userDataMark !== null && inst.userDataMark !== '') {
|
||||
if (typeof inst.userDataMark !== 'string') {
|
||||
errors.push(`${prefix}: userDataMark 必须是字符串`);
|
||||
} else if (!/^[a-zA-Z0-9_-]+$/.test(inst.userDataMark)) {
|
||||
errors.push(`${prefix}: userDataMark 只能包含字母、数字、下划线和连字符`);
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy 校验(可选)
|
||||
if (inst.proxy) {
|
||||
if (inst.proxy.enable !== undefined && typeof inst.proxy.enable !== 'boolean') {
|
||||
errors.push(`${prefix}.proxy: enable 必须是布尔值`);
|
||||
}
|
||||
if (inst.proxy.type !== undefined && !['http', 'socks5'].includes(inst.proxy.type)) {
|
||||
errors.push(`${prefix}.proxy: type 必须是 http 或 socks5`);
|
||||
}
|
||||
if (inst.proxy.port !== undefined) {
|
||||
if (typeof inst.proxy.port !== 'number') {
|
||||
errors.push(`${prefix}.proxy: port 必须是数字`);
|
||||
} else if (inst.proxy.port < 1 || inst.proxy.port > 65535) {
|
||||
errors.push(`${prefix}.proxy: port 必须在 1-65535 范围内`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workers 校验
|
||||
if (!inst.workers || !Array.isArray(inst.workers)) {
|
||||
errors.push(`${prefix}: workers 是必填字段且必须是数组`);
|
||||
} else if (inst.workers.length === 0) {
|
||||
errors.push(`${prefix}: workers 不能为空`);
|
||||
} else {
|
||||
for (let j = 0; j < inst.workers.length; j++) {
|
||||
const w = inst.workers[j];
|
||||
const wPrefix = `${prefix}.workers[${j}]`;
|
||||
|
||||
// Worker 名称校验
|
||||
if (!w.name || typeof w.name !== 'string') {
|
||||
errors.push(`${wPrefix}: name 是必填字段`);
|
||||
} else if (w.name.trim() === '') {
|
||||
errors.push(`${wPrefix}: name 不能为空`);
|
||||
} else if (workerNames.has(w.name)) {
|
||||
errors.push(`${wPrefix}: Worker 名称 "${w.name}" 全局重复(Worker 名称必须全局唯一)`);
|
||||
} else {
|
||||
workerNames.add(w.name);
|
||||
}
|
||||
|
||||
// Worker type 校验
|
||||
if (!w.type || typeof w.type !== 'string') {
|
||||
errors.push(`${wPrefix}: type 是必填字段`);
|
||||
} else if (!validAdapterTypes.has(w.type)) {
|
||||
errors.push(`${wPrefix}: type "${w.type}" 不是有效的适配器类型`);
|
||||
}
|
||||
|
||||
// merge 类型特殊校验
|
||||
if (w.type === 'merge') {
|
||||
if (!w.mergeTypes || !Array.isArray(w.mergeTypes) || w.mergeTypes.length === 0) {
|
||||
errors.push(`${wPrefix}: merge 类型必须指定 mergeTypes 数组`);
|
||||
} else {
|
||||
for (const mt of w.mergeTypes) {
|
||||
if (!validAdapterTypes.has(mt) || mt === 'merge') {
|
||||
errors.push(`${wPrefix}: mergeTypes 中的 "${mt}" 不是有效的适配器类型`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (w.mergeMonitor && !w.mergeTypes?.includes(w.mergeMonitor)) {
|
||||
errors.push(`${wPrefix}: mergeMonitor "${w.mergeMonitor}" 必须是 mergeTypes 中的一个`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Pool 配置
|
||||
* @param {object} data - Pool 配置
|
||||
* @returns {{valid: boolean, errors: string[]}}
|
||||
*/
|
||||
export function validatePoolConfig(data) {
|
||||
const errors = [];
|
||||
|
||||
// Strategy 校验
|
||||
if (data.strategy !== undefined) {
|
||||
if (!['least_busy', 'round_robin', 'random'].includes(data.strategy)) {
|
||||
errors.push('strategy 必须是 least_busy、round_robin 或 random');
|
||||
}
|
||||
}
|
||||
|
||||
// Failover 校验
|
||||
if (data.failover) {
|
||||
if (data.failover.enabled !== undefined && typeof data.failover.enabled !== 'boolean') {
|
||||
errors.push('failover.enabled 必须是布尔值');
|
||||
}
|
||||
if (data.failover.maxRetries !== undefined) {
|
||||
if (typeof data.failover.maxRetries !== 'number' || !Number.isInteger(data.failover.maxRetries)) {
|
||||
errors.push('failover.maxRetries 必须是整数');
|
||||
} else if (data.failover.maxRetries < 0) {
|
||||
errors.push('failover.maxRetries 不能为负数');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Adapters 配置
|
||||
* @param {object} data - 适配器配置
|
||||
* @returns {{valid: boolean, errors: string[]}}
|
||||
*/
|
||||
export function validateAdaptersConfig(data) {
|
||||
const errors = [];
|
||||
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return { valid: false, errors: ['adapters 配置必须是对象'] };
|
||||
}
|
||||
|
||||
// gemini_biz 配置校验
|
||||
if (data.gemini_biz) {
|
||||
if (data.gemini_biz.entryUrl !== undefined) {
|
||||
if (typeof data.gemini_biz.entryUrl !== 'string') {
|
||||
errors.push('gemini_biz.entryUrl 必须是字符串');
|
||||
} else if (data.gemini_biz.entryUrl && !data.gemini_biz.entryUrl.startsWith('https://')) {
|
||||
errors.push('gemini_biz.entryUrl 必须以 https:// 开头');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @fileoverview IPC 通信模块
|
||||
* @description 提供与 Supervisor 进程通信的能力
|
||||
*/
|
||||
|
||||
import net from 'net';
|
||||
|
||||
/**
|
||||
* 发送重启信号给 Supervisor
|
||||
* @param {string[]} [extraArgs] - 额外的命令行参数
|
||||
* @returns {Promise<boolean>} 是否成功发送
|
||||
*/
|
||||
export async function sendRestartSignal(extraArgs = []) {
|
||||
const ipcPath = process.env.SUPERVISOR_IPC;
|
||||
|
||||
if (!ipcPath) {
|
||||
console.warn('[IPC] 未运行在 Supervisor 模式下,无法发送重启信号');
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const client = net.createConnection(ipcPath, () => {
|
||||
// 格式: RESTART 或 RESTART:arg1 arg2
|
||||
const command = extraArgs.length > 0
|
||||
? `RESTART:${extraArgs.join(' ')}`
|
||||
: 'RESTART';
|
||||
client.write(command);
|
||||
client.end();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
console.error('[IPC] 连接 Supervisor 失败:', err.message);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送停止信号给 Supervisor
|
||||
* @returns {Promise<boolean>} 是否成功发送
|
||||
*/
|
||||
export async function sendStopSignal() {
|
||||
const ipcPath = process.env.SUPERVISOR_IPC;
|
||||
|
||||
if (!ipcPath) {
|
||||
console.warn('[IPC] 未运行在 Supervisor 模式下,无法发送停止信号');
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const client = net.createConnection(ipcPath, () => {
|
||||
client.write('STOP');
|
||||
client.end();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
console.error('[IPC] 连接 Supervisor 失败:', err.message);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否运行在 Supervisor 模式下
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isUnderSupervisor() {
|
||||
return !!process.env.SUPERVISOR_IPC;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 VNC 状态信息
|
||||
* @returns {Promise<{enabled: boolean, port: number, display: string, xvfbMode: boolean} | null>}
|
||||
*/
|
||||
export async function getVncInfo() {
|
||||
const ipcPath = process.env.SUPERVISOR_IPC;
|
||||
|
||||
if (!ipcPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const client = net.createConnection(ipcPath, () => {
|
||||
client.write('GET_VNC_INFO');
|
||||
});
|
||||
|
||||
let data = '';
|
||||
client.on('data', (chunk) => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
|
||||
client.on('end', () => {
|
||||
try {
|
||||
const info = JSON.parse(data.trim());
|
||||
resolve(info);
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', () => {
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
// 超时处理
|
||||
setTimeout(() => {
|
||||
client.destroy();
|
||||
resolve(null);
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
@@ -118,7 +118,8 @@ export function runPreflight() {
|
||||
logger.error('服务器', ` - ${err}`);
|
||||
}
|
||||
logger.error('服务器', '提示: 您可以使用 npm run init -- -custom 来自定义初始化步骤');
|
||||
process.exit(1);
|
||||
// 退出码 78 表示配置/依赖错误,看门狗不应自动重启
|
||||
process.exit(78);
|
||||
}
|
||||
|
||||
logger.info('服务器', '自检通过');
|
||||
|
||||
Reference in New Issue
Block a user