mirror of https://github.com/openclaw/openclaw.git
144 lines
4.6 KiB
TypeScript
144 lines
4.6 KiB
TypeScript
import type { OpenClawConfig } from "../config/config.js";
|
|
import { defaultSlotIdForKey } from "../plugins/slots.js";
|
|
import type { ContextEngine } from "./types.js";
|
|
|
|
/**
|
|
* A factory that creates a ContextEngine instance.
|
|
* Supports async creation for engines that need DB connections etc.
|
|
*/
|
|
export type ContextEngineFactory = () => ContextEngine | Promise<ContextEngine>;
|
|
export type ContextEngineRegistrationResult = { ok: true } | { ok: false; existingOwner: string };
|
|
|
|
type RegisterContextEngineForOwnerOptions = {
|
|
allowSameOwnerRefresh?: boolean;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Registry (module-level singleton)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const CONTEXT_ENGINE_REGISTRY_STATE = Symbol.for("openclaw.contextEngineRegistryState");
|
|
const CORE_CONTEXT_ENGINE_OWNER = "core";
|
|
const PUBLIC_CONTEXT_ENGINE_OWNER = "public-sdk";
|
|
|
|
type ContextEngineRegistryState = {
|
|
engines: Map<
|
|
string,
|
|
{
|
|
factory: ContextEngineFactory;
|
|
owner: string;
|
|
}
|
|
>;
|
|
};
|
|
|
|
// Keep context-engine registrations process-global so duplicated dist chunks
|
|
// still share one registry map at runtime.
|
|
function getContextEngineRegistryState(): ContextEngineRegistryState {
|
|
const globalState = globalThis as typeof globalThis & {
|
|
[CONTEXT_ENGINE_REGISTRY_STATE]?: ContextEngineRegistryState;
|
|
};
|
|
if (!globalState[CONTEXT_ENGINE_REGISTRY_STATE]) {
|
|
globalState[CONTEXT_ENGINE_REGISTRY_STATE] = {
|
|
engines: new Map(),
|
|
};
|
|
}
|
|
return globalState[CONTEXT_ENGINE_REGISTRY_STATE];
|
|
}
|
|
|
|
function requireContextEngineOwner(owner: string): string {
|
|
const normalizedOwner = owner.trim();
|
|
if (!normalizedOwner) {
|
|
throw new Error(
|
|
`registerContextEngineForOwner: owner must be a non-empty string, got ${JSON.stringify(owner)}`,
|
|
);
|
|
}
|
|
return normalizedOwner;
|
|
}
|
|
|
|
/**
|
|
* Register a context engine implementation under an explicit trusted owner.
|
|
*/
|
|
export function registerContextEngineForOwner(
|
|
id: string,
|
|
factory: ContextEngineFactory,
|
|
owner: string,
|
|
opts?: RegisterContextEngineForOwnerOptions,
|
|
): ContextEngineRegistrationResult {
|
|
const normalizedOwner = requireContextEngineOwner(owner);
|
|
const registry = getContextEngineRegistryState().engines;
|
|
const existing = registry.get(id);
|
|
if (
|
|
id === defaultSlotIdForKey("contextEngine") &&
|
|
normalizedOwner !== CORE_CONTEXT_ENGINE_OWNER
|
|
) {
|
|
return { ok: false, existingOwner: CORE_CONTEXT_ENGINE_OWNER };
|
|
}
|
|
if (existing && existing.owner !== normalizedOwner) {
|
|
return { ok: false, existingOwner: existing.owner };
|
|
}
|
|
if (existing && opts?.allowSameOwnerRefresh !== true) {
|
|
return { ok: false, existingOwner: existing.owner };
|
|
}
|
|
registry.set(id, { factory, owner: normalizedOwner });
|
|
return { ok: true };
|
|
}
|
|
|
|
/**
|
|
* Public SDK entry point for third-party registrations.
|
|
*
|
|
* This path is intentionally unprivileged: it cannot claim core-owned ids and
|
|
* it cannot safely refresh an existing registration because the caller's
|
|
* identity is not authenticated.
|
|
*/
|
|
export function registerContextEngine(
|
|
id: string,
|
|
factory: ContextEngineFactory,
|
|
): ContextEngineRegistrationResult {
|
|
return registerContextEngineForOwner(id, factory, PUBLIC_CONTEXT_ENGINE_OWNER);
|
|
}
|
|
|
|
/**
|
|
* Return the factory for a registered engine, or undefined.
|
|
*/
|
|
export function getContextEngineFactory(id: string): ContextEngineFactory | undefined {
|
|
return getContextEngineRegistryState().engines.get(id)?.factory;
|
|
}
|
|
|
|
/**
|
|
* List all registered engine ids.
|
|
*/
|
|
export function listContextEngineIds(): string[] {
|
|
return [...getContextEngineRegistryState().engines.keys()];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Resolve which ContextEngine to use based on plugin slot configuration.
|
|
*
|
|
* Resolution order:
|
|
* 1. `config.plugins.slots.contextEngine` (explicit slot override)
|
|
* 2. Default slot value ("legacy")
|
|
*
|
|
* Throws if the resolved engine id has no registered factory.
|
|
*/
|
|
export async function resolveContextEngine(config?: OpenClawConfig): Promise<ContextEngine> {
|
|
const slotValue = config?.plugins?.slots?.contextEngine;
|
|
const engineId =
|
|
typeof slotValue === "string" && slotValue.trim()
|
|
? slotValue.trim()
|
|
: defaultSlotIdForKey("contextEngine");
|
|
|
|
const entry = getContextEngineRegistryState().engines.get(engineId);
|
|
if (!entry) {
|
|
throw new Error(
|
|
`Context engine "${engineId}" is not registered. ` +
|
|
`Available engines: ${listContextEngineIds().join(", ") || "(none)"}`,
|
|
);
|
|
}
|
|
|
|
return entry.factory();
|
|
}
|