openclaw/extensions/telegram/src/conversation-route.ts
Bob ea15819ecf
ACP: harden startup and move configured routing behind plugin seams (#48197)
* ACPX: keep plugin-local runtime installs out of dist

* Gateway: harden ACP startup and service PATH

* ACP: reinitialize error-state configured bindings

* ACP: classify pre-turn runtime failures as session init failures

* Plugins: move configured ACP routing behind channel seams

* Telegram tests: align startup probe assertions after rebase

* Discord: harden ACP configured binding recovery

* ACP: recover Discord bindings after stale runtime exits

* ACPX: replace dead sessions during ensure

* Discord: harden ACP binding recovery

* Discord: fix review follow-ups

* ACP bindings: load channel snapshots across workspaces

* ACP bindings: cache snapshot channel plugin resolution

* Experiments: add ACP pluginification holy grail plan

* Experiments: rename ACP pluginification plan doc

* Experiments: drop old ACP pluginification doc path

* ACP: move configured bindings behind plugin services

* Experiments: update bindings capability architecture plan

* Bindings: isolate configured binding routing and targets

* Discord tests: fix runtime env helper path

* Tests: fix channel binding CI regressions

* Tests: normalize ACP workspace assertion on Windows

* Bindings: isolate configured binding registry

* Bindings: finish configured binding cleanup

* Bindings: finish generic cleanup

* Bindings: align runtime approval callbacks

* ACP: delete residual bindings barrel

* Bindings: restore legacy compatibility

* Revert "Bindings: restore legacy compatibility"

This reverts commit ac2ed68fa2426ecc874d68278c71c71ad363fcfe.

* Tests: drop ACP route legacy helper names

* Discord/ACP: fix binding regressions

---------

Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-17 17:27:52 +01:00

188 lines
6.1 KiB
TypeScript

import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
resolveConfiguredBindingRoute,
type ConfiguredBindingRouteResult,
} from "openclaw/plugin-sdk/conversation-runtime";
import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime";
import { isPluginOwnedSessionBindingRecord } from "openclaw/plugin-sdk/conversation-runtime";
import {
buildAgentSessionKey,
deriveLastRoutePolicy,
resolveAgentRoute,
} from "openclaw/plugin-sdk/routing";
import {
buildAgentMainSessionKey,
DEFAULT_ACCOUNT_ID,
resolveAgentIdFromSessionKey,
sanitizeAgentId,
} from "openclaw/plugin-sdk/routing";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import {
buildTelegramGroupPeerId,
buildTelegramParentPeer,
resolveTelegramDirectPeerId,
} from "./bot/helpers.js";
export function resolveTelegramConversationRoute(params: {
cfg: OpenClawConfig;
accountId: string;
chatId: number | string;
isGroup: boolean;
resolvedThreadId?: number;
replyThreadId?: number;
senderId?: string | number | null;
topicAgentId?: string | null;
}): {
route: ReturnType<typeof resolveAgentRoute>;
configuredBinding: ConfiguredBindingRouteResult["bindingResolution"];
configuredBindingSessionKey: string;
} {
const peerId = params.isGroup
? buildTelegramGroupPeerId(params.chatId, params.resolvedThreadId)
: resolveTelegramDirectPeerId({
chatId: params.chatId,
senderId: params.senderId,
});
const parentPeer = buildTelegramParentPeer({
isGroup: params.isGroup,
resolvedThreadId: params.resolvedThreadId,
chatId: params.chatId,
});
let route = resolveAgentRoute({
cfg: params.cfg,
channel: "telegram",
accountId: params.accountId,
peer: {
kind: params.isGroup ? "group" : "direct",
id: peerId,
},
parentPeer,
});
const rawTopicAgentId = params.topicAgentId?.trim();
if (rawTopicAgentId) {
// Preserve the configured topic agent ID so topic-bound sessions stay stable
// even when that agent is not present in the current config snapshot.
const topicAgentId = sanitizeAgentId(rawTopicAgentId);
route = {
...route,
agentId: topicAgentId,
sessionKey: buildAgentSessionKey({
agentId: topicAgentId,
channel: "telegram",
accountId: params.accountId,
peer: { kind: params.isGroup ? "group" : "direct", id: peerId },
dmScope: params.cfg.session?.dmScope,
identityLinks: params.cfg.session?.identityLinks,
}).toLowerCase(),
mainSessionKey: buildAgentMainSessionKey({
agentId: topicAgentId,
}).toLowerCase(),
lastRoutePolicy: deriveLastRoutePolicy({
sessionKey: buildAgentSessionKey({
agentId: topicAgentId,
channel: "telegram",
accountId: params.accountId,
peer: { kind: params.isGroup ? "group" : "direct", id: peerId },
dmScope: params.cfg.session?.dmScope,
identityLinks: params.cfg.session?.identityLinks,
}).toLowerCase(),
mainSessionKey: buildAgentMainSessionKey({
agentId: topicAgentId,
}).toLowerCase(),
}),
};
logVerbose(
`telegram: topic route override: topic=${params.resolvedThreadId ?? params.replyThreadId} agent=${topicAgentId} sessionKey=${route.sessionKey}`,
);
}
const configuredRoute = resolveConfiguredBindingRoute({
cfg: params.cfg,
route,
conversation: {
channel: "telegram",
accountId: params.accountId,
conversationId: peerId,
parentConversationId: params.isGroup ? String(params.chatId) : undefined,
},
});
let configuredBinding = configuredRoute.bindingResolution;
let configuredBindingSessionKey = configuredRoute.boundSessionKey ?? "";
route = configuredRoute.route;
const threadBindingConversationId =
params.replyThreadId != null
? `${params.chatId}:topic:${params.replyThreadId}`
: !params.isGroup
? String(params.chatId)
: undefined;
if (threadBindingConversationId) {
const threadBinding = getSessionBindingService().resolveByConversation({
channel: "telegram",
accountId: params.accountId,
conversationId: threadBindingConversationId,
});
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
if (threadBinding && boundSessionKey) {
if (!isPluginOwnedSessionBindingRecord(threadBinding)) {
route = {
...route,
sessionKey: boundSessionKey,
agentId: resolveAgentIdFromSessionKey(boundSessionKey),
lastRoutePolicy: deriveLastRoutePolicy({
sessionKey: boundSessionKey,
mainSessionKey: route.mainSessionKey,
}),
matchedBy: "binding.channel",
};
}
configuredBinding = null;
configuredBindingSessionKey = "";
getSessionBindingService().touch(threadBinding.bindingId);
logVerbose(
isPluginOwnedSessionBindingRecord(threadBinding)
? `telegram: plugin-bound conversation ${threadBindingConversationId}`
: `telegram: routed via bound conversation ${threadBindingConversationId} -> ${boundSessionKey}`,
);
}
}
return {
route,
configuredBinding,
configuredBindingSessionKey,
};
}
export function resolveTelegramConversationBaseSessionKey(params: {
cfg: OpenClawConfig;
route: Pick<
ReturnType<typeof resolveTelegramConversationRoute>["route"],
"agentId" | "accountId" | "matchedBy" | "sessionKey"
>;
chatId: number | string;
isGroup: boolean;
senderId?: string | number | null;
}): string {
const isNamedAccountFallback =
params.route.accountId !== DEFAULT_ACCOUNT_ID && params.route.matchedBy === "default";
if (!isNamedAccountFallback || params.isGroup) {
return params.route.sessionKey;
}
return buildAgentSessionKey({
agentId: params.route.agentId,
channel: "telegram",
accountId: params.route.accountId,
peer: {
kind: "direct",
id: resolveTelegramDirectPeerId({
chatId: params.chatId,
senderId: params.senderId,
}),
},
dmScope: "per-account-channel-peer",
identityLinks: params.cfg.session?.identityLinks,
}).toLowerCase();
}