mirror of https://github.com/openclaw/openclaw.git
Plugin SDK: require unified message discovery
This commit is contained in:
parent
870f260772
commit
682f4d1ca3
|
|
@ -46,7 +46,7 @@ vi.mock("./probe.js", () => ({
|
|||
}));
|
||||
|
||||
describe("bluebubblesMessageActions", () => {
|
||||
const listActions = bluebubblesMessageActions.listActions!;
|
||||
const describeMessageTool = bluebubblesMessageActions.describeMessageTool!;
|
||||
const supportsAction = bluebubblesMessageActions.supportsAction!;
|
||||
const extractToolSend = bluebubblesMessageActions.extractToolSend!;
|
||||
const handleAction = bluebubblesMessageActions.handleAction!;
|
||||
|
|
@ -74,12 +74,12 @@ describe("bluebubblesMessageActions", () => {
|
|||
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValue(null);
|
||||
});
|
||||
|
||||
describe("listActions", () => {
|
||||
describe("describeMessageTool", () => {
|
||||
it("returns empty array when account is not enabled", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: { bluebubbles: { enabled: false } },
|
||||
};
|
||||
const actions = listActions({ cfg });
|
||||
const actions = describeMessageTool({ cfg })?.actions ?? [];
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ describe("bluebubblesMessageActions", () => {
|
|||
const cfg: OpenClawConfig = {
|
||||
channels: { bluebubbles: { enabled: true } },
|
||||
};
|
||||
const actions = listActions({ cfg });
|
||||
const actions = describeMessageTool({ cfg })?.actions ?? [];
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ describe("bluebubblesMessageActions", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const actions = listActions({ cfg });
|
||||
const actions = describeMessageTool({ cfg })?.actions ?? [];
|
||||
expect(actions).toContain("react");
|
||||
});
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ describe("bluebubblesMessageActions", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const actions = listActions({ cfg });
|
||||
const actions = describeMessageTool({ cfg })?.actions ?? [];
|
||||
expect(actions).not.toContain("react");
|
||||
// Other actions should still be present
|
||||
expect(actions).toContain("edit");
|
||||
|
|
@ -134,7 +134,7 @@ describe("bluebubblesMessageActions", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const actions = listActions({ cfg });
|
||||
const actions = describeMessageTool({ cfg })?.actions ?? [];
|
||||
expect(actions).toContain("sendAttachment");
|
||||
expect(actions).not.toContain("react");
|
||||
expect(actions).not.toContain("reply");
|
||||
|
|
|
|||
|
|
@ -67,10 +67,10 @@ const PRIVATE_API_ACTIONS = new Set<ChannelMessageActionName>([
|
|||
]);
|
||||
|
||||
export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg, currentChannelId }) => {
|
||||
describeMessageTool: ({ cfg, currentChannelId }) => {
|
||||
const account = resolveBlueBubblesAccount({ cfg: cfg });
|
||||
if (!account.enabled || !account.configured) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
const gate = createActionGate(cfg.channels?.bluebubbles?.actions);
|
||||
const actions = new Set<ChannelMessageActionName>();
|
||||
|
|
@ -107,7 +107,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
|||
}
|
||||
}
|
||||
}
|
||||
return Array.from(actions);
|
||||
return { actions: Array.from(actions) };
|
||||
},
|
||||
supportsAction: ({ action }) => SUPPORTED_ACTIONS.has(action),
|
||||
extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export async function fetchBlueBubblesServerInfo(params: {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get cached server info synchronously (for use in listActions).
|
||||
* Get cached server info synchronously (for use in describeMessageTool).
|
||||
* Returns null if not cached or expired.
|
||||
*/
|
||||
export function getCachedBlueBubblesServerInfo(accountId?: string): BlueBubblesServerInfo | null {
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@ function resolveAppUserNames(account: { config: { botUser?: string | null } }) {
|
|||
}
|
||||
|
||||
export const googlechatMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const accounts = listEnabledAccounts(cfg);
|
||||
if (accounts.length === 0) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
const actions = new Set<ChannelMessageActionName>([]);
|
||||
actions.add("send");
|
||||
|
|
@ -62,7 +62,7 @@ export const googlechatMessageActions: ChannelMessageActionAdapter = {
|
|||
actions.add("react");
|
||||
actions.add("reactions");
|
||||
}
|
||||
return Array.from(actions);
|
||||
return { actions: Array.from(actions) };
|
||||
},
|
||||
extractToolSend: ({ args }) => {
|
||||
return extractToolSend(args, "sendMessage");
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const resolveGoogleChatDmPolicy = createScopedDmSecurityResolver<ResolvedGoogleC
|
|||
});
|
||||
|
||||
const googlechatActions: ChannelMessageActionAdapter = {
|
||||
listActions: (ctx) => googlechatMessageActions.listActions?.(ctx) ?? [],
|
||||
describeMessageTool: (ctx) => googlechatMessageActions.describeMessageTool?.(ctx) ?? null,
|
||||
extractToolSend: (ctx) => googlechatMessageActions.extractToolSend?.(ctx) ?? null,
|
||||
handleAction: async (ctx) => {
|
||||
if (!googlechatMessageActions.handleAction) {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import { handleMatrixAction } from "./tool-actions.js";
|
|||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
export const matrixMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const account = resolveMatrixAccount({ cfg: cfg as CoreConfig });
|
||||
if (!account.enabled || !account.configured) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
const gate = createActionGate((cfg as CoreConfig).channels?.matrix?.actions);
|
||||
const actions = new Set<ChannelMessageActionName>(["send", "poll"]);
|
||||
|
|
@ -39,7 +39,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
|
|||
if (gate("channelInfo")) {
|
||||
actions.add("channel-info");
|
||||
}
|
||||
return Array.from(actions);
|
||||
return { actions: Array.from(actions) };
|
||||
},
|
||||
supportsAction: ({ action }) => action !== "poll",
|
||||
extractToolSend: ({ args }): ChannelToolSend | null => {
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ describe("mattermostPlugin", () => {
|
|||
expect(actions).toContain("send");
|
||||
});
|
||||
|
||||
it("respects per-account actions.reactions in listActions", () => {
|
||||
it("respects per-account actions.reactions in message discovery", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
mattermost: {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ import { signalSetupAdapter } from "./setup-core.js";
|
|||
import { createSignalPluginBase, signalConfigAccessors, signalSetupWizard } from "./shared.js";
|
||||
|
||||
const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: (ctx) => getSignalRuntime().channel.signal.messageActions?.listActions?.(ctx) ?? [],
|
||||
describeMessageTool: (ctx) =>
|
||||
getSignalRuntime().channel.signal.messageActions?.describeMessageTool?.(ctx) ?? null,
|
||||
supportsAction: (ctx) =>
|
||||
getSignalRuntime().channel.signal.messageActions?.supportsAction?.(ctx) ?? false,
|
||||
handleAction: async (ctx) => {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export const twitchMessageActions: ChannelMessageActionAdapter = {
|
|||
/**
|
||||
* List available actions for this channel.
|
||||
*/
|
||||
listActions: () => [...TWITCH_ACTIONS],
|
||||
describeMessageTool: () => ({ actions: [...TWITCH_ACTIONS] }),
|
||||
|
||||
/**
|
||||
* Check if an action is supported.
|
||||
|
|
|
|||
|
|
@ -108,9 +108,9 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
|||
listGroups: async (params) => listWhatsAppDirectoryGroupsFromConfig(params),
|
||||
},
|
||||
actions: {
|
||||
listActions: ({ cfg }) => {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
if (!cfg.channels?.whatsapp) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
const gate = createActionGate(cfg.channels.whatsapp.actions);
|
||||
const actions = new Set<ChannelMessageActionName>();
|
||||
|
|
@ -120,7 +120,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
|||
if (gate("polls")) {
|
||||
actions.add("poll");
|
||||
}
|
||||
return Array.from(actions);
|
||||
return { actions: Array.from(actions) };
|
||||
},
|
||||
supportsAction: ({ action }) => action === "react",
|
||||
handleAction: async ({ action, params, cfg, accountId }) => {
|
||||
|
|
|
|||
|
|
@ -21,15 +21,14 @@ function listEnabledAccounts(cfg: OpenClawConfig) {
|
|||
}
|
||||
|
||||
export const zaloMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const accounts = listEnabledAccounts(cfg);
|
||||
if (accounts.length === 0) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||
return Array.from(actions);
|
||||
return { actions: Array.from(actions), capabilities: [] };
|
||||
},
|
||||
getCapabilities: () => [],
|
||||
extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
|
||||
handleAction: async ({ action, params, cfg, accountId }) => {
|
||||
if (action === "send") {
|
||||
|
|
|
|||
|
|
@ -131,9 +131,10 @@ describe("zalouser channel policies", () => {
|
|||
|
||||
it("handles react action", async () => {
|
||||
const actions = zalouserPlugin.actions;
|
||||
expect(actions?.listActions?.({ cfg: { channels: { zalouser: { enabled: true } } } })).toEqual([
|
||||
"react",
|
||||
]);
|
||||
expect(
|
||||
actions?.describeMessageTool?.({ cfg: { channels: { zalouser: { enabled: true } } } })
|
||||
?.actions,
|
||||
).toEqual(["react"]);
|
||||
const result = await actions?.handleAction?.({
|
||||
channel: "zalouser",
|
||||
action: "react",
|
||||
|
|
|
|||
|
|
@ -218,14 +218,14 @@ function resolveZalouserRequireMention(params: ChannelGroupContext): boolean {
|
|||
}
|
||||
|
||||
const zalouserMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const accounts = listZalouserAccountIds(cfg)
|
||||
.map((accountId) => resolveZalouserAccountSync({ cfg, accountId }))
|
||||
.filter((account) => account.enabled);
|
||||
if (accounts.length === 0) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
return ["react"];
|
||||
return { actions: ["react"] };
|
||||
},
|
||||
supportsAction: ({ action }) => action === "react",
|
||||
handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ describe("channel tools", () => {
|
|||
resolveAccount: () => ({}),
|
||||
},
|
||||
actions: {
|
||||
listActions: () => {
|
||||
describeMessageTool: () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
},
|
||||
|
|
@ -70,7 +70,7 @@ describe("channel tools", () => {
|
|||
resolveAccount: () => ({}),
|
||||
},
|
||||
actions: {
|
||||
listActions: () => [],
|
||||
describeMessageTool: () => ({ actions: [] }),
|
||||
},
|
||||
outbound: {
|
||||
deliveryMode: "gateway",
|
||||
|
|
@ -102,7 +102,7 @@ describe("channel tools", () => {
|
|||
resolveAccount: () => ({}),
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["react"],
|
||||
describeMessageTool: () => ({ actions: ["react"] }),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -112,10 +112,7 @@ describe("channel tools", () => {
|
|||
expect(listChannelSupportedActions({ cfg, channel: "tg" })).toEqual(["react"]);
|
||||
});
|
||||
|
||||
it("uses unified message tool discovery when available", () => {
|
||||
const listActions = vi.fn(() => {
|
||||
throw new Error("legacy listActions should not run");
|
||||
});
|
||||
it("uses unified message tool discovery", () => {
|
||||
const plugin: ChannelPlugin = {
|
||||
id: "telegram",
|
||||
meta: {
|
||||
|
|
@ -134,7 +131,6 @@ describe("channel tools", () => {
|
|||
describeMessageTool: () => ({
|
||||
actions: ["react"],
|
||||
}),
|
||||
listActions,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -142,6 +138,5 @@ describe("channel tools", () => {
|
|||
|
||||
const cfg = {} as OpenClawConfig;
|
||||
expect(listChannelSupportedActions({ cfg, channel: "telegram" })).toEqual(["react"]);
|
||||
expect(listActions).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ let createMessageTool: CreateMessageTool;
|
|||
let setActivePluginRegistry: SetActivePluginRegistry;
|
||||
let createTestRegistry: CreateTestRegistry;
|
||||
|
||||
type DescribeMessageTool = NonNullable<
|
||||
NonNullable<ChannelPlugin["actions"]>["describeMessageTool"]
|
||||
>;
|
||||
type MessageToolDiscoveryContext = Parameters<DescribeMessageTool>[0];
|
||||
type MessageToolSchema = NonNullable<ReturnType<DescribeMessageTool>>["schema"];
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
runMessageAction: vi.fn(),
|
||||
loadConfig: vi.fn(() => ({})),
|
||||
|
|
@ -88,12 +94,11 @@ function createChannelPlugin(params: {
|
|||
blurb: string;
|
||||
aliases?: string[];
|
||||
actions?: ChannelMessageActionName[];
|
||||
listActions?: NonNullable<NonNullable<ChannelPlugin["actions"]>["listActions"]>;
|
||||
capabilities?: readonly ChannelMessageCapability[];
|
||||
toolSchema?: NonNullable<NonNullable<ChannelPlugin["actions"]>["getToolSchema"]>;
|
||||
toolSchema?: MessageToolSchema | ((params: MessageToolDiscoveryContext) => MessageToolSchema);
|
||||
describeMessageTool?: DescribeMessageTool;
|
||||
messaging?: ChannelPlugin["messaging"];
|
||||
}): ChannelPlugin {
|
||||
const actionCapabilities = params.capabilities;
|
||||
return {
|
||||
id: params.id as ChannelPlugin["id"],
|
||||
meta: {
|
||||
|
|
@ -111,15 +116,17 @@ function createChannelPlugin(params: {
|
|||
},
|
||||
...(params.messaging ? { messaging: params.messaging } : {}),
|
||||
actions: {
|
||||
listActions:
|
||||
params.listActions ??
|
||||
(() => {
|
||||
return (params.actions ?? []) as never;
|
||||
describeMessageTool:
|
||||
params.describeMessageTool ??
|
||||
((ctx) => {
|
||||
const schema =
|
||||
typeof params.toolSchema === "function" ? params.toolSchema(ctx) : params.toolSchema;
|
||||
return {
|
||||
actions: params.actions ?? [],
|
||||
capabilities: params.capabilities,
|
||||
...(schema ? { schema } : {}),
|
||||
};
|
||||
}),
|
||||
...(actionCapabilities
|
||||
? { getCapabilities: (_params: { cfg: unknown }) => actionCapabilities }
|
||||
: {}),
|
||||
...(params.toolSchema ? { getToolSchema: params.toolSchema } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -398,30 +405,29 @@ describe("message tool schema scoping", () => {
|
|||
label: "Telegram",
|
||||
docsPath: "/channels/telegram",
|
||||
blurb: "Telegram test plugin.",
|
||||
listActions: ({ cfg }) => {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const telegramCfg = (cfg as { channels?: { telegram?: { actions?: { poll?: boolean } } } })
|
||||
.channels?.telegram;
|
||||
return telegramCfg?.actions?.poll === false ? ["send", "react"] : ["send", "react", "poll"];
|
||||
},
|
||||
capabilities: ["interactive", "buttons"],
|
||||
toolSchema: ({ cfg }) => {
|
||||
const telegramCfg = (cfg as { channels?: { telegram?: { actions?: { poll?: boolean } } } })
|
||||
.channels?.telegram;
|
||||
return [
|
||||
{
|
||||
properties: {
|
||||
buttons: createMessageToolButtonsSchema(),
|
||||
return {
|
||||
actions:
|
||||
telegramCfg?.actions?.poll === false ? ["send", "react"] : ["send", "react", "poll"],
|
||||
capabilities: ["interactive", "buttons"],
|
||||
schema: [
|
||||
{
|
||||
properties: {
|
||||
buttons: createMessageToolButtonsSchema(),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(telegramCfg?.actions?.poll === false
|
||||
? []
|
||||
: [
|
||||
{
|
||||
properties: createTelegramPollExtraToolSchemas(),
|
||||
visibility: "all-configured" as const,
|
||||
},
|
||||
]),
|
||||
];
|
||||
...(telegramCfg?.actions?.poll === false
|
||||
? []
|
||||
: [
|
||||
{
|
||||
properties: createTelegramPollExtraToolSchemas(),
|
||||
visibility: "all-configured" as const,
|
||||
},
|
||||
]),
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -458,13 +464,11 @@ describe("message tool schema scoping", () => {
|
|||
label: "Telegram",
|
||||
docsPath: "/channels/telegram",
|
||||
blurb: "Telegram test plugin.",
|
||||
actions: ["send"],
|
||||
toolSchema: () => null,
|
||||
describeMessageTool: ({ accountId }) => ({
|
||||
actions: ["send"],
|
||||
capabilities: accountId === "ops" ? ["interactive"] : [],
|
||||
}),
|
||||
});
|
||||
scopedInteractivePlugin.actions = {
|
||||
...scopedInteractivePlugin.actions,
|
||||
getCapabilities: ({ accountId }) => (accountId === "ops" ? ["interactive"] : []),
|
||||
};
|
||||
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
|
|
@ -499,12 +503,10 @@ describe("message tool schema scoping", () => {
|
|||
label: "Telegram",
|
||||
docsPath: "/channels/telegram",
|
||||
blurb: "Telegram test plugin.",
|
||||
actions: ["send"],
|
||||
describeMessageTool: ({ accountId }) => ({
|
||||
actions: accountId === "ops" ? ["react"] : [],
|
||||
}),
|
||||
});
|
||||
scopedOtherPlugin.actions = {
|
||||
...scopedOtherPlugin.actions,
|
||||
listActions: ({ accountId }) => (accountId === "ops" ? ["react"] : []),
|
||||
};
|
||||
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
|
|
@ -536,22 +538,14 @@ describe("message tool schema scoping", () => {
|
|||
label: "Discord",
|
||||
docsPath: "/channels/discord",
|
||||
blurb: "Discord context plugin.",
|
||||
listActions: (ctx) => {
|
||||
seenContexts.push({ phase: "listActions", ...ctx });
|
||||
return ["send", "react"];
|
||||
},
|
||||
toolSchema: (ctx) => {
|
||||
seenContexts.push({ phase: "getToolSchema", ...ctx });
|
||||
return null;
|
||||
describeMessageTool: (ctx) => {
|
||||
seenContexts.push({ phase: "describeMessageTool", ...ctx });
|
||||
return {
|
||||
actions: ["send", "react"],
|
||||
capabilities: ["interactive"],
|
||||
};
|
||||
},
|
||||
});
|
||||
contextPlugin.actions = {
|
||||
...contextPlugin.actions,
|
||||
getCapabilities: (ctx) => {
|
||||
seenContexts.push({ phase: "getCapabilities", ...ctx });
|
||||
return ["interactive"];
|
||||
},
|
||||
};
|
||||
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "discord", source: "test", plugin: contextPlugin }]),
|
||||
|
|
@ -595,7 +589,7 @@ describe("message tool description", () => {
|
|||
label: "BlueBubbles",
|
||||
docsPath: "/channels/bluebubbles",
|
||||
blurb: "BlueBubbles test plugin.",
|
||||
listActions: ({ currentChannelId }) => {
|
||||
describeMessageTool: ({ currentChannelId }) => {
|
||||
const all: ChannelMessageActionName[] = [
|
||||
"react",
|
||||
"renameGroup",
|
||||
|
|
@ -606,15 +600,17 @@ describe("message tool description", () => {
|
|||
const lowered = currentChannelId?.toLowerCase() ?? "";
|
||||
const isDmTarget =
|
||||
lowered.includes("chat_guid:imessage;-;") || lowered.includes("chat_guid:sms;-;");
|
||||
return isDmTarget
|
||||
? all.filter(
|
||||
(action) =>
|
||||
action !== "renameGroup" &&
|
||||
action !== "addParticipant" &&
|
||||
action !== "removeParticipant" &&
|
||||
action !== "leaveGroup",
|
||||
)
|
||||
: all;
|
||||
return {
|
||||
actions: isDmTarget
|
||||
? all.filter(
|
||||
(action) =>
|
||||
action !== "renameGroup" &&
|
||||
action !== "addParticipant" &&
|
||||
action !== "removeParticipant" &&
|
||||
action !== "leaveGroup",
|
||||
)
|
||||
: all,
|
||||
};
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: (raw) => {
|
||||
|
|
|
|||
|
|
@ -1089,7 +1089,7 @@ describe("signalMessageActions", () => {
|
|||
|
||||
for (const testCase of cases) {
|
||||
expect(
|
||||
signalMessageActions.listActions?.({ cfg: testCase.cfg }) ?? [],
|
||||
signalMessageActions.describeMessageTool?.({ cfg: testCase.cfg })?.actions ?? [],
|
||||
testCase.name,
|
||||
).toEqual(testCase.expected);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,14 +74,14 @@ async function mutateSignalReaction(params: {
|
|||
}
|
||||
|
||||
export const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const accounts = listEnabledSignalAccounts(cfg);
|
||||
if (accounts.length === 0) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
const configuredAccounts = accounts.filter((account) => account.configured);
|
||||
if (configuredAccounts.length === 0) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
|
||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||
|
|
@ -93,7 +93,7 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
|
|||
actions.add("react");
|
||||
}
|
||||
|
||||
return Array.from(actions);
|
||||
return { actions: Array.from(actions) };
|
||||
},
|
||||
supportsAction: ({ action }) => action !== "send",
|
||||
|
||||
|
|
|
|||
|
|
@ -43,16 +43,10 @@ function resolveContractMessageDiscovery(params: {
|
|||
capabilities: [] as readonly ChannelMessageCapability[],
|
||||
};
|
||||
}
|
||||
if (actions.describeMessageTool) {
|
||||
const discovery = actions.describeMessageTool({ cfg: params.cfg }) ?? null;
|
||||
return {
|
||||
actions: Array.isArray(discovery?.actions) ? [...discovery.actions] : [],
|
||||
capabilities: Array.isArray(discovery?.capabilities) ? discovery.capabilities : [],
|
||||
};
|
||||
}
|
||||
const discovery = actions.describeMessageTool({ cfg: params.cfg }) ?? null;
|
||||
return {
|
||||
actions: actions.listActions?.({ cfg: params.cfg }) ?? [],
|
||||
capabilities: actions.getCapabilities?.({ cfg: params.cfg }) ?? [],
|
||||
actions: Array.isArray(discovery?.actions) ? [...discovery.actions] : [],
|
||||
capabilities: Array.isArray(discovery?.capabilities) ? discovery.capabilities : [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -156,10 +150,7 @@ export function installChannelActionsContractSuite(params: {
|
|||
}) {
|
||||
it("exposes the base message actions contract", () => {
|
||||
expect(params.plugin.actions).toBeDefined();
|
||||
expect(
|
||||
typeof params.plugin.actions?.describeMessageTool === "function" ||
|
||||
typeof params.plugin.actions?.listActions === "function",
|
||||
).toBe(true);
|
||||
expect(typeof params.plugin.actions?.describeMessageTool).toBe("function");
|
||||
});
|
||||
|
||||
for (const testCase of params.cases) {
|
||||
|
|
@ -223,10 +214,7 @@ export function installChannelSurfaceContractSuite(params: {
|
|||
it(`exposes the ${surface} surface contract`, () => {
|
||||
if (surface === "actions") {
|
||||
expect(plugin.actions).toBeDefined();
|
||||
expect(
|
||||
typeof plugin.actions?.describeMessageTool === "function" ||
|
||||
typeof plugin.actions?.listActions === "function",
|
||||
).toBe(true);
|
||||
expect(typeof plugin.actions?.describeMessageTool).toBe("function");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export function createMessageActionDiscoveryContext(
|
|||
|
||||
function logMessageActionError(params: {
|
||||
pluginId: string;
|
||||
operation: "describeMessageTool" | "getCapabilities" | "getToolSchema" | "listActions";
|
||||
operation: "describeMessageTool";
|
||||
error: unknown;
|
||||
}) {
|
||||
const message = params.error instanceof Error ? params.error.message : String(params.error);
|
||||
|
|
@ -75,24 +75,6 @@ function logMessageActionError(params: {
|
|||
);
|
||||
}
|
||||
|
||||
function runListActionsSafely(params: {
|
||||
pluginId: string;
|
||||
context: ChannelMessageActionDiscoveryContext;
|
||||
listActions: NonNullable<ChannelActions["listActions"]>;
|
||||
}): ChannelMessageActionName[] {
|
||||
try {
|
||||
const listed = params.listActions(params.context);
|
||||
return Array.isArray(listed) ? listed : [];
|
||||
} catch (error) {
|
||||
logMessageActionError({
|
||||
pluginId: params.pluginId,
|
||||
operation: "listActions",
|
||||
error,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function describeMessageToolSafely(params: {
|
||||
pluginId: string;
|
||||
context: ChannelMessageActionDiscoveryContext;
|
||||
|
|
@ -110,44 +92,6 @@ function describeMessageToolSafely(params: {
|
|||
}
|
||||
}
|
||||
|
||||
function listCapabilitiesSafely(params: {
|
||||
pluginId: string;
|
||||
actions: ChannelActions;
|
||||
context: ChannelMessageActionDiscoveryContext;
|
||||
}): readonly ChannelMessageCapability[] {
|
||||
try {
|
||||
return params.actions.getCapabilities?.(params.context) ?? [];
|
||||
} catch (error) {
|
||||
logMessageActionError({
|
||||
pluginId: params.pluginId,
|
||||
operation: "getCapabilities",
|
||||
error,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function runGetToolSchemaSafely(params: {
|
||||
pluginId: string;
|
||||
context: ChannelMessageActionDiscoveryContext;
|
||||
getToolSchema: NonNullable<ChannelActions["getToolSchema"]>;
|
||||
}):
|
||||
| ChannelMessageToolSchemaContribution
|
||||
| ChannelMessageToolSchemaContribution[]
|
||||
| null
|
||||
| undefined {
|
||||
try {
|
||||
return params.getToolSchema(params.context);
|
||||
} catch (error) {
|
||||
logMessageActionError({
|
||||
pluginId: params.pluginId,
|
||||
operation: "getToolSchema",
|
||||
error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeToolSchemaContributions(
|
||||
value:
|
||||
| ChannelMessageToolSchemaContribution
|
||||
|
|
@ -184,52 +128,21 @@ export function resolveMessageActionDiscoveryForPlugin(params: {
|
|||
};
|
||||
}
|
||||
|
||||
if (adapter.describeMessageTool) {
|
||||
const described = describeMessageToolSafely({
|
||||
pluginId: params.pluginId,
|
||||
context: params.context,
|
||||
describeMessageTool: adapter.describeMessageTool,
|
||||
});
|
||||
return {
|
||||
actions:
|
||||
params.includeActions && Array.isArray(described?.actions) ? [...described.actions] : [],
|
||||
capabilities:
|
||||
params.includeCapabilities && Array.isArray(described?.capabilities)
|
||||
? described.capabilities
|
||||
: [],
|
||||
schemaContributions: params.includeSchema
|
||||
? normalizeToolSchemaContributions(described?.schema)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
const described = describeMessageToolSafely({
|
||||
pluginId: params.pluginId,
|
||||
context: params.context,
|
||||
describeMessageTool: adapter.describeMessageTool,
|
||||
});
|
||||
return {
|
||||
actions:
|
||||
params.includeActions && adapter.listActions
|
||||
? runListActionsSafely({
|
||||
pluginId: params.pluginId,
|
||||
context: params.context,
|
||||
listActions: adapter.listActions,
|
||||
})
|
||||
: [],
|
||||
params.includeActions && Array.isArray(described?.actions) ? [...described.actions] : [],
|
||||
capabilities:
|
||||
params.includeCapabilities && adapter.getCapabilities
|
||||
? listCapabilitiesSafely({
|
||||
pluginId: params.pluginId,
|
||||
actions: adapter,
|
||||
context: params.context,
|
||||
})
|
||||
: [],
|
||||
schemaContributions:
|
||||
params.includeSchema && adapter.getToolSchema
|
||||
? normalizeToolSchemaContributions(
|
||||
runGetToolSchemaSafely({
|
||||
pluginId: params.pluginId,
|
||||
context: params.context,
|
||||
getToolSchema: adapter.getToolSchema,
|
||||
}),
|
||||
)
|
||||
params.includeCapabilities && Array.isArray(described?.capabilities)
|
||||
? described.capabilities
|
||||
: [],
|
||||
schemaContributions: params.includeSchema
|
||||
? normalizeToolSchemaContributions(described?.schema)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const discordPlugin: ChannelPlugin = {
|
|||
},
|
||||
}),
|
||||
actions: {
|
||||
listActions: () => ["kick"],
|
||||
describeMessageTool: () => ({ actions: ["kick"] }),
|
||||
supportsAction: ({ action }) => action === "kick",
|
||||
requiresTrustedRequesterSender: ({ action, toolContext }) =>
|
||||
Boolean(action === "kick" && toolContext),
|
||||
|
|
|
|||
|
|
@ -41,8 +41,10 @@ function createMessageActionsPlugin(params: {
|
|||
...(params.aliases ? { aliases: params.aliases } : {}),
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
getCapabilities: () => params.capabilities,
|
||||
describeMessageTool: () => ({
|
||||
actions: ["send"],
|
||||
capabilities: params.capabilities,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -161,16 +163,7 @@ describe("message action capability checks", () => {
|
|||
).toEqual(["cards"]);
|
||||
});
|
||||
|
||||
it("prefers unified message tool discovery over legacy discovery methods", () => {
|
||||
const legacyListActions = vi.fn(() => {
|
||||
throw new Error("legacy listActions should not run");
|
||||
});
|
||||
const legacyCapabilities = vi.fn(() => {
|
||||
throw new Error("legacy getCapabilities should not run");
|
||||
});
|
||||
const legacySchema = vi.fn(() => {
|
||||
throw new Error("legacy getToolSchema should not run");
|
||||
});
|
||||
it("uses unified message tool discovery for actions, capabilities, and schema", () => {
|
||||
const unifiedPlugin: ChannelPlugin = {
|
||||
...createChannelTestPluginBase({
|
||||
id: "discord",
|
||||
|
|
@ -190,9 +183,6 @@ describe("message action capability checks", () => {
|
|||
},
|
||||
},
|
||||
}),
|
||||
listActions: legacyListActions,
|
||||
getCapabilities: legacyCapabilities,
|
||||
getToolSchema: legacySchema,
|
||||
},
|
||||
};
|
||||
setActivePluginRegistry(
|
||||
|
|
@ -207,9 +197,6 @@ describe("message action capability checks", () => {
|
|||
channel: "discord",
|
||||
}),
|
||||
).toHaveProperty("components");
|
||||
expect(legacyListActions).not.toHaveBeenCalled();
|
||||
expect(legacyCapabilities).not.toHaveBeenCalled();
|
||||
expect(legacySchema).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips crashing action/capability discovery paths and logs once", () => {
|
||||
|
|
@ -223,10 +210,7 @@ describe("message action capability checks", () => {
|
|||
},
|
||||
}),
|
||||
actions: {
|
||||
listActions: () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
getCapabilities: () => {
|
||||
describeMessageTool: () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
},
|
||||
|
|
@ -237,10 +221,10 @@ describe("message action capability checks", () => {
|
|||
|
||||
expect(listChannelMessageActions({} as OpenClawConfig)).toEqual(["send", "broadcast"]);
|
||||
expect(listChannelMessageCapabilities({} as OpenClawConfig)).toEqual([]);
|
||||
expect(errorSpy).toHaveBeenCalledTimes(2);
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(listChannelMessageActions({} as OpenClawConfig)).toEqual(["send", "broadcast"]);
|
||||
expect(listChannelMessageCapabilities({} as OpenClawConfig)).toEqual([]);
|
||||
expect(errorSpy).toHaveBeenCalledTimes(2);
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -489,38 +489,14 @@ export type ChannelToolSend = {
|
|||
|
||||
export type ChannelMessageActionAdapter = {
|
||||
/**
|
||||
* Preferred unified discovery surface for the shared `message` tool.
|
||||
* When provided, this is authoritative and should return the scoped actions,
|
||||
* Unified discovery surface for the shared `message` tool.
|
||||
* This returns the scoped actions,
|
||||
* capabilities, and schema fragments together so they cannot drift.
|
||||
*/
|
||||
describeMessageTool?: (
|
||||
describeMessageTool: (
|
||||
params: ChannelMessageActionDiscoveryContext,
|
||||
) => ChannelMessageToolDiscovery | null | undefined;
|
||||
/**
|
||||
* Advertise agent-discoverable actions for this channel.
|
||||
* Legacy fallback used when `describeMessageTool` is not implemented.
|
||||
* Keep this aligned with any gated capability checks. Poll discovery is
|
||||
* not inferred from `outbound.sendPoll`, so channels that want agents to
|
||||
* create polls should include `"poll"` here when enabled.
|
||||
*/
|
||||
listActions?: (params: ChannelMessageActionDiscoveryContext) => ChannelMessageActionName[];
|
||||
supportsAction?: (params: { action: ChannelMessageActionName }) => boolean;
|
||||
getCapabilities?: (
|
||||
params: ChannelMessageActionDiscoveryContext,
|
||||
) => readonly ChannelMessageCapability[];
|
||||
/**
|
||||
* Extend the shared `message` tool schema with channel-owned fields.
|
||||
* Legacy fallback used when `describeMessageTool` is not implemented.
|
||||
* Keep this aligned with `listActions` and `getCapabilities` so the exposed
|
||||
* schema matches what the channel can actually execute in the current scope.
|
||||
*/
|
||||
getToolSchema?: (
|
||||
params: ChannelMessageActionDiscoveryContext,
|
||||
) =>
|
||||
| ChannelMessageToolSchemaContribution
|
||||
| ChannelMessageToolSchemaContribution[]
|
||||
| null
|
||||
| undefined;
|
||||
requiresTrustedRequesterSender?: (params: {
|
||||
action: ChannelMessageActionName;
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ function makeResolvedTokenPluginWithoutInspectAccount(): ChannelPlugin {
|
|||
isEnabled: () => true,
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ function createTokenOnlyPlugin() {
|
|||
isEnabled: () => true,
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ function buildPlugin(params: {
|
|||
}
|
||||
: undefined,
|
||||
actions: {
|
||||
listActions: () => ["poll"],
|
||||
describeMessageTool: () => ({ actions: ["poll"] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ const createDiscordPollPluginRegistration = () => ({
|
|||
id: "discord",
|
||||
label: "Discord",
|
||||
actions: {
|
||||
listActions: () => ["poll"],
|
||||
describeMessageTool: () => ({ actions: ["poll"] }),
|
||||
handleAction: (async ({ action, params, cfg, accountId }: ChannelActionParams) => {
|
||||
return await handleDiscordAction(
|
||||
{ action, to: params.to, accountId: accountId ?? undefined },
|
||||
|
|
@ -168,7 +168,7 @@ const createTelegramSendPluginRegistration = () => ({
|
|||
id: "telegram",
|
||||
label: "Telegram",
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
handleAction: (async ({ action, params, cfg, accountId }: ChannelActionParams) => {
|
||||
return await handleTelegramAction(
|
||||
{ action, to: params.to, accountId: accountId ?? undefined },
|
||||
|
|
@ -186,7 +186,7 @@ const createTelegramPollPluginRegistration = () => ({
|
|||
id: "telegram",
|
||||
label: "Telegram",
|
||||
actions: {
|
||||
listActions: () => ["poll"],
|
||||
describeMessageTool: () => ({ actions: ["poll"] }),
|
||||
handleAction: (async ({ action, params, cfg, accountId }: ChannelActionParams) => {
|
||||
return await handleTelegramAction(
|
||||
{ action, to: params.to, accountId: accountId ?? undefined },
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function makeMattermostPlugin(): ChannelPlugin {
|
|||
isEnabled: () => true,
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ function makeSlackHttpSummaryPlugin(): ChannelPlugin {
|
|||
isEnabled: () => true,
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -125,7 +125,7 @@ function makeTelegramSummaryPlugin(params: {
|
|||
}),
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -170,7 +170,7 @@ function makeSignalSummaryPlugin(params: { enabled: boolean; configured: boolean
|
|||
isEnabled: (account) => Boolean((account as { enabled?: boolean }).enabled),
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -208,7 +208,7 @@ function makeFallbackSummaryPlugin(params: {
|
|||
isEnabled: (account) => Boolean((account as { enabled?: boolean }).enabled),
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ describe("runMessageAction media behavior", () => {
|
|||
isConfigured: () => true,
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["sendAttachment", "setGroupIcon"],
|
||||
describeMessageTool: () => ({ actions: ["sendAttachment", "setGroupIcon"] }),
|
||||
supportsAction: ({ action }) => action === "sendAttachment" || action === "setGroupIcon",
|
||||
handleAction: async ({ params }) =>
|
||||
jsonResult({
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ describe("runMessageAction plugin dispatch", () => {
|
|||
capabilities: { chatTypes: ["direct", "channel"] },
|
||||
config: createAlwaysConfiguredPluginConfig(),
|
||||
actions: {
|
||||
listActions: () => ["pin", "list-pins", "member-info"],
|
||||
describeMessageTool: () => ({ actions: ["pin", "list-pins", "member-info"] }),
|
||||
supportsAction: ({ action }) =>
|
||||
action === "pin" || action === "list-pins" || action === "member-info",
|
||||
handleAction,
|
||||
|
|
@ -240,7 +240,7 @@ describe("runMessageAction plugin dispatch", () => {
|
|||
capabilities: { chatTypes: ["direct"] },
|
||||
config: createAlwaysConfiguredPluginConfig(),
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
supportsAction: ({ action }) => action === "send",
|
||||
handleAction,
|
||||
},
|
||||
|
|
@ -332,7 +332,7 @@ describe("runMessageAction plugin dispatch", () => {
|
|||
},
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["poll"],
|
||||
describeMessageTool: () => ({ actions: ["poll"] }),
|
||||
supportsAction: ({ action }) => action === "poll",
|
||||
handleAction,
|
||||
},
|
||||
|
|
@ -439,6 +439,7 @@ describe("runMessageAction plugin dispatch", () => {
|
|||
},
|
||||
},
|
||||
actions: {
|
||||
describeMessageTool: () => ({ actions: ["poll"] }),
|
||||
supportsAction: ({ action }) => action === "poll",
|
||||
handleAction,
|
||||
},
|
||||
|
|
@ -521,7 +522,7 @@ describe("runMessageAction plugin dispatch", () => {
|
|||
capabilities: { chatTypes: ["direct"] },
|
||||
config: createAlwaysConfiguredPluginConfig({}),
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
supportsAction: ({ action }) => action === "send",
|
||||
handleAction,
|
||||
},
|
||||
|
|
@ -603,7 +604,7 @@ describe("runMessageAction plugin dispatch", () => {
|
|||
resolveAccount: () => ({}),
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
handleAction,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export function makeDirectPlugin(params: {
|
|||
capabilities: { chatTypes: ["direct"] },
|
||||
config: params.config,
|
||||
actions: {
|
||||
listActions: () => ["send"],
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue