openclaw/src/channels/plugins/configured-binding-compiler.ts

215 lines
6.0 KiB
TypeScript

import { listConfiguredBindings } from "../../config/bindings.js";
import type { OpenClawConfig } from "../../config/config.js";
import { getActivePluginChannelRegistryVersion } from "../../plugins/runtime.js";
import { pickFirstExistingAgentId } from "../../routing/resolve-route.js";
import { resolveChannelConfiguredBindingProvider } from "./binding-provider.js";
import type { CompiledConfiguredBinding, ConfiguredBindingChannel } from "./binding-types.js";
import { resolveConfiguredBindingConsumer } from "./configured-binding-consumers.js";
import { getChannelPlugin } from "./index.js";
import type {
ChannelConfiguredBindingConversationRef,
ChannelConfiguredBindingProvider,
} from "./types.adapters.js";
// Configured bindings are channel-owned rules compiled from config, separate
// from runtime plugin-owned conversation bindings.
export type CompiledConfiguredBindingRegistry = {
rulesByChannel: Map<ConfiguredBindingChannel, CompiledConfiguredBinding[]>;
};
type CachedCompiledConfiguredBindingRegistry = {
registryVersion: number;
registry: CompiledConfiguredBindingRegistry;
};
const compiledRegistryCache = new WeakMap<
OpenClawConfig,
CachedCompiledConfiguredBindingRegistry
>();
function resolveLoadedChannelPlugin(channel: string) {
const normalized = channel.trim().toLowerCase();
if (!normalized) {
return undefined;
}
return getChannelPlugin(normalized as ConfiguredBindingChannel);
}
function resolveConfiguredBindingAdapter(channel: string): {
channel: ConfiguredBindingChannel;
provider: ChannelConfiguredBindingProvider;
} | null {
const normalized = channel.trim().toLowerCase();
if (!normalized) {
return null;
}
const plugin = resolveLoadedChannelPlugin(normalized);
const provider = resolveChannelConfiguredBindingProvider(plugin);
if (
!plugin ||
!provider ||
!provider.compileConfiguredBinding ||
!provider.matchInboundConversation
) {
return null;
}
return {
channel: plugin.id,
provider,
};
}
function resolveBindingConversationId(binding: {
match?: { peer?: { id?: string } };
}): string | null {
const id = binding.match?.peer?.id?.trim();
return id ? id : null;
}
function compileConfiguredBindingTarget(params: {
provider: ChannelConfiguredBindingProvider;
binding: CompiledConfiguredBinding["binding"];
conversationId: string;
}): ChannelConfiguredBindingConversationRef | null {
return params.provider.compileConfiguredBinding({
binding: params.binding,
conversationId: params.conversationId,
});
}
function compileConfiguredBindingRule(params: {
cfg: OpenClawConfig;
channel: ConfiguredBindingChannel;
binding: CompiledConfiguredBinding["binding"];
target: ChannelConfiguredBindingConversationRef;
bindingConversationId: string;
provider: ChannelConfiguredBindingProvider;
}): CompiledConfiguredBinding | null {
const agentId = pickFirstExistingAgentId(params.cfg, params.binding.agentId ?? "main");
const consumer = resolveConfiguredBindingConsumer(params.binding);
if (!consumer) {
return null;
}
const targetFactory = consumer.buildTargetFactory({
cfg: params.cfg,
binding: params.binding,
channel: params.channel,
agentId,
target: params.target,
bindingConversationId: params.bindingConversationId,
});
if (!targetFactory) {
return null;
}
return {
channel: params.channel,
accountPattern: params.binding.match.accountId?.trim() || undefined,
binding: params.binding,
bindingConversationId: params.bindingConversationId,
target: params.target,
agentId,
provider: params.provider,
targetFactory,
};
}
function pushCompiledRule(
target: Map<ConfiguredBindingChannel, CompiledConfiguredBinding[]>,
rule: CompiledConfiguredBinding,
) {
const existing = target.get(rule.channel);
if (existing) {
existing.push(rule);
return;
}
target.set(rule.channel, [rule]);
}
function compileConfiguredBindingRegistry(params: {
cfg: OpenClawConfig;
}): CompiledConfiguredBindingRegistry {
const rulesByChannel = new Map<ConfiguredBindingChannel, CompiledConfiguredBinding[]>();
for (const binding of listConfiguredBindings(params.cfg)) {
const bindingConversationId = resolveBindingConversationId(binding);
if (!bindingConversationId) {
continue;
}
const resolvedChannel = resolveConfiguredBindingAdapter(binding.match.channel);
if (!resolvedChannel) {
continue;
}
const target = compileConfiguredBindingTarget({
provider: resolvedChannel.provider,
binding,
conversationId: bindingConversationId,
});
if (!target) {
continue;
}
const rule = compileConfiguredBindingRule({
cfg: params.cfg,
channel: resolvedChannel.channel,
binding,
target,
bindingConversationId,
provider: resolvedChannel.provider,
});
if (!rule) {
continue;
}
pushCompiledRule(rulesByChannel, rule);
}
return {
rulesByChannel,
};
}
export function resolveCompiledBindingRegistry(
cfg: OpenClawConfig,
): CompiledConfiguredBindingRegistry {
const registryVersion = getActivePluginChannelRegistryVersion();
const cached = compiledRegistryCache.get(cfg);
if (cached?.registryVersion === registryVersion) {
return cached.registry;
}
const registry = compileConfiguredBindingRegistry({
cfg,
});
compiledRegistryCache.set(cfg, {
registryVersion,
registry,
});
return registry;
}
export function primeCompiledBindingRegistry(
cfg: OpenClawConfig,
): CompiledConfiguredBindingRegistry {
const registry = compileConfiguredBindingRegistry({ cfg });
compiledRegistryCache.set(cfg, {
registryVersion: getActivePluginChannelRegistryVersion(),
registry,
});
return registry;
}
export function countCompiledBindingRegistry(registry: CompiledConfiguredBindingRegistry): {
bindingCount: number;
channelCount: number;
} {
return {
bindingCount: [...registry.rulesByChannel.values()].reduce(
(sum, rules) => sum + rules.length,
0,
),
channelCount: registry.rulesByChannel.size,
};
}