mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Fix Tavern regex reuse inspection and matching
This commit is contained in:
3
index.js
3
index.js
@@ -126,6 +126,7 @@ import {
|
||||
createDefaultTaskProfiles,
|
||||
migrateLegacyTaskProfiles,
|
||||
} from "./prompt-profiles.js";
|
||||
import { inspectTaskRegexReuse } from "./task-regex.js";
|
||||
import {
|
||||
applyRecallInjectionController,
|
||||
buildRecallRecentMessagesController,
|
||||
@@ -9497,6 +9498,8 @@ async function onReembedDirect() {
|
||||
testMemoryLLM: onTestMemoryLLM,
|
||||
fetchMemoryLLMModels: onFetchMemoryLLMModels,
|
||||
fetchEmbeddingModels: onFetchEmbeddingModels,
|
||||
inspectTaskRegexReuse: (taskType) =>
|
||||
inspectTaskRegexReuse(getSettings(), taskType),
|
||||
applyCurrentHide: () => applyMessageHideNow("panel-manual-apply"),
|
||||
clearCurrentHide: () => clearAllHiddenMessages("panel-manual-clear"),
|
||||
rebuildVectorIndex: () => onRebuildVectorIndex(),
|
||||
|
||||
165
panel.js
165
panel.js
@@ -1,5 +1,6 @@
|
||||
// ST-BME: 操控面板交互逻辑
|
||||
|
||||
import { callGenericPopup, POPUP_TYPE } from "../../../popup.js";
|
||||
import { renderTemplateAsync } from "../../../templates.js";
|
||||
import { GraphRenderer } from "./graph-renderer.js";
|
||||
import { getNodeDisplayName } from "./node-labels.js";
|
||||
@@ -2988,6 +2989,9 @@ async function _handleTaskProfileWorkspaceClick(event) {
|
||||
}
|
||||
_refreshTaskProfileWorkspace();
|
||||
return;
|
||||
case "inspect-tavern-regex":
|
||||
await _openRegexReuseInspector(state.taskType);
|
||||
return;
|
||||
case "select-block":
|
||||
currentTaskProfileBlockId = actionEl.dataset.blockId || "";
|
||||
_refreshTaskProfileWorkspace();
|
||||
@@ -3394,6 +3398,9 @@ function _renderTaskRegexTab(state) {
|
||||
任务预设可复用酒馆正则,并叠加当前任务自己的附加规则。
|
||||
</div>
|
||||
</div>
|
||||
<button class="bme-config-secondary-btn" data-task-action="inspect-tavern-regex" type="button">
|
||||
查看当前复用规则
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bme-task-toggle-list">
|
||||
@@ -3501,6 +3508,164 @@ function _renderTaskRegexTab(state) {
|
||||
`;
|
||||
}
|
||||
|
||||
function _formatRegexReuseSourceState(source = {}) {
|
||||
const states = [];
|
||||
states.push(source.enabled ? "已启用" : "已关闭");
|
||||
states.push(source.allowed === false ? "未获酒馆允许" : "允许参与");
|
||||
states.push(
|
||||
source.resolvedVia === "bridge"
|
||||
? "通过桥接读取"
|
||||
: source.resolvedVia === "fallback"
|
||||
? "通过 fallback 读取"
|
||||
: "来源未知",
|
||||
);
|
||||
return states.join(" · ");
|
||||
}
|
||||
|
||||
function _renderRegexReuseRuleList(rules = [], emptyText = "无") {
|
||||
if (!Array.isArray(rules) || rules.length === 0) {
|
||||
return `<div class="bme-task-empty">${_escHtml(emptyText)}</div>`;
|
||||
}
|
||||
|
||||
return rules
|
||||
.map((rule) => {
|
||||
const placementText = Array.isArray(rule.placementLabels) && rule.placementLabels.length
|
||||
? rule.placementLabels.join(" / ")
|
||||
: "未声明 placement";
|
||||
const flags = [
|
||||
rule.promptOnly ? "promptOnly" : "",
|
||||
rule.markdownOnly ? "markdownOnly" : "",
|
||||
rule.reason ? `原因: ${rule.reason}` : "",
|
||||
].filter(Boolean);
|
||||
return `
|
||||
<div class="bme-debug-row">
|
||||
<span class="bme-debug-key">${_escHtml(rule.name || rule.id || "未命名规则")}</span>
|
||||
<span class="bme-debug-value">${_escHtml(placementText)}</span>
|
||||
</div>
|
||||
<div class="bme-task-note">
|
||||
<code>${_escHtml(rule.findRegex || "(空 findRegex)")}</code>
|
||||
${rule.replaceString ? ` -> <code>${_escHtml(rule.replaceString)}</code>` : ""}
|
||||
${flags.length ? `<br>${_escHtml(flags.join(" · "))}` : ""}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function _buildRegexReusePopupContent(snapshot = {}) {
|
||||
const container = document.createElement("div");
|
||||
const sources = Array.isArray(snapshot.sources) ? snapshot.sources : [];
|
||||
const activeRules = Array.isArray(snapshot.activeRules) ? snapshot.activeRules : [];
|
||||
const stageConfig = snapshot.stageConfig && typeof snapshot.stageConfig === "object"
|
||||
? snapshot.stageConfig
|
||||
: {};
|
||||
const sourceConfig = snapshot.sourceConfig && typeof snapshot.sourceConfig === "object"
|
||||
? snapshot.sourceConfig
|
||||
: {};
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="bme-task-tab-body">
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-title">酒馆正则复用快照</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
这里展示的是当前任务预设下,ST-BME 实际会尝试复用的 Tavern 正则来源和规则,不是静态说明文案。
|
||||
</div>
|
||||
<div class="bme-debug-list">
|
||||
<div class="bme-debug-row">
|
||||
<span class="bme-debug-key">任务</span>
|
||||
<span class="bme-debug-value">${_escHtml(snapshot.taskType || "—")}</span>
|
||||
</div>
|
||||
<div class="bme-debug-row">
|
||||
<span class="bme-debug-key">预设</span>
|
||||
<span class="bme-debug-value">${_escHtml(snapshot.profileName || snapshot.profileId || "—")}</span>
|
||||
</div>
|
||||
<div class="bme-debug-row">
|
||||
<span class="bme-debug-key">任务正则</span>
|
||||
<span class="bme-debug-value">${snapshot.regexEnabled ? "已启用" : "已关闭"}</span>
|
||||
</div>
|
||||
<div class="bme-debug-row">
|
||||
<span class="bme-debug-key">复用酒馆正则</span>
|
||||
<span class="bme-debug-value">${snapshot.inheritStRegex ? "已启用" : "已关闭"}</span>
|
||||
</div>
|
||||
<div class="bme-debug-row">
|
||||
<span class="bme-debug-key">本地规则数</span>
|
||||
<span class="bme-debug-value">${Number(snapshot.localRuleCount || 0)}</span>
|
||||
</div>
|
||||
<div class="bme-debug-row">
|
||||
<span class="bme-debug-key">桥接模式</span>
|
||||
<span class="bme-debug-value">${_escHtml(snapshot.host?.sourceLabel || "unknown")} · ${_escHtml(snapshot.host?.capabilityStatus?.mode || snapshot.host?.mode || "unknown")}${snapshot.host?.fallback ? " · fallback" : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-title">当前启用开关</div>
|
||||
<div class="bme-task-note">
|
||||
来源:global=${sourceConfig.global === false ? "关" : "开"} / preset=${sourceConfig.preset === false ? "关" : "开"} / character=${sourceConfig.character === false ? "关" : "开"}
|
||||
</div>
|
||||
<div class="bme-task-note">
|
||||
阶段:${_escHtml(Object.entries(stageConfig).map(([key, value]) => `${key}=${value ? "on" : "off"}`).join(" | ") || "无")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-title">来源明细</div>
|
||||
${
|
||||
sources.length
|
||||
? sources.map((source) => `
|
||||
<details class="bme-debug-details" open>
|
||||
<summary>
|
||||
${_escHtml(source.label || source.type || "未知来源")}
|
||||
<span class="bme-debug-value"> · ${_escHtml(_formatRegexReuseSourceState(source))}</span>
|
||||
</summary>
|
||||
<div class="bme-task-note">
|
||||
raw=${Number(source.rawRuleCount || 0)} / active=${Number(source.activeRuleCount || 0)}
|
||||
${source.reason ? `<br>${_escHtml(source.reason)}` : ""}
|
||||
</div>
|
||||
<div class="bme-task-section-label">本来源当前生效规则</div>
|
||||
${_renderRegexReuseRuleList(source.rules, "该来源当前没有进入任务链的复用规则")}
|
||||
<div class="bme-task-section-label">被跳过的规则</div>
|
||||
${_renderRegexReuseRuleList(source.ignoredRules, "没有被额外跳过的规则")}
|
||||
</details>
|
||||
`).join("")
|
||||
: `<div class="bme-task-empty">当前没有可展示的酒馆正则来源。</div>`
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-title">汇总后的复用规则</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
这是经过来源开关、allowlist 和去重后,准备进入当前任务链的 Tavern 规则集合。
|
||||
</div>
|
||||
${_renderRegexReuseRuleList(activeRules, "当前没有复用到任何酒馆正则")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
async function _openRegexReuseInspector(taskType) {
|
||||
if (typeof _actionHandlers.inspectTaskRegexReuse !== "function") {
|
||||
toastr.info("当前运行时没有接入正则复用诊断入口", "ST-BME");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const snapshot = await _actionHandlers.inspectTaskRegexReuse(taskType);
|
||||
const content = _buildRegexReusePopupContent(snapshot || {});
|
||||
await callGenericPopup(content, POPUP_TYPE.TEXT, "", {
|
||||
okButton: "关闭",
|
||||
wide: true,
|
||||
large: true,
|
||||
allowVerticalScrolling: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[ST-BME] 打开正则复用检查弹窗失败:", error);
|
||||
toastr.error("打开正则复用检查弹窗失败", "ST-BME");
|
||||
}
|
||||
}
|
||||
|
||||
function _renderTaskDebugTab(state) {
|
||||
const hostCapabilities = state.runtimeDebug?.hostCapabilities || null;
|
||||
const runtimeDebug = state.runtimeDebug?.runtimeDebug || {};
|
||||
|
||||
@@ -46,6 +46,13 @@ const INPUT_REGEX_STAGE_BY_FIELD = {
|
||||
contradictionSummary: "input.candidateText",
|
||||
};
|
||||
|
||||
const INPUT_REGEX_ROLE_BY_FIELD = {
|
||||
userMessage: "user",
|
||||
recentMessages: "mixed",
|
||||
chatMessages: "mixed",
|
||||
dialogueText: "mixed",
|
||||
};
|
||||
|
||||
function cloneRuntimeDebugValue(value, fallback = null) {
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
@@ -601,6 +608,7 @@ 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(
|
||||
settings,
|
||||
taskType,
|
||||
@@ -610,7 +618,7 @@ function sanitizePromptContextInputs(
|
||||
path: fieldName,
|
||||
mode: "aggressive",
|
||||
regexStage,
|
||||
role: "system",
|
||||
role: regexRole,
|
||||
debugState,
|
||||
regexCollector,
|
||||
applyMvu,
|
||||
|
||||
487
task-regex.js
487
task-regex.js
@@ -13,6 +13,20 @@ import {
|
||||
const HTML_TAG_PATTERN =
|
||||
/<\/?(?:div|span|p|br|hr|img|details|summary|section|article|aside|header|footer|nav|ul|ol|li|table|tr|td|th|h[1-6]|a|em|strong|blockquote|pre|code|svg|path)\b/i;
|
||||
const HTML_ATTR_PATTERN = /\b(?:style|class|id|href|src|data-)\s*=/i;
|
||||
const TAVERN_REGEX_PLACEMENT = Object.freeze({
|
||||
USER_INPUT: 1,
|
||||
AI_OUTPUT: 2,
|
||||
SLASH_COMMAND: 3,
|
||||
WORLD_INFO: 5,
|
||||
REASONING: 6,
|
||||
});
|
||||
const TAVERN_REGEX_PLACEMENT_LABELS = Object.freeze({
|
||||
[TAVERN_REGEX_PLACEMENT.USER_INPUT]: "用户输入",
|
||||
[TAVERN_REGEX_PLACEMENT.AI_OUTPUT]: "AI 输出",
|
||||
[TAVERN_REGEX_PLACEMENT.SLASH_COMMAND]: "斜杠命令",
|
||||
[TAVERN_REGEX_PLACEMENT.WORLD_INFO]: "世界书",
|
||||
[TAVERN_REGEX_PLACEMENT.REASONING]: "推理/思维",
|
||||
});
|
||||
|
||||
const PROMPT_STAGES = new Set([
|
||||
"finalPrompt",
|
||||
@@ -69,6 +83,53 @@ function normalizeTrimStrings(rawTrim) {
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeRulePlacement(rawPlacement) {
|
||||
const placement = Array.isArray(rawPlacement) ? rawPlacement : [];
|
||||
return placement
|
||||
.map((item) => Number(item))
|
||||
.filter((item) => Number.isFinite(item));
|
||||
}
|
||||
|
||||
function isTavernRuleShape(raw = {}) {
|
||||
return (
|
||||
Array.isArray(raw?.placement) ||
|
||||
Object.prototype.hasOwnProperty.call(raw || {}, "promptOnly") ||
|
||||
Object.prototype.hasOwnProperty.call(raw || {}, "markdownOnly") ||
|
||||
Object.prototype.hasOwnProperty.call(raw || {}, "scriptName") ||
|
||||
Object.prototype.hasOwnProperty.call(raw || {}, "findRegex") ||
|
||||
Object.prototype.hasOwnProperty.call(raw || {}, "replaceString")
|
||||
);
|
||||
}
|
||||
|
||||
function buildRuleSourceFlags(source, placement, isTavernRule) {
|
||||
if (source && typeof source === "object") {
|
||||
return {
|
||||
user: Boolean(source.user_input),
|
||||
assistant: Boolean(source.ai_output),
|
||||
system: Boolean(source.ai_output),
|
||||
};
|
||||
}
|
||||
|
||||
if (isTavernRule && placement.length > 0) {
|
||||
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),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
user: true,
|
||||
assistant: true,
|
||||
system: true,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRule(raw = {}, fallbackSource = "local", index = 0) {
|
||||
const destination =
|
||||
raw?.destination && typeof raw.destination === "object"
|
||||
@@ -76,6 +137,8 @@ function normalizeRule(raw = {}, fallbackSource = "local", index = 0) {
|
||||
: null;
|
||||
const source =
|
||||
raw?.source && typeof raw.source === "object" ? raw.source : null;
|
||||
const placement = normalizeRulePlacement(raw?.placement);
|
||||
const isTavernRule = isTavernRuleShape(raw);
|
||||
|
||||
return {
|
||||
id: String(raw.id || `${fallbackSource}-${index + 1}`),
|
||||
@@ -86,19 +149,25 @@ function normalizeRule(raw = {}, fallbackSource = "local", index = 0) {
|
||||
raw.replace_string ?? raw.replaceString ?? raw.replace ?? "",
|
||||
),
|
||||
trimStrings: normalizeTrimStrings(raw.trim_strings ?? raw.trimStrings),
|
||||
sourceFlags: {
|
||||
user: source ? Boolean(source.user_input) : true,
|
||||
assistant: source ? Boolean(source.ai_output) : true,
|
||||
system: source ? Boolean(source.ai_output) : true,
|
||||
},
|
||||
sourceFlags: buildRuleSourceFlags(source, placement, isTavernRule),
|
||||
destinationFlags: {
|
||||
prompt: destination
|
||||
? Boolean(destination.prompt)
|
||||
: raw.promptOnly !== true,
|
||||
: raw.markdownOnly !== true,
|
||||
display: destination
|
||||
? Boolean(destination.display)
|
||||
: Boolean(raw.markdownOnly),
|
||||
},
|
||||
promptOnly: Boolean(raw.promptOnly),
|
||||
markdownOnly: Boolean(raw.markdownOnly),
|
||||
placement,
|
||||
minDepth: Number.isFinite(Number(raw.min_depth ?? raw.minDepth))
|
||||
? Number(raw.min_depth ?? raw.minDepth)
|
||||
: null,
|
||||
maxDepth: Number.isFinite(Number(raw.max_depth ?? raw.maxDepth))
|
||||
? Number(raw.max_depth ?? raw.maxDepth)
|
||||
: null,
|
||||
isTavernRule,
|
||||
sourceType: fallbackSource,
|
||||
raw,
|
||||
};
|
||||
@@ -194,6 +263,136 @@ function getRegexHost() {
|
||||
};
|
||||
}
|
||||
|
||||
function getPresetManagerFromContext(context = {}) {
|
||||
if (typeof context?.getPresetManager !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const manager = context.getPresetManager();
|
||||
return manager && typeof manager === "object" ? manager : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentPresetInfo(context = {}) {
|
||||
const presetManager = getPresetManagerFromContext(context);
|
||||
const apiId = String(presetManager?.apiId || "").trim();
|
||||
const presetName =
|
||||
typeof presetManager?.getSelectedPresetName === "function"
|
||||
? String(presetManager.getSelectedPresetName() || "").trim()
|
||||
: "";
|
||||
|
||||
return {
|
||||
presetManager,
|
||||
apiId,
|
||||
presetName,
|
||||
};
|
||||
}
|
||||
|
||||
function isPresetRegexAllowed(extSettings = {}, apiId = "", presetName = "") {
|
||||
if (!apiId || !presetName) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(extSettings?.preset_allowed_regex?.[apiId]?.includes?.(presetName));
|
||||
}
|
||||
|
||||
function getCurrentCharacterInfo(context = {}) {
|
||||
const rawCharacterId = context?.characterId;
|
||||
const characterId = Number(rawCharacterId);
|
||||
if (!Number.isFinite(characterId) || characterId < 0) {
|
||||
return {
|
||||
characterId: null,
|
||||
character: null,
|
||||
avatar: "",
|
||||
};
|
||||
}
|
||||
|
||||
const characters = Array.isArray(context?.characters) ? context.characters : [];
|
||||
const character = characters[characterId] || null;
|
||||
|
||||
return {
|
||||
characterId,
|
||||
character,
|
||||
avatar: String(character?.avatar || ""),
|
||||
};
|
||||
}
|
||||
|
||||
function isCharacterRegexAllowed(extSettings = {}, avatar = "") {
|
||||
if (!avatar) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(extSettings?.character_allowed_regex?.includes?.(avatar));
|
||||
}
|
||||
|
||||
function readGlobalFallbackRules(extSettings = {}) {
|
||||
return readArrayPath(extSettings, [
|
||||
["regex"],
|
||||
["regex_scripts"],
|
||||
["regex", "regex_scripts"],
|
||||
]);
|
||||
}
|
||||
|
||||
function readPresetFallbackRules(context = {}, oaiSettings = {}) {
|
||||
const { presetManager } = getCurrentPresetInfo(context);
|
||||
if (typeof presetManager?.readPresetExtensionField === "function") {
|
||||
try {
|
||||
const scripts = presetManager.readPresetExtensionField({
|
||||
path: "regex_scripts",
|
||||
});
|
||||
if (Array.isArray(scripts)) {
|
||||
return scripts;
|
||||
}
|
||||
} catch {
|
||||
// ignore and continue to legacy paths
|
||||
}
|
||||
}
|
||||
|
||||
return readArrayPath(oaiSettings, [
|
||||
["regex_scripts"],
|
||||
["extensions", "regex_scripts"],
|
||||
]);
|
||||
}
|
||||
|
||||
function readCharacterFallbackRules(context = {}) {
|
||||
const { character } = getCurrentCharacterInfo(context);
|
||||
if (!character) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return readArrayPath(character, [
|
||||
["data", "extensions", "regex_scripts"],
|
||||
["extensions", "regex_scripts"],
|
||||
]);
|
||||
}
|
||||
|
||||
function getPlacementLabels(placement = []) {
|
||||
return (Array.isArray(placement) ? placement : []).map(
|
||||
(item) => TAVERN_REGEX_PLACEMENT_LABELS[item] || `#${item}`,
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeRule(rule, reason = "") {
|
||||
const normalized = rule && typeof rule === "object" ? rule : {};
|
||||
return {
|
||||
id: String(normalized.id || ""),
|
||||
name: String(normalized.scriptName || normalized.id || ""),
|
||||
findRegex: String(normalized.findRegex || ""),
|
||||
replaceString: String(normalized.replaceString || ""),
|
||||
sourceType: String(normalized.sourceType || ""),
|
||||
promptOnly: Boolean(normalized.promptOnly),
|
||||
markdownOnly: Boolean(normalized.markdownOnly),
|
||||
placement: Array.isArray(normalized.placement) ? [...normalized.placement] : [],
|
||||
placementLabels: getPlacementLabels(normalized.placement),
|
||||
minDepth:
|
||||
normalized.minDepth == null ? null : Number(normalized.minDepth),
|
||||
maxDepth:
|
||||
normalized.maxDepth == null ? null : Number(normalized.maxDepth),
|
||||
reason: String(reason || ""),
|
||||
};
|
||||
}
|
||||
|
||||
function collectViaApi(sourceType, regexHost = null) {
|
||||
const getter = regexHost?.getTavernRegexes;
|
||||
if (typeof getter !== "function") {
|
||||
@@ -233,15 +432,13 @@ function collectViaApi(sourceType, regexHost = null) {
|
||||
return unsupported();
|
||||
}
|
||||
|
||||
function collectTavernRules(regexConfig = {}) {
|
||||
function collectTavernRulesDetailed(regexConfig = {}) {
|
||||
const shouldReuse = regexConfig.inheritStRegex !== false;
|
||||
if (!shouldReuse) return [];
|
||||
|
||||
const sourceConfig = regexConfig.sources || {};
|
||||
const enabledSources = {
|
||||
global: sourceConfig.global !== false,
|
||||
preset: sourceConfig.preset !== false,
|
||||
character: sourceConfig.character !== false,
|
||||
global: shouldReuse && sourceConfig.global !== false,
|
||||
preset: shouldReuse && sourceConfig.preset !== false,
|
||||
character: shouldReuse && sourceConfig.character !== false,
|
||||
};
|
||||
|
||||
const context = getContext?.() || {};
|
||||
@@ -251,66 +448,170 @@ function collectTavernRules(regexConfig = {}) {
|
||||
const regexHost = getRegexHost();
|
||||
const collected = [];
|
||||
const seen = new Set();
|
||||
const sources = [];
|
||||
|
||||
const pushRules = (items, sourceType) => {
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const normalized = normalizeRule(items[index], sourceType, index);
|
||||
if (!normalized.enabled || !normalized.findRegex) continue;
|
||||
const key = `${sourceType}:${normalized.id}:${normalized.findRegex}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
collected.push(normalized);
|
||||
const appendSourceSnapshot = ({
|
||||
type,
|
||||
label,
|
||||
enabled,
|
||||
supported,
|
||||
resolvedVia,
|
||||
allowed = true,
|
||||
reason = "",
|
||||
rawItems = [],
|
||||
}) => {
|
||||
const effectiveItems =
|
||||
enabled && allowed ? (Array.isArray(rawItems) ? rawItems : []) : [];
|
||||
const activeRules = [];
|
||||
const ignoredRules = [];
|
||||
|
||||
if (!enabled) {
|
||||
sources.push({
|
||||
type,
|
||||
label,
|
||||
enabled,
|
||||
supported,
|
||||
resolvedVia,
|
||||
allowed,
|
||||
reason:
|
||||
reason || (shouldReuse ? "当前任务已关闭该来源" : "当前任务未启用复用酒馆正则"),
|
||||
rawRuleCount: Array.isArray(rawItems) ? rawItems.length : 0,
|
||||
activeRuleCount: 0,
|
||||
rules: [],
|
||||
ignoredRules: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (enabledSources.global) {
|
||||
const viaApi = collectViaApi("global", regexHost);
|
||||
if (viaApi.supported) {
|
||||
pushRules(viaApi.items, "global");
|
||||
} else {
|
||||
pushRules(
|
||||
readArrayPath(extSettings, [["regex"], ["regex", "regex_scripts"]]),
|
||||
"global",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (enabledSources.preset) {
|
||||
const viaApi = collectViaApi("preset", regexHost);
|
||||
if (viaApi.supported) {
|
||||
pushRules(viaApi.items, "preset");
|
||||
} else {
|
||||
pushRules(
|
||||
readArrayPath(oaiSettings, [
|
||||
["regex_scripts"],
|
||||
["extensions", "regex_scripts"],
|
||||
]),
|
||||
"preset",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (enabledSources.character) {
|
||||
const viaApi = collectViaApi("character", regexHost);
|
||||
if (viaApi.supported) {
|
||||
pushRules(viaApi.items, "character");
|
||||
} else {
|
||||
const charId = context?.characterId;
|
||||
const characters = context?.characters;
|
||||
if (charId !== undefined && characters) {
|
||||
const character = characters[Number(charId)];
|
||||
pushRules(
|
||||
readArrayPath(character, [
|
||||
["extensions", "regex_scripts"],
|
||||
["data", "extensions", "regex_scripts"],
|
||||
]),
|
||||
"character",
|
||||
if (!allowed && Array.isArray(rawItems)) {
|
||||
for (let index = 0; index < rawItems.length; index++) {
|
||||
ignoredRules.push(
|
||||
summarizeRule(normalizeRule(rawItems[index], type, index), "not-allowed"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < effectiveItems.length; index++) {
|
||||
const normalized = normalizeRule(effectiveItems[index], type, index);
|
||||
if (!normalized.enabled) {
|
||||
ignoredRules.push(summarizeRule(normalized, "disabled"));
|
||||
continue;
|
||||
}
|
||||
if (!normalized.findRegex) {
|
||||
ignoredRules.push(summarizeRule(normalized, "missing-find-regex"));
|
||||
continue;
|
||||
}
|
||||
const key = `${type}:${normalized.id}:${normalized.findRegex}`;
|
||||
if (seen.has(key)) {
|
||||
ignoredRules.push(summarizeRule(normalized, "duplicate"));
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
collected.push(normalized);
|
||||
activeRules.push(summarizeRule(normalized));
|
||||
}
|
||||
|
||||
sources.push({
|
||||
type,
|
||||
label,
|
||||
enabled,
|
||||
supported,
|
||||
resolvedVia,
|
||||
allowed,
|
||||
reason,
|
||||
rawRuleCount: Array.isArray(rawItems) ? rawItems.length : 0,
|
||||
activeRuleCount: activeRules.length,
|
||||
rules: activeRules,
|
||||
ignoredRules,
|
||||
});
|
||||
};
|
||||
|
||||
const globalViaApi = collectViaApi("global", regexHost);
|
||||
appendSourceSnapshot({
|
||||
type: "global",
|
||||
label: "全局",
|
||||
enabled: enabledSources.global,
|
||||
supported: true,
|
||||
resolvedVia: globalViaApi.supported ? "bridge" : "fallback",
|
||||
rawItems: globalViaApi.supported
|
||||
? globalViaApi.items
|
||||
: readGlobalFallbackRules(extSettings),
|
||||
});
|
||||
|
||||
const presetViaApi = collectViaApi("preset", regexHost);
|
||||
if (presetViaApi.supported) {
|
||||
appendSourceSnapshot({
|
||||
type: "preset",
|
||||
label: "当前预设",
|
||||
enabled: enabledSources.preset,
|
||||
supported: true,
|
||||
resolvedVia: "bridge",
|
||||
rawItems: presetViaApi.items,
|
||||
});
|
||||
} else {
|
||||
const { apiId, presetName } = getCurrentPresetInfo(context);
|
||||
const rawItems = readPresetFallbackRules(context, oaiSettings);
|
||||
const allowed = isPresetRegexAllowed(extSettings, apiId, presetName);
|
||||
appendSourceSnapshot({
|
||||
type: "preset",
|
||||
label: "当前预设",
|
||||
enabled: enabledSources.preset,
|
||||
supported: true,
|
||||
resolvedVia: "fallback",
|
||||
allowed,
|
||||
reason: allowed
|
||||
? ""
|
||||
: apiId && presetName
|
||||
? `酒馆当前未允许预设 "${presetName}" 的正则参与运行`
|
||||
: "未识别到酒馆当前生效的预设",
|
||||
rawItems,
|
||||
});
|
||||
}
|
||||
|
||||
return collected;
|
||||
const characterViaApi = collectViaApi("character", regexHost);
|
||||
if (characterViaApi.supported) {
|
||||
appendSourceSnapshot({
|
||||
type: "character",
|
||||
label: "角色卡",
|
||||
enabled: enabledSources.character,
|
||||
supported: true,
|
||||
resolvedVia: "bridge",
|
||||
rawItems: characterViaApi.items,
|
||||
});
|
||||
} else {
|
||||
const { avatar } = getCurrentCharacterInfo(context);
|
||||
const rawItems = readCharacterFallbackRules(context);
|
||||
const allowed = isCharacterRegexAllowed(extSettings, avatar);
|
||||
appendSourceSnapshot({
|
||||
type: "character",
|
||||
label: "角色卡",
|
||||
enabled: enabledSources.character,
|
||||
supported: true,
|
||||
resolvedVia: "fallback",
|
||||
allowed,
|
||||
reason: allowed
|
||||
? ""
|
||||
: avatar
|
||||
? "酒馆当前未允许该角色卡的 scoped regex 参与运行"
|
||||
: "当前没有可用的角色卡上下文",
|
||||
rawItems,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
shouldReuse,
|
||||
host: {
|
||||
sourceLabel: regexHost.sourceLabel,
|
||||
fallback: Boolean(regexHost.fallback),
|
||||
capabilityStatus: regexHost.capabilityStatus || null,
|
||||
},
|
||||
sources,
|
||||
rules: collected,
|
||||
};
|
||||
}
|
||||
|
||||
function collectTavernRules(regexConfig = {}) {
|
||||
return collectTavernRulesDetailed(regexConfig).rules;
|
||||
}
|
||||
|
||||
function collectLocalRules(regexConfig = {}) {
|
||||
@@ -322,11 +623,39 @@ function collectLocalRules(regexConfig = {}) {
|
||||
.filter((rule) => rule.enabled && rule.findRegex);
|
||||
}
|
||||
|
||||
function shouldApplyRuleForTaskContext(rule, stage = "") {
|
||||
if (!rule?.isTavernRule) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rule.markdownOnly) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedStage = String(stage || "").trim();
|
||||
const isFinalPromptStage =
|
||||
normalizedStage === "finalPrompt" || normalizedStage === "input.finalPrompt";
|
||||
const isOutputStage = OUTPUT_STAGES.has(normalizedStage);
|
||||
|
||||
if (isFinalPromptStage) {
|
||||
return rule.promptOnly === true;
|
||||
}
|
||||
|
||||
if (isOutputStage) {
|
||||
return rule.promptOnly !== true;
|
||||
}
|
||||
|
||||
return rule.promptOnly !== true;
|
||||
}
|
||||
|
||||
function shouldApplyRuleForStage(rule, stage = "", stagesConfig = {}) {
|
||||
const normalizedStage = String(stage || "").trim();
|
||||
if (rule.destinationFlags.prompt === false) {
|
||||
return false;
|
||||
}
|
||||
if (!shouldApplyRuleForTaskContext(rule, normalizedStage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!normalizedStage) {
|
||||
return isTaskRegexStageEnabled(stagesConfig, "input");
|
||||
@@ -340,6 +669,9 @@ function shouldApplyRuleForStage(rule, stage = "", stagesConfig = {}) {
|
||||
}
|
||||
|
||||
function shouldApplyRuleForRole(rule, role = "system") {
|
||||
if (role === "mixed") {
|
||||
return rule.sourceFlags.user !== false || rule.sourceFlags.assistant !== false;
|
||||
}
|
||||
if (role === "user") return rule.sourceFlags.user !== false;
|
||||
if (role === "assistant") return rule.sourceFlags.assistant !== false;
|
||||
return rule.sourceFlags.system !== false;
|
||||
@@ -438,3 +770,30 @@ export function applyTaskRegex(
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export function inspectTaskRegexReuse(settings = {}, taskType = "") {
|
||||
const profile = getActiveTaskProfile(settings, taskType);
|
||||
const regexConfig = profile?.regex || {};
|
||||
const detailed = collectTavernRulesDetailed(regexConfig);
|
||||
|
||||
return {
|
||||
taskType: String(taskType || ""),
|
||||
profileId: String(profile?.id || ""),
|
||||
profileName: String(profile?.name || ""),
|
||||
regexEnabled: regexConfig.enabled !== false,
|
||||
inheritStRegex: regexConfig.inheritStRegex !== false,
|
||||
stageConfig: normalizeTaskRegexStages(regexConfig.stages || {}),
|
||||
sourceConfig: {
|
||||
global: regexConfig.sources?.global !== false,
|
||||
preset: regexConfig.sources?.preset !== false,
|
||||
character: regexConfig.sources?.character !== false,
|
||||
},
|
||||
localRuleCount: Array.isArray(regexConfig.localRules)
|
||||
? regexConfig.localRules.length
|
||||
: 0,
|
||||
sources: detailed.sources,
|
||||
host: detailed.host,
|
||||
activeRuleCount: detailed.rules.length,
|
||||
activeRules: detailed.rules.map((rule) => summarizeRule(rule)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,14 @@ const originalIsCharacterTavernRegexesEnabled =
|
||||
globalThis.isCharacterTavernRegexesEnabled;
|
||||
const originalExtensionSettings = globalThis.__taskRegexTestExtensionSettings;
|
||||
|
||||
function createRule(id, find, replace, overrides = {}) {
|
||||
const PLACEMENT = Object.freeze({
|
||||
USER_INPUT: 1,
|
||||
AI_OUTPUT: 2,
|
||||
WORLD_INFO: 5,
|
||||
REASONING: 6,
|
||||
});
|
||||
|
||||
function createLocalRule(id, find, replace, overrides = {}) {
|
||||
return {
|
||||
id,
|
||||
script_name: id,
|
||||
@@ -53,56 +60,32 @@ function createRule(id, find, replace, overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
globalThis.__taskRegexTestExtensionSettings = {
|
||||
regex: {
|
||||
regex_scripts: [createRule("legacy-global", "/Gamma/g", "G")],
|
||||
},
|
||||
function createTavernRule(id, findRegex, replaceString, overrides = {}) {
|
||||
return {
|
||||
id,
|
||||
scriptName: id,
|
||||
enabled: true,
|
||||
findRegex,
|
||||
replaceString,
|
||||
trimStrings: [],
|
||||
placement: [PLACEMENT.WORLD_INFO],
|
||||
promptOnly: false,
|
||||
markdownOnly: false,
|
||||
minDepth: null,
|
||||
maxDepth: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
globalThis.SillyTavern = {
|
||||
getContext() {
|
||||
return {
|
||||
extensionSettings: globalThis.__taskRegexTestExtensionSettings,
|
||||
chatCompletionSettings: {
|
||||
regex_scripts: [createRule("legacy-preset", "/Delta/g", "D")],
|
||||
},
|
||||
characterId: 0,
|
||||
characters: [
|
||||
{
|
||||
extensions: {
|
||||
regex_scripts: [
|
||||
createRule("legacy-character", "/Epsilon/g", "E"),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
globalThis.getTavernRegexes = () => {
|
||||
throw new Error(
|
||||
"legacy global getter should not be used when bridge exists",
|
||||
);
|
||||
};
|
||||
globalThis.isCharacterTavernRegexesEnabled = () => {
|
||||
throw new Error(
|
||||
"legacy character toggle should not be used when bridge full capability exists",
|
||||
);
|
||||
};
|
||||
|
||||
const { initializeHostAdapter } = await import("../host-adapter/index.js");
|
||||
const { applyTaskRegex } = await import("../task-regex.js");
|
||||
|
||||
const settings = {
|
||||
function buildSettings(regex = {}) {
|
||||
return {
|
||||
taskProfiles: {
|
||||
extract: {
|
||||
activeProfileId: "bridge-profile",
|
||||
activeProfileId: "default",
|
||||
profiles: [
|
||||
{
|
||||
id: "bridge-profile",
|
||||
name: "Regex Bridge Test",
|
||||
id: "default",
|
||||
name: "Regex Test",
|
||||
taskType: "extract",
|
||||
builtin: false,
|
||||
blocks: [],
|
||||
@@ -117,28 +100,105 @@ try {
|
||||
stages: {
|
||||
input: true,
|
||||
output: true,
|
||||
"input.userMessage": true,
|
||||
"input.recentMessages": true,
|
||||
"input.candidateText": true,
|
||||
"input.finalPrompt": true,
|
||||
"output.rawResponse": true,
|
||||
"output.beforeParse": true,
|
||||
},
|
||||
localRules: [createRule("local-tail", "/Beta/g", "B")],
|
||||
localRules: [],
|
||||
...regex,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setTestContext({
|
||||
extensionSettings,
|
||||
presetScripts = [],
|
||||
presetName = "Live Preset",
|
||||
apiId = "openai",
|
||||
characterId = 0,
|
||||
characters = [],
|
||||
} = {}) {
|
||||
globalThis.__taskRegexTestExtensionSettings = extensionSettings;
|
||||
globalThis.SillyTavern = {
|
||||
getContext() {
|
||||
return {
|
||||
extensionSettings,
|
||||
characterId,
|
||||
characters,
|
||||
getPresetManager() {
|
||||
return {
|
||||
apiId,
|
||||
getSelectedPresetName() {
|
||||
return presetName;
|
||||
},
|
||||
readPresetExtensionField({ path } = {}) {
|
||||
return path === "regex_scripts" ? presetScripts : [];
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { initializeHostAdapter } = await import("../host-adapter/index.js");
|
||||
const { applyTaskRegex, inspectTaskRegexReuse } = await import(
|
||||
"../task-regex.js"
|
||||
);
|
||||
|
||||
globalThis.getTavernRegexes = () => {
|
||||
throw new Error("legacy global getter should not be used in regex tests");
|
||||
};
|
||||
globalThis.isCharacterTavernRegexesEnabled = () => {
|
||||
throw new Error(
|
||||
"legacy character toggle should not be used in regex tests",
|
||||
);
|
||||
};
|
||||
|
||||
setTestContext({
|
||||
extensionSettings: {
|
||||
regex: [],
|
||||
preset_allowed_regex: {},
|
||||
character_allowed_regex: [],
|
||||
},
|
||||
});
|
||||
|
||||
const fullBridgeSettings = buildSettings({
|
||||
localRules: [createLocalRule("local-tail", "/Beta/g", "B")],
|
||||
});
|
||||
const bridgeCalls = [];
|
||||
initializeHostAdapter({
|
||||
regexProvider: {
|
||||
getTavernRegexes(request) {
|
||||
bridgeCalls.push(request);
|
||||
if (request?.type === "global") {
|
||||
return [createRule("bridge-global", "/Alpha/g", "A")];
|
||||
return [
|
||||
createTavernRule("bridge-global", "/Alpha/g", "A", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
];
|
||||
}
|
||||
if (request?.type === "preset") {
|
||||
return [createRule("bridge-preset", "/A/g", "P")];
|
||||
return [
|
||||
createTavernRule("bridge-preset", "/A/g", "P", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
];
|
||||
}
|
||||
if (request?.type === "character") {
|
||||
return [createRule("bridge-character", "/P/g", "C")];
|
||||
return [
|
||||
createTavernRule("bridge-character", "/P/g", "C", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
@@ -150,7 +210,7 @@ try {
|
||||
|
||||
const fullBridgeDebug = { entries: [] };
|
||||
const fullBridgeOutput = applyTaskRegex(
|
||||
settings,
|
||||
fullBridgeSettings,
|
||||
"extract",
|
||||
"finalPrompt",
|
||||
"Alpha Beta",
|
||||
@@ -168,140 +228,225 @@ try {
|
||||
fullBridgeDebug.entries[0].appliedRules.map((item) => item.id),
|
||||
["bridge-global", "bridge-preset", "bridge-character", "local-tail"],
|
||||
);
|
||||
assert.deepEqual(fullBridgeDebug.entries[0].sourceCount, {
|
||||
tavern: 3,
|
||||
local: 1,
|
||||
});
|
||||
|
||||
const partialBridgeCalls = [];
|
||||
initializeHostAdapter({
|
||||
regexProvider: {
|
||||
getTavernRegexes(request) {
|
||||
partialBridgeCalls.push(request);
|
||||
if (request?.type === "global") {
|
||||
return [createRule("partial-global", "/Gamma/g", "G1")];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
const fallbackExtensionSettings = {
|
||||
regex: [
|
||||
createTavernRule("global-fallback", "/Gamma/g", "G1", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
preset_allowed_regex: {
|
||||
openai: ["Live Preset"],
|
||||
},
|
||||
character_allowed_regex: ["hero.png"],
|
||||
};
|
||||
setTestContext({
|
||||
extensionSettings: fallbackExtensionSettings,
|
||||
presetScripts: [
|
||||
createTavernRule("preset-fallback", "/G1/g", "P1", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
characters: [
|
||||
{
|
||||
avatar: "hero.png",
|
||||
data: {
|
||||
extensions: {
|
||||
regex_scripts: [
|
||||
createTavernRule("character-fallback", "/P1/g", "C1", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
initializeHostAdapter({});
|
||||
|
||||
const partialBridgeDebug = { entries: [] };
|
||||
const partialBridgeOutput = applyTaskRegex(
|
||||
settings,
|
||||
const fallbackDebug = { entries: [] };
|
||||
const fallbackOutput = applyTaskRegex(
|
||||
buildSettings(),
|
||||
"extract",
|
||||
"finalPrompt",
|
||||
"Gamma Delta Epsilon",
|
||||
partialBridgeDebug,
|
||||
"input.finalPrompt",
|
||||
"Gamma",
|
||||
fallbackDebug,
|
||||
"system",
|
||||
);
|
||||
assert.equal(fallbackOutput, "C1");
|
||||
|
||||
assert.equal(partialBridgeOutput, "G1 Delta E");
|
||||
assert.deepEqual(partialBridgeCalls, [
|
||||
{ type: "global" },
|
||||
{ type: "preset", name: "in_use" },
|
||||
]);
|
||||
const fallbackInspect = inspectTaskRegexReuse(buildSettings(), "extract");
|
||||
assert.equal(fallbackInspect.activeRuleCount, 3);
|
||||
assert.deepEqual(
|
||||
partialBridgeDebug.entries[0].appliedRules.map((item) => item.id),
|
||||
["partial-global", "legacy-character"],
|
||||
);
|
||||
assert.deepEqual(partialBridgeDebug.entries[0].sourceCount, {
|
||||
tavern: 2,
|
||||
local: 1,
|
||||
});
|
||||
|
||||
const emptyBridgeCalls = [];
|
||||
initializeHostAdapter({
|
||||
regexProvider: {
|
||||
getTavernRegexes(request) {
|
||||
emptyBridgeCalls.push(request);
|
||||
if (request?.type === "global") {
|
||||
return [];
|
||||
}
|
||||
if (request?.type === "preset") {
|
||||
return [createRule("bridge-preset-empty-guard", "/Theta/g", "T")];
|
||||
}
|
||||
if (request?.type === "character") {
|
||||
return [createRule("bridge-character-empty-guard", "/T/g", "C2")];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
isCharacterTavernRegexesEnabled() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emptyBridgeDebug = { entries: [] };
|
||||
const emptyBridgeOutput = applyTaskRegex(
|
||||
settings,
|
||||
"extract",
|
||||
"finalPrompt",
|
||||
"Gamma Theta",
|
||||
emptyBridgeDebug,
|
||||
"system",
|
||||
);
|
||||
|
||||
assert.equal(emptyBridgeOutput, "Gamma C2");
|
||||
assert.deepEqual(emptyBridgeCalls, [
|
||||
{ type: "global" },
|
||||
{ type: "preset", name: "in_use" },
|
||||
{ type: "character", name: "current" },
|
||||
]);
|
||||
assert.deepEqual(
|
||||
emptyBridgeDebug.entries[0].appliedRules.map((item) => item.id),
|
||||
["bridge-preset-empty-guard", "bridge-character-empty-guard"],
|
||||
fallbackInspect.activeRules.map((rule) => rule.id),
|
||||
["global-fallback", "preset-fallback", "character-fallback"],
|
||||
);
|
||||
assert.equal(
|
||||
emptyBridgeDebug.entries[0].appliedRules.some(
|
||||
(item) => item.id === "legacy-global",
|
||||
),
|
||||
fallbackInspect.sources.find((source) => source.type === "preset")
|
||||
?.resolvedVia,
|
||||
"fallback",
|
||||
);
|
||||
assert.equal(
|
||||
fallbackInspect.sources.find((source) => source.type === "character")
|
||||
?.allowed,
|
||||
true,
|
||||
);
|
||||
|
||||
const disallowedExtensionSettings = {
|
||||
regex: [
|
||||
createTavernRule("global-only", "/Gamma/g", "G2", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
preset_allowed_regex: {},
|
||||
character_allowed_regex: [],
|
||||
};
|
||||
setTestContext({
|
||||
extensionSettings: disallowedExtensionSettings,
|
||||
presetScripts: [
|
||||
createTavernRule("preset-blocked", "/G2/g", "P2", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
characters: [
|
||||
{
|
||||
avatar: "blocked.png",
|
||||
data: {
|
||||
extensions: {
|
||||
regex_scripts: [
|
||||
createTavernRule("character-blocked", "/P2/g", "C2", {
|
||||
promptOnly: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
initializeHostAdapter({});
|
||||
|
||||
const disallowedOutput = applyTaskRegex(
|
||||
buildSettings(),
|
||||
"extract",
|
||||
"input.finalPrompt",
|
||||
"Gamma",
|
||||
{ entries: [] },
|
||||
"system",
|
||||
);
|
||||
assert.equal(disallowedOutput, "G2");
|
||||
|
||||
const disallowedInspect = inspectTaskRegexReuse(buildSettings(), "extract");
|
||||
assert.equal(disallowedInspect.activeRuleCount, 1);
|
||||
assert.equal(
|
||||
disallowedInspect.sources.find((source) => source.type === "preset")
|
||||
?.allowed,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
disallowedInspect.sources.find((source) => source.type === "character")
|
||||
?.allowed,
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(emptyBridgeDebug.entries[0].sourceCount, {
|
||||
tavern: 2,
|
||||
local: 1,
|
||||
});
|
||||
|
||||
const outputGuardSettings = {
|
||||
taskProfiles: {
|
||||
extract: {
|
||||
activeProfileId: "output-guard",
|
||||
profiles: [
|
||||
{
|
||||
id: "output-guard",
|
||||
name: "Output Guard",
|
||||
taskType: "extract",
|
||||
builtin: false,
|
||||
blocks: [],
|
||||
regex: {
|
||||
enabled: true,
|
||||
inheritStRegex: false,
|
||||
stages: {
|
||||
input: true,
|
||||
output: true,
|
||||
"output.rawResponse": true,
|
||||
},
|
||||
localRules: [
|
||||
createRule("display-only-output", "/美化/g", "<b>美化</b>", {
|
||||
destination: {
|
||||
prompt: false,
|
||||
display: true,
|
||||
},
|
||||
}),
|
||||
createRule("prompt-output", "/JSON/g", "DONE", {
|
||||
destination: {
|
||||
prompt: true,
|
||||
display: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
const tavernSemanticsSettings = buildSettings({
|
||||
sources: {
|
||||
global: true,
|
||||
preset: false,
|
||||
character: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
setTestContext({
|
||||
extensionSettings: {
|
||||
regex: [
|
||||
createTavernRule("user-prompt-only", "/Alpha/g", "A", {
|
||||
placement: [PLACEMENT.USER_INPUT],
|
||||
promptOnly: true,
|
||||
}),
|
||||
createTavernRule("markdown-only", "/Alpha/g", "M", {
|
||||
placement: [PLACEMENT.USER_INPUT],
|
||||
markdownOnly: true,
|
||||
}),
|
||||
createTavernRule("output-only", "/Answer/g", "AI", {
|
||||
placement: [PLACEMENT.AI_OUTPUT],
|
||||
}),
|
||||
createTavernRule("world-info-only", "/Lore/g", "SYS", {
|
||||
placement: [PLACEMENT.WORLD_INFO],
|
||||
}),
|
||||
createTavernRule("recent-user", "/User/g", "U", {
|
||||
placement: [PLACEMENT.USER_INPUT],
|
||||
}),
|
||||
createTavernRule("recent-ai", "/Reply/g", "R", {
|
||||
placement: [PLACEMENT.AI_OUTPUT],
|
||||
}),
|
||||
],
|
||||
preset_allowed_regex: {},
|
||||
character_allowed_regex: [],
|
||||
},
|
||||
});
|
||||
initializeHostAdapter({});
|
||||
|
||||
assert.equal(
|
||||
applyTaskRegex(
|
||||
tavernSemanticsSettings,
|
||||
"extract",
|
||||
"input.userMessage",
|
||||
"Alpha",
|
||||
{ entries: [] },
|
||||
"user",
|
||||
),
|
||||
"Alpha",
|
||||
);
|
||||
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(
|
||||
applyTaskRegex(
|
||||
tavernSemanticsSettings,
|
||||
"extract",
|
||||
"input.recentMessages",
|
||||
"User Reply Lore",
|
||||
{ entries: [] },
|
||||
"mixed",
|
||||
),
|
||||
"U R Lore",
|
||||
);
|
||||
|
||||
const outputGuardSettings = buildSettings({
|
||||
inheritStRegex: false,
|
||||
localRules: [
|
||||
createLocalRule("display-only-output", "/美化/g", "<b>美化</b>", {
|
||||
destination: {
|
||||
prompt: false,
|
||||
display: true,
|
||||
},
|
||||
}),
|
||||
createLocalRule("prompt-output", "/JSON/g", "DONE", {
|
||||
destination: {
|
||||
prompt: true,
|
||||
display: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
const outputGuardDebug = { entries: [] };
|
||||
const outputGuardResult = applyTaskRegex(
|
||||
outputGuardSettings,
|
||||
@@ -317,127 +462,6 @@ try {
|
||||
["prompt-output"],
|
||||
);
|
||||
|
||||
const exactStageSettings = {
|
||||
taskProfilesVersion: 1,
|
||||
taskProfiles: {
|
||||
extract: {
|
||||
activeProfileId: "default",
|
||||
profiles: [
|
||||
{
|
||||
id: "default",
|
||||
taskType: "extract",
|
||||
regex: {
|
||||
enabled: true,
|
||||
inheritStRegex: false,
|
||||
sources: {
|
||||
global: false,
|
||||
preset: false,
|
||||
character: false,
|
||||
},
|
||||
stages: {
|
||||
output: true,
|
||||
"output.rawResponse": false,
|
||||
"output.beforeParse": true,
|
||||
},
|
||||
localRules: [
|
||||
createRule("exact-stage", "/JSON/g", "DONE", {
|
||||
destination: {
|
||||
prompt: true,
|
||||
display: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const exactStageDebug = { entries: [] };
|
||||
const exactStageResult = applyTaskRegex(
|
||||
exactStageSettings,
|
||||
"extract",
|
||||
"output.rawResponse",
|
||||
"JSON",
|
||||
exactStageDebug,
|
||||
"assistant",
|
||||
);
|
||||
assert.equal(exactStageResult, "JSON");
|
||||
assert.deepEqual(exactStageDebug.entries[0].appliedRules, []);
|
||||
|
||||
const legacyStageCompatibilitySettings = {
|
||||
taskProfilesVersion: 1,
|
||||
taskProfiles: {
|
||||
extract: {
|
||||
activeProfileId: "legacy-stage-compat",
|
||||
profiles: [
|
||||
{
|
||||
id: "legacy-stage-compat",
|
||||
taskType: "extract",
|
||||
regex: {
|
||||
enabled: true,
|
||||
inheritStRegex: false,
|
||||
sources: {
|
||||
global: false,
|
||||
preset: false,
|
||||
character: false,
|
||||
},
|
||||
stages: {
|
||||
input: true,
|
||||
output: true,
|
||||
"input.userMessage": false,
|
||||
"input.recentMessages": false,
|
||||
"input.candidateText": false,
|
||||
"input.finalPrompt": false,
|
||||
"output.rawResponse": false,
|
||||
"output.beforeParse": false,
|
||||
},
|
||||
localRules: [
|
||||
createRule("legacy-input-user", "/Alpha/g", "A1"),
|
||||
createRule("legacy-output-raw", "/Omega/g", "O1", {
|
||||
source: {
|
||||
user_input: false,
|
||||
ai_output: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const legacyStageInputDebug = { entries: [] };
|
||||
const legacyStageInputResult = applyTaskRegex(
|
||||
legacyStageCompatibilitySettings,
|
||||
"extract",
|
||||
"input.userMessage",
|
||||
"Alpha",
|
||||
legacyStageInputDebug,
|
||||
"user",
|
||||
);
|
||||
assert.equal(legacyStageInputResult, "A1");
|
||||
assert.deepEqual(
|
||||
legacyStageInputDebug.entries[0].appliedRules.map((item) => item.id),
|
||||
["legacy-input-user"],
|
||||
);
|
||||
|
||||
const legacyStageOutputDebug = { entries: [] };
|
||||
const legacyStageOutputResult = applyTaskRegex(
|
||||
legacyStageCompatibilitySettings,
|
||||
"extract",
|
||||
"output.rawResponse",
|
||||
"Omega",
|
||||
legacyStageOutputDebug,
|
||||
"assistant",
|
||||
);
|
||||
assert.equal(legacyStageOutputResult, "O1");
|
||||
assert.deepEqual(
|
||||
legacyStageOutputDebug.entries[0].appliedRules.map((item) => item.id),
|
||||
["legacy-output-raw"],
|
||||
);
|
||||
|
||||
console.log("task-regex tests passed");
|
||||
} finally {
|
||||
if (originalSillyTavern === undefined) {
|
||||
|
||||
Reference in New Issue
Block a user