@@ -4451,8 +4469,8 @@ function _buildRegexReusePopupContent(snapshot = {}) {
来源与排除明细
diff --git a/prompt-builder.js b/prompt-builder.js
index 647363f..4f2eb96 100644
--- a/prompt-builder.js
+++ b/prompt-builder.js
@@ -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,
};
diff --git a/task-regex.js b/task-regex.js
index 33a23fa..9a5d03f 100644
--- a/task-regex.js
+++ b/task-regex.js
@@ -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),
diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs
index 70e2f5a..2faa430 100644
--- a/tests/p0-regressions.mjs
+++ b/tests/p0-regressions.mjs
@@ -4531,7 +4531,7 @@ async function testPersistentRecallSourceResolutionAndTargetRouting() {
assert.equal(
resolveGenerationTargetUserMessageIndex(chat, { generationType: "normal" }),
- null,
+ 2,
);
assert.equal(
resolveGenerationTargetUserMessageIndex(chat, {
diff --git a/tests/prompt-builder-defaults.mjs b/tests/prompt-builder-defaults.mjs
index 1f3a196..6fa9120 100644
--- a/tests/prompt-builder-defaults.mjs
+++ b/tests/prompt-builder-defaults.mjs
@@ -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);
},
});
diff --git a/tests/prompt-builder-mvu.mjs b/tests/prompt-builder-mvu.mjs
index accf5df..45d54aa 100644
--- a/tests/prompt-builder-mvu.mjs
+++ b/tests/prompt-builder-mvu.mjs
@@ -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 hidden text",
);
- assert.equal(systemOnlyPayload.userPrompt, "fallback text");
+ assert.equal(
+ systemOnlyPayload.userPrompt,
+ "fallback hidden text",
+ );
const rawWorldInfoEntries = [
createWorldbookEntry({
diff --git a/tests/task-regex.mjs b/tests/task-regex.mjs
index ca15fc8..c95793c 100644
--- a/tests/task-regex.mjs
+++ b/tests/task-regex.mjs
@@ -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, "M");
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,