From 1777b99ccc39bf55e871c2b30599cd42d1bf2e38 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 18 Mar 2026 03:19:23 +0000 Subject: [PATCH] Signal: move message actions behind plugin boundary --- extensions/signal/src/channel.test.ts | 19 +++++++++++++++++ extensions/signal/src/channel.ts | 16 +------------- extensions/signal/src/index.ts | 1 + .../signal/src/message-actions.ts | 21 +++++++++---------- extensions/signal/src/send-reactions.test.ts | 12 +++++++---- src/channels/plugins/actions/actions.test.ts | 4 ++-- src/plugin-sdk/channel-runtime.ts | 1 + src/plugins/runtime/runtime-signal.ts | 2 +- src/plugins/runtime/types-channel.ts | 2 +- tsdown.config.ts | 1 - 10 files changed, 44 insertions(+), 35 deletions(-) rename src/channels/plugins/actions/signal.ts => extensions/signal/src/message-actions.ts (90%) diff --git a/extensions/signal/src/channel.test.ts b/extensions/signal/src/channel.test.ts index ee15deb0ec8..9a821a6f329 100644 --- a/extensions/signal/src/channel.test.ts +++ b/extensions/signal/src/channel.test.ts @@ -32,3 +32,22 @@ describe("signalPlugin outbound sendMedia", () => { ); }); }); + +describe("signalPlugin actions", () => { + it("owns unified message tool discovery", () => { + const discovery = signalPlugin.actions?.describeMessageTool?.({ + cfg: { + channels: { + signal: { + actions: { reactions: false }, + accounts: { + work: { account: "+15550001111", actions: { reactions: true } }, + }, + }, + }, + } as never, + }); + + expect(discovery?.actions).toEqual(["send", "react"]); + }); +}); diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index 80519620cc6..454eaa2cb9f 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -19,7 +19,6 @@ import { normalizeSignalMessagingTarget, PAIRING_APPROVED_MESSAGE, resolveChannelMediaMaxBytes, - type ChannelMessageActionAdapter, type ChannelPlugin, } from "openclaw/plugin-sdk/signal"; import { resolveSignalAccount, type ResolvedSignalAccount } from "./accounts.js"; @@ -30,25 +29,12 @@ import { resolveSignalRecipient, resolveSignalSender, } from "./identity.js"; +import { signalMessageActions } from "./message-actions.js"; import type { SignalProbe } from "./probe.js"; import { getSignalRuntime } from "./runtime.js"; import { signalSetupAdapter } from "./setup-core.js"; import { createSignalPluginBase, signalConfigAccessors, signalSetupWizard } from "./shared.js"; -const signalMessageActions: ChannelMessageActionAdapter = { - describeMessageTool: (ctx) => - getSignalRuntime().channel.signal.messageActions?.describeMessageTool?.(ctx) ?? null, - supportsAction: (ctx) => - getSignalRuntime().channel.signal.messageActions?.supportsAction?.(ctx) ?? false, - handleAction: async (ctx) => { - const ma = getSignalRuntime().channel.signal.messageActions; - if (!ma?.handleAction) { - throw new Error("Signal message actions not available"); - } - return ma.handleAction(ctx); - }, -}; - type SignalSendFn = ReturnType["channel"]["signal"]["sendMessageSignal"]; function resolveSignalSendContext(params: { diff --git a/extensions/signal/src/index.ts b/extensions/signal/src/index.ts index 29f2411493a..d8f18aa40f0 100644 --- a/extensions/signal/src/index.ts +++ b/extensions/signal/src/index.ts @@ -3,3 +3,4 @@ export { probeSignal } from "./probe.js"; export { sendMessageSignal } from "./send.js"; export { sendReactionSignal, removeReactionSignal } from "./send-reactions.js"; export { resolveSignalReactionLevel } from "./reaction-level.js"; +export { signalMessageActions } from "./message-actions.js"; diff --git a/src/channels/plugins/actions/signal.ts b/extensions/signal/src/message-actions.ts similarity index 90% rename from src/channels/plugins/actions/signal.ts rename to extensions/signal/src/message-actions.ts index 073496ab2e2..c6082848f02 100644 --- a/src/channels/plugins/actions/signal.ts +++ b/extensions/signal/src/message-actions.ts @@ -1,13 +1,14 @@ -import { createActionGate, jsonResult, readStringParam } from "../../../agents/tools/common.js"; -import { resolveSignalAccount } from "../../../plugin-sdk/account-resolution.js"; import { - listEnabledSignalAccounts, - removeReactionSignal, - resolveSignalReactionLevel, - sendReactionSignal, -} from "../../../plugin-sdk/signal.js"; -import type { ChannelMessageActionAdapter, ChannelMessageActionName } from "../types.js"; -import { resolveReactionMessageId } from "./reaction-message-id.js"; + createActionGate, + jsonResult, + readStringParam, + resolveReactionMessageId, + type ChannelMessageActionAdapter, + type ChannelMessageActionName, +} from "openclaw/plugin-sdk/channel-runtime"; +import { listEnabledSignalAccounts, resolveSignalAccount } from "./accounts.js"; +import { resolveSignalReactionLevel } from "./reaction-level.js"; +import { removeReactionSignal, sendReactionSignal } from "./send-reactions.js"; const providerId = "signal"; const GROUP_PREFIX = "group:"; @@ -103,7 +104,6 @@ export const signalMessageActions: ChannelMessageActionAdapter = { } if (action === "react") { - // Check reaction level first const reactionLevelInfo = resolveSignalReactionLevel({ cfg, accountId: accountId ?? undefined, @@ -115,7 +115,6 @@ export const signalMessageActions: ChannelMessageActionAdapter = { ); } - // Also check the action gate for backward compatibility const actionConfig = resolveSignalAccount({ cfg, accountId }).config.actions; const isActionEnabled = createActionGate(actionConfig); if (!isActionEnabled("reactions")) { diff --git a/extensions/signal/src/send-reactions.test.ts b/extensions/signal/src/send-reactions.test.ts index 47f0bbd8814..698d836df0e 100644 --- a/extensions/signal/src/send-reactions.test.ts +++ b/extensions/signal/src/send-reactions.test.ts @@ -1,10 +1,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { removeReactionSignal, sendReactionSignal } from "./send-reactions.js"; const rpcMock = vi.fn(); -vi.mock("../../../src/config/config.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, loadConfig: () => ({}), @@ -25,9 +24,14 @@ vi.mock("./client.js", () => ({ signalRpcRequest: (...args: unknown[]) => rpcMock(...args), })); +let sendReactionSignal: typeof import("./send-reactions.js").sendReactionSignal; +let removeReactionSignal: typeof import("./send-reactions.js").removeReactionSignal; + describe("sendReactionSignal", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); rpcMock.mockClear().mockResolvedValue({ timestamp: 123 }); + ({ sendReactionSignal, removeReactionSignal } = await import("./send-reactions.js")); }); it("uses recipients array and targetAuthor for uuid dms", async () => { diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index fca5b468066..f740a6da4ea 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -28,7 +28,7 @@ vi.mock("../../../../extensions/slack/src/action-runtime.js", () => ({ let discordMessageActions: typeof import("./discord.js").discordMessageActions; let handleDiscordMessageAction: typeof import("./discord/handle-action.js").handleDiscordMessageAction; let telegramMessageActions: typeof import("./telegram.js").telegramMessageActions; -let signalMessageActions: typeof import("./signal.js").signalMessageActions; +let signalMessageActions: typeof import("../../../../extensions/signal/src/message-actions.js").signalMessageActions; let createSlackActions: typeof import("../../../../extensions/slack/src/channel-actions.js").createSlackActions; function getDescribedActions(params: { @@ -204,7 +204,7 @@ beforeEach(async () => { ({ discordMessageActions } = await import("./discord.js")); ({ handleDiscordMessageAction } = await import("./discord/handle-action.js")); ({ telegramMessageActions } = await import("./telegram.js")); - ({ signalMessageActions } = await import("./signal.js")); + ({ signalMessageActions } = await import("../../../../extensions/signal/src/message-actions.js")); ({ createSlackActions } = await import("../../../../extensions/slack/src/channel-actions.js")); vi.clearAllMocks(); }); diff --git a/src/plugin-sdk/channel-runtime.ts b/src/plugin-sdk/channel-runtime.ts index cf916194580..0479efd5820 100644 --- a/src/plugin-sdk/channel-runtime.ts +++ b/src/plugin-sdk/channel-runtime.ts @@ -44,6 +44,7 @@ export * from "../channels/plugins/whatsapp-heartbeat.js"; export * from "../infra/outbound/send-deps.js"; export * from "../polls.js"; export * from "../utils/message-channel.js"; +export { createActionGate, jsonResult, readStringParam } from "../agents/tools/common.js"; export * from "./channel-lifecycle.js"; export type { InteractiveButtonStyle, diff --git a/src/plugins/runtime/runtime-signal.ts b/src/plugins/runtime/runtime-signal.ts index dc83f3fd1e2..5eade131012 100644 --- a/src/plugins/runtime/runtime-signal.ts +++ b/src/plugins/runtime/runtime-signal.ts @@ -1,9 +1,9 @@ import { monitorSignalProvider, probeSignal, + signalMessageActions, sendMessageSignal, } from "../../../extensions/signal/runtime-api.js"; -import { signalMessageActions } from "../../channels/plugins/actions/signal.js"; import type { PluginRuntimeChannel } from "./types-channel.js"; export function createRuntimeSignal(): PluginRuntimeChannel["signal"] { diff --git a/src/plugins/runtime/types-channel.ts b/src/plugins/runtime/types-channel.ts index f13dd010c0e..0f98d85ed90 100644 --- a/src/plugins/runtime/types-channel.ts +++ b/src/plugins/runtime/types-channel.ts @@ -197,7 +197,7 @@ export type PluginRuntimeChannel = { probeSignal: typeof import("../../../extensions/signal/runtime-api.js").probeSignal; sendMessageSignal: typeof import("../../../extensions/signal/runtime-api.js").sendMessageSignal; monitorSignalProvider: typeof import("../../../extensions/signal/runtime-api.js").monitorSignalProvider; - messageActions: typeof import("../../channels/plugins/actions/signal.js").signalMessageActions; + messageActions: typeof import("../../../extensions/signal/runtime-api.js").signalMessageActions; }; imessage: { monitorIMessageProvider: typeof import("../../../extensions/imessage/runtime-api.js").monitorIMessageProvider; diff --git a/tsdown.config.ts b/tsdown.config.ts index 48e69927f98..58a578d812b 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -164,7 +164,6 @@ function buildCoreDistEntries(): Record { "channels/plugins/agent-tools/whatsapp-login": "src/channels/plugins/agent-tools/whatsapp-login.ts", "channels/plugins/actions/discord": "src/channels/plugins/actions/discord.ts", - "channels/plugins/actions/signal": "src/channels/plugins/actions/signal.ts", "channels/plugins/actions/telegram": "src/channels/plugins/actions/telegram.ts", "telegram/audit": "extensions/telegram/src/audit.ts", "telegram/token": "extensions/telegram/src/token.ts",