mirror of https://github.com/openclaw/openclaw.git
fix(telegram): enable HTML formatting for model switch messages (#60042)
* fix(telegram): enable HTML formatting for model switch messages The model switch confirmation message was displaying raw Markdown (**text**) instead of bold formatting because parse_mode was not set. Changes: - Add optional extra parameter to editMessageWithButtons for parse_mode - Change format from Markdown ** to HTML <b> tags - Pass parse_mode: 'HTML' when editing model switch message Fixes the issue where model names appeared as **provider/model** instead of bold text in Telegram. * fix(telegram): escape HTML entities in model switch confirmation Add defensive `escapeHtml` helper to sanitize `selection.provider` and `selection.model` before interpolating them into the HTML callback message. This prevents potential API rejection (HTTP 400) if future provider or model names contain `<`, `>`, or `&`. Addresses review feedback on unescaped HTML interpolation. * test(telegram): cover HTML model switch confirmation --------- Co-authored-by: Frank Yang <frank.ekn@gmail.com>
This commit is contained in:
parent
7c738ad036
commit
33e6a6724d
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1445,17 +1445,22 @@ export const registerTelegramHandlers = ({
|
|||
const editMessageWithButtons = async (
|
||||
text: string,
|
||||
buttons: ReturnType<typeof buildProviderKeyboard>,
|
||||
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, "<").replace(/>/g, ">");
|
||||
const actionText = isDefaultSelection
|
||||
? "reset to default"
|
||||
: `changed to **${selection.provider}/${selection.model}**`;
|
||||
: `changed to <b>${escapeHtml(selection.provider)}/${escapeHtml(selection.model)}</b>`;
|
||||
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)}`, []);
|
||||
|
|
|
|||
|
|
@ -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<Parameters<typeof createTelegramBot>[0]["config"]>;
|
||||
|
||||
loadConfig.mockReturnValue(config);
|
||||
createTelegramBot({
|
||||
token: "tok",
|
||||
config,
|
||||
});
|
||||
const callbackHandler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "callback_query",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
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 <b>openai/gpt-5.4</b>\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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue