import {readdir} from 'node:fs/promises'; import type {Bot} from '@wp2508/mineflayer'; import type {BaseFunction, FunctionArgument, FunctionI18n} from '../../types/bot/function/function.ts'; import type {data_type} from '../../types/bot/instance/bot_runtime.ts'; import type {bot_instance_context} from '../instance/bot_instance_init.ts'; import {create_function_cancellation, on_function_cancel} from './cancellation.ts'; import {measure_sync} from '../info/resource_metrics.ts'; export interface FunctionTemplate { name: string; i18n: FunctionI18n; args: FunctionArgument[]; } export interface FunctionCategory { path: string; name: string; i18n: FunctionI18n[]; function_list: FunctionTemplate[]; } export type FunctionList = FunctionCategory[]; export interface FunctionManager { get_function_list(): FunctionList; run(path: string, name: string, args: unknown[], before_run?: () => void, options?: {signal?: AbortSignal}): Promise; /** Cancels queued calls and notifies the running call, which drains before rejecting. */ dispose(): void; } const data_types = new Set([ 'string', 'boolean', 'number', 'datetime', 'string-select', 'number-select', 'string-multi-select', 'number-multi-select', 'string-array', 'number-array', ]); const valid_name = (value: unknown): value is string => typeof value === 'string' && /^[a-zA-Z0-9_-]+$/.test(value); const valid_i18n = (value: unknown): value is FunctionI18n => { if (!value || typeof value !== 'object') return false; const item = value as FunctionI18n; return typeof item.lang === 'string' && item.lang.length > 0 && typeof item.name === 'string' && typeof item.description === 'string'; }; async function load_functions(check_active: () => void = () => {}): Promise<{ list: FunctionList; functions: Map; }> { const list: FunctionList = []; const functions = new Map(); const directories = ['']; const stamp = `${Date.now()}-${process.hrtime.bigint()}`; const import_module = async (url: URL): Promise> => { check_active(); url.searchParams.set('t', stamp); const module = await import(url.href); check_active(); return module; }; for (const path of directories) { check_active(); const directory = new URL(path ? `./${path}/` : './', import.meta.url); const entries = await readdir(directory, {withFileTypes: true}); check_active(); entries.sort((a, b) => a.name.localeCompare(b.name)); for (const entry of entries) { if (entry.isDirectory() && valid_name(entry.name)) directories.push(path ? `${path}/${entry.name}` : entry.name); } if (!path) continue; const files = entries.filter(entry => entry.isFile() && entry.name !== 'define.ts' && /^[a-zA-Z0-9_-]+\.ts$/.test(entry.name)); const has_define = entries.some(entry => entry.isFile() && entry.name === 'define.ts'); if (!files.length && !has_define) continue; if (!has_define) throw new Error(`函数分类 ${path} 缺少 define.ts`); try { const {define} = await import_module(new URL('define.ts', directory)); const config = define as {path?: unknown; name?: unknown; i18n?: unknown} | undefined; if (config?.path !== path || !Array.isArray(config.i18n) || !config.i18n.length || !config.i18n.every(valid_i18n) || config.name !== undefined && !valid_name(config.name)) { throw new Error('分类 define 无效,path 必须与目录路径一致'); } const category: FunctionCategory = { path, name: typeof config.name === 'string' ? config.name : path.split('/').at(-1)!, i18n: config.i18n.map(item => ({...item})), function_list: [], }; for (const file of files) { const name = file.name.slice(0, -3); try { const module = await import_module(new URL(file.name, directory)); if (!Object.hasOwn(module, 'init')) continue; if (typeof module.init !== 'function') throw new Error('必须导出 init 类'); const instance = new (module.init as new () => BaseFunction)(); if (instance.name !== name || !valid_i18n(instance.i18n) || !Array.isArray(instance.args) || typeof instance.run !== 'function') { throw new Error('函数必须满足 BaseFunction 且 name 与文件名一致'); } const names = new Set(); let optional = false; for (const arg of instance.args) { if (!arg || !valid_name(arg.name) || names.has(arg.name) || !data_types.has(arg.type) || !valid_i18n(arg.i18n)) { throw new Error('函数参数定义无效或名称重复'); } if (Object.hasOwn(arg, 'default')) { if (arg.type !== 'boolean' || typeof arg.default !== 'boolean') throw new Error('默认值必须匹配布尔参数'); optional = true; } else if (optional) throw new Error('带默认值的参数必须位于末尾'); names.add(arg.name); } const key = `${path}/${name}`; if (functions.has(key)) throw new Error('函数重复注册'); functions.set(key, instance); category.function_list.push({name, i18n: {...instance.i18n}, args: instance.args.map(arg => ({name: arg.name, type: arg.type, i18n: {...arg.i18n}, ...(Object.hasOwn(arg, 'default') ? {default: arg.default} : {})}))}); } catch (error) { throw new Error(`函数 ${path}/${name} 加载失败`, {cause: error}); } } list.push(category); } catch (error) { throw new Error(`函数分类 ${path} 加载失败`, {cause: error}); } } list.sort((a, b) => a.path.localeCompare(b.path)); return {list, functions}; } /** Builds grouped metadata without a Bot; constructors must have no side effects. */ export async function get_function_list(): Promise { return (await load_functions()).list; } function valid_argument(value: unknown, type: data_type): boolean { switch (type) { case 'number': case 'number-select': return typeof value === 'number' && Number.isFinite(value); case 'string': case 'string-select': return typeof value === 'string'; case 'boolean': return typeof value === 'boolean'; case 'datetime': return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value) && Number.isFinite(Date.parse(value)); case 'string-array': case 'string-multi-select': return Array.isArray(value) && Array.from(value).every(item => typeof item === 'string'); case 'number-array': case 'number-multi-select': return Array.isArray(value) && Array.from(value).every(item => typeof item === 'number' && Number.isFinite(item)); } } /** The caller owns disposal; this manager never registers end listeners or follows a replacement Bot. */ export async function create_function_manager( context: Pick, 'bot_instance' | 'bot_status'> & Partial, 'workflow_manager'>>, ): Promise { const bot: Bot | null = context.bot_instance; if (!bot) throw new Error('Bot 实例尚未创建'); const check_instance = () => { if (context.bot_instance !== bot) throw new Error('Bot 实例已变化'); }; const {list, functions} = await load_functions(check_instance); let disposed = false; let tail = Promise.resolve(); const pending = new Set<{reject: (error: unknown) => void; resolve: (value: boolean) => void}>(); let cancel_running: ((reason: unknown) => void) | undefined; const check_active = () => { if (disposed) throw new Error('Function manager 已销毁'); check_instance(); }; return { get_function_list() { check_active(); return structuredClone(list); }, async run(path, name, args, before_run, options) { const signal = options?.signal; signal?.throwIfAborted(); check_active(); const template = list.find(category => category.path === path)?.function_list.find(item => item.name === name); const instance = template && functions.get(`${path}/${name}`); if (!instance || !template) throw new Error('函数不存在'); if (!Array.isArray(args) || args.length > template.args.length || !template.args.every((arg, index) => index < args.length ? valid_argument(args[index], arg.type) : Object.hasOwn(arg, 'default'))) { throw new Error('函数参数数量或类型不正确'); } const values = structuredClone(template.args.map((arg, index) => index < args.length ? args[index] : arg.default)); if (path === 'workflow' && instance.execution === 'workflow_control') { const workflow = context.workflow_manager; const check_control = () => { signal?.throwIfAborted(); check_active(); if (context.workflow_manager !== workflow) throw new Error('Workflow manager 已变化'); }; before_run?.(); check_control(); if (!workflow) return false; // Bind only the control capability to this dispatch, never the raw Bot context. let result = false; try { result = await measure_sync(bot, () => instance.run(bot, values, {workflow: { control(request) { check_control(); const command = workflow.control(request); check_control(); return command; }, }})) === true; } catch { /* Function failures retain the boolean API. */ } check_control(); return result; } if (context.bot_status !== 'online') throw new Error('Bot 当前不在线'); if (path === 'bot' && name === 'stop_current_function') { before_run?.(); signal?.throwIfAborted(); check_active(); for (const task of pending) task.resolve(false); pending.clear(); const result = await measure_sync(bot, () => instance.run(bot, values)); signal?.throwIfAborted(); return result; } if (pending.size >= 64) throw new Error('函数等待队列已满'); return new Promise((resolve, reject) => { let cancel_execution: ((reason: unknown) => void) | undefined; const cleanup = () => signal?.removeEventListener('abort', abort); const task = { reject(error: unknown) { cleanup(); reject(error); }, resolve(value: boolean) { cleanup(); resolve(value); }, }; const abort = () => { if (pending.delete(task)) task.reject(signal!.reason); else cancel_execution?.(signal!.reason); }; pending.add(task); signal?.addEventListener('abort', abort, {once: true}); const execute = async () => { if (!pending.delete(task)) return; try { check_active(); if (context.bot_status !== 'online') throw new Error('Bot 当前不在线'); signal?.throwIfAborted(); before_run?.(); signal?.throwIfAborted(); check_active(); const cancellation = create_function_cancellation(bot); let interrupted = false; let reason: unknown; cancel_running = cancel_execution = error => { if (interrupted) return; interrupted = true; reason = error; cancellation.cancel(); }; try { await cancellation.run(async () => { // Global stop may answer early, but the queue still drains instance.run. const unwatch = on_function_cancel(bot, () => { if (!interrupted) task.resolve(false); }); let result = false; try { result = await measure_sync(bot, () => instance.run(bot, values)) === true; } catch { /* Function failures retain the boolean API. */ } finally { unwatch(); } if (interrupted) task.reject(reason); else task.resolve(result); }); } finally { cleanup(); cancel_running = cancel_execution = undefined; cancellation.dispose(); } } catch (error) { task.reject(error); } }; tail = tail.then(execute, execute); }); }, dispose() { if (disposed) return; disposed = true; for (const task of pending) task.reject(new Error('Function manager 已销毁,排队调用已取消')); pending.clear(); functions.clear(); cancel_running?.(new Error('Function manager 已销毁,当前调用已取消')); }, }; }