mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Merge main into pr-6-review with safe message trace resolution
This commit is contained in:
9
index.js
9
index.js
@@ -126,6 +126,7 @@ import {
|
||||
createDefaultTaskProfiles,
|
||||
migrateLegacyTaskProfiles,
|
||||
} from "./prompt-profiles.js";
|
||||
import { inspectTaskRegexReuse } from "./task-regex.js";
|
||||
import {
|
||||
applyRecallInjectionController,
|
||||
buildRecallRecentMessagesController,
|
||||
@@ -350,6 +351,9 @@ function readRuntimeDebugSnapshot() {
|
||||
taskPromptBuilds: {},
|
||||
taskLlmRequests: {},
|
||||
injections: {},
|
||||
messageTrace: {
|
||||
lastSentUserMessage: null,
|
||||
},
|
||||
maintenance: {
|
||||
lastAction: null,
|
||||
lastUndoResult: null,
|
||||
@@ -1206,6 +1210,9 @@ function clearRecallInputTracking() {
|
||||
pendingRecallSendIntent = createRecallInputRecord();
|
||||
lastRecallSentUserMessage = createRecallInputRecord();
|
||||
pendingHostGenerationInputSnapshot = createRecallInputRecord();
|
||||
recordMessageTraceSnapshot({
|
||||
lastSentUserMessage: null,
|
||||
});
|
||||
clearPlannerRecallHandoffsForChat("", { clearAll: true });
|
||||
}
|
||||
|
||||
@@ -9521,6 +9528,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(),
|
||||
|
||||
166
llm.js
166
llm.js
@@ -215,6 +215,104 @@ function applyTaskOutputRegexStages(taskType, text) {
|
||||
};
|
||||
}
|
||||
|
||||
function applyTaskFinalInputRegex(taskType, messages = []) {
|
||||
const normalizedMessages = (Array.isArray(messages) ? messages : [])
|
||||
.map((message) => {
|
||||
if (!message || typeof message !== "object") {
|
||||
return null;
|
||||
}
|
||||
const role = String(message.role || "").trim().toLowerCase();
|
||||
if (!["system", "user", "assistant"].includes(role)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
role,
|
||||
content: String(message.content || ""),
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
const normalizedTaskType = String(taskType || "").trim();
|
||||
|
||||
if (!normalizedTaskType || normalizedMessages.length === 0) {
|
||||
const cleanedMessages = normalizedMessages.filter((message) =>
|
||||
String(message.content || "").trim(),
|
||||
);
|
||||
return {
|
||||
messages: cleanedMessages,
|
||||
debug: {
|
||||
stage: "input.finalPrompt",
|
||||
changed: cleanedMessages.length !== normalizedMessages.length,
|
||||
applied: false,
|
||||
rawMessageCount: normalizedMessages.length,
|
||||
cleanedMessageCount: cleanedMessages.length,
|
||||
droppedMessageCount: normalizedMessages.length - cleanedMessages.length,
|
||||
stages: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const settings = extension_settings[MODULE_NAME] || {};
|
||||
const regexDebug = { entries: [] };
|
||||
let changed = false;
|
||||
let droppedMessageCount = 0;
|
||||
const cleanedMessages = normalizedMessages
|
||||
.map((message) => {
|
||||
const originalContent = String(message.content || "");
|
||||
const cleanedContent = applyTaskRegex(
|
||||
settings,
|
||||
normalizedTaskType,
|
||||
"input.finalPrompt",
|
||||
originalContent,
|
||||
regexDebug,
|
||||
message.role,
|
||||
);
|
||||
if (cleanedContent !== originalContent) {
|
||||
changed = true;
|
||||
}
|
||||
if (!String(cleanedContent || "").trim()) {
|
||||
droppedMessageCount += 1;
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
content: cleanedContent,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
const normalizedEntries = normalizeRegexDebugEntries(regexDebug);
|
||||
const applied = normalizedEntries.some(
|
||||
(entry) => entry.appliedRules.length > 0,
|
||||
);
|
||||
|
||||
return {
|
||||
messages: cleanedMessages,
|
||||
debug: {
|
||||
stage: "input.finalPrompt",
|
||||
changed: changed || droppedMessageCount > 0,
|
||||
applied,
|
||||
rawMessageCount: normalizedMessages.length,
|
||||
cleanedMessageCount: cleanedMessages.length,
|
||||
droppedMessageCount,
|
||||
stages: normalizedEntries,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function attachRequestCleaningToPromptExecution(
|
||||
promptExecutionSummary,
|
||||
requestCleaning,
|
||||
) {
|
||||
const base =
|
||||
promptExecutionSummary && typeof promptExecutionSummary === "object"
|
||||
? cloneRuntimeDebugValue(promptExecutionSummary, {})
|
||||
: {};
|
||||
if (requestCleaning && typeof requestCleaning === "object") {
|
||||
base.requestCleaning = cloneRuntimeDebugValue(requestCleaning, null);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function buildEffectiveLlmRoute(
|
||||
hasDedicatedConfig,
|
||||
privateRequestSource,
|
||||
@@ -1477,7 +1575,7 @@ export async function callLLMForJSON({
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const messages = buildJsonAttemptMessages(
|
||||
const assembledMessages = buildJsonAttemptMessages(
|
||||
systemPrompt,
|
||||
userPrompt,
|
||||
attempt,
|
||||
@@ -1485,7 +1583,25 @@ export async function callLLMForJSON({
|
||||
additionalMessages,
|
||||
promptMessages,
|
||||
);
|
||||
const response = await callDedicatedOpenAICompatible(messages, {
|
||||
const requestCleaning = applyTaskFinalInputRegex(
|
||||
taskType,
|
||||
assembledMessages,
|
||||
);
|
||||
const promptExecutionSnapshot = attachRequestCleaningToPromptExecution(
|
||||
promptExecutionSummary,
|
||||
requestCleaning.debug,
|
||||
);
|
||||
recordTaskLlmRequest(
|
||||
taskType || privateRequestSource,
|
||||
{
|
||||
requestCleaning: requestCleaning.debug,
|
||||
promptExecution: promptExecutionSnapshot,
|
||||
},
|
||||
{
|
||||
merge: true,
|
||||
},
|
||||
);
|
||||
const response = await callDedicatedOpenAICompatible(requestCleaning.messages, {
|
||||
signal,
|
||||
jsonMode: true,
|
||||
taskType,
|
||||
@@ -1500,8 +1616,9 @@ export async function callLLMForJSON({
|
||||
recordTaskLlmRequest(
|
||||
taskType || privateRequestSource,
|
||||
{
|
||||
requestCleaning: requestCleaning.debug,
|
||||
responseCleaning: outputCleanup.debug,
|
||||
promptExecution: promptExecutionSummary,
|
||||
promptExecution: promptExecutionSnapshot,
|
||||
},
|
||||
{
|
||||
merge: true,
|
||||
@@ -1592,19 +1709,48 @@ export async function callLLM(systemPrompt, userPrompt, options = {}) {
|
||||
return await override(systemPrompt, userPrompt, options);
|
||||
}
|
||||
|
||||
const messages = [
|
||||
const taskType = String(options.taskType || "").trim();
|
||||
const privateRequestSource = resolvePrivateRequestSource(
|
||||
taskType,
|
||||
options.requestSource || options.source || "diagnostic:call-llm",
|
||||
{ allowAnonymous: true },
|
||||
);
|
||||
const promptExecutionSummary = buildPromptExecutionSummary(
|
||||
options.debugContext || null,
|
||||
);
|
||||
const assembledMessages = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt },
|
||||
];
|
||||
const requestCleaning = applyTaskFinalInputRegex(taskType, assembledMessages);
|
||||
const promptExecutionSnapshot = attachRequestCleaningToPromptExecution(
|
||||
promptExecutionSummary,
|
||||
requestCleaning.debug,
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await callDedicatedOpenAICompatible(messages, {
|
||||
signal: options.signal,
|
||||
taskType: options.taskType || "",
|
||||
requestSource:
|
||||
options.requestSource || options.source || "diagnostic:call-llm",
|
||||
recordTaskLlmRequest(taskType || privateRequestSource, {
|
||||
requestCleaning: requestCleaning.debug,
|
||||
promptExecution: promptExecutionSnapshot,
|
||||
}, {
|
||||
merge: true,
|
||||
});
|
||||
return response?.content || null;
|
||||
const response = await callDedicatedOpenAICompatible(requestCleaning.messages, {
|
||||
signal: options.signal,
|
||||
taskType,
|
||||
requestSource: privateRequestSource,
|
||||
});
|
||||
const responseText =
|
||||
typeof response?.content === "string" ? response.content : "";
|
||||
const outputCleanup = applyTaskOutputRegexStages(taskType, responseText);
|
||||
recordTaskLlmRequest(taskType || privateRequestSource, {
|
||||
requestCleaning: requestCleaning.debug,
|
||||
responseCleaning: outputCleanup.debug,
|
||||
promptExecution: promptExecutionSnapshot,
|
||||
}, {
|
||||
merge: true,
|
||||
});
|
||||
return outputCleanup.cleanedText || null;
|
||||
} catch (e) {
|
||||
console.error("[ST-BME] LLM 调用失败:", e);
|
||||
return null;
|
||||
|
||||
263
panel.js
263
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";
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
getLegacyPromptFieldForTask,
|
||||
getTaskTypeOptions,
|
||||
importTaskProfile as parseImportedTaskProfile,
|
||||
isTaskRegexStageEnabled,
|
||||
normalizeTaskRegexStages,
|
||||
restoreDefaultTaskProfile,
|
||||
setActiveTaskProfileId,
|
||||
upsertTaskProfile,
|
||||
@@ -143,8 +146,46 @@ const TASK_PROFILE_GENERATION_GROUPS = [
|
||||
];
|
||||
|
||||
const TASK_PROFILE_REGEX_STAGES = [
|
||||
{ key: "input", label: "输入阶段", desc: "对发送给 LLM 的 prompt 执行正则替换。" },
|
||||
{ key: "output", label: "输出阶段", desc: "对 LLM 返回的结果执行正则替换。" },
|
||||
{
|
||||
key: "input",
|
||||
label: "输入总开关",
|
||||
desc: "控制全部输入阶段;未单独覆写的细分阶段会跟随它。",
|
||||
},
|
||||
{
|
||||
key: "input.userMessage",
|
||||
label: "输入: 用户消息",
|
||||
desc: "处理当前 userMessage。",
|
||||
},
|
||||
{
|
||||
key: "input.recentMessages",
|
||||
label: "输入: 最近上下文",
|
||||
desc: "处理 recentMessages、chatMessages、dialogueText。",
|
||||
},
|
||||
{
|
||||
key: "input.candidateText",
|
||||
label: "输入: 候选与摘要",
|
||||
desc: "处理 candidateText、candidateNodes、nodeContent 和各类摘要。",
|
||||
},
|
||||
{
|
||||
key: "input.finalPrompt",
|
||||
label: "输入: 发送前最终消息",
|
||||
desc: "在最终 messages 全部组装完成、真正发送给 LLM 前统一清洗。",
|
||||
},
|
||||
{
|
||||
key: "output",
|
||||
label: "输出总开关",
|
||||
desc: "控制全部输出阶段;未单独覆写的细分阶段会跟随它。",
|
||||
},
|
||||
{
|
||||
key: "output.rawResponse",
|
||||
label: "输出: 原始响应",
|
||||
desc: "LLM 原始文本到手后先清洗一次。",
|
||||
},
|
||||
{
|
||||
key: "output.beforeParse",
|
||||
label: "输出: 解析前",
|
||||
desc: "在 JSON 提取/解析前再清洗一次。",
|
||||
},
|
||||
];
|
||||
|
||||
let panelEl = null;
|
||||
@@ -792,6 +833,11 @@ function _applyWorkspaceMode() {
|
||||
function _switchConfigSection(sectionId) {
|
||||
currentConfigSectionId = sectionId || "api";
|
||||
_syncConfigSectionState();
|
||||
if (currentConfigSectionId === "prompts") {
|
||||
_refreshTaskProfileWorkspace();
|
||||
} else if (currentConfigSectionId === "trace") {
|
||||
_refreshMessageTraceWorkspace();
|
||||
}
|
||||
}
|
||||
|
||||
function _syncConfigSectionState() {
|
||||
@@ -2738,7 +2784,6 @@ function _renderMessageTraceWorkspace(state) {
|
||||
|
||||
function _renderMessageTraceRecallCard(state) {
|
||||
const injectionSnapshot = state.recallInjection || null;
|
||||
const recallLlmRequest = state.recallLlmRequest || null;
|
||||
const recentMessages = Array.isArray(injectionSnapshot?.recentMessages)
|
||||
? injectionSnapshot.recentMessages.map((item) => String(item || ""))
|
||||
: [];
|
||||
@@ -2747,12 +2792,19 @@ function _renderMessageTraceRecallCard(state) {
|
||||
).trim();
|
||||
const triggeredUserMessage =
|
||||
lastSentUserMessage ||
|
||||
_extractTriggeredUserMessageFromRecentMessages(recentMessages) ||
|
||||
_getLastDebugMessageContent(recallLlmRequest?.messages, "user");
|
||||
_extractTriggeredUserMessageFromRecentMessages(recentMessages);
|
||||
const hostPayloadText = _buildMainAiTraceText(
|
||||
triggeredUserMessage,
|
||||
injectionSnapshot?.injectionText || "",
|
||||
);
|
||||
const missingUserMessageNotice =
|
||||
injectionSnapshot && !triggeredUserMessage
|
||||
? `
|
||||
<div class="bme-config-help">
|
||||
这次没有可靠捕获到主 AI 那边的用户消息,因此这里只展示真实记录到的记忆注入文本,不再用 recall 模型请求去反推,避免误导排查。
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
|
||||
if (!injectionSnapshot) {
|
||||
return `
|
||||
@@ -2770,6 +2822,7 @@ function _renderMessageTraceRecallCard(state) {
|
||||
</div>
|
||||
<span class="bme-task-pill">${_escHtml(_formatTaskProfileTime(injectionSnapshot.updatedAt))}</span>
|
||||
</div>
|
||||
${missingUserMessageNotice}
|
||||
${_renderMessageTraceTextBlock(
|
||||
"发送给主 AI 的内容",
|
||||
hostPayloadText,
|
||||
@@ -2838,18 +2891,6 @@ function _normalizeDebugMessages(messages = []) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function _getLastDebugMessageContent(messages = [], role = "") {
|
||||
const normalizedRole = String(role || "").trim().toLowerCase();
|
||||
const normalizedMessages = _normalizeDebugMessages(messages);
|
||||
for (let index = normalizedMessages.length - 1; index >= 0; index--) {
|
||||
const message = normalizedMessages[index];
|
||||
if (!normalizedRole || message.role === normalizedRole) {
|
||||
return message.content;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function _stringifyTraceMessages(messages = []) {
|
||||
const normalizedMessages = _normalizeDebugMessages(messages);
|
||||
if (!normalizedMessages.length) return "";
|
||||
@@ -2953,6 +2994,9 @@ async function _handleTaskProfileWorkspaceClick(event) {
|
||||
}
|
||||
_refreshTaskProfileWorkspace();
|
||||
return;
|
||||
case "inspect-tavern-regex":
|
||||
await _openRegexReuseInspector(state.taskType);
|
||||
return;
|
||||
case "select-block":
|
||||
currentTaskProfileBlockId = actionEl.dataset.blockId || "";
|
||||
_refreshTaskProfileWorkspace();
|
||||
@@ -3347,6 +3391,7 @@ function _renderTaskGenerationTab(state) {
|
||||
|
||||
function _renderTaskRegexTab(state) {
|
||||
const regex = state.profile.regex || {};
|
||||
const normalizedStages = normalizeTaskRegexStages(regex.stages || {});
|
||||
return `
|
||||
<div class="bme-task-tab-body">
|
||||
<div class="bme-task-regex-top">
|
||||
@@ -3358,6 +3403,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">
|
||||
@@ -3420,14 +3468,14 @@ function _renderTaskRegexTab(state) {
|
||||
<span class="bme-toggle-title">${_escHtml(stage.label)}</span>
|
||||
<span class="bme-toggle-desc">${_escHtml(stage.desc)}</span>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
data-regex-stage="${_escAttr(stage.key)}"
|
||||
${(regex.stages?.[stage.key] ?? true) ? "checked" : ""}
|
||||
/>
|
||||
</label>
|
||||
`,
|
||||
).join("")}
|
||||
<input
|
||||
type="checkbox"
|
||||
data-regex-stage="${_escAttr(stage.key)}"
|
||||
${isTaskRegexStageEnabled(normalizedStages, stage.key) ? "checked" : ""}
|
||||
/>
|
||||
</label>
|
||||
`,
|
||||
).join("")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3465,6 +3513,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 || {};
|
||||
@@ -3759,8 +3965,13 @@ function _renderTaskDebugLlmCard(taskType, llmRequest) {
|
||||
<span class="bme-debug-kv-key">输出清洗</span>
|
||||
<span class="bme-debug-kv-value">${_escHtml(llmRequest.responseCleaning?.applied ? "已生效" : "未生效")}</span>
|
||||
</div>
|
||||
<div class="bme-debug-kv-item">
|
||||
<span class="bme-debug-kv-key">发送前输入清洗</span>
|
||||
<span class="bme-debug-kv-value">${_escHtml(llmRequest.requestCleaning?.applied ? "已生效" : "未生效")}</span>
|
||||
</div>
|
||||
</div>
|
||||
${_renderDebugDetails("提示词执行摘要", llmRequest.promptExecution || null)}
|
||||
${_renderDebugDetails("发送前输入清洗", llmRequest.requestCleaning || null)}
|
||||
${_renderDebugDetails("实际请求路径", llmRequest.effectiveRoute || null)}
|
||||
${_renderDebugDetails("输出清洗", llmRequest.responseCleaning || null)}
|
||||
${_renderDebugDetails("实际保留参数", llmRequest.filteredGeneration || {})}
|
||||
@@ -4628,7 +4839,7 @@ function _normalizeTaskProfileDraft(profile = {}) {
|
||||
stages: {
|
||||
input: true,
|
||||
output: true,
|
||||
...(draft.regex?.stages || {}),
|
||||
...normalizeTaskRegexStages(draft.regex?.stages || {}),
|
||||
},
|
||||
localRules: Array.isArray(draft.regex?.localRules)
|
||||
? draft.regex.localRules.map((rule) => ({
|
||||
|
||||
@@ -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;
|
||||
@@ -526,7 +533,7 @@ function sanitizePromptMessages(
|
||||
messages = [],
|
||||
{
|
||||
blockedContents = [],
|
||||
regexStage = "input.finalPrompt",
|
||||
regexStage = "",
|
||||
debugState = null,
|
||||
regexCollector = null,
|
||||
} = {},
|
||||
@@ -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,
|
||||
@@ -646,7 +654,7 @@ function sanitizeWorldInfoEntries(
|
||||
{
|
||||
mode: "aggressive",
|
||||
blockedContents,
|
||||
regexStage: "input.finalPrompt",
|
||||
regexStage: "",
|
||||
role: entry?.role || "system",
|
||||
regexCollector,
|
||||
},
|
||||
@@ -728,7 +736,7 @@ function sanitizeWorldInfoContext(
|
||||
{
|
||||
mode: "aggressive",
|
||||
blockedContents: runtimeBlockedContents,
|
||||
regexStage: "input.finalPrompt",
|
||||
regexStage: "",
|
||||
role: message?.role || "system",
|
||||
regexCollector,
|
||||
},
|
||||
@@ -1107,7 +1115,7 @@ export async function buildTaskPrompt(settings = {}, taskType, context = {}) {
|
||||
{
|
||||
mode: "final-safe",
|
||||
blockedContents: worldInfoRuntimeBlockedContents,
|
||||
regexStage: "input.finalPrompt",
|
||||
regexStage: "",
|
||||
role,
|
||||
regexCollector: promptRegexInput,
|
||||
},
|
||||
|
||||
@@ -569,6 +569,88 @@ function normalizeRegexLocalRule(rule = {}, taskType = "task", index = 0) {
|
||||
};
|
||||
}
|
||||
|
||||
const TASK_REGEX_STAGE_ALIAS_MAP = Object.freeze({
|
||||
finalPrompt: "input.finalPrompt",
|
||||
rawResponse: "output.rawResponse",
|
||||
beforeParse: "output.beforeParse",
|
||||
});
|
||||
|
||||
const TASK_REGEX_STAGE_GROUPS = Object.freeze({
|
||||
input: Object.freeze([
|
||||
"input.userMessage",
|
||||
"input.recentMessages",
|
||||
"input.candidateText",
|
||||
"input.finalPrompt",
|
||||
]),
|
||||
output: Object.freeze([
|
||||
"output.rawResponse",
|
||||
"output.beforeParse",
|
||||
]),
|
||||
});
|
||||
|
||||
function normalizeRegexStageKey(stageKey = "") {
|
||||
const normalized = String(stageKey || "").trim();
|
||||
return TASK_REGEX_STAGE_ALIAS_MAP[normalized] || normalized;
|
||||
}
|
||||
|
||||
export function normalizeTaskRegexStages(stages = {}) {
|
||||
const source =
|
||||
stages && typeof stages === "object" && !Array.isArray(stages) ? stages : {};
|
||||
const normalized = { ...source };
|
||||
|
||||
for (const [legacyKey, canonicalKey] of Object.entries(
|
||||
TASK_REGEX_STAGE_ALIAS_MAP,
|
||||
)) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(normalized, canonicalKey) &&
|
||||
Object.prototype.hasOwnProperty.call(normalized, legacyKey)
|
||||
) {
|
||||
normalized[canonicalKey] = Boolean(normalized[legacyKey]);
|
||||
}
|
||||
delete normalized[legacyKey];
|
||||
}
|
||||
|
||||
for (const [groupKey, stageKeys] of Object.entries(TASK_REGEX_STAGE_GROUPS)) {
|
||||
if (normalized[groupKey] === false) {
|
||||
continue;
|
||||
}
|
||||
const allSpecificStagesFalse =
|
||||
stageKeys.length > 0 &&
|
||||
stageKeys.every((stageKey) => normalized[stageKey] === false);
|
||||
if (!allSpecificStagesFalse) {
|
||||
continue;
|
||||
}
|
||||
for (const stageKey of stageKeys) {
|
||||
delete normalized[stageKey];
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function isTaskRegexStageEnabled(stages = {}, stageKey = "") {
|
||||
const normalizedStages = normalizeTaskRegexStages(stages);
|
||||
const normalizedStageKey = normalizeRegexStageKey(stageKey);
|
||||
|
||||
if (!normalizedStageKey) {
|
||||
return normalizedStages.input !== false;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(normalizedStages, normalizedStageKey)) {
|
||||
return normalizedStages[normalizedStageKey] !== false;
|
||||
}
|
||||
|
||||
if (normalizedStageKey.startsWith("input.")) {
|
||||
return normalizedStages.input !== false;
|
||||
}
|
||||
|
||||
if (normalizedStageKey.startsWith("output.")) {
|
||||
return normalizedStages.output !== false;
|
||||
}
|
||||
|
||||
return normalizedStages[normalizedStageKey] !== false;
|
||||
}
|
||||
|
||||
function normalizeTaskProfilesState(taskProfiles = {}) {
|
||||
return ensureTaskProfiles({ taskProfiles });
|
||||
}
|
||||
@@ -741,7 +823,7 @@ function createFallbackDefaultTaskProfile(taskType) {
|
||||
preset: true,
|
||||
character: true,
|
||||
},
|
||||
stages: {
|
||||
stages: normalizeTaskRegexStages({
|
||||
finalPrompt: true,
|
||||
"input.userMessage": false,
|
||||
"input.recentMessages": false,
|
||||
@@ -751,7 +833,7 @@ function createFallbackDefaultTaskProfile(taskType) {
|
||||
beforeParse: false,
|
||||
"output.rawResponse": false,
|
||||
"output.beforeParse": false,
|
||||
},
|
||||
}),
|
||||
localRules: [],
|
||||
},
|
||||
metadata: {
|
||||
@@ -799,10 +881,10 @@ export function createDefaultTaskProfile(taskType) {
|
||||
...fallback.regex.sources,
|
||||
...(template?.regex?.sources || {}),
|
||||
},
|
||||
stages: {
|
||||
stages: normalizeTaskRegexStages({
|
||||
...fallback.regex.stages,
|
||||
...(template?.regex?.stages || {}),
|
||||
},
|
||||
}),
|
||||
localRules: Array.isArray(template?.regex?.localRules)
|
||||
? template.regex.localRules.map((rule, index) =>
|
||||
normalizeRegexLocalRule(rule, taskType, index),
|
||||
@@ -978,10 +1060,10 @@ export function normalizeTaskProfile(taskType, profile = {}, settings = {}) {
|
||||
...base.regex.sources,
|
||||
...(profile?.regex?.sources || {}),
|
||||
},
|
||||
stages: {
|
||||
stages: normalizeTaskRegexStages({
|
||||
...base.regex.stages,
|
||||
...(profile?.regex?.stages || {}),
|
||||
},
|
||||
}),
|
||||
localRules: Array.isArray(profile?.regex?.localRules)
|
||||
? profile.regex.localRules.map((rule, index) =>
|
||||
normalizeRegexLocalRule(rule, taskType, index),
|
||||
|
||||
522
task-regex.js
522
task-regex.js
@@ -4,11 +4,29 @@
|
||||
|
||||
import { extension_settings, getContext } from "../../../extensions.js";
|
||||
import { getHostAdapter } from "./host-adapter/index.js";
|
||||
import { getActiveTaskProfile } from "./prompt-profiles.js";
|
||||
import {
|
||||
getActiveTaskProfile,
|
||||
isTaskRegexStageEnabled,
|
||||
normalizeTaskRegexStages,
|
||||
} from "./prompt-profiles.js";
|
||||
|
||||
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",
|
||||
@@ -65,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"
|
||||
@@ -72,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}`),
|
||||
@@ -82,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,
|
||||
};
|
||||
@@ -190,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") {
|
||||
@@ -229,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?.() || {};
|
||||
@@ -247,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 = {}) {
|
||||
@@ -318,31 +623,55 @@ 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 (
|
||||
normalizedStage &&
|
||||
Object.prototype.hasOwnProperty.call(stagesConfig, normalizedStage)
|
||||
) {
|
||||
return (
|
||||
stagesConfig[normalizedStage] !== false &&
|
||||
rule.destinationFlags.prompt !== false
|
||||
);
|
||||
if (rule.destinationFlags.prompt === false) {
|
||||
return false;
|
||||
}
|
||||
if (PROMPT_STAGES.has(normalizedStage)) {
|
||||
return (
|
||||
stagesConfig.input !== false && rule.destinationFlags.prompt !== false
|
||||
);
|
||||
if (!shouldApplyRuleForTaskContext(rule, normalizedStage)) {
|
||||
return false;
|
||||
}
|
||||
if (OUTPUT_STAGES.has(normalizedStage)) {
|
||||
return (
|
||||
stagesConfig.output !== false && rule.destinationFlags.prompt !== false
|
||||
);
|
||||
|
||||
if (!normalizedStage) {
|
||||
return isTaskRegexStageEnabled(stagesConfig, "input");
|
||||
}
|
||||
return stagesConfig.input !== false && rule.destinationFlags.prompt !== false;
|
||||
|
||||
if (PROMPT_STAGES.has(normalizedStage) || OUTPUT_STAGES.has(normalizedStage)) {
|
||||
return isTaskRegexStageEnabled(stagesConfig, normalizedStage);
|
||||
}
|
||||
|
||||
return isTaskRegexStageEnabled(stagesConfig, normalizedStage);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -398,7 +727,7 @@ export function applyTaskRegex(
|
||||
}
|
||||
|
||||
// 阶段检查已移到 shouldApplyRuleForStage 中,无需单独 gate
|
||||
const stagesConfig = regexConfig?.stages || {};
|
||||
const stagesConfig = normalizeTaskRegexStages(regexConfig?.stages || {});
|
||||
|
||||
const tavernRules = collectTavernRules(regexConfig);
|
||||
const localRules = collectLocalRules(regexConfig);
|
||||
@@ -441,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)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -254,7 +254,8 @@ try {
|
||||
|
||||
assert.match(promptBuild.systemPrompt, /GOOD_RECENT/);
|
||||
assert.match(JSON.stringify(promptBuild.executionMessages), /GOOD_CANDIDATE/);
|
||||
assert.match(promptBuild.systemPrompt, /FINAL_GOOD/);
|
||||
assert.match(promptBuild.systemPrompt, /FINAL_BAD/);
|
||||
assert.doesNotMatch(promptBuild.systemPrompt, /FINAL_GOOD/);
|
||||
assert.equal(
|
||||
promptBuild.debug.mvu.sanitizedFields.some((entry) => entry.name === "userMessage"),
|
||||
true,
|
||||
@@ -454,6 +455,8 @@ try {
|
||||
|
||||
const payload = buildTaskLlmPayload(promptBuild, "unused fallback");
|
||||
assert.equal(payload.systemPrompt, "");
|
||||
assert.match(JSON.stringify(payload.promptMessages), /FINAL_BAD/);
|
||||
assert.doesNotMatch(JSON.stringify(payload.promptMessages), /FINAL_GOOD/);
|
||||
const result = await llm.callLLMForJSON({
|
||||
systemPrompt: payload.systemPrompt,
|
||||
userPrompt: payload.userPrompt,
|
||||
@@ -466,6 +469,8 @@ try {
|
||||
|
||||
assert.deepEqual(result, { ok: true });
|
||||
assert.equal(capturedBodies.length, 1);
|
||||
assert.match(JSON.stringify(capturedBodies[0].messages), /FINAL_GOOD/);
|
||||
assert.doesNotMatch(JSON.stringify(capturedBodies[0].messages), /FINAL_BAD/);
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(capturedBodies[0].messages),
|
||||
/status_current_variable|updatevariable|StatusPlaceHolderImpl|stat_data|display_data|delta_data|get_message_variable/i,
|
||||
@@ -478,6 +483,18 @@ try {
|
||||
|
||||
assert.ok(runtimePromptBuild);
|
||||
assert.ok(runtimeLlmRequest);
|
||||
assert.match(JSON.stringify(runtimeLlmRequest.messages), /FINAL_GOOD/);
|
||||
assert.equal(runtimeLlmRequest.requestCleaning?.applied, true);
|
||||
assert.equal(
|
||||
runtimeLlmRequest.requestCleaning?.stages?.length > 0,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
runtimeLlmRequest.requestCleaning?.stages?.every(
|
||||
(entry) => entry.stage === "input.finalPrompt",
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(runtimePromptBuild.executionMessages),
|
||||
/status_current_variable|updatevariable|StatusPlaceHolderImpl|stat_data|display_data|delta_data|get_message_variable/i,
|
||||
|
||||
@@ -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,54 +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, []);
|
||||
|
||||
console.log("task-regex tests passed");
|
||||
} finally {
|
||||
if (originalSillyTavern === undefined) {
|
||||
|
||||
Reference in New Issue
Block a user