From 4bb8a65edd52aa2a277ad1370ea1a1e0ea749d9c Mon Sep 17 00:00:00 2001 From: Frank Yang Date: Sun, 15 Mar 2026 17:23:53 +0800 Subject: [PATCH] fix: forward forceDocument through sendPayload path (follow-up to #45111) (#47119) Merged via squash. Prepared head SHA: d791190f8303c664cea8737046eb653c0514e939 Co-authored-by: thepagent <262003297+thepagent@users.noreply.github.com> Reviewed-by: @frankekn --- CHANGELOG.md | 1 + extensions/telegram/src/channel.test.ts | 27 ++++++++++++++++ extensions/telegram/src/channel.ts | 4 +++ extensions/telegram/src/outbound-adapter.ts | 2 ++ src/infra/outbound/deliver.ts | 1 + src/tts/tts.test.ts | 36 ++++++++++----------- 6 files changed, 53 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 965d13eb4d8..baa3c2f687e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai - Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark. - Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc. - Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411) +- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent. ## 2026.3.13 diff --git a/extensions/telegram/src/channel.test.ts b/extensions/telegram/src/channel.test.ts index a957a3e5b1c..965a66d0f2c 100644 --- a/extensions/telegram/src/channel.test.ts +++ b/extensions/telegram/src/channel.test.ts @@ -403,3 +403,30 @@ describe("telegramPlugin duplicate token guard", () => { ); }); }); + +describe("telegramPlugin outbound sendPayload forceDocument", () => { + it("forwards forceDocument to the underlying send call when channelData is present", async () => { + const sendMessageTelegram = installSendMessageRuntime( + vi.fn(async () => ({ messageId: "tg-fd" })), + ); + + await telegramPlugin.outbound!.sendPayload!({ + cfg: createCfg(), + to: "12345", + text: "", + payload: { + text: "here is an image", + mediaUrls: ["https://example.com/photo.png"], + channelData: { telegram: {} }, + }, + accountId: "ops", + forceDocument: true, + }); + + expect(sendMessageTelegram).toHaveBeenCalledWith( + "12345", + expect.any(String), + expect.objectContaining({ forceDocument: true }), + ); + }); +}); diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 50509e51fca..a8745591db3 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -96,6 +96,7 @@ function buildTelegramSendOptions(params: { replyToId?: string | null; threadId?: string | number | null; silent?: boolean | null; + forceDocument?: boolean | null; }): TelegramSendOptions { return { verbose: false, @@ -106,6 +107,7 @@ function buildTelegramSendOptions(params: { replyToMessageId: parseTelegramReplyToMessageId(params.replyToId), accountId: params.accountId ?? undefined, silent: params.silent ?? undefined, + forceDocument: params.forceDocument ?? undefined, }; } @@ -386,6 +388,7 @@ export const telegramPlugin: ChannelPlugin { const send = resolveOutboundSendDep(deps, "telegram") ?? @@ -401,6 +404,7 @@ export const telegramPlugin: ChannelPlugin { const { send, baseOpts } = resolveTelegramSendContext({ cfg, @@ -156,6 +157,7 @@ export const telegramOutbound: ChannelOutboundAdapter = { baseOpts: { ...baseOpts, mediaLocalRoots, + forceDocument: forceDocument ?? false, }, }); return { channel: "telegram", ...result }; diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index 7932cae2968..509ff278a1d 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -695,6 +695,7 @@ async function deliverOutboundPayloadsCore( const sendOverrides = { replyToId: effectivePayload.replyToId ?? params.replyToId ?? undefined, threadId: params.threadId ?? undefined, + forceDocument: params.forceDocument, }; if (handler.sendPayload && effectivePayload.channelData) { const delivery = await handler.sendPayload(effectivePayload, sendOverrides); diff --git a/src/tts/tts.test.ts b/src/tts/tts.test.ts index b326b4835e5..8b232ed034d 100644 --- a/src/tts/tts.test.ts +++ b/src/tts/tts.test.ts @@ -2,7 +2,7 @@ import { completeSimple, type AssistantMessage } from "@mariozechner/pi-ai"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { ensureCustomApiRegistered } from "../agents/custom-api-registry.js"; import { getApiKeyForModel } from "../agents/model-auth.js"; -import { resolveModel } from "../agents/pi-embedded-runner/model.js"; +import { resolveModelAsync } from "../agents/pi-embedded-runner/model.js"; import type { OpenClawConfig } from "../config/config.js"; import { withEnv } from "../test-utils/env.js"; import * as tts from "./tts.js"; @@ -20,13 +20,13 @@ vi.mock("@mariozechner/pi-ai/oauth", () => ({ getOAuthApiKey: vi.fn(async () => null), })); -vi.mock("../agents/pi-embedded-runner/model.js", () => ({ - resolveModel: vi.fn((provider: string, modelId: string) => ({ +function createResolvedModel(provider: string, modelId: string, api = "openai-completions") { + return { model: { provider, id: modelId, name: modelId, - api: "openai-completions", + api, reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, @@ -35,7 +35,16 @@ vi.mock("../agents/pi-embedded-runner/model.js", () => ({ }, authStorage: { profiles: {} }, modelRegistry: { find: vi.fn() }, - })), + }; +} + +vi.mock("../agents/pi-embedded-runner/model.js", () => ({ + resolveModel: vi.fn((provider: string, modelId: string) => + createResolvedModel(provider, modelId), + ), + resolveModelAsync: vi.fn(async (provider: string, modelId: string) => + createResolvedModel(provider, modelId), + ), })); vi.mock("../agents/model-auth.js", () => ({ @@ -411,25 +420,16 @@ describe("tts", () => { timeoutMs: 30_000, }); - expect(resolveModel).toHaveBeenCalledWith("openai", "gpt-4.1-mini", undefined, cfg); + expect(resolveModelAsync).toHaveBeenCalledWith("openai", "gpt-4.1-mini", undefined, cfg); }); it("registers the Ollama api before direct summarization", async () => { - vi.mocked(resolveModel).mockReturnValue({ + vi.mocked(resolveModelAsync).mockResolvedValue({ + ...createResolvedModel("ollama", "qwen3:8b", "ollama"), model: { - provider: "ollama", - id: "qwen3:8b", - name: "qwen3:8b", - api: "ollama", + ...createResolvedModel("ollama", "qwen3:8b", "ollama").model, baseUrl: "http://127.0.0.1:11434", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 8192, }, - authStorage: { profiles: {} } as never, - modelRegistry: { find: vi.fn() } as never, } as never); await summarizeText({