mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
Fix Telegram presentation-only payload sends (#82449)
* fix telegram presentation payload fallback * changelog telegram presentation payload fallback * fix telegram presentation reply delivery
This commit is contained in:
@@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/embeddings: reject malformed successful OpenAI-compatible, Google Gemini, and Amazon Bedrock embedding responses instead of silently returning empty or coerced vectors.
|
||||
- Providers/catalogs: reject malformed successful LM Studio, GitHub Copilot, DeepInfra, Vercel AI Gateway, and Kilocode model-list responses with provider-owned errors instead of raw parser/type failures or silent fallback catalogs.
|
||||
- Providers/polling: reject array, null, or scalar successful operation status responses with provider-owned malformed JSON errors instead of waiting until timeout.
|
||||
- Telegram: send presentation-only payloads by rendering fallback text and inline buttons instead of treating them as empty. Fixes #82404. (#82449) Thanks @joshavant.
|
||||
- Trajectory export: skip and report malformed session/runtime JSONL rows in `manifest.json` instead of letting wrong-shaped session rows crash support bundle export.
|
||||
- Voice calls: persist rejected inbound-call replay keys so duplicate carrier webhook retries stay ignored after a Gateway restart.
|
||||
- Config/doctor: copy fallback-enabled channel `allowFrom` entries into explicit `groupAllowFrom` allowlists during `openclaw doctor --fix`, preserving current group access without adding runtime fallback-transition flags.
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
toPluginMessageSentEvent,
|
||||
} from "openclaw/plugin-sdk/hook-runtime";
|
||||
import type { ReplyPayloadDelivery } from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import {
|
||||
normalizeMessagePresentation,
|
||||
presentationToInteractiveReply,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import {
|
||||
buildOutboundMediaLoadOptions,
|
||||
isGifMedia,
|
||||
@@ -755,10 +759,15 @@ export async function deliverReplies(params: {
|
||||
? [reply.mediaUrl]
|
||||
: [];
|
||||
const hasMedia = mediaList.length > 0;
|
||||
const presentation = normalizeMessagePresentation(reply?.presentation);
|
||||
const interactive =
|
||||
reply?.interactive ??
|
||||
(presentation ? presentationToInteractiveReply(presentation) : undefined);
|
||||
const resolvedReplyText =
|
||||
resolveTelegramInteractiveTextFallback({
|
||||
text: reply?.text,
|
||||
interactive: reply?.interactive,
|
||||
interactive,
|
||||
presentation,
|
||||
}) ??
|
||||
reply?.text ??
|
||||
"";
|
||||
@@ -820,7 +829,7 @@ export async function deliverReplies(params: {
|
||||
const replyMarkup = buildInlineKeyboard(
|
||||
resolveTelegramInlineButtons({
|
||||
buttons: telegramData?.buttons,
|
||||
interactive: reply.interactive,
|
||||
interactive,
|
||||
}),
|
||||
);
|
||||
let firstDeliveredMessageId: number | undefined;
|
||||
|
||||
@@ -328,6 +328,33 @@ describe("deliverReplies", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses presentation button labels as fallback text for presentation-only replies", async () => {
|
||||
const runtime = createRuntime(false);
|
||||
const sendMessage = vi.fn().mockResolvedValue({ message_id: 4, chat: { id: "123" } });
|
||||
const bot = createBot({ sendMessage });
|
||||
|
||||
await deliverWith({
|
||||
replies: [
|
||||
{
|
||||
presentation: {
|
||||
blocks: [{ type: "buttons", buttons: [{ label: "Retry", value: "cmd:retry" }] }],
|
||||
},
|
||||
},
|
||||
],
|
||||
runtime,
|
||||
bot,
|
||||
});
|
||||
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
expect(firstMockCallArg(sendMessage, 0)).toBe("123");
|
||||
expect(firstMockCallArg(sendMessage, 1)).toContain("Retry");
|
||||
expectRecordFields(mockCallArg(sendMessage, 0, 2), {
|
||||
reply_markup: {
|
||||
inline_keyboard: [[{ text: "Retry", callback_data: "cmd:retry" }]],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reports message_sent success=false when hooks blank out a text-only reply", async () => {
|
||||
messageHookRunner.hasHooks.mockImplementation(
|
||||
(name: string) => name === "message_sending" || name === "message_sent",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
interactiveReplyToPresentation,
|
||||
normalizeMessagePresentation,
|
||||
normalizeInteractiveReply,
|
||||
renderMessagePresentationFallbackText,
|
||||
resolveInteractiveTextFallback,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
export function resolveTelegramInteractiveTextFallback(params: {
|
||||
text?: string | null;
|
||||
interactive?: unknown;
|
||||
presentation?: unknown;
|
||||
}): string | undefined {
|
||||
const interactive = normalizeInteractiveReply(params.interactive);
|
||||
const text = resolveInteractiveTextFallback({
|
||||
@@ -17,13 +19,23 @@ export function resolveTelegramInteractiveTextFallback(params: {
|
||||
if (text?.trim()) {
|
||||
return text;
|
||||
}
|
||||
const presentation = normalizeMessagePresentation(params.presentation);
|
||||
if (presentation) {
|
||||
const fallback = renderMessagePresentationFallbackText({
|
||||
text: params.text ?? undefined,
|
||||
presentation,
|
||||
});
|
||||
if (fallback.trim()) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
if (!interactive) {
|
||||
return text;
|
||||
}
|
||||
const presentation = interactiveReplyToPresentation(interactive);
|
||||
if (!presentation) {
|
||||
const interactivePresentation = interactiveReplyToPresentation(interactive);
|
||||
if (!interactivePresentation) {
|
||||
return text;
|
||||
}
|
||||
const fallback = renderMessagePresentationFallbackText({ presentation });
|
||||
const fallback = renderMessagePresentationFallbackText({ presentation: interactivePresentation });
|
||||
return fallback.trim() ? fallback : text;
|
||||
}
|
||||
|
||||
@@ -150,6 +150,33 @@ describe("telegramOutbound", () => {
|
||||
expect(result).toEqual({ channel: "telegram", messageId: "tg-buttons", chatId: "12345" });
|
||||
});
|
||||
|
||||
it("uses presentation button labels as fallback text for presentation-only payloads", async () => {
|
||||
sendMessageTelegramMock.mockResolvedValueOnce({
|
||||
messageId: "tg-presentation-buttons",
|
||||
chatId: "12345",
|
||||
});
|
||||
|
||||
const result = await telegramOutbound.sendPayload!({
|
||||
cfg: {} as never,
|
||||
to: "12345",
|
||||
text: "",
|
||||
payload: {
|
||||
presentation: {
|
||||
blocks: [{ type: "buttons", buttons: [{ label: "Retry", value: "cmd:retry" }] }],
|
||||
},
|
||||
},
|
||||
deps: { sendTelegram: sendMessageTelegramMock },
|
||||
});
|
||||
|
||||
const options = callOptionsAt(sendMessageTelegramMock, 0, "12345", "- Retry");
|
||||
expect(options.buttons).toEqual([[{ text: "Retry", callback_data: "cmd:retry" }]]);
|
||||
expect(result).toEqual({
|
||||
channel: "telegram",
|
||||
messageId: "tg-presentation-buttons",
|
||||
chatId: "12345",
|
||||
});
|
||||
});
|
||||
|
||||
it("renders presentation web app buttons for payload sends", async () => {
|
||||
sendMessageTelegramMock.mockResolvedValueOnce({ messageId: "tg-web-app", chatId: "12345" });
|
||||
const presentation = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createAttachedChannelResultAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-send-result";
|
||||
import {
|
||||
normalizeMessagePresentation,
|
||||
presentationToInteractiveReply,
|
||||
renderMessagePresentationFallbackText,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
@@ -116,15 +117,20 @@ export async function sendTelegramPayloadMessages(params: {
|
||||
| undefined;
|
||||
const quoteText =
|
||||
typeof telegramData?.quoteText === "string" ? telegramData.quoteText : undefined;
|
||||
const presentation = normalizeMessagePresentation(params.payload.presentation);
|
||||
const interactive =
|
||||
params.payload.interactive ??
|
||||
(presentation ? presentationToInteractiveReply(presentation) : undefined);
|
||||
const text =
|
||||
resolveTelegramInteractiveTextFallback({
|
||||
text: params.payload.text,
|
||||
interactive: params.payload.interactive,
|
||||
interactive,
|
||||
presentation,
|
||||
}) ?? "";
|
||||
const mediaUrls = resolvePayloadMediaUrls(params.payload);
|
||||
const buttons = resolveTelegramInlineButtons({
|
||||
buttons: telegramData?.buttons,
|
||||
interactive: params.payload.interactive,
|
||||
interactive,
|
||||
});
|
||||
const payloadOpts = {
|
||||
...params.baseOpts,
|
||||
|
||||
Reference in New Issue
Block a user