openclaw/src/acp/persistent-bindings.resolve.ts

339 lines
10 KiB
TypeScript

import { listAcpBindings } from "../config/bindings.js";
import type { OpenClawConfig } from "../config/config.js";
import type { AgentAcpBinding } from "../config/types.js";
import { pickFirstExistingAgentId } from "../routing/resolve-route.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
parseAgentSessionKey,
} from "../routing/session-key.js";
import { parseTelegramTopicConversation } from "./conversation-id.js";
import {
buildConfiguredAcpSessionKey,
normalizeBindingConfig,
normalizeMode,
normalizeText,
toConfiguredAcpBindingRecord,
type ConfiguredAcpBindingChannel,
type ConfiguredAcpBindingSpec,
type ResolvedConfiguredAcpBinding,
} from "./persistent-bindings.types.js";
function normalizeBindingChannel(value: string | undefined): ConfiguredAcpBindingChannel | null {
const normalized = (value ?? "").trim().toLowerCase();
if (normalized === "discord" || normalized === "telegram") {
return normalized;
}
return null;
}
function resolveAccountMatchPriority(match: string | undefined, actual: string): 0 | 1 | 2 {
const trimmed = (match ?? "").trim();
if (!trimmed) {
return actual === DEFAULT_ACCOUNT_ID ? 2 : 0;
}
if (trimmed === "*") {
return 1;
}
return normalizeAccountId(trimmed) === actual ? 2 : 0;
}
function resolveBindingConversationId(binding: AgentAcpBinding): string | null {
const id = binding.match.peer?.id?.trim();
return id ? id : null;
}
function parseConfiguredBindingSessionKey(params: {
sessionKey: string;
}): { channel: ConfiguredAcpBindingChannel; accountId: string } | null {
const parsed = parseAgentSessionKey(params.sessionKey);
const rest = parsed?.rest?.trim().toLowerCase() ?? "";
if (!rest) {
return null;
}
const tokens = rest.split(":");
if (tokens.length !== 5 || tokens[0] !== "acp" || tokens[1] !== "binding") {
return null;
}
const channel = normalizeBindingChannel(tokens[2]);
if (!channel) {
return null;
}
const accountId = normalizeAccountId(tokens[3]);
return {
channel,
accountId,
};
}
function resolveAgentRuntimeAcpDefaults(params: { cfg: OpenClawConfig; ownerAgentId: string }): {
acpAgentId?: string;
mode?: string;
cwd?: string;
backend?: string;
} {
const agent = params.cfg.agents?.list?.find(
(entry) => entry.id?.trim().toLowerCase() === params.ownerAgentId.toLowerCase(),
);
if (!agent || agent.runtime?.type !== "acp") {
return {};
}
return {
acpAgentId: normalizeText(agent.runtime.acp?.agent),
mode: normalizeText(agent.runtime.acp?.mode),
cwd: normalizeText(agent.runtime.acp?.cwd),
backend: normalizeText(agent.runtime.acp?.backend),
};
}
function toConfiguredBindingSpec(params: {
cfg: OpenClawConfig;
channel: ConfiguredAcpBindingChannel;
accountId: string;
conversationId: string;
parentConversationId?: string;
binding: AgentAcpBinding;
}): ConfiguredAcpBindingSpec {
const accountId = normalizeAccountId(params.accountId);
const agentId = pickFirstExistingAgentId(params.cfg, params.binding.agentId ?? "main");
const runtimeDefaults = resolveAgentRuntimeAcpDefaults({
cfg: params.cfg,
ownerAgentId: agentId,
});
const bindingOverrides = normalizeBindingConfig(params.binding.acp);
const acpAgentId = normalizeText(runtimeDefaults.acpAgentId);
const mode = normalizeMode(bindingOverrides.mode ?? runtimeDefaults.mode);
return {
channel: params.channel,
accountId,
conversationId: params.conversationId,
parentConversationId: params.parentConversationId,
agentId,
acpAgentId,
mode,
cwd: bindingOverrides.cwd ?? runtimeDefaults.cwd,
backend: bindingOverrides.backend ?? runtimeDefaults.backend,
label: bindingOverrides.label,
};
}
function resolveConfiguredBindingRecord(params: {
cfg: OpenClawConfig;
bindings: AgentAcpBinding[];
channel: ConfiguredAcpBindingChannel;
accountId: string;
selectConversation: (
binding: AgentAcpBinding,
) => { conversationId: string; parentConversationId?: string } | null;
}): ResolvedConfiguredAcpBinding | null {
let wildcardMatch: {
binding: AgentAcpBinding;
conversationId: string;
parentConversationId?: string;
} | null = null;
for (const binding of params.bindings) {
if (normalizeBindingChannel(binding.match.channel) !== params.channel) {
continue;
}
const accountMatchPriority = resolveAccountMatchPriority(
binding.match.accountId,
params.accountId,
);
if (accountMatchPriority === 0) {
continue;
}
const conversation = params.selectConversation(binding);
if (!conversation) {
continue;
}
const spec = toConfiguredBindingSpec({
cfg: params.cfg,
channel: params.channel,
accountId: params.accountId,
conversationId: conversation.conversationId,
parentConversationId: conversation.parentConversationId,
binding,
});
if (accountMatchPriority === 2) {
return {
spec,
record: toConfiguredAcpBindingRecord(spec),
};
}
if (!wildcardMatch) {
wildcardMatch = { binding, ...conversation };
}
}
if (!wildcardMatch) {
return null;
}
const spec = toConfiguredBindingSpec({
cfg: params.cfg,
channel: params.channel,
accountId: params.accountId,
conversationId: wildcardMatch.conversationId,
parentConversationId: wildcardMatch.parentConversationId,
binding: wildcardMatch.binding,
});
return {
spec,
record: toConfiguredAcpBindingRecord(spec),
};
}
export function resolveConfiguredAcpBindingSpecBySessionKey(params: {
cfg: OpenClawConfig;
sessionKey: string;
}): ConfiguredAcpBindingSpec | null {
const sessionKey = params.sessionKey.trim();
if (!sessionKey) {
return null;
}
const parsedSessionKey = parseConfiguredBindingSessionKey({ sessionKey });
if (!parsedSessionKey) {
return null;
}
let wildcardMatch: ConfiguredAcpBindingSpec | null = null;
for (const binding of listAcpBindings(params.cfg)) {
const channel = normalizeBindingChannel(binding.match.channel);
if (!channel || channel !== parsedSessionKey.channel) {
continue;
}
const accountMatchPriority = resolveAccountMatchPriority(
binding.match.accountId,
parsedSessionKey.accountId,
);
if (accountMatchPriority === 0) {
continue;
}
const targetConversationId = resolveBindingConversationId(binding);
if (!targetConversationId) {
continue;
}
if (channel === "discord") {
const spec = toConfiguredBindingSpec({
cfg: params.cfg,
channel: "discord",
accountId: parsedSessionKey.accountId,
conversationId: targetConversationId,
binding,
});
if (buildConfiguredAcpSessionKey(spec) === sessionKey) {
if (accountMatchPriority === 2) {
return spec;
}
if (!wildcardMatch) {
wildcardMatch = spec;
}
}
continue;
}
const parsedTopic = parseTelegramTopicConversation({
conversationId: targetConversationId,
});
if (!parsedTopic || !parsedTopic.chatId.startsWith("-")) {
continue;
}
const spec = toConfiguredBindingSpec({
cfg: params.cfg,
channel: "telegram",
accountId: parsedSessionKey.accountId,
conversationId: parsedTopic.canonicalConversationId,
parentConversationId: parsedTopic.chatId,
binding,
});
if (buildConfiguredAcpSessionKey(spec) === sessionKey) {
if (accountMatchPriority === 2) {
return spec;
}
if (!wildcardMatch) {
wildcardMatch = spec;
}
}
}
return wildcardMatch;
}
export function resolveConfiguredAcpBindingRecord(params: {
cfg: OpenClawConfig;
channel: string;
accountId: string;
conversationId: string;
parentConversationId?: string;
}): ResolvedConfiguredAcpBinding | null {
const channel = params.channel.trim().toLowerCase();
const accountId = normalizeAccountId(params.accountId);
const conversationId = params.conversationId.trim();
const parentConversationId = params.parentConversationId?.trim() || undefined;
if (!conversationId) {
return null;
}
if (channel === "discord") {
const bindings = listAcpBindings(params.cfg);
const resolveDiscordBindingForConversation = (targetConversationId: string) =>
resolveConfiguredBindingRecord({
cfg: params.cfg,
bindings,
channel: "discord",
accountId,
selectConversation: (binding) => {
const bindingConversationId = resolveBindingConversationId(binding);
if (!bindingConversationId || bindingConversationId !== targetConversationId) {
return null;
}
return { conversationId: targetConversationId };
},
});
const directMatch = resolveDiscordBindingForConversation(conversationId);
if (directMatch) {
return directMatch;
}
if (parentConversationId && parentConversationId !== conversationId) {
const inheritedMatch = resolveDiscordBindingForConversation(parentConversationId);
if (inheritedMatch) {
return inheritedMatch;
}
}
return null;
}
if (channel === "telegram") {
const parsed = parseTelegramTopicConversation({
conversationId,
parentConversationId,
});
if (!parsed || !parsed.chatId.startsWith("-")) {
return null;
}
return resolveConfiguredBindingRecord({
cfg: params.cfg,
bindings: listAcpBindings(params.cfg),
channel: "telegram",
accountId,
selectConversation: (binding) => {
const targetConversationId = resolveBindingConversationId(binding);
if (!targetConversationId) {
return null;
}
const targetParsed = parseTelegramTopicConversation({
conversationId: targetConversationId,
});
if (!targetParsed || !targetParsed.chatId.startsWith("-")) {
return null;
}
if (targetParsed.canonicalConversationId !== parsed.canonicalConversationId) {
return null;
}
return {
conversationId: parsed.canonicalConversationId,
parentConversationId: parsed.chatId,
};
},
});
}
return null;
}