mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(plugin-sdk): honor suppressReply across native commands
This commit is contained in:
@@ -183,21 +183,21 @@ generic contracts; Plan Mode can use them, but so can approval workflows,
|
||||
workspace policy gates, background monitors, setup wizards, and UI companion
|
||||
plugins.
|
||||
|
||||
| Method | Contract it owns |
|
||||
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `api.session.state.registerSessionExtension(...)` | Plugin-owned, JSON-compatible session state projected through Gateway sessions |
|
||||
| `api.session.workflow.enqueueNextTurnInjection(...)` | Durable exactly-once context injected into the next agent turn for one session |
|
||||
| `api.registerTrustedToolPolicy(...)` | Manifest-gated trusted pre-plugin tool policy that can block or rewrite tool params |
|
||||
| `api.registerToolMetadata(...)` | Tool catalog display metadata without changing the tool implementation |
|
||||
| Method | Contract it owns |
|
||||
| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `api.session.state.registerSessionExtension(...)` | Plugin-owned, JSON-compatible session state projected through Gateway sessions |
|
||||
| `api.session.workflow.enqueueNextTurnInjection(...)` | Durable exactly-once context injected into the next agent turn for one session |
|
||||
| `api.registerTrustedToolPolicy(...)` | Manifest-gated trusted pre-plugin tool policy that can block or rewrite tool params |
|
||||
| `api.registerToolMetadata(...)` | Tool catalog display metadata without changing the tool implementation |
|
||||
| `api.registerCommand(...)` | Scoped plugin commands; command results can set `continueAgent: true` or `suppressReply: true`; Discord native commands support `descriptionLocalizations` |
|
||||
| `api.session.controls.registerControlUiDescriptor(...)` | Control UI contribution descriptors for session, tool, run, or settings surfaces |
|
||||
| `api.lifecycle.registerRuntimeLifecycle(...)` | Cleanup callbacks for plugin-owned runtime resources on reset/delete/reload paths |
|
||||
| `api.agent.events.registerAgentEventSubscription(...)` | Sanitized event subscriptions for workflow state and monitors |
|
||||
| `api.runContext.setRunContext(...)` / `getRunContext(...)` / `clearRunContext(...)` | Per-run plugin scratch state cleared on terminal run lifecycle |
|
||||
| `api.session.workflow.registerSessionSchedulerJob(...)` | Cleanup metadata for plugin-owned scheduler jobs; does not schedule work or create task records |
|
||||
| `api.session.workflow.sendSessionAttachment(...)` | Bundled-only host-mediated file attachment delivery to the active direct-outbound session route |
|
||||
| `api.session.workflow.scheduleSessionTurn(...)` / `unscheduleSessionTurnsByTag(...)` | Bundled-only Cron-backed scheduled session turns plus tag-based cleanup |
|
||||
| `api.session.controls.registerSessionAction(...)` | Typed session actions clients can dispatch through the Gateway |
|
||||
| `api.session.controls.registerControlUiDescriptor(...)` | Control UI contribution descriptors for session, tool, run, or settings surfaces |
|
||||
| `api.lifecycle.registerRuntimeLifecycle(...)` | Cleanup callbacks for plugin-owned runtime resources on reset/delete/reload paths |
|
||||
| `api.agent.events.registerAgentEventSubscription(...)` | Sanitized event subscriptions for workflow state and monitors |
|
||||
| `api.runContext.setRunContext(...)` / `getRunContext(...)` / `clearRunContext(...)` | Per-run plugin scratch state cleared on terminal run lifecycle |
|
||||
| `api.session.workflow.registerSessionSchedulerJob(...)` | Cleanup metadata for plugin-owned scheduler jobs; does not schedule work or create task records |
|
||||
| `api.session.workflow.sendSessionAttachment(...)` | Bundled-only host-mediated file attachment delivery to the active direct-outbound session route |
|
||||
| `api.session.workflow.scheduleSessionTurn(...)` / `unscheduleSessionTurnsByTag(...)` | Bundled-only Cron-backed scheduled session turns plus tag-based cleanup |
|
||||
| `api.session.controls.registerSessionAction(...)` | Typed session actions clients can dispatch through the Gateway |
|
||||
|
||||
Use the grouped namespaces for new plugin code:
|
||||
|
||||
|
||||
@@ -1020,6 +1020,39 @@ describe("Discord native plugin command dispatch", () => {
|
||||
expect(interaction.reply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("suppresses the warning when a direct plugin command suppresses replies", async () => {
|
||||
const cfg = createConfig();
|
||||
const commandSpec: NativeCommandSpec = {
|
||||
name: "cron_jobs",
|
||||
description: "List cron jobs",
|
||||
acceptsArgs: false,
|
||||
};
|
||||
const interaction = createInteraction();
|
||||
const pluginMatch = {
|
||||
command: {
|
||||
name: "cron_jobs",
|
||||
description: "List cron jobs",
|
||||
pluginId: "cron-jobs",
|
||||
acceptsArgs: false,
|
||||
handler: vi.fn().mockResolvedValue({ suppressReply: true }),
|
||||
},
|
||||
args: undefined,
|
||||
};
|
||||
|
||||
runtimeModuleMocks.matchPluginCommand.mockReturnValue(pluginMatch as never);
|
||||
runtimeModuleMocks.executePluginCommand.mockResolvedValue({ suppressReply: true });
|
||||
const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue(
|
||||
{} as never,
|
||||
);
|
||||
const command = await createNativeCommand(cfg, commandSpec);
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
||||
|
||||
expect(dispatchSpy).not.toHaveBeenCalled();
|
||||
expectNoFollowUpContent(interaction, "⚠️ Command produced no visible reply.");
|
||||
expect(interaction.reply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forwards Discord thread metadata into direct plugin command execution", async () => {
|
||||
const cfg = {
|
||||
commands: {
|
||||
|
||||
@@ -573,6 +573,9 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
messageThreadId,
|
||||
threadParentId: pluginThreadParentId,
|
||||
});
|
||||
if (pluginReply.suppressReply === true) {
|
||||
return { accepted: true, effectiveRoute };
|
||||
}
|
||||
if (!hasRenderableReplyPayload(pluginReply)) {
|
||||
await respond(DISCORD_EMPTY_VISIBLE_REPLY_WARNING);
|
||||
return { accepted: true, effectiveRoute };
|
||||
|
||||
@@ -116,7 +116,7 @@ type TelegramNativeCommandContext = Context & { match?: string };
|
||||
type TelegramChunkMode = ReturnType<
|
||||
typeof import("openclaw/plugin-sdk/reply-dispatch-runtime").resolveChunkMode
|
||||
>;
|
||||
type TelegramNativeReplyPayload = import("openclaw/plugin-sdk/reply-dispatch-runtime").ReplyPayload;
|
||||
type TelegramNativeReplyPayload = import("openclaw/plugin-sdk/plugin-entry").PluginCommandResult;
|
||||
type TelegramNativeReplyChannelData = {
|
||||
buttons?: TelegramInlineButtons;
|
||||
pin?: boolean;
|
||||
@@ -458,9 +458,7 @@ function normalizeTelegramNativeReplyPayload(
|
||||
}
|
||||
|
||||
function isSuppressedTelegramNativeReplyPayload(result: TelegramNativeReplyPayload): boolean {
|
||||
return Boolean(
|
||||
(result as TelegramNativeReplyPayload & { suppressReply?: boolean }).suppressReply,
|
||||
);
|
||||
return result.suppressReply === true;
|
||||
}
|
||||
|
||||
function hasRenderableTelegramNativeReplyPayload(result: TelegramNativeReplyPayload): boolean {
|
||||
@@ -1657,24 +1655,13 @@ export const registerTelegramNativeCommands = ({
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
const suppressTelegramNativeReply =
|
||||
shouldSuppressLocalTelegramExecApprovalPrompt({
|
||||
cfg: runtimeCfg,
|
||||
accountId: route.accountId,
|
||||
payload: result,
|
||||
})
|
||||
) {
|
||||
await cleanupTelegramProgressPlaceholder({
|
||||
bot,
|
||||
chatId,
|
||||
progressMessageId,
|
||||
runtime,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If the plugin handled delivery itself and wants no fallback, just clean up
|
||||
if (isSuppressedTelegramNativeReplyPayload(result)) {
|
||||
}) || isSuppressedTelegramNativeReplyPayload(result);
|
||||
if (suppressTelegramNativeReply) {
|
||||
await cleanupTelegramProgressPlaceholder({
|
||||
bot,
|
||||
chatId,
|
||||
|
||||
@@ -2025,15 +2025,9 @@ export type PluginCommandContext = {
|
||||
* Result returned by a plugin command handler.
|
||||
*/
|
||||
export type PluginCommandResult = ReplyPayload & {
|
||||
/** When true, allows the agent session to continue processing after the command */
|
||||
/** Allows the agent session to continue processing after the command. */
|
||||
continueAgent?: boolean;
|
||||
/**
|
||||
* When true, the channel adapter should not send a fallback reply.
|
||||
* Use this when the plugin command handler delivers its own response
|
||||
* directly via the channel API (e.g. Telegram Bot API with custom
|
||||
* retry logic or transport guarantees) instead of returning a payload
|
||||
* for OpenClaw to deliver.
|
||||
*/
|
||||
/** Suppresses channel fallback replies when the handler already delivered a response. */
|
||||
suppressReply?: boolean;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user