fix: telegram topic auto-threading — use parseTelegramTarget, add tests (#7235) (thanks @Lukavyi)

This commit is contained in:
Ayaan Zaidi
2026-02-06 00:22:40 +05:30
committed by Ayaan Zaidi
parent a13efbe2b5
commit 01db1dde1a
4 changed files with 78 additions and 14 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Telegram: auto-inject forum topic `threadId` in message tool and subagent announce so media, buttons, and subagent results land in the correct topic instead of General. (#7235) Thanks @Lukavyi.
- CLI: sort `openclaw --help` commands (and options) alphabetically. (#8068) Thanks @deepsoumya617.
- Telegram: remove last `@ts-nocheck` from `bot-handlers.ts`, use Grammy types directly, deduplicate `StickerMetadata`. Zero `@ts-nocheck` remaining in `src/telegram/`. (#9206)
- Telegram: remove `@ts-nocheck` from `bot-message.ts`, type deps via `Omit<BuildTelegramMessageContextParams>`, widen `allMedia` to `TelegramMediaRef[]`. (#9180)

View File

@@ -152,7 +152,63 @@ describe("runMessageAction threading auto-injection", () => {
agentId: "main",
});
const call = mocks.executeSendAction.mock.calls[0]?.[0] as { ctx?: { params?: any } };
const call = mocks.executeSendAction.mock.calls[0]?.[0] as {
ctx?: { params?: Record<string, unknown> };
};
expect(call?.ctx?.params?.threadId).toBe("42");
});
it("skips telegram auto-threading when target chat differs", async () => {
mocks.executeSendAction.mockResolvedValue({
handledBy: "plugin",
payload: {},
});
await runMessageAction({
cfg: telegramConfig,
action: "send",
params: {
channel: "telegram",
target: "telegram:999",
message: "hi",
},
toolContext: {
currentChannelId: "telegram:123",
currentThreadTs: "42",
},
agentId: "main",
});
const call = mocks.executeSendAction.mock.calls[0]?.[0] as {
ctx?: { params?: Record<string, unknown> };
};
expect(call?.ctx?.params?.threadId).toBeUndefined();
});
it("matches telegram target with internal prefix variations", async () => {
mocks.executeSendAction.mockResolvedValue({
handledBy: "plugin",
payload: {},
});
await runMessageAction({
cfg: telegramConfig,
action: "send",
params: {
channel: "telegram",
target: "telegram:group:123",
message: "hi",
},
toolContext: {
currentChannelId: "telegram:123",
currentThreadTs: "42",
},
agentId: "main",
});
const call = mocks.executeSendAction.mock.calls[0]?.[0] as {
ctx?: { params?: Record<string, unknown> };
};
expect(call?.ctx?.params?.threadId).toBe("42");
});
@@ -178,7 +234,9 @@ describe("runMessageAction threading auto-injection", () => {
agentId: "main",
});
const call = mocks.executeSendAction.mock.calls[0]?.[0] as { ctx?: { params?: any } };
const call = mocks.executeSendAction.mock.calls[0]?.[0] as {
ctx?: { params?: Record<string, unknown> };
};
expect(call?.ctx?.params?.threadId).toBe("999");
});
});

View File

@@ -20,7 +20,7 @@ import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js
import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js";
import { extensionForMime } from "../../media/mime.js";
import { parseSlackTarget } from "../../slack/targets.js";
// parseTelegramTarget no longer used (telegram auto-threading uses string matching)
import { parseTelegramTarget } from "../../telegram/targets.js";
import {
isDeliverableMessageChannel,
normalizeMessageChannel,
@@ -250,6 +250,10 @@ function resolveSlackAutoThreadId(params: {
* the same chat the session originated from. Mirrors the Slack auto-threading
* pattern so media, buttons, and other tool-sent messages land in the correct
* topic instead of the General Topic.
*
* Unlike Slack, we do not gate on `replyToMode` here: Telegram forum topics
* are persistent sub-channels (not ephemeral reply threads), so auto-injection
* should always apply when the target chat matches.
*/
function resolveTelegramAutoThreadId(params: {
to: string;
@@ -259,12 +263,12 @@ function resolveTelegramAutoThreadId(params: {
if (!context?.currentThreadTs || !context.currentChannelId) {
return undefined;
}
// Only apply when the target matches the originating chat.
// Note: Telegram topic routing is carried via threadId/message_thread_id;
// `currentChannelId` (and most agent targets) are typically the base chat id.
const normalizedTo = params.to.trim().toLowerCase();
const normalizedChannel = context.currentChannelId.trim().toLowerCase();
if (normalizedTo !== normalizedChannel) {
// Use parseTelegramTarget to extract canonical chatId from both sides,
// mirroring how Slack uses parseSlackTarget. This handles format variations
// like `telegram:group:123:topic:456` vs `telegram:123`.
const parsedTo = parseTelegramTarget(params.to);
const parsedChannel = parseTelegramTarget(context.currentChannelId);
if (parsedTo.chatId.toLowerCase() !== parsedChannel.chatId.toLowerCase()) {
return undefined;
}
return context.currentThreadTs;
@@ -823,10 +827,11 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
channel === "telegram" && !threadId
? resolveTelegramAutoThreadId({ to, toolContext: input.toolContext })
: undefined;
const resolvedAutoThreadId = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
// Inject the resolved thread ID back into params so downstream dispatch (plugin/gateway) sees it.
if (resolvedAutoThreadId && !params.threadId) {
params.threadId = resolvedAutoThreadId;
const resolvedThreadId = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
// Write auto-resolved threadId back into params so downstream dispatch
// (plugin `readStringParam(params, "threadId")`) picks it up.
if (resolvedThreadId && !params.threadId) {
params.threadId = resolvedThreadId;
}
const outboundRoute =
agentId && !dryRun
@@ -838,7 +843,7 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
target: to,
resolvedTarget,
replyToId,
threadId: resolvedAutoThreadId,
threadId: resolvedThreadId,
})
: null;
if (outboundRoute && agentId && !dryRun) {