mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
refactor: purify task worldinfo and ejs runtime
This commit is contained in:
@@ -65,13 +65,13 @@ const BUILTIN_BLOCK_DEFINITIONS = [
|
||||
sourceKey: "worldInfoBefore",
|
||||
name: "世界书前块",
|
||||
role: "system",
|
||||
description: "注入 EW 同款世界书引擎解析后的 before 桶内容,支持角色主/附加世界书、用户设定世界书、聊天世界书,以及世界书条目中的 EJS / getwi。",
|
||||
description: "注入按酒馆世界书规则解析后的 before 桶内容,支持角色主/附加世界书、用户设定世界书、聊天世界书,以及世界书条目中的 EJS / getwi。",
|
||||
},
|
||||
{
|
||||
sourceKey: "worldInfoAfter",
|
||||
name: "世界书后块",
|
||||
role: "system",
|
||||
description: "注入 EW 同款世界书引擎解析后的 after 桶内容。atDepth 条目不会出现在这里,而是自动并入额外消息链路。",
|
||||
description: "注入按酒馆世界书规则解析后的 after 桶内容。atDepth 条目不会出现在这里,而是自动并入额外消息链路。",
|
||||
},
|
||||
{
|
||||
sourceKey: "outputRules",
|
||||
|
||||
521
task-ejs.js
521
task-ejs.js
@@ -14,10 +14,8 @@ const EJS_RUNTIME_STATUS = {
|
||||
|
||||
const FALLBACK_LODASH = {
|
||||
get: getByPath,
|
||||
set: setByPath,
|
||||
unset: unsetByPath,
|
||||
cloneDeep: cloneDeep,
|
||||
escapeRegExp: escapeRegExp,
|
||||
cloneDeep,
|
||||
escapeRegExp,
|
||||
sum(values = []) {
|
||||
return (Array.isArray(values) ? values : []).reduce(
|
||||
(total, value) => total + (Number(value) || 0),
|
||||
@@ -63,6 +61,15 @@ function createTaskEjsRuntimeUnavailableError(backend, 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) {
|
||||
@@ -73,10 +80,7 @@ async function ensureEjsRuntime() {
|
||||
}
|
||||
|
||||
ejsRuntimeStatePromise = (async () => {
|
||||
const hadWindow = Object.prototype.hasOwnProperty.call(
|
||||
globalThis,
|
||||
"window",
|
||||
);
|
||||
const hadWindow = Object.prototype.hasOwnProperty.call(globalThis, "window");
|
||||
const previousWindow = globalThis.window;
|
||||
let importError = null;
|
||||
|
||||
@@ -121,10 +125,6 @@ function resolveHostSnapshot(injectedSnapshot) {
|
||||
return getSTContextSnapshot();
|
||||
}
|
||||
|
||||
function getStContext(injectedSnapshot) {
|
||||
return resolveHostSnapshot(injectedSnapshot).snapshot.raw || {};
|
||||
}
|
||||
|
||||
function getStChat(injectedSnapshot) {
|
||||
return resolveHostSnapshot(injectedSnapshot).snapshot.chat.messages || [];
|
||||
}
|
||||
@@ -189,43 +189,6 @@ function getByPath(target, path, defaultValue = undefined) {
|
||||
return result === undefined ? defaultValue : result;
|
||||
}
|
||||
|
||||
function setByPath(target, path, value) {
|
||||
const segments = String(path || "")
|
||||
.split(".")
|
||||
.filter(Boolean);
|
||||
if (segments.length === 0 || target == null || typeof target !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
let cursor = target;
|
||||
for (let index = 0; index < segments.length - 1; index += 1) {
|
||||
const key = segments[index];
|
||||
if (cursor[key] == null || typeof cursor[key] !== "object") {
|
||||
cursor[key] = {};
|
||||
}
|
||||
cursor = cursor[key];
|
||||
}
|
||||
cursor[segments[segments.length - 1]] = value;
|
||||
}
|
||||
|
||||
function unsetByPath(target, path) {
|
||||
const segments = String(path || "")
|
||||
.split(".")
|
||||
.filter(Boolean);
|
||||
if (segments.length === 0 || target == null || typeof target !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
let cursor = target;
|
||||
for (let index = 0; index < segments.length - 1; index += 1) {
|
||||
cursor = cursor?.[segments[index]];
|
||||
if (cursor == null || typeof cursor !== "object") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
delete cursor[segments[segments.length - 1]];
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -240,6 +203,13 @@ function normalizeIdentifier(value) {
|
||||
.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 ?? "");
|
||||
}
|
||||
@@ -271,7 +241,7 @@ export function substituteTaskEjsParams(
|
||||
});
|
||||
}
|
||||
|
||||
function createVariableState(hostSnapshot) {
|
||||
function createReadOnlyVariableState(hostSnapshot) {
|
||||
const snapshot = resolveHostSnapshot(hostSnapshot).snapshot;
|
||||
const chat = snapshot.chat.messages || [];
|
||||
const lastMessage = chat[chat.length - 1] || {};
|
||||
@@ -283,7 +253,7 @@ function createVariableState(hostSnapshot) {
|
||||
const globalVars = cloneDeep(snapshot.variables.global || {});
|
||||
const localVars = cloneDeep(snapshot.variables.local || {});
|
||||
|
||||
return {
|
||||
return Object.freeze({
|
||||
globalVars,
|
||||
localVars,
|
||||
messageVars,
|
||||
@@ -292,15 +262,7 @@ function createVariableState(hostSnapshot) {
|
||||
...localVars,
|
||||
...messageVars,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rebuildVariableCache(state) {
|
||||
state.cacheVars = {
|
||||
...state.globalVars,
|
||||
...state.localVars,
|
||||
...state.messageVars,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getVariable(state, path, options = {}) {
|
||||
@@ -317,21 +279,23 @@ function getVariable(state, path, options = {}) {
|
||||
return getByPath(state.cacheVars, path, options.defaults);
|
||||
}
|
||||
|
||||
function setVariable(state, path, value, options = {}) {
|
||||
const scope = normalizeIdentifier(options.scope) || "message";
|
||||
const target =
|
||||
scope === "global"
|
||||
? state.globalVars
|
||||
: scope === "local"
|
||||
? state.localVars
|
||||
: state.messageVars;
|
||||
|
||||
if (value === undefined) {
|
||||
unsetByPath(target, path);
|
||||
} else {
|
||||
setByPath(target, path, cloneDeep(value));
|
||||
}
|
||||
rebuildVariableCache(state);
|
||||
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) {
|
||||
@@ -340,16 +304,75 @@ function registerEntryLookup(lookup, key, entry) {
|
||||
lookup.set(normalizedKey, entry);
|
||||
}
|
||||
|
||||
function activationKey(entry) {
|
||||
return `${entry.worldbook}::${entry.comment || entry.name}`;
|
||||
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 findEntry(
|
||||
renderCtx,
|
||||
currentWorldbook,
|
||||
worldbookOrEntry,
|
||||
entryNameOrData,
|
||||
) {
|
||||
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)
|
||||
@@ -366,11 +389,20 @@ function findEntry(
|
||||
return renderCtx.entriesByWorldbook.get(worldbook)?.get(identifier);
|
||||
};
|
||||
|
||||
return (
|
||||
let resolved =
|
||||
lookupInWorldbook(explicitWorldbook) ||
|
||||
lookupInWorldbook(fallbackWorldbook) ||
|
||||
renderCtx.allEntries.get(identifier)
|
||||
);
|
||||
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(
|
||||
@@ -380,32 +412,35 @@ async function activateWorldInfoInContext(
|
||||
entryOrForce,
|
||||
maybeForce,
|
||||
) {
|
||||
const force = typeof entryOrForce === "boolean" ? entryOrForce : maybeForce;
|
||||
const explicitWorldbook = typeof entryOrForce === "string" ? world : null;
|
||||
const identifier = typeof entryOrForce === "string" ? entryOrForce : world;
|
||||
const entry = identifier
|
||||
? findEntry(renderCtx, currentWorldbook, explicitWorldbook, identifier)
|
||||
: undefined;
|
||||
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 = force
|
||||
? {
|
||||
...entry,
|
||||
content: String(entry.content || "").replaceAll("@@dont_activate", ""),
|
||||
}
|
||||
: entry;
|
||||
const normalizedEntry = normalizeRenderEntry({
|
||||
...entry,
|
||||
content: String(entry.content || "").replaceAll("@@dont_activate", ""),
|
||||
});
|
||||
|
||||
renderCtx.activatedEntries.set(
|
||||
activationKey(normalizedEntry),
|
||||
normalizedEntry,
|
||||
);
|
||||
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",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -415,7 +450,7 @@ async function getwi(
|
||||
worldbookOrEntry,
|
||||
entryNameOrData,
|
||||
) {
|
||||
const entry = findEntry(
|
||||
const entry = await resolveEntry(
|
||||
renderCtx,
|
||||
currentWorldbook,
|
||||
worldbookOrEntry,
|
||||
@@ -427,23 +462,30 @@ async function getwi(
|
||||
|
||||
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 substituteTaskEjsParams(entry.content, renderCtx.templateContext);
|
||||
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 substituteTaskEjsParams(entry.content, renderCtx.templateContext);
|
||||
return "";
|
||||
}
|
||||
|
||||
const processed = substituteTaskEjsParams(
|
||||
entry.content,
|
||||
renderCtx.templateContext,
|
||||
);
|
||||
const processed = substituteTaskEjsParams(entry.content, renderCtx.templateContext, {
|
||||
hostSnapshot: renderCtx.hostSnapshot,
|
||||
});
|
||||
let finalContent = processed;
|
||||
|
||||
if (processed.includes("<%")) {
|
||||
@@ -461,20 +503,18 @@ async function getwi(
|
||||
}
|
||||
}
|
||||
|
||||
if (!renderCtx.pulledEntries.has(entryKey)) {
|
||||
renderCtx.pulledEntries.set(entryKey, {
|
||||
name: entry.name,
|
||||
comment: entry.comment,
|
||||
content: finalContent,
|
||||
worldbook: entry.worldbook,
|
||||
});
|
||||
}
|
||||
renderCtx.inlinePulledEntries.set(entryKey, {
|
||||
name: entry.name,
|
||||
comment: entry.comment,
|
||||
content: finalContent,
|
||||
worldbook: entry.worldbook,
|
||||
});
|
||||
|
||||
return finalContent;
|
||||
return String(finalContent || "");
|
||||
}
|
||||
|
||||
function getChatMessageCompat(index, role) {
|
||||
const chat = getStChat()
|
||||
function getChatMessageCompat(renderCtx, index, role) {
|
||||
const chat = getStChat(renderCtx?.hostSnapshot)
|
||||
.filter((message) => {
|
||||
if (!role) return true;
|
||||
if (role === "user") return Boolean(message?.is_user);
|
||||
@@ -487,12 +527,9 @@ function getChatMessageCompat(index, role) {
|
||||
return chat[resolvedIndex] || "";
|
||||
}
|
||||
|
||||
function getChatMessagesCompat(
|
||||
startOrCount = getStChat().length,
|
||||
endOrRole,
|
||||
role,
|
||||
) {
|
||||
const allMessages = getStChat().map((message, index) => ({
|
||||
function getChatMessagesCompat(renderCtx, startOrCount, endOrRole, role) {
|
||||
const chat = getStChat(renderCtx?.hostSnapshot);
|
||||
const allMessages = chat.map((message, index) => ({
|
||||
raw: message,
|
||||
id: index,
|
||||
text: processChatMessage(message),
|
||||
@@ -529,10 +566,11 @@ function getChatMessagesCompat(
|
||||
.map((item) => item.text);
|
||||
}
|
||||
|
||||
function matchChatMessagesCompat(pattern) {
|
||||
const regex =
|
||||
typeof pattern === "string" ? new RegExp(pattern, "i") : pattern;
|
||||
return getStChat().some((message) => regex.test(processChatMessage(message)));
|
||||
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) {
|
||||
@@ -553,55 +591,55 @@ function rethrow(err, str, filename, lineNumber, esc) {
|
||||
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 normalizedEntries = (Array.isArray(entries) ? entries : []).map(
|
||||
(entry) => ({
|
||||
name: normalizeEntryKey(entry?.name),
|
||||
comment: normalizeEntryKey(entry?.comment),
|
||||
content: String(entry?.content || ""),
|
||||
worldbook: normalizeEntryKey(entry?.worldbook),
|
||||
}),
|
||||
);
|
||||
|
||||
const allEntries = new Map();
|
||||
const entriesByWorldbook = new Map();
|
||||
|
||||
for (const entry of normalizedEntries) {
|
||||
registerEntryLookup(allEntries, entry.name, entry);
|
||||
registerEntryLookup(allEntries, entry.comment, entry);
|
||||
|
||||
if (!entriesByWorldbook.has(entry.worldbook)) {
|
||||
entriesByWorldbook.set(entry.worldbook, new Map());
|
||||
}
|
||||
const worldbookLookup = entriesByWorldbook.get(entry.worldbook);
|
||||
registerEntryLookup(worldbookLookup, entry.name, entry);
|
||||
registerEntryLookup(worldbookLookup, entry.comment, entry);
|
||||
}
|
||||
|
||||
return {
|
||||
entries: normalizedEntries,
|
||||
allEntries,
|
||||
entriesByWorldbook,
|
||||
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.isFinite(Number(options.maxRecursion)) && Number(options.maxRecursion) > 0
|
||||
? Number(options.maxRecursion)
|
||||
: DEFAULT_MAX_RECURSION,
|
||||
hostSnapshot,
|
||||
variableState: createVariableState(hostSnapshot),
|
||||
activatedEntries: new Map(),
|
||||
pulledEntries: new Map(),
|
||||
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 = {}) {
|
||||
@@ -616,29 +654,20 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
: String(backend.error)
|
||||
: null;
|
||||
}
|
||||
|
||||
const hostSnapshot = resolveHostSnapshot(renderCtx?.hostSnapshot);
|
||||
const snapshot = hostSnapshot.snapshot;
|
||||
const processed = substituteTaskEjsParams(content, renderCtx?.templateContext, {
|
||||
hostSnapshot,
|
||||
});
|
||||
|
||||
if (!runtime) {
|
||||
const substituted = substituteTaskEjsParams(content, renderCtx?.templateContext, {
|
||||
hostSnapshot,
|
||||
});
|
||||
if (substituted.includes("<%")) {
|
||||
throw createTaskEjsRuntimeUnavailableError(backend, substituted);
|
||||
if (processed.includes("<%")) {
|
||||
throw createTaskEjsRuntimeUnavailableError(backend, processed);
|
||||
}
|
||||
console.warn(
|
||||
"[ST-BME] task-ejs 未找到可用 ejs runtime,回退为轻量变量替换:",
|
||||
backend,
|
||||
);
|
||||
return substituted;
|
||||
return processed;
|
||||
}
|
||||
|
||||
const processed = substituteTaskEjsParams(
|
||||
content,
|
||||
renderCtx?.templateContext,
|
||||
{
|
||||
hostSnapshot,
|
||||
},
|
||||
);
|
||||
if (!processed.includes("<%")) {
|
||||
return processed;
|
||||
}
|
||||
@@ -651,12 +680,47 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
? 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,
|
||||
@@ -667,9 +731,11 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
return renderCtx.variableState.cacheVars;
|
||||
},
|
||||
get lastUserMessageId() {
|
||||
return chat.findLastIndex
|
||||
? chat.findLastIndex((message) => message?.is_user)
|
||||
: [...chat].reverse().findIndex((message) => message?.is_user);
|
||||
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 (
|
||||
@@ -689,18 +755,19 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
return workflowUserInput;
|
||||
},
|
||||
get lastCharMessageId() {
|
||||
return chat.findLastIndex
|
||||
? chat.findLastIndex(
|
||||
(message) => !message?.is_user && !message?.is_system,
|
||||
)
|
||||
: [...chat]
|
||||
.reverse()
|
||||
.findIndex((message) => !message?.is_user && !message?.is_system);
|
||||
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.findLast?.((message) => !message?.is_user && !message?.is_system)?.mes ||
|
||||
[...chat]
|
||||
.reverse()
|
||||
.find((message) => !message?.is_user && !message?.is_system)?.mes ||
|
||||
@@ -745,8 +812,7 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
worldbookOrEntry,
|
||||
entryNameOrData,
|
||||
),
|
||||
getvar: (path, options) =>
|
||||
getVariable(renderCtx.variableState, path, options),
|
||||
getvar: (path, options) => getVariable(renderCtx.variableState, path, options),
|
||||
getLocalVar: (path, options = {}) =>
|
||||
getVariable(renderCtx.variableState, path, {
|
||||
...options,
|
||||
@@ -762,51 +828,13 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
...options,
|
||||
scope: "message",
|
||||
}),
|
||||
setvar: (path, value, options = {}) =>
|
||||
setVariable(renderCtx.variableState, path, value, options),
|
||||
setLocalVar: (path, value, options = {}) =>
|
||||
setVariable(renderCtx.variableState, path, value, {
|
||||
...options,
|
||||
scope: "local",
|
||||
}),
|
||||
setGlobalVar: (path, value, options = {}) =>
|
||||
setVariable(renderCtx.variableState, path, value, {
|
||||
...options,
|
||||
scope: "global",
|
||||
}),
|
||||
setMessageVar: (path, value, options = {}) =>
|
||||
setVariable(renderCtx.variableState, path, value, {
|
||||
...options,
|
||||
scope: "message",
|
||||
}),
|
||||
incvar: () => undefined,
|
||||
decvar: () => undefined,
|
||||
delvar: () => undefined,
|
||||
insvar: () => undefined,
|
||||
incLocalVar: () => undefined,
|
||||
incGlobalVar: () => undefined,
|
||||
incMessageVar: () => undefined,
|
||||
decLocalVar: () => undefined,
|
||||
decGlobalVar: () => undefined,
|
||||
decMessageVar: () => undefined,
|
||||
patchVariables: () => undefined,
|
||||
getChatMessage: (id, role) => getChatMessageCompat(id, role),
|
||||
getChatMessages: (startOrCount, endOrRole, role) =>
|
||||
getChatMessagesCompat(startOrCount, endOrRole, role),
|
||||
matchChatMessages: (pattern) => matchChatMessagesCompat(pattern),
|
||||
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 || "",
|
||||
getchar: undefined,
|
||||
getChara: undefined,
|
||||
getprp: async () => "",
|
||||
getpreset: async () => "",
|
||||
getPresetPrompt: async () => "",
|
||||
execute: async () => "",
|
||||
define: () => undefined,
|
||||
evalTemplate: async (innerContent, data = {}) =>
|
||||
evalTaskEjsTemplate(innerContent, renderCtx, data),
|
||||
getqr: async () => "",
|
||||
getQuickReply: async () => "",
|
||||
findVariables: () => ({}),
|
||||
getWorldInfoData: async () =>
|
||||
renderCtx.entries.map((entry) => ({
|
||||
comment: entry.comment || entry.name,
|
||||
@@ -814,7 +842,7 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
world: entry.worldbook,
|
||||
})),
|
||||
getWorldInfoActivatedData: async () =>
|
||||
[...renderCtx.activatedEntries.values()].map((entry) => ({
|
||||
getCurrentActivatedEntries(renderCtx).map((entry) => ({
|
||||
comment: entry.comment || entry.name,
|
||||
content: entry.content,
|
||||
world: entry.worldbook,
|
||||
@@ -825,11 +853,7 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
content: entry.content,
|
||||
world: entry.worldbook,
|
||||
})),
|
||||
selectActivatedEntries: () => [],
|
||||
activateWorldInfoByKeywords: async () => [],
|
||||
getEnabledLoreBooks: () => [
|
||||
...new Set(renderCtx.entries.map((entry) => entry.worldbook)),
|
||||
],
|
||||
getEnabledLoreBooks: () => [...new Set(renderCtx.entries.map((entry) => entry.worldbook))],
|
||||
activewi: async (world, entryOrForce, maybeForce) =>
|
||||
activateWorldInfoInContext(
|
||||
renderCtx,
|
||||
@@ -846,11 +870,6 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
entryOrForce,
|
||||
maybeForce,
|
||||
),
|
||||
activateRegex: () => undefined,
|
||||
injectPrompt: () => undefined,
|
||||
getPromptsInjected: () => [],
|
||||
hasPromptsInjected: () => false,
|
||||
jsonPatch: () => undefined,
|
||||
parseJSON: (raw) => {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
@@ -860,6 +879,7 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
},
|
||||
print: (...parts) =>
|
||||
parts.filter((part) => part !== undefined && part !== null).join(""),
|
||||
...unsupported,
|
||||
...extraEnv,
|
||||
};
|
||||
|
||||
@@ -887,8 +907,11 @@ export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
|
||||
renderCtx.ejsLastError =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
console.warn("[ST-BME] task-ejs 渲染失败,回退原文本:", error);
|
||||
return processed;
|
||||
if (error?.code === "st_bme_task_ejs_unsupported_helper") {
|
||||
throw error;
|
||||
}
|
||||
console.warn("[ST-BME] task-ejs 渲染失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// ST-BME: 任务级世界书激活引擎
|
||||
// 复刻 Evolution_World 的世界书来源、激活与 EJS 渲染主逻辑,
|
||||
// 但只接入 ST-BME 的任务预设系统,不引入完整工作流调度层。
|
||||
// 对标 SillyTavern 原生世界书扫描逻辑,并在私有 prompt 组装阶段
|
||||
// 提供最小 EJS 配合能力,用于 getwi / activewi。
|
||||
|
||||
import {
|
||||
createTaskEjsRenderContext,
|
||||
@@ -36,34 +36,9 @@ const DEPTH_MAPPING = {
|
||||
};
|
||||
|
||||
const DEFAULT_DEPTH = 4;
|
||||
const DEFAULT_CONTROLLER_ENTRY_PREFIX = "EW/Controller/";
|
||||
const DEFAULT_MAX_RESOLVE_PASSES = 10;
|
||||
const WORLDINFO_CACHE_TTL_MS = 3000;
|
||||
const KNOWN_DECORATORS = [
|
||||
"@@activate",
|
||||
"@@dont_activate",
|
||||
"@@message_formatting",
|
||||
"@@generate",
|
||||
"@@generate_before",
|
||||
"@@generate_after",
|
||||
"@@render",
|
||||
"@@render_before",
|
||||
"@@render_after",
|
||||
"@@dont_preload",
|
||||
"@@initial_variables",
|
||||
"@@always_enabled",
|
||||
"@@only_preload",
|
||||
"@@iframe",
|
||||
"@@preprocessing",
|
||||
"@@if",
|
||||
"@@private",
|
||||
];
|
||||
|
||||
const SPECIAL_NAME_MARKERS = [
|
||||
"[GENERATE:",
|
||||
"[RENDER:",
|
||||
"@INJECT",
|
||||
"[InitialVariables]",
|
||||
];
|
||||
const KNOWN_DECORATORS = ["@@activate", "@@dont_activate"];
|
||||
|
||||
let worldbookEntriesCache = {
|
||||
key: "",
|
||||
@@ -219,34 +194,42 @@ function simpleHash(input = "") {
|
||||
}
|
||||
|
||||
function parseDecorators(content = "") {
|
||||
const decorators = [];
|
||||
const cleanLines = [];
|
||||
const rawContent = String(content || "");
|
||||
if (!rawContent.startsWith("@@")) {
|
||||
return {
|
||||
decorators: [],
|
||||
cleanContent: rawContent,
|
||||
};
|
||||
}
|
||||
|
||||
for (const line of String(content || "").split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
const matched = KNOWN_DECORATORS.find((decorator) =>
|
||||
trimmed.startsWith(decorator),
|
||||
);
|
||||
if (matched) {
|
||||
const firstSpace = trimmed.indexOf(" ");
|
||||
decorators.push(firstSpace > 0 ? trimmed.slice(0, firstSpace) : trimmed);
|
||||
} else {
|
||||
cleanLines.push(line);
|
||||
const lines = rawContent.split("\n");
|
||||
const decorators = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < lines.length) {
|
||||
const line = String(lines[index] || "");
|
||||
if (!line.startsWith("@@")) {
|
||||
break;
|
||||
}
|
||||
if (line.startsWith("@@@")) {
|
||||
break;
|
||||
}
|
||||
const matched = KNOWN_DECORATORS.find((decorator) =>
|
||||
line.startsWith(decorator),
|
||||
);
|
||||
if (!matched) {
|
||||
break;
|
||||
}
|
||||
decorators.push(line);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
decorators,
|
||||
cleanContent: cleanLines.join("\n").trim(),
|
||||
cleanContent: index > 0 ? lines.slice(index).join("\n") : rawContent,
|
||||
};
|
||||
}
|
||||
|
||||
function isSpecialEntryByComment(comment = "") {
|
||||
return SPECIAL_NAME_MARKERS.some((marker) =>
|
||||
String(comment).includes(marker),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeEntry(raw = {}, worldbookName = "") {
|
||||
const { decorators, cleanContent } = parseDecorators(raw.content || "");
|
||||
|
||||
@@ -494,27 +477,6 @@ function selectActivatedEntries(
|
||||
continue;
|
||||
}
|
||||
if (entry.decorators.includes("@@dont_activate")) continue;
|
||||
if (entry.decorators.includes("@@only_preload")) continue;
|
||||
|
||||
const specialDecorators = [
|
||||
"@@generate",
|
||||
"@@generate_before",
|
||||
"@@generate_after",
|
||||
"@@render",
|
||||
"@@render_before",
|
||||
"@@render_after",
|
||||
"@@initial_variables",
|
||||
"@@preprocessing",
|
||||
"@@iframe",
|
||||
];
|
||||
if (
|
||||
entry.decorators.some((decorator) =>
|
||||
specialDecorators.includes(decorator),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (isSpecialEntryByComment(entry.comment)) continue;
|
||||
|
||||
if (entry.keys.length === 0) continue;
|
||||
const matchedPrimary = entry.keys
|
||||
@@ -649,16 +611,52 @@ function selectActivatedEntries(
|
||||
return ungrouped.concat(matched).sort(sortEntries);
|
||||
}
|
||||
|
||||
async function collectAllWorldbookEntries() {
|
||||
async function loadNormalizedWorldbookEntries(worldbookHost, worldbookName) {
|
||||
const normalizedName = normalizeKey(worldbookName);
|
||||
if (!normalizedName || typeof worldbookHost?.getWorldbook !== "function") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = await worldbookHost.getWorldbook(normalizedName);
|
||||
let commentByUid = new Map();
|
||||
if (typeof worldbookHost?.getLorebookEntries === "function") {
|
||||
try {
|
||||
const loreEntries = await worldbookHost.getLorebookEntries(normalizedName);
|
||||
commentByUid = new Map(
|
||||
(Array.isArray(loreEntries) ? loreEntries : []).map((entry) => [
|
||||
entry.uid,
|
||||
String(entry.comment ?? ""),
|
||||
]),
|
||||
);
|
||||
} catch (error) {
|
||||
console.debug(
|
||||
`[ST-BME] task-worldinfo 读取 lorebook comment 失败: ${normalizedName}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (Array.isArray(entries) ? entries : []).map((entry) =>
|
||||
normalizeEntry(
|
||||
{
|
||||
...entry,
|
||||
comment: commentByUid.get(entry.uid) ?? entry.comment ?? "",
|
||||
},
|
||||
normalizedName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function collectAllWorldbookEntries(worldbookHost = null) {
|
||||
const resolvedWorldbookHost = worldbookHost || (await getWorldbookHost());
|
||||
const {
|
||||
getWorldbook,
|
||||
getLorebookEntries,
|
||||
getCharWorldbookNames,
|
||||
sourceLabel,
|
||||
fallback,
|
||||
capabilityStatus,
|
||||
snapshotRevision,
|
||||
} = await getWorldbookHost();
|
||||
} = resolvedWorldbookHost;
|
||||
const ctx = getStContext();
|
||||
const debug = {
|
||||
sourceLabel,
|
||||
@@ -777,36 +775,11 @@ async function collectAllWorldbookEntries() {
|
||||
loadedNames.add(normalizedName);
|
||||
|
||||
try {
|
||||
const entries = await getWorldbook(normalizedName);
|
||||
let commentByUid = new Map();
|
||||
if (getLorebookEntries) {
|
||||
try {
|
||||
const loreEntries = await getLorebookEntries(normalizedName);
|
||||
commentByUid = new Map(
|
||||
(Array.isArray(loreEntries) ? loreEntries : []).map((entry) => [
|
||||
entry.uid,
|
||||
String(entry.comment ?? ""),
|
||||
]),
|
||||
);
|
||||
} catch (error) {
|
||||
console.debug(
|
||||
`[ST-BME] task-worldinfo 读取 lorebook comment 失败: ${normalizedName} [${sourceTag}]`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of Array.isArray(entries) ? entries : []) {
|
||||
allEntries.push(
|
||||
normalizeEntry(
|
||||
{
|
||||
...entry,
|
||||
comment: commentByUid.get(entry.uid) ?? entry.comment ?? "",
|
||||
},
|
||||
normalizedName,
|
||||
),
|
||||
);
|
||||
}
|
||||
const entries = await loadNormalizedWorldbookEntries(
|
||||
resolvedWorldbookHost,
|
||||
normalizedName,
|
||||
);
|
||||
allEntries.push(...entries);
|
||||
} catch (error) {
|
||||
console.debug(
|
||||
`[ST-BME] task-worldinfo 读取世界书失败: ${normalizedName} [${sourceTag}]`,
|
||||
@@ -876,7 +849,6 @@ function normalizeResolvedEntry(entry = {}, fallbackIndex = 0) {
|
||||
...entry.activationDebug,
|
||||
}
|
||||
: null,
|
||||
controllerSource: String(entry.controllerSource || ""),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -956,6 +928,48 @@ function buildActivationSourceTexts({
|
||||
return uniq(texts.map((text) => String(text).trim()).filter(Boolean));
|
||||
}
|
||||
|
||||
function getEntryIdentity(entry = {}) {
|
||||
return `${entry.worldbook}:${entry.uid}:${entry.name}`;
|
||||
}
|
||||
|
||||
function toActivationMap(entries = []) {
|
||||
const map = new Map();
|
||||
for (const entry of Array.isArray(entries) ? entries : []) {
|
||||
map.set(getEntryIdentity(entry), entry);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function warnLegacyEntryNames(entries = [], warnings = []) {
|
||||
const legacyNames = uniq(
|
||||
(Array.isArray(entries) ? entries : [])
|
||||
.map((entry) => String(entry?.name || "").trim())
|
||||
.filter(
|
||||
(name) => name.startsWith("EW/Controller/") || name.startsWith("EW/Dyn/"),
|
||||
),
|
||||
);
|
||||
|
||||
if (legacyNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const warning =
|
||||
`检测到旧 EW 命名条目 (${legacyNames.join(", ")});这些条目现在只按普通世界书条目处理,不再有专用魔法行为`;
|
||||
if (!warnings.includes(warning)) {
|
||||
warnings.push(warning);
|
||||
}
|
||||
console.warn(`[ST-BME] task-worldinfo ${warning}`);
|
||||
}
|
||||
|
||||
function mergeActivationDebug(entry = {}, overrides = {}) {
|
||||
return {
|
||||
...(entry.activationDebug && typeof entry.activationDebug === "object"
|
||||
? entry.activationDebug
|
||||
: {}),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveTaskWorldInfo({
|
||||
settings = {},
|
||||
chatMessages = [],
|
||||
@@ -983,8 +997,13 @@ export async function resolveTaskWorldInfo({
|
||||
activatedEntryCount: 0,
|
||||
constantActivatedCount: 0,
|
||||
selectiveActivatedCount: 0,
|
||||
controllerActivatedCount: 0,
|
||||
controllerPulledCount: 0,
|
||||
ejsForcedActivationCount: 0,
|
||||
ejsInlinePullCount: 0,
|
||||
resolvePassCount: 0,
|
||||
forcedActivatedEntries: [],
|
||||
inlinePulledEntries: [],
|
||||
lazyLoadedWorldbooks: [],
|
||||
recursionWarnings: [],
|
||||
cache: {
|
||||
hit: false,
|
||||
key: "",
|
||||
@@ -1001,7 +1020,8 @@ export async function resolveTaskWorldInfo({
|
||||
};
|
||||
|
||||
try {
|
||||
const collected = await collectAllWorldbookEntries();
|
||||
const worldbookHost = await getWorldbookHost();
|
||||
const collected = await collectAllWorldbookEntries(worldbookHost);
|
||||
const allEntries = Array.isArray(collected?.entries) ? collected.entries : [];
|
||||
result.allEntries = allEntries;
|
||||
result.debug = {
|
||||
@@ -1021,6 +1041,7 @@ export async function resolveTaskWorldInfo({
|
||||
if (allEntries.length === 0) {
|
||||
return result;
|
||||
}
|
||||
warnLegacyEntryNames(allEntries, result.debug.warnings);
|
||||
|
||||
const triggerTexts = buildActivationSourceTexts({
|
||||
chatMessages,
|
||||
@@ -1038,91 +1059,195 @@ export async function resolveTaskWorldInfo({
|
||||
: String(ejsBackend.error)
|
||||
: "";
|
||||
|
||||
const activated = selectActivatedEntries(allEntries, trigger, {
|
||||
const normalizedTemplateContext = {
|
||||
...templateContext,
|
||||
user_input: userMessage || templateContext?.user_input || "",
|
||||
});
|
||||
result.debug.activatedEntryCount = activated.length;
|
||||
result.debug.constantActivatedCount = activated.filter(
|
||||
(entry) => entry.activationDebug?.mode === "constant",
|
||||
).length;
|
||||
result.debug.selectiveActivatedCount = activated.filter(
|
||||
(entry) => entry.activationDebug?.mode === "selective",
|
||||
).length;
|
||||
result.debug.controllerActivatedCount = activated.filter((entry) =>
|
||||
entry.name.startsWith(String(
|
||||
settings.worldInfoControllerEntryPrefix ||
|
||||
settings.controller_entry_prefix ||
|
||||
DEFAULT_CONTROLLER_ENTRY_PREFIX,
|
||||
)),
|
||||
).length;
|
||||
if (activated.length === 0) {
|
||||
};
|
||||
const initialActivated = selectActivatedEntries(
|
||||
allEntries,
|
||||
trigger,
|
||||
normalizedTemplateContext,
|
||||
);
|
||||
if (initialActivated.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const allActivated = toActivationMap(initialActivated);
|
||||
const aggregatedForcedEntries = new Map();
|
||||
const aggregatedInlineEntries = new Map();
|
||||
const recursionWarnings = new Set();
|
||||
const knownWorldbooks = new Set(
|
||||
allEntries.map((entry) => entry.worldbook).filter(Boolean),
|
||||
);
|
||||
const lazyLoadWorldbookEntries = async (worldbookName) => {
|
||||
const normalizedWorldbook = normalizeKey(worldbookName);
|
||||
if (!normalizedWorldbook || knownWorldbooks.has(normalizedWorldbook)) {
|
||||
return [];
|
||||
}
|
||||
const lazyEntries = await loadNormalizedWorldbookEntries(
|
||||
worldbookHost,
|
||||
normalizedWorldbook,
|
||||
);
|
||||
knownWorldbooks.add(normalizedWorldbook);
|
||||
return lazyEntries;
|
||||
};
|
||||
|
||||
const renderCtx = createTaskEjsRenderContext(
|
||||
allEntries.map((entry) => ({
|
||||
uid: entry.uid,
|
||||
name: entry.name,
|
||||
comment: entry.comment,
|
||||
content: entry.cleanContent || entry.content,
|
||||
worldbook: entry.worldbook,
|
||||
role: entry.role,
|
||||
position: entry.position,
|
||||
depth: entry.depth,
|
||||
order: entry.order,
|
||||
activationDebug: entry.activationDebug,
|
||||
})),
|
||||
{
|
||||
templateContext: {
|
||||
...templateContext,
|
||||
user_input: userMessage || templateContext?.user_input || "",
|
||||
},
|
||||
templateContext: normalizedTemplateContext,
|
||||
currentActivatedEntries: [...allActivated.values()],
|
||||
loadWorldbookEntries: lazyLoadWorldbookEntries,
|
||||
},
|
||||
);
|
||||
|
||||
const controllerPrefix =
|
||||
settings.worldInfoControllerEntryPrefix ||
|
||||
settings.controller_entry_prefix ||
|
||||
DEFAULT_CONTROLLER_ENTRY_PREFIX;
|
||||
const maxResolvePasses =
|
||||
Number.isFinite(Number(settings.worldInfoMaxResolvePasses)) &&
|
||||
Number(settings.worldInfoMaxResolvePasses) > 0
|
||||
? Number(settings.worldInfoMaxResolvePasses)
|
||||
: DEFAULT_MAX_RESOLVE_PASSES;
|
||||
|
||||
const beforeEntries = [];
|
||||
const afterEntries = [];
|
||||
const atDepthEntries = [];
|
||||
let resolvedIndex = 0;
|
||||
let finalResolvedEntries = [];
|
||||
let hitResolveCap = false;
|
||||
|
||||
for (const entry of activated) {
|
||||
renderCtx.pulledEntries.clear();
|
||||
for (let pass = 0; pass < maxResolvePasses; pass += 1) {
|
||||
result.debug.resolvePassCount = pass + 1;
|
||||
renderCtx.currentActivatedEntries = [...allActivated.values()];
|
||||
renderCtx.forcedActivatedEntries.clear();
|
||||
renderCtx.inlinePulledEntries.clear();
|
||||
renderCtx.warnings = [];
|
||||
finalResolvedEntries = [];
|
||||
resolvedIndex = 0;
|
||||
|
||||
const sourceContent = entry.cleanContent || entry.content;
|
||||
const isControllerEntry = entry.name.startsWith(String(controllerPrefix || ""));
|
||||
let renderedContent = sourceContent;
|
||||
try {
|
||||
renderedContent = await evalTaskEjsTemplate(sourceContent, renderCtx, {
|
||||
world_info: {
|
||||
comment: entry.comment || entry.name,
|
||||
name: entry.name,
|
||||
world: entry.worldbook,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
result.debug.warnings.push(
|
||||
error?.code === "st_bme_task_ejs_runtime_unavailable"
|
||||
? `世界书条目 ${entry.name} 依赖 EJS runtime,当前已跳过`
|
||||
: `世界书条目 ${entry.name} 渲染失败,已跳过`,
|
||||
);
|
||||
console.warn(
|
||||
`[ST-BME] task-worldinfo 渲染世界书条目失败: ${entry.name}`,
|
||||
error,
|
||||
);
|
||||
if (
|
||||
error?.code === "st_bme_task_ejs_runtime_unavailable" &&
|
||||
!result.debug.ejsLastError
|
||||
) {
|
||||
result.debug.ejsLastError =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
const activatedEntries = [...allActivated.values()].sort(sortEntries);
|
||||
|
||||
for (const entry of activatedEntries) {
|
||||
const sourceContent = entry.cleanContent || entry.content;
|
||||
let renderedContent = sourceContent;
|
||||
try {
|
||||
renderedContent = await evalTaskEjsTemplate(sourceContent, renderCtx, {
|
||||
world_info: {
|
||||
comment: entry.comment || entry.name,
|
||||
name: entry.name,
|
||||
world: entry.worldbook,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const warning =
|
||||
error?.code === "st_bme_task_ejs_unsupported_helper"
|
||||
? `世界书条目 ${entry.name} 调用了不支持的 helper: ${error.helperName}`
|
||||
: error?.code === "st_bme_task_ejs_runtime_unavailable"
|
||||
? `世界书条目 ${entry.name} 依赖 EJS runtime,当前已跳过`
|
||||
: `世界书条目 ${entry.name} 渲染失败,已跳过`;
|
||||
if (!result.debug.warnings.includes(warning)) {
|
||||
result.debug.warnings.push(warning);
|
||||
}
|
||||
console.warn(
|
||||
`[ST-BME] task-worldinfo 渲染世界书条目失败: ${entry.name}`,
|
||||
error,
|
||||
);
|
||||
if (
|
||||
error?.code === "st_bme_task_ejs_runtime_unavailable" &&
|
||||
!result.debug.ejsLastError
|
||||
) {
|
||||
result.debug.ejsLastError =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
renderedContent = "";
|
||||
}
|
||||
renderedContent = "";
|
||||
|
||||
for (const warning of renderCtx.warnings || []) {
|
||||
recursionWarnings.add(String(warning || ""));
|
||||
}
|
||||
|
||||
const trimmedContent = String(renderedContent || "").trim();
|
||||
if (!trimmedContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
finalResolvedEntries.push(
|
||||
normalizeResolvedEntry(
|
||||
{
|
||||
name: entry.comment || entry.name,
|
||||
sourceName: entry.name,
|
||||
worldbook: entry.worldbook,
|
||||
content: trimmedContent,
|
||||
role: entry.role,
|
||||
position: entry.position,
|
||||
depth: entry.depth,
|
||||
order: entry.order,
|
||||
activationDebug: entry.activationDebug,
|
||||
},
|
||||
resolvedIndex++,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isControllerEntry && !String(renderedContent || "").trim()) {
|
||||
continue;
|
||||
for (const pulledEntry of renderCtx.inlinePulledEntries.values()) {
|
||||
const key = `${pulledEntry.worldbook}:${pulledEntry.name}`;
|
||||
if (!aggregatedInlineEntries.has(key)) {
|
||||
aggregatedInlineEntries.set(key, {
|
||||
name: pulledEntry.comment || pulledEntry.name,
|
||||
sourceName: pulledEntry.name,
|
||||
worldbook: pulledEntry.worldbook,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let discoveredNewActivation = false;
|
||||
for (const forcedEntry of renderCtx.forcedActivatedEntries.values()) {
|
||||
const key = getEntryIdentity(forcedEntry);
|
||||
if (!aggregatedForcedEntries.has(key)) {
|
||||
aggregatedForcedEntries.set(key, {
|
||||
name: forcedEntry.comment || forcedEntry.name,
|
||||
sourceName: forcedEntry.name,
|
||||
worldbook: forcedEntry.worldbook,
|
||||
});
|
||||
}
|
||||
if (!allActivated.has(key)) {
|
||||
allActivated.set(key, {
|
||||
...forcedEntry,
|
||||
activationDebug: mergeActivationDebug(forcedEntry, {
|
||||
mode: "ejs-forced",
|
||||
}),
|
||||
});
|
||||
discoveredNewActivation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!discoveredNewActivation) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (pass + 1 >= maxResolvePasses) {
|
||||
hitResolveCap = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hitResolveCap) {
|
||||
const warning = `世界书 EJS 激活达到递归上限 ${maxResolvePasses},已停止继续展开`;
|
||||
if (!result.debug.warnings.includes(warning)) {
|
||||
result.debug.warnings.push(warning);
|
||||
}
|
||||
recursionWarnings.add(warning);
|
||||
}
|
||||
|
||||
for (const entry of finalResolvedEntries) {
|
||||
const bucketName = classifyPosition(entry);
|
||||
const bucket =
|
||||
bucketName === "before"
|
||||
@@ -1130,57 +1255,7 @@ export async function resolveTaskWorldInfo({
|
||||
: bucketName === "after"
|
||||
? afterEntries
|
||||
: atDepthEntries;
|
||||
|
||||
if (isControllerEntry) {
|
||||
for (const pulledEntry of renderCtx.pulledEntries.values()) {
|
||||
if (!String(pulledEntry.content || "").trim()) continue;
|
||||
if (
|
||||
pulledEntry.worldbook === entry.worldbook &&
|
||||
pulledEntry.name === entry.name
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
bucket.push(
|
||||
normalizeResolvedEntry(
|
||||
{
|
||||
name: pulledEntry.comment || pulledEntry.name,
|
||||
sourceName: pulledEntry.name,
|
||||
worldbook: pulledEntry.worldbook,
|
||||
content: pulledEntry.content,
|
||||
role: entry.role,
|
||||
position: entry.position,
|
||||
depth: entry.depth,
|
||||
order: entry.order,
|
||||
activationDebug: {
|
||||
...(entry.activationDebug || {}),
|
||||
mode: "controller-pulled",
|
||||
},
|
||||
controllerSource: entry.name,
|
||||
},
|
||||
resolvedIndex++,
|
||||
),
|
||||
);
|
||||
result.debug.controllerPulledCount += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
bucket.push(
|
||||
normalizeResolvedEntry(
|
||||
{
|
||||
name: entry.comment || entry.name,
|
||||
sourceName: entry.name,
|
||||
worldbook: entry.worldbook,
|
||||
content: renderedContent,
|
||||
role: entry.role,
|
||||
position: entry.position,
|
||||
depth: entry.depth,
|
||||
order: entry.order,
|
||||
activationDebug: entry.activationDebug,
|
||||
},
|
||||
resolvedIndex++,
|
||||
),
|
||||
);
|
||||
bucket.push(entry);
|
||||
}
|
||||
|
||||
result.beforeEntries = beforeEntries;
|
||||
@@ -1189,33 +1264,48 @@ export async function resolveTaskWorldInfo({
|
||||
result.beforeText = buildWorldInfoText(result.beforeEntries);
|
||||
result.afterText = buildWorldInfoText(result.afterEntries);
|
||||
result.additionalMessages = buildAdditionalMessages(result.atDepthEntries);
|
||||
result.debug.activatedEntryCount = allActivated.size;
|
||||
result.debug.constantActivatedCount = [...allActivated.values()].filter(
|
||||
(entry) => entry.activationDebug?.mode === "constant",
|
||||
).length;
|
||||
result.debug.selectiveActivatedCount = [...allActivated.values()].filter(
|
||||
(entry) =>
|
||||
entry.activationDebug?.mode === "selective" ||
|
||||
entry.activationDebug?.mode === "forced",
|
||||
).length;
|
||||
result.debug.ejsForcedActivationCount = aggregatedForcedEntries.size;
|
||||
result.debug.ejsInlinePullCount = aggregatedInlineEntries.size;
|
||||
result.debug.forcedActivatedEntries = [...aggregatedForcedEntries.values()];
|
||||
result.debug.inlinePulledEntries = [...aggregatedInlineEntries.values()];
|
||||
result.debug.lazyLoadedWorldbooks = [...renderCtx.lazyLoadedWorldbooks];
|
||||
result.debug.recursionWarnings = [...recursionWarnings];
|
||||
result.debug.resolvedEntries = [
|
||||
...result.beforeEntries.map((entry) => ({
|
||||
name: entry.name,
|
||||
bucket: "before",
|
||||
sourceName: entry.sourceName,
|
||||
worldbook: entry.worldbook,
|
||||
activationMode: entry.activationDebug?.mode || "",
|
||||
matchedPrimaryKey: entry.activationDebug?.matchedPrimaryKey || "",
|
||||
matchedSecondaryKeys: entry.activationDebug?.matchedSecondaryKeys || [],
|
||||
controllerSource: entry.controllerSource || "",
|
||||
})),
|
||||
...result.afterEntries.map((entry) => ({
|
||||
name: entry.name,
|
||||
bucket: "after",
|
||||
sourceName: entry.sourceName,
|
||||
worldbook: entry.worldbook,
|
||||
activationMode: entry.activationDebug?.mode || "",
|
||||
matchedPrimaryKey: entry.activationDebug?.matchedPrimaryKey || "",
|
||||
matchedSecondaryKeys: entry.activationDebug?.matchedSecondaryKeys || [],
|
||||
controllerSource: entry.controllerSource || "",
|
||||
})),
|
||||
...result.atDepthEntries.map((entry) => ({
|
||||
name: entry.name,
|
||||
bucket: "atDepth",
|
||||
sourceName: entry.sourceName,
|
||||
worldbook: entry.worldbook,
|
||||
activationMode: entry.activationDebug?.mode || "",
|
||||
matchedPrimaryKey: entry.activationDebug?.matchedPrimaryKey || "",
|
||||
matchedSecondaryKeys: entry.activationDebug?.matchedSecondaryKeys || [],
|
||||
controllerSource: entry.controllerSource || "",
|
||||
})),
|
||||
];
|
||||
result.activatedEntryNames = uniq(
|
||||
@@ -1223,8 +1313,8 @@ export async function resolveTaskWorldInfo({
|
||||
...result.beforeEntries.map((entry) => entry.name),
|
||||
...result.afterEntries.map((entry) => entry.name),
|
||||
...result.atDepthEntries.map((entry) => entry.name),
|
||||
...[...renderCtx.activatedEntries.values()].map(
|
||||
(entry) => entry.comment || entry.name,
|
||||
...[...aggregatedForcedEntries.values()].map(
|
||||
(entry) => entry.name || entry.sourceName,
|
||||
),
|
||||
].filter(Boolean),
|
||||
);
|
||||
|
||||
@@ -129,6 +129,12 @@ try {
|
||||
if (template === "<% broken") {
|
||||
throw new Error("Unexpected end of input");
|
||||
}
|
||||
if (template === "<% await execute() %>") {
|
||||
return async function compiled(locals) {
|
||||
await locals.execute();
|
||||
return "";
|
||||
};
|
||||
}
|
||||
return async function compiled(locals) {
|
||||
return [
|
||||
locals.charName,
|
||||
@@ -167,6 +173,13 @@ try {
|
||||
);
|
||||
assert.deepEqual(compileCalls, ["<%= 1 %>", "<%= 1 %>"]);
|
||||
|
||||
await assert.rejects(
|
||||
() => evalTaskEjsTemplate("<% await execute() %>", renderCtx),
|
||||
(error) =>
|
||||
error?.code === "st_bme_task_ejs_unsupported_helper" &&
|
||||
error?.helperName === "execute",
|
||||
);
|
||||
|
||||
const syntaxError = await checkTaskEjsSyntax("<% broken");
|
||||
assert.equal(syntaxError, "Unexpected end of input");
|
||||
|
||||
|
||||
@@ -30,113 +30,145 @@ const originalGetCharWorldbookNames = globalThis.getCharWorldbookNames;
|
||||
const originalGetWorldbook = globalThis.getWorldbook;
|
||||
const originalGetLorebookEntries = globalThis.getLorebookEntries;
|
||||
|
||||
const constantEntry = {
|
||||
uid: 1,
|
||||
name: "常驻设定",
|
||||
comment: "常驻设定",
|
||||
content: "这里是常驻世界设定。",
|
||||
enabled: true,
|
||||
position: {
|
||||
type: "before_character_definition",
|
||||
role: "system",
|
||||
depth: 0,
|
||||
order: 10,
|
||||
},
|
||||
strategy: {
|
||||
type: "constant",
|
||||
keys: [],
|
||||
keys_secondary: { logic: "and_any", keys: [] },
|
||||
},
|
||||
probability: 100,
|
||||
extra: {},
|
||||
};
|
||||
|
||||
const dynEntry = {
|
||||
uid: 2,
|
||||
name: "Dyn/线索",
|
||||
comment: "线索条目",
|
||||
content: "隐藏线索:<%= charName %> 正在调查。",
|
||||
enabled: false,
|
||||
position: {
|
||||
type: "before_character_definition",
|
||||
role: "system",
|
||||
depth: 0,
|
||||
order: 20,
|
||||
},
|
||||
strategy: {
|
||||
type: "selective",
|
||||
keys: ["调查"],
|
||||
keys_secondary: { logic: "and_any", keys: [] },
|
||||
},
|
||||
probability: 100,
|
||||
extra: {},
|
||||
};
|
||||
|
||||
const controllerEntry = {
|
||||
uid: 3,
|
||||
name: "EW/Controller/Main",
|
||||
comment: "控制器",
|
||||
content: '<%= await getwi("Dyn/线索") %>',
|
||||
enabled: true,
|
||||
position: {
|
||||
type: "before_character_definition",
|
||||
role: "system",
|
||||
depth: 0,
|
||||
order: 30,
|
||||
},
|
||||
strategy: {
|
||||
type: "constant",
|
||||
keys: [],
|
||||
keys_secondary: { logic: "and_any", keys: [] },
|
||||
},
|
||||
probability: 100,
|
||||
extra: {},
|
||||
};
|
||||
|
||||
const atDepthEntry = {
|
||||
uid: 4,
|
||||
name: "深度注入",
|
||||
comment: "深度注入",
|
||||
content: "这是一条 atDepth 消息。",
|
||||
enabled: true,
|
||||
position: {
|
||||
type: "at_depth_as_system",
|
||||
role: "system",
|
||||
depth: 2,
|
||||
order: 5,
|
||||
},
|
||||
strategy: {
|
||||
type: "constant",
|
||||
keys: [],
|
||||
keys_secondary: { logic: "and_any", keys: [] },
|
||||
},
|
||||
probability: 100,
|
||||
extra: {},
|
||||
};
|
||||
|
||||
function createConstantWorldbookEntry(uid, name, content, comment = "") {
|
||||
function createWorldbookEntry({
|
||||
uid,
|
||||
name,
|
||||
comment = name,
|
||||
content,
|
||||
enabled = true,
|
||||
positionType = "before_character_definition",
|
||||
role = "system",
|
||||
depth = 0,
|
||||
order = 10,
|
||||
strategyType = "constant",
|
||||
keys = [],
|
||||
keysSecondary = [],
|
||||
}) {
|
||||
return {
|
||||
uid,
|
||||
name,
|
||||
comment,
|
||||
content,
|
||||
enabled: true,
|
||||
enabled,
|
||||
position: {
|
||||
type: "before_character_definition",
|
||||
role: "system",
|
||||
depth: 0,
|
||||
order: 10,
|
||||
type: positionType,
|
||||
role,
|
||||
depth,
|
||||
order,
|
||||
},
|
||||
strategy: {
|
||||
type: "constant",
|
||||
keys: [],
|
||||
keys_secondary: { logic: "and_any", keys: [] },
|
||||
type: strategyType,
|
||||
keys,
|
||||
keys_secondary: { logic: "and_any", keys: keysSecondary },
|
||||
},
|
||||
probability: 100,
|
||||
extra: {},
|
||||
};
|
||||
}
|
||||
|
||||
function createConstantWorldbookEntry(uid, name, content, comment = name) {
|
||||
return createWorldbookEntry({
|
||||
uid,
|
||||
name,
|
||||
comment,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
const constantEntry = createWorldbookEntry({
|
||||
uid: 1,
|
||||
name: "常驻设定",
|
||||
comment: "常驻设定",
|
||||
content: "这里是常驻世界设定。",
|
||||
order: 10,
|
||||
});
|
||||
|
||||
const dynEntry = createWorldbookEntry({
|
||||
uid: 2,
|
||||
name: "EW/Dyn/线索",
|
||||
comment: "线索条目",
|
||||
content: "隐藏线索:<%= charName %> 正在调查。",
|
||||
enabled: false,
|
||||
strategyType: "selective",
|
||||
keys: ["调查"],
|
||||
order: 15,
|
||||
});
|
||||
|
||||
const inlineSummaryEntry = createWorldbookEntry({
|
||||
uid: 3,
|
||||
name: "普通 EJS 汇总",
|
||||
comment: "EJS 汇总",
|
||||
content: '控制摘要:<%= await getwi("EW/Dyn/线索") %>',
|
||||
order: 20,
|
||||
});
|
||||
|
||||
const extensionLiteralEntry = createWorldbookEntry({
|
||||
uid: 4,
|
||||
name: "扩展语义正文",
|
||||
comment: "扩展语义正文",
|
||||
content: "@@generate\n[GENERATE:Test]\n扩展语义只是普通文本。",
|
||||
order: 25,
|
||||
});
|
||||
|
||||
const externalInlineEntry = createWorldbookEntry({
|
||||
uid: 5,
|
||||
name: "外部书汇总",
|
||||
comment: "外部书汇总",
|
||||
content: '外部补充:<%= await getwi("bonus-book", "Bonus 条目") %>',
|
||||
order: 26,
|
||||
});
|
||||
|
||||
const forceControlEntry = createWorldbookEntry({
|
||||
uid: 6,
|
||||
name: "普通 EJS 控制",
|
||||
comment: "EJS 控制",
|
||||
content: '<% await activewi("强制 after") %>',
|
||||
order: 30,
|
||||
});
|
||||
|
||||
const forcedAfterEntry = createWorldbookEntry({
|
||||
uid: 7,
|
||||
name: "强制 after",
|
||||
comment: "强制后置",
|
||||
content: "这是被 EJS 强制激活的后置条目。",
|
||||
positionType: "after_character_definition",
|
||||
strategyType: "selective",
|
||||
keys: ["永远不会命中"],
|
||||
order: 40,
|
||||
});
|
||||
|
||||
const atDepthEntry = createWorldbookEntry({
|
||||
uid: 8,
|
||||
name: "深度注入",
|
||||
comment: "深度注入",
|
||||
content: "这是一条 atDepth 消息。",
|
||||
positionType: "at_depth_as_system",
|
||||
depth: 2,
|
||||
order: 5,
|
||||
});
|
||||
|
||||
const bonusEntry = createWorldbookEntry({
|
||||
uid: 101,
|
||||
name: "Bonus 条目",
|
||||
comment: "Bonus 条目",
|
||||
content: "来自 bonus-book 的补充内容。",
|
||||
order: 10,
|
||||
});
|
||||
|
||||
const worldbooksByName = {
|
||||
"main-book": [
|
||||
constantEntry,
|
||||
dynEntry,
|
||||
inlineSummaryEntry,
|
||||
extensionLiteralEntry,
|
||||
externalInlineEntry,
|
||||
forceControlEntry,
|
||||
forcedAfterEntry,
|
||||
atDepthEntry,
|
||||
],
|
||||
"bonus-book": [bonusEntry],
|
||||
};
|
||||
|
||||
try {
|
||||
globalThis.SillyTavern = {
|
||||
getContext() {
|
||||
@@ -153,13 +185,13 @@ try {
|
||||
primary: "main-book",
|
||||
additional: [],
|
||||
});
|
||||
globalThis.getWorldbook = async () => [
|
||||
constantEntry,
|
||||
dynEntry,
|
||||
controllerEntry,
|
||||
atDepthEntry,
|
||||
];
|
||||
globalThis.getLorebookEntries = async () => [];
|
||||
globalThis.getWorldbook = async (worldbookName) =>
|
||||
worldbooksByName[worldbookName] || [];
|
||||
globalThis.getLorebookEntries = async (worldbookName) =>
|
||||
(worldbooksByName[worldbookName] || []).map((entry) => ({
|
||||
uid: entry.uid,
|
||||
comment: entry.comment,
|
||||
}));
|
||||
|
||||
const { resolveTaskWorldInfo } = await import("../task-worldinfo.js");
|
||||
const { buildTaskPrompt } = await import("../prompt-builder.js");
|
||||
@@ -185,14 +217,30 @@ try {
|
||||
|
||||
assert.deepEqual(
|
||||
worldInfo.beforeEntries.map((entry) => entry.name),
|
||||
["常驻设定", "线索条目"],
|
||||
["常驻设定", "EJS 汇总", "扩展语义正文", "外部书汇总"],
|
||||
);
|
||||
assert.doesNotMatch(worldInfo.beforeText, /getwi|<%=?/);
|
||||
assert.equal(worldInfo.debug.controllerPulledCount, 1);
|
||||
assert.deepEqual(worldInfo.afterEntries.map((entry) => entry.name), ["强制后置"]);
|
||||
assert.equal(worldInfo.additionalMessages.length, 1);
|
||||
assert.equal(worldInfo.additionalMessages[0].content, "这是一条 atDepth 消息。");
|
||||
assert.match(worldInfo.beforeText, /控制摘要:隐藏线索:Alice 正在调查。/);
|
||||
assert.match(worldInfo.beforeText, /外部补充:来自 bonus-book 的补充内容。/);
|
||||
assert.match(worldInfo.beforeText, /@@generate/);
|
||||
assert.match(worldInfo.beforeText, /\[GENERATE:Test\]/);
|
||||
assert.doesNotMatch(worldInfo.beforeText, /getwi|<%=?/);
|
||||
assert.equal(worldInfo.debug.ejsInlinePullCount, 2);
|
||||
assert.equal(worldInfo.debug.ejsForcedActivationCount, 1);
|
||||
assert.equal(worldInfo.debug.resolvePassCount >= 2, true);
|
||||
assert.deepEqual(worldInfo.debug.forcedActivatedEntries.map((entry) => entry.name), [
|
||||
"强制后置",
|
||||
]);
|
||||
assert.deepEqual(
|
||||
worldInfo.debug.inlinePulledEntries.map((entry) => entry.name).sort(),
|
||||
["Bonus 条目", "线索条目"].sort(),
|
||||
);
|
||||
assert.deepEqual(worldInfo.debug.lazyLoadedWorldbooks, ["bonus-book"]);
|
||||
assert.equal(
|
||||
worldInfo.additionalMessages[0].content,
|
||||
"这是一条 atDepth 消息。",
|
||||
worldInfo.debug.warnings.some((warning) => warning.includes("旧 EW 命名条目")),
|
||||
true,
|
||||
);
|
||||
|
||||
const settings = {
|
||||
@@ -217,11 +265,20 @@ try {
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
type: "builtin",
|
||||
sourceKey: "worldInfoAfter",
|
||||
role: "system",
|
||||
enabled: true,
|
||||
order: 1,
|
||||
injectionMode: "append",
|
||||
},
|
||||
{
|
||||
id: "b3",
|
||||
type: "custom",
|
||||
content: "角色: {{charName}}",
|
||||
role: "user",
|
||||
enabled: true,
|
||||
order: 1,
|
||||
order: 2,
|
||||
injectionMode: "append",
|
||||
},
|
||||
],
|
||||
@@ -239,7 +296,10 @@ try {
|
||||
});
|
||||
|
||||
assert.match(promptBuild.systemPrompt, /这里是常驻世界设定/);
|
||||
assert.match(promptBuild.systemPrompt, /隐藏线索:Alice 正在调查/);
|
||||
assert.match(promptBuild.systemPrompt, /控制摘要:隐藏线索:Alice 正在调查/);
|
||||
assert.match(promptBuild.systemPrompt, /扩展语义只是普通文本/);
|
||||
assert.match(promptBuild.systemPrompt, /来自 bonus-book 的补充内容/);
|
||||
assert.doesNotMatch(promptBuild.systemPrompt, /getwi|<%=?/);
|
||||
assert.equal(
|
||||
promptBuild.privateTaskMessages.length,
|
||||
2,
|
||||
@@ -251,41 +311,44 @@ try {
|
||||
);
|
||||
assert.deepEqual(
|
||||
promptBuild.hostInjections.before.map((entry) => entry.name),
|
||||
["常驻设定", "线索条目"],
|
||||
["常驻设定", "EJS 汇总", "扩展语义正文", "外部书汇总"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
promptBuild.hostInjections.after.map((entry) => entry.name),
|
||||
["强制后置"],
|
||||
);
|
||||
assert.equal(promptBuild.hostInjections.atDepth.length, 1);
|
||||
assert.equal(promptBuild.hostInjections.atDepth[0].depth, 2);
|
||||
assert.equal(promptBuild.hostInjectionPlan.before.length, 1);
|
||||
assert.equal(promptBuild.hostInjectionPlan.before[0].blockId, "b1");
|
||||
assert.equal(promptBuild.hostInjectionPlan.before[0].sourceKey, "worldInfoBefore");
|
||||
assert.deepEqual(promptBuild.hostInjectionPlan.before[0].entryNames, [
|
||||
"常驻设定",
|
||||
"线索条目",
|
||||
"EJS 汇总",
|
||||
"扩展语义正文",
|
||||
"外部书汇总",
|
||||
]);
|
||||
assert.equal(promptBuild.hostInjections.after.length, 0);
|
||||
assert.equal(promptBuild.hostInjections.atDepth.length, 1);
|
||||
assert.equal(promptBuild.hostInjections.atDepth[0].depth, 2);
|
||||
assert.equal(promptBuild.hostInjectionPlan.after.length, 1);
|
||||
assert.equal(promptBuild.hostInjectionPlan.after[0].blockId, "b2");
|
||||
assert.equal(promptBuild.hostInjectionPlan.after[0].sourceKey, "worldInfoAfter");
|
||||
assert.deepEqual(promptBuild.hostInjectionPlan.after[0].entryNames, ["强制后置"]);
|
||||
assert.equal(promptBuild.hostInjectionPlan.atDepth.length, 1);
|
||||
assert.equal(promptBuild.hostInjectionPlan.atDepth[0].entryName, "深度注入");
|
||||
assert.equal(typeof promptBuild.debug.worldInfoCacheHit, "boolean");
|
||||
assert.doesNotMatch(promptBuild.systemPrompt, /getwi|<%=?/);
|
||||
assert.deepEqual(
|
||||
promptBuild.renderedBlocks.map((block) => block.delivery),
|
||||
["host.before", "private.message"],
|
||||
["host.before", "host.after", "private.message"],
|
||||
);
|
||||
assert.equal(promptBuild.additionalMessages.length, 1);
|
||||
assert.equal(
|
||||
promptBuild.additionalMessages[0].content,
|
||||
"这是一条 atDepth 消息。",
|
||||
);
|
||||
assert.equal(promptBuild.additionalMessages[0].content, "这是一条 atDepth 消息。");
|
||||
|
||||
const { initializeHostAdapter } = await import("../host-adapter/index.js");
|
||||
const partialBridgeCalls = [];
|
||||
const partialBridgeEntriesByWorldbook = {
|
||||
"main-book": [createConstantWorldbookEntry(11, "主书原名", "主书内容。")],
|
||||
"side-book": [createConstantWorldbookEntry(12, "支线原名", "支线内容。")],
|
||||
"persona-book": [
|
||||
createConstantWorldbookEntry(13, "人格原名", "人格内容。"),
|
||||
],
|
||||
"chat-book": [createConstantWorldbookEntry(14, "聊天原名", "聊天内容。")],
|
||||
"main-book": [createConstantWorldbookEntry(11, "主书原名", "主书内容。", "主书注释")],
|
||||
"side-book": [createConstantWorldbookEntry(12, "支线原名", "支线内容。", "支线注释")],
|
||||
"persona-book": [createConstantWorldbookEntry(13, "人格原名", "人格内容。", "人格注释")],
|
||||
"chat-book": [createConstantWorldbookEntry(14, "聊天原名", "聊天内容。", "聊天注释")],
|
||||
};
|
||||
|
||||
globalThis.SillyTavern = {
|
||||
@@ -313,12 +376,10 @@ try {
|
||||
);
|
||||
};
|
||||
globalThis.getLorebookEntries = async (worldbookName) =>
|
||||
({
|
||||
"main-book": [{ uid: 11, comment: "主书注释" }],
|
||||
"side-book": [{ uid: 12, comment: "支线注释" }],
|
||||
"persona-book": [{ uid: 13, comment: "人格注释" }],
|
||||
"chat-book": [{ uid: 14, comment: "聊天注释" }],
|
||||
})[worldbookName] || [];
|
||||
(partialBridgeEntriesByWorldbook[worldbookName] || []).map((entry) => ({
|
||||
uid: entry.uid,
|
||||
comment: entry.comment,
|
||||
}));
|
||||
|
||||
initializeHostAdapter({
|
||||
worldbookProvider: {
|
||||
|
||||
Reference in New Issue
Block a user