Compare commits

...

11 Commits

8 changed files with 130 additions and 4 deletions

View File

@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai
- Agents/compaction: preserve safeguard compaction summary language continuity via default and configurable custom instructions so persona drift is reduced after auto-compaction. (#10456) Thanks @keepitmello.
- Agents/tool warnings: distinguish gated core tools like `apply_patch` from plugin-only unknown entries in `tools.profile` warnings, so unavailable core tools now report current runtime/provider/model/config gating instead of suggesting a missing plugin.
- Slack/probe: keep `auth.test()` bot and team metadata mapping stable while simplifying the probe result path. (#44775) Thanks @Cafexss.
- Telegram/native commands: preserve native command routing for `/fast` option-button callbacks even when text commands are disabled, so `/fast status|on|off` no longer falls through into normal model runs. Thanks @vincentkoc.
- Dashboard/chat UI: restore the `chat-new-messages` class on the New messages scroll pill so the button uses its existing compact styling instead of rendering as a full-screen SVG overlay. (#44856) Thanks @Astro-Han.
## 2026.3.12

View File

@ -172,6 +172,22 @@ describe("runPreparedReply media-only handling", () => {
expect(call?.followupRun.prompt).toContain("[User sent media without caption]");
});
it("passes Anthropic fast-mode session state into runReplyAgent", async () => {
await runPreparedReply(
baseParams({
sessionEntry: {
fastMode: true,
} as never,
}),
);
const call = vi.mocked(runReplyAgent).mock.calls[0]?.[0];
expect(call).toBeTruthy();
expect(call?.followupRun.run.fastMode).toBe(true);
expect(call?.followupRun.run.provider).toBe("anthropic");
expect(call?.followupRun.run.model).toBe("claude-opus-4-1");
});
it("keeps thread history context on follow-up turns", async () => {
const result = await runPreparedReply(
baseParams({

View File

@ -43,7 +43,10 @@ import {
type NormalizedAllowFrom,
} from "./bot-access.js";
import type { TelegramMediaRef } from "./bot-message-context.js";
import { RegisterTelegramHandlerParams } from "./bot-native-commands.js";
import {
parseTelegramNativeCommandCallbackData,
RegisterTelegramHandlerParams,
} from "./bot-native-commands.js";
import {
MEDIA_GROUP_TIMEOUT_MS,
type MediaGroupEntry,
@ -1437,14 +1440,16 @@ export const registerTelegramHandlers = ({
return;
}
const nativeCommandText = parseTelegramNativeCommandCallbackData(data);
const syntheticMessage = buildSyntheticTextMessage({
base: callbackMessage,
from: callback.from,
text: data,
text: nativeCommandText ?? data,
});
await processMessage(buildSyntheticContext(ctx, syntheticMessage), [], storeAllowFrom, {
forceWasMentioned: true,
messageIdOverride: callback.id,
commandSource: nativeCommandText ? "native" : undefined,
});
} catch (err) {
runtime.error?.(danger(`callback handler failed: ${String(err)}`));

View File

@ -240,6 +240,7 @@ export async function buildTelegramInboundContextPayload(params: {
StickerMediaIncluded: allMedia[0]?.stickerMetadata ? !stickerCacheHit : undefined,
...(locationData ? toLocationContext(locationData) : undefined),
CommandAuthorized: commandAuthorized,
CommandSource: options?.commandSource,
MessageThreadId: threadSpec.id,
IsForum: isForum,
OriginatingChannel: "telegram" as const,

View File

@ -18,6 +18,7 @@ export type TelegramMediaRef = {
export type TelegramMessageContextOptions = {
forceWasMentioned?: boolean;
messageIdOverride?: string;
commandSource?: "text" | "native";
};
export type TelegramLogger = {

View File

@ -5,7 +5,10 @@ import { STATE_DIR } from "../config/paths.js";
import { TELEGRAM_COMMAND_NAME_PATTERN } from "../config/telegram-custom-commands.js";
import type { TelegramAccountConfig } from "../config/types.js";
import type { RuntimeEnv } from "../runtime.js";
import { registerTelegramNativeCommands } from "./bot-native-commands.js";
import {
buildTelegramNativeCommandCallbackData,
registerTelegramNativeCommands,
} from "./bot-native-commands.js";
import { createNativeCommandTestParams } from "./bot-native-commands.test-helpers.js";
const { listSkillCommandsForAgents } = vi.hoisted(() => ({
@ -213,6 +216,55 @@ describe("registerTelegramNativeCommands", () => {
expect(registeredCommands.some((entry) => entry.command === "custom-bad")).toBe(false);
});
it("prefixes native command menu callbacks so Telegram callback routing preserves native mode", async () => {
const commandHandlers = new Map<string, (ctx: unknown) => Promise<void>>();
const sendMessage = vi.fn().mockResolvedValue(undefined);
registerTelegramNativeCommands({
...buildParams({}),
bot: {
api: {
setMyCommands: vi.fn().mockResolvedValue(undefined),
sendMessage,
},
command: vi.fn((name: string, handler: (ctx: unknown) => Promise<void>) => {
commandHandlers.set(name, handler);
}),
} as unknown as Parameters<typeof registerTelegramNativeCommands>[0]["bot"],
allowFrom: ["*"],
});
const fastHandler = commandHandlers.get("fast");
expect(fastHandler).toBeDefined();
await fastHandler?.({
message: {
chat: { id: 1234, type: "private" },
date: 1736380800,
from: { id: 9, first_name: "Ada", username: "ada_bot" },
message_id: 44,
},
me: { username: "openclaw_bot" },
match: "",
});
expect(sendMessage).toHaveBeenCalledTimes(1);
const [, , params] = sendMessage.mock.calls[0] ?? [];
expect(params).toEqual(
expect.objectContaining({
reply_markup: expect.objectContaining({
inline_keyboard: expect.arrayContaining([
expect.arrayContaining([
expect.objectContaining({
callback_data: buildTelegramNativeCommandCallbackData("/fast status"),
}),
]),
]),
}),
}),
);
});
it("passes agent-scoped media roots for plugin command replies with media", async () => {
const commandHandlers = new Map<string, (ctx: unknown) => Promise<void>>();
const sendMessage = vi.fn().mockResolvedValue(undefined);

View File

@ -74,6 +74,7 @@ import { resolveTelegramGroupPromptSettings } from "./group-config-helpers.js";
import { buildInlineKeyboard } from "./send.js";
const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again.";
const TELEGRAM_NATIVE_COMMAND_CALLBACK_PREFIX = "tgcmd:";
type TelegramNativeCommandContext = Context & { match?: string };
@ -142,6 +143,18 @@ type RegisterTelegramNativeCommandsParams = {
opts: { token: string };
};
export function buildTelegramNativeCommandCallbackData(commandText: string): string {
return `${TELEGRAM_NATIVE_COMMAND_CALLBACK_PREFIX}${commandText}`;
}
export function parseTelegramNativeCommandCallbackData(data: string): string | null {
if (!data.startsWith(TELEGRAM_NATIVE_COMMAND_CALLBACK_PREFIX)) {
return null;
}
const commandText = data.slice(TELEGRAM_NATIVE_COMMAND_CALLBACK_PREFIX.length).trim();
return commandText || null;
}
async function resolveTelegramCommandAuth(params: {
msg: NonNullable<TelegramNativeCommandContext["message"]>;
bot: Bot;
@ -632,7 +645,9 @@ export const registerTelegramNativeCommands = ({
};
return {
text: choice.label,
callback_data: buildCommandTextFromArgs(commandDefinition, args),
callback_data: buildTelegramNativeCommandCallbackData(
buildCommandTextFromArgs(commandDefinition, args),
),
};
}),
);

View File

@ -143,6 +143,41 @@ describe("createTelegramBot", () => {
expect(payload.Body).toContain("cmd:option_a");
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-1");
});
it("preserves native command source for Telegram command-menu callbacks when text commands are disabled", async () => {
loadConfig.mockReturnValue({
commands: { text: false },
channels: {
telegram: { dmPolicy: "open", allowFrom: ["*"] },
},
});
createTelegramBot({ token: "tok" });
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-native-fast",
data: "tgcmd:/fast status",
from: { id: 9, first_name: "Ada", username: "ada_bot" },
message: {
chat: { id: 1234, type: "private" },
date: 1736380800,
message_id: 10,
},
},
me: { username: "openclaw_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expect(payload.CommandBody).toBe("/fast status");
expect(payload.CommandSource).toBe("native");
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-native-fast");
});
it("wraps inbound message with Telegram envelope", async () => {
await withEnvAsync({ TZ: "Europe/Vienna" }, async () => {
createTelegramBot({ token: "tok" });