diff --git a/CHANGELOG.md b/CHANGELOG.md index 4301cbaeb7f..1dc9d4f4523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Channels/Telegram: skip the optional webhook-info API call during polling-mode status checks and startup bot-label probes so long-polling setups avoid an unnecessary Telegram round trip. Carries forward #72990. Thanks @danielgruneberg. +- CLI/message: load only the selected channel plugin for targeted `openclaw message` actions, and fall back to configured channel plugins when the channel must be inferred, so scripted sends avoid full bundled plugin registry scans. Fixes #73006. Thanks @jasonftl. - CLI/models: keep route-first `models status --json` stdout reserved for the JSON payload by routing auth-profile and startup diagnostics to stderr. Fixes #72962. Thanks @vishutdhar. - Sessions: ignore future-dated session activity timestamps during reset freshness checks and cap future `updatedAt` values at the merge boundary so clock-skewed messages cannot keep stale sessions alive forever. Fixes #72989. Thanks @martingarramon. - Plugins/CLI: allow managed plugin installs when the active extensions root is a symlink to a real state directory, while keeping nested target symlinks blocked and suppressing misleading hook-pack fallback errors for install-boundary failures. Fixes #72946. Thanks @mayank6136. diff --git a/docs/cli/message.md b/docs/cli/message.md index 2298b886c4e..949d142b182 100644 --- a/docs/cli/message.md +++ b/docs/cli/message.md @@ -22,6 +22,7 @@ Channel selection: - `--channel` required if more than one channel is configured. - If exactly one channel is configured, it becomes the default. - Values: `discord|googlechat|imessage|matrix|mattermost|msteams|signal|slack|telegram|whatsapp` (Mattermost requires plugin) +- `openclaw message` loads only the selected channel plugin when `--channel` or a channel-prefixed target is present; otherwise it loads configured channel plugins for default-channel inference. Target formats (`--target`): diff --git a/src/cli/program/message/helpers.test.ts b/src/cli/program/message/helpers.test.ts index 0b20ecf1f0b..bcf8a512d16 100644 --- a/src/cli/program/message/helpers.test.ts +++ b/src/cli/program/message/helpers.test.ts @@ -96,11 +96,46 @@ describe("runMessageAction", () => { it("calls exit(0) after successful message delivery", async () => { await runSendAction(); - expect(ensurePluginRegistryLoaded).toHaveBeenCalledOnce(); + expect(ensurePluginRegistryLoaded).toHaveBeenCalledWith({ + scope: "configured-channels", + onlyPluginIds: ["discord"], + }); expect(exitMock).toHaveBeenCalledOnce(); expect(exitMock).toHaveBeenCalledWith(0); }); + it("loads configured channel plugins when no target channel is known yet", async () => { + await runSendAction({ channel: undefined }); + + expect(ensurePluginRegistryLoaded).toHaveBeenCalledWith({ + scope: "configured-channels", + }); + }); + + it("narrows plugin loading from a channel-prefixed target", async () => { + await runSendAction({ channel: undefined, target: "telegram:12345" }); + + expect(ensurePluginRegistryLoaded).toHaveBeenCalledWith({ + scope: "configured-channels", + onlyPluginIds: ["telegram"], + }); + }); + + it("loads configured channel plugins for mixed broadcast target prefixes", async () => { + const runMessageAction = createRunMessageAction(); + + await expect( + runMessageAction("broadcast", { + targets: ["discord:channel:1", "telegram:123"], + message: "hi", + }), + ).rejects.toThrow("exit"); + + expect(ensurePluginRegistryLoaded).toHaveBeenCalledWith({ + scope: "configured-channels", + }); + }); + it("runs gateway_stop hooks before exit when registered", async () => { hasHooksMock.mockReturnValueOnce(true); await runSendAction(); diff --git a/src/cli/program/message/helpers.ts b/src/cli/program/message/helpers.ts index cb94498e86a..582563723a3 100644 --- a/src/cli/program/message/helpers.ts +++ b/src/cli/program/message/helpers.ts @@ -1,4 +1,5 @@ import type { Command } from "commander"; +import { resolveMessageSecretScope } from "../../../cli/message-secret-scope.js"; import { messageCommand } from "../../../commands/message.js"; import { danger, setVerbose } from "../../../globals.js"; import { CHANNEL_TARGET_DESCRIPTION } from "../../../infra/outbound/channel-target.js"; @@ -6,7 +7,7 @@ import { runGlobalGatewayStopSafely } from "../../../plugins/hook-runner-global. import { defaultRuntime } from "../../../runtime.js"; import { runCommandWithRuntime } from "../../cli-utils.js"; import { createDefaultDeps } from "../../deps.js"; -import { ensurePluginRegistryLoaded } from "../../plugin-registry.js"; +import { ensurePluginRegistryLoaded, type PluginRegistryScope } from "../../plugin-registry.js"; export type MessageCliHelpers = { withMessageBase: (command: Command) => Command; @@ -31,6 +32,20 @@ async function runPluginStopHooks(): Promise { }); } +function resolveMessagePluginLoadOptions( + opts: Record, +): { scope: PluginRegistryScope; onlyPluginIds?: string[] } | undefined { + const scopedChannel = resolveMessageSecretScope({ + channel: opts.channel, + target: opts.target, + targets: opts.targets, + }).channel; + if (scopedChannel) { + return { scope: "configured-channels", onlyPluginIds: [scopedChannel] }; + } + return { scope: "configured-channels" }; +} + export function createMessageCliHelpers( message: Command, messageChannelOptions: string, @@ -50,7 +65,7 @@ export function createMessageCliHelpers( const runMessageAction = async (action: string, opts: Record) => { setVerbose(Boolean(opts.verbose)); - ensurePluginRegistryLoaded(); + ensurePluginRegistryLoaded(resolveMessagePluginLoadOptions(opts)); const deps = createDefaultDeps(); let failed = false; await runCommandWithRuntime(