mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 15:50:20 +00:00
Plugin SDK: require unified message discovery
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user