mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 16:37:35 +00:00
refactor(telegram): encode conversation binding mode
This commit is contained in:
@@ -146,6 +146,7 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- No `@ts-nocheck`. Lint suppressions only intentional + explained.
|
||||
- External boundaries: prefer `zod` or existing schema helpers.
|
||||
- Runtime branching: discriminated unions/closed codes over freeform strings. Avoid semantic sentinels (`?? 0`, empty object/string).
|
||||
- Cross-function state: when valid combos matter, return a closed mode/result shape. Avoid parallel nullable fields or derived booleans that callers must keep in sync; make impossible states unrepresentable.
|
||||
- Formatter-friendly shape: when oxfmt explodes an expression vertically, extract named booleans, payloads, or small helpers. Do not change width or use format-ignore for local compactness.
|
||||
- Calls should be boring: complex decisions happen above; call args/object fields are names, literals, or simple property reads.
|
||||
- Prefer early returns over nested condition pyramids. Split code into gather -> normalize -> decide -> act.
|
||||
|
||||
@@ -63,70 +63,73 @@ function createConfiguredTelegramBinding() {
|
||||
function createConfiguredTelegramRoute() {
|
||||
const configuredBinding = createConfiguredTelegramBinding();
|
||||
return {
|
||||
configuredBinding: {
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "work",
|
||||
conversationId: "-1001234567890:topic:42",
|
||||
parentConversationId: "-1001234567890",
|
||||
},
|
||||
compiledBinding: {
|
||||
channel: "telegram",
|
||||
accountPattern: "work",
|
||||
binding: {
|
||||
type: "acp",
|
||||
agentId: "codex",
|
||||
match: {
|
||||
channel: "telegram",
|
||||
accountId: "work",
|
||||
peer: {
|
||||
kind: "group",
|
||||
id: "-1001234567890:topic:42",
|
||||
},
|
||||
},
|
||||
},
|
||||
bindingConversationId: "-1001234567890:topic:42",
|
||||
target: {
|
||||
bindingMode: {
|
||||
kind: "configured",
|
||||
binding: {
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "work",
|
||||
conversationId: "-1001234567890:topic:42",
|
||||
parentConversationId: "-1001234567890",
|
||||
},
|
||||
agentId: "codex",
|
||||
provider: {
|
||||
compileConfiguredBinding: () => ({
|
||||
conversationId: "-1001234567890:topic:42",
|
||||
parentConversationId: "-1001234567890",
|
||||
}),
|
||||
matchInboundConversation: () => ({
|
||||
conversationId: "-1001234567890:topic:42",
|
||||
parentConversationId: "-1001234567890",
|
||||
}),
|
||||
},
|
||||
targetFactory: {
|
||||
driverId: "acp",
|
||||
materialize: () => ({
|
||||
record: configuredBinding.record,
|
||||
statefulTarget: {
|
||||
kind: "stateful",
|
||||
driverId: "acp",
|
||||
sessionKey: configuredBinding.record.targetSessionKey,
|
||||
agentId: configuredBinding.spec.agentId,
|
||||
compiledBinding: {
|
||||
channel: "telegram",
|
||||
accountPattern: "work",
|
||||
binding: {
|
||||
type: "acp",
|
||||
agentId: "codex",
|
||||
match: {
|
||||
channel: "telegram",
|
||||
accountId: "work",
|
||||
peer: {
|
||||
kind: "group",
|
||||
id: "-1001234567890:topic:42",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
bindingConversationId: "-1001234567890:topic:42",
|
||||
target: {
|
||||
conversationId: "-1001234567890:topic:42",
|
||||
parentConversationId: "-1001234567890",
|
||||
},
|
||||
agentId: "codex",
|
||||
provider: {
|
||||
compileConfiguredBinding: () => ({
|
||||
conversationId: "-1001234567890:topic:42",
|
||||
parentConversationId: "-1001234567890",
|
||||
}),
|
||||
matchInboundConversation: () => ({
|
||||
conversationId: "-1001234567890:topic:42",
|
||||
parentConversationId: "-1001234567890",
|
||||
}),
|
||||
},
|
||||
targetFactory: {
|
||||
driverId: "acp",
|
||||
materialize: () => ({
|
||||
record: configuredBinding.record,
|
||||
statefulTarget: {
|
||||
kind: "stateful",
|
||||
driverId: "acp",
|
||||
sessionKey: configuredBinding.record.targetSessionKey,
|
||||
agentId: configuredBinding.spec.agentId,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
match: {
|
||||
conversationId: "-1001234567890:topic:42",
|
||||
parentConversationId: "-1001234567890",
|
||||
},
|
||||
record: configuredBinding.record,
|
||||
statefulTarget: {
|
||||
kind: "stateful",
|
||||
driverId: "acp",
|
||||
sessionKey: configuredBinding.record.targetSessionKey,
|
||||
agentId: configuredBinding.spec.agentId,
|
||||
},
|
||||
},
|
||||
match: {
|
||||
conversationId: "-1001234567890:topic:42",
|
||||
parentConversationId: "-1001234567890",
|
||||
},
|
||||
record: configuredBinding.record,
|
||||
statefulTarget: {
|
||||
kind: "stateful",
|
||||
driverId: "acp",
|
||||
sessionKey: configuredBinding.record.targetSessionKey,
|
||||
agentId: configuredBinding.spec.agentId,
|
||||
},
|
||||
sessionKey: configuredBinding.record.targetSessionKey,
|
||||
},
|
||||
configuredBindingSessionKey: configuredBinding.record.targetSessionKey,
|
||||
route: {
|
||||
agentId: "codex",
|
||||
accountId: "work",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { telegramRouteTestSessionRuntime } from "./bot-message-context.route-test-support.js";
|
||||
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
|
||||
import type { TelegramConversationBindingMode } from "./conversation-route.js";
|
||||
|
||||
const recordInboundSessionMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
||||
const resolveTelegramConversationRouteMock = vi.hoisted(() => vi.fn());
|
||||
@@ -32,12 +33,13 @@ function createBoundRoute(params: {
|
||||
accountId: string;
|
||||
sessionKey: string;
|
||||
agentId: string;
|
||||
pluginOwnedRuntimeBinding?: boolean;
|
||||
bindingMode?: TelegramConversationBindingMode;
|
||||
}) {
|
||||
return {
|
||||
configuredBinding: null,
|
||||
configuredBindingSessionKey: "",
|
||||
pluginOwnedRuntimeBinding: params.pluginOwnedRuntimeBinding ?? false,
|
||||
bindingMode: params.bindingMode ?? {
|
||||
kind: "runtime-bound",
|
||||
sessionKey: params.sessionKey,
|
||||
},
|
||||
route: {
|
||||
accountId: params.accountId,
|
||||
agentId: params.agentId,
|
||||
@@ -112,7 +114,7 @@ describe("buildTelegramMessageContext thread binding override", () => {
|
||||
accountId: "default",
|
||||
sessionKey: "plugin-binding:openclaw-codex-app-server:session-1",
|
||||
agentId: "main",
|
||||
pluginOwnedRuntimeBinding: true,
|
||||
bindingMode: { kind: "plugin-owned-runtime" },
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -242,17 +242,16 @@ export const buildTelegramMessageContext = async ({
|
||||
const freshCfg =
|
||||
loadFreshConfig?.() ??
|
||||
(runtime?.getRuntimeConfig ?? (await loadTelegramMessageContextRuntime()).getRuntimeConfig)();
|
||||
let { route, configuredBinding, configuredBindingSessionKey, pluginOwnedRuntimeBinding } =
|
||||
resolveTelegramConversationRoute({
|
||||
cfg: freshCfg,
|
||||
accountId: account.accountId,
|
||||
chatId,
|
||||
isGroup,
|
||||
resolvedThreadId,
|
||||
replyThreadId,
|
||||
senderId,
|
||||
topicAgentId: topicConfig?.agentId,
|
||||
});
|
||||
let { route, bindingMode } = resolveTelegramConversationRoute({
|
||||
cfg: freshCfg,
|
||||
accountId: account.accountId,
|
||||
chatId,
|
||||
isGroup,
|
||||
resolvedThreadId,
|
||||
replyThreadId,
|
||||
senderId,
|
||||
topicAgentId: topicConfig?.agentId,
|
||||
});
|
||||
const requiresExplicitAccountBinding = (
|
||||
candidate: ReturnType<typeof resolveTelegramConversationRoute>["route"],
|
||||
): boolean =>
|
||||
@@ -372,7 +371,7 @@ export const buildTelegramMessageContext = async ({
|
||||
}
|
||||
let initialTypingCueSent = false;
|
||||
const ensureConfiguredBindingReady = async (): Promise<boolean> => {
|
||||
if (!configuredBinding) {
|
||||
if (bindingMode.kind !== "configured") {
|
||||
return true;
|
||||
}
|
||||
const ensureConfiguredBindingRouteReady =
|
||||
@@ -380,22 +379,22 @@ export const buildTelegramMessageContext = async ({
|
||||
(await loadTelegramMessageContextRuntime()).ensureConfiguredBindingRouteReady;
|
||||
const ensured = await ensureConfiguredBindingRouteReady({
|
||||
cfg: freshCfg,
|
||||
bindingResolution: configuredBinding,
|
||||
bindingResolution: bindingMode.binding,
|
||||
});
|
||||
if (ensured.ok) {
|
||||
logVerbose(
|
||||
`telegram: using configured ACP binding for ${configuredBinding.record.conversation.conversationId} -> ${configuredBindingSessionKey}`,
|
||||
`telegram: using configured ACP binding for ${bindingMode.binding.record.conversation.conversationId} -> ${bindingMode.sessionKey}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
logVerbose(
|
||||
`telegram: configured ACP binding unavailable for ${configuredBinding.record.conversation.conversationId}: ${ensured.error}`,
|
||||
`telegram: configured ACP binding unavailable for ${bindingMode.binding.record.conversation.conversationId}: ${ensured.error}`,
|
||||
);
|
||||
logInboundDrop({
|
||||
log: logVerbose,
|
||||
channel: "telegram",
|
||||
reason: "configured ACP binding unavailable",
|
||||
target: configuredBinding.record.conversation.conversationId,
|
||||
target: bindingMode.binding.record.conversation.conversationId,
|
||||
});
|
||||
return false;
|
||||
};
|
||||
@@ -432,7 +431,7 @@ export const buildTelegramMessageContext = async ({
|
||||
});
|
||||
const baseRequireMention = resolveGroupRequireMention(chatId);
|
||||
const requireMention =
|
||||
isGroup && pluginOwnedRuntimeBinding
|
||||
isGroup && bindingMode.kind === "plugin-owned-runtime"
|
||||
? false
|
||||
: firstDefined(
|
||||
topicConfig?.requireMention,
|
||||
|
||||
@@ -886,7 +886,7 @@ export const registerTelegramNativeCommands = ({
|
||||
isForum,
|
||||
messageThreadId: resolvedThreadId ?? messageThreadId,
|
||||
});
|
||||
let { route, configuredBinding } = resolveTelegramConversationRoute({
|
||||
let { route, bindingMode } = resolveTelegramConversationRoute({
|
||||
cfg: runtimeCfg,
|
||||
accountId,
|
||||
chatId,
|
||||
@@ -897,14 +897,14 @@ export const registerTelegramNativeCommands = ({
|
||||
topicAgentId,
|
||||
});
|
||||
const nativeCommandRuntime = await loadTelegramNativeCommandRuntime();
|
||||
if (configuredBinding) {
|
||||
if (bindingMode.kind === "configured") {
|
||||
const ensured = await nativeCommandRuntime.ensureConfiguredBindingRouteReady({
|
||||
cfg: runtimeCfg,
|
||||
bindingResolution: configuredBinding,
|
||||
bindingResolution: bindingMode.binding,
|
||||
});
|
||||
if (!ensured.ok) {
|
||||
logVerbose(
|
||||
`telegram native command: configured ACP binding unavailable for topic ${configuredBinding.record.conversation.conversationId}: ${ensured.error}`,
|
||||
`telegram native command: configured ACP binding unavailable for topic ${bindingMode.binding.record.conversation.conversationId}: ${ensured.error}`,
|
||||
);
|
||||
await withTelegramApiErrorLogging({
|
||||
operation: "sendMessage",
|
||||
|
||||
@@ -156,12 +156,10 @@ describe("resolveTelegramConversationBaseSessionKey", () => {
|
||||
});
|
||||
|
||||
expect(touch).not.toHaveBeenCalled();
|
||||
expect(result.configuredBinding).toBeNull();
|
||||
expect(result.configuredBindingSessionKey).toBe("");
|
||||
expect(result.bindingMode).toEqual({ kind: "none" });
|
||||
expect(result.route.agentId).toBe("main");
|
||||
expect(result.route.sessionKey).toBe("agent:main:main");
|
||||
expect(result.route.matchedBy).toBe("default");
|
||||
expect(result.pluginOwnedRuntimeBinding).toBe(false);
|
||||
});
|
||||
|
||||
it("detects plugin-owned runtime bindings without replacing the route", () => {
|
||||
@@ -205,11 +203,9 @@ describe("resolveTelegramConversationBaseSessionKey", () => {
|
||||
});
|
||||
|
||||
expect(touch).toHaveBeenCalledWith("binding-plugin-owned", undefined);
|
||||
expect(result.configuredBinding).toBeNull();
|
||||
expect(result.configuredBindingSessionKey).toBe("");
|
||||
expect(result.bindingMode).toEqual({ kind: "plugin-owned-runtime" });
|
||||
expect(result.route.agentId).toBe("main");
|
||||
expect(result.route.sessionKey).toBe("agent:main:telegram:group:-1001234567890:topic:11");
|
||||
expect(result.route.matchedBy).toBe("default");
|
||||
expect(result.pluginOwnedRuntimeBinding).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,27 @@ import {
|
||||
resolveTelegramDirectPeerId,
|
||||
} from "./bot/helpers.js";
|
||||
|
||||
type TelegramResolvedRoute = ReturnType<typeof resolveAgentRoute>;
|
||||
type ConfiguredTelegramBinding = NonNullable<ConfiguredBindingRouteResult["bindingResolution"]>;
|
||||
|
||||
export type TelegramConversationBindingMode =
|
||||
| { kind: "none" }
|
||||
| {
|
||||
kind: "configured";
|
||||
binding: ConfiguredTelegramBinding;
|
||||
sessionKey: string;
|
||||
}
|
||||
| {
|
||||
kind: "runtime-bound";
|
||||
sessionKey: string;
|
||||
}
|
||||
| { kind: "plugin-owned-runtime" };
|
||||
|
||||
export type TelegramConversationRouteResult = {
|
||||
route: TelegramResolvedRoute;
|
||||
bindingMode: TelegramConversationBindingMode;
|
||||
};
|
||||
|
||||
export function resolveTelegramConversationRoute(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
@@ -29,12 +50,7 @@ export function resolveTelegramConversationRoute(params: {
|
||||
replyThreadId?: number;
|
||||
senderId?: string | number | null;
|
||||
topicAgentId?: string | null;
|
||||
}): {
|
||||
route: ReturnType<typeof resolveAgentRoute>;
|
||||
configuredBinding: ConfiguredBindingRouteResult["bindingResolution"];
|
||||
configuredBindingSessionKey: string;
|
||||
pluginOwnedRuntimeBinding: boolean;
|
||||
} {
|
||||
}): TelegramConversationRouteResult {
|
||||
const peerId = params.isGroup
|
||||
? buildTelegramGroupPeerId(params.chatId, params.resolvedThreadId)
|
||||
: resolveTelegramDirectPeerId({
|
||||
@@ -102,9 +118,14 @@ export function resolveTelegramConversationRoute(params: {
|
||||
parentConversationId: params.isGroup ? String(params.chatId) : undefined,
|
||||
},
|
||||
});
|
||||
let configuredBinding = configuredRoute.bindingResolution;
|
||||
let configuredBindingSessionKey = configuredRoute.boundSessionKey ?? "";
|
||||
route = configuredRoute.route;
|
||||
let bindingMode: TelegramConversationBindingMode = configuredRoute.bindingResolution
|
||||
? {
|
||||
kind: "configured",
|
||||
binding: configuredRoute.bindingResolution,
|
||||
sessionKey: configuredRoute.boundSessionKey ?? route.sessionKey,
|
||||
}
|
||||
: { kind: "none" };
|
||||
|
||||
const runtimeBindingConversationId =
|
||||
params.replyThreadId != null
|
||||
@@ -119,12 +140,10 @@ export function resolveTelegramConversationRoute(params: {
|
||||
},
|
||||
});
|
||||
route = runtimeRoute.route;
|
||||
const pluginOwnedRuntimeBinding = Boolean(
|
||||
runtimeRoute.bindingRecord && !runtimeRoute.boundSessionKey,
|
||||
);
|
||||
if (runtimeRoute.bindingRecord) {
|
||||
configuredBinding = null;
|
||||
configuredBindingSessionKey = "";
|
||||
bindingMode = runtimeRoute.boundSessionKey
|
||||
? { kind: "runtime-bound", sessionKey: runtimeRoute.boundSessionKey }
|
||||
: { kind: "plugin-owned-runtime" };
|
||||
logVerbose(
|
||||
runtimeRoute.boundSessionKey
|
||||
? `telegram: routed via bound conversation ${runtimeBindingConversationId} -> ${runtimeRoute.boundSessionKey}`
|
||||
@@ -134,9 +153,7 @@ export function resolveTelegramConversationRoute(params: {
|
||||
|
||||
return {
|
||||
route,
|
||||
configuredBinding,
|
||||
configuredBindingSessionKey,
|
||||
pluginOwnedRuntimeBinding,
|
||||
bindingMode,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user