mirror of https://github.com/openclaw/openclaw.git
339 lines
10 KiB
TypeScript
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;
|
|
}
|