🎨 优化扩展模块,完成ai接入和对话功能
This commit is contained in:
212
data/st-core-scripts/scripts/macros/engine/MacroEngine.js
Normal file
212
data/st-core-scripts/scripts/macros/engine/MacroEngine.js
Normal file
@@ -0,0 +1,212 @@
|
||||
import { MacroParser } from './MacroParser.js';
|
||||
import { MacroCstWalker } from './MacroCstWalker.js';
|
||||
import { MacroRegistry } from './MacroRegistry.js';
|
||||
import { logMacroGeneralError, logMacroInternalError, logMacroRuntimeWarning, logMacroSyntaxWarning } from './MacroDiagnostics.js';
|
||||
|
||||
/** @typedef {import('./MacroCstWalker.js').MacroCall} MacroCall */
|
||||
/** @typedef {import('./MacroEnv.types.js').MacroEnv} MacroEnv */
|
||||
/** @typedef {import('./MacroRegistry.js').MacroDefinition} MacroDefinition */
|
||||
|
||||
/**
|
||||
* The singleton instance of the MacroEngine.
|
||||
*
|
||||
* @type {MacroEngine}
|
||||
*/
|
||||
let instance;
|
||||
export { instance as MacroEngine };
|
||||
|
||||
class MacroEngine {
|
||||
/** @type {MacroEngine} */ static #instance;
|
||||
/** @type {MacroEngine} */ static get instance() { return MacroEngine.#instance ?? (MacroEngine.#instance = new MacroEngine()); }
|
||||
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* Evaluates a string containing macros and resolves them.
|
||||
*
|
||||
* @param {string} input - The input string to evaluate.
|
||||
* @param {MacroEnv} env - The environment to pass to the macro handler.
|
||||
* @returns {string} The resolved string.
|
||||
*/
|
||||
evaluate(input, env) {
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
const safeEnv = Object.freeze({ ...env });
|
||||
|
||||
const preProcessed = this.#runPreProcessors(input, safeEnv);
|
||||
|
||||
const { cst, lexingErrors, parserErrors } = MacroParser.parseDocument(preProcessed);
|
||||
|
||||
// For now, we log and still try to process what we can.
|
||||
if (lexingErrors && lexingErrors.length > 0) {
|
||||
logMacroSyntaxWarning({ phase: 'lexing', input, errors: lexingErrors });
|
||||
}
|
||||
if (parserErrors && parserErrors.length > 0) {
|
||||
logMacroSyntaxWarning({ phase: 'parsing', input, errors: parserErrors });
|
||||
}
|
||||
|
||||
// If the parser did not produce a valid CST, fall back to the original input.
|
||||
if (!cst || typeof cst !== 'object' || !cst.children) {
|
||||
logMacroGeneralError({ message: 'Macro parser produced an invalid CST. Returning original input.', error: { input, lexingErrors, parserErrors } });
|
||||
return input;
|
||||
}
|
||||
|
||||
let evaluated;
|
||||
try {
|
||||
evaluated = MacroCstWalker.evaluateDocument({
|
||||
text: preProcessed,
|
||||
cst,
|
||||
env: safeEnv,
|
||||
resolveMacro: this.#resolveMacro.bind(this),
|
||||
});
|
||||
} catch (error) {
|
||||
logMacroGeneralError({ message: 'Macro evaluation failed. Returning original input.', error: { input, error } });
|
||||
return input;
|
||||
}
|
||||
|
||||
const result = this.#runPostProcessors(evaluated, safeEnv);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a macro call.
|
||||
*
|
||||
* @param {MacroCall} call - The macro call to resolve.
|
||||
* @returns {string} The resolved macro.
|
||||
*/
|
||||
#resolveMacro(call) {
|
||||
const { name, env } = call;
|
||||
|
||||
const raw = `{{${call.rawInner}}}`;
|
||||
if (!name) return raw;
|
||||
|
||||
// First check if this is a dynamic macro to use. If so, we will create a temporary macro definition for it and use that over any registered macro.
|
||||
/** @type {MacroDefinition?} */
|
||||
let defOverride = null;
|
||||
if (Object.hasOwn(env.dynamicMacros, name)) {
|
||||
const impl = env.dynamicMacros[name];
|
||||
defOverride = {
|
||||
name,
|
||||
aliases: [],
|
||||
category: 'dynamic',
|
||||
description: 'Dynamic macro',
|
||||
minArgs: 0,
|
||||
maxArgs: 0,
|
||||
unnamedArgDefs: [],
|
||||
list: null,
|
||||
strictArgs: true, // Fail dynamic macros if they are called with arguments
|
||||
returns: null,
|
||||
returnType: 'string',
|
||||
displayOverride: null,
|
||||
exampleUsage: [],
|
||||
source: { name: 'dynamic', isExtension: false, isThirdParty: false },
|
||||
aliasOf: null,
|
||||
aliasVisible: null,
|
||||
handler: typeof impl === 'function' ? impl : () => impl,
|
||||
};
|
||||
}
|
||||
|
||||
// If not, check if the macro exists and is registered
|
||||
if (!defOverride && !MacroRegistry.hasMacro(name)) {
|
||||
return raw; // Unknown macro: keep macro syntax, but nested macros inside rawInner are already resolved.
|
||||
}
|
||||
|
||||
try {
|
||||
const result = MacroRegistry.executeMacro(call, { defOverride });
|
||||
|
||||
try {
|
||||
return call.env.functions.postProcess(result);
|
||||
} catch (error) {
|
||||
logMacroInternalError({ message: `Macro "${name}" postProcess function failed.`, call, error });
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
const isRuntimeError = !!(error && (error.name === 'MacroRuntimeError' || error.isMacroRuntimeError));
|
||||
if (isRuntimeError) {
|
||||
logMacroRuntimeWarning({ message: (error.message || `Macro "${name}" execution failed.`), call, error });
|
||||
} else {
|
||||
logMacroInternalError({ message: `Macro "${name}" internal execution error.`, call, error });
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs pre-processors on the input text, before the engine processes the input.
|
||||
*
|
||||
* @param {string} text - The input text to process.
|
||||
* @param {MacroEnv} env - The environment to pass to the macro handler.
|
||||
* @returns {string} The processed text.
|
||||
*/
|
||||
#runPreProcessors(text, env) {
|
||||
let result = text;
|
||||
|
||||
// This legacy macro will not be supported by the new macro parser, but rather regex-replaced beforehand
|
||||
// {{time_UTC-10}} => {{time::UTC-10}}
|
||||
result = result.replace(/{{time_(UTC[+-]\d+)}}/gi, (_match, utcOffset) => {
|
||||
return `{{time::${utcOffset}}}`;
|
||||
});
|
||||
|
||||
// Legacy non-curly markers like <USER>, <BOT>, <GROUP>, etc.
|
||||
// These are rewritten into their equivalent macro forms so they go through the normal engine pipeline.
|
||||
result = result.replace(/<USER>/gi, '{{user}}');
|
||||
result = result.replace(/<BOT>/gi, '{{char}}');
|
||||
result = result.replace(/<CHAR>/gi, '{{char}}');
|
||||
result = result.replace(/<GROUP>/gi, '{{group}}');
|
||||
result = result.replace(/<CHARIFNOTGROUP>/gi, '{{charIfNotGroup}}');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs post-processors on the input text, after the engine finished processing the input.
|
||||
*
|
||||
* @param {string} text - The input text to process.
|
||||
* @param {MacroEnv} env - The environment to pass to the macro handler.
|
||||
* @returns {string} The processed text.
|
||||
*/
|
||||
#runPostProcessors(text, env) {
|
||||
let result = text;
|
||||
|
||||
// Unescape braces: \{ → { and \} → }
|
||||
// Since \{\{ doesn't match {{ (MacroStart), it passes through as plain text.
|
||||
// We only need to remove the backslashes in post-processing.
|
||||
result = result.replace(/\\([{}])/g, '$1');
|
||||
|
||||
// The original trim macro is reaching over the boundaries of the defined macro. This is not something the engine supports.
|
||||
// To treat {{trim}} as it was before, we won't process it by the engine itself,
|
||||
// but doing a regex replace on {{trim}} and the surrounding area, after all other macros have been processed.
|
||||
result = result.replace(/(?:\r?\n)*{{trim}}(?:\r?\n)*/gi, '');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes macro results into a string.
|
||||
* This mirrors the behavior of the legacy macro system in a simplified way.
|
||||
*
|
||||
* @param {any} value
|
||||
* @returns {string}
|
||||
*/
|
||||
normalizeMacroResult(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
if (typeof value === 'object' || Array.isArray(value)) {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (_error) {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
instance = MacroEngine.instance;
|
||||
Reference in New Issue
Block a user