mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 22:14:34 +00:00
fix(telegram): restore named-account DM fallback routing (from #32426)
Rebased and landed contributor work from @chengzhichao-xydt for the Telegram multi-account DM regression in #32351. Co-authored-by: Zhichao Cheng <cheng.zhichao@xydigit.com>
This commit is contained in:
147
src/telegram/bot-message-context.named-account-dm.test.ts
Normal file
147
src/telegram/bot-message-context.named-account-dm.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
|
||||
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
|
||||
|
||||
describe("buildTelegramMessageContext named-account DM fallback", () => {
|
||||
const baseCfg = {
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
|
||||
channels: { telegram: {} },
|
||||
messages: { groupChat: { mentionPatterns: [] } },
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
});
|
||||
|
||||
it("allows DM through for a named account with no explicit binding", async () => {
|
||||
setRuntimeConfigSnapshot(baseCfg);
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
cfg: baseCfg,
|
||||
accountId: "atlas",
|
||||
message: {
|
||||
message_id: 1,
|
||||
chat: { id: 814912386, type: "private" },
|
||||
date: 1700000000,
|
||||
text: "hello",
|
||||
from: { id: 814912386, first_name: "Alice" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx?.route.matchedBy).toBe("default");
|
||||
expect(ctx?.route.accountId).toBe("atlas");
|
||||
});
|
||||
|
||||
it("uses a per-account session key for named-account DMs", async () => {
|
||||
setRuntimeConfigSnapshot(baseCfg);
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
cfg: baseCfg,
|
||||
accountId: "atlas",
|
||||
message: {
|
||||
message_id: 1,
|
||||
chat: { id: 814912386, type: "private" },
|
||||
date: 1700000000,
|
||||
text: "hello",
|
||||
from: { id: 814912386, first_name: "Alice" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:telegram:atlas:direct:814912386");
|
||||
});
|
||||
|
||||
it("isolates sessions between named accounts that share the default agent", async () => {
|
||||
setRuntimeConfigSnapshot(baseCfg);
|
||||
|
||||
const atlas = await buildTelegramMessageContextForTest({
|
||||
cfg: baseCfg,
|
||||
accountId: "atlas",
|
||||
message: {
|
||||
message_id: 1,
|
||||
chat: { id: 814912386, type: "private" },
|
||||
date: 1700000000,
|
||||
text: "hello",
|
||||
from: { id: 814912386, first_name: "Alice" },
|
||||
},
|
||||
});
|
||||
const skynet = await buildTelegramMessageContextForTest({
|
||||
cfg: baseCfg,
|
||||
accountId: "skynet",
|
||||
message: {
|
||||
message_id: 2,
|
||||
chat: { id: 814912386, type: "private" },
|
||||
date: 1700000001,
|
||||
text: "hello",
|
||||
from: { id: 814912386, first_name: "Alice" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(atlas?.ctxPayload?.SessionKey).toBe("agent:main:telegram:atlas:direct:814912386");
|
||||
expect(skynet?.ctxPayload?.SessionKey).toBe("agent:main:telegram:skynet:direct:814912386");
|
||||
expect(atlas?.ctxPayload?.SessionKey).not.toBe(skynet?.ctxPayload?.SessionKey);
|
||||
});
|
||||
|
||||
it("keeps identity-linked peer canonicalization in the named-account fallback path", async () => {
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
session: {
|
||||
identityLinks: {
|
||||
"alice-shared": ["telegram:814912386"],
|
||||
},
|
||||
},
|
||||
};
|
||||
setRuntimeConfigSnapshot(cfg);
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
cfg,
|
||||
accountId: "atlas",
|
||||
message: {
|
||||
message_id: 1,
|
||||
chat: { id: 999999999, type: "private" },
|
||||
date: 1700000000,
|
||||
text: "hello",
|
||||
from: { id: 814912386, first_name: "Alice" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:telegram:atlas:direct:alice-shared");
|
||||
});
|
||||
|
||||
it("still drops named-account group messages without an explicit binding", async () => {
|
||||
setRuntimeConfigSnapshot(baseCfg);
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
cfg: baseCfg,
|
||||
accountId: "atlas",
|
||||
options: { forceWasMentioned: true },
|
||||
resolveGroupActivation: () => true,
|
||||
message: {
|
||||
message_id: 1,
|
||||
chat: { id: -1001234567890, type: "supergroup", title: "Test Group" },
|
||||
date: 1700000000,
|
||||
text: "@bot hello",
|
||||
from: { id: 814912386, first_name: "Alice" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(ctx).toBeNull();
|
||||
});
|
||||
|
||||
it("does not change the default-account DM session key", async () => {
|
||||
setRuntimeConfigSnapshot(baseCfg);
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
cfg: baseCfg,
|
||||
message: {
|
||||
message_id: 1,
|
||||
chat: { id: 42, type: "private" },
|
||||
date: 1700000000,
|
||||
text: "hello",
|
||||
from: { id: 42, first_name: "Alice" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main");
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,7 @@ import type {
|
||||
} from "../config/types.js";
|
||||
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
||||
import { recordChannelActivity } from "../infra/channel-activity.js";
|
||||
import { buildAgentSessionKey } from "../routing/resolve-route.js";
|
||||
import { DEFAULT_ACCOUNT_ID, resolveThreadSessionKeys } from "../routing/session-key.js";
|
||||
import { resolvePinnedMainDmOwnerFromAllowlist } from "../security/dm-policy-shared.js";
|
||||
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||
@@ -55,6 +56,7 @@ import {
|
||||
buildTelegramGroupFrom,
|
||||
buildTelegramGroupPeerId,
|
||||
buildTypingThreadParams,
|
||||
resolveTelegramDirectPeerId,
|
||||
resolveTelegramMediaPlaceholder,
|
||||
expandTextLinks,
|
||||
normalizeForwardedContext,
|
||||
@@ -208,9 +210,10 @@ export const buildTelegramMessageContext = async ({
|
||||
const requiresExplicitAccountBinding = (
|
||||
candidate: ReturnType<typeof resolveTelegramConversationRoute>["route"],
|
||||
): boolean => candidate.accountId !== DEFAULT_ACCOUNT_ID && candidate.matchedBy === "default";
|
||||
// Fail closed for named Telegram accounts when route resolution falls back to
|
||||
// default-agent routing. This prevents cross-account DM/session contamination.
|
||||
if (requiresExplicitAccountBinding(route)) {
|
||||
const isNamedAccountFallback = requiresExplicitAccountBinding(route);
|
||||
// Named-account groups still require an explicit binding; DMs get a
|
||||
// per-account fallback session key below to preserve isolation.
|
||||
if (isNamedAccountFallback && isGroup) {
|
||||
logInboundDrop({
|
||||
log: logVerbose,
|
||||
channel: "telegram",
|
||||
@@ -337,7 +340,22 @@ export const buildTelegramMessageContext = async ({
|
||||
return false;
|
||||
};
|
||||
|
||||
const baseSessionKey = route.sessionKey;
|
||||
const baseSessionKey = isNamedAccountFallback
|
||||
? buildAgentSessionKey({
|
||||
agentId: route.agentId,
|
||||
channel: "telegram",
|
||||
accountId: route.accountId,
|
||||
peer: {
|
||||
kind: "direct",
|
||||
id: resolveTelegramDirectPeerId({
|
||||
chatId,
|
||||
senderId,
|
||||
}),
|
||||
},
|
||||
dmScope: "per-account-channel-peer",
|
||||
identityLinks: freshCfg.session?.identityLinks,
|
||||
}).toLowerCase()
|
||||
: route.sessionKey;
|
||||
// DMs: use thread suffix for session isolation (works regardless of dmScope)
|
||||
const threadKeys =
|
||||
dmThreadId != null
|
||||
|
||||
Reference in New Issue
Block a user