Files
openclaw/extensions/telegram/src/channel-actions.ts
Bakhtier Sizhaev a0cb443aa3 fix: document Telegram asDocument alias (#52461) (thanks @bakhtiersizhaev)
* feat(telegram): add asDocument param to message tool

Adds `asDocument` as a user-facing alias for the existing `forceDocument`
parameter in the message tool. When set to `true`, media files (images,
videos, GIFs) are sent via `sendDocument` instead of `sendPhoto`/
`sendVideo`/`sendAnimation`, preserving the original file quality
without Telegram compression.

This is useful when agents need to deliver high-resolution images or
uncompressed files to users via Telegram.

`asDocument` is intentionally an alias rather than a replacement — the
existing `forceDocument` continues to work unchanged.

Changes:
- src/agents/tools/message-tool.ts: add asDocument to send schema
- src/agents/tools/telegram-actions.ts: OR asDocument into forceDocument
- src/infra/outbound/message-action-runner.ts: same OR logic for outbound path
- extensions/telegram/src/channel-actions.ts: read and forward asDocument
- src/channels/plugins/actions/actions.test.ts: add test case

* fix: restore channel-actions.ts to main version (rebase conflict fix)

* fix(test): match asDocument test payload to actual params structure

* fix(telegram): preserve forceDocument alias semantics

* fix: document Telegram asDocument alias (#52461) (thanks @bakhtiersizhaev)

---------

Co-authored-by: Бахтиер Сижаев <bkh@MacBook-Air.local>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-03-23 17:32:46 +05:30

157 lines
4.7 KiB
TypeScript

import { Type } from "@sinclair/typebox";
import {
createUnionActionGate,
listTokenSourcedAccounts,
resolveReactionMessageId,
} from "openclaw/plugin-sdk/channel-actions";
import { createMessageToolButtonsSchema } from "openclaw/plugin-sdk/channel-actions";
import type {
ChannelMessageActionAdapter,
ChannelMessageActionName,
ChannelMessageToolDiscovery,
ChannelMessageToolSchemaContribution,
} from "openclaw/plugin-sdk/channel-contract";
import type { TelegramActionConfig } from "openclaw/plugin-sdk/config-runtime";
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
import {
createTelegramActionGate,
listEnabledTelegramAccounts,
resolveTelegramPollActionGateState,
} from "./accounts.js";
import { handleTelegramAction } from "./action-runtime.js";
import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js";
import { createTelegramPollExtraToolSchemas } from "./message-tool-schema.js";
export const telegramMessageActionRuntime = {
handleTelegramAction,
};
const TELEGRAM_MESSAGE_ACTION_MAP = {
delete: "deleteMessage",
edit: "editMessage",
poll: "poll",
react: "react",
send: "sendMessage",
sticker: "sendSticker",
"sticker-search": "searchSticker",
"topic-create": "createForumTopic",
"topic-edit": "editForumTopic",
} as const satisfies Partial<Record<ChannelMessageActionName, string>>;
function resolveTelegramMessageActionName(action: ChannelMessageActionName) {
return TELEGRAM_MESSAGE_ACTION_MAP[action as keyof typeof TELEGRAM_MESSAGE_ACTION_MAP];
}
function resolveTelegramActionDiscovery(cfg: Parameters<typeof listEnabledTelegramAccounts>[0]) {
const accounts = listTokenSourcedAccounts(listEnabledTelegramAccounts(cfg));
if (accounts.length === 0) {
return null;
}
const unionGate = createUnionActionGate(accounts, (account) =>
createTelegramActionGate({
cfg,
accountId: account.accountId,
}),
);
const pollEnabled = accounts.some((account) => {
const accountGate = createTelegramActionGate({
cfg,
accountId: account.accountId,
});
return resolveTelegramPollActionGateState(accountGate).enabled;
});
const buttonsEnabled = accounts.some((account) =>
isTelegramInlineButtonsEnabled({ cfg, accountId: account.accountId }),
);
return {
isEnabled: (key: keyof TelegramActionConfig, defaultValue = true) =>
unionGate(key, defaultValue),
pollEnabled,
buttonsEnabled,
};
}
function describeTelegramMessageTool({
cfg,
}: Parameters<
NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>
>[0]): ChannelMessageToolDiscovery {
const discovery = resolveTelegramActionDiscovery(cfg);
if (!discovery) {
return {
actions: [],
capabilities: [],
schema: null,
};
}
const actions = new Set<ChannelMessageActionName>(["send"]);
if (discovery.pollEnabled) {
actions.add("poll");
}
if (discovery.isEnabled("reactions")) {
actions.add("react");
}
if (discovery.isEnabled("deleteMessage")) {
actions.add("delete");
}
if (discovery.isEnabled("editMessage")) {
actions.add("edit");
}
if (discovery.isEnabled("sticker", false)) {
actions.add("sticker");
actions.add("sticker-search");
}
if (discovery.isEnabled("createForumTopic")) {
actions.add("topic-create");
}
if (discovery.isEnabled("editForumTopic")) {
actions.add("topic-edit");
}
const schema: ChannelMessageToolSchemaContribution[] = [];
if (discovery.buttonsEnabled) {
schema.push({
properties: {
buttons: createMessageToolButtonsSchema(),
},
});
}
if (discovery.pollEnabled) {
schema.push({
properties: createTelegramPollExtraToolSchemas(),
visibility: "all-configured",
});
}
return {
actions: Array.from(actions),
capabilities: discovery.buttonsEnabled ? ["interactive", "buttons"] : [],
schema,
};
}
export const telegramMessageActions: ChannelMessageActionAdapter = {
describeMessageTool: describeTelegramMessageTool,
extractToolSend: ({ args }) => {
return extractToolSend(args, "sendMessage");
},
handleAction: async ({ action, params, cfg, accountId, mediaLocalRoots, toolContext }) => {
const telegramAction = resolveTelegramMessageActionName(action);
if (!telegramAction) {
throw new Error(`Unsupported Telegram action: ${action}`);
}
return await telegramMessageActionRuntime.handleTelegramAction(
{
...params,
action: telegramAction,
accountId: accountId ?? undefined,
...(action === "react"
? {
messageId: resolveReactionMessageId({ args: params, toolContext }),
}
: {}),
},
cfg,
{ mediaLocalRoots },
);
},
};