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; }