mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
feat: MVU规则模块+世界书MVU过滤+prompt组装MVU清洗+端到端测试
This commit is contained in:
4
llm.js
4
llm.js
@@ -256,6 +256,10 @@ function buildPromptExecutionSummary(debugContext = null) {
|
||||
debugContext.worldInfo && typeof debugContext.worldInfo === "object"
|
||||
? cloneRuntimeDebugValue(debugContext.worldInfo, {})
|
||||
: null,
|
||||
mvu:
|
||||
debugContext.mvu && typeof debugContext.mvu === "object"
|
||||
? cloneRuntimeDebugValue(debugContext.mvu, {})
|
||||
: null,
|
||||
regexInput: normalizeRegexDebugEntries(debugContext.regexInput),
|
||||
};
|
||||
}
|
||||
|
||||
188
mvu-compat.js
Normal file
188
mvu-compat.js
Normal file
@@ -0,0 +1,188 @@
|
||||
// ST-BME: MVU (MagVarUpdate) compatibility helpers for private task prompts.
|
||||
// These rules are intentionally narrow so we strip MVU artifacts without
|
||||
// disturbing normal prompt or world info content.
|
||||
|
||||
export const MVU_ENTRY_COMMENT_REGEX = /\[(mvu_update|mvu_plot|initvar)\]/i;
|
||||
|
||||
const MVU_UPDATE_BLOCK_REGEX =
|
||||
/\n?<(update(?:variable)?|variableupdate)>(?:(?!<\1>).)*?<\/\1>/gis;
|
||||
const MVU_STATUS_PLACEHOLDER_REGEX = /\n?<StatusPlaceHolderImpl\/>/gi;
|
||||
const MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX =
|
||||
/\n?<status_current_variables?>[\s\S]*?<\/status_current_variables?>/gi;
|
||||
const MVU_STATUS_CURRENT_VARIABLE_DETECT_REGEX =
|
||||
/<status_current_variables?>[\s\S]*?<\/status_current_variables?>/i;
|
||||
const MVU_VARIABLE_OUTPUT_ENTRY_REGEX = /变量输出格式:\s*[\s\S]*?<UpdateVariable>/i;
|
||||
const MVU_VARIABLE_RULES_ENTRY_REGEX =
|
||||
/变量更新规则:\s*[\s\S]*?(?:type:\s*|check:\s*|当前时间:|近期事务:)/i;
|
||||
const MVU_FORMAT_EMPHASIS_ENTRY_REGEX =
|
||||
/(?:变量输出格式强调|格式强调[::]?-?变量更新规则|格式强调[::]?-?剧情演绎|The following must be inserted to the end of (?:each )?reply,? and cannot be omitted)[\s\S]*?format:\s*\|-?/i;
|
||||
|
||||
function uniq(values = []) {
|
||||
return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))];
|
||||
}
|
||||
|
||||
function normalizeText(value = "") {
|
||||
return String(value || "").replace(/\r\n/g, "\n");
|
||||
}
|
||||
|
||||
function collapseWhitespace(value = "") {
|
||||
return String(value || "")
|
||||
.replace(/[ \t]{2,}/g, " ")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function countRegexMatches(text = "", regex) {
|
||||
if (!text || !(regex instanceof RegExp)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const source = new RegExp(regex.source, regex.flags);
|
||||
let count = 0;
|
||||
while (source.exec(text)) {
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function stripMvuPromptArtifactsDetailed(content = "") {
|
||||
const input = normalizeText(content);
|
||||
if (!input) {
|
||||
return {
|
||||
text: "",
|
||||
changed: false,
|
||||
artifactRemovedCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const artifactRemovedCount =
|
||||
countRegexMatches(input, MVU_UPDATE_BLOCK_REGEX) +
|
||||
countRegexMatches(input, MVU_STATUS_PLACEHOLDER_REGEX) +
|
||||
countRegexMatches(input, MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX);
|
||||
|
||||
const stripped = input
|
||||
.replace(MVU_UPDATE_BLOCK_REGEX, "")
|
||||
.replace(MVU_STATUS_PLACEHOLDER_REGEX, "")
|
||||
.replace(MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX, "");
|
||||
|
||||
const normalized = collapseWhitespace(stripped);
|
||||
return {
|
||||
text: normalized,
|
||||
changed: normalized !== collapseWhitespace(input),
|
||||
artifactRemovedCount,
|
||||
};
|
||||
}
|
||||
|
||||
function stripBlockedPromptContentsDetailed(content = "", blockedContents = []) {
|
||||
const input = normalizeText(content);
|
||||
const normalizedBlocked = uniq(
|
||||
(Array.isArray(blockedContents) ? blockedContents : [])
|
||||
.map((item) => collapseWhitespace(item))
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => right.length - left.length),
|
||||
);
|
||||
|
||||
if (!input || normalizedBlocked.length === 0) {
|
||||
return {
|
||||
text: collapseWhitespace(input),
|
||||
changed: false,
|
||||
blockedHitCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let output = input;
|
||||
let blockedHitCount = 0;
|
||||
for (const blocked of normalizedBlocked) {
|
||||
let index = output.indexOf(blocked);
|
||||
while (index >= 0) {
|
||||
blockedHitCount += 1;
|
||||
output = `${output.slice(0, index)}${output.slice(index + blocked.length)}`;
|
||||
index = output.indexOf(blocked);
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = collapseWhitespace(output);
|
||||
return {
|
||||
text: normalized,
|
||||
changed: normalized !== collapseWhitespace(input),
|
||||
blockedHitCount,
|
||||
};
|
||||
}
|
||||
|
||||
export function isMvuTaggedWorldInfoComment(comment = "") {
|
||||
return MVU_ENTRY_COMMENT_REGEX.test(String(comment || ""));
|
||||
}
|
||||
|
||||
export function isMvuTaggedWorldInfoNameOrComment(name = "", comment = "") {
|
||||
return (
|
||||
MVU_ENTRY_COMMENT_REGEX.test(String(name || "")) ||
|
||||
MVU_ENTRY_COMMENT_REGEX.test(String(comment || ""))
|
||||
);
|
||||
}
|
||||
|
||||
export function isLikelyMvuWorldInfoContent(content = "") {
|
||||
const normalized = collapseWhitespace(content);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
MVU_STATUS_CURRENT_VARIABLE_DETECT_REGEX.test(normalized) ||
|
||||
MVU_VARIABLE_OUTPUT_ENTRY_REGEX.test(normalized) ||
|
||||
MVU_VARIABLE_RULES_ENTRY_REGEX.test(normalized) ||
|
||||
MVU_FORMAT_EMPHASIS_ENTRY_REGEX.test(normalized)
|
||||
);
|
||||
}
|
||||
|
||||
export function stripMvuPromptArtifacts(content = "") {
|
||||
return stripMvuPromptArtifactsDetailed(content).text;
|
||||
}
|
||||
|
||||
export function stripBlockedPromptContents(content = "", blockedContents = []) {
|
||||
return stripBlockedPromptContentsDetailed(content, blockedContents).text;
|
||||
}
|
||||
|
||||
export function sanitizeMvuContent(
|
||||
content = "",
|
||||
{ mode = "aggressive", blockedContents = [] } = {},
|
||||
) {
|
||||
const originalText = normalizeText(content);
|
||||
const originalCollapsed = collapseWhitespace(originalText);
|
||||
const sanitizedMode = String(mode || "aggressive").trim().toLowerCase();
|
||||
|
||||
const artifactResult = stripMvuPromptArtifactsDetailed(originalCollapsed);
|
||||
const blockedResult = stripBlockedPromptContentsDetailed(
|
||||
artifactResult.text,
|
||||
blockedContents,
|
||||
);
|
||||
|
||||
const reasons = [];
|
||||
if (artifactResult.artifactRemovedCount > 0) {
|
||||
reasons.push("artifact_stripped");
|
||||
}
|
||||
if (blockedResult.blockedHitCount > 0) {
|
||||
reasons.push("blocked_content_removed");
|
||||
}
|
||||
|
||||
let text = blockedResult.text;
|
||||
let dropped = false;
|
||||
if (sanitizedMode === "aggressive") {
|
||||
if (
|
||||
isLikelyMvuWorldInfoContent(originalCollapsed) ||
|
||||
isLikelyMvuWorldInfoContent(text)
|
||||
) {
|
||||
text = "";
|
||||
dropped = true;
|
||||
reasons.push("likely_mvu_content");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text: collapseWhitespace(text),
|
||||
changed: collapseWhitespace(text) !== originalCollapsed,
|
||||
dropped,
|
||||
reasons: uniq(reasons),
|
||||
blockedHitCount: blockedResult.blockedHitCount,
|
||||
artifactRemovedCount: artifactResult.artifactRemovedCount,
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
// 统一负责任务预设块排序、变量渲染,以及世界书/EJS 上下文接入。
|
||||
|
||||
import { getActiveTaskProfile, getLegacyPromptForTask } from "./prompt-profiles.js";
|
||||
import { sanitizeMvuContent } from "./mvu-compat.js";
|
||||
import { resolveTaskWorldInfo } from "./task-worldinfo.js";
|
||||
import { applyTaskRegex } from "./task-regex.js";
|
||||
|
||||
const WORLD_INFO_VARIABLE_KEYS = [
|
||||
"worldInfoBefore",
|
||||
@@ -14,6 +16,34 @@ const WORLD_INFO_VARIABLE_KEYS = [
|
||||
"taskAdditionalMessages",
|
||||
];
|
||||
|
||||
const INPUT_CONTEXT_MVU_FIELDS = [
|
||||
"userMessage",
|
||||
"recentMessages",
|
||||
"dialogueText",
|
||||
"candidateText",
|
||||
"candidateNodes",
|
||||
"nodeContent",
|
||||
"eventSummary",
|
||||
"characterSummary",
|
||||
"threadSummary",
|
||||
"contradictionSummary",
|
||||
"charDescription",
|
||||
"userPersona",
|
||||
];
|
||||
|
||||
const INPUT_REGEX_STAGE_BY_FIELD = {
|
||||
userMessage: "input.userMessage",
|
||||
recentMessages: "input.recentMessages",
|
||||
dialogueText: "input.recentMessages",
|
||||
candidateText: "input.candidateText",
|
||||
candidateNodes: "input.candidateText",
|
||||
nodeContent: "input.candidateText",
|
||||
eventSummary: "input.candidateText",
|
||||
characterSummary: "input.candidateText",
|
||||
threadSummary: "input.candidateText",
|
||||
contradictionSummary: "input.candidateText",
|
||||
};
|
||||
|
||||
function cloneRuntimeDebugValue(value, fallback = null) {
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
@@ -53,6 +83,19 @@ function recordTaskPromptBuild(taskType, snapshot = {}) {
|
||||
state.updatedAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
function mergeRegexCollectors(...collectors) {
|
||||
const mergedEntries = [];
|
||||
for (const collector of collectors) {
|
||||
if (!Array.isArray(collector?.entries)) {
|
||||
continue;
|
||||
}
|
||||
mergedEntries.push(...collector.entries);
|
||||
}
|
||||
return {
|
||||
entries: mergedEntries,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTaskExecutionDebugContext(
|
||||
promptBuild = null,
|
||||
options = {},
|
||||
@@ -102,10 +145,20 @@ export function buildTaskExecutionDebugContext(
|
||||
atDepthCount: Number(promptDebug.worldInfoAtDepthCount || 0),
|
||||
loadMs: Number(worldInfoDebug.loadMs || 0),
|
||||
},
|
||||
regexInput:
|
||||
options.regexInput && typeof options.regexInput === "object"
|
||||
? cloneRuntimeDebugValue(options.regexInput, {})
|
||||
mvu:
|
||||
promptDebug.mvu && typeof promptDebug.mvu === "object"
|
||||
? cloneRuntimeDebugValue(promptDebug.mvu, {})
|
||||
: null,
|
||||
regexInput:
|
||||
(() => {
|
||||
const merged = mergeRegexCollectors(
|
||||
promptBuild?.regexInput,
|
||||
options.regexInput,
|
||||
);
|
||||
return Array.isArray(merged.entries) && merged.entries.length > 0
|
||||
? cloneRuntimeDebugValue(merged, {})
|
||||
: null;
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -175,6 +228,317 @@ function buildEmptyWorldInfoContext() {
|
||||
};
|
||||
}
|
||||
|
||||
function createEmptyMvuPromptDebug() {
|
||||
return {
|
||||
sanitizedFieldCount: 0,
|
||||
sanitizedFields: [],
|
||||
finalMessageStripCount: 0,
|
||||
worldInfoBlockedContentHits: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function pushMvuPromptDebugEntry(debugState, entry = {}) {
|
||||
if (!debugState || !entry || (!entry.changed && !entry.dropped)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugState.sanitizedFields.push({
|
||||
name: String(entry.name || ""),
|
||||
stage: String(entry.stage || ""),
|
||||
changed: Boolean(entry.changed),
|
||||
dropped: Boolean(entry.dropped),
|
||||
reasons: Array.isArray(entry.reasons) ? [...entry.reasons] : [],
|
||||
blockedHitCount: Number(entry.blockedHitCount || 0),
|
||||
});
|
||||
debugState.sanitizedFieldCount = debugState.sanitizedFields.length;
|
||||
}
|
||||
|
||||
function sanitizeTaskPromptText(
|
||||
settings = {},
|
||||
taskType,
|
||||
text,
|
||||
{
|
||||
mode = "aggressive",
|
||||
blockedContents = [],
|
||||
regexStage = "",
|
||||
role = "system",
|
||||
regexCollector = null,
|
||||
} = {},
|
||||
) {
|
||||
const originalText = typeof text === "string" ? text : "";
|
||||
const mvuResult = sanitizeMvuContent(originalText, {
|
||||
mode,
|
||||
blockedContents,
|
||||
});
|
||||
const afterMvu = String(mvuResult.text || "");
|
||||
const finalText = regexStage
|
||||
? applyTaskRegex(
|
||||
settings,
|
||||
taskType,
|
||||
regexStage,
|
||||
afterMvu,
|
||||
regexCollector,
|
||||
role,
|
||||
)
|
||||
: afterMvu;
|
||||
|
||||
return {
|
||||
text: finalText,
|
||||
changed: finalText !== originalText,
|
||||
dropped: Boolean(mvuResult.dropped),
|
||||
reasons: Array.isArray(mvuResult.reasons) ? mvuResult.reasons : [],
|
||||
blockedHitCount: Number(mvuResult.blockedHitCount || 0),
|
||||
artifactRemovedCount: Number(mvuResult.artifactRemovedCount || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeChatMessageList(
|
||||
settings = {},
|
||||
taskType,
|
||||
chatMessages = [],
|
||||
debugState = null,
|
||||
regexCollector = null,
|
||||
) {
|
||||
if (!Array.isArray(chatMessages) || chatMessages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return chatMessages
|
||||
.map((message, index) => {
|
||||
const rawContent =
|
||||
typeof message === "string"
|
||||
? message
|
||||
: typeof message?.content === "string"
|
||||
? message.content
|
||||
: typeof message?.mes === "string"
|
||||
? message.mes
|
||||
: "";
|
||||
const sanitized = sanitizeTaskPromptText(settings, taskType, rawContent, {
|
||||
mode: "aggressive",
|
||||
regexStage: "input.recentMessages",
|
||||
role: "system",
|
||||
regexCollector,
|
||||
});
|
||||
pushMvuPromptDebugEntry(debugState, {
|
||||
name: `chatMessages[${index}]`,
|
||||
stage: "input.recentMessages",
|
||||
...sanitized,
|
||||
});
|
||||
if (!sanitized.text.trim()) {
|
||||
return null;
|
||||
}
|
||||
if (typeof message === "string") {
|
||||
return sanitized.text;
|
||||
}
|
||||
if (message && typeof message === "object") {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? sanitized.text
|
||||
: message.content,
|
||||
mes:
|
||||
typeof message.mes === "string"
|
||||
? sanitized.text
|
||||
: message.mes,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function sanitizePromptContextInputs(
|
||||
settings = {},
|
||||
taskType,
|
||||
context = {},
|
||||
debugState = null,
|
||||
regexCollector = null,
|
||||
) {
|
||||
const sanitizedContext = {
|
||||
...context,
|
||||
};
|
||||
|
||||
for (const fieldName of INPUT_CONTEXT_MVU_FIELDS) {
|
||||
const value = sanitizedContext[fieldName];
|
||||
if (typeof value !== "string") {
|
||||
continue;
|
||||
}
|
||||
const regexStage = INPUT_REGEX_STAGE_BY_FIELD[fieldName] || "";
|
||||
const sanitized = sanitizeTaskPromptText(settings, taskType, value, {
|
||||
mode: "aggressive",
|
||||
regexStage,
|
||||
role: "system",
|
||||
regexCollector,
|
||||
});
|
||||
sanitizedContext[fieldName] = sanitized.text;
|
||||
pushMvuPromptDebugEntry(debugState, {
|
||||
name: fieldName,
|
||||
stage: regexStage,
|
||||
...sanitized,
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(sanitizedContext.chatMessages)) {
|
||||
sanitizedContext.chatMessages = sanitizeChatMessageList(
|
||||
settings,
|
||||
taskType,
|
||||
sanitizedContext.chatMessages,
|
||||
debugState,
|
||||
regexCollector,
|
||||
);
|
||||
}
|
||||
|
||||
return sanitizedContext;
|
||||
}
|
||||
|
||||
function sanitizeWorldInfoEntries(
|
||||
settings = {},
|
||||
taskType,
|
||||
entries = [],
|
||||
blockedContents = [],
|
||||
debugState = null,
|
||||
regexCollector = null,
|
||||
) {
|
||||
return (Array.isArray(entries) ? entries : [])
|
||||
.map((entry, index) => {
|
||||
const sanitized = sanitizeTaskPromptText(
|
||||
settings,
|
||||
taskType,
|
||||
String(entry?.content || ""),
|
||||
{
|
||||
mode: "aggressive",
|
||||
blockedContents,
|
||||
regexStage: "input.finalPrompt",
|
||||
role: entry?.role || "system",
|
||||
regexCollector,
|
||||
},
|
||||
);
|
||||
debugState.worldInfoBlockedContentHits += sanitized.blockedHitCount;
|
||||
if (sanitized.changed || sanitized.dropped) {
|
||||
debugState.finalMessageStripCount += 1;
|
||||
}
|
||||
if (!sanitized.text.trim()) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...entry,
|
||||
content: sanitized.text,
|
||||
index:
|
||||
Number.isFinite(Number(entry?.index))
|
||||
? Number(entry.index)
|
||||
: index,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function sanitizeWorldInfoContext(
|
||||
settings = {},
|
||||
taskType,
|
||||
worldInfo = null,
|
||||
debugState = null,
|
||||
regexCollector = null,
|
||||
) {
|
||||
const rawDebug =
|
||||
worldInfo?.debug && typeof worldInfo.debug === "object"
|
||||
? worldInfo.debug
|
||||
: null;
|
||||
const blockedContentsCount = Number(rawDebug?.mvu?.blockedContentsCount || 0);
|
||||
const blockedContents = [];
|
||||
if (blockedContentsCount > 0 && Array.isArray(rawDebug?.mvu?.filteredEntries)) {
|
||||
// Use only the structural count for debug; blocked content strings stay internal
|
||||
// on the world info object via the non-enumerable runtime property below.
|
||||
}
|
||||
|
||||
const runtimeBlockedContents = Array.isArray(worldInfo?.__mvuBlockedContents)
|
||||
? worldInfo.__mvuBlockedContents
|
||||
: [];
|
||||
|
||||
const beforeEntries = sanitizeWorldInfoEntries(
|
||||
settings,
|
||||
taskType,
|
||||
worldInfo?.beforeEntries,
|
||||
runtimeBlockedContents,
|
||||
debugState,
|
||||
regexCollector,
|
||||
);
|
||||
const afterEntries = sanitizeWorldInfoEntries(
|
||||
settings,
|
||||
taskType,
|
||||
worldInfo?.afterEntries,
|
||||
runtimeBlockedContents,
|
||||
debugState,
|
||||
regexCollector,
|
||||
);
|
||||
const atDepthEntries = sanitizeWorldInfoEntries(
|
||||
settings,
|
||||
taskType,
|
||||
worldInfo?.atDepthEntries,
|
||||
runtimeBlockedContents,
|
||||
debugState,
|
||||
regexCollector,
|
||||
);
|
||||
const additionalMessages = (Array.isArray(worldInfo?.additionalMessages)
|
||||
? worldInfo.additionalMessages
|
||||
: []
|
||||
)
|
||||
.map((message) => {
|
||||
const sanitized = sanitizeTaskPromptText(
|
||||
settings,
|
||||
taskType,
|
||||
String(message?.content || ""),
|
||||
{
|
||||
mode: "aggressive",
|
||||
blockedContents: runtimeBlockedContents,
|
||||
regexStage: "input.finalPrompt",
|
||||
role: message?.role || "system",
|
||||
regexCollector,
|
||||
},
|
||||
);
|
||||
debugState.worldInfoBlockedContentHits += sanitized.blockedHitCount;
|
||||
if (sanitized.changed || sanitized.dropped) {
|
||||
debugState.finalMessageStripCount += 1;
|
||||
}
|
||||
if (!sanitized.text.trim()) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
content: sanitized.text,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const beforeText = beforeEntries.map((entry) => entry.content).join("\n\n");
|
||||
const afterText = afterEntries.map((entry) => entry.content).join("\n\n");
|
||||
const activatedEntryNames = [
|
||||
...beforeEntries.map((entry) => entry.name),
|
||||
...afterEntries.map((entry) => entry.name),
|
||||
...atDepthEntries.map((entry) => entry.name),
|
||||
].filter(Boolean);
|
||||
|
||||
const sanitizedWorldInfo = {
|
||||
beforeEntries,
|
||||
afterEntries,
|
||||
atDepthEntries,
|
||||
beforeText,
|
||||
afterText,
|
||||
additionalMessages,
|
||||
activatedEntryNames: [...new Set(activatedEntryNames)],
|
||||
debug: rawDebug,
|
||||
};
|
||||
|
||||
Object.defineProperty(sanitizedWorldInfo, "__mvuBlockedContents", {
|
||||
value: [...runtimeBlockedContents],
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
return sanitizedWorldInfo;
|
||||
}
|
||||
|
||||
function createHostInjectionEntry(
|
||||
entry = {},
|
||||
position = "after",
|
||||
@@ -394,6 +758,15 @@ function extractWorldInfoChatMessages(context = {}) {
|
||||
export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
|
||||
const profile = getActiveTaskProfile(settings, taskType);
|
||||
const legacyPrompt = getLegacyPromptForTask(settings, taskType);
|
||||
const promptRegexInput = { entries: [] };
|
||||
const mvuPromptDebug = createEmptyMvuPromptDebug();
|
||||
const sanitizedInputContext = sanitizePromptContextInputs(
|
||||
settings,
|
||||
taskType,
|
||||
context,
|
||||
mvuPromptDebug,
|
||||
promptRegexInput,
|
||||
);
|
||||
const rawBlocks = Array.isArray(profile?.blocks) ? profile.blocks : [];
|
||||
const blocks = rawBlocks
|
||||
.map((block, index) => ({ ...block, _orderIndex: index }))
|
||||
@@ -410,28 +783,41 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
|
||||
const worldInfoRequested = profileRequiresWorldInfo(profile);
|
||||
const emptyWorldInfo = buildEmptyWorldInfoContext();
|
||||
let resolvedWorldInfo = emptyWorldInfo;
|
||||
let worldInfoRuntimeBlockedContents = [];
|
||||
|
||||
if (worldInfoRequested) {
|
||||
const worldInfo = await resolveTaskWorldInfo({
|
||||
settings,
|
||||
chatMessages: extractWorldInfoChatMessages(context),
|
||||
userMessage: String(context.userMessage || ""),
|
||||
templateContext: context,
|
||||
chatMessages: extractWorldInfoChatMessages(sanitizedInputContext),
|
||||
userMessage: String(sanitizedInputContext.userMessage || ""),
|
||||
templateContext: sanitizedInputContext,
|
||||
});
|
||||
const sanitizedWorldInfo = sanitizeWorldInfoContext(
|
||||
settings,
|
||||
taskType,
|
||||
worldInfo,
|
||||
mvuPromptDebug,
|
||||
promptRegexInput,
|
||||
);
|
||||
worldInfoRuntimeBlockedContents = Array.isArray(
|
||||
sanitizedWorldInfo.__mvuBlockedContents,
|
||||
)
|
||||
? sanitizedWorldInfo.__mvuBlockedContents
|
||||
: [];
|
||||
resolvedWorldInfo = {
|
||||
worldInfoBefore: worldInfo.beforeText || "",
|
||||
worldInfoAfter: worldInfo.afterText || "",
|
||||
worldInfoBeforeEntries: worldInfo.beforeEntries || [],
|
||||
worldInfoAfterEntries: worldInfo.afterEntries || [],
|
||||
worldInfoAtDepthEntries: worldInfo.atDepthEntries || [],
|
||||
activatedWorldInfoNames: worldInfo.activatedEntryNames || [],
|
||||
taskAdditionalMessages: worldInfo.additionalMessages || [],
|
||||
worldInfoDebug: worldInfo.debug || null,
|
||||
worldInfoBefore: sanitizedWorldInfo.beforeText || "",
|
||||
worldInfoAfter: sanitizedWorldInfo.afterText || "",
|
||||
worldInfoBeforeEntries: sanitizedWorldInfo.beforeEntries || [],
|
||||
worldInfoAfterEntries: sanitizedWorldInfo.afterEntries || [],
|
||||
worldInfoAtDepthEntries: sanitizedWorldInfo.atDepthEntries || [],
|
||||
activatedWorldInfoNames: sanitizedWorldInfo.activatedEntryNames || [],
|
||||
taskAdditionalMessages: sanitizedWorldInfo.additionalMessages || [],
|
||||
worldInfoDebug: sanitizedWorldInfo.debug || null,
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedContext = {
|
||||
...context,
|
||||
...sanitizedInputContext,
|
||||
...emptyWorldInfo,
|
||||
...resolvedWorldInfo,
|
||||
};
|
||||
@@ -465,6 +851,25 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
|
||||
content = interpolateVariables(block.content || "", resolvedContext);
|
||||
}
|
||||
|
||||
const sanitizedBlockContent = sanitizeTaskPromptText(
|
||||
settings,
|
||||
taskType,
|
||||
content,
|
||||
{
|
||||
mode: "final-safe",
|
||||
blockedContents: worldInfoRuntimeBlockedContents,
|
||||
regexStage: "input.finalPrompt",
|
||||
role,
|
||||
regexCollector: promptRegexInput,
|
||||
},
|
||||
);
|
||||
mvuPromptDebug.worldInfoBlockedContentHits +=
|
||||
sanitizedBlockContent.blockedHitCount;
|
||||
if (sanitizedBlockContent.changed || sanitizedBlockContent.dropped) {
|
||||
mvuPromptDebug.finalMessageStripCount += 1;
|
||||
}
|
||||
content = sanitizedBlockContent.text;
|
||||
|
||||
if (!String(content || "").trim()) continue;
|
||||
|
||||
const mode = normalizeInjectionMode(block.injectionMode);
|
||||
@@ -554,6 +959,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
|
||||
executionMessages,
|
||||
privateTaskMessages,
|
||||
renderedBlocks,
|
||||
regexInput: promptRegexInput,
|
||||
worldInfoResolution,
|
||||
systemPrompt,
|
||||
customMessages,
|
||||
@@ -601,6 +1007,15 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
|
||||
},
|
||||
worldInfoCacheHit: Boolean(worldInfoResolution.debug?.cache?.hit),
|
||||
ejsRuntimeStatus: worldInfoResolution.debug?.ejsRuntimeStatus || "",
|
||||
mvu: {
|
||||
sanitizedFieldCount: mvuPromptDebug.sanitizedFieldCount,
|
||||
sanitizedFields: cloneRuntimeDebugValue(
|
||||
mvuPromptDebug.sanitizedFields,
|
||||
[],
|
||||
),
|
||||
finalMessageStripCount: mvuPromptDebug.finalMessageStripCount,
|
||||
worldInfoBlockedContentHits: mvuPromptDebug.worldInfoBlockedContentHits,
|
||||
},
|
||||
effectivePath: {
|
||||
promptAssembly: "ordered-private-messages",
|
||||
hostInjectionPlan: "diagnostic-plan-only",
|
||||
@@ -617,6 +1032,15 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(result, "__mvuRuntime", {
|
||||
value: {
|
||||
blockedContents: [...worldInfoRuntimeBlockedContents],
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
recordTaskPromptBuild(taskType, {
|
||||
taskType,
|
||||
profileId: profile?.id || "",
|
||||
@@ -628,6 +1052,8 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
|
||||
hostInjections: worldInfoResolution.injections,
|
||||
hostInjectionPlan,
|
||||
worldInfoResolution,
|
||||
mvu: result.debug.mvu,
|
||||
regexInput: promptRegexInput,
|
||||
debug: result.debug,
|
||||
});
|
||||
|
||||
@@ -635,6 +1061,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
|
||||
}
|
||||
|
||||
export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "") {
|
||||
const runtimeMvu = promptBuild?.__mvuRuntime || {};
|
||||
const executionMessages = Array.isArray(promptBuild?.executionMessages)
|
||||
? promptBuild.executionMessages
|
||||
.map((message) =>
|
||||
@@ -653,10 +1080,21 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
|
||||
const hasUserMessage = executionMessages.some(
|
||||
(message) => message.role === "user",
|
||||
);
|
||||
const sanitizedFallbackUserPrompt = sanitizeTaskPromptText(
|
||||
{},
|
||||
promptBuild?.debug?.taskType || "",
|
||||
String(fallbackUserPrompt || ""),
|
||||
{
|
||||
mode: "final-safe",
|
||||
blockedContents: Array.isArray(runtimeMvu?.blockedContents)
|
||||
? runtimeMvu.blockedContents
|
||||
: [],
|
||||
},
|
||||
).text;
|
||||
|
||||
return {
|
||||
systemPrompt: String(promptBuild?.systemPrompt || ""),
|
||||
userPrompt: hasUserMessage ? "" : String(fallbackUserPrompt || ""),
|
||||
userPrompt: hasUserMessage ? "" : sanitizedFallbackUserPrompt,
|
||||
promptMessages: executionMessages,
|
||||
additionalMessages:
|
||||
executionMessages.length > 0
|
||||
|
||||
20
task-ejs.js
20
task-ejs.js
@@ -402,6 +402,22 @@ async function resolveEntry(renderCtx, currentWorldbook, worldbookOrEntry, entry
|
||||
renderCtx.allEntries.get(identifier);
|
||||
}
|
||||
|
||||
if (
|
||||
!resolved &&
|
||||
typeof renderCtx.resolveIgnoredEntry === "function"
|
||||
) {
|
||||
const ignoredEntry =
|
||||
renderCtx.resolveIgnoredEntry(explicitWorldbook || fallbackWorldbook, identifier) ||
|
||||
renderCtx.resolveIgnoredEntry("", identifier);
|
||||
if (ignoredEntry) {
|
||||
const descriptor = ignoredEntry.sourceName || ignoredEntry.name || identifier;
|
||||
recordRenderWarning(
|
||||
renderCtx,
|
||||
`mvu filtered world info blocked: ${ignoredEntry.worldbook ? `${ignoredEntry.worldbook}/` : ""}${descriptor}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -631,6 +647,10 @@ export function createTaskEjsRenderContext(entries = [], options = {}) {
|
||||
typeof options.loadWorldbookEntries === "function"
|
||||
? options.loadWorldbookEntries
|
||||
: null,
|
||||
resolveIgnoredEntry:
|
||||
typeof options.resolveIgnoredEntry === "function"
|
||||
? options.resolveIgnoredEntry
|
||||
: null,
|
||||
templateContext: {
|
||||
...(options.templateContext || {}),
|
||||
hostSnapshot: hostSnapshot.snapshot,
|
||||
|
||||
@@ -8,6 +8,11 @@ import {
|
||||
inspectTaskEjsRuntimeBackend,
|
||||
substituteTaskEjsParams,
|
||||
} from "./task-ejs.js";
|
||||
import {
|
||||
isLikelyMvuWorldInfoContent,
|
||||
isMvuTaggedWorldInfoNameOrComment,
|
||||
sanitizeMvuContent,
|
||||
} from "./mvu-compat.js";
|
||||
|
||||
const WI_POSITION = {
|
||||
before: 0,
|
||||
@@ -45,9 +50,130 @@ let worldbookEntriesCache = {
|
||||
createdAt: 0,
|
||||
expiresAt: 0,
|
||||
entries: [],
|
||||
blockedContents: [],
|
||||
ignoredEntries: [],
|
||||
ignoredLookup: new Map(),
|
||||
debug: null,
|
||||
};
|
||||
|
||||
function buildIgnoredEntryLookupKey(worldbookName, identifier) {
|
||||
return `${normalizeKey(worldbookName)}::${normalizeKey(identifier)}`;
|
||||
}
|
||||
|
||||
function createMvuCollector() {
|
||||
return {
|
||||
blockedContents: [],
|
||||
filteredEntries: [],
|
||||
lazyFilteredEntries: [],
|
||||
ignoredLookup: new Map(),
|
||||
seenEntries: new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
function registerIgnoredEntryLookup(collector, worldbookName, identifier, meta) {
|
||||
const normalizedIdentifier = normalizeKey(identifier);
|
||||
if (!collector || !normalizedIdentifier) return;
|
||||
collector.ignoredLookup.set(
|
||||
buildIgnoredEntryLookupKey(worldbookName, normalizedIdentifier),
|
||||
meta,
|
||||
);
|
||||
}
|
||||
|
||||
function registerIgnoredWorldInfoEntry(
|
||||
collector,
|
||||
entry = {},
|
||||
reason = "",
|
||||
{ lazy = false } = {},
|
||||
) {
|
||||
if (!collector || !entry) return;
|
||||
|
||||
const worldbook = normalizeKey(entry.worldbook);
|
||||
const name = normalizeKey(entry.name);
|
||||
const comment = normalizeKey(entry.comment);
|
||||
const content = String(entry.cleanContent || entry.content || "").trim();
|
||||
const identity = `${worldbook}:${entry.uid || 0}:${name}:${reason}`;
|
||||
const meta = {
|
||||
worldbook,
|
||||
name: comment || name,
|
||||
sourceName: name,
|
||||
reason: String(reason || ""),
|
||||
};
|
||||
|
||||
registerIgnoredEntryLookup(collector, worldbook, name, meta);
|
||||
registerIgnoredEntryLookup(collector, worldbook, comment, meta);
|
||||
|
||||
if (collector.seenEntries.has(identity)) {
|
||||
return;
|
||||
}
|
||||
collector.seenEntries.add(identity);
|
||||
|
||||
if (content) {
|
||||
collector.blockedContents.push(content);
|
||||
}
|
||||
|
||||
if (lazy) {
|
||||
collector.lazyFilteredEntries.push(meta);
|
||||
} else {
|
||||
collector.filteredEntries.push(meta);
|
||||
}
|
||||
}
|
||||
|
||||
function findIgnoredWorldInfoEntry(collector, worldbookName, identifier) {
|
||||
if (!collector || !normalizeKey(identifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedWorldbook = normalizeKey(worldbookName);
|
||||
const normalizedIdentifier = normalizeKey(identifier);
|
||||
const exact = collector.ignoredLookup.get(
|
||||
buildIgnoredEntryLookupKey(normalizedWorldbook, normalizedIdentifier),
|
||||
);
|
||||
if (exact) {
|
||||
return exact;
|
||||
}
|
||||
|
||||
if (normalizedWorldbook) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const [lookupKey, value] of collector.ignoredLookup.entries()) {
|
||||
if (lookupKey.endsWith(`::${normalizedIdentifier}`)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMvuIgnoreReason(entry = {}) {
|
||||
if (isMvuTaggedWorldInfoNameOrComment(entry.name, entry.comment)) {
|
||||
return "mvu_tagged";
|
||||
}
|
||||
if (isLikelyMvuWorldInfoContent(entry.cleanContent || entry.content)) {
|
||||
return "mvu_content";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function buildMvuDebugSummary(collector) {
|
||||
const filteredEntries = Array.isArray(collector?.filteredEntries)
|
||||
? collector.filteredEntries
|
||||
: [];
|
||||
const lazyFilteredEntries = Array.isArray(collector?.lazyFilteredEntries)
|
||||
? collector.lazyFilteredEntries
|
||||
: [];
|
||||
const blockedContents = Array.isArray(collector?.blockedContents)
|
||||
? collector.blockedContents
|
||||
: [];
|
||||
|
||||
return {
|
||||
filteredEntryCount: filteredEntries.length,
|
||||
filteredEntries: [...filteredEntries, ...lazyFilteredEntries],
|
||||
blockedContentsCount: uniq(blockedContents.map((item) => String(item || "").trim()).filter(Boolean)).length,
|
||||
lazyFilteredEntryCount: lazyFilteredEntries.length,
|
||||
};
|
||||
}
|
||||
|
||||
function getStContext() {
|
||||
try {
|
||||
return globalThis.SillyTavern?.getContext?.() || {};
|
||||
@@ -611,7 +737,11 @@ function selectActivatedEntries(
|
||||
return ungrouped.concat(matched).sort(sortEntries);
|
||||
}
|
||||
|
||||
async function loadNormalizedWorldbookEntries(worldbookHost, worldbookName) {
|
||||
async function loadNormalizedWorldbookEntries(
|
||||
worldbookHost,
|
||||
worldbookName,
|
||||
{ mvuCollector = null, lazy = false } = {},
|
||||
) {
|
||||
const normalizedName = normalizeKey(worldbookName);
|
||||
if (!normalizedName || typeof worldbookHost?.getWorldbook !== "function") {
|
||||
return [];
|
||||
@@ -636,15 +766,26 @@ async function loadNormalizedWorldbookEntries(worldbookHost, worldbookName) {
|
||||
}
|
||||
}
|
||||
|
||||
return (Array.isArray(entries) ? entries : []).map((entry) =>
|
||||
normalizeEntry(
|
||||
const normalizedEntries = [];
|
||||
for (const entry of Array.isArray(entries) ? entries : []) {
|
||||
const normalizedEntry = normalizeEntry(
|
||||
{
|
||||
...entry,
|
||||
comment: commentByUid.get(entry.uid) ?? entry.comment ?? "",
|
||||
},
|
||||
normalizedName,
|
||||
),
|
||||
);
|
||||
);
|
||||
const ignoreReason = getMvuIgnoreReason(normalizedEntry);
|
||||
if (ignoreReason) {
|
||||
registerIgnoredWorldInfoEntry(mvuCollector, normalizedEntry, ignoreReason, {
|
||||
lazy,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
normalizedEntries.push(normalizedEntry);
|
||||
}
|
||||
|
||||
return normalizedEntries;
|
||||
}
|
||||
|
||||
async function collectAllWorldbookEntries(worldbookHost = null) {
|
||||
@@ -750,12 +891,16 @@ async function collectAllWorldbookEntries(worldbookHost = null) {
|
||||
) {
|
||||
return {
|
||||
entries: worldbookEntriesCache.entries,
|
||||
blockedContents: worldbookEntriesCache.blockedContents,
|
||||
ignoredEntries: worldbookEntriesCache.ignoredEntries,
|
||||
ignoredLookup: worldbookEntriesCache.ignoredLookup,
|
||||
debug: {
|
||||
...debug,
|
||||
loadedWorldbooks:
|
||||
worldbookEntriesCache.debug?.loadedWorldbooks || requestedWorldbooks,
|
||||
worldbookCount: worldbookEntriesCache.entries.length,
|
||||
loadMs: worldbookEntriesCache.debug?.loadMs || 0,
|
||||
mvu: worldbookEntriesCache.debug?.mvu || buildMvuDebugSummary(null),
|
||||
cache: {
|
||||
...debug.cache,
|
||||
hit: true,
|
||||
@@ -768,6 +913,7 @@ async function collectAllWorldbookEntries(worldbookHost = null) {
|
||||
const allEntries = [];
|
||||
const loadedNames = new Set();
|
||||
const startedAt = Date.now();
|
||||
const mvuCollector = createMvuCollector();
|
||||
|
||||
async function loadWorldbookOnce(worldbookName) {
|
||||
const normalizedName = normalizeKey(worldbookName);
|
||||
@@ -778,6 +924,7 @@ async function collectAllWorldbookEntries(worldbookHost = null) {
|
||||
const entries = await loadNormalizedWorldbookEntries(
|
||||
resolvedWorldbookHost,
|
||||
normalizedName,
|
||||
{ mvuCollector },
|
||||
);
|
||||
allEntries.push(...entries);
|
||||
} catch (error) {
|
||||
@@ -795,11 +942,15 @@ async function collectAllWorldbookEntries(worldbookHost = null) {
|
||||
debug.loadedWorldbooks = [...loadedNames];
|
||||
debug.worldbookCount = allEntries.length;
|
||||
debug.loadMs = Date.now() - startedAt;
|
||||
debug.mvu = buildMvuDebugSummary(mvuCollector);
|
||||
worldbookEntriesCache = {
|
||||
key: cacheKey,
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + WORLDINFO_CACHE_TTL_MS,
|
||||
entries: allEntries,
|
||||
blockedContents: [...mvuCollector.blockedContents],
|
||||
ignoredEntries: [...debug.mvu.filteredEntries],
|
||||
ignoredLookup: new Map(mvuCollector.ignoredLookup),
|
||||
debug: {
|
||||
...debug,
|
||||
},
|
||||
@@ -807,6 +958,9 @@ async function collectAllWorldbookEntries(worldbookHost = null) {
|
||||
|
||||
return {
|
||||
entries: allEntries,
|
||||
blockedContents: [...mvuCollector.blockedContents],
|
||||
ignoredEntries: [...debug.mvu.filteredEntries],
|
||||
ignoredLookup: new Map(mvuCollector.ignoredLookup),
|
||||
debug,
|
||||
};
|
||||
}
|
||||
@@ -1016,6 +1170,7 @@ export async function resolveTaskWorldInfo({
|
||||
ejsLastError: "",
|
||||
warnings: [],
|
||||
resolvedEntries: [],
|
||||
mvu: buildMvuDebugSummary(null),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1023,7 +1178,20 @@ export async function resolveTaskWorldInfo({
|
||||
const worldbookHost = await getWorldbookHost();
|
||||
const collected = await collectAllWorldbookEntries(worldbookHost);
|
||||
const allEntries = Array.isArray(collected?.entries) ? collected.entries : [];
|
||||
const blockedContents = Array.isArray(collected?.blockedContents)
|
||||
? collected.blockedContents
|
||||
: [];
|
||||
const ignoredLookup =
|
||||
collected?.ignoredLookup instanceof Map
|
||||
? collected.ignoredLookup
|
||||
: new Map();
|
||||
result.allEntries = allEntries;
|
||||
Object.defineProperty(result, "__mvuBlockedContents", {
|
||||
value: blockedContents,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
});
|
||||
result.debug = {
|
||||
...result.debug,
|
||||
...(collected?.debug || {}),
|
||||
@@ -1037,6 +1205,10 @@ export async function resolveTaskWorldInfo({
|
||||
resolvedEntries: Array.isArray(result.debug.resolvedEntries)
|
||||
? result.debug.resolvedEntries
|
||||
: [],
|
||||
mvu:
|
||||
collected?.debug?.mvu && typeof collected.debug.mvu === "object"
|
||||
? { ...collected.debug.mvu }
|
||||
: buildMvuDebugSummary(null),
|
||||
};
|
||||
if (allEntries.length === 0) {
|
||||
return result;
|
||||
@@ -1076,6 +1248,15 @@ export async function resolveTaskWorldInfo({
|
||||
const aggregatedForcedEntries = new Map();
|
||||
const aggregatedInlineEntries = new Map();
|
||||
const recursionWarnings = new Set();
|
||||
const lazyMvuCollector = {
|
||||
blockedContents,
|
||||
filteredEntries: Array.isArray(result.debug.mvu.filteredEntries)
|
||||
? result.debug.mvu.filteredEntries
|
||||
: [],
|
||||
lazyFilteredEntries: [],
|
||||
ignoredLookup,
|
||||
seenEntries: new Set(),
|
||||
};
|
||||
const knownWorldbooks = new Set(
|
||||
allEntries.map((entry) => entry.worldbook).filter(Boolean),
|
||||
);
|
||||
@@ -1087,8 +1268,29 @@ export async function resolveTaskWorldInfo({
|
||||
const lazyEntries = await loadNormalizedWorldbookEntries(
|
||||
worldbookHost,
|
||||
normalizedWorldbook,
|
||||
{
|
||||
mvuCollector: lazyMvuCollector,
|
||||
lazy: true,
|
||||
},
|
||||
);
|
||||
knownWorldbooks.add(normalizedWorldbook);
|
||||
const newLazyIgnoredEntries = [...lazyMvuCollector.lazyFilteredEntries];
|
||||
result.debug.mvu = {
|
||||
...result.debug.mvu,
|
||||
blockedContentsCount: uniq(
|
||||
blockedContents.map((item) => String(item || "").trim()).filter(Boolean),
|
||||
).length,
|
||||
filteredEntries: [
|
||||
...(Array.isArray(result.debug.mvu.filteredEntries)
|
||||
? result.debug.mvu.filteredEntries
|
||||
: []),
|
||||
...newLazyIgnoredEntries,
|
||||
],
|
||||
lazyFilteredEntryCount:
|
||||
Number(result.debug.mvu.lazyFilteredEntryCount || 0) +
|
||||
newLazyIgnoredEntries.length,
|
||||
};
|
||||
lazyMvuCollector.lazyFilteredEntries = [];
|
||||
return lazyEntries;
|
||||
};
|
||||
|
||||
@@ -1109,6 +1311,12 @@ export async function resolveTaskWorldInfo({
|
||||
templateContext: normalizedTemplateContext,
|
||||
currentActivatedEntries: [...allActivated.values()],
|
||||
loadWorldbookEntries: lazyLoadWorldbookEntries,
|
||||
resolveIgnoredEntry: (worldbookName, identifier) =>
|
||||
findIgnoredWorldInfoEntry(
|
||||
{ ignoredLookup },
|
||||
worldbookName,
|
||||
identifier,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1175,7 +1383,17 @@ export async function resolveTaskWorldInfo({
|
||||
recursionWarnings.add(String(warning || ""));
|
||||
}
|
||||
|
||||
const trimmedContent = String(renderedContent || "").trim();
|
||||
const mvuSanitized = sanitizeMvuContent(renderedContent, {
|
||||
mode: "aggressive",
|
||||
blockedContents,
|
||||
});
|
||||
if (mvuSanitized.dropped) {
|
||||
const warning = `世界书条目 ${entry.name} 渲染结果命中 MVU 规则,已跳过`;
|
||||
if (!result.debug.warnings.includes(warning)) {
|
||||
result.debug.warnings.push(warning);
|
||||
}
|
||||
}
|
||||
const trimmedContent = String(mvuSanitized.text || "").trim();
|
||||
if (!trimmedContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
56
tests/mvu-compat.mjs
Normal file
56
tests/mvu-compat.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const {
|
||||
isLikelyMvuWorldInfoContent,
|
||||
isMvuTaggedWorldInfoNameOrComment,
|
||||
sanitizeMvuContent,
|
||||
} = await import("../mvu-compat.js");
|
||||
|
||||
assert.equal(
|
||||
isMvuTaggedWorldInfoNameOrComment("[mvu_update] 状态", ""),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isMvuTaggedWorldInfoNameOrComment("普通条目", "[initvar]"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isLikelyMvuWorldInfoContent(
|
||||
"变量更新规则:\ntype: state\n当前时间: 12:00",
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(isLikelyMvuWorldInfoContent("正常世界设定"), false);
|
||||
|
||||
const aggressive = sanitizeMvuContent(
|
||||
"正文\n<updatevariable>hp=1</updatevariable>\n<status_current_variable>secret</status_current_variable>",
|
||||
{
|
||||
mode: "aggressive",
|
||||
},
|
||||
);
|
||||
assert.equal(aggressive.text, "");
|
||||
assert.equal(aggressive.dropped, true);
|
||||
assert.deepEqual(
|
||||
aggressive.reasons.sort(),
|
||||
["artifact_stripped", "likely_mvu_content"].sort(),
|
||||
);
|
||||
|
||||
const finalSafe = sanitizeMvuContent(
|
||||
"说明文字\n<updatevariable>hp=1</updatevariable>\n尾巴",
|
||||
{
|
||||
mode: "final-safe",
|
||||
},
|
||||
);
|
||||
assert.equal(finalSafe.dropped, false);
|
||||
assert.equal(finalSafe.text, "说明文字\n尾巴");
|
||||
assert.deepEqual(finalSafe.reasons, ["artifact_stripped"]);
|
||||
|
||||
const blocked = sanitizeMvuContent("前缀\n被拦截条目\n后缀", {
|
||||
mode: "final-safe",
|
||||
blockedContents: ["被拦截条目"],
|
||||
});
|
||||
assert.equal(blocked.text, "前缀\n\n后缀");
|
||||
assert.equal(blocked.blockedHitCount, 1);
|
||||
assert.deepEqual(blocked.reasons, ["blocked_content_removed"]);
|
||||
|
||||
console.log("mvu-compat tests passed");
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire, registerHooks } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import vm from "node:vm";
|
||||
|
||||
const extensionsShimSource = [
|
||||
@@ -34,6 +35,8 @@ const scriptShimUrl = `data:text/javascript,${encodeURIComponent(
|
||||
const openAiShimUrl = `data:text/javascript,${encodeURIComponent(
|
||||
openAiShimSource,
|
||||
)}`;
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const indexPath = path.resolve(moduleDir, "../index.js");
|
||||
|
||||
registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
@@ -138,7 +141,6 @@ const schema = [
|
||||
];
|
||||
|
||||
function createBatchStageHarness() {
|
||||
const indexPath = path.resolve("./index.js");
|
||||
return fs.readFile(indexPath, "utf8").then((source) => {
|
||||
const marker = "function isAssistantChatMessage(message) {";
|
||||
const start = source.indexOf("function shouldAdvanceProcessedHistory(");
|
||||
@@ -181,7 +183,6 @@ function createBatchStageHarness() {
|
||||
}
|
||||
|
||||
function createGenerationRecallHarness() {
|
||||
const indexPath = path.resolve("./index.js");
|
||||
return fs.readFile(indexPath, "utf8").then((source) => {
|
||||
const start = source.indexOf("const RECALL_INPUT_RECORD_TTL_MS = 60000;");
|
||||
const end = source.indexOf("function onMessageReceived() {");
|
||||
@@ -243,7 +244,6 @@ function createGenerationRecallHarness() {
|
||||
}
|
||||
|
||||
function createRerollHarness() {
|
||||
const indexPath = path.resolve("./index.js");
|
||||
return fs.readFile(indexPath, "utf8").then((source) => {
|
||||
const helperStart = source.indexOf(
|
||||
"function pruneProcessedMessageHashesFromFloor(",
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { registerHooks } from "node:module";
|
||||
|
||||
const extensionsShimSource = [
|
||||
"export const extension_settings = {};",
|
||||
"export function getContext() {",
|
||||
" return {",
|
||||
" chat: [],",
|
||||
|
||||
333
tests/prompt-builder-mvu.mjs
Normal file
333
tests/prompt-builder-mvu.mjs
Normal file
@@ -0,0 +1,333 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createRequire, registerHooks } from "node:module";
|
||||
|
||||
const extensionsShimSource = [
|
||||
"export const extension_settings = globalThis.__promptBuilderMvuExtensionSettings || {};",
|
||||
"export function getContext() {",
|
||||
" return globalThis.__promptBuilderMvuContext || {",
|
||||
" chat: [],",
|
||||
" chatMetadata: {},",
|
||||
" extensionSettings: {},",
|
||||
" powerUserSettings: {},",
|
||||
" characters: [],",
|
||||
" characterId: null,",
|
||||
" name1: '',",
|
||||
" name2: '',",
|
||||
" chatId: 'mvu-test-chat',",
|
||||
" };",
|
||||
"}",
|
||||
].join("\n");
|
||||
const scriptShimSource = [
|
||||
"export function getRequestHeaders() {",
|
||||
" return { 'Content-Type': 'application/json' };",
|
||||
"}",
|
||||
].join("\n");
|
||||
const openAiShimSource = [
|
||||
"export const chat_completion_sources = { CUSTOM: 'custom', OPENAI: 'openai' };",
|
||||
"export async function sendOpenAIRequest(...args) {",
|
||||
" if (typeof globalThis.__promptBuilderMvuSendOpenAIRequest === 'function') {",
|
||||
" return await globalThis.__promptBuilderMvuSendOpenAIRequest(...args);",
|
||||
" }",
|
||||
" return { choices: [{ message: { content: '{}' } }] };",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (
|
||||
specifier === "../../../extensions.js" ||
|
||||
specifier === "../../../../extensions.js"
|
||||
) {
|
||||
return {
|
||||
shortCircuit: true,
|
||||
url: `data:text/javascript,${encodeURIComponent(extensionsShimSource)}`,
|
||||
};
|
||||
}
|
||||
if (specifier === "../../../../script.js") {
|
||||
return {
|
||||
shortCircuit: true,
|
||||
url: `data:text/javascript,${encodeURIComponent(scriptShimSource)}`,
|
||||
};
|
||||
}
|
||||
if (specifier === "../../../openai.js") {
|
||||
return {
|
||||
shortCircuit: true,
|
||||
url: `data:text/javascript,${encodeURIComponent(openAiShimSource)}`,
|
||||
};
|
||||
}
|
||||
return nextResolve(specifier, context);
|
||||
},
|
||||
});
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const originalRequire = globalThis.require;
|
||||
const originalExtensionSettings = globalThis.__promptBuilderMvuExtensionSettings;
|
||||
const originalContext = globalThis.__promptBuilderMvuContext;
|
||||
const originalSendOpenAIRequest = globalThis.__promptBuilderMvuSendOpenAIRequest;
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.require = require;
|
||||
globalThis.__promptBuilderMvuExtensionSettings = {
|
||||
st_bme: {},
|
||||
};
|
||||
globalThis.__promptBuilderMvuContext = {
|
||||
chat: [],
|
||||
chatMetadata: {},
|
||||
extensionSettings: {},
|
||||
powerUserSettings: {},
|
||||
characters: [],
|
||||
characterId: null,
|
||||
name1: "User",
|
||||
name2: "Alice",
|
||||
chatId: "mvu-test-chat",
|
||||
};
|
||||
|
||||
try {
|
||||
const extensionsApi = await import("../../../../extensions.js");
|
||||
const { createDefaultTaskProfiles } = await import("../prompt-profiles.js");
|
||||
const {
|
||||
buildTaskExecutionDebugContext,
|
||||
buildTaskLlmPayload,
|
||||
buildTaskPrompt,
|
||||
} = await import("../prompt-builder.js");
|
||||
const llm = await import("../llm.js");
|
||||
|
||||
function createRule(id, findRegex, replaceString) {
|
||||
return {
|
||||
id,
|
||||
script_name: id,
|
||||
enabled: true,
|
||||
find_regex: findRegex,
|
||||
replace_string: replaceString,
|
||||
source: {
|
||||
user_input: true,
|
||||
ai_output: true,
|
||||
},
|
||||
destination: {
|
||||
prompt: true,
|
||||
display: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildSettings() {
|
||||
const taskProfiles = createDefaultTaskProfiles();
|
||||
const recallProfile = taskProfiles.recall.profiles[0];
|
||||
recallProfile.generation = {
|
||||
...recallProfile.generation,
|
||||
stream: false,
|
||||
};
|
||||
recallProfile.regex = {
|
||||
enabled: true,
|
||||
inheritStRegex: false,
|
||||
sources: {
|
||||
global: false,
|
||||
preset: false,
|
||||
character: false,
|
||||
},
|
||||
stages: {
|
||||
"input.userMessage": true,
|
||||
"input.recentMessages": true,
|
||||
"input.candidateText": true,
|
||||
"input.finalPrompt": true,
|
||||
},
|
||||
localRules: [
|
||||
createRule("user-rule", "/BAD_USER/g", "GOOD_USER"),
|
||||
createRule("recent-rule", "/BAD_RECENT/g", "GOOD_RECENT"),
|
||||
createRule("candidate-rule", "/BAD_CANDIDATE/g", "GOOD_CANDIDATE"),
|
||||
createRule("final-rule", "/FINAL_BAD/g", "FINAL_GOOD"),
|
||||
],
|
||||
};
|
||||
recallProfile.blocks.push({
|
||||
id: "mvu-final-custom",
|
||||
name: "最终检查块",
|
||||
type: "custom",
|
||||
enabled: true,
|
||||
role: "system",
|
||||
sourceKey: "",
|
||||
sourceField: "",
|
||||
content: "FINAL_BAD",
|
||||
injectionMode: "append",
|
||||
order: recallProfile.blocks.length,
|
||||
});
|
||||
|
||||
return {
|
||||
llmApiUrl: "https://example.com/v1",
|
||||
llmApiKey: "sk-mvu-secret",
|
||||
llmModel: "gpt-mvu-test",
|
||||
timeoutMs: 4321,
|
||||
taskProfilesVersion: 3,
|
||||
taskProfiles,
|
||||
};
|
||||
}
|
||||
|
||||
const settings = buildSettings();
|
||||
extensionsApi.extension_settings.st_bme = settings;
|
||||
delete globalThis.__stBmeRuntimeDebugState;
|
||||
|
||||
const promptBuild = await buildTaskPrompt(settings, "recall", {
|
||||
taskName: "recall",
|
||||
charDescription: "角色设定 <StatusPlaceHolderImpl/> BAD_RECENT",
|
||||
userPersona: "变量更新规则:\ntype: state\n当前时间: 12:00",
|
||||
recentMessages:
|
||||
"最近消息 <status_current_variable>hp=3</status_current_variable> BAD_RECENT",
|
||||
userMessage:
|
||||
"用户输入 <updatevariable>secret</updatevariable> BAD_USER",
|
||||
candidateNodes: "候选节点 BAD_CANDIDATE",
|
||||
candidateText: "候选节点 BAD_CANDIDATE",
|
||||
graphStats: "candidate_count=1",
|
||||
});
|
||||
|
||||
assert.match(promptBuild.systemPrompt, /GOOD_RECENT/);
|
||||
assert.match(JSON.stringify(promptBuild.executionMessages), /GOOD_USER/);
|
||||
assert.match(JSON.stringify(promptBuild.executionMessages), /GOOD_CANDIDATE/);
|
||||
assert.match(promptBuild.systemPrompt, /FINAL_GOOD/);
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(promptBuild),
|
||||
/status_current_variable|updatevariable|StatusPlaceHolderImpl/i,
|
||||
);
|
||||
assert.equal(promptBuild.debug.mvu.sanitizedFieldCount >= 4, true);
|
||||
assert.equal(promptBuild.debug.mvu.finalMessageStripCount >= 1, true);
|
||||
assert.equal(Array.isArray(promptBuild.regexInput?.entries), true);
|
||||
assert.equal(promptBuild.regexInput.entries.length > 0, true);
|
||||
|
||||
const systemOnlySettings = buildSettings();
|
||||
systemOnlySettings.taskProfiles.recall = {
|
||||
activeProfileId: "system-only",
|
||||
profiles: [
|
||||
{
|
||||
id: "system-only",
|
||||
name: "system only",
|
||||
taskType: "recall",
|
||||
builtin: false,
|
||||
blocks: [
|
||||
{
|
||||
id: "only-system",
|
||||
name: "Only System",
|
||||
type: "custom",
|
||||
enabled: true,
|
||||
role: "system",
|
||||
sourceKey: "",
|
||||
sourceField: "",
|
||||
content: "系统块",
|
||||
injectionMode: "append",
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
generation: createDefaultTaskProfiles().recall.profiles[0].generation,
|
||||
regex: {
|
||||
enabled: false,
|
||||
inheritStRegex: false,
|
||||
stages: {},
|
||||
localRules: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const systemOnlyPromptBuild = await buildTaskPrompt(systemOnlySettings, "recall", {
|
||||
taskName: "recall",
|
||||
});
|
||||
const systemOnlyPayload = buildTaskLlmPayload(
|
||||
systemOnlyPromptBuild,
|
||||
"fallback <updatevariable>hidden</updatevariable> text",
|
||||
);
|
||||
assert.equal(systemOnlyPayload.userPrompt, "fallback text");
|
||||
|
||||
const capturedBodies = [];
|
||||
globalThis.fetch = async (_url, options = {}) => {
|
||||
capturedBodies.push(JSON.parse(String(options.body || "{}")));
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: '{"ok":true}',
|
||||
},
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const payload = buildTaskLlmPayload(promptBuild, "unused fallback");
|
||||
const result = await llm.callLLMForJSON({
|
||||
systemPrompt: payload.systemPrompt,
|
||||
userPrompt: payload.userPrompt,
|
||||
maxRetries: 0,
|
||||
taskType: "recall",
|
||||
promptMessages: payload.promptMessages,
|
||||
additionalMessages: payload.additionalMessages,
|
||||
debugContext: buildTaskExecutionDebugContext(promptBuild),
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { ok: true });
|
||||
assert.equal(capturedBodies.length, 1);
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(capturedBodies[0].messages),
|
||||
/status_current_variable|updatevariable|StatusPlaceHolderImpl/i,
|
||||
);
|
||||
|
||||
const runtimePromptBuild =
|
||||
globalThis.__stBmeRuntimeDebugState?.taskPromptBuilds?.recall || null;
|
||||
const runtimeLlmRequest =
|
||||
globalThis.__stBmeRuntimeDebugState?.taskLlmRequests?.recall || null;
|
||||
|
||||
assert.ok(runtimePromptBuild);
|
||||
assert.ok(runtimeLlmRequest);
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(runtimePromptBuild.executionMessages),
|
||||
/status_current_variable|updatevariable|StatusPlaceHolderImpl/i,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(runtimeLlmRequest.messages),
|
||||
/status_current_variable|updatevariable|StatusPlaceHolderImpl/i,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(runtimeLlmRequest.requestBody?.messages || []),
|
||||
/status_current_variable|updatevariable|StatusPlaceHolderImpl/i,
|
||||
);
|
||||
assert.deepEqual(
|
||||
runtimeLlmRequest.messages,
|
||||
runtimeLlmRequest.requestBody.messages,
|
||||
);
|
||||
assert.equal(
|
||||
runtimeLlmRequest.promptExecution?.mvu?.sanitizedFieldCount,
|
||||
promptBuild.debug.mvu.sanitizedFieldCount,
|
||||
);
|
||||
|
||||
console.log("prompt-builder-mvu tests passed");
|
||||
} finally {
|
||||
if (originalRequire === undefined) {
|
||||
delete globalThis.require;
|
||||
} else {
|
||||
globalThis.require = originalRequire;
|
||||
}
|
||||
|
||||
if (originalExtensionSettings === undefined) {
|
||||
delete globalThis.__promptBuilderMvuExtensionSettings;
|
||||
} else {
|
||||
globalThis.__promptBuilderMvuExtensionSettings = originalExtensionSettings;
|
||||
}
|
||||
|
||||
if (originalContext === undefined) {
|
||||
delete globalThis.__promptBuilderMvuContext;
|
||||
} else {
|
||||
globalThis.__promptBuilderMvuContext = originalContext;
|
||||
}
|
||||
|
||||
if (originalSendOpenAIRequest === undefined) {
|
||||
delete globalThis.__promptBuilderMvuSendOpenAIRequest;
|
||||
} else {
|
||||
globalThis.__promptBuilderMvuSendOpenAIRequest = originalSendOpenAIRequest;
|
||||
}
|
||||
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { registerHooks } from "node:module";
|
||||
|
||||
const extensionsShimSource = [
|
||||
"export const extension_settings = {};",
|
||||
"export function getContext(...args) {",
|
||||
" return globalThis.SillyTavern?.getContext?.(...args) || null;",
|
||||
"}",
|
||||
@@ -147,6 +148,30 @@ const atDepthEntry = createWorldbookEntry({
|
||||
order: 5,
|
||||
});
|
||||
|
||||
const mvuTaggedEntry = createWorldbookEntry({
|
||||
uid: 9,
|
||||
name: "[mvu_update] 状态同步",
|
||||
comment: "MVU tagged",
|
||||
content: "这一条不应该进入结果。",
|
||||
order: 28,
|
||||
});
|
||||
|
||||
const mvuHeuristicEntry = createWorldbookEntry({
|
||||
uid: 10,
|
||||
name: "MVU 启发式条目",
|
||||
comment: "MVU heuristic",
|
||||
content: "<status_current_variable>secret=true</status_current_variable>",
|
||||
order: 29,
|
||||
});
|
||||
|
||||
const mvuLazyProbeEntry = createWorldbookEntry({
|
||||
uid: 11,
|
||||
name: "MVU 懒加载探测",
|
||||
comment: "MVU 懒加载探测",
|
||||
content: 'MVU lazy: <%= await getwi("bonus-book", "Bonus MVU") %>',
|
||||
order: 27,
|
||||
});
|
||||
|
||||
const bonusEntry = createWorldbookEntry({
|
||||
uid: 101,
|
||||
name: "Bonus 条目",
|
||||
@@ -155,6 +180,14 @@ const bonusEntry = createWorldbookEntry({
|
||||
order: 10,
|
||||
});
|
||||
|
||||
const bonusMvuEntry = createWorldbookEntry({
|
||||
uid: 102,
|
||||
name: "Bonus MVU",
|
||||
comment: "Bonus MVU",
|
||||
content: "变量更新规则:\ntype: sync\n当前时间: 12:00",
|
||||
order: 20,
|
||||
});
|
||||
|
||||
const worldbooksByName = {
|
||||
"main-book": [
|
||||
constantEntry,
|
||||
@@ -162,11 +195,14 @@ const worldbooksByName = {
|
||||
inlineSummaryEntry,
|
||||
extensionLiteralEntry,
|
||||
externalInlineEntry,
|
||||
mvuLazyProbeEntry,
|
||||
forceControlEntry,
|
||||
forcedAfterEntry,
|
||||
atDepthEntry,
|
||||
mvuTaggedEntry,
|
||||
mvuHeuristicEntry,
|
||||
],
|
||||
"bonus-book": [bonusEntry],
|
||||
"bonus-book": [bonusEntry, bonusMvuEntry],
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -217,16 +253,18 @@ try {
|
||||
|
||||
assert.deepEqual(
|
||||
worldInfo.beforeEntries.map((entry) => entry.name),
|
||||
["常驻设定", "EJS 汇总", "扩展语义正文", "外部书汇总"],
|
||||
["常驻设定", "EJS 汇总", "扩展语义正文", "外部书汇总", "MVU 懒加载探测"],
|
||||
);
|
||||
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, /MVU lazy:/);
|
||||
assert.match(worldInfo.beforeText, /@@generate/);
|
||||
assert.match(worldInfo.beforeText, /\[GENERATE:Test\]/);
|
||||
assert.doesNotMatch(worldInfo.beforeText, /getwi|<%=?/);
|
||||
assert.doesNotMatch(worldInfo.beforeText, /status_current_variable|变量更新规则|updatevariable/i);
|
||||
assert.equal(worldInfo.debug.ejsInlinePullCount, 2);
|
||||
assert.equal(worldInfo.debug.ejsForcedActivationCount, 1);
|
||||
assert.equal(worldInfo.debug.resolvePassCount >= 2, true);
|
||||
@@ -238,10 +276,23 @@ try {
|
||||
["Bonus 条目", "线索条目"].sort(),
|
||||
);
|
||||
assert.deepEqual(worldInfo.debug.lazyLoadedWorldbooks, ["bonus-book"]);
|
||||
assert.equal(worldInfo.debug.mvu.filteredEntryCount, 2);
|
||||
assert.equal(worldInfo.debug.mvu.lazyFilteredEntryCount, 1);
|
||||
assert.equal(worldInfo.debug.mvu.blockedContentsCount, 3);
|
||||
assert.deepEqual(
|
||||
worldInfo.debug.mvu.filteredEntries.map((entry) => entry.sourceName).sort(),
|
||||
["[mvu_update] 状态同步", "MVU 启发式条目", "Bonus MVU"].sort(),
|
||||
);
|
||||
assert.equal(
|
||||
worldInfo.debug.warnings.some((warning) => warning.includes("旧 EW 命名条目")),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
worldInfo.debug.recursionWarnings.some((warning) =>
|
||||
warning.includes("mvu filtered world info blocked"),
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
const settings = {
|
||||
taskProfiles: {
|
||||
@@ -299,7 +350,9 @@ try {
|
||||
assert.match(promptBuild.systemPrompt, /控制摘要:隐藏线索:Alice 正在调查/);
|
||||
assert.match(promptBuild.systemPrompt, /扩展语义只是普通文本/);
|
||||
assert.match(promptBuild.systemPrompt, /来自 bonus-book 的补充内容/);
|
||||
assert.match(promptBuild.systemPrompt, /MVU lazy:/);
|
||||
assert.doesNotMatch(promptBuild.systemPrompt, /getwi|<%=?/);
|
||||
assert.doesNotMatch(promptBuild.systemPrompt, /status_current_variable|变量更新规则|updatevariable/i);
|
||||
assert.equal(
|
||||
promptBuild.privateTaskMessages.length,
|
||||
2,
|
||||
@@ -311,7 +364,7 @@ try {
|
||||
);
|
||||
assert.deepEqual(
|
||||
promptBuild.hostInjections.before.map((entry) => entry.name),
|
||||
["常驻设定", "EJS 汇总", "扩展语义正文", "外部书汇总"],
|
||||
["常驻设定", "EJS 汇总", "扩展语义正文", "外部书汇总", "MVU 懒加载探测"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
promptBuild.hostInjections.after.map((entry) => entry.name),
|
||||
@@ -327,6 +380,7 @@ try {
|
||||
"EJS 汇总",
|
||||
"扩展语义正文",
|
||||
"外部书汇总",
|
||||
"MVU 懒加载探测",
|
||||
]);
|
||||
assert.equal(promptBuild.hostInjectionPlan.after.length, 1);
|
||||
assert.equal(promptBuild.hostInjectionPlan.after[0].blockId, "b2");
|
||||
@@ -346,6 +400,7 @@ try {
|
||||
);
|
||||
assert.equal(promptBuild.additionalMessages.length, 1);
|
||||
assert.equal(promptBuild.additionalMessages[0].content, "这是一条 atDepth 消息。");
|
||||
assert.equal(promptBuild.debug.mvu.sanitizedFieldCount >= 0, true);
|
||||
|
||||
const { initializeHostAdapter } = await import("../host-adapter/index.js");
|
||||
const partialBridgeCalls = [];
|
||||
|
||||
Reference in New Issue
Block a user