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 = Parameters; export type bot_event_handler = (...args: bot_event_args) => void; // onAny / onceAny 在事件名之后接收对应事件的全部参数; // 事件名受联合类型约束,参数保持宽松以便统一处理任意事件 export type bot_event_any_handler = (event: bot_event_name, ...args: any[]) => void; export type bot_instance_context = { 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: (event: E, callback: bot_event_handler) => void, onAny: (callback: bot_event_any_handler) => void, once: (event: E, callback: bot_event_handler) => void, onceAny: (callback: bot_event_any_handler) => void, off: (event: E, callback: bot_event_handler) => void, emit: (event: E, ...args: bot_event_args) => void, connect: () => Promise, create_instance: () => Promise, disconnect: () => Promise, schedule_reconnect: (delay: number) => Promise, cancel_reconnect: () => void, inject: (callback: (context: bot_instance_context) => void) => void, clear_this_context: () => Promise } export class BotContext implements bot_instance_context { 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 | undefined; private dispose_task: Promise | undefined; private disposal_requested = false; private operation_sequence = 0; private reconnect_timer: ReturnType | 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 = { 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(event: E, ...args: bot_event_args) { 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.notify('log', log); } public create_instance(): Promise { 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 { 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 = (event: E, ...args: bot_event_args) => { 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[0] = args[0] as Parameters[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) => void) { callback(this as bot_instance_context); } public connect(): Promise { 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 { 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 { 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 { 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(event: E, callback: bot_event_handler) { const wrapper: bot_event_handler = (...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(event: E, callback: bot_event_handler) { const wrapper: bot_event_handler = (...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(event: E, callback: bot_event_handler) { 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(event: E, ...args: bot_event_args): 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 { 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(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, 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; 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) { 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 { 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; } }