fix(commands): tolerate empty plugin command replies

Fixes #74800.
This commit is contained in:
Vincent Koc
2026-05-03 10:55:58 -07:00
parent 63ebe372e8
commit 863198f0c9
5 changed files with 103 additions and 22 deletions

View File

@@ -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." }],
}),
);
});
});

View File

@@ -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,
});
});
}