mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Reorganize modules into layered directories
This commit is contained in:
113
host/adapter/capabilities.js
Normal file
113
host/adapter/capabilities.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const DEFAULT_UNAVAILABLE_REASON = "宿主能力不可用";
|
||||
|
||||
export const HOST_ADAPTER_VERSION = "phase1-bridge-skeleton";
|
||||
|
||||
export function normalizeVersionHints(versionHints = {}) {
|
||||
const normalized = {};
|
||||
|
||||
for (const [key, rawValue] of Object.entries(versionHints || {})) {
|
||||
if (rawValue == null || rawValue === "") continue;
|
||||
|
||||
if (Array.isArray(rawValue)) {
|
||||
const values = rawValue
|
||||
.map((value) => String(value ?? "").trim())
|
||||
.filter(Boolean);
|
||||
if (values.length > 0) {
|
||||
normalized[key] = values;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof rawValue === "object") {
|
||||
const nested = normalizeVersionHints(rawValue);
|
||||
if (Object.keys(nested).length > 0) {
|
||||
normalized[key] = nested;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized[key] = String(rawValue).trim();
|
||||
}
|
||||
|
||||
return Object.freeze(normalized);
|
||||
}
|
||||
|
||||
export function mergeVersionHints(...sources) {
|
||||
const merged = {};
|
||||
|
||||
for (const source of sources) {
|
||||
if (!source || typeof source !== "object") continue;
|
||||
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (value == null || value === "") continue;
|
||||
merged[key] = Array.isArray(value) ? [...value] : value;
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeVersionHints(merged);
|
||||
}
|
||||
|
||||
export function buildCapabilityStatus({
|
||||
available = false,
|
||||
mode = "",
|
||||
fallbackReason = "",
|
||||
versionHints = {},
|
||||
} = {}) {
|
||||
const normalizedAvailable = Boolean(available);
|
||||
const normalizedMode =
|
||||
String(mode || "").trim() ||
|
||||
(normalizedAvailable ? "available" : "unavailable");
|
||||
|
||||
return Object.freeze({
|
||||
available: normalizedAvailable,
|
||||
mode: normalizedMode,
|
||||
fallbackReason: normalizedAvailable
|
||||
? String(fallbackReason || "").trim()
|
||||
: String(fallbackReason || DEFAULT_UNAVAILABLE_REASON).trim(),
|
||||
versionHints: normalizeVersionHints(versionHints),
|
||||
});
|
||||
}
|
||||
|
||||
export function buildCapabilityCollectionSnapshot(
|
||||
capabilities = {},
|
||||
options = {},
|
||||
) {
|
||||
const normalizedCapabilities = {};
|
||||
const capabilityNames = Object.keys(capabilities || {});
|
||||
let availableCount = 0;
|
||||
|
||||
for (const name of capabilityNames) {
|
||||
const capability = buildCapabilityStatus(capabilities[name]);
|
||||
normalizedCapabilities[name] = capability;
|
||||
if (capability.available) {
|
||||
availableCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const totalCount = capabilityNames.length;
|
||||
const available = availableCount > 0;
|
||||
const mode =
|
||||
totalCount === 0
|
||||
? "empty"
|
||||
: availableCount === totalCount
|
||||
? "full"
|
||||
: available
|
||||
? "partial"
|
||||
: "fallback";
|
||||
|
||||
return Object.freeze({
|
||||
available,
|
||||
mode,
|
||||
fallbackReason:
|
||||
available || totalCount === 0 ? "" : "未检测到可用宿主桥接能力",
|
||||
versionHints: mergeVersionHints(
|
||||
{
|
||||
adapter: HOST_ADAPTER_VERSION,
|
||||
availableCount: String(availableCount),
|
||||
totalCount: String(totalCount),
|
||||
},
|
||||
options.versionHints,
|
||||
),
|
||||
...normalizedCapabilities,
|
||||
});
|
||||
}
|
||||
89
host/adapter/context.js
Normal file
89
host/adapter/context.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { getContext as extensionGetContext } from "../../../../extensions.js";
|
||||
|
||||
import { buildCapabilityStatus, mergeVersionHints } from "./capabilities.js";
|
||||
import { debugDebug } from "../../runtime/debug-logging.js";
|
||||
|
||||
function resolveContextGetter(providedGetter = null) {
|
||||
if (typeof providedGetter === "function") {
|
||||
return providedGetter;
|
||||
}
|
||||
|
||||
if (typeof extensionGetContext === "function") {
|
||||
return extensionGetContext;
|
||||
}
|
||||
|
||||
const globalGetter = globalThis?.SillyTavern?.getContext;
|
||||
return typeof globalGetter === "function" ? globalGetter : null;
|
||||
}
|
||||
|
||||
function detectContextMode(getContext) {
|
||||
if (typeof getContext !== "function") {
|
||||
return "unavailable";
|
||||
}
|
||||
|
||||
if (getContext === extensionGetContext) {
|
||||
return "extensions-api";
|
||||
}
|
||||
|
||||
return "global-api";
|
||||
}
|
||||
|
||||
export function createContextHostFacade(options = {}) {
|
||||
const getContext = resolveContextGetter(options.getContext);
|
||||
const available = typeof getContext === "function";
|
||||
const mode = detectContextMode(getContext);
|
||||
|
||||
return Object.freeze({
|
||||
available,
|
||||
mode,
|
||||
fallbackReason: available ? "" : "未检测到 getContext 宿主接口",
|
||||
versionHints: mergeVersionHints(
|
||||
{
|
||||
getter: "getContext",
|
||||
source: mode,
|
||||
sillyTavernGlobal:
|
||||
globalThis?.SillyTavern && typeof globalThis.SillyTavern === "object"
|
||||
? "available"
|
||||
: "missing",
|
||||
},
|
||||
options.versionHints,
|
||||
),
|
||||
getContext: (...args) => {
|
||||
if (!available) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return getContext(...args);
|
||||
} catch (error) {
|
||||
debugDebug(
|
||||
"[ST-BME] host-adapter/context getContext 调用失败",
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
readContextSnapshot: (...args) => {
|
||||
if (!available) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const context = getContext(...args);
|
||||
return context && typeof context === "object" ? context : null;
|
||||
} catch (error) {
|
||||
debugDebug("[ST-BME] host-adapter/context 读取上下文失败", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function inspectContextHostCapability(options = {}) {
|
||||
const facade = createContextHostFacade(options);
|
||||
return buildCapabilityStatus(facade);
|
||||
}
|
||||
|
||||
export function readHostContext(options = {}) {
|
||||
return createContextHostFacade(options).readContextSnapshot();
|
||||
}
|
||||
176
host/adapter/index.js
Normal file
176
host/adapter/index.js
Normal file
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
HOST_ADAPTER_VERSION,
|
||||
buildCapabilityCollectionSnapshot,
|
||||
buildCapabilityStatus,
|
||||
mergeVersionHints,
|
||||
} from "./capabilities.js";
|
||||
import { createContextHostFacade } from "./context.js";
|
||||
import { createInjectionHostFacade } from "./injection.js";
|
||||
import { createRegexHostFacade } from "./regex.js";
|
||||
import { createWorldbookHostFacade } from "./worldbook.js";
|
||||
|
||||
export const HOST_ADAPTER_STATE_SEMANTICS =
|
||||
"manual-refresh-diagnostic-snapshot";
|
||||
export const HOST_ADAPTER_REFRESH_MODE = "manual-rebuild";
|
||||
|
||||
let currentHostAdapter = null;
|
||||
let currentHostAdapterOptions = {};
|
||||
let currentHostAdapterRevision = 0;
|
||||
|
||||
function createSnapshotMetadata(options = {}) {
|
||||
const snapshotRevision = Number.isSafeInteger(options.snapshotRevision)
|
||||
? options.snapshotRevision
|
||||
: 0;
|
||||
const snapshotCreatedAt =
|
||||
typeof options.snapshotCreatedAt === "string" && options.snapshotCreatedAt
|
||||
? options.snapshotCreatedAt
|
||||
: new Date().toISOString();
|
||||
|
||||
return Object.freeze({
|
||||
stateSemantics: String(
|
||||
options.stateSemantics || HOST_ADAPTER_STATE_SEMANTICS,
|
||||
),
|
||||
refreshMode: String(options.refreshMode || HOST_ADAPTER_REFRESH_MODE),
|
||||
snapshotRevision,
|
||||
snapshotCreatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
function buildManagedCreateOptions(options = {}) {
|
||||
currentHostAdapterRevision += 1;
|
||||
|
||||
return {
|
||||
...options,
|
||||
stateSemantics: HOST_ADAPTER_STATE_SEMANTICS,
|
||||
refreshMode: HOST_ADAPTER_REFRESH_MODE,
|
||||
snapshotRevision: currentHostAdapterRevision,
|
||||
snapshotCreatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildHostCapabilitySnapshot(
|
||||
adapter,
|
||||
options = {},
|
||||
snapshotMetadata = createSnapshotMetadata(options),
|
||||
) {
|
||||
const snapshot = buildCapabilityCollectionSnapshot(
|
||||
{
|
||||
context: buildCapabilityStatus(adapter.context),
|
||||
worldbook: buildCapabilityStatus(adapter.worldbook),
|
||||
regex: buildCapabilityStatus(adapter.regex),
|
||||
injection: buildCapabilityStatus(adapter.injection),
|
||||
},
|
||||
{
|
||||
versionHints: mergeVersionHints(
|
||||
{
|
||||
adapter: HOST_ADAPTER_VERSION,
|
||||
scope: "st-bme-host-adapter",
|
||||
stateSemantics: snapshotMetadata.stateSemantics,
|
||||
refreshMode: snapshotMetadata.refreshMode,
|
||||
snapshotRevision: String(snapshotMetadata.snapshotRevision),
|
||||
snapshotCreatedAt: snapshotMetadata.snapshotCreatedAt,
|
||||
},
|
||||
options.versionHints,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
return Object.freeze({
|
||||
...snapshot,
|
||||
stateSemantics: snapshotMetadata.stateSemantics,
|
||||
refreshMode: snapshotMetadata.refreshMode,
|
||||
snapshotRevision: snapshotMetadata.snapshotRevision,
|
||||
snapshotCreatedAt: snapshotMetadata.snapshotCreatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
export function createHostAdapter(options = {}) {
|
||||
const context = createContextHostFacade(options);
|
||||
const sharedOptions = {
|
||||
...options,
|
||||
contextHost: context,
|
||||
};
|
||||
const worldbook = createWorldbookHostFacade(sharedOptions);
|
||||
const regex = createRegexHostFacade(sharedOptions);
|
||||
const injection = createInjectionHostFacade(sharedOptions);
|
||||
const adapter = {
|
||||
context,
|
||||
worldbook,
|
||||
regex,
|
||||
injection,
|
||||
};
|
||||
const snapshotMetadata = createSnapshotMetadata(sharedOptions);
|
||||
const snapshot = buildHostCapabilitySnapshot(
|
||||
adapter,
|
||||
sharedOptions,
|
||||
snapshotMetadata,
|
||||
);
|
||||
|
||||
return Object.freeze({
|
||||
...snapshot,
|
||||
...adapter,
|
||||
getSnapshot() {
|
||||
return snapshot;
|
||||
},
|
||||
readStateMetadata() {
|
||||
return snapshotMetadata;
|
||||
},
|
||||
refresh(options = {}) {
|
||||
return refreshHostAdapter(options);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function initializeHostAdapter(options = {}) {
|
||||
currentHostAdapterOptions = { ...options };
|
||||
currentHostAdapter = createHostAdapter(
|
||||
buildManagedCreateOptions(currentHostAdapterOptions),
|
||||
);
|
||||
return currentHostAdapter;
|
||||
}
|
||||
|
||||
export function refreshHostAdapter(options = {}) {
|
||||
currentHostAdapterOptions = {
|
||||
...currentHostAdapterOptions,
|
||||
...options,
|
||||
};
|
||||
currentHostAdapter = createHostAdapter(
|
||||
buildManagedCreateOptions(currentHostAdapterOptions),
|
||||
);
|
||||
return currentHostAdapter;
|
||||
}
|
||||
|
||||
export function getHostAdapter() {
|
||||
if (!currentHostAdapter) {
|
||||
currentHostAdapter = createHostAdapter(
|
||||
buildManagedCreateOptions(currentHostAdapterOptions),
|
||||
);
|
||||
}
|
||||
return currentHostAdapter;
|
||||
}
|
||||
|
||||
export function getHostCapabilitySnapshot() {
|
||||
return getHostAdapter().getSnapshot();
|
||||
}
|
||||
|
||||
export function refreshHostCapabilitySnapshot(options = {}) {
|
||||
return refreshHostAdapter(options).getSnapshot();
|
||||
}
|
||||
|
||||
export function readHostCapability(name, options = {}) {
|
||||
const normalizedName = String(name || "").trim();
|
||||
if (!normalizedName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const refreshOptions =
|
||||
options && typeof options === "object" ? { ...options } : {};
|
||||
const shouldRefresh = refreshOptions.refresh === true;
|
||||
delete refreshOptions.refresh;
|
||||
|
||||
const snapshot = shouldRefresh
|
||||
? refreshHostCapabilitySnapshot(refreshOptions)
|
||||
: getHostCapabilitySnapshot();
|
||||
|
||||
return snapshot?.[normalizedName] || null;
|
||||
}
|
||||
103
host/adapter/injection.js
Normal file
103
host/adapter/injection.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { buildCapabilityStatus, mergeVersionHints } from "./capabilities.js";
|
||||
import { createContextHostFacade } from "./context.js";
|
||||
import { debugDebug } from "../../runtime/debug-logging.js";
|
||||
|
||||
function resolvePromptSetter(providedSetter = null, contextHost = null) {
|
||||
if (typeof providedSetter === "function") {
|
||||
return {
|
||||
setter: providedSetter,
|
||||
source: "provided",
|
||||
};
|
||||
}
|
||||
|
||||
const context = contextHost?.readContextSnapshot?.();
|
||||
if (typeof context?.setExtensionPrompt === "function") {
|
||||
return {
|
||||
setter: context.setExtensionPrompt.bind(context),
|
||||
source: "context",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
setter: null,
|
||||
source: contextHost?.available ? "context-missing-setter" : "unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
function detectInjectionMode(setterRecord, contextHost) {
|
||||
if (typeof setterRecord?.setter === "function") {
|
||||
return setterRecord.source === "provided"
|
||||
? "provided-setter"
|
||||
: "context-extension-prompt";
|
||||
}
|
||||
|
||||
if (contextHost?.available) {
|
||||
return "context-without-extension-prompt";
|
||||
}
|
||||
|
||||
return "unavailable";
|
||||
}
|
||||
|
||||
export function createInjectionHostFacade(options = {}) {
|
||||
const contextHost = options.contextHost || createContextHostFacade(options);
|
||||
const setterRecord = resolvePromptSetter(
|
||||
options.setExtensionPrompt,
|
||||
contextHost,
|
||||
);
|
||||
const available = typeof setterRecord.setter === "function";
|
||||
const mode = detectInjectionMode(setterRecord, contextHost);
|
||||
|
||||
return Object.freeze({
|
||||
available,
|
||||
mode,
|
||||
fallbackReason: available
|
||||
? ""
|
||||
: contextHost?.available
|
||||
? "当前上下文未暴露 setExtensionPrompt 接口"
|
||||
: "未检测到可用注入宿主接口",
|
||||
versionHints: mergeVersionHints(
|
||||
{
|
||||
setter: "setExtensionPrompt",
|
||||
source: setterRecord.source,
|
||||
contextMode: contextHost?.mode || "unknown",
|
||||
},
|
||||
options.versionHints,
|
||||
),
|
||||
setExtensionPrompt: (...args) => {
|
||||
const liveSetterRecord = resolvePromptSetter(
|
||||
options.setExtensionPrompt,
|
||||
contextHost,
|
||||
);
|
||||
if (typeof liveSetterRecord.setter !== "function") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
liveSetterRecord.setter(...args);
|
||||
return true;
|
||||
} catch (error) {
|
||||
debugDebug(
|
||||
"[ST-BME] host-adapter/injection setExtensionPrompt 调用失败",
|
||||
error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
readInjectionSupport: () => {
|
||||
const liveSetterRecord = resolvePromptSetter(
|
||||
options.setExtensionPrompt,
|
||||
contextHost,
|
||||
);
|
||||
return Object.freeze({
|
||||
available: typeof liveSetterRecord.setter === "function",
|
||||
mode: detectInjectionMode(liveSetterRecord, contextHost),
|
||||
source: liveSetterRecord.source,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function inspectInjectionHostCapability(options = {}) {
|
||||
const facade = createInjectionHostFacade(options);
|
||||
return buildCapabilityStatus(facade);
|
||||
}
|
||||
308
host/adapter/regex.js
Normal file
308
host/adapter/regex.js
Normal file
@@ -0,0 +1,308 @@
|
||||
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",
|
||||
];
|
||||
|
||||
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 buildApiMap(container = null) {
|
||||
return REGEX_API_NAMES.reduce((result, name) => {
|
||||
result[name] = bindHostFunction(container, name);
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function countResolvedApis(apiMap = {}) {
|
||||
return Object.values(apiMap).filter((api) => typeof api === "function")
|
||||
.length;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return Object.freeze({
|
||||
label,
|
||||
sourceKind,
|
||||
fallback,
|
||||
apiMap,
|
||||
apiCount: countResolvedApis(apiMap),
|
||||
});
|
||||
}
|
||||
|
||||
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 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 resolveRegexSource(options = {}, contextHost = null) {
|
||||
const records = [
|
||||
...collectExplicitRegexSourceRecords(options),
|
||||
...collectContextRegexSourceRecords(contextHost, options),
|
||||
...collectGlobalFallbackRecords(),
|
||||
];
|
||||
|
||||
return (
|
||||
records.find(
|
||||
(record) =>
|
||||
typeof record.apiMap.getTavernRegexes === "function" ||
|
||||
typeof record.apiMap.formatAsTavernRegexedString === "function",
|
||||
) ||
|
||||
buildSourceRecord({
|
||||
label: "none",
|
||||
sourceKind: "unavailable",
|
||||
container: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function detectRegexMode(apiMap = {}) {
|
||||
const hasGetter = typeof apiMap.getTavernRegexes === "function";
|
||||
const hasFormatter =
|
||||
typeof apiMap.formatAsTavernRegexedString === "function";
|
||||
|
||||
if (!hasGetter && !hasFormatter) {
|
||||
return "unavailable";
|
||||
}
|
||||
|
||||
if (hasGetter && hasFormatter) {
|
||||
return typeof apiMap.isCharacterTavernRegexesEnabled === "function"
|
||||
? "full"
|
||||
: "partial";
|
||||
}
|
||||
|
||||
return hasFormatter ? "formatter-only" : "getter-only";
|
||||
}
|
||||
|
||||
function buildFallbackReason(sourceRecord, available, mode) {
|
||||
if (!available) {
|
||||
return "未检测到 Tavern Regex 宿主接口";
|
||||
}
|
||||
|
||||
if (sourceRecord?.fallback && mode === "partial") {
|
||||
return `当前通过 ${sourceRecord.label} fallback 提供部分 Tavern Regex 能力`;
|
||||
}
|
||||
|
||||
if (sourceRecord?.fallback) {
|
||||
return `当前通过 ${sourceRecord.label} fallback 提供 Tavern Regex 能力`;
|
||||
}
|
||||
|
||||
if (mode === "partial") {
|
||||
return `Tavern Regex 桥接仅发现部分接口,来源: ${sourceRecord?.label || "unknown"}`;
|
||||
}
|
||||
|
||||
if (mode === "formatter-only") {
|
||||
return `Tavern Regex 桥接仅发现 formatter 接口,来源: ${sourceRecord?.label || "unknown"}`;
|
||||
}
|
||||
|
||||
if (mode === "getter-only") {
|
||||
return `Tavern Regex 桥接仅发现规则读取接口,来源: ${sourceRecord?.label || "unknown"}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function createRegexHostFacade(options = {}) {
|
||||
const contextHost = options.contextHost || createContextHostFacade(options);
|
||||
const sourceRecord = resolveRegexSource(options, contextHost);
|
||||
const mode = detectRegexMode(sourceRecord.apiMap);
|
||||
const available = mode !== "unavailable";
|
||||
|
||||
return Object.freeze({
|
||||
available,
|
||||
mode,
|
||||
fallbackReason: buildFallbackReason(sourceRecord, available, mode),
|
||||
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",
|
||||
},
|
||||
options.versionHints,
|
||||
),
|
||||
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:
|
||||
typeof sourceRecord.apiMap.formatAsTavernRegexedString === "function",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function inspectRegexHostCapability(options = {}) {
|
||||
const facade = createRegexHostFacade(options);
|
||||
return buildCapabilityStatus(facade);
|
||||
}
|
||||
277
host/adapter/worldbook.js
Normal file
277
host/adapter/worldbook.js
Normal file
@@ -0,0 +1,277 @@
|
||||
import { buildCapabilityStatus, mergeVersionHints } from "./capabilities.js";
|
||||
import { createContextHostFacade } from "./context.js";
|
||||
import { debugDebug } from "../../runtime/debug-logging.js";
|
||||
|
||||
const WORLDBOOK_API_NAMES = [
|
||||
"getWorldbook",
|
||||
"getLorebookEntries",
|
||||
"getCharWorldbookNames",
|
||||
];
|
||||
|
||||
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 buildApiMap(container = null) {
|
||||
return WORLDBOOK_API_NAMES.reduce((result, name) => {
|
||||
result[name] = bindHostFunction(container, name);
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function countResolvedApis(apiMap = {}) {
|
||||
return Object.values(apiMap).filter((api) => typeof api === "function")
|
||||
.length;
|
||||
}
|
||||
|
||||
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/worldbook provider 解析失败", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return isObjectLike(candidate) ? candidate : null;
|
||||
}
|
||||
|
||||
function buildSourceRecord({
|
||||
label = "unknown",
|
||||
sourceKind = "unknown",
|
||||
container = null,
|
||||
fallback = false,
|
||||
} = {}) {
|
||||
const apiMap = buildApiMap(container);
|
||||
|
||||
return Object.freeze({
|
||||
label,
|
||||
sourceKind,
|
||||
fallback,
|
||||
apiMap,
|
||||
apiCount: countResolvedApis(apiMap),
|
||||
});
|
||||
}
|
||||
|
||||
function collectExplicitWorldbookSourceRecords(options = {}) {
|
||||
const records = [];
|
||||
const providerCandidates = [
|
||||
["worldbookProvider", options.worldbookProvider],
|
||||
["providers.worldbook", options.providers?.worldbook],
|
||||
["provider.worldbook", options.provider?.worldbook],
|
||||
["host.worldbook", options.host?.worldbook],
|
||||
["host.providers.worldbook", options.host?.providers?.worldbook],
|
||||
];
|
||||
|
||||
for (const [label, candidate] of providerCandidates) {
|
||||
const container = resolveProviderCandidate(candidate, options);
|
||||
if (!container) continue;
|
||||
|
||||
records.push(
|
||||
buildSourceRecord({
|
||||
label,
|
||||
sourceKind: "provider",
|
||||
container,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const apiCandidates = [
|
||||
["worldbookApis", options.worldbookApis],
|
||||
["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 collectContextWorldbookSourceRecords(contextHost, options = {}) {
|
||||
const context = contextHost?.readContextSnapshot?.();
|
||||
if (!isObjectLike(context)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const records = [];
|
||||
const contextCandidates = [
|
||||
["context.worldbook", context.worldbook],
|
||||
["context.worldInfo", context.worldInfo],
|
||||
["context.host.worldbook", context.host?.worldbook],
|
||||
["context.hostAdapter.worldbook", context.hostAdapter?.worldbook],
|
||||
["context.providers.worldbook", context.providers?.worldbook],
|
||||
["context.extensions.worldbook", context.extensions?.worldbook],
|
||||
["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 resolveWorldbookSource(options = {}, contextHost = null) {
|
||||
const records = [
|
||||
...collectExplicitWorldbookSourceRecords(options),
|
||||
...collectContextWorldbookSourceRecords(contextHost, options),
|
||||
...collectGlobalFallbackRecords(),
|
||||
];
|
||||
|
||||
return (
|
||||
records.find((record) => record.apiCount > 0) ||
|
||||
buildSourceRecord({
|
||||
label: "none",
|
||||
sourceKind: "unavailable",
|
||||
container: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function detectWorldbookMode(apiMap = {}) {
|
||||
const availableCount = countResolvedApis(apiMap);
|
||||
|
||||
if (availableCount === 0) return "unavailable";
|
||||
if (availableCount === WORLDBOOK_API_NAMES.length) return "full";
|
||||
return "partial";
|
||||
}
|
||||
|
||||
function buildFallbackReason(sourceRecord, available, mode) {
|
||||
if (!available) {
|
||||
return "未检测到世界书宿主接口";
|
||||
}
|
||||
|
||||
if (sourceRecord?.fallback && mode === "partial") {
|
||||
return `当前通过 ${sourceRecord.label} fallback 提供部分世界书能力`;
|
||||
}
|
||||
|
||||
if (sourceRecord?.fallback) {
|
||||
return `当前通过 ${sourceRecord.label} fallback 提供世界书能力`;
|
||||
}
|
||||
|
||||
if (mode === "partial") {
|
||||
return `世界书桥接仅发现部分接口,来源: ${sourceRecord?.label || "unknown"}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function createWorldbookHostFacade(options = {}) {
|
||||
const contextHost = options.contextHost || createContextHostFacade(options);
|
||||
const sourceRecord = resolveWorldbookSource(options, contextHost);
|
||||
const mode = detectWorldbookMode(sourceRecord.apiMap);
|
||||
const available = mode !== "unavailable";
|
||||
|
||||
return Object.freeze({
|
||||
available,
|
||||
mode,
|
||||
fallbackReason: buildFallbackReason(sourceRecord, available, mode),
|
||||
versionHints: mergeVersionHints(
|
||||
{
|
||||
apiCount: String(sourceRecord.apiCount),
|
||||
apis: WORLDBOOK_API_NAMES.filter(
|
||||
(name) => typeof sourceRecord.apiMap[name] === "function",
|
||||
),
|
||||
source: sourceRecord.sourceKind,
|
||||
sourceLabel: sourceRecord.label,
|
||||
fallback: sourceRecord.fallback ? "yes" : "no",
|
||||
contextMode: contextHost?.mode || "unknown",
|
||||
},
|
||||
options.versionHints,
|
||||
),
|
||||
getWorldbook: sourceRecord.apiMap.getWorldbook,
|
||||
getLorebookEntries: sourceRecord.apiMap.getLorebookEntries,
|
||||
getCharWorldbookNames: sourceRecord.apiMap.getCharWorldbookNames,
|
||||
getApi(name) {
|
||||
return sourceRecord.apiMap[String(name || "")] || null;
|
||||
},
|
||||
readApiAvailability() {
|
||||
return Object.freeze(
|
||||
WORLDBOOK_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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function inspectWorldbookHostCapability(options = {}) {
|
||||
const facade = createWorldbookHostFacade(options);
|
||||
return buildCapabilityStatus(facade);
|
||||
}
|
||||
Reference in New Issue
Block a user