chore: initial commit
This commit is contained in:
Executable
+626
@@ -0,0 +1,626 @@
|
||||
import { createBot } from "@wp2508/mineflayer";
|
||||
import type { Bot, BotEvents } from "@wp2508/mineflayer";
|
||||
import {bot_instance_config, bot_status} from "../../types/bot/instance/bot_runtime.ts";
|
||||
import {
|
||||
validate_host,
|
||||
validate_port,
|
||||
validate_proxy,
|
||||
validate_username,
|
||||
validate_version
|
||||
} from "../../utils/validate.ts";
|
||||
import {type bot_operation_code, type validate_code, validate_code_map} from "../../utils/validate_code_map.ts";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { Mutex } from "async-mutex";
|
||||
import {Logger} from "../../utils/logger.ts";
|
||||
import {get_project_root_path} from "../../utils/path_utils.ts";
|
||||
import * as path from 'path';
|
||||
import {log} from "../../types/utils/logger.ts";
|
||||
import {MinecraftJsonParser} from "../../utils/message_json_parser.ts";
|
||||
import {remember_command_tree} from '../../utils/command_completion.ts';
|
||||
import {create_info_manager, type InfoManager, type InfoUpdate} from '../info/init.ts';
|
||||
import {create_function_manager, type FunctionManager} from '../function/init.ts';
|
||||
import {PluginManager} from '../plugins/init.ts';
|
||||
import type {WorkflowManager, WorkflowRunEvent} from '../../types/bot/workflow/runtime.ts';
|
||||
|
||||
// 深度清理的最大递归深度与最大节点数,避免遍历无关的深层对象图
|
||||
const CLEANUP_MAX_DEPTH = 8;
|
||||
const CLEANUP_MAX_VISITED = 2000;
|
||||
// 共享 / 全局对象,禁止递归进入,避免误清理缓存或全局状态
|
||||
const CLEANUP_SKIP_KEYS = new Set(['registry', 'supportFeature', 'constructor']);
|
||||
// 需要显式清理的定时器与待发队列字段名
|
||||
const CLEANUP_TIMER_KEYS = ['closeTimer', '_keepAliveTimer', 'keepAliveTimer', 'doPhysicsTimer'];
|
||||
const CLEANUP_QUEUE_KEYS = ['_writeQueue', '_priorityWriteQueue', '_mcBundle'];
|
||||
// 全局对象,绝不能被遍历清理
|
||||
const CLEANUP_PROTECTED: unknown[] = [globalThis, process, console];
|
||||
// 'end' 事件未按时派发时的兜底清理延时(毫秒)
|
||||
const CLEANUP_FALLBACK_DELAY = 1000;
|
||||
|
||||
// 事件表:mineflayer 的全部事件 + 实例自有的 log 事件
|
||||
export interface bot_instance_event extends BotEvents {
|
||||
log: (log: log[]) => void;
|
||||
status_update: (status: bot_status) => void;
|
||||
instance_created: (instance: Bot) => void;
|
||||
info_update: (event: InfoUpdate) => void;
|
||||
connection_lost: () => void;
|
||||
connection_requested: () => void;
|
||||
reconnect_attempt: () => void;
|
||||
workflow_update: (event: WorkflowRunEvent) => void;
|
||||
}
|
||||
|
||||
export type bot_event_name = keyof bot_instance_event;
|
||||
// 单个事件的回调参数元组,例如 chat -> [username, message, translate, jsonMsg, matches]
|
||||
export type bot_event_args<E extends bot_event_name> = Parameters<bot_instance_event[E]>;
|
||||
export type bot_event_handler<E extends bot_event_name> = (...args: bot_event_args<E>) => void;
|
||||
// onAny / onceAny 在事件名之后接收对应事件的全部参数;
|
||||
// 事件名受联合类型约束,参数保持宽松以便统一处理任意事件
|
||||
export type bot_event_any_handler = (event: bot_event_name, ...args: any[]) => void;
|
||||
|
||||
export type bot_instance_context<T> = {
|
||||
readonly bot_instance: Bot | null,
|
||||
readonly info_manager: InfoManager | null,
|
||||
readonly function_manager: FunctionManager | null,
|
||||
workflow_manager: WorkflowManager | null,
|
||||
bot_config: bot_instance_config,
|
||||
readonly bot_status: bot_status,
|
||||
readonly plugin_manager: PluginManager,
|
||||
event_emitter: EventEmitter,
|
||||
logger: Logger | null,
|
||||
action_lock: boolean,
|
||||
instance_lock: Mutex,
|
||||
validate_config: (config: bot_instance_config) => validate_code,
|
||||
log: (log: log[]) => void,
|
||||
on: <E extends bot_event_name>(event: E, callback: bot_event_handler<E>) => void,
|
||||
onAny: (callback: bot_event_any_handler) => void,
|
||||
once: <E extends bot_event_name>(event: E, callback: bot_event_handler<E>) => void,
|
||||
onceAny: (callback: bot_event_any_handler) => void,
|
||||
off: <E extends bot_event_name>(event: E, callback: bot_event_handler<E>) => void,
|
||||
emit: <E extends bot_event_name>(event: E, ...args: bot_event_args<E>) => void,
|
||||
|
||||
connect: () => Promise<bot_operation_code>,
|
||||
create_instance: () => Promise<bot_operation_code>,
|
||||
disconnect: () => Promise<bot_operation_code>,
|
||||
schedule_reconnect: (delay: number) => Promise<void>,
|
||||
cancel_reconnect: () => void,
|
||||
inject: (callback: (context: bot_instance_context<T>) => void) => void,
|
||||
clear_this_context: () => Promise<void>
|
||||
}
|
||||
|
||||
export class BotContext implements bot_instance_context<any> {
|
||||
get is_disposed(): boolean {return this.disposal_requested;}
|
||||
public workflow_manager: WorkflowManager | null = null;
|
||||
private bot_instance_origin: Bot | null = null;
|
||||
public get bot_instance(): Bot | null {
|
||||
return this.bot_instance_origin;
|
||||
}
|
||||
private info_manager_origin: InfoManager | null = null;
|
||||
public get info_manager(): InfoManager | null {
|
||||
return this.info_manager_origin;
|
||||
}
|
||||
private info_unsubscribe: (() => void) | undefined;
|
||||
private function_manager_origin: FunctionManager | null = null;
|
||||
public get function_manager(): FunctionManager | null {
|
||||
return this.function_manager_origin;
|
||||
}
|
||||
private cleanup_task: Promise<void> | undefined;
|
||||
private dispose_task: Promise<void> | undefined;
|
||||
private disposal_requested = false;
|
||||
private operation_sequence = 0;
|
||||
private reconnect_timer: ReturnType<typeof setTimeout> | undefined;
|
||||
public readonly bot_config: bot_instance_config = {
|
||||
username: '',
|
||||
host: '',
|
||||
port: 0,
|
||||
version: '',
|
||||
user_id: 0,
|
||||
bot_id: 0,
|
||||
proxy: null
|
||||
};
|
||||
private bot_status_origin: bot_status = 'offline';
|
||||
public get bot_status(): bot_status {
|
||||
return this.bot_status_origin;
|
||||
}
|
||||
|
||||
private set_status(value: bot_status) {
|
||||
if (this.bot_status_origin === value) {
|
||||
return;
|
||||
}
|
||||
const transitions: Record<bot_status, readonly bot_status[]> = {
|
||||
offline: ['ready', 'reconnecting'],
|
||||
ready: ['connecting', 'disconnecting'],
|
||||
connecting: ['online', 'disconnecting'],
|
||||
online: ['disconnecting'],
|
||||
disconnecting: ['offline'],
|
||||
reconnecting: ['ready', 'offline'],
|
||||
};
|
||||
if (!transitions[this.bot_status_origin].includes(value)) {
|
||||
throw new Error(`无效的机器人状态转换: ${this.bot_status_origin} -> ${value}`);
|
||||
}
|
||||
this.bot_status_origin = value;
|
||||
this.notify('status_update', value);
|
||||
}
|
||||
|
||||
private notify<E extends bot_event_name>(event: E, ...args: bot_event_args<E>) {
|
||||
try {
|
||||
this.emit(event, ...args);
|
||||
} catch (error) {
|
||||
this.logger?.log([this.logger.error(`生命周期事件 ${String(event)} 处理失败: ${String(error)}`)]);
|
||||
}
|
||||
}
|
||||
public readonly plugin_manager: PluginManager;
|
||||
public event_emitter = new EventEmitter()
|
||||
// 记录每个原始回调对应的包装函数,off 时才能找回并移除同一个监听器
|
||||
private readonly event_wrappers = new Map<
|
||||
string,
|
||||
Map<(...args: any[]) => void, ((...args: any[]) => void)[]>
|
||||
>();
|
||||
public logger: Logger | null = null;
|
||||
public action_lock: boolean = false;
|
||||
// 串行化 create_instance / connect / disconnect,防止并发操作实例
|
||||
public readonly instance_lock : Mutex = new Mutex();
|
||||
|
||||
constructor(config: bot_instance_config) {
|
||||
this.bot_config = config;
|
||||
const log_path = path.join(get_project_root_path(), 'data/bot/', this.bot_config.bot_id.toString(), '/log');
|
||||
this.logger = new Logger(log_path);
|
||||
this.plugin_manager = new PluginManager(this);
|
||||
}
|
||||
|
||||
public log(log: log[]) {
|
||||
this.logger.log(log);
|
||||
this.emit('log', log);
|
||||
}
|
||||
|
||||
public create_instance(): Promise<bot_operation_code> {
|
||||
if (this.bot_status === 'reconnecting' || this.reconnect_timer !== undefined) this.cancel_reconnect();
|
||||
const sequence = this.operation_sequence;
|
||||
return this.instance_lock.runExclusive(async () => {
|
||||
await this.cleanup_task;
|
||||
if (this.disposal_requested) return validate_code_map.bot_context.disposed;
|
||||
if (sequence !== this.operation_sequence) return validate_code_map.bot_context.cancelled;
|
||||
if (this.bot_status === 'online') return validate_code_map.bot_context.instance_connected;
|
||||
if (this.bot_status === 'connecting') return validate_code_map.bot_context.instance_connecting;
|
||||
if (this.bot_instance) return validate_code_map.bot_context.ok;
|
||||
const result = await this.create_bot_instance(sequence);
|
||||
if (result !== validate_code_map.bot_context.ok) await this.cleanup_task;
|
||||
if (this.disposal_requested) return validate_code_map.bot_context.disposed;
|
||||
if (sequence !== this.operation_sequence) return validate_code_map.bot_context.cancelled;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private async create_bot_instance(sequence: number): Promise<bot_operation_code> {
|
||||
const result = this.validate_config(this.bot_config);
|
||||
if (result !== validate_code_map.username.ok) return result;
|
||||
let instance: Bot | undefined;
|
||||
let pending_manager: InfoManager | undefined;
|
||||
let pending_functions: FunctionManager | undefined;
|
||||
let initialized = false;
|
||||
const failure = (): bot_operation_code | undefined => {
|
||||
if (this.disposal_requested) return validate_code_map.bot_context.disposed;
|
||||
if (sequence !== this.operation_sequence) return validate_code_map.bot_context.cancelled;
|
||||
if (this.bot_instance !== instance) return validate_code_map.bot_context.error;
|
||||
};
|
||||
try {
|
||||
const {port, plugins, workflow, config_revision, ...options} = this.bot_config;
|
||||
instance = createBot({...options, ...(port != null ? {port} : {}), autoConnect: false, hideErrors: true});
|
||||
(instance as Bot & {server_address: string}).server_address = `${options.host.includes(':') ? `[${options.host}]` : options.host}:${port ?? 25565}`;
|
||||
this.bot_instance_origin = instance;
|
||||
instance._client.on('declare_commands', packet => remember_command_tree(instance, packet));
|
||||
const terminate = <E extends 'end' | 'kicked' | 'error'>(event: E, ...args: bot_event_args<E>) => {
|
||||
if (this.bot_instance !== instance || this.disposal_requested) return;
|
||||
const reason = args[0];
|
||||
if (event === 'error') {
|
||||
this.log([this.logger.error(`连接错误: ${String(reason)}`)]);
|
||||
} else if (event === 'kicked') {
|
||||
let kickReason: Parameters<typeof MinecraftJsonParser.toLog>[0] = args[0] as Parameters<typeof MinecraftJsonParser.toLog>[0];
|
||||
if (typeof kickReason === 'string') {
|
||||
try { kickReason = JSON.parse(kickReason); }
|
||||
catch { /* Plain-text disconnect reasons are valid too. */ }
|
||||
}
|
||||
const reasonLog = MinecraftJsonParser.toLog(kickReason);
|
||||
if (!reasonLog.length) {
|
||||
let fallback = '未知原因';
|
||||
try { fallback = JSON.stringify(kickReason) ?? fallback; }
|
||||
catch { /* Keep a readable fallback for malformed data. */ }
|
||||
reasonLog.push(this.logger.error(fallback));
|
||||
}
|
||||
this.log([
|
||||
this.logger.custom('[被踢出] ', '#cf8e8e', ['bold']),
|
||||
...reasonLog,
|
||||
]);
|
||||
} else {
|
||||
this.log([this.logger.warning(`连接已断开: ${String(reason ?? '未知原因')}`)]);
|
||||
}
|
||||
const sequence = this.operation_sequence;
|
||||
// Initialization failures are handled by the caller, including retry attempts.
|
||||
const report_connection_lost = initialized;
|
||||
const cleanup = this.clear_bot_instance(event === 'end');
|
||||
this.notify(event, ...args);
|
||||
void cleanup.then(() => {
|
||||
if (report_connection_lost && !this.disposal_requested && sequence === this.operation_sequence) {
|
||||
this.notify('connection_lost');
|
||||
}
|
||||
}).catch(error => {
|
||||
this.logger?.log([this.logger.error(`清理实例失败: ${String(error)}`)]);
|
||||
});
|
||||
};
|
||||
instance.on('end', reason => terminate('end', reason));
|
||||
instance.on('kicked', (reason, logged_in) => terminate('kicked', reason, logged_in));
|
||||
instance.on('error', (error: Error) => terminate('error', error));
|
||||
instance.on('initialLogin', () => {
|
||||
if (this.bot_instance !== instance || this.bot_status !== 'connecting' || this.disposal_requested) return;
|
||||
this.set_status('online');
|
||||
});
|
||||
instance.on("message", (jsonMsg, position) => {
|
||||
const channel = {
|
||||
chat: {label: '聊天', color: '#8fb0c9'},
|
||||
system: {label: '系统', color: '#d1b478'},
|
||||
game_info: {label: '提示', color: '#8fb98f'},
|
||||
}[position] ?? {label: String(position), color: '#8fb0c9'};
|
||||
this.log([
|
||||
this.logger.custom(`[${channel.label}] `, channel.color, ['bold']),
|
||||
...MinecraftJsonParser.parse(jsonMsg).log,
|
||||
]);
|
||||
})
|
||||
instance.onAny((event: string, ...args: any[]) => {
|
||||
if (this.bot_instance === instance && !this.disposal_requested) this.emit(event, ...args);
|
||||
});
|
||||
this.set_status('ready');
|
||||
const before_loading = failure();
|
||||
if (before_loading !== undefined) return before_loading;
|
||||
const manager = pending_manager = await create_info_manager(instance, {
|
||||
bot_id: this.bot_config.bot_id, plugin_source: this.plugin_manager,
|
||||
workflow_source: this.workflow_manager,
|
||||
});
|
||||
const after_loading = failure();
|
||||
if (after_loading !== undefined) return after_loading;
|
||||
this.info_manager_origin = manager;
|
||||
pending_manager = undefined;
|
||||
this.info_unsubscribe = manager.on_update(event => {
|
||||
if (this.bot_instance === instance && this.info_manager === manager
|
||||
&& !this.disposal_requested) {
|
||||
this.notify('info_update', event);
|
||||
}
|
||||
});
|
||||
pending_functions = await create_function_manager(this);
|
||||
const after_functions = failure();
|
||||
if (after_functions !== undefined) return after_functions;
|
||||
this.function_manager_origin = pending_functions;
|
||||
pending_functions = undefined;
|
||||
this.notify('instance_created', instance);
|
||||
const after_notification = failure();
|
||||
if (after_notification !== undefined) return after_notification;
|
||||
this.log([this.logger.custom('[NODE] 创建机器人实例成功', '#8fb98f', ['bold'])]);
|
||||
const after_log = failure();
|
||||
if (after_log !== undefined) return after_log;
|
||||
initialized = true;
|
||||
return validate_code_map.bot_context.ok;
|
||||
} catch (error) {
|
||||
this.logger?.log([this.logger.error(`创建实例失败: ${String(error)}`)]);
|
||||
if (this.disposal_requested) return validate_code_map.bot_context.disposed;
|
||||
if (sequence !== this.operation_sequence) return validate_code_map.bot_context.cancelled;
|
||||
return validate_code_map.bot_context.error;
|
||||
} finally {
|
||||
if (!initialized) {
|
||||
pending_functions?.dispose();
|
||||
try { pending_manager?.dispose(); }
|
||||
catch (error) { this.logger?.log([this.logger.error(`清理信息管理器失败: ${String(error)}`)]); }
|
||||
if (instance && this.bot_instance === instance) await this.clear_bot_instance();
|
||||
else await this.cleanup_task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public inject(callback: (context: bot_instance_context<any>) => void) {
|
||||
callback(this as bot_instance_context<any>);
|
||||
}
|
||||
|
||||
public connect(): Promise<bot_operation_code> {
|
||||
if (this.bot_status === 'reconnecting' || this.reconnect_timer !== undefined) this.cancel_reconnect();
|
||||
const sequence = this.operation_sequence;
|
||||
if (!this.disposal_requested) this.notify('connection_requested');
|
||||
return this.connect_instance(sequence);
|
||||
}
|
||||
|
||||
private connect_instance(sequence: number): Promise<bot_operation_code> {
|
||||
return this.instance_lock.runExclusive(async () => {
|
||||
await this.cleanup_task;
|
||||
if (this.disposal_requested) return validate_code_map.bot_context.disposed;
|
||||
if (sequence !== this.operation_sequence) return validate_code_map.bot_context.cancelled;
|
||||
if (this.bot_status === 'online') return validate_code_map.bot_context.instance_connected;
|
||||
if (this.bot_status === 'connecting') return validate_code_map.bot_context.instance_connecting;
|
||||
if (this.bot_status === 'reconnecting') this.set_status('offline');
|
||||
if (!this.bot_instance) {
|
||||
const result = await this.create_bot_instance(sequence);
|
||||
if (result !== validate_code_map.bot_context.ok) {
|
||||
await this.cleanup_task;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (this.disposal_requested) return validate_code_map.bot_context.disposed;
|
||||
if (sequence !== this.operation_sequence) return validate_code_map.bot_context.cancelled;
|
||||
const instance = this.bot_instance;
|
||||
if (!instance) return validate_code_map.bot_context.error;
|
||||
this.set_status('connecting');
|
||||
if (this.disposal_requested || sequence !== this.operation_sequence) {
|
||||
await this.clear_bot_instance();
|
||||
return validate_code_map.bot_context.cancelled;
|
||||
}
|
||||
try {
|
||||
instance.connect();
|
||||
if (this.bot_instance !== instance) {
|
||||
await this.cleanup_task;
|
||||
return validate_code_map.bot_context.error;
|
||||
}
|
||||
return validate_code_map.bot_context.ok;
|
||||
} catch (error) {
|
||||
this.logger?.log([this.logger.error(`连接实例失败: ${String(error)}`)]);
|
||||
await this.clear_bot_instance();
|
||||
return validate_code_map.bot_context.error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public disconnect(): Promise<bot_operation_code> {
|
||||
this.cancel_reconnect();
|
||||
return this.instance_lock.runExclusive(async () => {
|
||||
if (this.disposal_requested) return validate_code_map.bot_context.disposed;
|
||||
await this.clear_bot_instance();
|
||||
return validate_code_map.bot_context.ok;
|
||||
});
|
||||
}
|
||||
|
||||
public cancel_reconnect(): void {
|
||||
this.operation_sequence++;
|
||||
clearTimeout(this.reconnect_timer);
|
||||
this.reconnect_timer = undefined;
|
||||
if (this.bot_status === 'reconnecting') this.set_status('offline');
|
||||
}
|
||||
|
||||
public async schedule_reconnect(delay: number): Promise<void> {
|
||||
if (!Number.isInteger(delay) || delay < 0 || delay > 2_147_483_647) {
|
||||
throw new Error('重连延时必须是有效的毫秒整数');
|
||||
}
|
||||
const sequence = this.operation_sequence;
|
||||
await this.instance_lock.runExclusive(() => {
|
||||
if (this.disposal_requested || sequence !== this.operation_sequence || this.bot_status !== 'offline') return;
|
||||
this.reconnect_timer = setTimeout(() => {
|
||||
this.reconnect_timer = undefined;
|
||||
if (this.disposal_requested || sequence !== this.operation_sequence) return;
|
||||
this.notify('reconnect_attempt');
|
||||
void this.connect_instance(sequence).then(result => {
|
||||
if (result !== validate_code_map.bot_context.ok && !this.disposal_requested
|
||||
&& sequence === this.operation_sequence && this.bot_status === 'offline') {
|
||||
this.notify('connection_lost');
|
||||
}
|
||||
}).catch(error => {
|
||||
this.logger?.log([this.logger.error(`自动重连失败: ${String(error)}`)]);
|
||||
});
|
||||
}, delay);
|
||||
this.set_status('reconnecting');
|
||||
});
|
||||
}
|
||||
|
||||
public on<E extends bot_event_name>(event: E, callback: bot_event_handler<E>) {
|
||||
const wrapper: bot_event_handler<E> = (...args) => callback(...args);
|
||||
this.remember_wrapper(event, callback, wrapper);
|
||||
this.event_emitter.on(event, wrapper);
|
||||
}
|
||||
|
||||
public onAny(callback: bot_event_any_handler) {
|
||||
const wrapper = (event: string, ...args: any[]) => callback(event as bot_event_name, ...args);
|
||||
this.remember_wrapper('any', callback, wrapper);
|
||||
this.event_emitter.on('any', wrapper);
|
||||
}
|
||||
|
||||
public once<E extends bot_event_name>(event: E, callback: bot_event_handler<E>) {
|
||||
const wrapper: bot_event_handler<E> = (...args) => callback(...args);
|
||||
this.remember_wrapper(event, callback, wrapper);
|
||||
this.event_emitter.once(event, wrapper);
|
||||
}
|
||||
|
||||
public onceAny(callback: bot_event_any_handler) {
|
||||
const wrapper = (event: string, ...args: any[]) => callback(event as bot_event_name, ...args);
|
||||
this.remember_wrapper('any', callback, wrapper);
|
||||
this.event_emitter.once('any', wrapper);
|
||||
}
|
||||
|
||||
public off<E extends bot_event_name>(event: E, callback: bot_event_handler<E>) {
|
||||
const callbacks = this.event_wrappers.get(event);
|
||||
const wrappers = callbacks?.get(callback);
|
||||
if (!callbacks || !wrappers) return;
|
||||
const wrapper = wrappers.pop();
|
||||
if (!wrapper) return;
|
||||
this.event_emitter.off(event, wrapper);
|
||||
if (wrappers.length === 0) {
|
||||
callbacks.delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
// 记录原始回调 -> 包装函数栈,off 时才能找回并移除同一个监听器;
|
||||
// 同一回调可多次注册,off 从栈顶移除最近一次
|
||||
private remember_wrapper(
|
||||
event: string,
|
||||
callback: (...args: any[]) => void,
|
||||
wrapper: (...args: any[]) => void,
|
||||
) {
|
||||
let callbacks = this.event_wrappers.get(event);
|
||||
if (!callbacks) {
|
||||
callbacks = new Map();
|
||||
this.event_wrappers.set(event, callbacks);
|
||||
}
|
||||
let wrappers = callbacks.get(callback);
|
||||
if (!wrappers) {
|
||||
wrappers = [];
|
||||
callbacks.set(callback, wrappers);
|
||||
}
|
||||
wrappers.push(wrapper);
|
||||
}
|
||||
|
||||
public emit<E extends bot_event_name>(event: E, ...args: bot_event_args<E>): void;
|
||||
public emit(event: string, ...args: any[]): void;
|
||||
public emit(event: string, ...args: any[]) {
|
||||
// 'error' 无监听器时 EventEmitter 会抛出;转发前先兜底,避免 bot 的错误
|
||||
// 变成未捕获异常,同时保证 'any' / onAny 仍能观察到该事件
|
||||
if (event !== 'error' || this.event_emitter.listenerCount('error') > 0) {
|
||||
this.event_emitter.emit(event, ...args);
|
||||
}
|
||||
this.event_emitter.emit('any', event, ...args);
|
||||
}
|
||||
|
||||
private clear_bot_instance(ended = false): Promise<void> {
|
||||
if (this.cleanup_task) return this.cleanup_task;
|
||||
const instance = this.bot_instance;
|
||||
if (!instance) return Promise.resolve();
|
||||
this.bot_instance_origin = null;
|
||||
const manager = this.info_manager_origin;
|
||||
const functions = this.function_manager_origin;
|
||||
this.function_manager_origin = null;
|
||||
const unsubscribe = this.info_unsubscribe;
|
||||
this.info_manager_origin = null;
|
||||
this.info_unsubscribe = undefined;
|
||||
let cleaned = false;
|
||||
let finish!: () => void;
|
||||
const task = new Promise<void>(resolve => { finish = resolve; });
|
||||
this.cleanup_task = task.then(() => {
|
||||
this.cleanup_task = undefined;
|
||||
if (!this.disposal_requested) this.set_status('offline');
|
||||
});
|
||||
const cleanup_task = this.cleanup_task;
|
||||
const run_cleanup = () => {
|
||||
if (cleaned) return;
|
||||
cleaned = true;
|
||||
clearTimeout(fallback_timer);
|
||||
try {
|
||||
this.deep_cleanup(instance, new Set(), 0);
|
||||
this.log([this.logger.custom('[NODE] 机器人实例已关闭', '#cf8e8e', ['bold'])]);
|
||||
} catch (error) {
|
||||
this.logger?.log([this.logger.error(`清理实例资源失败: ${String(error)}`)]);
|
||||
} finally {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
// 等 end 的所有监听器执行完毕后再摘除监听器,保留 mineflayer 的内部资源清理。
|
||||
const on_end = () => { queueMicrotask(run_cleanup); };
|
||||
const fallback_timer = setTimeout(run_cleanup, CLEANUP_FALLBACK_DELAY);
|
||||
for (const cleanup of [unsubscribe, manager ? () => manager.dispose() : undefined, functions ? () => functions.dispose() : undefined]) {
|
||||
try { cleanup?.(); }
|
||||
catch (error) { this.logger?.log([this.logger.error(`清理信息管理器失败: ${String(error)}`)]); }
|
||||
}
|
||||
this.set_status('disconnecting');
|
||||
if (ended) {
|
||||
on_end();
|
||||
} else {
|
||||
instance.once('end', on_end);
|
||||
try {
|
||||
instance.end(validate_code_map.bot_context.instance_end_by_user.toString());
|
||||
} catch {
|
||||
on_end();
|
||||
}
|
||||
}
|
||||
return cleanup_task;
|
||||
}
|
||||
|
||||
// 深度遍历对象图,释放监听器 / 定时器 / 流句柄等残留状态;
|
||||
// seen 去重,depth 限制递归深度,visited 限制遍历规模,避免开销失控
|
||||
private deep_cleanup(target: unknown, seen: Set<object>, depth: number, visited = { count: 0 }) {
|
||||
if (target === null || typeof target !== 'object') return;
|
||||
if (depth > CLEANUP_MAX_DEPTH || visited.count > CLEANUP_MAX_VISITED) return;
|
||||
const object = target as Record<string, any>;
|
||||
if (CLEANUP_PROTECTED.includes(object)) return;
|
||||
if (seen.has(object)) return;
|
||||
seen.add(object);
|
||||
visited.count++;
|
||||
|
||||
this.release_own_state(object);
|
||||
|
||||
for (const key of Object.keys(object)) {
|
||||
if (CLEANUP_SKIP_KEYS.has(key)) continue;
|
||||
let value: unknown;
|
||||
try {
|
||||
value = object[key];
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (value === null || typeof value !== 'object') continue;
|
||||
this.deep_cleanup(value, seen, depth + 1, visited);
|
||||
}
|
||||
}
|
||||
|
||||
// 释放单个对象自身持有的监听器 / 定时器 / 流句柄 / 待发队列
|
||||
private release_own_state(object: Record<string, any>) {
|
||||
for (const key of CLEANUP_TIMER_KEYS) {
|
||||
const timer = object[key];
|
||||
if (timer === null || timer === undefined) continue;
|
||||
clearInterval(timer as NodeJS.Timeout);
|
||||
clearTimeout(timer as NodeJS.Timeout);
|
||||
try {
|
||||
object[key] = null;
|
||||
} catch {
|
||||
// 只读 / 冻结对象无法赋值,忽略
|
||||
}
|
||||
}
|
||||
for (const key of CLEANUP_QUEUE_KEYS) {
|
||||
if (Array.isArray(object[key])) {
|
||||
(object[key] as unknown[]).length = 0;
|
||||
}
|
||||
}
|
||||
// 仅销毁流 / socket 类句柄,避免误调用无关对象的 destroy
|
||||
if (typeof object.destroy === 'function' && object.destroyed !== true
|
||||
&& (typeof object.pipe === 'function' || typeof object.write === 'function')) {
|
||||
try {
|
||||
object.destroy();
|
||||
} catch {
|
||||
// 忽略已关闭句柄在销毁时的异常
|
||||
}
|
||||
}
|
||||
if (typeof object.removeAllListeners === 'function') {
|
||||
object.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
public validate_config(config: bot_instance_config): validate_code {
|
||||
const username_validate = validate_username(config.username);
|
||||
if (username_validate !== validate_code_map.username.ok) return username_validate;
|
||||
const host_validate = validate_host(config.host);
|
||||
if (host_validate !== validate_code_map.host.ok) return host_validate;
|
||||
if (config.port != null) {
|
||||
const port_validate = validate_port(config.port);
|
||||
if (port_validate !== validate_code_map.port.ok) return port_validate;
|
||||
}
|
||||
const version_validate = validate_version(config.version);
|
||||
if (version_validate !== validate_code_map.version.ok) return version_validate;
|
||||
if (config.proxy != null) {
|
||||
const proxy_validate = validate_proxy(config.proxy);
|
||||
if (proxy_validate !== validate_code_map.proxy.ok) return proxy_validate;
|
||||
}
|
||||
return validate_code_map.username.ok;
|
||||
}
|
||||
|
||||
public clear_this_context(): Promise<void> {
|
||||
if (this.dispose_task) return this.dispose_task;
|
||||
this.disposal_requested = true;
|
||||
this.cancel_reconnect();
|
||||
const plugins_disposed = this.plugin_manager.dispose().catch(error => {
|
||||
this.logger?.log([this.logger.error(`插件卸载失败: ${String(error)}`)]);
|
||||
});
|
||||
const workflow_disposed = this.workflow_manager?.dispose().catch(error => {
|
||||
this.logger?.log([this.logger.error(`Workflow cleanup failed: ${String(error)}`)]);
|
||||
});
|
||||
this.dispose_task = Promise.all([plugins_disposed, workflow_disposed]).then(() => this.instance_lock.runExclusive(async () => {
|
||||
await this.clear_bot_instance();
|
||||
this.action_lock = false;
|
||||
if (this.bot_status === 'offline') {
|
||||
this.notify('status_update', 'offline');
|
||||
} else {
|
||||
this.set_status('offline');
|
||||
}
|
||||
this.event_emitter.removeAllListeners();
|
||||
this.event_wrappers.clear();
|
||||
this.workflow_manager = null;
|
||||
await this.logger?.close();
|
||||
}));
|
||||
return this.dispose_task;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import {bot_instance_context, BotContext} from "./bot_instance_init";
|
||||
import {bot_config, bot_status} from "../../types/bot/instance/bot_runtime";
|
||||
import {validate_code_map} from "../../utils/validate_code_map";
|
||||
import {init_workflow} from "../workflow/init";
|
||||
import {update_one_bot_status} from "../../service/server/update/bot_status.ts";
|
||||
import type {Socket} from "socket.io-client";
|
||||
|
||||
|
||||
const bot_context_map: Map<number, bot_instance_context<any>> = new Map();
|
||||
const bot_action_lock_map = new Map<number, boolean>();
|
||||
type ContextObserver = (context: bot_instance_context<any> | undefined) => void;
|
||||
const bot_context_observers = new Map<number, Set<ContextObserver>>();
|
||||
|
||||
export function get_bot_context(bot_id: number): bot_instance_context<any> | undefined {
|
||||
return bot_context_map.get(bot_id);
|
||||
}
|
||||
|
||||
function notify_context_observer(callback: ContextObserver, context: bot_instance_context<any> | undefined) {
|
||||
try {
|
||||
callback(context);
|
||||
} catch {
|
||||
console.error('Bot context observer failed');
|
||||
}
|
||||
}
|
||||
|
||||
function notify_bot_context(bot_id: number) {
|
||||
for (const callback of Array.from(bot_context_observers.get(bot_id) ?? [])) {
|
||||
notify_context_observer(callback, bot_context_map.get(bot_id));
|
||||
}
|
||||
}
|
||||
|
||||
export function observe_bot_context(bot_id: number, callback: ContextObserver): () => void {
|
||||
let observers = bot_context_observers.get(bot_id);
|
||||
if (!observers) {
|
||||
observers = new Set();
|
||||
bot_context_observers.set(bot_id, observers);
|
||||
}
|
||||
observers.add(callback);
|
||||
notify_context_observer(callback, bot_context_map.get(bot_id));
|
||||
return () => {
|
||||
observers.delete(callback);
|
||||
if (observers.size === 0 && bot_context_observers.get(bot_id) === observers) {
|
||||
bot_context_observers.delete(bot_id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function remove_bot_context(bot_id: number, context: bot_instance_context<any>) {
|
||||
if (bot_context_map.get(bot_id) === context) {
|
||||
bot_context_map.delete(bot_id);
|
||||
notify_bot_context(bot_id);
|
||||
}
|
||||
}
|
||||
|
||||
export async function start_bot_context(main_server_socket: Socket, bot_config: bot_config, node_id: number) {
|
||||
if (!Number.isSafeInteger(node_id) || node_id <= 0
|
||||
|| !bot_config || typeof bot_config !== "object" || Array.isArray(bot_config)
|
||||
|| !bot_config.instance_config || typeof bot_config.instance_config !== "object"
|
||||
|| Array.isArray(bot_config.instance_config)
|
||||
|| !Number.isSafeInteger(bot_config.instance_config.bot_id) || bot_config.instance_config.bot_id <= 0
|
||||
|| !Number.isSafeInteger(bot_config.instance_config.user_id) || bot_config.instance_config.user_id <= 0
|
||||
|| !Array.isArray(bot_config.plugins) || !Array.isArray(bot_config.workflow)
|
||||
|| !bot_config.plugins.every(item => item && typeof item === "object" && !Array.isArray(item))
|
||||
|| !bot_config.workflow.every(item => item && typeof item === "object" && !Array.isArray(item))) {
|
||||
return validate_code_map.web.invalid_parameter;
|
||||
}
|
||||
const bot_id = bot_config.instance_config.bot_id;
|
||||
if (bot_action_lock_map.get(bot_id) ?? false) {
|
||||
return validate_code_map.bot_context.processing;
|
||||
}
|
||||
bot_action_lock_map.set(bot_id, true);
|
||||
let new_context: BotContext | undefined;
|
||||
let context_started = false;
|
||||
try {
|
||||
const temp_context = bot_context_map.get(bot_id);
|
||||
if (temp_context !== undefined) {
|
||||
if (temp_context.bot_status === "online" || temp_context.bot_status === "connecting"
|
||||
|| temp_context.bot_status === "reconnecting" || temp_context.bot_status === "ready") {
|
||||
const result = await temp_context.connect();
|
||||
return result === validate_code_map.bot_context.ok ? validate_code_map.bot_context.context_created : result;
|
||||
} else if (temp_context.bot_status === "disconnecting") {
|
||||
return validate_code_map.bot_context.processing;
|
||||
} else {
|
||||
await temp_context.clear_this_context();
|
||||
remove_bot_context(bot_id, temp_context);
|
||||
}
|
||||
}
|
||||
|
||||
const context = new_context = new BotContext(bot_config.instance_config);
|
||||
context.bot_config.plugins = structuredClone(bot_config.plugins);
|
||||
context.bot_config.workflow = structuredClone(bot_config.workflow);
|
||||
context.on("status_update", (status) => {
|
||||
void update_one_bot_status(main_server_socket, node_id, bot_id, bot_config.instance_config.user_id, status)
|
||||
.catch(error => console.error(`Bot ${bot_id} status update failed`, error));
|
||||
});
|
||||
const origin_clean = context.clear_this_context.bind(context);
|
||||
context.clear_this_context = async () => {
|
||||
try {
|
||||
await origin_clean();
|
||||
} finally {
|
||||
remove_bot_context(bot_id, context);
|
||||
}
|
||||
};
|
||||
const validation_result = context.validate_config(bot_config.instance_config);
|
||||
if (validation_result !== validate_code_map.bot_context.ok) {
|
||||
console.error(`Bot ${bot_id} configuration validation failed, code: ${validation_result}`);
|
||||
return validation_result;
|
||||
}
|
||||
await context.plugin_manager.init(bot_config.plugins);
|
||||
await init_workflow(context, bot_config.workflow);
|
||||
bot_context_map.set(bot_id, context);
|
||||
notify_bot_context(bot_id);
|
||||
const result = await context.connect();
|
||||
if (result !== validate_code_map.bot_context.ok
|
||||
&& result !== validate_code_map.bot_context.instance_connected
|
||||
&& result !== validate_code_map.bot_context.instance_connecting) {
|
||||
console.error(`Bot ${bot_id} startup failed, code: ${result}`);
|
||||
return result;
|
||||
}
|
||||
context_started = true;
|
||||
return result === validate_code_map.bot_context.ok ? validate_code_map.bot_context.context_created : result;
|
||||
} catch (error) {
|
||||
console.error(`Bot ${bot_id} startup failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return validate_code_map.bot_context.error;
|
||||
} finally {
|
||||
if (new_context && !context_started) {
|
||||
try {
|
||||
await new_context.clear_this_context();
|
||||
} catch (error) {
|
||||
console.error(`Bot ${bot_id} cleanup failed`, error);
|
||||
}
|
||||
}
|
||||
bot_action_lock_map.delete(bot_id);
|
||||
}
|
||||
}
|
||||
|
||||
export async function get_bot_context_status() : Promise<{
|
||||
bot_id: number;
|
||||
status: bot_status;
|
||||
}[]> {
|
||||
return Array.from(bot_context_map).map(([bot_id, context]) => {
|
||||
return {
|
||||
bot_id,
|
||||
status: context.bot_status,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function get_bot_context_status_by_ids(bot_id: number[]) : Promise<{
|
||||
bot_id: number;
|
||||
status: bot_status;
|
||||
}[]> {
|
||||
return bot_id.map((id) => {
|
||||
return {
|
||||
bot_id: id,
|
||||
status: bot_context_map.get(id)?.bot_status ?? "offline",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function stop_bot_context(bot_id: number) {
|
||||
if (bot_action_lock_map.get(bot_id) ?? false) {
|
||||
return validate_code_map.bot_context.processing;
|
||||
}
|
||||
bot_action_lock_map.set(bot_id, true);
|
||||
try {
|
||||
if (!bot_context_map.has(bot_id)) {
|
||||
return validate_code_map.bot_context.instance_end_by_user;
|
||||
}
|
||||
const context = bot_context_map.get(bot_id);
|
||||
if (context !== undefined) {
|
||||
await context.clear_this_context();
|
||||
}
|
||||
if (context) remove_bot_context(bot_id, context);
|
||||
return validate_code_map.bot_context.instance_end_by_user;
|
||||
} catch (error) {
|
||||
return validate_code_map.bot_context.error;
|
||||
} finally {
|
||||
bot_action_lock_map.delete(bot_id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user