mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Plugins/commands: normalize empty plugin command handler results and let Telegram native plugin commands send the empty-response fallback instead of throwing when a handler returns `undefined`. Fixes #74800. Thanks @vincentkoc.
|
||||
- Plugins/OpenRouter: advertise DeepSeek V4 thinking levels, including `xhigh` and `max`, through the runtime and lightweight provider policy surfaces so `/think` validation no longer rejects OpenRouter-routed DeepSeek V4 models. Fixes #74788. Thanks @vincentkoc.
|
||||
- Status/sessions: ignore malformed non-string persisted session provider/model metadata instead of throwing while rendering status summaries. Thanks @vincentkoc.
|
||||
- CLI/config: remove only the targeted array element for `openclaw config unset array[index]` instead of replaying the unset during config write and deleting the shifted next element. Fixes #76290. Thanks @SymbolStar and @vincentkoc.
|
||||
|
||||
@@ -1106,4 +1106,40 @@ describe("registerTelegramNativeCommands — session metadata", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends an empty-response fallback when a plugin command returns undefined", async () => {
|
||||
pluginRuntimeMocks.executePluginCommand.mockResolvedValue(undefined as never);
|
||||
|
||||
const { handler } = registerAndResolveCommandHandler({
|
||||
commandName: "codex",
|
||||
cfg: { commands: { allowFrom: { telegram: ["200"] } } } as OpenClawConfig,
|
||||
useAccessGroups: false,
|
||||
pluginCommandSpecs: [
|
||||
{
|
||||
name: "codex",
|
||||
description: "Codex",
|
||||
acceptsArgs: true,
|
||||
},
|
||||
] as TelegramPluginCommandSpecs,
|
||||
});
|
||||
pluginRuntimeMocks.matchPluginCommand.mockReturnValue({
|
||||
command: {
|
||||
name: "codex",
|
||||
description: "Codex",
|
||||
handler: vi.fn(),
|
||||
pluginId: "openclaw-codex-app-server",
|
||||
pluginName: "Codex",
|
||||
requireAuth: true,
|
||||
},
|
||||
args: "status",
|
||||
});
|
||||
|
||||
await handler(createTelegramPrivateCommandContext({ match: "status" }));
|
||||
|
||||
expect(deliveryMocks.deliverReplies).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
replies: [{ text: "No response generated. Please try again." }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
TelegramTopicConfig,
|
||||
} from "openclaw/plugin-sdk/config-types";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
||||
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
||||
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
@@ -249,6 +250,16 @@ function resolveTelegramNativeReplyChannelData(
|
||||
return result.channelData?.telegram as TelegramNativeReplyChannelData | undefined;
|
||||
}
|
||||
|
||||
function normalizeTelegramNativeReplyPayload(
|
||||
result: TelegramNativeReplyPayload | null | undefined,
|
||||
): TelegramNativeReplyPayload {
|
||||
return result && typeof result === "object" ? result : {};
|
||||
}
|
||||
|
||||
function hasRenderableTelegramNativeReplyPayload(result: TelegramNativeReplyPayload): boolean {
|
||||
return resolveSendableOutboundReplyParts(result).hasContent;
|
||||
}
|
||||
|
||||
function isEditableTelegramProgressResult(result: TelegramNativeReplyPayload): boolean {
|
||||
const telegramData = resolveTelegramNativeReplyChannelData(result);
|
||||
return Boolean(
|
||||
@@ -1276,23 +1287,25 @@ export const registerTelegramNativeCommands = ({
|
||||
threadId: threadSpec.id,
|
||||
});
|
||||
|
||||
const result = await nativeCommandRuntime.executePluginCommand({
|
||||
command: match.command,
|
||||
args: match.args,
|
||||
senderId,
|
||||
channel: "telegram",
|
||||
isAuthorizedSender: commandAuthorized,
|
||||
senderIsOwner,
|
||||
sessionKey: route.sessionKey,
|
||||
sessionId: sessionFileContext.sessionId,
|
||||
sessionFile: sessionFileContext.sessionFile,
|
||||
commandBody,
|
||||
config: runtimeCfg,
|
||||
from,
|
||||
to,
|
||||
accountId,
|
||||
messageThreadId: threadSpec.id,
|
||||
});
|
||||
const result = normalizeTelegramNativeReplyPayload(
|
||||
await nativeCommandRuntime.executePluginCommand({
|
||||
command: match.command,
|
||||
args: match.args,
|
||||
senderId,
|
||||
channel: "telegram",
|
||||
isAuthorizedSender: commandAuthorized,
|
||||
senderIsOwner,
|
||||
sessionKey: route.sessionKey,
|
||||
sessionId: sessionFileContext.sessionId,
|
||||
sessionFile: sessionFileContext.sessionFile,
|
||||
commandBody,
|
||||
config: runtimeCfg,
|
||||
from,
|
||||
to,
|
||||
accountId,
|
||||
messageThreadId: threadSpec.id,
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
shouldSuppressLocalTelegramExecApprovalPrompt({
|
||||
@@ -1310,14 +1323,19 @@ export const registerTelegramNativeCommands = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const deliverableResult = hasRenderableTelegramNativeReplyPayload(result)
|
||||
? result
|
||||
: { text: EMPTY_RESPONSE_FALLBACK };
|
||||
const progressResultText =
|
||||
typeof result.text === "string" && result.text.trim().length > 0 ? result.text : null;
|
||||
const telegramResultData = resolveTelegramNativeReplyChannelData(result);
|
||||
typeof deliverableResult.text === "string" && deliverableResult.text.trim().length > 0
|
||||
? deliverableResult.text
|
||||
: null;
|
||||
const telegramResultData = resolveTelegramNativeReplyChannelData(deliverableResult);
|
||||
if (
|
||||
progressMessageId != null &&
|
||||
telegramDeps.editMessageTelegram &&
|
||||
progressResultText &&
|
||||
isEditableTelegramProgressResult(result)
|
||||
isEditableTelegramProgressResult(deliverableResult)
|
||||
) {
|
||||
try {
|
||||
await telegramDeps.editMessageTelegram(chatId, progressMessageId, progressResultText, {
|
||||
@@ -1350,9 +1368,10 @@ export const registerTelegramNativeCommands = ({
|
||||
runtime,
|
||||
});
|
||||
await deliverReplies({
|
||||
replies: [result],
|
||||
replies: [deliverableResult],
|
||||
...deliveryBaseOptions,
|
||||
silent: runtimeTelegramCfg.silentErrorReplies === true && result.isError === true,
|
||||
silent:
|
||||
runtimeTelegramCfg.silentErrorReplies === true && deliverableResult.isError === true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -901,6 +901,27 @@ describe("registerPluginCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes undefined plugin command handler results to an empty reply payload", async () => {
|
||||
const handler = async () => undefined as never;
|
||||
|
||||
const result = await executePluginCommand({
|
||||
command: {
|
||||
name: "silentcheck",
|
||||
description: "Demo command",
|
||||
acceptsArgs: false,
|
||||
handler,
|
||||
pluginId: "demo-plugin",
|
||||
},
|
||||
channel: "telegram",
|
||||
senderId: "U123",
|
||||
isAuthorizedSender: true,
|
||||
commandBody: "/silentcheck",
|
||||
config: {} as never,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("passes the effective default account to plugin command handlers when accountId is omitted", async () => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
|
||||
@@ -339,6 +339,10 @@ export async function executePluginCommand(params: {
|
||||
logVerbose(
|
||||
`Plugin command /${command.name} executed successfully for ${senderId || "unknown"}`,
|
||||
);
|
||||
if (!result || typeof result !== "object") {
|
||||
logVerbose(`Plugin command /${command.name} returned no reply payload`);
|
||||
return {};
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
|
||||
Reference in New Issue
Block a user