refactor: 代码解耦合,完善WebUI功能,添加网页VNC

This commit is contained in:
foxhui
2025-12-20 04:40:05 +08:00
Unverified
parent 41e3e022a3
commit c8c7aec0e1
74 changed files with 4984 additions and 644 deletions
-282
View File
@@ -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;
-295
View File
@@ -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);
}
-270
View File
@@ -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 };
}
+113
View File
@@ -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);
});
}
+2 -1
View File
@@ -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('服务器', '自检通过');