diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d3a5c2f79..e53cf4552a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,12 @@ Docs: https://docs.openclaw.ai - Gateway/connect: omit admin-scoped config and auth metadata from lower-privilege `hello-ok` snapshots while preserving those fields for admin reconnects. (#58469) Thanks @eleqtrizit. - iOS/canvas: restrict A2UI bridge trust to the bundled scaffold and exact capability-backed remote canvas URLs, so generic `canvas.navigate` and `canvas.present` loads no longer gain action-dispatch authority. (#58471) Thanks @eleqtrizit. - Agents/tool policy: preserve restrictive plugin-only allowlists instead of silently widening access to core tools, and keep allowlist warnings aligned with the enforced policy. (#58476) Thanks @eleqtrizit. +- Telegram/native commands: clean up metadata-driven progress placeholders when replies fall back, edits fail, or local exec approval prompts are suppressed. (#59300) Thanks @jalehman. +- Matrix: allow secret-storage recreation during automatic repair bootstrap so clients that lose their recovery key can recover and persist new cross-signing keys. (#59846) Thanks @al3mart. +- Matrix/crypto persistence: capture and write the IndexedDB snapshot while holding the snapshot file lock so concurrent gateway and CLI persists cannot overwrite newer crypto state. (#59851) Thanks @al3mart. +- Telegram/media: keep inbound image attachments readable on upgraded installs where legacy state roots still differ from the managed config-dir media cache. (#59971) Thanks @neeravmakwana. +- Telegram/local Bot API: thread `channels.telegram.apiRoot` through buffered reply-media and album downloads so self-hosted Bot API file paths stop falling back to `api.telegram.org` and 404ing. (#59544) Thanks @SARAMALI15792. +- Telegram/replies: preserve explicit topic targets when `replyTo` is present while still inheriting the current topic for same-chat replies without an explicit topic. (#59634) Thanks @dashhuang. - Hooks/session_end: preserve deterministic reason metadata for custom reset aliases and overlapping idle-plus-daily rollovers so plugins can rely on lifecycle reason reporting. (#59715) Thanks @jalehman. - Tools/image generation: stop inferring unsupported resolution overrides for OpenAI reference-image edits when no explicit `size` or `resolution` is provided, so default edit flows no longer fail before the provider request is sent. - Agents/sessions: release embedded runner session locks even when teardown cleanup throws, so timed-out or failed cleanup paths no longer leave sessions wedged until the stale-lock watchdog recovers them. (#59194) Thanks @samzong. @@ -72,6 +78,7 @@ Docs: https://docs.openclaw.ai - Allowlist/commands: require owner access for `/allowlist add` and `/allowlist remove` so command-authorized non-owners cannot mutate persisted allowlists. (#59836) Thanks @eleqtrizit. - Control UI/skills: clear stale ClawHub results immediately when the search query changes, so debounced searches cannot keep outdated install targets visible. Related #60134. - Discord/ack reactions: keep automatic ACK reaction auth on the active hydrated Discord account so SecretRef-backed and non-default-account reactions stop falling back to stale default config resolution. (#60081) Thanks @FunJim. +- Telegram/model switching: render non-default `/model` callback confirmations with HTML formatting so Telegram shows the selected model in bold instead of raw `**...**` markers. (#60042) Thanks @GitZhangChi. ## 2026.4.2 diff --git a/extensions/telegram/src/bot-handlers.runtime.ts b/extensions/telegram/src/bot-handlers.runtime.ts index b09915f6e3c..200e48aabc3 100644 --- a/extensions/telegram/src/bot-handlers.runtime.ts +++ b/extensions/telegram/src/bot-handlers.runtime.ts @@ -1445,17 +1445,22 @@ export const registerTelegramHandlers = ({ const editMessageWithButtons = async ( text: string, buttons: ReturnType, + extra?: { parse_mode?: "HTML" | "Markdown" | "MarkdownV2" }, ) => { const keyboard = buildInlineKeyboard(buttons); + const editParams = keyboard ? { reply_markup: keyboard, ...extra } : extra; try { - await editCallbackMessage(text, keyboard ? { reply_markup: keyboard } : undefined); + await editCallbackMessage(text, editParams); } catch (editErr) { const errStr = String(editErr); if (errStr.includes("no text in the message")) { try { await deleteCallbackMessage(); } catch {} - await replyToCallbackChat(text, keyboard ? { reply_markup: keyboard } : undefined); + await replyToCallbackChat( + text, + keyboard ? { reply_markup: keyboard, ...extra } : extra, + ); } else if (!errStr.includes("message is not modified")) { throw editErr; } @@ -1585,12 +1590,15 @@ export const registerTelegramHandlers = ({ }); // Update message to show success with visual feedback + const escapeHtml = (text: string) => + text.replace(/&/g, "&").replace(//g, ">"); const actionText = isDefaultSelection ? "reset to default" - : `changed to **${selection.provider}/${selection.model}**`; + : `changed to ${escapeHtml(selection.provider)}/${escapeHtml(selection.model)}`; await editMessageWithButtons( `✅ Model ${actionText}\n\nThis model will be used for your next message.`, [], // Empty buttons = remove inline keyboard + { parse_mode: "HTML" }, ); } catch (err) { await editMessageWithButtons(`❌ Failed to change model: ${String(err)}`, []); diff --git a/extensions/telegram/src/bot.test.ts b/extensions/telegram/src/bot.test.ts index dba51905cff..e480b4dede8 100644 --- a/extensions/telegram/src/bot.test.ts +++ b/extensions/telegram/src/bot.test.ts @@ -965,6 +965,79 @@ describe("createTelegramBot", () => { } }); + it("formats non-default model selection confirmations with Telegram HTML parse mode", async () => { + onSpy.mockClear(); + replySpy.mockClear(); + editMessageTextSpy.mockClear(); + + const storePath = `/tmp/openclaw-telegram-model-html-${process.pid}-${Date.now()}.json`; + + await rm(storePath, { force: true }); + try { + const config = { + agents: { + defaults: { + model: "anthropic/claude-opus-4-6", + models: { + "anthropic/claude-opus-4-6": {}, + "openai/gpt-5.4": {}, + }, + }, + }, + channels: { + telegram: { + dmPolicy: "open", + allowFrom: ["*"], + }, + }, + session: { + store: storePath, + }, + } satisfies NonNullable[0]["config"]>; + + loadConfig.mockReturnValue(config); + createTelegramBot({ + token: "tok", + config, + }); + const callbackHandler = onSpy.mock.calls.find( + (call) => call[0] === "callback_query", + )?.[1] as (ctx: Record) => Promise; + expect(callbackHandler).toBeDefined(); + + await callbackHandler({ + callbackQuery: { + id: "cbq-model-html-1", + data: "mdl_sel_openai/gpt-5.4", + from: { id: 9, first_name: "Ada", username: "ada_bot" }, + message: { + chat: { id: 1234, type: "private" }, + date: 1736380800, + message_id: 17, + }, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(replySpy).not.toHaveBeenCalled(); + expect(editMessageTextSpy).toHaveBeenCalledTimes(1); + expect(editMessageTextSpy).toHaveBeenCalledWith( + 1234, + 17, + `${CHECK_MARK_EMOJI} Model changed to openai/gpt-5.4\n\nThis model will be used for your next message.`, + expect.objectContaining({ parse_mode: "HTML" }), + ); + + const entry = Object.values(loadSessionStore(storePath, { skipCache: true }))[0]; + expect(entry?.providerOverride).toBe("openai"); + expect(entry?.modelOverride).toBe("gpt-5.4"); + expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-model-html-1"); + } finally { + await rm(storePath, { force: true }); + } + }); + it("rejects ambiguous compact model callbacks and returns provider list", async () => { onSpy.mockClear(); replySpy.mockClear();