mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
277 lines
7.2 KiB
JavaScript
277 lines
7.2 KiB
JavaScript
import { buildCapabilityStatus, mergeVersionHints } from "./capabilities.js";
|
|
import { createContextHostFacade } from "./context.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) {
|
|
console.debug("[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);
|
|
}
|