// 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?/gi; const MVU_STATUS_CURRENT_VARIABLE_REPLACE_REGEX = /\n?[\s\S]*?<\/status_current_variables?>/gi; const MVU_STATUS_CURRENT_VARIABLE_DETECT_REGEX = /[\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]*?/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, }; }