Signal: move message actions behind plugin boundary

This commit is contained in:
Gustavo Madeira Santana
2026-03-18 03:19:23 +00:00
parent 56066dccb0
commit 1777b99ccc
10 changed files with 44 additions and 35 deletions

View File

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

View File

@@ -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<typeof getSignalRuntime>["channel"]["signal"]["sendMessageSignal"];
function resolveSignalSendContext(params: {

View File

@@ -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";

View File

@@ -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")) {

View File

@@ -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<typeof import("../../../src/config/config.js")>();
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
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 () => {

View File

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

View File

@@ -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,

View File

@@ -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"] {

View File

@@ -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;

View File

@@ -164,7 +164,6 @@ function buildCoreDistEntries(): Record<string, string> {
"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",