mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
用户原文字段(recentMessages/charDescription/userPersona/candidateNodes 等)
现在使用 passive mode 清洗——只剥离 MVU 容器/宏,不整段 drop。
这修复了含 MVU 状态栏角色卡时提取 LLM 收到空 context 的问题。
- mvu-compat.js: 导出 MVU_SANITIZE_MODES 常量,passive 分支显式注释
- prompt-builder.js: 加 INPUT_CONTEXT_FIELD_MODE 策略表,
sanitizePromptContextInputs 按字段族查表传 mode;
关键字段 omit 时 warn(兜底告警)
- 世界书条目路径(sanitizeWorldInfoEntries)保持 aggressive,守卫 6cec031 正收益
- 新增 6 条测试:passive 字段族不被整段 drop + worldInfo 仍 aggressive + warn 路径
Refs: mvu-aggressive-strip-regression-plan.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
247 lines
8.2 KiB
JavaScript
247 lines
8.2 KiB
JavaScript
// 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_SANITIZE_MODES = Object.freeze({
|
||
/** 整段 drop likely MVU 内容(用于世界书条目)。 */
|
||
AGGRESSIVE: "aggressive",
|
||
/** 只剥离 MVU 容器/宏,不整段 drop(用于用户原文、角色描述等任务输入字段)。 */
|
||
PASSIVE: "passive",
|
||
});
|
||
|
||
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_MESSAGE_VARIABLE_MACRO_REGEX =
|
||
/\{\{\s*get_message_variable::(?:stat_data|display_data|delta_data)(?:\.[^}]+)?\s*}}/gi;
|
||
const MVU_GETVAR_REFERENCE_REGEX =
|
||
/getvar\(\s*["'](?:stat_data|display_data|delta_data)["']\s*\)/gi;
|
||
const MVU_STATEFUL_TEMPLATE_TAG_REGEX =
|
||
/<%[-=]?[\s\S]*?(?:SafeGetValue|getvar\(\s*["'](?:stat_data|display_data|delta_data)["']\s*\)|\b(?:stat_data|display_data|delta_data)\b)[\s\S]*?%>/gi;
|
||
const EJS_TEMPLATE_TAG_REGEX = /<%[-=]?[\s\S]*?%>/gi;
|
||
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;
|
||
const MVU_STATE_OBJECT_FIELD_REGEX =
|
||
/["']?(?:stat_data|display_data|delta_data)["']?\s*:/i;
|
||
const MVU_STATE_PATH_REFERENCE_REGEX =
|
||
/\b(?:stat_data|display_data|delta_data)(?:\.[\w$\u4e00-\u9fff\[\]"'-]+){1,}/i;
|
||
const MVU_STATE_HELPER_REFERENCE_REGEX =
|
||
/\b(?:SafeGetValue\([^)]*(?:stat_data|display_data|delta_data)[^)]*\)|message_data\[\d+\]\.data\.(?:stat_data|display_data|delta_data))\b/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 matchesRegex(text = "", regex) {
|
||
if (!text || !(regex instanceof RegExp)) {
|
||
return false;
|
||
}
|
||
|
||
return new RegExp(regex.source, regex.flags).test(text);
|
||
}
|
||
|
||
function stripMvuPromptArtifactsDetailed(content = "") {
|
||
const input = normalizeText(content);
|
||
if (!input) {
|
||
return {
|
||
text: "",
|
||
changed: false,
|
||
artifactRemovedCount: 0,
|
||
};
|
||
}
|
||
|
||
const statefulTemplateTagCount = countRegexMatches(
|
||
input,
|
||
MVU_STATEFUL_TEMPLATE_TAG_REGEX,
|
||
);
|
||
const artifactRemovedCount =
|
||
countRegexMatches(input, MVU_UPDATE_BLOCK_REGEX) +
|
||
countRegexMatches(input, MVU_STATUS_PLACEHOLDER_REGEX) +
|
||
countRegexMatches(input, MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX) +
|
||
countRegexMatches(input, MVU_MESSAGE_VARIABLE_MACRO_REGEX) +
|
||
countRegexMatches(input, MVU_GETVAR_REFERENCE_REGEX) +
|
||
statefulTemplateTagCount;
|
||
|
||
let stripped = input
|
||
.replace(MVU_UPDATE_BLOCK_REGEX, "")
|
||
.replace(MVU_STATUS_PLACEHOLDER_REGEX, "")
|
||
.replace(MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX, "")
|
||
.replace(MVU_MESSAGE_VARIABLE_MACRO_REGEX, "")
|
||
.replace(MVU_GETVAR_REFERENCE_REGEX, "")
|
||
.replace(MVU_STATEFUL_TEMPLATE_TAG_REGEX, "");
|
||
if (statefulTemplateTagCount > 0) {
|
||
stripped = stripped.replace(EJS_TEMPLATE_TAG_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;
|
||
}
|
||
const stateKeyMentionCount =
|
||
normalized.match(/\b(?:stat_data|display_data|delta_data)\b/gi)?.length || 0;
|
||
|
||
const stateSignals = [
|
||
MVU_MESSAGE_VARIABLE_MACRO_REGEX,
|
||
MVU_GETVAR_REFERENCE_REGEX,
|
||
MVU_STATE_OBJECT_FIELD_REGEX,
|
||
MVU_STATE_PATH_REFERENCE_REGEX,
|
||
MVU_STATE_HELPER_REFERENCE_REGEX,
|
||
].reduce(
|
||
(count, pattern) => count + (matchesRegex(normalized, pattern) ? 1 : 0),
|
||
0,
|
||
);
|
||
|
||
return (
|
||
matchesRegex(normalized, MVU_STATUS_CURRENT_VARIABLE_DETECT_REGEX) ||
|
||
matchesRegex(normalized, MVU_VARIABLE_OUTPUT_ENTRY_REGEX) ||
|
||
matchesRegex(normalized, MVU_VARIABLE_RULES_ENTRY_REGEX) ||
|
||
matchesRegex(normalized, MVU_FORMAT_EMPHASIS_ENTRY_REGEX) ||
|
||
stateSignals >= 2 ||
|
||
(stateSignals >= 1 && stateKeyMentionCount >= 2)
|
||
);
|
||
}
|
||
|
||
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 === MVU_SANITIZE_MODES.AGGRESSIVE) {
|
||
// 整段 drop:用于世界书条目,不用于用户原文字段
|
||
if (
|
||
isLikelyMvuWorldInfoContent(originalCollapsed) ||
|
||
isLikelyMvuWorldInfoContent(text)
|
||
) {
|
||
text = "";
|
||
dropped = true;
|
||
reasons.push("likely_mvu_content");
|
||
}
|
||
}
|
||
// MVU_SANITIZE_MODES.PASSIVE:只做 artifact 剥离 + blocked 过滤,不整段 drop。
|
||
|
||
return {
|
||
text: collapseWhitespace(text),
|
||
changed: collapseWhitespace(text) !== originalCollapsed,
|
||
dropped,
|
||
reasons: uniq(reasons),
|
||
blockedHitCount: blockedResult.blockedHitCount,
|
||
artifactRemovedCount: artifactResult.artifactRemovedCount,
|
||
};
|
||
}
|