mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
473 lines
12 KiB
JavaScript
473 lines
12 KiB
JavaScript
import {
|
|
getRegexedString as coreGetRegexedString,
|
|
regex_placement as coreRegexPlacement,
|
|
} from "../../../../regex/engine.js";
|
|
import { buildCapabilityStatus, mergeVersionHints } from "./capabilities.js";
|
|
import { createContextHostFacade } from "./context.js";
|
|
import { debugDebug } from "../../runtime/debug-logging.js";
|
|
|
|
const REGEX_API_NAMES = [
|
|
"getTavernRegexes",
|
|
"isCharacterTavernRegexesEnabled",
|
|
"formatAsTavernRegexedString",
|
|
];
|
|
const CORE_REGEX_SOURCE_TO_PLACEMENT_KEY = Object.freeze({
|
|
user_input: "USER_INPUT",
|
|
ai_output: "AI_OUTPUT",
|
|
slash_command: "SLASH_COMMAND",
|
|
world_info: "WORLD_INFO",
|
|
reasoning: "REASONING",
|
|
});
|
|
const REGEX_SOURCE_KIND_PRIORITY = Object.freeze({
|
|
unknown: 0,
|
|
unavailable: 0,
|
|
"global-fallback": 1,
|
|
context: 2,
|
|
"core-bridge": 3,
|
|
"api-map": 4,
|
|
provider: 5,
|
|
});
|
|
const REGEX_BRIDGE_TIER_PRIORITY = Object.freeze({
|
|
unavailable: 0,
|
|
"helper-getter-only": 1,
|
|
"helper-bridge": 2,
|
|
"core-real": 3,
|
|
});
|
|
|
|
function isObjectLike(value) {
|
|
return (
|
|
value != null && (typeof value === "object" || typeof value === "function")
|
|
);
|
|
}
|
|
|
|
function bindHostFunction(container, name) {
|
|
const fn = container?.[name];
|
|
return typeof fn === "function" ? fn.bind(container) : null;
|
|
}
|
|
|
|
function resolveCorePlacement(regexPlacement, source) {
|
|
const normalizedSource = String(source || "").trim().toLowerCase();
|
|
const placementKey = CORE_REGEX_SOURCE_TO_PLACEMENT_KEY[normalizedSource];
|
|
if (!placementKey || !isObjectLike(regexPlacement)) {
|
|
return null;
|
|
}
|
|
const placement = regexPlacement?.[placementKey];
|
|
return Number.isFinite(Number(placement)) ? Number(placement) : null;
|
|
}
|
|
|
|
function hasCoreRegexApi(container) {
|
|
return (
|
|
typeof container?.getRegexedString === "function" &&
|
|
resolveCorePlacement(container?.regex_placement, "user_input") != null
|
|
);
|
|
}
|
|
|
|
function normalizeCoreFormatterOptions(destination, options = {}) {
|
|
const normalizedDestination =
|
|
typeof destination === "string" ? String(destination || "").trim() : "";
|
|
const normalizedOptions =
|
|
destination &&
|
|
typeof destination === "object" &&
|
|
!Array.isArray(destination)
|
|
? { ...destination }
|
|
: options && typeof options === "object" && !Array.isArray(options)
|
|
? { ...options }
|
|
: {};
|
|
|
|
if (normalizedDestination === "display" && normalizedOptions.isMarkdown == null) {
|
|
normalizedOptions.isMarkdown = true;
|
|
}
|
|
if (normalizedDestination === "prompt" && normalizedOptions.isPrompt == null) {
|
|
normalizedOptions.isPrompt = true;
|
|
}
|
|
if (
|
|
normalizedOptions.character_name != null &&
|
|
normalizedOptions.characterOverride == null
|
|
) {
|
|
normalizedOptions.characterOverride = normalizedOptions.character_name;
|
|
}
|
|
delete normalizedOptions.character_name;
|
|
return normalizedOptions;
|
|
}
|
|
|
|
function createCoreFormatterBridge(container) {
|
|
if (!hasCoreRegexApi(container)) {
|
|
return null;
|
|
}
|
|
const getRegexedString = bindHostFunction(container, "getRegexedString");
|
|
const regexPlacement = container?.regex_placement;
|
|
if (typeof getRegexedString !== "function") {
|
|
return null;
|
|
}
|
|
|
|
return function formatAsTavernRegexedString(
|
|
text,
|
|
source,
|
|
destination,
|
|
options = {}
|
|
) {
|
|
const placement = resolveCorePlacement(regexPlacement, source);
|
|
if (placement == null) {
|
|
return String(text ?? "");
|
|
}
|
|
return getRegexedString(
|
|
String(text ?? ""),
|
|
placement,
|
|
normalizeCoreFormatterOptions(destination, options)
|
|
);
|
|
};
|
|
}
|
|
|
|
function buildApiMap(container = null) {
|
|
const apiMap = REGEX_API_NAMES.reduce((result, name) => {
|
|
result[name] = bindHostFunction(container, name);
|
|
return result;
|
|
}, {});
|
|
|
|
if (typeof apiMap.formatAsTavernRegexedString !== "function") {
|
|
apiMap.formatAsTavernRegexedString = createCoreFormatterBridge(container);
|
|
}
|
|
|
|
return apiMap;
|
|
}
|
|
|
|
function countResolvedApis(apiMap = {}) {
|
|
return Object.values(apiMap).filter((api) => typeof api === "function")
|
|
.length;
|
|
}
|
|
|
|
function detectBridgeTier({ hasCoreApi = false, apiMap = {} } = {}) {
|
|
const hasGetter = typeof apiMap.getTavernRegexes === "function";
|
|
const hasFormatter =
|
|
typeof apiMap.formatAsTavernRegexedString === "function";
|
|
|
|
if (hasCoreApi && hasFormatter) {
|
|
return "core-real";
|
|
}
|
|
if (hasFormatter) {
|
|
return "helper-bridge";
|
|
}
|
|
if (hasGetter) {
|
|
return "helper-getter-only";
|
|
}
|
|
return "unavailable";
|
|
}
|
|
|
|
function resolveProviderCandidate(candidate, options = {}) {
|
|
if (!candidate) {
|
|
return null;
|
|
}
|
|
|
|
if (typeof candidate === "function") {
|
|
try {
|
|
const resolved = candidate(options);
|
|
return isObjectLike(resolved) ? resolved : null;
|
|
} catch (error) {
|
|
debugDebug("[ST-BME] host-adapter/regex provider 解析失败", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return isObjectLike(candidate) ? candidate : null;
|
|
}
|
|
|
|
function buildSourceRecord({
|
|
label = "unknown",
|
|
sourceKind = "unknown",
|
|
container = null,
|
|
fallback = false,
|
|
} = {}) {
|
|
const apiMap = buildApiMap(container);
|
|
const hasCoreApi = hasCoreRegexApi(container);
|
|
const bridgeTier = detectBridgeTier({ hasCoreApi, apiMap });
|
|
|
|
return Object.freeze({
|
|
label,
|
|
sourceKind,
|
|
fallback,
|
|
apiMap,
|
|
apiCount: countResolvedApis(apiMap),
|
|
hasCoreApi,
|
|
bridgeTier,
|
|
});
|
|
}
|
|
|
|
function collectExplicitRegexSourceRecords(options = {}) {
|
|
const records = [];
|
|
const providerCandidates = [
|
|
["regexProvider", options.regexProvider],
|
|
["providers.regex", options.providers?.regex],
|
|
["provider.regex", options.provider?.regex],
|
|
["host.regex", options.host?.regex],
|
|
["host.providers.regex", options.host?.providers?.regex],
|
|
];
|
|
|
|
for (const [label, candidate] of providerCandidates) {
|
|
const container = resolveProviderCandidate(candidate, options);
|
|
if (!container) continue;
|
|
|
|
records.push(
|
|
buildSourceRecord({
|
|
label,
|
|
sourceKind: "provider",
|
|
container,
|
|
}),
|
|
);
|
|
}
|
|
|
|
const apiCandidates = [
|
|
["regexApis", options.regexApis],
|
|
["apis", options.apis],
|
|
["host.apis", options.host?.apis],
|
|
["host", options.host],
|
|
];
|
|
|
|
for (const [label, candidate] of apiCandidates) {
|
|
if (!isObjectLike(candidate)) continue;
|
|
|
|
records.push(
|
|
buildSourceRecord({
|
|
label,
|
|
sourceKind: "api-map",
|
|
container: candidate,
|
|
}),
|
|
);
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
function collectCoreBridgeSourceRecords(options = {}) {
|
|
if (options?.disableCoreRegexBridge === true) {
|
|
return [];
|
|
}
|
|
const coreBridge = {
|
|
getRegexedString: coreGetRegexedString,
|
|
regex_placement: coreRegexPlacement,
|
|
};
|
|
if (!hasCoreRegexApi(coreBridge)) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
buildSourceRecord({
|
|
label: "sillytavern.core.regex",
|
|
sourceKind: "core-bridge",
|
|
container: coreBridge,
|
|
}),
|
|
];
|
|
}
|
|
|
|
function collectContextRegexSourceRecords(contextHost, options = {}) {
|
|
const context = contextHost?.readContextSnapshot?.();
|
|
if (!isObjectLike(context)) {
|
|
return [];
|
|
}
|
|
|
|
const records = [];
|
|
const contextCandidates = [
|
|
["context.regex", context.regex],
|
|
["context.tavernRegex", context.tavernRegex],
|
|
["context.host.regex", context.host?.regex],
|
|
["context.hostAdapter.regex", context.hostAdapter?.regex],
|
|
["context.providers.regex", context.providers?.regex],
|
|
["context.extensions.regex", context.extensions?.regex],
|
|
["context.TavernHelper", context.TavernHelper],
|
|
["context.sillyTavern.TavernHelper", context.sillyTavern?.TavernHelper],
|
|
["context", context],
|
|
];
|
|
|
|
for (const [label, candidate] of contextCandidates) {
|
|
const container = resolveProviderCandidate(candidate, {
|
|
...options,
|
|
context,
|
|
contextHost,
|
|
});
|
|
if (!container) continue;
|
|
|
|
records.push(
|
|
buildSourceRecord({
|
|
label,
|
|
sourceKind: "context",
|
|
container,
|
|
}),
|
|
);
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
function collectGlobalFallbackRecords() {
|
|
const records = [];
|
|
const fallbackCandidates = [
|
|
["globalThis.TavernHelper", globalThis?.TavernHelper],
|
|
[
|
|
"globalThis.SillyTavern.TavernHelper",
|
|
globalThis?.SillyTavern?.TavernHelper,
|
|
],
|
|
["globalThis", globalThis],
|
|
];
|
|
|
|
for (const [label, candidate] of fallbackCandidates) {
|
|
if (!isObjectLike(candidate)) continue;
|
|
|
|
records.push(
|
|
buildSourceRecord({
|
|
label,
|
|
sourceKind: "global-fallback",
|
|
container: candidate,
|
|
fallback: true,
|
|
}),
|
|
);
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
function scoreSourceRecord(record = {}) {
|
|
const sourceScore =
|
|
REGEX_SOURCE_KIND_PRIORITY[String(record?.sourceKind || "unknown")] || 0;
|
|
const tierScore =
|
|
REGEX_BRIDGE_TIER_PRIORITY[String(record?.bridgeTier || "unavailable")] || 0;
|
|
if (tierScore <= 0) {
|
|
return 0;
|
|
}
|
|
return sourceScore * 100 + tierScore * 10 + Number(record?.apiCount || 0);
|
|
}
|
|
|
|
function selectBestRegexSource(records = []) {
|
|
let bestRecord = null;
|
|
let bestScore = -1;
|
|
|
|
for (const record of Array.isArray(records) ? records : []) {
|
|
const score = scoreSourceRecord(record);
|
|
if (!bestRecord || score > bestScore) {
|
|
bestRecord = record;
|
|
bestScore = score;
|
|
}
|
|
}
|
|
|
|
return (
|
|
bestRecord ||
|
|
buildSourceRecord({
|
|
label: "none",
|
|
sourceKind: "unavailable",
|
|
container: null,
|
|
})
|
|
);
|
|
}
|
|
|
|
function resolveRegexSource(options = {}, contextHost = null) {
|
|
const records = [
|
|
...collectExplicitRegexSourceRecords(options),
|
|
...collectCoreBridgeSourceRecords(options),
|
|
...collectContextRegexSourceRecords(contextHost, options),
|
|
...collectGlobalFallbackRecords(),
|
|
];
|
|
|
|
return selectBestRegexSource(records);
|
|
}
|
|
|
|
function detectRegexMode(sourceRecord = {}) {
|
|
return String(sourceRecord?.bridgeTier || "").trim() || "unavailable";
|
|
}
|
|
|
|
function buildFallbackReason(sourceRecord, available, mode) {
|
|
if (!available) {
|
|
return "未检测到 Tavern Regex 宿主接口";
|
|
}
|
|
|
|
if (mode === "core-real") {
|
|
return "";
|
|
}
|
|
|
|
if (mode === "helper-bridge") {
|
|
return `当前通过 ${sourceRecord?.label || "unknown"} helper bridge 提供 Tavern Regex formatter`;
|
|
}
|
|
|
|
if (mode === "helper-getter-only") {
|
|
return `Tavern Regex 桥接仅发现规则读取接口,来源: ${sourceRecord?.label || "unknown"}`;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
export function createRegexHostFacade(options = {}) {
|
|
const contextHost = options.contextHost || createContextHostFacade(options);
|
|
const sourceRecord = resolveRegexSource(options, contextHost);
|
|
const mode = detectRegexMode(sourceRecord);
|
|
const available = mode !== "unavailable";
|
|
const formatterAvailable =
|
|
typeof sourceRecord.apiMap.formatAsTavernRegexedString === "function";
|
|
const rulesAvailable =
|
|
typeof sourceRecord.apiMap.getTavernRegexes === "function";
|
|
const fallbackReason = buildFallbackReason(sourceRecord, available, mode);
|
|
const versionHints = mergeVersionHints(
|
|
{
|
|
apis: REGEX_API_NAMES.filter(
|
|
(name) => typeof sourceRecord.apiMap[name] === "function",
|
|
),
|
|
apiCount: String(sourceRecord.apiCount),
|
|
supportsCharacterToggle:
|
|
typeof sourceRecord.apiMap.isCharacterTavernRegexesEnabled === "function"
|
|
? "yes"
|
|
: "no",
|
|
source: sourceRecord.sourceKind,
|
|
sourceLabel: sourceRecord.label,
|
|
fallback: sourceRecord.fallback ? "yes" : "no",
|
|
contextMode: contextHost?.mode || "unknown",
|
|
bridgeTier: sourceRecord.bridgeTier,
|
|
hasCoreApi: sourceRecord.hasCoreApi ? "yes" : "no",
|
|
},
|
|
options.versionHints,
|
|
);
|
|
const capabilityStatus = buildCapabilityStatus({
|
|
available,
|
|
mode,
|
|
fallbackReason,
|
|
versionHints,
|
|
});
|
|
|
|
return Object.freeze({
|
|
available,
|
|
mode,
|
|
fallbackReason,
|
|
versionHints,
|
|
capabilityStatus,
|
|
getTavernRegexes: sourceRecord.apiMap.getTavernRegexes,
|
|
isCharacterTavernRegexesEnabled:
|
|
sourceRecord.apiMap.isCharacterTavernRegexesEnabled,
|
|
formatAsTavernRegexedString:
|
|
sourceRecord.apiMap.formatAsTavernRegexedString,
|
|
getApi(name) {
|
|
return sourceRecord.apiMap[String(name || "")] || null;
|
|
},
|
|
readApiAvailability() {
|
|
return Object.freeze(
|
|
REGEX_API_NAMES.reduce((result, name) => {
|
|
result[name] = typeof sourceRecord.apiMap[name] === "function";
|
|
return result;
|
|
}, {}),
|
|
);
|
|
},
|
|
readCapabilitySupport() {
|
|
return Object.freeze({
|
|
available,
|
|
mode,
|
|
source: sourceRecord.sourceKind,
|
|
sourceLabel: sourceRecord.label,
|
|
fallback: sourceRecord.fallback,
|
|
formatterAvailable,
|
|
rulesAvailable,
|
|
bridgeTier: sourceRecord.bridgeTier,
|
|
hasCoreApi: sourceRecord.hasCoreApi,
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export function inspectRegexHostCapability(options = {}) {
|
|
const facade = createRegexHostFacade(options);
|
|
return buildCapabilityStatus(facade);
|
|
}
|