fix: preserve telegram exec approval topic routing

This commit is contained in:
Peter Steinberger
2026-04-01 13:33:56 +01:00
parent f55b6b1acf
commit ab3c646bb1
8 changed files with 152 additions and 9 deletions

View File

@@ -57,6 +57,31 @@ describe("telegram native approval adapter", () => {
});
});
it("parses topic-scoped turn-source targets in the extension", async () => {
const target = await telegramNativeApprovalAdapter.native?.resolveOriginTarget?.({
cfg: buildConfig(),
accountId: "default",
approvalKind: "exec",
request: {
id: "req-topic-1",
request: {
command: "echo hi",
turnSourceChannel: "telegram",
turnSourceTo: "telegram:-1003841603622:topic:928",
turnSourceAccountId: "default",
sessionKey: "agent:main:telegram:group:-1003841603622:topic:928",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
});
expect(target).toEqual({
to: "-1003841603622",
threadId: 928,
});
});
it("falls back to the session-bound origin target for plugin approvals", async () => {
writeStore({
"agent:main:telegram:group:-1003841603622:topic:928": {

View File

@@ -12,7 +12,7 @@ import {
isTelegramExecApprovalClientEnabled,
resolveTelegramExecApprovalTarget,
} from "./exec-approvals.js";
import { normalizeTelegramChatId } from "./targets.js";
import { normalizeTelegramChatId, parseTelegramTarget } from "./targets.js";
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
type TelegramOriginTarget = { to: string; threadId?: number };
@@ -22,15 +22,18 @@ function resolveTurnSourceTelegramOriginTarget(
): TelegramOriginTarget | null {
const turnSourceChannel = request.request.turnSourceChannel?.trim().toLowerCase() || "";
const rawTurnSourceTo = request.request.turnSourceTo?.trim() || "";
const turnSourceTo = normalizeTelegramChatId(rawTurnSourceTo) ?? rawTurnSourceTo;
const parsedTurnSourceTarget = rawTurnSourceTo ? parseTelegramTarget(rawTurnSourceTo) : null;
const turnSourceTo = normalizeTelegramChatId(parsedTurnSourceTarget?.chatId ?? rawTurnSourceTo);
if (turnSourceChannel !== "telegram" || !turnSourceTo) {
return null;
}
const rawThreadId =
request.request.turnSourceThreadId ?? parsedTurnSourceTarget?.messageThreadId ?? undefined;
const threadId =
typeof request.request.turnSourceThreadId === "number"
? request.request.turnSourceThreadId
: typeof request.request.turnSourceThreadId === "string"
? Number.parseInt(request.request.turnSourceThreadId, 10)
typeof rawThreadId === "number"
? rawThreadId
: typeof rawThreadId === "string"
? Number.parseInt(rawThreadId, 10)
: undefined;
return {
to: turnSourceTo,

View File

@@ -220,6 +220,42 @@ describe("telegramPlugin messaging", () => {
});
});
describe("telegramPlugin threading", () => {
it("keeps topic thread state in plugin-owned tool context", () => {
expect(
telegramPlugin.threading?.buildToolContext?.({
cfg: {} as OpenClawConfig,
accountId: "default",
context: {
To: "telegram:-1001:topic:77",
MessageThreadId: 77,
CurrentMessageId: "msg-1",
},
hasRepliedRef: { value: false },
}),
).toMatchObject({
currentChannelId: "telegram:-1001:topic:77",
currentThreadTs: "77",
});
});
it("parses topic thread state from target grammar when MessageThreadId is absent", () => {
expect(
telegramPlugin.threading?.buildToolContext?.({
cfg: {} as OpenClawConfig,
accountId: "default",
context: {
To: "telegram:-1001:topic:77",
CurrentMessageId: "msg-1",
},
}),
).toMatchObject({
currentChannelId: "telegram:-1001:topic:77",
currentThreadTs: "77",
});
});
});
describe("telegramPlugin duplicate token guard", () => {
it("marks secondary account as not configured when token is shared", async () => {
const cfg = createCfg();

View File

@@ -88,6 +88,7 @@ import {
setTelegramThreadBindingIdleTimeoutBySessionKey,
setTelegramThreadBindingMaxAgeBySessionKey,
} from "./thread-bindings.js";
import { buildTelegramThreadingToolContext } from "./threading-tool-context.js";
import { resolveTelegramToken } from "./token.js";
type TelegramSendFn = typeof sendMessageTelegram;
@@ -825,6 +826,7 @@ export const telegramPlugin = createChatChannelPlugin({
},
threading: {
topLevelReplyToMode: "telegram",
buildToolContext: (params) => buildTelegramThreadingToolContext(params),
resolveAutoThreadId: ({ to, toolContext, replyToId }) =>
replyToId ? undefined : resolveTelegramAutoThreadId({ to, toolContext }),
},

View File

@@ -0,0 +1,34 @@
import type {
ChannelThreadingContext,
ChannelThreadingToolContext,
} from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { parseTelegramTarget } from "./targets.js";
function resolveTelegramToolContextThreadId(context: ChannelThreadingContext): string | undefined {
if (context.MessageThreadId != null) {
return String(context.MessageThreadId);
}
const currentChannelId = context.To?.trim();
if (!currentChannelId) {
return undefined;
}
const parsedTarget = parseTelegramTarget(currentChannelId);
return parsedTarget.messageThreadId != null ? String(parsedTarget.messageThreadId) : undefined;
}
export function buildTelegramThreadingToolContext(params: {
cfg: OpenClawConfig;
accountId?: string | null;
context: ChannelThreadingContext;
hasRepliedRef?: { value: boolean };
}): ChannelThreadingToolContext {
void params.cfg;
void params.accountId;
return {
currentChannelId: params.context.To?.trim() || undefined,
currentThreadTs: resolveTelegramToolContextThreadId(params.context),
hasRepliedRef: params.hasRepliedRef,
};
}