Files
2026-10-22 17:07:08 +08:00

352 lines
16 KiB
TypeScript

import config from "./config.json" with {type: "json"};
import { io } from "socket.io-client";
import {console_print} from "./utils/console_print.ts";
import {event_handler, init_event_handler_list} from "./event_handler/init.ts";
import {update_plugins} from "./service/server/update/plugins.ts";
import {update_version_list} from "./service/server/update/version_list.ts";
import {update_function_list} from './service/server/update/function_list.ts';
import {update_workflow_catalog} from './service/server/update/workflow_catalog.ts';
import {start_forward_server, type ControlTransport, type ForwardServerHandle} from "./forward_server/index.ts";
import {update_all_bot_status} from "./service/server/update/bot_status.ts";
import {bind_node_session, clear_node_session} from "./bot/forward/manager.ts";
import {Buffer} from 'node:buffer';
import {get_bot_context_status, stop_bot_context} from './bot/instance/index.ts';
async function main() {
await init_event_handler_list();
const {main_server, node} = config;
if (!main_server || typeof main_server.host !== 'string' || !main_server.host.trim()
|| !Number.isInteger(main_server.port) || main_server.port < 1 || main_server.port > 65535
|| typeof main_server.use_https !== 'boolean'
|| !node || !Number.isSafeInteger(node.id) || node.id < 1
|| typeof node.key !== 'string' || !node.key.trim()) {
console_print.log(console_print.error('配置文件错误,请检查 main_server 和 node 配置'));
throw new Error('Invalid node configuration');
}
const reconnect_interval = retry_interval();
const protocol = main_server.use_https ? 'https' : 'http';
const main_server_url = `${protocol}://${main_server.host}:${main_server.port}`;
const main_server_socket = io(main_server_url, {
path: '/api/node',
autoConnect: false,
timeout: 10_000,
reconnection: false,
});
let reconnect_timer: ReturnType<typeof setTimeout> | undefined;
let reconnect_attempts = 0;
let connection_sequence = 0;
let stopped = false;
let node_session_id: string | undefined;
let relay_handle: ForwardServerHandle | undefined;
let relay_starting: Promise<ForwardServerHandle> | undefined;
let release_control: (() => void) | undefined;
let shutdown: Promise<void> | undefined;
let signal_exit_requested = false;
function clear_session() {
node_session_id = undefined;
clear_node_session(main_server_socket);
release_control?.();
release_control = undefined;
}
function authenticated(sequence: number) {
return !stopped && sequence === connection_sequence
&& main_server_socket.connected && node_session_id !== undefined;
}
function control_transport(sequence: number): ControlTransport {
const listeners = new Map<(...args: any[]) => void, (...args: any[]) => void>();
return {
on(event, listener) {
const guarded = (...args: any[]) => {
// Hosted relay commands may target any Node, not just this host.
const disconnected = event === 'disconnect' && !stopped
&& !main_server_socket.connected && connection_sequence === sequence + 1;
if (authenticated(sequence) || disconnected) listener(...args);
};
listeners.set(listener, guarded);
main_server_socket.on(event, guarded);
},
off(event, listener) {
const guarded = listeners.get(listener);
if (guarded) main_server_socket.off(event, guarded);
listeners.delete(listener);
},
emit(event, ...args) {
if (authenticated(sequence)) main_server_socket.emit(event, ...args);
},
};
}
function stop_connection() {
if (!stopped) {
stopped = true;
connection_sequence++;
clearTimeout(reconnect_timer);
reconnect_timer = undefined;
clear_session();
main_server_socket.disconnect();
process.exitCode = 0;
shutdown = (async () => {
let deadline: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
Promise.allSettled([
(async () => {
// A failed startup has no handle to stop; it is already reported by initialization.
const handle = relay_handle ?? await relay_starting?.catch((): undefined => undefined);
await handle?.stop();
})(),
(async () => {
const contexts = await get_bot_context_status();
const results = await Promise.allSettled(contexts.map(({bot_id}) => stop_bot_context(bot_id)));
if (results.some(result => result.status === 'rejected')) throw new Error('Bot cleanup failed');
})(),
]).then(results => {
if (results.some(result => result.status === 'rejected')) throw new Error('Shutdown failed');
}),
new Promise<never>((_resolve, reject) => {
deadline = setTimeout(() => reject(new Error('Shutdown timed out')), 10_000);
}),
]);
} catch {
console_print.log(console_print.error('节点关闭失败或超时'));
process.exitCode = 1;
} finally {
clearTimeout(deadline);
}
})();
}
if (!signal_exit_requested) {
signal_exit_requested = true;
// Signal listeners override Node's default exit; do not leave bot timers alive.
void shutdown!.then(() => process.exit(process.exitCode ?? 0));
}
}
process.once('SIGINT', stop_connection);
process.once('SIGTERM', stop_connection);
function schedule_reconnect() {
if (stopped || reconnect_timer !== undefined) {
return;
}
console_print.log(console_print.info(`将在 ${reconnect_interval} 毫秒后重连主服务器,持续重试直至成功`));
reconnect_timer = setTimeout(() => {
reconnect_timer = undefined;
reconnect_attempts++;
console_print.log(console_print.info(`正在尝试重连主服务器,第 ${reconnect_attempts} 次`));
try {
main_server_socket.connect();
} catch {
console_print.log(console_print.error('连接主服务器失败,将继续重试'));
schedule_reconnect();
}
}, reconnect_interval);
}
function retry_connection() {
if (stopped) return;
connection_sequence++;
clear_session();
main_server_socket.disconnect();
schedule_reconnect();
}
main_server_socket.on('connect', async () => {
if (stopped) return;
const sequence = ++connection_sequence;
clearTimeout(reconnect_timer);
reconnect_timer = undefined;
clear_session();
console_print.log(console_print.info('已连接主服务器,正在注册节点'));
try {
const response = await main_server_socket
.timeout(5000)
.emitWithAck('register_node', {
id: node.id,
key: node.key,
});
// 旧连接的 ACK 不得影响重连后的注册状态。
if (stopped || sequence !== connection_sequence || !main_server_socket.connected) {
return;
}
if (!response || (response.status !== 'success' && response.status !== 'error')) {
throw new Error('节点注册响应格式无效');
}
if (response.status === 'error') {
console_print.log(console_print.error('节点注册被主服务器拒绝'));
retry_connection();
return;
}
const {data} = response;
if (Object.keys(response).length !== 2 || !data || Array.isArray(data)
|| Object.keys(data).length !== 2 || typeof data.name !== 'string'
|| typeof data.node_session_id !== 'string' || !data.node_session_id.trim()
|| Buffer.byteLength(data.node_session_id, 'utf8') > 256) {
throw new Error('节点注册响应格式无效');
}
node_session_id = data.node_session_id;
bind_node_session(main_server_socket, node.id, node_session_id);
console_print.log(console_print.success('节点注册成功'));
} catch {
if (stopped || sequence !== connection_sequence) {
return;
}
console_print.log(console_print.error('节点注册失败,等待重新连接'));
retry_connection();
return;
}
let initialization_stage = '插件列表上报';
try {
// @ts-ignore
await update_plugins(main_server_socket, node.id, () => authenticated(sequence));
if (!authenticated(sequence)) return;
initialization_stage = '函数列表上报';
await update_function_list(main_server_socket, node.id, () => authenticated(sequence));
if (!authenticated(sequence)) return;
initialization_stage = '工作流目录上报';
await update_workflow_catalog(main_server_socket, node.id, () => authenticated(sequence));
if (!authenticated(sequence)) return;
initialization_stage = '版本列表上报';
// @ts-ignore
await update_version_list(main_server_socket, node.id);
if (!authenticated(sequence)) return;
initialization_stage = '机器人状态上报';
// @ts-ignore
await update_all_bot_status(main_server_socket, node.id);
if (!authenticated(sequence)) return;
if (config.forward_server.status) {
initialization_stage = '转发服务器启动';
const control = control_transport(sequence);
if (!relay_starting) {
relay_starting = start_forward_server({
forward_id: config.forward_server.id,
key: config.forward_server.key,
allowed_origins: config.forward_server.allowed_origins,
control,
path: config.forward_server.path,
namespace: config.forward_server.namespace,
port: config.forward_server.port,
logger: console_print,
}).then(handle => {
relay_handle = handle;
return handle;
}).catch(error => {
relay_starting = undefined;
throw error;
});
}
const handle = await relay_starting;
if (!authenticated(sequence)) return;
release_control = handle.bind_control(control);
console_print.log(console_print.info('转发服务器已就绪'));
try {
const response = await main_server_socket
.timeout(5000)
.emitWithAck('register_forward', {
id: config.forward_server.id,
key: config.forward_server.key,
node_id: node.id,
});
if (!authenticated(sequence)) {
return;
}
if (!response || (response.status !== 'success' && response.status !== 'error')) {
throw new Error('转发服务器注册响应格式无效');
}
if (response.status === 'error') {
const code = Number.isSafeInteger(response.data) ? String(response.data) : '未知';
console_print.log(console_print.error(`转发服务器注册被主服务器拒绝,错误码: ${code}`));
retry_connection();
return;
}
console_print.log(console_print.success('转发服务器注册成功'));
} catch {
if (!authenticated(sequence)) {
return;
}
console_print.log(console_print.error('转发服务器注册请求异常'));
retry_connection();
return;
}
}
reconnect_attempts = 0;
} catch (error) {
if (!authenticated(sequence)) return;
let reason = '操作失败,错误详情已隐藏';
if (initialization_stage === '转发服务器启动') {
const messages: Record<string, string> = {
'A relay secret of at least 16 characters is required': 'forward_server.key 至少需要 16 个字符,且必须与 Main 登记值一致',
'forward_id must be a nonnegative safe integer': 'forward_server.id 必须是非负安全整数',
'allowed_origins must contain explicit HTTP(S) origins': 'forward_server.allowed_origins 必须是明确的 HTTP(S) Origin',
'path must be an absolute Socket.IO path without a trailing slash': 'forward_server.path 格式无效',
'namespace must be an absolute Socket.IO namespace': 'forward_server.namespace 格式无效',
'Invalid relay port': 'forward_server.port 格式无效',
};
if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string'
&& Object.hasOwn(messages, error.message)) reason = messages[error.message];
if (error && typeof error === 'object' && 'code' in error) {
if (error.code === 'EADDRINUSE') reason = '转发服务器监听端口已被占用';
if (error.code === 'EACCES') reason = '没有权限监听转发服务器端口';
}
}
console_print.log(console_print.error(`节点注册后初始化失败,阶段: ${initialization_stage},原因: ${reason}`));
retry_connection();
}
});
main_server_socket.on('connect_error', () => {
console_print.log(console_print.error('连接主服务器失败'));
schedule_reconnect();
});
main_server_socket.on('error', () => {
console_print.log(console_print.error('主服务器返回错误'));
retry_connection();
});
main_server_socket.on('disconnect', () => {
connection_sequence++;
clear_session();
console_print.log(console_print.warning('与主服务器断开连接'));
if (stopped) {
return;
}
schedule_reconnect();
});
main_server_socket.onAny((event: string, ...args: any[]) => {
if (!authenticated(connection_sequence)) return;
void event_handler(main_server_socket, event, ...args).catch(() => {
// event_handler 已记录错误,避免重复打印和未处理的 Promise 异常。
});
});
console_print.log(console_print.info('正在连接主服务器'));
try {
main_server_socket.connect();
} catch {
console_print.log(console_print.error('连接主服务器失败,将继续重试'));
schedule_reconnect();
}
}
function retry_interval(): number {
const interval = config.main_server?.auto_reconnect?.interval;
return Number.isInteger(interval) && interval >= 1 && interval <= 2_147_483_647 ? interval : 10_000;
}
function bootstrap() {
void main().catch(() => {
console_print.log(console_print.error('节点启动失败,将继续重试'));
setTimeout(bootstrap, retry_interval());
});
}
bootstrap();