fix: telegram topic auto-threading — use parseTelegramTarget, add tests (#7235) (thanks @Lukavyi)
parent
a13efbe2b5
commit
01db1dde1a
|
|
@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||||
|
|
||||||
### Changes
|
### 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.
|
- 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 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)
|
- Telegram: remove `@ts-nocheck` from `bot-message.ts`, type deps via `Omit<BuildTelegramMessageContextParams>`, widen `allMedia` to `TelegramMediaRef[]`. (#9180)
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,63 @@ describe("runMessageAction threading auto-injection", () => {
|
||||||
agentId: "main",
|
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");
|
expect(call?.ctx?.params?.threadId).toBe("42");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -178,7 +234,9 @@ describe("runMessageAction threading auto-injection", () => {
|
||||||
agentId: "main",
|
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");
|
expect(call?.ctx?.params?.threadId).toBe("999");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js
|
||||||
import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js";
|
import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js";
|
||||||
import { extensionForMime } from "../../media/mime.js";
|
import { extensionForMime } from "../../media/mime.js";
|
||||||
import { parseSlackTarget } from "../../slack/targets.js";
|
import { parseSlackTarget } from "../../slack/targets.js";
|
||||||
// parseTelegramTarget no longer used (telegram auto-threading uses string matching)
|
import { parseTelegramTarget } from "../../telegram/targets.js";
|
||||||
import {
|
import {
|
||||||
isDeliverableMessageChannel,
|
isDeliverableMessageChannel,
|
||||||
normalizeMessageChannel,
|
normalizeMessageChannel,
|
||||||
|
|
@ -250,6 +250,10 @@ function resolveSlackAutoThreadId(params: {
|
||||||
* the same chat the session originated from. Mirrors the Slack auto-threading
|
* 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
|
* pattern so media, buttons, and other tool-sent messages land in the correct
|
||||||
* topic instead of the General Topic.
|
* 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: {
|
function resolveTelegramAutoThreadId(params: {
|
||||||
to: string;
|
to: string;
|
||||||
|
|
@ -259,12 +263,12 @@ function resolveTelegramAutoThreadId(params: {
|
||||||
if (!context?.currentThreadTs || !context.currentChannelId) {
|
if (!context?.currentThreadTs || !context.currentChannelId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
// Only apply when the target matches the originating chat.
|
// Use parseTelegramTarget to extract canonical chatId from both sides,
|
||||||
// Note: Telegram topic routing is carried via threadId/message_thread_id;
|
// mirroring how Slack uses parseSlackTarget. This handles format variations
|
||||||
// `currentChannelId` (and most agent targets) are typically the base chat id.
|
// like `telegram:group:123:topic:456` vs `telegram:123`.
|
||||||
const normalizedTo = params.to.trim().toLowerCase();
|
const parsedTo = parseTelegramTarget(params.to);
|
||||||
const normalizedChannel = context.currentChannelId.trim().toLowerCase();
|
const parsedChannel = parseTelegramTarget(context.currentChannelId);
|
||||||
if (normalizedTo !== normalizedChannel) {
|
if (parsedTo.chatId.toLowerCase() !== parsedChannel.chatId.toLowerCase()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return context.currentThreadTs;
|
return context.currentThreadTs;
|
||||||
|
|
@ -823,10 +827,11 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||||
channel === "telegram" && !threadId
|
channel === "telegram" && !threadId
|
||||||
? resolveTelegramAutoThreadId({ to, toolContext: input.toolContext })
|
? resolveTelegramAutoThreadId({ to, toolContext: input.toolContext })
|
||||||
: undefined;
|
: undefined;
|
||||||
const resolvedAutoThreadId = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
|
const resolvedThreadId = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
|
||||||
// Inject the resolved thread ID back into params so downstream dispatch (plugin/gateway) sees it.
|
// Write auto-resolved threadId back into params so downstream dispatch
|
||||||
if (resolvedAutoThreadId && !params.threadId) {
|
// (plugin `readStringParam(params, "threadId")`) picks it up.
|
||||||
params.threadId = resolvedAutoThreadId;
|
if (resolvedThreadId && !params.threadId) {
|
||||||
|
params.threadId = resolvedThreadId;
|
||||||
}
|
}
|
||||||
const outboundRoute =
|
const outboundRoute =
|
||||||
agentId && !dryRun
|
agentId && !dryRun
|
||||||
|
|
@ -838,7 +843,7 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||||
target: to,
|
target: to,
|
||||||
resolvedTarget,
|
resolvedTarget,
|
||||||
replyToId,
|
replyToId,
|
||||||
threadId: resolvedAutoThreadId,
|
threadId: resolvedThreadId,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
if (outboundRoute && agentId && !dryRun) {
|
if (outboundRoute && agentId && !dryRun) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue