mirror of https://github.com/openclaw/openclaw.git
fix(commands): harden fast status and Telegram callbacks
This commit is contained in:
parent
8657b65b05
commit
3034adfdb3
|
|
@ -77,6 +77,8 @@ Docs: https://docs.openclaw.ai
|
|||
- Plugins/CLI: collect root-help plugin descriptors through a dedicated non-activating CLI metadata path so enabled plugins keep validated config semantics without triggering runtime-only plugin registration work, while preserving runtime CLI command registration for legacy channel plugins that still wire commands from full registration. (#57294) thanks @gumadeiras.
|
||||
- Anthropic/OAuth: inject `/fast` `service_tier` hints for direct `sk-ant-oat-*` requests so OAuth-authenticated Anthropic runs stop missing the same overload-routing signal as API-key traffic. Fixes #55758. Thanks @Cypherm and @vincentkoc.
|
||||
- Anthropic/service tiers: support explicit `serviceTier` model params for direct Anthropic requests and let them override `/fast` defaults when both are set. (#45453) Thanks @vincentkoc.
|
||||
- Auto-reply/fast: accept `/fast status` on the directive-only path, align help/status text with the documented `status|on|off` syntax, and keep current-state replies consistent across command surfaces. Fixes #46095. Thanks @weissfl and @vincentkoc.
|
||||
- Telegram/native commands: prefix native command menu callback payloads and preserve `CommandSource: "native"` when Telegram replays them through callback queries, so `/fast` and other native command menus keep working even when text-command routing is disabled. Thanks @vincentkoc.
|
||||
- Docs/anchors: fix broken English docs links and make Mint anchor audits run against the English-source docs tree. (#57039) thanks @velvet-shark.
|
||||
- Cron/announce: preserve all deliverable text payloads for announce mode instead of collapsing to the last chunk, so multi-line cron reports deliver in full to Telegram forum topics.
|
||||
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no**
|
|||
- Optional rotation: `ANTHROPIC_API_KEYS`, `ANTHROPIC_API_KEY_1`, `ANTHROPIC_API_KEY_2`, plus `OPENCLAW_LIVE_ANTHROPIC_KEY` (single override)
|
||||
- Example model: `anthropic/claude-opus-4-6`
|
||||
- CLI: `openclaw onboard --auth-choice token` (paste setup-token) or `openclaw models auth paste-token --provider anthropic`
|
||||
- Direct API-key models support the shared `/fast` toggle and `params.fastMode`; OpenClaw maps that to Anthropic `service_tier` (`auto` vs `standard_only`)
|
||||
- Direct public Anthropic requests support the shared `/fast` toggle and `params.fastMode`, including API-key and OAuth-authenticated traffic sent to `api.anthropic.com`; OpenClaw maps that to Anthropic `service_tier` (`auto` vs `standard_only`)
|
||||
- Policy note: setup-token support is technical compatibility; Anthropic has blocked some subscription usage outside Claude Code in the past. Verify current Anthropic terms and decide based on your risk tolerance.
|
||||
- Recommendation: Anthropic API key auth is the safer, recommended path over subscription setup-token auth.
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ Notes:
|
|||
- ACP command reference and runtime behavior: [ACP Agents](/tools/acp-agents).
|
||||
- `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use.
|
||||
- `/fast on|off` persists a session override. Use the Sessions UI `inherit` option to clear it and fall back to config defaults.
|
||||
- `/fast` is provider-specific: OpenAI/OpenAI Codex map it to `service_tier=priority` on native Responses endpoints, while direct Anthropic API-key requests map it to `service_tier=auto` or `standard_only`. See [OpenAI](/providers/openai) and [Anthropic](/providers/anthropic).
|
||||
- `/fast` is provider-specific: OpenAI/OpenAI Codex map it to `service_tier=priority` on native Responses endpoints, while direct public Anthropic requests, including OAuth-authenticated traffic sent to `api.anthropic.com`, map it to `service_tier=auto` or `standard_only`. See [OpenAI](/providers/openai) and [Anthropic](/providers/anthropic).
|
||||
- Tool failure summaries are still shown when relevant, but detailed failure text is only included when `/verbose` is `on` or `full`.
|
||||
- `/reasoning` (and `/verbose`) are risky in group settings: they may reveal internal reasoning or tool output you did not intend to expose. Prefer leaving them off, especially in group chats.
|
||||
- **Fast path:** command-only messages from allowlisted senders are handled immediately (bypass queue + model).
|
||||
|
|
|
|||
|
|
@ -51,7 +51,10 @@ import {
|
|||
resolveInboundMediaFileId,
|
||||
} from "./bot-handlers.media.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,
|
||||
|
|
@ -1548,12 +1551,14 @@ export const registerTelegramHandlers = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const nativeCommandText = parseTelegramNativeCommandCallbackData(data);
|
||||
const syntheticMessage = buildSyntheticTextMessage({
|
||||
base: withResolvedTelegramForumFlag(callbackMessage, isForum),
|
||||
from: callback.from,
|
||||
text: data,
|
||||
text: nativeCommandText ?? data,
|
||||
});
|
||||
await processMessage(buildSyntheticContext(ctx, syntheticMessage), [], storeAllowFrom, {
|
||||
commandSource: nativeCommandText ? "native" : undefined,
|
||||
forceWasMentioned: true,
|
||||
messageIdOverride: callback.id,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -244,6 +244,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,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export type TelegramMediaRef = {
|
|||
};
|
||||
|
||||
export type TelegramMessageContextOptions = {
|
||||
commandSource?: "text" | "native";
|
||||
forceWasMentioned?: boolean;
|
||||
messageIdOverride?: string;
|
||||
receivedAtMs?: number;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|||
import { pluginCommandMocks, resetPluginCommandMocks } from "./test-support/plugin-command.js";
|
||||
|
||||
let registerTelegramNativeCommands: typeof import("./bot-native-commands.js").registerTelegramNativeCommands;
|
||||
let parseTelegramNativeCommandCallbackData: typeof import("./bot-native-commands.js").parseTelegramNativeCommandCallbackData;
|
||||
import {
|
||||
createCommandBot,
|
||||
createNativeCommandTestParams,
|
||||
createPrivateCommandContext,
|
||||
deliverReplies,
|
||||
|
|
@ -19,7 +21,8 @@ import {
|
|||
describe("registerTelegramNativeCommands", () => {
|
||||
beforeAll(async () => {
|
||||
vi.resetModules();
|
||||
({ registerTelegramNativeCommands } = await import("./bot-native-commands.js"));
|
||||
({ registerTelegramNativeCommands, parseTelegramNativeCommandCallbackData } =
|
||||
await import("./bot-native-commands.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -161,6 +164,30 @@ describe("registerTelegramNativeCommands", () => {
|
|||
expect(registeredCommands.some((entry) => entry.command === "custom-bad")).toBe(false);
|
||||
});
|
||||
|
||||
it("prefixes native command menu callback data so callback handlers can preserve native routing", async () => {
|
||||
const { bot, commandHandlers, sendMessage } = createCommandBot();
|
||||
|
||||
registerTelegramNativeCommands({
|
||||
...createNativeCommandTestParams({}, { bot }),
|
||||
});
|
||||
|
||||
const handler = commandHandlers.get("fast");
|
||||
expect(handler).toBeTruthy();
|
||||
await handler?.(createPrivateCommandContext());
|
||||
|
||||
const replyMarkup = sendMessage.mock.calls[0]?.[2]?.reply_markup as
|
||||
| { inline_keyboard?: Array<Array<{ callback_data?: string }>> }
|
||||
| undefined;
|
||||
const callbackData = replyMarkup?.inline_keyboard
|
||||
?.flat()
|
||||
.map((button) => button.callback_data)
|
||||
.filter(Boolean);
|
||||
|
||||
expect(callbackData).toEqual(["tgcmd:/fast status", "tgcmd:/fast on", "tgcmd:/fast off"]);
|
||||
expect(parseTelegramNativeCommandCallbackData("tgcmd:/fast status")).toBe("/fast status");
|
||||
expect(parseTelegramNativeCommandCallbackData("tgcmd:fast status")).toBeNull();
|
||||
});
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -86,6 +86,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 };
|
||||
|
||||
|
|
@ -129,6 +130,22 @@ export type RegisterTelegramHandlerParams = {
|
|||
logger: ReturnType<typeof getChildLogger>;
|
||||
};
|
||||
|
||||
export function buildTelegramNativeCommandCallbackData(commandText: string): string {
|
||||
return `${TELEGRAM_NATIVE_COMMAND_CALLBACK_PREFIX}${commandText}`;
|
||||
}
|
||||
|
||||
export function parseTelegramNativeCommandCallbackData(data?: string | null): string | null {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = data.trim();
|
||||
if (!trimmed.startsWith(TELEGRAM_NATIVE_COMMAND_CALLBACK_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
const commandText = trimmed.slice(TELEGRAM_NATIVE_COMMAND_CALLBACK_PREFIX.length).trim();
|
||||
return commandText.startsWith("/") ? commandText : null;
|
||||
}
|
||||
|
||||
export type RegisterTelegramNativeCommandsParams = {
|
||||
bot: Bot;
|
||||
cfg: OpenClawConfig;
|
||||
|
|
@ -679,7 +696,9 @@ export const registerTelegramNativeCommands = ({
|
|||
};
|
||||
return {
|
||||
text: choice.label,
|
||||
callback_data: buildCommandTextFromArgs(commandDefinition, args),
|
||||
callback_data: buildTelegramNativeCommandCallbackData(
|
||||
buildCommandTextFromArgs(commandDefinition, args),
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -345,6 +345,43 @@ describe("createTelegramBot", () => {
|
|||
expect(payload.Body).toContain("cmd:option_a");
|
||||
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-1");
|
||||
});
|
||||
it("preserves native command source for prefixed callback_query payloads", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
commands: { text: false, native: true },
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const callbackHandler = getOnHandler("callback_query") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
await callbackHandler({
|
||||
callbackQuery: {
|
||||
id: "cbq-native-1",
|
||||
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-1");
|
||||
});
|
||||
it("reloads callback model routing bindings without recreating the bot", async () => {
|
||||
const buildModelsProviderDataMock =
|
||||
telegramBotDepsForTest.buildModelsProviderData as unknown as ReturnType<typeof vi.fn>;
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ describe("directive behavior", () => {
|
|||
},
|
||||
});
|
||||
expect(fastText).toContain("Current fast mode: on (config)");
|
||||
expect(fastText).toContain("Options: on, off.");
|
||||
expect(fastText).toContain("Options: status, on, off.");
|
||||
|
||||
const verboseText = await runCommand(home, "/verbose", {
|
||||
defaults: { verboseDefault: "on" },
|
||||
|
|
@ -200,6 +200,23 @@ describe("directive behavior", () => {
|
|||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it("treats /fast status like the no-argument status query", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const statusText = await runCommand(home, "/fast status", {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {
|
||||
params: { fastMode: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(statusText).toContain("Current fast mode: on (config)");
|
||||
expect(statusText).toContain("Options: status, on, off.");
|
||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it("persists elevated toggles across /status and /elevated", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const storePath = sessionStorePath(home);
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ export async function handleDirectiveOnly(
|
|||
};
|
||||
}
|
||||
if (directives.hasFastDirective && directives.fastMode === undefined) {
|
||||
if (!directives.rawFastMode) {
|
||||
if (!directives.rawFastMode || directives.rawFastMode.toLowerCase() === "status") {
|
||||
const sourceSuffix =
|
||||
effectiveFastModeSource === "config"
|
||||
? " (config)"
|
||||
|
|
@ -210,12 +210,12 @@ export async function handleDirectiveOnly(
|
|||
return {
|
||||
text: withOptions(
|
||||
`Current fast mode: ${effectiveFastMode ? "on" : "off"}${sourceSuffix}.`,
|
||||
"on, off",
|
||||
"status, on, off",
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: `Unrecognized fast mode "${directives.rawFastMode}". Valid levels: on, off.`,
|
||||
text: `Unrecognized fast mode "${directives.rawFastMode}". Valid levels: status, on, off.`,
|
||||
};
|
||||
}
|
||||
if (directives.hasReasoningDirective && !directives.reasoningLevel) {
|
||||
|
|
|
|||
|
|
@ -1309,7 +1309,7 @@ describe("buildHelpMessage", () => {
|
|||
});
|
||||
|
||||
it("includes /fast in help output", () => {
|
||||
expect(buildHelpMessage()).toContain("/fast on|off");
|
||||
expect(buildHelpMessage()).toContain("/fast status|on|off");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -842,7 +842,7 @@ export function buildHelpMessage(cfg?: OpenClawConfig): string {
|
|||
lines.push(" /new | /reset | /compact [instructions] | /stop");
|
||||
lines.push("");
|
||||
|
||||
const optionParts = ["/think <level>", "/model <id>", "/fast on|off", "/verbose on|off"];
|
||||
const optionParts = ["/think <level>", "/model <id>", "/fast status|on|off", "/verbose on|off"];
|
||||
if (isCommandFlagEnabled(cfg, "config")) {
|
||||
optionParts.push("/config");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue