Collect chunk voxel data from the bot world and stream snapshot, chunk, and unwatch messages through the forward protocol.
627 lines
30 KiB
TypeScript
Executable File
627 lines
30 KiB
TypeScript
Executable File
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.notify('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;
|
|
}
|
|
|
|
}
|