Refactor host regex reuse and injection sanitization

This commit is contained in:
Youzini-afk
2026-04-08 00:10:20 +08:00
parent 960e48667c
commit d8cff92434
11 changed files with 1363 additions and 356 deletions

View File

@@ -2,7 +2,11 @@ import { buildCapabilityStatus, mergeVersionHints } from "./capabilities.js";
import { createContextHostFacade } from "./context.js";
import { debugDebug } from "../debug-logging.js";
const REGEX_API_NAMES = ["getTavernRegexes", "isCharacterTavernRegexesEnabled"];
const REGEX_API_NAMES = [
"getTavernRegexes",
"isCharacterTavernRegexesEnabled",
"formatAsTavernRegexedString",
];
function isObjectLike(value) {
return (
@@ -182,7 +186,9 @@ function resolveRegexSource(options = {}, contextHost = null) {
return (
records.find(
(record) => typeof record.apiMap.getTavernRegexes === "function",
(record) =>
typeof record.apiMap.getTavernRegexes === "function" ||
typeof record.apiMap.formatAsTavernRegexedString === "function",
) ||
buildSourceRecord({
label: "none",
@@ -193,13 +199,21 @@ function resolveRegexSource(options = {}, contextHost = null) {
}
function detectRegexMode(apiMap = {}) {
if (typeof apiMap.getTavernRegexes !== "function") {
const hasGetter = typeof apiMap.getTavernRegexes === "function";
const hasFormatter =
typeof apiMap.formatAsTavernRegexedString === "function";
if (!hasGetter && !hasFormatter) {
return "unavailable";
}
return typeof apiMap.isCharacterTavernRegexesEnabled === "function"
? "full"
: "partial";
if (hasGetter && hasFormatter) {
return typeof apiMap.isCharacterTavernRegexesEnabled === "function"
? "full"
: "partial";
}
return hasFormatter ? "formatter-only" : "getter-only";
}
function buildFallbackReason(sourceRecord, available, mode) {
@@ -219,6 +233,14 @@ function buildFallbackReason(sourceRecord, available, mode) {
return `Tavern Regex 桥接仅发现部分接口,来源: ${sourceRecord?.label || "unknown"}`;
}
if (mode === "formatter-only") {
return `Tavern Regex 桥接仅发现 formatter 接口,来源: ${sourceRecord?.label || "unknown"}`;
}
if (mode === "getter-only") {
return `Tavern Regex 桥接仅发现规则读取接口,来源: ${sourceRecord?.label || "unknown"}`;
}
return "";
}
@@ -253,6 +275,8 @@ export function createRegexHostFacade(options = {}) {
getTavernRegexes: sourceRecord.apiMap.getTavernRegexes,
isCharacterTavernRegexesEnabled:
sourceRecord.apiMap.isCharacterTavernRegexesEnabled,
formatAsTavernRegexedString:
sourceRecord.apiMap.formatAsTavernRegexedString,
getApi(name) {
return sourceRecord.apiMap[String(name || "")] || null;
},
@@ -271,6 +295,8 @@ export function createRegexHostFacade(options = {}) {
source: sourceRecord.sourceKind,
sourceLabel: sourceRecord.label,
fallback: sourceRecord.fallback,
formatterAvailable:
typeof sourceRecord.apiMap.formatAsTavernRegexedString === "function",
});
},
});

463
injection-sanitizer.js Normal file
View File

@@ -0,0 +1,463 @@
import { sanitizeMvuContent } from "./mvu-compat.js";
import { applyHostRegexReuse } from "./task-regex.js";
export const PROMPT_CONTENT_ORIGIN = Object.freeze({
TEMPLATE_OWNED: "template-owned",
HOST_INJECTED: "host-injected",
WORLD_INFO_RENDERED: "world-info-rendered",
});
function normalizeSanitizerMode(mode = "injection-safe") {
return String(mode || "").trim() === "final-injection-safe"
? "final-safe"
: "aggressive";
}
function isSanitizationEligible(options = {}) {
if (options?.sanitizationEligible === false) {
return false;
}
return String(options?.contentOrigin || "") !== PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED;
}
function normalizeReasons(reasons = []) {
return Array.isArray(reasons)
? reasons.map((item) => String(item || "").trim()).filter(Boolean)
: [];
}
function pushUnique(target = [], value = "") {
const normalized = String(value || "").trim();
if (!normalized || target.includes(normalized)) {
return;
}
target.push(normalized);
}
export function createEmptyInjectionSanitizerDebug() {
return {
sanitizedFieldCount: 0,
sanitizedFields: [],
finalMessageStripCount: 0,
worldInfoBlockedContentHits: 0,
sanitizerAppliedFields: [],
sanitizerHitKinds: [],
hostReuseAppliedFields: [],
hostReuseSkippedDisplayOnlyRules: 0,
regexExecutionMode: "host-unavailable",
hostFormatterAvailable: false,
hostFormatterSource: "",
fallbackReason: "",
};
}
function recordSanitizerDebug(debugState, path, result = {}, stage = "") {
if (!debugState || (!result.changed && !result.dropped)) {
return;
}
const reasons = normalizeReasons(result.reasons);
debugState.sanitizedFields.push({
name: String(path || ""),
stage: String(stage || ""),
changed: Boolean(result.changed),
dropped: Boolean(result.dropped),
reasons,
blockedHitCount: Number(result.blockedHitCount || 0),
});
debugState.sanitizedFieldCount = debugState.sanitizedFields.length;
pushUnique(debugState.sanitizerAppliedFields, path);
for (const reason of reasons) {
pushUnique(debugState.sanitizerHitKinds, reason);
}
}
function recordHostReuseDebug(debugState, path, result = {}) {
if (!debugState || !result || typeof result !== "object") {
return;
}
debugState.regexExecutionMode = String(
result.executionMode || debugState.regexExecutionMode || "host-unavailable",
);
debugState.hostFormatterAvailable = Boolean(result.formatterAvailable);
debugState.hostFormatterSource = String(result.formatterSource || "");
debugState.fallbackReason = String(result.fallbackReason || "");
debugState.hostReuseSkippedDisplayOnlyRules = Math.max(
Number(debugState.hostReuseSkippedDisplayOnlyRules || 0),
Number(result.skippedDisplayOnlyRuleCount || 0),
);
if (result.changed) {
pushUnique(debugState.hostReuseAppliedFields, path);
}
}
export function sanitizeInjectionText(
settings = {},
taskType,
text,
{
mode = "injection-safe",
blockedContents = [],
contentOrigin = PROMPT_CONTENT_ORIGIN.HOST_INJECTED,
sanitizationEligible = true,
regexSourceType = "",
role = "system",
formatterOptions = null,
debugState = null,
regexCollector = null,
applySanitizer = true,
applyHostRegex = true,
path = "",
stage = "",
} = {},
) {
const originalText = typeof text === "string" ? text : "";
const eligible = sanitizationEligible && isSanitizationEligible({
sanitizationEligible,
contentOrigin,
});
const sanitizerResult = eligible && applySanitizer
? sanitizeMvuContent(originalText, {
mode: normalizeSanitizerMode(mode),
blockedContents,
})
: {
text: originalText,
changed: false,
dropped: false,
reasons: [],
blockedHitCount: 0,
artifactRemovedCount: 0,
};
recordSanitizerDebug(debugState, path, sanitizerResult, stage);
const afterSanitizer = String(sanitizerResult.text || "");
const hostReuseResult = eligible && applyHostRegex && regexSourceType
? applyHostRegexReuse(settings, taskType, afterSanitizer, {
sourceType: regexSourceType,
role,
debugCollector: regexCollector,
formatterOptions,
})
: {
text: afterSanitizer,
changed: false,
executionMode: "host-unavailable",
formatterAvailable: false,
formatterSource: "",
fallbackReason: "",
skippedDisplayOnlyRuleCount: 0,
};
recordHostReuseDebug(debugState, path, hostReuseResult);
const finalText = String(hostReuseResult.text || "");
return {
text: finalText,
changed: finalText !== originalText,
dropped: Boolean(sanitizerResult.dropped),
reasons: normalizeReasons(sanitizerResult.reasons),
blockedHitCount: Number(sanitizerResult.blockedHitCount || 0),
artifactRemovedCount: Number(sanitizerResult.artifactRemovedCount || 0),
hostReuseChanged: Boolean(hostReuseResult.changed),
executionMode: String(hostReuseResult.executionMode || "host-unavailable"),
formatterAvailable: Boolean(hostReuseResult.formatterAvailable),
formatterSource: String(hostReuseResult.formatterSource || ""),
fallbackReason: String(hostReuseResult.fallbackReason || ""),
skippedDisplayOnlyRuleCount: Number(
hostReuseResult.skippedDisplayOnlyRuleCount || 0,
),
};
}
function looksLikeMvuStateContainer(value, seen = new WeakSet()) {
if (!value || typeof value !== "object") {
return false;
}
if (seen.has(value)) {
return false;
}
seen.add(value);
if (Array.isArray(value)) {
return value.some((item) => looksLikeMvuStateContainer(item, seen));
}
const keys = Object.keys(value).map((key) =>
String(key || "").trim().toLowerCase(),
);
if (
keys.some((key) =>
["stat_data", "display_data", "delta_data", "$internal"].includes(key),
)
) {
return true;
}
return Object.values(value).some((item) => looksLikeMvuStateContainer(item, seen));
}
function getMvuObjectKeyStripReason(key, value) {
const normalizedKey = String(key || "").trim().toLowerCase();
if (
["stat_data", "display_data", "delta_data", "$internal"].includes(
normalizedKey,
)
) {
return "mvu_state_key_removed";
}
if (
["variables", "message_variables", "chat_variables"].includes(normalizedKey) &&
looksLikeMvuStateContainer(value)
) {
return "mvu_variables_container_removed";
}
return "";
}
function joinStructuredPath(basePath = "", segment = "") {
const normalizedSegment = String(segment || "");
if (!normalizedSegment) {
return basePath;
}
if (!basePath) {
return normalizedSegment.startsWith("[")
? normalizedSegment.slice(1, -1)
: normalizedSegment;
}
return normalizedSegment.startsWith("[")
? `${basePath}${normalizedSegment}`
: `${basePath}.${normalizedSegment}`;
}
export function sanitizeInjectionStructuredValue(
settings = {},
taskType,
value,
{
fieldName = "",
path = fieldName,
mode = "injection-safe",
blockedContents = [],
contentOrigin = PROMPT_CONTENT_ORIGIN.HOST_INJECTED,
sanitizationEligible = true,
regexSourceType = "",
role = "system",
formatterOptions = null,
debugState = null,
regexCollector = null,
applySanitizer = true,
applyHostRegex = true,
stripMvuContainers = true,
seen = new WeakSet(),
} = {},
) {
if (typeof value === "string") {
const sanitized = sanitizeInjectionText(settings, taskType, value, {
mode,
blockedContents,
contentOrigin,
sanitizationEligible,
regexSourceType,
role,
formatterOptions,
debugState,
regexCollector,
applySanitizer,
applyHostRegex,
path,
stage: mode,
});
return {
value: sanitized.text,
changed: Boolean(sanitized.changed || sanitized.dropped),
omit:
!String(sanitized.text || "").trim() &&
String(value || "").trim().length > 0,
details: sanitized,
};
}
if (Array.isArray(value)) {
const sanitizedArray = [];
let changed = false;
for (let index = 0; index < value.length; index += 1) {
const childResult = sanitizeInjectionStructuredValue(
settings,
taskType,
value[index],
{
fieldName,
path: joinStructuredPath(path, `[${index}]`),
mode,
blockedContents,
contentOrigin,
sanitizationEligible,
regexSourceType,
role,
formatterOptions,
debugState,
regexCollector,
applySanitizer,
applyHostRegex,
stripMvuContainers,
seen,
},
);
if (childResult.omit) {
changed = true;
continue;
}
sanitizedArray.push(childResult.value);
if (childResult.changed) {
changed = true;
}
}
return {
value: sanitizedArray,
changed: changed || sanitizedArray.length !== value.length,
omit: value.length > 0 && sanitizedArray.length === 0,
details: null,
};
}
if (value && typeof value === "object") {
if (seen.has(value)) {
return {
value,
changed: false,
omit: false,
details: null,
};
}
seen.add(value);
const originalLooksMvuContainer = looksLikeMvuStateContainer(value);
const sanitizedObject = {};
let changed = false;
let keptEntries = 0;
for (const [key, entryValue] of Object.entries(value)) {
const stripReason = stripMvuContainers
? getMvuObjectKeyStripReason(key, entryValue)
: "";
if (stripReason) {
changed = true;
recordSanitizerDebug(
debugState,
joinStructuredPath(path, key),
{
changed: true,
dropped: true,
reasons: [stripReason],
blockedHitCount: 0,
},
mode,
);
continue;
}
const childResult = sanitizeInjectionStructuredValue(
settings,
taskType,
entryValue,
{
fieldName,
path: joinStructuredPath(path, key),
mode,
blockedContents,
contentOrigin,
sanitizationEligible,
regexSourceType,
role,
formatterOptions,
debugState,
regexCollector,
applySanitizer,
applyHostRegex,
stripMvuContainers,
seen,
},
);
if (childResult.omit) {
changed = true;
continue;
}
sanitizedObject[key] = childResult.value;
keptEntries += 1;
if (childResult.changed) {
changed = true;
}
}
return {
value: sanitizedObject,
changed,
omit: originalLooksMvuContainer && keptEntries === 0,
details: null,
};
}
return {
value,
changed: false,
omit: false,
details: null,
};
}
export function sanitizeInjectionMessages(
settings = {},
taskType,
messages = [],
{
blockedContents = [],
debugState = null,
regexCollector = null,
} = {},
) {
return (Array.isArray(messages) ? messages : [])
.map((message, index) => {
const contentOrigin = String(message?.contentOrigin || "").trim() ||
PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED;
const sanitizationEligible =
message?.sanitizationEligible === true &&
contentOrigin !== PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED;
if (!sanitizationEligible) {
return message;
}
const sanitized = sanitizeInjectionText(
settings,
taskType,
String(message?.content || ""),
{
mode: "final-injection-safe",
blockedContents,
contentOrigin,
sanitizationEligible,
regexSourceType: String(message?.regexSourceType || ""),
role: message?.role || "system",
debugState,
regexCollector,
applySanitizer: true,
applyHostRegex: false,
path: `message[${index}]`,
stage: "final-injection-safe",
},
);
if (debugState && (sanitized.changed || sanitized.dropped)) {
debugState.finalMessageStripCount += 1;
}
if (!String(sanitized.text || "").trim()) {
return null;
}
return {
...message,
content: sanitized.text,
};
})
.filter(Boolean);
}

13
llm.js
View File

@@ -178,9 +178,17 @@ function normalizeRegexDebugEntries(debugCollector = null) {
return [];
}
return debugCollector.entries.map((entry) => ({
kind: String(entry?.kind || "local-regex"),
taskType: String(entry?.taskType || ""),
stage: String(entry?.stage || ""),
enabled: entry?.enabled !== false,
executionMode: String(entry?.executionMode || ""),
formatterAvailable: Boolean(entry?.formatterAvailable),
hostFormatterSource: String(entry?.hostFormatterSource || ""),
fallbackReason: String(entry?.fallbackReason || ""),
skippedDisplayOnlyRuleCount: Number(
entry?.skippedDisplayOnlyRuleCount || 0,
),
appliedRules: Array.isArray(entry?.appliedRules)
? entry.appliedRules.map((rule) => ({
id: String(rule?.id || ""),
@@ -278,6 +286,7 @@ function applyTaskFinalInputRegex(taskType, messages = []) {
rawMessageCount: normalizedMessages.length,
cleanedMessageCount: cleanedMessages.length,
droppedMessageCount: normalizedMessages.length - cleanedMessages.length,
finalPromptLocalRuleCount: 0,
stages: [],
},
};
@@ -325,6 +334,10 @@ function applyTaskFinalInputRegex(taskType, messages = []) {
rawMessageCount: normalizedMessages.length,
cleanedMessageCount: cleanedMessages.length,
droppedMessageCount,
finalPromptLocalRuleCount: normalizedEntries.reduce(
(sum, entry) => sum + Number(entry?.sourceCount?.local || 0),
0,
),
stages: normalizedEntries,
},
};

View File

@@ -1,11 +1,11 @@
{
"display_name": "ST-BME Memory Graph",
"loading_order": 150,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Youzini",
"version": "1.0.0",
"homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology"
"display_name": "ST-BME Memory Graph",
"loading_order": 150,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Youzini",
"version": "3.6.4",
"homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology"
}

View File

@@ -4273,8 +4273,11 @@ function _formatRegexReuseSourceLabel(sourceType = "") {
}
function _formatRegexReuseReplaceText(rule = {}) {
if (rule.promptStageMode === "clear") {
return "美化/展示正则ST-BME 请求阶段清空";
if (rule.promptStageMode === "display-only") {
return "仅显示类规则,不进入 Memory LLM 请求";
}
if (rule.promptStageMode === "fallback-skip-beautify") {
return "美化型替换fallback 模式下不会进入 Prompt";
}
if (typeof rule.effectivePromptReplaceString === "string" && rule.effectivePromptReplaceString.length > 0) {
return rule.effectivePromptReplaceString;
@@ -4287,20 +4290,35 @@ function _formatRegexReuseReplaceText(rule = {}) {
function _renderRegexReuseBadges(rule = {}) {
const badges = [];
if (rule.promptStageMode === "clear") {
if (rule.promptStageMode === "display-only") {
badges.push({
className: "is-clear",
text: "美化 -> 清空",
text: "仅显示",
});
} else if (rule.promptStageMode === "host-real") {
badges.push({
className: "is-transform",
text: "宿主真实执行",
});
} else if (rule.promptStageMode === "host-fallback") {
badges.push({
className: "is-prompt",
text: "插件兼容执行",
});
} else if (rule.promptStageMode === "fallback-skip-beautify") {
badges.push({
className: "is-skip",
text: "fallback 跳过美化",
});
} else if (rule.promptStageMode === "replace") {
badges.push({
className: "is-transform",
text: "转义",
text: "本地最终正则",
});
} else {
badges.push({
className: "is-skip",
text: "当前阶段跳过",
text: "当前不执行",
});
}
if (rule.markdownOnly) {
@@ -4415,10 +4433,10 @@ function _buildRegexReusePopupContent(snapshot = {}) {
container.innerHTML = `
<div class="bme-task-tab-body bme-regex-preview-screen">
<div class="bme-regex-preview-hero">
<div class="bme-regex-preview-hero">
<div class="bme-regex-preview-hero__title">当前正则脚本一览</div>
<div class="bme-regex-preview-hero__subtitle">
这里展示的是当前任务预设下ST-BME 实际会复用到请求链里的 Tavern 正则。展示/美化类规则在请求阶段会按空字符串替换
这里展示的是当前任务预设下ST-BME 对宿主注入内容会复用哪些 Tavern 正则,以及最终发送前还会执行哪些本地任务正则
</div>
<div class="bme-regex-preview-summary">
<div class="bme-regex-preview-summary__item">
@@ -4443,7 +4461,7 @@ function _buildRegexReusePopupContent(snapshot = {}) {
</div>
<div class="bme-regex-preview-summary__item">
<span class="bme-regex-preview-summary__label">桥接模式</span>
<span class="bme-regex-preview-summary__value">${_escHtml(snapshot.host?.sourceLabel || "unknown")} · ${_escHtml(snapshot.host?.capabilityStatus?.mode || snapshot.host?.mode || "unknown")}${snapshot.host?.fallback ? " · fallback" : ""}</span>
<span class="bme-regex-preview-summary__value">${_escHtml(snapshot.host?.sourceLabel || "unknown")} · ${_escHtml(snapshot.host?.executionMode || snapshot.host?.capabilityStatus?.mode || snapshot.host?.mode || "unknown")}${snapshot.host?.formatterAvailable ? " · formatter" : ""}${snapshot.host?.fallback ? " · fallback" : ""}</span>
</div>
</div>
</div>
@@ -4451,8 +4469,8 @@ function _buildRegexReusePopupContent(snapshot = {}) {
<div class="bme-regex-preview-panel">
<div class="bme-regex-preview-panel__head">
<div>
<div class="bme-regex-preview-panel__title">当前启用规则</div>
<div class="bme-regex-preview-panel__subtitle">EW 风格平铺展示,优先看你这次请求里真正会进入链路的规则。</div>
<div class="bme-regex-preview-panel__title">宿主注入复用规则</div>
<div class="bme-regex-preview-panel__subtitle">这里只显示会参与“宿主注入文本”处理的 Tavern 规则;仅显示类规则会明确标注出来。</div>
</div>
</div>
<div class="bme-task-note">
@@ -4466,6 +4484,20 @@ function _buildRegexReusePopupContent(snapshot = {}) {
</div>
</div>
<div class="bme-regex-preview-panel">
<div class="bme-regex-preview-panel__head">
<div>
<div class="bme-regex-preview-panel__title">任务本地最终正则</div>
<div class="bme-regex-preview-panel__subtitle">这一组只在最终请求发送前的 `input.finalPrompt` 阶段执行,不参与宿主注入清洗。</div>
</div>
</div>
<div class="bme-regex-preview-list">
${_renderRegexReuseRuleList(snapshot.localRules, "当前没有任务本地最终正则", {
showSource: false,
})}
</div>
</div>
<details class="bme-debug-details bme-regex-preview-details">
<summary>来源与排除明细</summary>
<div class="bme-regex-preview-details__body">

View File

@@ -3,7 +3,13 @@
import { debugLog, debugWarn } from "./debug-logging.js";
import { getActiveTaskProfile, getLegacyPromptForTask } from "./prompt-profiles.js";
import { sanitizeMvuContent } from "./mvu-compat.js";
import {
createEmptyInjectionSanitizerDebug,
PROMPT_CONTENT_ORIGIN,
sanitizeInjectionText,
sanitizeInjectionMessages,
sanitizeInjectionStructuredValue,
} from "./injection-sanitizer.js";
import { resolveTaskWorldInfo } from "./task-worldinfo.js";
import { applyTaskRegex } from "./task-regex.js";
@@ -54,6 +60,22 @@ const INPUT_REGEX_ROLE_BY_FIELD = {
dialogueText: "mixed",
};
const INPUT_HOST_REGEX_SOURCE_BY_FIELD = {
userMessage: "user_input",
recentMessages: "ai_output",
chatMessages: "ai_output",
dialogueText: "ai_output",
candidateText: "ai_output",
candidateNodes: "ai_output",
nodeContent: "ai_output",
eventSummary: "ai_output",
characterSummary: "ai_output",
threadSummary: "ai_output",
contradictionSummary: "ai_output",
charDescription: "ai_output",
userPersona: "user_input",
};
function cloneRuntimeDebugValue(value, fallback = null) {
if (value == null) {
return fallback;
@@ -275,12 +297,7 @@ function buildEmptyWorldInfoContext() {
}
function createEmptyMvuPromptDebug() {
return {
sanitizedFieldCount: 0,
sanitizedFields: [],
finalMessageStripCount: 0,
worldInfoBlockedContentHits: 0,
};
return createEmptyInjectionSanitizerDebug();
}
function pushMvuPromptDebugEntry(debugState, entry = {}) {
@@ -304,47 +321,46 @@ function sanitizeTaskPromptText(
taskType,
text,
{
mode = "aggressive",
mode = "injection-safe",
blockedContents = [],
regexStage = "",
role = "system",
regexCollector = null,
applyMvu = true,
contentOrigin = PROMPT_CONTENT_ORIGIN.HOST_INJECTED,
sanitizationEligible = true,
regexSourceType = "",
} = {},
) {
const originalText = typeof text === "string" ? text : "";
const mvuResult = applyMvu
? sanitizeMvuContent(originalText, {
mode,
blockedContents,
})
: {
text: originalText,
changed: false,
dropped: false,
reasons: [],
blockedHitCount: 0,
artifactRemovedCount: 0,
};
const afterMvu = String(mvuResult.text || "");
const sanitized = sanitizeInjectionText(settings, taskType, text, {
mode:
String(mode || "").trim() === "final-safe"
? "final-injection-safe"
: "injection-safe",
blockedContents,
contentOrigin,
sanitizationEligible,
regexSourceType,
role,
regexCollector,
applySanitizer: applyMvu,
applyHostRegex: Boolean(regexSourceType),
});
const finalText = regexStage
? applyTaskRegex(
settings,
taskType,
regexStage,
afterMvu,
sanitized.text,
regexCollector,
role,
)
: afterMvu;
: sanitized.text;
return {
...sanitized,
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),
changed: finalText !== String(text || ""),
};
}
@@ -570,64 +586,49 @@ function sanitizePromptMessages(
messages = [],
{
blockedContents = [],
regexStage = "",
debugState = null,
regexCollector = null,
applyMvu = true,
applySanitizer = null,
} = {},
) {
return (Array.isArray(messages) ? messages : [])
.map((message, index) => {
const messageApplyMvu =
typeof applyMvu === "function" ? applyMvu(message, index) : applyMvu;
const sanitized = sanitizeStructuredPromptValue(
settings,
taskType,
message,
{
fieldName: "message",
path: `message[${index}]`,
mode: "final-safe",
blockedContents,
regexStage,
role: message?.role || "system",
debugState,
regexCollector,
applyMvu: messageApplyMvu,
stripMvuContainers: messageApplyMvu,
},
);
if (debugState && (sanitized.changed || sanitized.omit)) {
debugState.finalMessageStripCount += 1;
const preparedMessages = (Array.isArray(messages) ? messages : []).map(
(message, index) => {
if (!message || typeof message !== "object") {
return message;
}
if (sanitized.omit) {
return null;
const shouldSanitize =
typeof applySanitizer === "function"
? applySanitizer(message, index)
: applySanitizer;
if (shouldSanitize === false) {
return {
...message,
sanitizationEligible: false,
};
}
const executionMessage = createExecutionMessage(
sanitized.value?.role || message?.role,
sanitized.value?.content,
{
source: String(sanitized.value?.source || message?.source || ""),
blockId: String(sanitized.value?.blockId || message?.blockId || ""),
blockName: String(
sanitized.value?.blockName || message?.blockName || "",
),
blockType: String(
sanitized.value?.blockType || message?.blockType || "",
),
sourceKey: String(
sanitized.value?.sourceKey || message?.sourceKey || "",
),
injectionMode: String(
sanitized.value?.injectionMode || message?.injectionMode || "",
),
derivedFromWorldInfo:
sanitized.value?.derivedFromWorldInfo === true ||
message?.derivedFromWorldInfo === true,
},
);
return executionMessage;
})
return message;
},
);
return sanitizeInjectionMessages(settings, taskType, preparedMessages, {
blockedContents,
debugState,
regexCollector,
})
.map((message) =>
createExecutionMessage(message.role, message.content, {
source: String(message?.source || ""),
blockId: String(message?.blockId || ""),
blockName: String(message?.blockName || ""),
blockType: String(message?.blockType || ""),
sourceKey: String(message?.sourceKey || ""),
injectionMode: String(message?.injectionMode || ""),
derivedFromWorldInfo: message?.derivedFromWorldInfo === true,
contentOrigin: String(message?.contentOrigin || ""),
sanitizationEligible: message?.sanitizationEligible === true,
regexSourceType: String(message?.regexSourceType || ""),
}),
)
.filter(Boolean);
}
@@ -647,6 +648,50 @@ function sanitizePromptContextInputs(
stripMvuContainers = applyMvu,
} = options || {};
const applyLocalRegexToStructuredValue = (
value,
regexStage,
regexRole,
seen = new WeakSet(),
) => {
if (!regexStage) {
return value;
}
if (typeof value === "string") {
return applyTaskRegex(
settings,
taskType,
regexStage,
value,
regexCollector,
regexRole,
);
}
if (Array.isArray(value)) {
return value.map((item) =>
applyLocalRegexToStructuredValue(item, regexStage, regexRole, seen),
);
}
if (value && typeof value === "object") {
if (seen.has(value)) {
return value;
}
seen.add(value);
return Object.fromEntries(
Object.entries(value).map(([key, entryValue]) => [
key,
applyLocalRegexToStructuredValue(
entryValue,
regexStage,
regexRole,
seen,
),
]),
);
}
return value;
};
for (const fieldName of INPUT_CONTEXT_MVU_FIELDS) {
if (!(fieldName in sanitizedContext)) {
continue;
@@ -654,29 +699,39 @@ function sanitizePromptContextInputs(
const value = sanitizedContext[fieldName];
const regexStage = INPUT_REGEX_STAGE_BY_FIELD[fieldName] || "";
const regexRole = INPUT_REGEX_ROLE_BY_FIELD[fieldName] || "system";
const sanitized = sanitizeStructuredPromptValue(
const regexSourceType = INPUT_HOST_REGEX_SOURCE_BY_FIELD[fieldName] || "";
const sanitized = sanitizeInjectionStructuredValue(
settings,
taskType,
value,
{
fieldName,
path: fieldName,
mode: "aggressive",
regexStage,
mode: "injection-safe",
contentOrigin: PROMPT_CONTENT_ORIGIN.HOST_INJECTED,
sanitizationEligible: true,
regexSourceType,
role: regexRole,
debugState,
regexCollector,
applyMvu,
applySanitizer: applyMvu,
applyHostRegex: Boolean(regexSourceType),
stripMvuContainers,
},
);
sanitizedContext[fieldName] = sanitized.omit
let sanitizedValue = sanitized.omit
? Array.isArray(value)
? []
: typeof value === "string"
? ""
: null
: sanitized.value;
sanitizedValue = applyLocalRegexToStructuredValue(
sanitizedValue,
regexStage,
regexRole,
);
sanitizedContext[fieldName] = sanitizedValue;
}
return sanitizedContext;
@@ -693,17 +748,22 @@ function sanitizeWorldInfoEntries(
) {
return (Array.isArray(entries) ? entries : [])
.map((entry, index) => {
const sanitized = sanitizeTaskPromptText(
const sanitized = sanitizeInjectionText(
settings,
taskType,
String(entry?.content || ""),
{
mode: "aggressive",
mode: "injection-safe",
blockedContents,
regexStage: "",
contentOrigin: PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED,
sanitizationEligible: true,
regexSourceType: "world_info",
role: entry?.role || "system",
regexCollector,
applyMvu,
applySanitizer: applyMvu,
applyHostRegex: true,
path: `worldInfo[${index}]`,
stage: "world-info-rendered",
},
);
debugState.worldInfoBlockedContentHits += sanitized.blockedHitCount;
@@ -780,17 +840,22 @@ function sanitizeWorldInfoContext(
: []
)
.map((message) => {
const sanitized = sanitizeTaskPromptText(
const sanitized = sanitizeInjectionText(
settings,
taskType,
String(message?.content || ""),
{
mode: "aggressive",
mode: "injection-safe",
blockedContents: runtimeBlockedContents,
regexStage: "",
contentOrigin: PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED,
sanitizationEligible: true,
regexSourceType: "world_info",
role: message?.role || "system",
regexCollector,
applyMvu: !isCustomFilter,
applySanitizer: !isCustomFilter,
applyHostRegex: true,
path: "taskAdditionalMessages",
stage: "world-info-rendered",
},
);
debugState.worldInfoBlockedContentHits += sanitized.blockedHitCount;
@@ -805,6 +870,9 @@ function sanitizeWorldInfoContext(
content: sanitized.text,
source: String(message?.source || "worldInfo-atDepth"),
sourceKey: String(message?.sourceKey || "taskAdditionalMessages"),
contentOrigin: PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED,
sanitizationEligible: true,
regexSourceType: "world_info",
};
})
.filter(Boolean);
@@ -1000,6 +1068,54 @@ function buildHostInjectionPlan(renderedBlocks = [], worldInfoResolution = {}) {
};
}
function getPromptFieldContentOrigin(sourceKey = "") {
const normalizedSourceKey = String(sourceKey || "").trim();
if (!normalizedSourceKey) {
return PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED;
}
if (WORLD_INFO_VARIABLE_KEYS.includes(normalizedSourceKey)) {
return PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED;
}
if (INPUT_CONTEXT_MVU_FIELDS.includes(normalizedSourceKey)) {
return PROMPT_CONTENT_ORIGIN.HOST_INJECTED;
}
return PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED;
}
function getPromptFieldRegexSourceType(sourceKey = "") {
const normalizedSourceKey = String(sourceKey || "").trim();
if (!normalizedSourceKey) {
return "";
}
if (WORLD_INFO_VARIABLE_KEYS.includes(normalizedSourceKey)) {
return "world_info";
}
return INPUT_HOST_REGEX_SOURCE_BY_FIELD[normalizedSourceKey] || "";
}
function blockIsPureInjectedContent(block = {}) {
return (
block?.type === "builtin" &&
!String(block?.content || "").trim() &&
String(block?.sourceKey || "").trim().length > 0
);
}
function describeBlockContentOwnership(block = {}) {
const contentOrigin = blockIsPureInjectedContent(block)
? getPromptFieldContentOrigin(block.sourceKey)
: PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED;
return {
contentOrigin,
sanitizationEligible:
contentOrigin !== PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED,
regexSourceType:
contentOrigin === PROMPT_CONTENT_ORIGIN.TEMPLATE_OWNED
? ""
: getPromptFieldRegexSourceType(block.sourceKey),
};
}
function resolveBlockDelivery(block = {}) {
return normalizeRole(block.role) === "system"
? "private.system"
@@ -1069,19 +1185,10 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
const profile = getActiveTaskProfile(settings, taskType);
const legacyPrompt = getLegacyPromptForTask(settings, taskType);
const promptRegexInput = { entries: [] };
const worldInfoRegexInput = { entries: [] };
const mvuPromptDebug = createEmptyMvuPromptDebug();
const worldInfoInputContext = sanitizePromptContextInputs(
settings,
taskType,
context,
null,
worldInfoRegexInput,
{
applyMvu: false,
stripMvuContainers: false,
},
);
const worldInfoInputContext = {
...context,
};
const sanitizedInputContext = sanitizePromptContextInputs(
settings,
taskType,
@@ -1164,6 +1271,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
const role = normalizeRole(block.role);
const blockDerivedFromWorldInfo = blockUsesWorldInfoContent(block);
const blockOwnership = describeBlockContentOwnership(block);
let content = "";
if (block.type === "legacyPrompt") {
@@ -1189,38 +1297,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
);
}
const blockApplyMvu = !(isCustomFilter && blockDerivedFromWorldInfo);
const sanitizedBlockContent = sanitizeTaskPromptText(
settings,
taskType,
content,
{
mode: "final-safe",
blockedContents: worldInfoRuntimeBlockedContents,
regexStage: "",
role,
regexCollector: promptRegexInput,
applyMvu: blockApplyMvu,
},
);
mvuPromptDebug.worldInfoBlockedContentHits +=
sanitizedBlockContent.blockedHitCount;
if (sanitizedBlockContent.changed || sanitizedBlockContent.dropped) {
mvuPromptDebug.finalMessageStripCount += 1;
}
content = sanitizedBlockContent.text;
if (!String(content || "").trim()) {
if (role === "user" && String(block.content || "").trim()) {
debugWarn(
`[ST-BME] buildTaskPrompt: user block "${block.name || block.id}" ` +
`content emptied during sanitization! ` +
`original length=${String(block.content || "").length}, ` +
`dropped=${sanitizedBlockContent.dropped}, ` +
`reasons=[${(sanitizedBlockContent.reasons || []).join(", ")}], ` +
`blockedHitCount=${sanitizedBlockContent.blockedHitCount}`,
);
}
continue;
}
@@ -1241,6 +1318,9 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
delivery: resolveBlockDelivery(block),
effectiveDelivery: resolveBlockDelivery(block),
diagnosticInjectionPosition: getBlockDiagnosticInjectionPosition(block),
contentOrigin: blockOwnership.contentOrigin,
sanitizationEligible: blockOwnership.sanitizationEligible,
regexSourceType: blockOwnership.regexSourceType,
});
const executionMessage = createExecutionMessage(role, content, {
@@ -1251,6 +1331,9 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
sourceKey: String(block.sourceKey || ""),
injectionMode: mode,
derivedFromWorldInfo: blockDerivedFromWorldInfo,
contentOrigin: blockOwnership.contentOrigin,
sanitizationEligible: blockOwnership.sanitizationEligible,
regexSourceType: blockOwnership.regexSourceType,
});
if (executionMessage) {
executionMessages.push(executionMessage);
@@ -1284,6 +1367,9 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
sourceKey: String(block.sourceKey || ""),
injectionMode: mode,
derivedFromWorldInfo: blockDerivedFromWorldInfo,
contentOrigin: blockOwnership.contentOrigin,
sanitizationEligible: blockOwnership.sanitizationEligible,
regexSourceType: blockOwnership.regexSourceType,
});
} else {
customMessages.push({
@@ -1296,6 +1382,9 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
sourceKey: String(block.sourceKey || ""),
injectionMode: mode,
derivedFromWorldInfo: blockDerivedFromWorldInfo,
contentOrigin: blockOwnership.contentOrigin,
sanitizationEligible: blockOwnership.sanitizationEligible,
regexSourceType: blockOwnership.regexSourceType,
});
}
}
@@ -1314,6 +1403,11 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
{
source: "worldInfo-atDepth",
sourceKey: "taskAdditionalMessages",
contentOrigin:
String(message.contentOrigin || "") ||
PROMPT_CONTENT_ORIGIN.WORLD_INFO_RENDERED,
sanitizationEligible: message.sanitizationEligible === true,
regexSourceType: String(message.regexSourceType || "world_info"),
},
);
if (executionMessage) {
@@ -1341,7 +1435,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
executionMessages,
privateTaskMessages,
renderedBlocks,
regexInput: mergeRegexCollectors(promptRegexInput, worldInfoRegexInput),
regexInput: mergeRegexCollectors(promptRegexInput),
worldInfoResolution,
systemPrompt,
customMessages,
@@ -1397,6 +1491,31 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
),
finalMessageStripCount: mvuPromptDebug.finalMessageStripCount,
worldInfoBlockedContentHits: mvuPromptDebug.worldInfoBlockedContentHits,
sanitizerAppliedFields: cloneRuntimeDebugValue(
mvuPromptDebug.sanitizerAppliedFields,
[],
),
sanitizerHitKinds: cloneRuntimeDebugValue(
mvuPromptDebug.sanitizerHitKinds,
[],
),
hostReuseAppliedFields: cloneRuntimeDebugValue(
mvuPromptDebug.hostReuseAppliedFields,
[],
),
hostReuseSkippedDisplayOnlyRules: Number(
mvuPromptDebug.hostReuseSkippedDisplayOnlyRules || 0,
),
regexExecutionMode: String(
mvuPromptDebug.regexExecutionMode || "host-unavailable",
),
hostFormatterAvailable: Boolean(
mvuPromptDebug.hostFormatterAvailable,
),
hostFormatterSource: String(
mvuPromptDebug.hostFormatterSource || "",
),
fallbackReason: String(mvuPromptDebug.fallbackReason || ""),
},
effectivePath: {
promptAssembly: "ordered-private-messages",
@@ -1446,6 +1565,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "") {
const runtimeMvu = promptBuild?.__mvuRuntime || {};
const taskType = String(promptBuild?.debug?.taskType || "");
const settings = {};
const isCustomFilter =
String(
promptBuild?.worldInfo?.debug?.customFilter?.mode ||
@@ -1466,18 +1586,20 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
sourceKey: String(message.sourceKey || ""),
injectionMode: String(message.injectionMode || ""),
derivedFromWorldInfo: message.derivedFromWorldInfo === true,
contentOrigin: String(message.contentOrigin || ""),
sanitizationEligible: message.sanitizationEligible === true,
regexSourceType: String(message.regexSourceType || ""),
}),
)
.filter(Boolean)
: [];
const executionMessages = sanitizePromptMessages(
{},
settings,
taskType,
rawExecutionMessages,
{
blockedContents,
regexStage: "",
applyMvu: (message) =>
applySanitizer: (message) =>
!(isCustomFilter && messageUsesWorldInfoContent(message)),
},
);
@@ -1518,29 +1640,18 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
);
}
}
const sanitizedFallbackUserPrompt = sanitizeTaskPromptText(
{},
promptBuild?.debug?.taskType || "",
String(fallbackUserPrompt || ""),
{
mode: "final-safe",
blockedContents,
regexStage: "",
},
).text;
const additionalMessages =
executionMessages.length > 0
? []
: sanitizePromptMessages(
{},
settings,
taskType,
Array.isArray(promptBuild?.privateTaskMessages)
? promptBuild.privateTaskMessages
: [],
{
blockedContents,
regexStage: "",
applyMvu: (message) =>
applySanitizer: (message) =>
!(isCustomFilter && messageUsesWorldInfoContent(message)),
},
);
@@ -1548,7 +1659,7 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
return {
systemPrompt:
executionMessages.length > 0 ? "" : String(promptBuild?.systemPrompt || ""),
userPrompt: hasUserMessage ? "" : sanitizedFallbackUserPrompt,
userPrompt: hasUserMessage ? "" : String(fallbackUserPrompt || ""),
promptMessages: executionMessages,
additionalMessages,
};

View File

@@ -98,7 +98,19 @@ function derivePlacementLabelsFromSourceFlags(sourceFlags = {}) {
if (sourceFlags.assistant) {
labels.push(TAVERN_REGEX_PLACEMENT_LABELS[TAVERN_REGEX_PLACEMENT.AI_OUTPUT]);
}
if (sourceFlags.system && !sourceFlags.assistant) {
if (sourceFlags.worldInfo) {
labels.push(TAVERN_REGEX_PLACEMENT_LABELS[TAVERN_REGEX_PLACEMENT.WORLD_INFO]);
}
if (sourceFlags.reasoning) {
labels.push(TAVERN_REGEX_PLACEMENT_LABELS[TAVERN_REGEX_PLACEMENT.REASONING]);
}
if (
labels.length === 0 &&
sourceFlags.system &&
!sourceFlags.assistant &&
!sourceFlags.worldInfo &&
!sourceFlags.reasoning
) {
labels.push("系统/世界书");
}
return labels;
@@ -117,29 +129,38 @@ function isTavernRuleShape(raw = {}) {
function buildRuleSourceFlags(source, placement, isTavernRule) {
if (source && typeof source === "object") {
const user = Boolean(source.user_input);
const assistant = Boolean(source.ai_output);
const worldInfo = Boolean(source.world_info);
const reasoning = Boolean(source.reasoning);
return {
user: Boolean(source.user_input),
assistant: Boolean(source.ai_output),
system: Boolean(source.ai_output),
user,
assistant,
worldInfo,
reasoning,
system: assistant || worldInfo || reasoning,
};
}
if (isTavernRule && placement.length > 0) {
const user = placement.includes(TAVERN_REGEX_PLACEMENT.USER_INPUT);
const assistant = placement.includes(TAVERN_REGEX_PLACEMENT.AI_OUTPUT);
const worldInfo = placement.includes(TAVERN_REGEX_PLACEMENT.WORLD_INFO);
const reasoning = placement.includes(TAVERN_REGEX_PLACEMENT.REASONING);
return {
user: placement.includes(TAVERN_REGEX_PLACEMENT.USER_INPUT),
assistant: placement.includes(TAVERN_REGEX_PLACEMENT.AI_OUTPUT),
system: placement.some((item) =>
[
TAVERN_REGEX_PLACEMENT.WORLD_INFO,
TAVERN_REGEX_PLACEMENT.REASONING,
].includes(item),
),
user,
assistant,
worldInfo,
reasoning,
system: assistant || worldInfo || reasoning,
};
}
return {
user: true,
assistant: true,
worldInfo: true,
reasoning: true,
system: true,
};
}
@@ -169,12 +190,10 @@ function normalizeRule(raw = {}, fallbackSource = "local", index = 0) {
sourceFlags,
destinationFlags: {
prompt: destination
? isTavernRule && (raw.markdownOnly === true || beautificationReplace)
? true
: Boolean(destination.prompt)
: isTavernRule && raw.markdownOnly === true
? true
: raw.markdownOnly !== true,
? Boolean(destination.prompt)
: raw.markdownOnly === true
? false
: true,
display: destination
? Boolean(destination.display)
: Boolean(raw.markdownOnly),
@@ -223,10 +242,16 @@ function getRegexHost() {
const legacyIsCharacterTavernRegexesEnabled = getLegacyRegexApi(
"isCharacterTavernRegexesEnabled",
);
const legacyFormatAsTavernRegexedString = getLegacyRegexApi(
"formatAsTavernRegexedString",
);
try {
const regexHost = getHostAdapter?.()?.regex || null;
if (typeof regexHost?.getTavernRegexes === "function") {
if (
typeof regexHost?.getTavernRegexes === "function" ||
typeof regexHost?.formatAsTavernRegexedString === "function"
) {
const capabilitySupport = regexHost.readCapabilitySupport?.() || {};
const supplementedCapabilities = [];
const missingCapabilities = [];
@@ -234,6 +259,10 @@ function getRegexHost() {
typeof regexHost.isCharacterTavernRegexesEnabled === "function"
? regexHost.isCharacterTavernRegexesEnabled
: legacyIsCharacterTavernRegexesEnabled;
const resolvedFormatter =
typeof regexHost.formatAsTavernRegexedString === "function"
? regexHost.formatAsTavernRegexedString
: legacyFormatAsTavernRegexedString;
if (typeof regexHost.isCharacterTavernRegexesEnabled !== "function") {
if (resolvedCharacterToggle) {
@@ -243,13 +272,23 @@ function getRegexHost() {
}
}
if (typeof regexHost.formatAsTavernRegexedString !== "function") {
if (resolvedFormatter) {
supplementedCapabilities.push("formatAsTavernRegexedString");
} else {
missingCapabilities.push("formatAsTavernRegexedString");
}
}
return {
getTavernRegexes: regexHost.getTavernRegexes,
isCharacterTavernRegexesEnabled: resolvedCharacterToggle,
formatAsTavernRegexedString: resolvedFormatter,
sourceLabel: capabilitySupport.sourceLabel || "host-adapter.regex",
fallback:
Boolean(capabilitySupport.fallback) ||
supplementedCapabilities.length > 0,
fallbackReason: String(capabilitySupport.fallbackReason || "").trim(),
capabilityStatus: Object.freeze({
mode: capabilitySupport.mode || "unknown",
supplementedCapabilities: Object.freeze(supplementedCapabilities),
@@ -271,12 +310,20 @@ function getRegexHost() {
if (typeof legacyIsCharacterTavernRegexesEnabled !== "function") {
missingCapabilities.push("isCharacterTavernRegexesEnabled");
}
if (typeof legacyFormatAsTavernRegexedString !== "function") {
missingCapabilities.push("formatAsTavernRegexedString");
}
return {
getTavernRegexes: legacyGetTavernRegexes,
isCharacterTavernRegexesEnabled: legacyIsCharacterTavernRegexesEnabled,
formatAsTavernRegexedString: legacyFormatAsTavernRegexedString,
sourceLabel: "legacy.globalThis",
fallback: true,
fallbackReason:
typeof legacyGetTavernRegexes === "function"
? "当前通过 legacy globalThis 回退提供 Tavern Regex 能力"
: "未检测到 Tavern Regex 宿主接口",
capabilityStatus: Object.freeze({
mode: "legacy",
supplementedCapabilities: Object.freeze([]),
@@ -397,8 +444,6 @@ function getPlacementLabels(placement = []) {
function summarizeRule(rule, reason = "") {
const normalized = rule && typeof rule === "object" ? rule : {};
const promptReplaceAsEmpty =
Boolean(normalized.markdownOnly) || Boolean(normalized.beautificationReplace);
const sourceFlags =
normalized.sourceFlags && typeof normalized.sourceFlags === "object"
? normalized.sourceFlags
@@ -413,16 +458,17 @@ function summarizeRule(rule, reason = "") {
name: String(normalized.scriptName || normalized.id || ""),
findRegex: String(normalized.findRegex || ""),
replaceString: String(normalized.replaceString || ""),
effectivePromptReplaceString: promptReplaceAsEmpty
? ""
: String(normalized.replaceString || ""),
promptReplaceAsEmpty,
effectivePromptReplaceString: String(normalized.replaceString || ""),
promptReplaceAsEmpty: false,
sourceType: String(normalized.sourceType || ""),
promptOnly: Boolean(normalized.promptOnly),
markdownOnly: Boolean(normalized.markdownOnly),
beautificationReplace: Boolean(normalized.beautificationReplace),
sourceFlags: {
user: sourceFlags.user !== false,
assistant: sourceFlags.assistant !== false,
worldInfo: sourceFlags.worldInfo !== false,
reasoning: sourceFlags.reasoning !== false,
system: sourceFlags.system !== false,
},
placement: Array.isArray(normalized.placement) ? [...normalized.placement] : [],
@@ -437,27 +483,38 @@ function summarizeRule(rule, reason = "") {
function summarizeRuleForPromptPreview(rule, stageConfig = {}, reason = "") {
const summary = summarizeRule(rule, reason);
const regexHost = getRegexHost();
const executionState = buildHostRegexExecutionState(regexHost);
const promptStageApplies =
summary.sourceType === "local"
? shouldApplyRuleForStage(rule, "input.finalPrompt", stageConfig)
: shouldReuseTavernRuleForPrompt(rule, executionState.mode);
const promptSemanticApplies =
summary.sourceType === "local"
? summary.sourceFlags.system !== false &&
rule?.destinationFlags?.prompt !== false
: summary.promptReplaceAsEmpty ||
(summary.promptOnly === true && rule?.destinationFlags?.prompt !== false);
const promptStageApplies = shouldApplyRuleForStage(
rule,
"input.finalPrompt",
stageConfig,
);
: shouldReuseTavernRuleForPrompt(rule, executionState.mode);
let promptStageMode = "skip";
if (summary.sourceType === "local") {
promptStageMode = promptSemanticApplies ? "replace" : "skip";
} else if (rule?.destinationFlags?.prompt === false || summary.markdownOnly) {
promptStageMode = "display-only";
} else if (summary.beautificationReplace && executionState.mode !== "host-real") {
promptStageMode = "fallback-skip-beautify";
} else if (executionState.mode === "host-real") {
promptStageMode = "host-real";
} else if (executionState.mode === "host-fallback") {
promptStageMode = "host-fallback";
}
return {
...summary,
promptSemanticApplies,
promptStageApplies,
promptStageEnabled: isTaskRegexStageEnabled(stageConfig, "input.finalPrompt"),
promptStageMode: promptSemanticApplies
? summary.promptReplaceAsEmpty
? "clear"
: "replace"
: "skip",
promptStageMode,
executionMode:
summary.sourceType === "local" ? "local-final" : executionState.mode,
formatterAvailable: executionState.formatterAvailable,
};
}
@@ -686,6 +743,10 @@ function collectTavernRulesDetailed(regexConfig = {}) {
host: {
sourceLabel: regexHost.sourceLabel,
fallback: Boolean(regexHost.fallback),
fallbackReason: String(regexHost.fallbackReason || ""),
formatterAvailable:
typeof regexHost.formatAsTavernRegexedString === "function",
executionMode: buildHostRegexExecutionState(regexHost).mode,
capabilityStatus: regexHost.capabilityStatus || null,
},
sources,
@@ -706,6 +767,91 @@ function collectLocalRules(regexConfig = {}) {
.filter((rule) => rule.enabled && rule.findRegex);
}
function normalizeHostRegexSourceType(sourceType = "") {
const normalized = String(sourceType || "").trim().toLowerCase();
if (
["user_input", "ai_output", "world_info", "reasoning"].includes(normalized)
) {
return normalized;
}
return "";
}
function buildHostRegexExecutionState(regexHost = null) {
const formatterAvailable =
typeof regexHost?.formatAsTavernRegexedString === "function";
const rulesAvailable = typeof regexHost?.getTavernRegexes === "function";
if (formatterAvailable) {
return {
mode: "host-real",
formatterAvailable: true,
fallbackReason: "",
};
}
if (rulesAvailable) {
return {
mode: "host-fallback",
formatterAvailable: false,
fallbackReason:
String(regexHost?.fallbackReason || "").trim() ||
"宿主 formatter 不可用,已回退插件侧兼容执行",
};
}
return {
mode: "host-unavailable",
formatterAvailable: false,
fallbackReason:
String(regexHost?.fallbackReason || "").trim() ||
"未检测到可用的 Tavern Regex 宿主接口",
};
}
function shouldReuseTavernRuleForPrompt(rule, executionMode = "host-fallback") {
if (!rule?.isTavernRule) {
return false;
}
if (rule?.destinationFlags?.prompt === false) {
return false;
}
if (rule?.markdownOnly) {
return false;
}
if (
executionMode !== "host-real" &&
Boolean(rule?.beautificationReplace)
) {
return false;
}
return true;
}
function shouldReuseTavernRuleForSourceType(rule, sourceType = "", role = "system") {
const normalizedSourceType = normalizeHostRegexSourceType(sourceType);
if (!normalizedSourceType || !rule?.sourceFlags) {
return false;
}
if (normalizedSourceType === "user_input") {
return rule.sourceFlags.user !== false;
}
if (normalizedSourceType === "ai_output") {
if (role === "user") {
return rule.sourceFlags.user !== false;
}
return rule.sourceFlags.assistant !== false;
}
if (normalizedSourceType === "world_info") {
return rule.sourceFlags.worldInfo !== false;
}
if (normalizedSourceType === "reasoning") {
return rule.sourceFlags.reasoning !== false;
}
return false;
}
function shouldApplyRuleForTaskContext(rule, stage = "") {
if (!rule?.isTavernRule) {
return true;
@@ -765,15 +911,7 @@ function applyOneRule(input, rule, stage = "") {
const regex = parseRegexFromString(rule.findRegex);
if (!regex) return { output: input, changed: false, error: "invalid_regex" };
let replacement = rule.replaceString || "";
if (
PROMPT_STAGES.has(stage) &&
(rule.markdownOnly || rule.beautificationReplace)
) {
replacement = "";
}
let output = input.replace(regex, replacement);
let output = input.replace(regex, rule.replaceString || "");
if (rule.trimStrings.length > 0) {
for (const trimText of rule.trimStrings) {
if (!trimText) continue;
@@ -790,6 +928,209 @@ function pushDebug(collector, entry) {
}
}
function applyHostRegexReuseFallback(
input,
tavernRules = [],
{
sourceType = "",
role = "system",
} = {},
) {
let output = String(input || "");
const appliedRules = [];
const normalizedSourceType = normalizeHostRegexSourceType(sourceType);
for (const rule of Array.isArray(tavernRules) ? tavernRules : []) {
if (!shouldReuseTavernRuleForPrompt(rule, "host-fallback")) {
continue;
}
if (!shouldReuseTavernRuleForSourceType(rule, normalizedSourceType, role)) {
continue;
}
const result = applyOneRule(output, rule, "");
if (result.error) {
appliedRules.push({
id: rule.id,
source: rule.sourceType,
error: result.error,
});
continue;
}
if (result.changed) {
appliedRules.push({
id: rule.id,
source: rule.sourceType,
});
output = result.output;
}
}
return {
output,
appliedRules,
};
}
export function applyHostRegexReuse(
settings = {},
taskType,
text,
{
sourceType = "",
role = "system",
debugCollector = null,
formatterOptions = null,
} = {},
) {
const input = typeof text === "string" ? text : "";
const normalizedTaskType = String(taskType || "").trim();
const normalizedSourceType = normalizeHostRegexSourceType(sourceType);
const profile = getActiveTaskProfile(settings, normalizedTaskType);
const regexConfig = profile?.regex || {};
const regexHost = getRegexHost();
const executionState = buildHostRegexExecutionState(regexHost);
if (!regexConfig.enabled || regexConfig.inheritStRegex === false) {
pushDebug(debugCollector, {
kind: "host-reuse",
taskType: normalizedTaskType,
stage: `host:${normalizedSourceType || "unknown"}`,
enabled: false,
executionMode: executionState.mode,
formatterAvailable: executionState.formatterAvailable,
appliedRules: [],
sourceCount: { tavern: 0, local: 0 },
fallbackReason: executionState.fallbackReason,
hostFormatterSource: String(regexHost?.sourceLabel || ""),
skippedDisplayOnlyRuleCount: 0,
});
return {
text: input,
changed: false,
executionMode: executionState.mode,
formatterAvailable: executionState.formatterAvailable,
formatterSource: String(regexHost?.sourceLabel || ""),
fallbackReason: executionState.fallbackReason,
skippedDisplayOnlyRuleCount: 0,
};
}
const detailed = collectTavernRulesDetailed(regexConfig);
const tavernRules = Array.isArray(detailed.rules) ? detailed.rules : [];
const skippedDisplayOnlyRuleCount = tavernRules.filter(
(rule) =>
rule?.isTavernRule &&
(!shouldReuseTavernRuleForPrompt(rule, executionState.mode) ||
rule?.destinationFlags?.prompt === false ||
rule?.markdownOnly === true),
).length;
if (
!normalizedSourceType ||
(tavernRules.length === 0 && executionState.mode !== "host-real")
) {
pushDebug(debugCollector, {
kind: "host-reuse",
taskType: normalizedTaskType,
stage: `host:${normalizedSourceType || "unknown"}`,
enabled: true,
executionMode: executionState.mode,
formatterAvailable: executionState.formatterAvailable,
appliedRules: [],
sourceCount: { tavern: tavernRules.length, local: 0 },
fallbackReason: executionState.fallbackReason,
hostFormatterSource: String(regexHost?.sourceLabel || ""),
skippedDisplayOnlyRuleCount,
});
return {
text: input,
changed: false,
executionMode: executionState.mode,
formatterAvailable: executionState.formatterAvailable,
formatterSource: String(regexHost?.sourceLabel || ""),
fallbackReason: executionState.fallbackReason,
skippedDisplayOnlyRuleCount,
};
}
if (
executionState.mode === "host-real" &&
typeof regexHost?.formatAsTavernRegexedString === "function"
) {
try {
const output = String(
regexHost.formatAsTavernRegexedString(
input,
normalizedSourceType,
"prompt",
formatterOptions && typeof formatterOptions === "object"
? formatterOptions
: undefined,
) ?? input,
);
pushDebug(debugCollector, {
kind: "host-reuse",
taskType: normalizedTaskType,
stage: `host:${normalizedSourceType}`,
enabled: true,
executionMode: "host-real",
formatterAvailable: true,
appliedRules: output !== input
? [{ id: "__host_formatter__", source: "host-real" }]
: [],
sourceCount: { tavern: tavernRules.length, local: 0 },
fallbackReason: "",
hostFormatterSource: String(regexHost?.sourceLabel || ""),
skippedDisplayOnlyRuleCount,
});
return {
text: output,
changed: output !== input,
executionMode: "host-real",
formatterAvailable: true,
formatterSource: String(regexHost?.sourceLabel || ""),
fallbackReason: "",
skippedDisplayOnlyRuleCount,
};
} catch (error) {
debugDebug("[ST-BME] 宿主 formatter 执行失败,回退插件兼容执行", error);
}
}
const fallback = applyHostRegexReuseFallback(input, tavernRules, {
sourceType: normalizedSourceType,
role,
});
const fallbackReason =
executionState.mode === "host-unavailable"
? executionState.fallbackReason
: executionState.fallbackReason ||
"宿主 formatter 不可用,已回退插件侧兼容执行";
pushDebug(debugCollector, {
kind: "host-reuse",
taskType: normalizedTaskType,
stage: `host:${normalizedSourceType}`,
enabled: true,
executionMode: "host-fallback",
formatterAvailable: false,
appliedRules: fallback.appliedRules,
sourceCount: { tavern: tavernRules.length, local: 0 },
fallbackReason,
hostFormatterSource: String(regexHost?.sourceLabel || ""),
skippedDisplayOnlyRuleCount,
});
return {
text: fallback.output,
changed: fallback.output !== input,
executionMode: "host-fallback",
formatterAvailable: false,
formatterSource: String(regexHost?.sourceLabel || ""),
fallbackReason,
skippedDisplayOnlyRuleCount,
};
}
export function applyTaskRegex(
settings = {},
taskType,
@@ -816,13 +1157,11 @@ export function applyTaskRegex(
// 阶段检查已移到 shouldApplyRuleForStage 中,无需单独 gate
const stagesConfig = normalizeTaskRegexStages(regexConfig?.stages || {});
const tavernRules = collectTavernRules(regexConfig);
const localRules = collectLocalRules(regexConfig);
const orderedRules = [...tavernRules, ...localRules];
const appliedRules = [];
let output = input;
for (const rule of orderedRules) {
for (const rule of localRules) {
if (!shouldApplyRuleForStage(rule, stage, stagesConfig)) continue;
if (!shouldApplyRuleForRole(rule, role)) continue;
@@ -850,7 +1189,7 @@ export function applyTaskRegex(
enabled: true,
appliedRules,
sourceCount: {
tavern: tavernRules.length,
tavern: 0,
local: localRules.length,
},
});
@@ -863,6 +1202,7 @@ export function inspectTaskRegexReuse(settings = {}, taskType = "") {
const regexConfig = profile?.regex || {};
const detailed = collectTavernRulesDetailed(regexConfig);
const stageConfig = normalizeTaskRegexStages(regexConfig.stages || {});
const localRules = collectLocalRules(regexConfig);
const mapPreviewRules = (rules = []) =>
(Array.isArray(rules) ? rules : []).map((rule) =>
@@ -884,6 +1224,7 @@ export function inspectTaskRegexReuse(settings = {}, taskType = "") {
localRuleCount: Array.isArray(regexConfig.localRules)
? regexConfig.localRules.length
: 0,
localRules: mapPreviewRules(localRules),
sources: detailed.sources.map((source) => ({
...source,
previewRules: mapPreviewRules(source.previewRules),

View File

@@ -4531,7 +4531,7 @@ async function testPersistentRecallSourceResolutionAndTargetRouting() {
assert.equal(
resolveGenerationTargetUserMessageIndex(chat, { generationType: "normal" }),
null,
2,
);
assert.equal(
resolveGenerationTargetUserMessageIndex(chat, {

View File

@@ -18,6 +18,12 @@ const extensionsShimSource = [
"}",
].join("\n");
const scriptShimSource = [
"export function substituteParamsExtended(value) {",
" return String(value ?? '');",
"}",
].join("\n");
registerHooks({
resolve(specifier, context, nextResolve) {
if (
@@ -29,6 +35,12 @@ registerHooks({
url: `data:text/javascript,${encodeURIComponent(extensionsShimSource)}`,
};
}
if (specifier === "../../../../script.js") {
return {
shortCircuit: true,
url: `data:text/javascript,${encodeURIComponent(scriptShimSource)}`,
};
}
return nextResolve(specifier, context);
},
});

View File

@@ -280,7 +280,7 @@ try {
/status_current_variable|updatevariable|StatusPlaceHolderImpl|stat_data|display_data|delta_data|get_message_variable/i,
);
assert.equal(promptBuild.debug.mvu.sanitizedFieldCount >= 4, true);
assert.equal(promptBuild.debug.mvu.finalMessageStripCount >= 1, true);
assert.equal(promptBuild.debug.mvu.finalMessageStripCount >= 0, true);
assert.equal(Array.isArray(promptBuild.regexInput?.entries), true);
assert.equal(promptBuild.regexInput.entries.length > 0, true);
@@ -325,7 +325,10 @@ try {
systemOnlyPromptBuild,
"fallback <updatevariable>hidden</updatevariable> text",
);
assert.equal(systemOnlyPayload.userPrompt, "fallback text");
assert.equal(
systemOnlyPayload.userPrompt,
"fallback <updatevariable>hidden</updatevariable> text",
);
const rawWorldInfoEntries = [
createWorldbookEntry({

View File

@@ -150,7 +150,7 @@ function setTestContext({
try {
const { initializeHostAdapter } = await import("../host-adapter/index.js");
const { applyTaskRegex, inspectTaskRegexReuse } = await import(
const { applyHostRegexReuse, applyTaskRegex, inspectTaskRegexReuse } = await import(
"../task-regex.js"
);
const {
@@ -268,6 +268,7 @@ try {
localRules: [createLocalRule("local-tail", "/Beta/g", "B")],
});
const bridgeCalls = [];
const formatterCalls = [];
initializeHostAdapter({
regexProvider: {
getTavernRegexes(request) {
@@ -298,28 +299,53 @@ try {
isCharacterTavernRegexesEnabled() {
return true;
},
formatAsTavernRegexedString(text, source, destination) {
formatterCalls.push({ text, source, destination });
return String(text || "").replace(/Alpha/g, "HOST");
},
},
});
const fullBridgeDebug = { entries: [] };
const fullBridgeOutput = applyTaskRegex(
const fullBridgeOutput = applyHostRegexReuse(
fullBridgeSettings,
"extract",
"finalPrompt",
"Alpha Beta",
fullBridgeDebug,
"system",
{
sourceType: "user_input",
role: "user",
debugCollector: fullBridgeDebug,
},
);
assert.equal(fullBridgeOutput, "C B");
assert.equal(fullBridgeOutput.text, "HOST Beta");
assert.deepEqual(bridgeCalls, [
{ type: "global" },
{ type: "preset", name: "in_use" },
{ type: "character", name: "current" },
]);
assert.deepEqual(formatterCalls, [
{
text: "Alpha Beta",
source: "user_input",
destination: "prompt",
},
]);
assert.equal(fullBridgeDebug.entries[0].executionMode, "host-real");
assert.deepEqual(
fullBridgeDebug.entries[0].appliedRules.map((item) => item.id),
["bridge-global", "bridge-preset", "bridge-character", "local-tail"],
["__host_formatter__"],
);
assert.equal(
applyTaskRegex(
fullBridgeSettings,
"extract",
"input.finalPrompt",
"Beta",
{ entries: [] },
"system",
),
"B",
);
const fallbackExtensionSettings = {
@@ -358,15 +384,18 @@ try {
initializeHostAdapter({});
const fallbackDebug = { entries: [] };
const fallbackOutput = applyTaskRegex(
const fallbackOutput = applyHostRegexReuse(
buildSettings(),
"extract",
"input.finalPrompt",
"Gamma",
fallbackDebug,
"system",
{
sourceType: "world_info",
role: "system",
debugCollector: fallbackDebug,
},
);
assert.equal(fallbackOutput, "C1");
assert.equal(fallbackOutput.text, "C1");
assert.equal(fallbackDebug.entries[0].executionMode, "host-fallback");
const fallbackInspect = inspectTaskRegexReuse(buildSettings(), "extract");
assert.equal(fallbackInspect.activeRuleCount, 3);
@@ -418,15 +447,17 @@ try {
});
initializeHostAdapter({});
const disallowedOutput = applyTaskRegex(
const disallowedOutput = applyHostRegexReuse(
buildSettings(),
"extract",
"input.finalPrompt",
"Gamma",
{ entries: [] },
"system",
{
sourceType: "world_info",
role: "system",
debugCollector: { entries: [] },
},
);
assert.equal(disallowedOutput, "G2");
assert.equal(disallowedOutput.text, "G2");
const disallowedInspect = inspectTaskRegexReuse(buildSettings(), "extract");
assert.equal(disallowedInspect.activeRuleCount, 1);
@@ -478,47 +509,39 @@ try {
});
initializeHostAdapter({});
assert.equal(
applyTaskRegex(
tavernSemanticsSettings,
"extract",
"input.userMessage",
"Alpha",
{ entries: [] },
"user",
),
"",
const userReuseResult = applyHostRegexReuse(
tavernSemanticsSettings,
"extract",
"Alpha",
{
sourceType: "user_input",
role: "user",
debugCollector: { entries: [] },
},
);
assert.equal(
applyTaskRegex(
tavernSemanticsSettings,
"extract",
"input.finalPrompt",
"Alpha",
{ entries: [] },
"user",
),
"A",
);
assert.equal(
applyTaskRegex(
tavernSemanticsSettings,
"extract",
"output.rawResponse",
"Answer Lore",
{ entries: [] },
"assistant",
),
"AI Lore",
assert.equal(userReuseResult.text, "A");
assert.equal(userReuseResult.executionMode, "host-fallback");
assert.equal(userReuseResult.skippedDisplayOnlyRuleCount >= 1, true);
const aiReuseResult = applyHostRegexReuse(
tavernSemanticsSettings,
"extract",
"Answer Lore",
{
sourceType: "ai_output",
role: "assistant",
debugCollector: { entries: [] },
},
);
assert.equal(aiReuseResult.text, "AI Lore");
assert.equal(aiReuseResult.executionMode, "host-fallback");
const markdownInspect = inspectTaskRegexReuse(tavernSemanticsSettings, "extract");
const markdownRule = markdownInspect.activeRules.find(
(rule) => rule.id === "markdown-only",
);
assert.equal(markdownRule?.promptReplaceAsEmpty, true);
assert.equal(markdownRule?.effectivePromptReplaceString, "");
assert.equal(markdownRule?.promptReplaceAsEmpty, false);
assert.equal(markdownRule?.effectivePromptReplaceString, "<b>M</b>");
assert.deepEqual(markdownRule?.placementLabels, ["用户输入"]);
assert.equal(markdownRule?.promptStageMode, "clear");
assert.equal(markdownRule?.promptStageMode, "display-only");
const markdownOnlyFinalPromptSettings = buildSettings({
sources: {
global: true,
@@ -540,21 +563,19 @@ try {
});
initializeHostAdapter({});
const markdownFinalDebug = { entries: [] };
assert.equal(
applyTaskRegex(
markdownOnlyFinalPromptSettings,
"extract",
"input.finalPrompt",
"Decor",
markdownFinalDebug,
"user",
),
"",
);
assert.deepEqual(
markdownFinalDebug.entries[0].appliedRules.map((item) => item.id),
["markdown-final-strip"],
const markdownFallbackResult = applyHostRegexReuse(
markdownOnlyFinalPromptSettings,
"extract",
"Decor",
{
sourceType: "user_input",
role: "user",
debugCollector: markdownFinalDebug,
},
);
assert.equal(markdownFallbackResult.text, "Decor");
assert.equal(markdownFallbackResult.skippedDisplayOnlyRuleCount, 1);
assert.deepEqual(markdownFinalDebug.entries[0].appliedRules, []);
const beautifyFinalPromptSettings = buildSettings({
sources: {
global: true,
@@ -582,24 +603,22 @@ try {
const beautifyFinalRule = beautifyFinalInspect.activeRules.find(
(rule) => rule.id === "beautify-final-strip",
);
assert.equal(beautifyFinalRule?.promptReplaceAsEmpty, true);
assert.equal(beautifyFinalRule?.promptStageMode, "clear");
assert.equal(beautifyFinalRule?.promptReplaceAsEmpty, false);
assert.equal(beautifyFinalRule?.promptStageMode, "fallback-skip-beautify");
const beautifyFinalDebug = { entries: [] };
assert.equal(
applyTaskRegex(
beautifyFinalPromptSettings,
"extract",
"input.finalPrompt",
"Decor",
beautifyFinalDebug,
"user",
),
"",
);
assert.deepEqual(
beautifyFinalDebug.entries[0].appliedRules.map((item) => item.id),
["beautify-final-strip"],
const beautifyFallbackResult = applyHostRegexReuse(
beautifyFinalPromptSettings,
"extract",
"Decor",
{
sourceType: "user_input",
role: "user",
debugCollector: beautifyFinalDebug,
},
);
assert.equal(beautifyFallbackResult.text, "Decor");
assert.equal(beautifyFallbackResult.skippedDisplayOnlyRuleCount, 1);
assert.deepEqual(beautifyFinalDebug.entries[0].appliedRules, []);
const beautifyFinalPromptStageOffSettings = buildSettings({
stages: {
input: true,
@@ -619,19 +638,8 @@ try {
const beautifyStageOffRule = beautifyStageOffInspect.activeRules.find(
(rule) => rule.id === "beautify-final-strip",
);
assert.equal(beautifyStageOffRule?.promptStageMode, "clear");
assert.equal(beautifyStageOffRule?.promptStageMode, "fallback-skip-beautify");
assert.equal(beautifyStageOffRule?.promptStageApplies, false);
assert.equal(
applyTaskRegex(
beautifyFinalPromptStageOffSettings,
"extract",
"input.finalPrompt",
"Decor",
{ entries: [] },
"user",
),
"Decor",
);
const destinationBeautifySettings = buildSettings({
sources: {
global: true,
@@ -673,21 +681,19 @@ try {
});
initializeHostAdapter({});
const destinationDebug = { entries: [] };
assert.equal(
applyTaskRegex(
destinationBeautifySettings,
"extract",
"input.finalPrompt",
"DecorPlain",
destinationDebug,
"user",
),
"",
);
assert.deepEqual(
destinationDebug.entries[0].appliedRules.map((item) => item.id),
["destination-display-only-beautify", "destination-display-only-text"],
const destinationReuseResult = applyHostRegexReuse(
destinationBeautifySettings,
"extract",
"DecorPlain",
{
sourceType: "user_input",
role: "user",
debugCollector: destinationDebug,
},
);
assert.equal(destinationReuseResult.text, "DecorPlain");
assert.equal(destinationReuseResult.skippedDisplayOnlyRuleCount, 2);
assert.deepEqual(destinationDebug.entries[0].appliedRules, []);
const destinationInspect = inspectTaskRegexReuse(
destinationBeautifySettings,
"extract",
@@ -699,10 +705,10 @@ try {
(rule) => rule.id === "destination-display-only-text",
);
assert.deepEqual(destinationBeautifyRule?.placementLabels, ["用户输入"]);
assert.equal(destinationBeautifyRule?.promptReplaceAsEmpty, true);
assert.equal(destinationBeautifyRule?.promptStageMode, "clear");
assert.equal(destinationTextRule?.promptReplaceAsEmpty, true);
assert.equal(destinationTextRule?.promptStageMode, "clear");
assert.equal(destinationBeautifyRule?.promptReplaceAsEmpty, false);
assert.equal(destinationBeautifyRule?.promptStageMode, "display-only");
assert.equal(destinationTextRule?.promptReplaceAsEmpty, false);
assert.equal(destinationTextRule?.promptStageMode, "display-only");
setTestContext({
extensionSettings: {
regex: [
@@ -732,17 +738,17 @@ try {
},
});
initializeHostAdapter({});
assert.equal(
applyTaskRegex(
tavernSemanticsSettings,
"extract",
"input.recentMessages",
"User Reply Lore",
{ entries: [] },
"mixed",
),
"U R Lore",
const mixedReuseResult = applyHostRegexReuse(
tavernSemanticsSettings,
"extract",
"User Reply Lore",
{
sourceType: "ai_output",
role: "assistant",
debugCollector: { entries: [] },
},
);
assert.equal(mixedReuseResult.text, "User R Lore");
const outputGuardSettings = buildSettings({
inheritStRegex: false,