Files
openclaw/src/plugins/interactive-registry.ts
Mark 5db7c3797d Fix Telegram plugin callback routing (#97174)
* fix telegram plugin callback routing

* Add Telegram callback proof coverage

* Fix proof script lint

* Fix Telegram proof CI regressions

* test: fix telegram callback proof CI regressions

* fix: retire registry-owned callbacks

* fix: retire registry-owned callbacks

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-01 19:49:54 +01:00

150 lines
5.3 KiB
TypeScript

// Maintains interactive plugin registry entries discovered from manifests.
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import {
normalizePluginInteractiveNamespace,
resolvePluginInteractiveMatch,
toPluginInteractiveRegistryKey,
validatePluginInteractiveNamespace,
} from "./interactive-shared.js";
import {
clearPluginInteractiveHandlerRegistrationsState,
clearPluginInteractiveHandlersState,
getPluginInteractiveHandlersState,
type RegisteredInteractiveHandler,
} from "./interactive-state.js";
import type { PluginInteractiveHandlerRegistration } from "./types.js";
/** Registration result for plugin interactive namespace handlers. */
export type InteractiveRegistrationResult = {
ok: boolean;
error?: string;
};
/** Resolves a channel payload to a registered plugin interactive namespace handler. */
export function resolvePluginInteractiveNamespaceMatch(
channel: string,
data: string,
): { registration: RegisteredInteractiveHandler; namespace: string; payload: string } | null {
return resolvePluginInteractiveMatch({
interactiveHandlers: getPluginInteractiveHandlersState(),
channel,
data,
});
}
/** Resolves a handler from registry-owned registrations without changing global state. */
export function resolvePluginInteractiveRegistrationsMatch(
registrations: readonly RegisteredInteractiveHandler[],
channel: string,
data: string,
): { registration: RegisteredInteractiveHandler; namespace: string; payload: string } | null {
return resolvePluginInteractiveMatch({
interactiveHandlers: {
get: (key) =>
registrations.find(
(registration) =>
toPluginInteractiveRegistryKey(registration.channel, registration.namespace) === key,
),
},
channel,
data,
});
}
/** Registers one plugin interactive namespace for a channel. */
function registerPluginInteractiveHandlerWithOptions(
pluginId: string,
registration: PluginInteractiveHandlerRegistration,
opts?: { pluginName?: string; pluginRoot?: string; registryOwned?: true },
): InteractiveRegistrationResult {
const interactiveHandlers = getPluginInteractiveHandlersState();
const namespace = normalizePluginInteractiveNamespace(registration.namespace);
const validationError = validatePluginInteractiveNamespace(namespace);
if (validationError) {
return { ok: false, error: validationError };
}
const key = toPluginInteractiveRegistryKey(registration.channel, namespace);
const existing = interactiveHandlers.get(key);
if (existing) {
return {
ok: false,
error: `Interactive handler namespace "${namespace}" already registered by plugin "${existing.pluginId}"`,
};
}
interactiveHandlers.set(key, {
...registration,
namespace,
channel: normalizeOptionalLowercaseString(registration.channel) ?? "",
pluginId,
pluginName: opts?.pluginName,
pluginRoot: opts?.pluginRoot,
registryOwned: opts?.registryOwned,
});
return { ok: true };
}
/** Registers one process-global interactive handler. */
export function registerPluginInteractiveHandler(
pluginId: string,
registration: PluginInteractiveHandlerRegistration,
opts?: { pluginName?: string; pluginRoot?: string },
): InteractiveRegistrationResult {
return registerPluginInteractiveHandlerWithOptions(pluginId, registration, opts);
}
/** Registers one handler whose lifetime follows its owning plugin registry. */
export function registerRegistryPluginInteractiveHandler(
pluginId: string,
registration: PluginInteractiveHandlerRegistration,
opts?: { pluginName?: string; pluginRoot?: string },
): InteractiveRegistrationResult {
return registerPluginInteractiveHandlerWithOptions(pluginId, registration, {
...opts,
registryOwned: true,
});
}
/** Clears all active plugin interactive handlers. */
export function clearPluginInteractiveHandlers(): void {
clearPluginInteractiveHandlersState();
}
/** Clears stored plugin interactive handler registrations. */
export function clearPluginInteractiveHandlerRegistrations(): void {
clearPluginInteractiveHandlerRegistrationsState();
}
/** Clears active interactive handlers owned by one plugin. */
export function clearPluginInteractiveHandlersForPlugin(pluginId: string): void {
const interactiveHandlers = getPluginInteractiveHandlersState();
for (const [key, value] of interactiveHandlers.entries()) {
if (value.pluginId === pluginId) {
interactiveHandlers.delete(key);
}
}
}
/** Lists active plugin interactive handlers. */
export function listPluginInteractiveHandlers(): RegisteredInteractiveHandler[] {
return Array.from(getPluginInteractiveHandlersState().values());
}
/** Restores active plugin interactive handlers from a saved registry snapshot. */
export function restorePluginInteractiveHandlers(
registrations: readonly RegisteredInteractiveHandler[],
): void {
clearPluginInteractiveHandlerRegistrations();
const interactiveHandlers = getPluginInteractiveHandlersState();
for (const registration of registrations) {
const namespace = normalizePluginInteractiveNamespace(registration.namespace);
if (!namespace) {
continue;
}
interactiveHandlers.set(toPluginInteractiveRegistryKey(registration.channel, namespace), {
...registration,
namespace,
channel: normalizeOptionalLowercaseString(registration.channel) ?? "",
});
}
}