mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 23:14:31 +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:
@@ -174,6 +174,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Telegram/draft preview boundary + silent-token reliability: stabilize answer-lane message boundaries across late-partial/message-start races, preserve/reset finalized preview state at the correct boundaries, and suppress `NO_REPLY` lead-fragment leaks without broad heartbeat-prefix false positives. (#33169) Thanks @obviyus.
|
- Telegram/draft preview boundary + silent-token reliability: stabilize answer-lane message boundaries across late-partial/message-start races, preserve/reset finalized preview state at the correct boundaries, and suppress `NO_REPLY` lead-fragment leaks without broad heartbeat-prefix false positives. (#33169) Thanks @obviyus.
|
||||||
- Telegram/`groupAllowFrom` sender-ID validation: restore sender-only runtime validation so negative chat/group IDs remain invalid entries instead of appearing accepted while still being unable to authorize group access. (#37134) Thanks @qiuyuemartin-max and @vincentkoc.
|
- Telegram/`groupAllowFrom` sender-ID validation: restore sender-only runtime validation so negative chat/group IDs remain invalid entries instead of appearing accepted while still being unable to authorize group access. (#37134) Thanks @qiuyuemartin-max and @vincentkoc.
|
||||||
- Telegram/native group command auth: authorize native commands in groups and forum topics against `groupAllowFrom` and per-group/topic sender overrides, while keeping auth rejection replies in the originating topic thread. (#39267) Thanks @edwluo.
|
- Telegram/native group command auth: authorize native commands in groups and forum topics against `groupAllowFrom` and per-group/topic sender overrides, while keeping auth rejection replies in the originating topic thread. (#39267) Thanks @edwluo.
|
||||||
|
- Telegram/named-account DMs: restore non-default-account DM routing when a named Telegram account falls back to the default agent by keeping groups fail-closed but deriving a per-account session key for DMs, including identity-link canonicalization and regression coverage for account isolation. (from #32426; fixes #32351) Thanks @chengzhichao-xydt.
|
||||||
- Discord/audit wildcard warnings: ignore "\*" wildcard keys when counting unresolved guild channels so doctor/status no longer warns on allow-all configs. (#33125) Thanks @thewilloftheshadow.
|
- Discord/audit wildcard warnings: ignore "\*" wildcard keys when counting unresolved guild channels so doctor/status no longer warns on allow-all configs. (#33125) Thanks @thewilloftheshadow.
|
||||||
- Discord/channel resolution: default bare numeric recipients to channels, harden allowlist numeric ID handling with safe fallbacks, and avoid inbound WS heartbeat stalls. (#33142) Thanks @thewilloftheshadow.
|
- Discord/channel resolution: default bare numeric recipients to channels, harden allowlist numeric ID handling with safe fallbacks, and avoid inbound WS heartbeat stalls. (#33142) Thanks @thewilloftheshadow.
|
||||||
- Discord/chunk delivery reliability: preserve chunk ordering when using a REST client and retry chunk sends on 429/5xx using account retry settings. (#33226) Thanks @thewilloftheshadow.
|
- Discord/chunk delivery reliability: preserve chunk ordering when using a REST client and retry chunk sends on 429/5xx using account retry settings. (#33226) Thanks @thewilloftheshadow.
|
||||||
|
|||||||
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";
|
} from "../config/types.js";
|
||||||
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
||||||
import { recordChannelActivity } from "../infra/channel-activity.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 { DEFAULT_ACCOUNT_ID, resolveThreadSessionKeys } from "../routing/session-key.js";
|
||||||
import { resolvePinnedMainDmOwnerFromAllowlist } from "../security/dm-policy-shared.js";
|
import { resolvePinnedMainDmOwnerFromAllowlist } from "../security/dm-policy-shared.js";
|
||||||
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||||
@@ -55,6 +56,7 @@ import {
|
|||||||
buildTelegramGroupFrom,
|
buildTelegramGroupFrom,
|
||||||
buildTelegramGroupPeerId,
|
buildTelegramGroupPeerId,
|
||||||
buildTypingThreadParams,
|
buildTypingThreadParams,
|
||||||
|
resolveTelegramDirectPeerId,
|
||||||
resolveTelegramMediaPlaceholder,
|
resolveTelegramMediaPlaceholder,
|
||||||
expandTextLinks,
|
expandTextLinks,
|
||||||
normalizeForwardedContext,
|
normalizeForwardedContext,
|
||||||
@@ -208,9 +210,10 @@ export const buildTelegramMessageContext = async ({
|
|||||||
const requiresExplicitAccountBinding = (
|
const requiresExplicitAccountBinding = (
|
||||||
candidate: ReturnType<typeof resolveTelegramConversationRoute>["route"],
|
candidate: ReturnType<typeof resolveTelegramConversationRoute>["route"],
|
||||||
): boolean => candidate.accountId !== DEFAULT_ACCOUNT_ID && candidate.matchedBy === "default";
|
): boolean => candidate.accountId !== DEFAULT_ACCOUNT_ID && candidate.matchedBy === "default";
|
||||||
// Fail closed for named Telegram accounts when route resolution falls back to
|
const isNamedAccountFallback = requiresExplicitAccountBinding(route);
|
||||||
// default-agent routing. This prevents cross-account DM/session contamination.
|
// Named-account groups still require an explicit binding; DMs get a
|
||||||
if (requiresExplicitAccountBinding(route)) {
|
// per-account fallback session key below to preserve isolation.
|
||||||
|
if (isNamedAccountFallback && isGroup) {
|
||||||
logInboundDrop({
|
logInboundDrop({
|
||||||
log: logVerbose,
|
log: logVerbose,
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
@@ -337,7 +340,22 @@ export const buildTelegramMessageContext = async ({
|
|||||||
return false;
|
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)
|
// DMs: use thread suffix for session isolation (works regardless of dmScope)
|
||||||
const threadKeys =
|
const threadKeys =
|
||||||
dmThreadId != null
|
dmThreadId != null
|
||||||
|
|||||||
Reference in New Issue
Block a user