mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Refactor host regex reuse and injection sanitization
This commit is contained in:
@@ -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
463
injection-sanitizer.js
Normal 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
13
llm.js
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
54
panel.js
54
panel.js
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
441
task-regex.js
441
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),
|
||||
|
||||
@@ -4531,7 +4531,7 @@ async function testPersistentRecallSourceResolutionAndTargetRouting() {
|
||||
|
||||
assert.equal(
|
||||
resolveGenerationTargetUserMessageIndex(chat, { generationType: "normal" }),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
assert.equal(
|
||||
resolveGenerationTargetUserMessageIndex(chat, {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user