Reorganize modules into layered directories

This commit is contained in:
Youzini-afk
2026-04-08 01:17:47 +08:00
parent 59942541ea
commit feec17f3e3
90 changed files with 284 additions and 219 deletions

View 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
View 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
View 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
View 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
View 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
View 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);
}