Files
ST-Bionic-Memory-Ecology/task-ejs.js
2026-03-27 02:28:34 +08:00

957 lines
28 KiB
JavaScript

// ST-BME: 任务级 EJS / 世界书渲染引擎
// 仅用于世界书条目渲染,不开放给用户自定义 prompt 块。
import { getSTContextSnapshot } from "./st-context.js";
const DEFAULT_MAX_RECURSION = 10;
let ejsRuntimeStatePromise = null;
const EJS_RUNTIME_STATUS = {
PRIMARY: "primary",
FALLBACK: "fallback",
FAILED: "failed",
};
const FALLBACK_LODASH = {
get: getByPath,
cloneDeep,
escapeRegExp,
sum(values = []) {
return (Array.isArray(values) ? values : []).reduce(
(total, value) => total + (Number(value) || 0),
0,
);
},
};
function getUtilityLib() {
return globalThis._ || FALLBACK_LODASH;
}
function getEjsRuntime() {
return globalThis.ejs || null;
}
function buildEjsRuntimeState(runtime, status, error = null) {
return {
runtime: runtime || null,
status,
isAvailable: Boolean(runtime),
isFallback: status === EJS_RUNTIME_STATUS.FALLBACK,
error: error || null,
};
}
function getCurrentEjsRuntimeState() {
const runtime = getEjsRuntime();
if (!runtime) {
return buildEjsRuntimeState(null, EJS_RUNTIME_STATUS.FAILED);
}
return buildEjsRuntimeState(runtime, EJS_RUNTIME_STATUS.PRIMARY);
}
function createTaskEjsRuntimeUnavailableError(backend, content = "") {
const error = new Error(
`task-ejs runtime unavailable (${backend?.status || EJS_RUNTIME_STATUS.FAILED})`,
);
error.name = "TaskEjsRuntimeUnavailableError";
error.code = "st_bme_task_ejs_runtime_unavailable";
error.backend = backend || null;
error.content = String(content || "");
return error;
}
function createTaskEjsUnsupportedHelperError(helperName, args = []) {
const error = new Error(`task-ejs unsupported helper: ${String(helperName || "unknown")}`);
error.name = "TaskEjsUnsupportedHelperError";
error.code = "st_bme_task_ejs_unsupported_helper";
error.helperName = String(helperName || "unknown");
error.args = Array.isArray(args) ? cloneDeep(args) : [];
return error;
}
async function ensureEjsRuntime() {
const currentState = getCurrentEjsRuntimeState();
if (currentState.isAvailable) {
return currentState;
}
if (ejsRuntimeStatePromise) {
return await ejsRuntimeStatePromise;
}
ejsRuntimeStatePromise = (async () => {
const hadWindow = Object.prototype.hasOwnProperty.call(globalThis, "window");
const previousWindow = globalThis.window;
let importError = null;
if (!hadWindow) {
globalThis.window = globalThis;
}
try {
await import("./vendor/ejs.js");
} catch (error) {
importError = error;
console.warn("[ST-BME] task-ejs 加载 vendor/ejs.js 失败:", error);
} finally {
if (!hadWindow) {
delete globalThis.window;
} else {
globalThis.window = previousWindow;
}
}
const runtime = getEjsRuntime();
if (runtime) {
return buildEjsRuntimeState(runtime, EJS_RUNTIME_STATUS.FALLBACK);
}
return buildEjsRuntimeState(null, EJS_RUNTIME_STATUS.FAILED, importError);
})();
return await ejsRuntimeStatePromise;
}
async function resolveTaskEjsBackend(options = {}) {
if (options.ensureRuntime === false) {
return getCurrentEjsRuntimeState();
}
return await ensureEjsRuntime();
}
function resolveHostSnapshot(injectedSnapshot) {
if (injectedSnapshot?.snapshot) {
return injectedSnapshot;
}
return getSTContextSnapshot();
}
function getStChat(injectedSnapshot) {
return resolveHostSnapshot(injectedSnapshot).snapshot.chat.messages || [];
}
function buildTemplateContext(templateContext = {}, hostSnapshot) {
const resolvedHost = resolveHostSnapshot(hostSnapshot);
const snapshot = resolvedHost.snapshot;
const promptAliases = resolvedHost.prompt || {};
const lastUserMessage =
typeof templateContext.user_input === "string"
? templateContext.user_input
: snapshot.chat.lastUserMessage || "";
return {
user: snapshot.user.name,
char: snapshot.character.name,
userName: promptAliases.userName || snapshot.user.name,
charName: promptAliases.charName || snapshot.character.name,
persona: promptAliases.userPersona || snapshot.persona.text,
userPersona: promptAliases.userPersona || snapshot.persona.text,
charDescription:
promptAliases.charDescription || snapshot.character.description,
currentTime: promptAliases.currentTime || snapshot.time.current,
stSnapshot: snapshot,
hostSnapshot: snapshot,
lastUserMessage,
last_user_message: lastUserMessage,
userInput: lastUserMessage,
user_input: lastUserMessage,
original: "",
input: "",
lastMessage: "",
lastMessageId: "",
newline: "\n",
trim: "",
...templateContext,
};
}
function cloneDeep(value) {
if (value == null) return value;
try {
if (typeof structuredClone === "function") {
return structuredClone(value);
}
} catch {
// ignore and fall back to JSON
}
try {
return JSON.parse(JSON.stringify(value));
} catch {
return value;
}
}
function getByPath(target, path, defaultValue = undefined) {
const result = String(path || "")
.split(".")
.filter(Boolean)
.reduce((acc, key) => (acc == null ? undefined : acc[key]), target);
return result === undefined ? defaultValue : result;
}
function escapeRegExp(value) {
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function normalizeEntryKey(value) {
return String(value ?? "").trim();
}
function normalizeIdentifier(value) {
return String(value || "")
.trim()
.toLowerCase();
}
function normalizeRole(role) {
const normalized = String(role || "system").trim().toLowerCase();
return ["system", "user", "assistant"].includes(normalized)
? normalized
: "system";
}
function processChatMessage(message) {
return String(message?.mes ?? message?.message ?? message?.content ?? "");
}
export function substituteTaskEjsParams(
text,
templateContext = {},
options = {},
) {
if (!text || !String(text).includes("{{")) {
return String(text || "");
}
const context = buildTemplateContext(
templateContext,
options.hostSnapshot || templateContext.hostSnapshot,
);
return String(text).replace(/\{\{\s*([a-zA-Z0-9_.$]+)\s*\}\}/g, (_, path) => {
const value = getByPath(context, path);
if (value == null) return "";
if (typeof value === "object") {
try {
return JSON.stringify(value);
} catch {
return "";
}
}
return String(value);
});
}
function createReadOnlyVariableState(hostSnapshot) {
const snapshot = resolveHostSnapshot(hostSnapshot).snapshot;
const chat = snapshot.chat.messages || [];
const lastMessage = chat[chat.length - 1] || {};
const swipeId = Number(lastMessage?.swipe_id ?? 0);
const messageVars =
lastMessage?.variables && typeof lastMessage.variables === "object"
? cloneDeep(lastMessage.variables[swipeId] || {})
: {};
const globalVars = cloneDeep(snapshot.variables.global || {});
const localVars = cloneDeep(snapshot.variables.local || {});
return Object.freeze({
globalVars,
localVars,
messageVars,
cacheVars: {
...globalVars,
...localVars,
...messageVars,
},
});
}
function getVariable(state, path, options = {}) {
const scope = normalizeIdentifier(options.scope);
if (scope === "global") {
return getByPath(state.globalVars, path, options.defaults);
}
if (scope === "local") {
return getByPath(state.localVars, path, options.defaults);
}
if (scope === "message") {
return getByPath(state.messageVars, path, options.defaults);
}
return getByPath(state.cacheVars, path, options.defaults);
}
function normalizeRenderEntry(entry = {}) {
return {
uid: Number(entry.uid) || 0,
name: normalizeEntryKey(entry.name),
comment: normalizeEntryKey(entry.comment),
content: String(entry.content || ""),
worldbook: normalizeEntryKey(entry.worldbook),
role: normalizeRole(entry.role),
position: Number(entry.position ?? 0),
depth: Number(entry.depth ?? 0),
order: Number(entry.order ?? 100),
enabled: entry.enabled !== false,
activationDebug:
entry.activationDebug && typeof entry.activationDebug === "object"
? cloneDeep(entry.activationDebug)
: null,
};
}
function registerEntryLookup(lookup, key, entry) {
const normalizedKey = normalizeEntryKey(key);
if (!normalizedKey || lookup.has(normalizedKey)) return;
lookup.set(normalizedKey, entry);
}
function registerEntries(renderCtx, entries = []) {
for (const rawEntry of Array.isArray(entries) ? entries : []) {
const entry = normalizeRenderEntry(rawEntry);
renderCtx.entries.push(entry);
registerEntryLookup(renderCtx.allEntries, entry.name, entry);
registerEntryLookup(renderCtx.allEntries, entry.comment, entry);
if (!renderCtx.entriesByWorldbook.has(entry.worldbook)) {
renderCtx.entriesByWorldbook.set(entry.worldbook, new Map());
}
const worldbookLookup = renderCtx.entriesByWorldbook.get(entry.worldbook);
registerEntryLookup(worldbookLookup, entry.name, entry);
registerEntryLookup(worldbookLookup, entry.comment, entry);
}
}
function activationKey(entry) {
return [entry.worldbook, entry.uid || entry.comment || entry.name].join("::");
}
function recordRenderWarning(renderCtx, warning) {
const text = String(warning || "").trim();
if (!text) return;
if (!Array.isArray(renderCtx?.warnings)) {
renderCtx.warnings = [];
}
if (!renderCtx.warnings.includes(text)) {
renderCtx.warnings.push(text);
}
}
async function ensureWorldbookEntriesLoaded(renderCtx, worldbookName) {
const normalizedWorldbook = normalizeEntryKey(worldbookName);
if (!normalizedWorldbook) {
return false;
}
if (renderCtx.entriesByWorldbook.has(normalizedWorldbook)) {
return true;
}
if (renderCtx.worldbookLoadAttempts.has(normalizedWorldbook)) {
return renderCtx.entriesByWorldbook.has(normalizedWorldbook);
}
if (typeof renderCtx.loadWorldbookEntries !== "function") {
return false;
}
renderCtx.worldbookLoadAttempts.add(normalizedWorldbook);
try {
const loadedEntries = await renderCtx.loadWorldbookEntries(normalizedWorldbook);
registerEntries(renderCtx, loadedEntries);
if ((Array.isArray(loadedEntries) ? loadedEntries : []).length > 0) {
renderCtx.lazyLoadedWorldbooks.add(normalizedWorldbook);
return true;
}
} catch (error) {
recordRenderWarning(
renderCtx,
`lazy load worldbook failed: ${normalizedWorldbook}`,
);
console.warn(
`[ST-BME] task-ejs 懒加载世界书失败: ${normalizedWorldbook}`,
error,
);
}
return renderCtx.entriesByWorldbook.has(normalizedWorldbook);
}
async function resolveEntry(renderCtx, currentWorldbook, worldbookOrEntry, entryNameOrData) {
const explicitWorldbook =
typeof entryNameOrData === "string"
? normalizeEntryKey(worldbookOrEntry)
: "";
const fallbackWorldbook = normalizeEntryKey(currentWorldbook);
const identifier = normalizeEntryKey(
typeof entryNameOrData === "string" ? entryNameOrData : worldbookOrEntry,
);
if (!identifier) return undefined;
const lookupInWorldbook = (worldbook) => {
if (!worldbook) return undefined;
return renderCtx.entriesByWorldbook.get(worldbook)?.get(identifier);
};
let resolved =
lookupInWorldbook(explicitWorldbook) ||
lookupInWorldbook(fallbackWorldbook) ||
renderCtx.allEntries.get(identifier);
if (!resolved && explicitWorldbook) {
await ensureWorldbookEntriesLoaded(renderCtx, explicitWorldbook);
resolved =
lookupInWorldbook(explicitWorldbook) ||
lookupInWorldbook(fallbackWorldbook) ||
renderCtx.allEntries.get(identifier);
}
return resolved;
}
async function activateWorldInfoInContext(
renderCtx,
currentWorldbook,
world,
entryOrForce,
maybeForce,
) {
const hasExplicitWorldbook = typeof entryOrForce === "string";
const identifier = normalizeEntryKey(hasExplicitWorldbook ? entryOrForce : world);
const explicitWorldbook = hasExplicitWorldbook ? normalizeEntryKey(world) : "";
const entry = await resolveEntry(
renderCtx,
currentWorldbook,
explicitWorldbook || identifier,
hasExplicitWorldbook ? identifier : undefined,
);
if (!entry) {
recordRenderWarning(
renderCtx,
`activewi target not found: ${explicitWorldbook ? `${explicitWorldbook}/` : ""}${identifier}`,
);
return null;
}
const normalizedEntry = normalizeRenderEntry({
...entry,
content: String(entry.content || "").replaceAll("@@dont_activate", ""),
});
renderCtx.forcedActivatedEntries.set(activationKey(normalizedEntry), normalizedEntry);
return {
world: normalizedEntry.worldbook,
comment: normalizedEntry.comment || normalizedEntry.name,
content: normalizedEntry.content,
forced: typeof maybeForce === "boolean" ? maybeForce : typeof entryOrForce === "boolean",
};
}
async function getwi(
renderCtx,
currentWorldbook,
worldbookOrEntry,
entryNameOrData,
) {
const entry = await resolveEntry(
renderCtx,
currentWorldbook,
worldbookOrEntry,
entryNameOrData,
);
if (!entry) {
return "";
}
const entryKey = activationKey(entry);
if (renderCtx.renderStack.has(entryKey)) {
recordRenderWarning(
renderCtx,
`recursive getwi blocked: ${entry.comment || entry.name}`,
);
console.warn(
`[ST-BME] task-ejs 检测到循环 getwi: ${entry.comment || entry.name}`,
);
return "";
}
if (renderCtx.renderStack.size >= renderCtx.maxRecursion) {
recordRenderWarning(
renderCtx,
`getwi recursion limit reached: ${entry.comment || entry.name}`,
);
console.warn(
`[ST-BME] task-ejs 超过最大递归深度: ${renderCtx.maxRecursion}`,
);
return "";
}
const processed = substituteTaskEjsParams(entry.content, renderCtx.templateContext, {
hostSnapshot: renderCtx.hostSnapshot,
});
let finalContent = processed;
if (processed.includes("<%")) {
renderCtx.renderStack.add(entryKey);
try {
finalContent = await evalTaskEjsTemplate(processed, renderCtx, {
world_info: {
comment: entry.comment || entry.name,
name: entry.name,
world: entry.worldbook,
},
});
} finally {
renderCtx.renderStack.delete(entryKey);
}
}
renderCtx.inlinePulledEntries.set(entryKey, {
name: entry.name,
comment: entry.comment,
content: finalContent,
worldbook: entry.worldbook,
});
return String(finalContent || "");
}
function getChatMessageCompat(renderCtx, index, role) {
const chat = getStChat(renderCtx?.hostSnapshot)
.filter((message) => {
if (!role) return true;
if (role === "user") return Boolean(message?.is_user);
if (role === "system") return Boolean(message?.is_system);
return !message?.is_user && !message?.is_system;
})
.map(processChatMessage);
const resolvedIndex = index >= 0 ? index : chat.length + index;
return chat[resolvedIndex] || "";
}
function getChatMessagesCompat(renderCtx, startOrCount, endOrRole, role) {
const chat = getStChat(renderCtx?.hostSnapshot);
const allMessages = chat.map((message, index) => ({
raw: message,
id: index,
text: processChatMessage(message),
}));
const filterByRole = (items, currentRole) => {
if (!currentRole) return items;
return items.filter((item) => {
if (currentRole === "user") return Boolean(item.raw?.is_user);
if (currentRole === "system") return Boolean(item.raw?.is_system);
return !item.raw?.is_user && !item.raw?.is_system;
});
};
if (endOrRole == null) {
return (
startOrCount > 0
? allMessages.slice(0, startOrCount)
: allMessages.slice(startOrCount)
).map((item) => item.text);
}
if (typeof endOrRole === "string") {
const filtered = filterByRole(allMessages, endOrRole);
return (
startOrCount > 0
? filtered.slice(0, startOrCount)
: filtered.slice(startOrCount)
).map((item) => item.text);
}
return filterByRole(allMessages, role)
.slice(startOrCount, endOrRole)
.map((item) => item.text);
}
function matchChatMessagesCompat(renderCtx, pattern) {
const regex = typeof pattern === "string" ? new RegExp(pattern, "i") : pattern;
return getStChat(renderCtx?.hostSnapshot).some((message) =>
regex.test(processChatMessage(message)),
);
}
function rethrow(err, str, filename, lineNumber, esc) {
const lines = String(str || "").split("\n");
const start = Math.max(lineNumber - 3, 0);
const end = Math.min(lines.length, lineNumber + 3);
const escapedFileName =
typeof esc === "function" ? esc(filename) : filename || "ejs";
const context = lines
.slice(start, end)
.map((line, index) => {
const currentLine = index + start + 1;
return `${currentLine === lineNumber ? " >> " : " "}${currentLine}| ${line}`;
})
.join("\n");
err.message = `${escapedFileName}:${lineNumber}\n${context}\n\n${err.message}`;
throw err;
}
function makeUnsupportedHelper(helperName) {
return (...args) => {
throw createTaskEjsUnsupportedHelperError(helperName, args);
};
}
function getCurrentActivatedEntries(renderCtx) {
return Array.isArray(renderCtx?.currentActivatedEntries)
? renderCtx.currentActivatedEntries
: [];
}
export function createTaskEjsRenderContext(entries = [], options = {}) {
const hostSnapshot = resolveHostSnapshot(options.hostSnapshot);
const renderCtx = {
entries: [],
allEntries: new Map(),
entriesByWorldbook: new Map(),
renderStack: new Set(),
worldbookLoadAttempts: new Set(),
lazyLoadedWorldbooks: new Set(),
warnings: [],
maxRecursion:
Number.isFinite(Number(options.maxRecursion)) && Number(options.maxRecursion) > 0
? Number(options.maxRecursion)
: DEFAULT_MAX_RECURSION,
hostSnapshot,
variableState: createReadOnlyVariableState(hostSnapshot),
currentActivatedEntries: Array.isArray(options.currentActivatedEntries)
? options.currentActivatedEntries.map((entry) => normalizeRenderEntry(entry))
: [],
forcedActivatedEntries: new Map(),
inlinePulledEntries: new Map(),
ejsRuntimeStatus: EJS_RUNTIME_STATUS.FAILED,
ejsRuntimeFallback: false,
ejsLastError: null,
loadWorldbookEntries:
typeof options.loadWorldbookEntries === "function"
? options.loadWorldbookEntries
: null,
templateContext: {
...(options.templateContext || {}),
hostSnapshot: hostSnapshot.snapshot,
stSnapshot: hostSnapshot.snapshot,
},
};
registerEntries(renderCtx, entries);
return renderCtx;
}
export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
const backend = await resolveTaskEjsBackend();
const runtime = backend.runtime;
if (renderCtx && typeof renderCtx === "object") {
renderCtx.ejsRuntimeStatus = backend.status;
renderCtx.ejsRuntimeFallback = Boolean(backend.isFallback);
renderCtx.ejsLastError = backend.error
? backend.error instanceof Error
? backend.error.message
: String(backend.error)
: null;
}
const hostSnapshot = resolveHostSnapshot(renderCtx?.hostSnapshot);
const snapshot = hostSnapshot.snapshot;
const processed = substituteTaskEjsParams(content, renderCtx?.templateContext, {
hostSnapshot,
});
if (!runtime) {
if (processed.includes("<%")) {
throw createTaskEjsRuntimeUnavailableError(backend, processed);
}
return processed;
}
if (!processed.includes("<%")) {
return processed;
}
const stCtx = snapshot.raw || {};
const chat = snapshot.chat.messages || [];
const utilityLib = getUtilityLib();
const workflowUserInput =
typeof renderCtx?.templateContext?.user_input === "string"
? renderCtx.templateContext.user_input
: snapshot.chat.lastUserMessage || "";
const unsupported = {
setvar: makeUnsupportedHelper("setvar"),
setLocalVar: makeUnsupportedHelper("setLocalVar"),
setGlobalVar: makeUnsupportedHelper("setGlobalVar"),
setMessageVar: makeUnsupportedHelper("setMessageVar"),
incvar: makeUnsupportedHelper("incvar"),
decvar: makeUnsupportedHelper("decvar"),
delvar: makeUnsupportedHelper("delvar"),
insvar: makeUnsupportedHelper("insvar"),
incLocalVar: makeUnsupportedHelper("incLocalVar"),
incGlobalVar: makeUnsupportedHelper("incGlobalVar"),
incMessageVar: makeUnsupportedHelper("incMessageVar"),
decLocalVar: makeUnsupportedHelper("decLocalVar"),
decGlobalVar: makeUnsupportedHelper("decGlobalVar"),
decMessageVar: makeUnsupportedHelper("decMessageVar"),
patchVariables: makeUnsupportedHelper("patchVariables"),
getprp: makeUnsupportedHelper("getprp"),
getpreset: makeUnsupportedHelper("getpreset"),
getPresetPrompt: makeUnsupportedHelper("getPresetPrompt"),
execute: makeUnsupportedHelper("execute"),
define: makeUnsupportedHelper("define"),
getqr: makeUnsupportedHelper("getqr"),
getQuickReply: makeUnsupportedHelper("getQuickReply"),
selectActivatedEntries: makeUnsupportedHelper("selectActivatedEntries"),
activateWorldInfoByKeywords: makeUnsupportedHelper("activateWorldInfoByKeywords"),
activateRegex: makeUnsupportedHelper("activateRegex"),
injectPrompt: makeUnsupportedHelper("injectPrompt"),
getPromptsInjected: makeUnsupportedHelper("getPromptsInjected"),
hasPromptsInjected: makeUnsupportedHelper("hasPromptsInjected"),
jsonPatch: makeUnsupportedHelper("jsonPatch"),
};
const context = {
_: utilityLib,
console,
userName: snapshot.user.name,
charName: snapshot.character.name,
assistantName: snapshot.character.name,
charDescription: snapshot.character.description || "",
userPersona: snapshot.persona.text || "",
currentTime: snapshot.time.current || "",
characterId: snapshot.character.id,
hostSnapshot: snapshot,
stSnapshot: snapshot,
get chatId() {
return snapshot.chat.id || "";
},
get variables() {
return renderCtx.variableState.cacheVars;
},
get lastUserMessageId() {
if (typeof chat.findLastIndex === "function") {
return chat.findLastIndex((message) => message?.is_user);
}
const reversedIndex = [...chat].reverse().findIndex((message) => message?.is_user);
return reversedIndex < 0 ? -1 : chat.length - 1 - reversedIndex;
},
get lastUserMessage() {
return (
workflowUserInput ||
chat.findLast?.((message) => message?.is_user)?.mes ||
[...chat].reverse().find((message) => message?.is_user)?.mes ||
""
);
},
get last_user_message() {
return this.lastUserMessage;
},
get userInput() {
return workflowUserInput;
},
get user_input() {
return workflowUserInput;
},
get lastCharMessageId() {
if (typeof chat.findLastIndex === "function") {
return chat.findLastIndex(
(message) => !message?.is_user && !message?.is_system,
);
}
const reversedIndex = [...chat]
.reverse()
.findIndex((message) => !message?.is_user && !message?.is_system);
return reversedIndex < 0 ? -1 : chat.length - 1 - reversedIndex;
},
get lastCharMessage() {
return (
chat.findLast?.((message) => !message?.is_user && !message?.is_system)?.mes ||
[...chat]
.reverse()
.find((message) => !message?.is_user && !message?.is_system)?.mes ||
""
);
},
get lastMessageId() {
return chat.length - 1;
},
get charLoreBook() {
return snapshot.worldbook.character || "";
},
get userLoreBook() {
return snapshot.worldbook.persona || "";
},
get chatLoreBook() {
return snapshot.worldbook.chat || "";
},
get charAvatar() {
return snapshot.character.avatar || "";
},
userAvatar: snapshot.user.avatar || "",
groups: stCtx.groups || [],
groupId: snapshot.host.meta.selectedGroupId,
get model() {
return snapshot.host.meta.onlineStatus || "";
},
get SillyTavern() {
return stCtx;
},
getwi: (worldbookOrEntry, entryNameOrData) =>
getwi(
renderCtx,
String(context.world_info?.world || ""),
worldbookOrEntry,
entryNameOrData,
),
getWorldInfo: (worldbookOrEntry, entryNameOrData) =>
getwi(
renderCtx,
String(context.world_info?.world || ""),
worldbookOrEntry,
entryNameOrData,
),
getvar: (path, options) => getVariable(renderCtx.variableState, path, options),
getLocalVar: (path, options = {}) =>
getVariable(renderCtx.variableState, path, {
...options,
scope: "local",
}),
getGlobalVar: (path, options = {}) =>
getVariable(renderCtx.variableState, path, {
...options,
scope: "global",
}),
getMessageVar: (path, options = {}) =>
getVariable(renderCtx.variableState, path, {
...options,
scope: "message",
}),
getChatMessage: (id, role) => getChatMessageCompat(renderCtx, id, role),
getChatMessages: (startOrCount = getStChat(hostSnapshot).length, endOrRole, role) =>
getChatMessagesCompat(renderCtx, startOrCount, endOrRole, role),
matchChatMessages: (pattern) => matchChatMessagesCompat(renderCtx, pattern),
getchr: () => snapshot.character.description || "",
evalTemplate: async (innerContent, data = {}) =>
evalTaskEjsTemplate(innerContent, renderCtx, data),
getWorldInfoData: async () =>
renderCtx.entries.map((entry) => ({
comment: entry.comment || entry.name,
content: entry.content,
world: entry.worldbook,
})),
getWorldInfoActivatedData: async () =>
getCurrentActivatedEntries(renderCtx).map((entry) => ({
comment: entry.comment || entry.name,
content: entry.content,
world: entry.worldbook,
})),
getEnabledWorldInfoEntries: async () =>
renderCtx.entries.map((entry) => ({
comment: entry.comment || entry.name,
content: entry.content,
world: entry.worldbook,
})),
getEnabledLoreBooks: () => [...new Set(renderCtx.entries.map((entry) => entry.worldbook))],
activewi: async (world, entryOrForce, maybeForce) =>
activateWorldInfoInContext(
renderCtx,
String(context.world_info?.world || ""),
world,
entryOrForce,
maybeForce,
),
activateWorldInfo: async (world, entryOrForce, maybeForce) =>
activateWorldInfoInContext(
renderCtx,
String(context.world_info?.world || ""),
world,
entryOrForce,
maybeForce,
),
parseJSON: (raw) => {
try {
return JSON.parse(raw);
} catch {
return null;
}
},
print: (...parts) =>
parts.filter((part) => part !== undefined && part !== null).join(""),
...unsupported,
...extraEnv,
};
context.getchar = context.getchr;
context.getChara = context.getchr;
try {
const compiled = runtime.compile(processed, {
async: true,
outputFunctionName: "print",
_with: true,
localsName: "locals",
client: true,
});
const result = await compiled.call(
context,
context,
(value) => value,
() => ({ filename: "", template: "" }),
rethrow,
);
return result ?? "";
} catch (error) {
if (renderCtx && typeof renderCtx === "object") {
renderCtx.ejsLastError =
error instanceof Error ? error.message : String(error);
}
if (error?.code === "st_bme_task_ejs_unsupported_helper") {
throw error;
}
console.warn("[ST-BME] task-ejs 渲染失败:", error);
throw error;
}
}
export async function renderTaskEjsContent(content, templateContext = {}) {
const hostSnapshot = resolveHostSnapshot(templateContext.hostSnapshot);
const processed = substituteTaskEjsParams(content, templateContext, {
hostSnapshot,
});
if (!processed.includes("<%")) {
return processed;
}
const renderCtx = createTaskEjsRenderContext([], {
templateContext,
hostSnapshot,
});
return await evalTaskEjsTemplate(processed, renderCtx);
}
export async function checkTaskEjsSyntax(content) {
const backend = await resolveTaskEjsBackend();
const runtime = backend.runtime;
if (!runtime || !String(content || "").includes("<%")) {
return null;
}
try {
runtime.compile(content, {
async: true,
client: true,
_with: true,
localsName: "locals",
});
return null;
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
}
export async function inspectTaskEjsRuntimeBackend(options = {}) {
return await resolveTaskEjsBackend(options);
}