Files
openclaw/src/plugins/interactive-registry.ts
2026-04-04 01:07:28 +09:00

118 lines
3.4 KiB
TypeScript

import {
clearPluginInteractiveHandlersState,
getPluginInteractiveHandlersState,
type RegisteredInteractiveHandler,
} from "./interactive-state.js";
import type { PluginInteractiveHandlerRegistration } from "./types.js";
export type InteractiveRegistrationResult = {
ok: boolean;
error?: string;
};
function toRegistryKey(channel: string, namespace: string): string {
return `${channel.trim().toLowerCase()}:${namespace.trim()}`;
}
function normalizeNamespace(namespace: string): string {
return namespace.trim();
}
function validateNamespace(namespace: string): string | null {
if (!namespace.trim()) {
return "Interactive handler namespace cannot be empty";
}
if (!/^[A-Za-z0-9._-]+$/.test(namespace.trim())) {
return "Interactive handler namespace must contain only letters, numbers, dots, underscores, and hyphens";
}
return null;
}
export function resolvePluginInteractiveNamespaceMatch(
channel: string,
data: string,
): { registration: RegisteredInteractiveHandler; namespace: string; payload: string } | null {
const interactiveHandlers = getPluginInteractiveHandlersState();
const trimmedData = data.trim();
if (!trimmedData) {
return null;
}
const separatorIndex = trimmedData.indexOf(":");
const namespace =
separatorIndex >= 0 ? trimmedData.slice(0, separatorIndex) : normalizeNamespace(trimmedData);
const registration = interactiveHandlers.get(toRegistryKey(channel, namespace));
if (!registration) {
return null;
}
return {
registration,
namespace,
payload: separatorIndex >= 0 ? trimmedData.slice(separatorIndex + 1) : "",
};
}
export function registerPluginInteractiveHandler(
pluginId: string,
registration: PluginInteractiveHandlerRegistration,
opts?: { pluginName?: string; pluginRoot?: string },
): InteractiveRegistrationResult {
const interactiveHandlers = getPluginInteractiveHandlersState();
const namespace = normalizeNamespace(registration.namespace);
const validationError = validateNamespace(namespace);
if (validationError) {
return { ok: false, error: validationError };
}
const key = toRegistryKey(registration.channel, namespace);
const existing = interactiveHandlers.get(key);
if (existing) {
return {
ok: false,
error: `Interactive handler namespace "${namespace}" already registered by plugin "${existing.pluginId}"`,
};
}
if (registration.channel === "telegram") {
interactiveHandlers.set(key, {
...registration,
namespace,
channel: "telegram",
pluginId,
pluginName: opts?.pluginName,
pluginRoot: opts?.pluginRoot,
});
} else if (registration.channel === "slack") {
interactiveHandlers.set(key, {
...registration,
namespace,
channel: "slack",
pluginId,
pluginName: opts?.pluginName,
pluginRoot: opts?.pluginRoot,
});
} else {
interactiveHandlers.set(key, {
...registration,
namespace,
channel: "discord",
pluginId,
pluginName: opts?.pluginName,
pluginRoot: opts?.pluginRoot,
});
}
return { ok: true };
}
export function clearPluginInteractiveHandlers(): void {
clearPluginInteractiveHandlersState();
}
export function clearPluginInteractiveHandlersForPlugin(pluginId: string): void {
const interactiveHandlers = getPluginInteractiveHandlersState();
for (const [key, value] of interactiveHandlers.entries()) {
if (value.pluginId === pluginId) {
interactiveHandlers.delete(key);
}
}
}