Merge branch 'main' into feat/deepseek-provider

This commit is contained in:
07akioni
2026-03-18 10:24:55 +08:00
committed by GitHub
38 changed files with 192 additions and 176 deletions

View File

@@ -231,9 +231,9 @@ surface for the current turn.
For channel-owned execution helpers, bundled plugins should keep the execution
runtime inside their own extension modules. Core no longer owns the Discord,
Slack, Telegram, or WhatsApp message-action runtimes under `src/agents/tools`.
`agent-runtime` still re-exports the Discord and Telegram helpers for backward
compatibility, but we do not publish separate `plugin-sdk/*-action-runtime`
subpaths and new plugins should import their own local runtime code directly.
We do not publish separate `plugin-sdk/*-action-runtime` subpaths, and bundled
plugins should import their own local runtime code directly from their
extension-owned modules.
## Capability ownership model

View File

@@ -1,5 +1,4 @@
import {
createLegacyMessageToolDiscoveryMethods,
createDiscordMessageToolComponentsSchema,
createUnionActionGate,
listTokenSourcedAccounts,
@@ -133,7 +132,6 @@ function describeDiscordMessageTool({
export const discordMessageActions: ChannelMessageActionAdapter = {
describeMessageTool: describeDiscordMessageTool,
...createLegacyMessageToolDiscoveryMethods(describeDiscordMessageTool),
extractToolSend: ({ args }) => {
const action = typeof args.action === "string" ? args.action.trim() : "";
if (action === "sendMessage") {

View File

@@ -79,12 +79,6 @@ function formatDiscordIntents(intents?: {
const discordMessageActions: ChannelMessageActionAdapter = {
describeMessageTool: (ctx) =>
getDiscordRuntime().channel.discord.messageActions?.describeMessageTool?.(ctx) ?? null,
listActions: (ctx) =>
getDiscordRuntime().channel.discord.messageActions?.listActions?.(ctx) ?? [],
getCapabilities: (ctx) =>
getDiscordRuntime().channel.discord.messageActions?.getCapabilities?.(ctx) ?? [],
getToolSchema: (ctx) =>
getDiscordRuntime().channel.discord.messageActions?.getToolSchema?.(ctx) ?? null,
extractToolSend: (ctx) =>
getDiscordRuntime().channel.discord.messageActions?.extractToolSend?.(ctx) ?? null,
handleAction: async (ctx) => {

View File

@@ -54,6 +54,10 @@ vi.mock("./channel.runtime.js", () => ({
import { feishuPlugin } from "./channel.js";
function getDescribedActions(cfg: OpenClawConfig): string[] {
return [...(feishuPlugin.actions?.describeMessageTool?.({ cfg })?.actions ?? [])];
}
describe("feishuPlugin.status.probeAccount", () => {
it("uses current account credentials for multi-account config", async () => {
const cfg = {
@@ -112,7 +116,7 @@ describe("feishuPlugin actions", () => {
});
it("advertises the expanded Feishu action surface", () => {
expect(feishuPlugin.actions?.listActions?.({ cfg })).toEqual([
expect(getDescribedActions(cfg)).toEqual([
"send",
"read",
"edit",
@@ -142,7 +146,7 @@ describe("feishuPlugin actions", () => {
},
} as OpenClawConfig;
expect(feishuPlugin.actions?.listActions?.({ cfg: disabledCfg })).toEqual([
expect(getDescribedActions(disabledCfg)).toEqual([
"send",
"read",
"edit",

View File

@@ -1,10 +1,7 @@
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
import {
createLegacyMessageToolDiscoveryMethods,
createMessageToolCardSchema,
} from "openclaw/plugin-sdk/channel-runtime";
import { createMessageToolCardSchema } from "openclaw/plugin-sdk/channel-runtime";
import type {
ChannelMessageActionAdapter,
ChannelMessageToolDiscovery,
@@ -453,7 +450,6 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
},
actions: {
describeMessageTool: describeFeishuMessageTool,
...createLegacyMessageToolDiscoveryMethods(describeFeishuMessageTool),
handleAction: async (ctx) => {
const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId ?? undefined });
if (

View File

@@ -17,6 +17,10 @@ import {
withMockedGlobalFetch,
} from "./mattermost/reactions.test-helpers.js";
function getDescribedActions(cfg: OpenClawConfig): string[] {
return [...(mattermostPlugin.actions?.describeMessageTool?.({ cfg })?.actions ?? [])];
}
describe("mattermostPlugin", () => {
beforeEach(() => {
sendMessageMattermostMock.mockReset();
@@ -132,7 +136,7 @@ describe("mattermostPlugin", () => {
},
};
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
const actions = getDescribedActions(cfg);
expect(actions).toContain("react");
expect(actions).toContain("send");
expect(mattermostPlugin.actions?.supportsAction?.({ action: "react" })).toBe(true);
@@ -148,7 +152,7 @@ describe("mattermostPlugin", () => {
},
};
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
const actions = getDescribedActions(cfg);
expect(actions).toEqual([]);
});
@@ -164,7 +168,7 @@ describe("mattermostPlugin", () => {
},
};
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
const actions = getDescribedActions(cfg);
expect(actions).not.toContain("react");
expect(actions).toContain("send");
});
@@ -187,7 +191,7 @@ describe("mattermostPlugin", () => {
},
};
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
const actions = getDescribedActions(cfg);
expect(actions).toContain("react");
});

View File

@@ -4,24 +4,8 @@ import {
buildAccountScopedDmSecurityPolicy,
collectAllowlistProviderRestrictSendersWarnings,
} from "openclaw/plugin-sdk/channel-policy";
import {
createLegacyMessageToolDiscoveryMethods,
createMessageToolButtonsSchema,
} from "openclaw/plugin-sdk/channel-runtime";
import { createMessageToolButtonsSchema } from "openclaw/plugin-sdk/channel-runtime";
import type { ChannelMessageToolDiscovery } from "openclaw/plugin-sdk/channel-runtime";
import {
buildComputedAccountStatusSnapshot,
buildChannelConfigSchema,
createAccountStatusSink,
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
setAccountEnabledInConfigSection,
type ChannelMessageActionAdapter,
type ChannelMessageActionName,
type ChannelPlugin,
} from "./runtime-api.js";
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
import { MattermostConfigSchema } from "./config-schema.js";
import { resolveMattermostGroupRequireMention } from "./group-mentions.js";
@@ -42,6 +26,19 @@ import { addMattermostReaction, removeMattermostReaction } from "./mattermost/re
import { sendMessageMattermost } from "./mattermost/send.js";
import { resolveMattermostOpaqueTarget } from "./mattermost/target-resolution.js";
import { looksLikeMattermostTargetId, normalizeMattermostMessagingTarget } from "./normalize.js";
import {
buildComputedAccountStatusSnapshot,
buildChannelConfigSchema,
createAccountStatusSink,
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
setAccountEnabledInConfigSection,
type ChannelMessageActionAdapter,
type ChannelMessageActionName,
type ChannelPlugin,
} from "./runtime-api.js";
import { getMattermostRuntime } from "./runtime.js";
import { mattermostSetupAdapter } from "./setup-core.js";
import { mattermostSetupWizard } from "./setup-surface.js";
@@ -88,7 +85,6 @@ function describeMattermostMessageTool({
const mattermostMessageActions: ChannelMessageActionAdapter = {
describeMessageTool: describeMattermostMessageTool,
...createLegacyMessageToolDiscoveryMethods(describeMattermostMessageTool),
supportsAction: ({ action }) => {
return action === "send" || action === "react";
},

View File

@@ -1,3 +1,5 @@
import { z } from "zod";
import { requireChannelOpenAllowFrom } from "../../shared/config-schema-helpers.js";
import {
BlockStreamingCoalesceSchema,
DmPolicySchema,
@@ -5,8 +7,6 @@ import {
MarkdownConfigSchema,
requireOpenAllowFrom,
} from "./runtime-api.js";
import { z } from "zod";
import { requireChannelOpenAllowFrom } from "../../shared/config-schema-helpers.js";
import { buildSecretInputSchema } from "./secret-input.js";
const DmChannelRetrySchema = z

View File

@@ -1,6 +1,6 @@
import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
import type { ChannelGroupContext } from "./runtime-api.js";
import { resolveMattermostAccount } from "./mattermost/accounts.js";
import type { ChannelGroupContext } from "./runtime-api.js";
export function resolveMattermostGroupRequireMention(
params: ChannelGroupContext & { requireMentionOverride?: boolean },

View File

@@ -1,8 +1,4 @@
import type {
ChannelDirectoryEntry,
OpenClawConfig,
RuntimeEnv,
} from "../runtime-api.js";
import type { ChannelDirectoryEntry, OpenClawConfig, RuntimeEnv } from "../runtime-api.js";
import { listMattermostAccountIds, resolveMattermostAccount } from "./accounts.js";
import {
createMattermostClient,

View File

@@ -1,10 +1,6 @@
import { createHmac, timingSafeEqual } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import {
isTrustedProxyAddress,
resolveClientIp,
type OpenClawConfig,
} from "../runtime-api.js";
import { isTrustedProxyAddress, resolveClientIp, type OpenClawConfig } from "../runtime-api.js";
import { getMattermostRuntime } from "../runtime.js";
import { updateMattermostPost, type MattermostClient, type MattermostPost } from "./client.js";

View File

@@ -1,5 +1,5 @@
import type { ChannelAccountSnapshot, RuntimeEnv } from "../runtime-api.js";
import WebSocket from "ws";
import type { ChannelAccountSnapshot, RuntimeEnv } from "../runtime-api.js";
import type { MattermostPost } from "./client.js";
import { rawDataToString } from "./monitor-helpers.js";

View File

@@ -6,6 +6,7 @@
*/
import type { IncomingMessage, ServerResponse } from "node:http";
import type { ResolvedMattermostAccount } from "../mattermost/accounts.js";
import {
buildModelsProviderData,
createReplyPrefixOptions,
@@ -17,7 +18,6 @@ import {
type ReplyPayload,
type RuntimeEnv,
} from "../runtime-api.js";
import type { ResolvedMattermostAccount } from "../mattermost/accounts.js";
import { getMattermostRuntime } from "../runtime.js";
import {
createMattermostClient,

View File

@@ -1,5 +1,5 @@
import type { PluginRuntime } from "./runtime-api.js";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import type { PluginRuntime } from "./runtime-api.js";
const { setRuntime: setMattermostRuntime, getRuntime: getMattermostRuntime } =
createPluginRuntimeStore<PluginRuntime>("Mattermost runtime not initialized");

View File

@@ -1,4 +1,6 @@
import type { ChannelSetupAdapter } from "openclaw/plugin-sdk/channel-runtime";
import { resolveMattermostAccount, type ResolvedMattermostAccount } from "./mattermost/accounts.js";
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
import {
applyAccountNameToChannelSection,
applySetupAccountConfigPatch,
@@ -8,8 +10,6 @@ import {
normalizeAccountId,
type OpenClawConfig,
} from "./runtime-api.js";
import { resolveMattermostAccount, type ResolvedMattermostAccount } from "./mattermost/accounts.js";
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
const channel = "mattermost" as const;

View File

@@ -1,13 +1,13 @@
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
import { formatDocsLink } from "openclaw/plugin-sdk/setup";
import { listMattermostAccountIds } from "./mattermost/accounts.js";
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
import {
applySetupAccountConfigPatch,
DEFAULT_ACCOUNT_ID,
hasConfiguredSecretInput,
type OpenClawConfig,
} from "./runtime-api.js";
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
import { formatDocsLink } from "openclaw/plugin-sdk/setup";
import { listMattermostAccountIds } from "./mattermost/accounts.js";
import { normalizeMattermostBaseUrl } from "./mattermost/client.js";
import {
isMattermostConfigured,
mattermostSetupAdapter,

View File

@@ -1,9 +1,6 @@
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import { collectAllowlistProviderRestrictSendersWarnings } from "openclaw/plugin-sdk/channel-policy";
import {
createLegacyMessageToolDiscoveryMethods,
createMessageToolCardSchema,
} from "openclaw/plugin-sdk/channel-runtime";
import { createMessageToolCardSchema } from "openclaw/plugin-sdk/channel-runtime";
import type {
ChannelMessageActionAdapter,
ChannelMessageToolDiscovery,
@@ -398,7 +395,6 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
},
actions: {
describeMessageTool: describeMSTeamsMessageTool,
...createLegacyMessageToolDiscoveryMethods(describeMSTeamsMessageTool),
handleAction: async (ctx) => {
// Handle send action with card parameter
if (ctx.action === "send" && ctx.params.card) {

View File

@@ -1,7 +1,3 @@
import {
buildPassiveChannelStatusSummary,
buildTrafficStatusSummary,
} from "../../shared/channel-status-summary.js";
import {
buildChannelConfigSchema,
collectStatusIssuesFromLastError,
@@ -10,7 +6,11 @@ import {
formatPairingApproveHint,
mapAllowFromEntries,
type ChannelPlugin,
} from "../api.js";
} from "openclaw/plugin-sdk/nostr";
import {
buildPassiveChannelStatusSummary,
buildTrafficStatusSummary,
} from "../../shared/channel-status-summary.js";
import type { NostrProfile } from "./config-schema.js";
import { NostrConfigSchema } from "./config-schema.js";
import type { MetricEvent, MetricsSnapshot } from "./metrics.js";

View File

@@ -1,6 +1,6 @@
import { AllowFromListSchema, DmPolicySchema } from "openclaw/plugin-sdk/channel-config-schema";
import { MarkdownConfigSchema, buildChannelConfigSchema } from "openclaw/plugin-sdk/nostr";
import { z } from "zod";
import { MarkdownConfigSchema, buildChannelConfigSchema } from "../api.js";
/**
* Validates https:// URLs only (no javascript:, data:, file:, etc.)

View File

@@ -7,7 +7,6 @@ import {
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-runtime";
import {
createLegacyMessageToolDiscoveryMethods,
createMessageToolButtonsSchema,
createTelegramPollExtraToolSchemas,
createUnionActionGate,
@@ -178,7 +177,6 @@ function readTelegramMessageIdParam(params: Record<string, unknown>): number {
export const telegramMessageActions: ChannelMessageActionAdapter = {
describeMessageTool: describeTelegramMessageTool,
...createLegacyMessageToolDiscoveryMethods(describeTelegramMessageTool),
extractToolSend: ({ args }) => {
return extractToolSend(args, "sendMessage");
},

View File

@@ -250,12 +250,6 @@ function hasTelegramExecApprovalDmRoute(cfg: OpenClawConfig): boolean {
const telegramMessageActions: ChannelMessageActionAdapter = {
describeMessageTool: (ctx) =>
getTelegramRuntime().channel.telegram.messageActions?.describeMessageTool?.(ctx) ?? null,
listActions: (ctx) =>
getTelegramRuntime().channel.telegram.messageActions?.listActions?.(ctx) ?? [],
getCapabilities: (ctx) =>
getTelegramRuntime().channel.telegram.messageActions?.getCapabilities?.(ctx) ?? [],
getToolSchema: (ctx) =>
getTelegramRuntime().channel.telegram.messageActions?.getToolSchema?.(ctx) ?? null,
extractToolSend: (ctx) =>
getTelegramRuntime().channel.telegram.messageActions?.extractToolSend?.(ctx) ?? null,
handleAction: async (ctx) => {

View File

@@ -1,6 +1,11 @@
import crypto from "node:crypto";
import { configureClient } from "@tloncorp/api";
import type { ChannelOutboundAdapter, ChannelPlugin, OpenClawConfig } from "../api.js";
import type {
ChannelAccountSnapshot,
ChannelOutboundAdapter,
ChannelPlugin,
OpenClawConfig,
} from "../api.js";
import { createLoggerBackedRuntime, createReplyPrefixOptions } from "../api.js";
import { monitorTlonProvider } from "./monitor/index.js";
import { tlonSetupWizard } from "./setup-surface.js";

View File

@@ -1,4 +1,5 @@
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import type { ChannelAccountSnapshot, ChannelPlugin, OpenClawConfig } from "../api.js";
import { tlonChannelConfigSchema } from "./config-schema.js";
import {
applyTlonSetupConfig,
@@ -13,7 +14,6 @@ import {
resolveTlonOutboundTarget,
} from "./targets.js";
import { resolveTlonAccount, listTlonAccountIds } from "./types.js";
import type { ChannelAccountSnapshot, ChannelPlugin, OpenClawConfig } from "../api.js";
import { validateUrbitBaseUrl } from "./urbit/base-url.js";
const TLON_CHANNEL_ID = "tlon" as const;

View File

@@ -1,5 +1,5 @@
import { buildChannelConfigSchema } from "../api.js";
import { z } from "zod";
import { buildChannelConfigSchema } from "../api.js";
const ShipSchema = z.string().min(1);
const ChannelNestSchema = z.string().min(1);

View File

@@ -5,7 +5,7 @@ import { homedir } from "node:os";
import * as path from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { fetchWithSsrFGuard } from "../api.js";
import { fetchWithSsrFGuard } from "../../api.js";
import { getDefaultSsrFPolicy } from "../urbit/context.js";
// Default to OpenClaw workspace media directory

View File

@@ -0,0 +1 @@
export { handleWhatsAppAction } from "./src/action-runtime.js";

View File

@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js";
import type { ChannelMessageActionAdapter } from "../types.js";
const handleDiscordAction = vi.fn(async (..._args: unknown[]) => ({ details: { ok: true } }));
const handleTelegramAction = vi.fn(async (..._args: unknown[]) => ({ ok: true }));
@@ -30,6 +31,13 @@ let telegramMessageActions: typeof import("./telegram.js").telegramMessageAction
let signalMessageActions: typeof import("./signal.js").signalMessageActions;
let createSlackActions: typeof import("../slack.actions.js").createSlackActions;
function getDescribedActions(params: {
describeMessageTool?: ChannelMessageActionAdapter["describeMessageTool"];
cfg: OpenClawConfig;
}) {
return [...(params.describeMessageTool?.({ cfg: params.cfg })?.actions ?? [])];
}
function telegramCfg(): OpenClawConfig {
return { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
}
@@ -284,7 +292,10 @@ describe("discord message actions", () => {
] as const;
for (const testCase of cases) {
const actions = discordMessageActions.listActions?.({ cfg: testCase.cfg }) ?? [];
const actions = getDescribedActions({
describeMessageTool: discordMessageActions.describeMessageTool,
cfg: testCase.cfg,
});
if (testCase.expectUploads) {
expect(actions, testCase.name).toContain("emoji-upload");
expect(actions, testCase.name).toContain("sticker-upload");
@@ -629,7 +640,10 @@ describe("telegramMessageActions", () => {
expectTopicEdit: true,
},
]) {
const actions = telegramMessageActions.listActions?.({ cfg: testCase.cfg }) ?? [];
const actions = getDescribedActions({
describeMessageTool: telegramMessageActions.describeMessageTool,
cfg: testCase.cfg,
});
if (testCase.expectPoll) {
expect(actions, testCase.name).toContain("poll");
} else {
@@ -680,7 +694,10 @@ describe("telegramMessageActions", () => {
] as const;
for (const testCase of cases) {
const actions = telegramMessageActions.listActions?.({ cfg: testCase.cfg }) ?? [];
const actions = getDescribedActions({
describeMessageTool: telegramMessageActions.describeMessageTool,
cfg: testCase.cfg,
});
if (testCase.expectSticker) {
expect(actions, testCase.name).toContain("sticker");
expect(actions, testCase.name).toContain("sticker-search");
@@ -903,7 +920,10 @@ describe("telegramMessageActions", () => {
},
},
} as OpenClawConfig;
const actions = telegramMessageActions.listActions?.({ cfg }) ?? [];
const actions = getDescribedActions({
describeMessageTool: telegramMessageActions.describeMessageTool,
cfg,
});
expect(actions).toContain("sticker");
expect(actions).toContain("sticker-search");

View File

@@ -174,17 +174,14 @@ function expectClearedSessionBinding(params: {
).toBeNull();
}
const telegramListActionsMock = vi.fn();
const telegramGetCapabilitiesMock = vi.fn();
const discordListActionsMock = vi.fn();
const discordGetCapabilitiesMock = vi.fn();
const telegramDescribeMessageToolMock = vi.fn();
const discordDescribeMessageToolMock = vi.fn();
bundledChannelRuntimeSetters.setTelegramRuntime({
channel: {
telegram: {
messageActions: {
listActions: telegramListActionsMock,
getCapabilities: telegramGetCapabilitiesMock,
describeMessageTool: telegramDescribeMessageToolMock,
},
},
},
@@ -194,8 +191,7 @@ bundledChannelRuntimeSetters.setDiscordRuntime({
channel: {
discord: {
messageActions: {
listActions: discordListActionsMock,
getCapabilities: discordGetCapabilitiesMock,
describeMessageTool: discordDescribeMessageToolMock,
},
},
},
@@ -358,10 +354,11 @@ export const actionContractRegistry: ActionsContractEntry[] = [
expectedActions: ["send", "poll", "react"],
expectedCapabilities: ["interactive", "buttons"],
beforeTest: () => {
telegramListActionsMock.mockReset();
telegramGetCapabilitiesMock.mockReset();
telegramListActionsMock.mockReturnValue(["send", "poll", "react"]);
telegramGetCapabilitiesMock.mockReturnValue(["interactive", "buttons"]);
telegramDescribeMessageToolMock.mockReset();
telegramDescribeMessageToolMock.mockReturnValue({
actions: ["send", "poll", "react"],
capabilities: ["interactive", "buttons"],
});
},
},
],
@@ -376,10 +373,11 @@ export const actionContractRegistry: ActionsContractEntry[] = [
expectedActions: ["send", "react", "poll"],
expectedCapabilities: ["interactive", "components"],
beforeTest: () => {
discordListActionsMock.mockReset();
discordGetCapabilitiesMock.mockReset();
discordListActionsMock.mockReturnValue(["send", "react", "poll"]);
discordGetCapabilitiesMock.mockReturnValue(["interactive", "components"]);
discordDescribeMessageToolMock.mockReset();
discordDescribeMessageToolMock.mockReturnValue({
actions: ["send", "react", "poll"],
capabilities: ["interactive", "components"],
});
},
},
],

View File

@@ -32,6 +32,30 @@ function sortStrings(values: readonly string[]) {
return [...values].toSorted((left, right) => left.localeCompare(right));
}
function resolveContractMessageDiscovery(params: {
plugin: Pick<ChannelPlugin, "actions">;
cfg: OpenClawConfig;
}) {
const actions = params.plugin.actions;
if (!actions) {
return {
actions: [] as ChannelMessageActionName[],
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 : [],
};
}
return {
actions: actions.listActions?.({ cfg: params.cfg }) ?? [],
capabilities: actions.getCapabilities?.({ cfg: params.cfg }) ?? [],
};
}
const contractRuntime = createNonExitingRuntime();
function expectDirectoryEntryShape(entry: ChannelDirectoryEntry) {
expect(["user", "group", "channel"]).toContain(entry.kind);
@@ -132,15 +156,22 @@ export function installChannelActionsContractSuite(params: {
}) {
it("exposes the base message actions contract", () => {
expect(params.plugin.actions).toBeDefined();
expect(typeof params.plugin.actions?.listActions).toBe("function");
expect(
typeof params.plugin.actions?.describeMessageTool === "function" ||
typeof params.plugin.actions?.listActions === "function",
).toBe(true);
});
for (const testCase of params.cases) {
it(`actions contract: ${testCase.name}`, () => {
testCase.beforeTest?.();
const actions = params.plugin.actions?.listActions?.({ cfg: testCase.cfg }) ?? [];
const capabilities = params.plugin.actions?.getCapabilities?.({ cfg: testCase.cfg }) ?? [];
const discovery = resolveContractMessageDiscovery({
plugin: params.plugin,
cfg: testCase.cfg,
});
const actions = discovery.actions;
const capabilities = discovery.capabilities;
expect(actions).toEqual([...new Set(actions)]);
expect(capabilities).toEqual([...new Set(capabilities)]);
@@ -192,7 +223,10 @@ export function installChannelSurfaceContractSuite(params: {
it(`exposes the ${surface} surface contract`, () => {
if (surface === "actions") {
expect(plugin.actions).toBeDefined();
expect(typeof plugin.actions?.listActions).toBe("function");
expect(
typeof plugin.actions?.describeMessageTool === "function" ||
typeof plugin.actions?.listActions === "function",
).toBe(true);
return;
}

View File

@@ -1,15 +1,16 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { ChannelMessageActionAdapter, ChannelPlugin } from "./types.js";
const telegramGetCapabilitiesMock = vi.fn();
const discordGetCapabilitiesMock = vi.fn();
const telegramDescribeMessageToolMock = vi.fn();
const discordDescribeMessageToolMock = vi.fn();
vi.mock("../../../extensions/telegram/src/runtime.js", () => ({
getTelegramRuntime: () => ({
channel: {
telegram: {
messageActions: {
getCapabilities: telegramGetCapabilitiesMock,
describeMessageTool: telegramDescribeMessageToolMock,
},
},
},
@@ -21,7 +22,7 @@ vi.mock("../../../extensions/discord/src/runtime.js", () => ({
channel: {
discord: {
messageActions: {
getCapabilities: discordGetCapabilitiesMock,
describeMessageTool: discordDescribeMessageToolMock,
},
},
},
@@ -38,10 +39,16 @@ const { zaloPlugin } = await import("../../../extensions/zalo/src/channel.js");
describe("channel action capability matrix", () => {
afterEach(() => {
telegramGetCapabilitiesMock.mockReset();
discordGetCapabilitiesMock.mockReset();
telegramDescribeMessageToolMock.mockReset();
discordDescribeMessageToolMock.mockReset();
});
function getCapabilities(plugin: Pick<ChannelPlugin, "actions">, cfg: OpenClawConfig) {
const describeMessageTool: ChannelMessageActionAdapter["describeMessageTool"] | undefined =
plugin.actions?.describeMessageTool;
return [...(describeMessageTool?.({ cfg })?.capabilities ?? [])];
}
it("exposes Slack blocks by default and interactive when enabled", () => {
const baseCfg = {
channels: {
@@ -61,26 +68,27 @@ describe("channel action capability matrix", () => {
},
} as OpenClawConfig;
expect(slackPlugin.actions?.getCapabilities?.({ cfg: baseCfg })).toEqual(["blocks"]);
expect(slackPlugin.actions?.getCapabilities?.({ cfg: interactiveCfg })).toEqual([
"blocks",
"interactive",
]);
expect(getCapabilities(slackPlugin, baseCfg)).toEqual(["blocks"]);
expect(getCapabilities(slackPlugin, interactiveCfg)).toEqual(["blocks", "interactive"]);
});
it("forwards Telegram action capabilities through the channel wrapper", () => {
telegramGetCapabilitiesMock.mockReturnValue(["interactive", "buttons"]);
telegramDescribeMessageToolMock.mockReturnValue({
capabilities: ["interactive", "buttons"],
});
const result = telegramPlugin.actions?.getCapabilities?.({ cfg: {} as OpenClawConfig });
const result = getCapabilities(telegramPlugin, {} as OpenClawConfig);
expect(result).toEqual(["interactive", "buttons"]);
expect(telegramGetCapabilitiesMock).toHaveBeenCalledWith({ cfg: {} });
discordGetCapabilitiesMock.mockReturnValue(["interactive", "components"]);
expect(telegramDescribeMessageToolMock).toHaveBeenCalledWith({ cfg: {} });
discordDescribeMessageToolMock.mockReturnValue({
capabilities: ["interactive", "components"],
});
const discordResult = discordPlugin.actions?.getCapabilities?.({ cfg: {} as OpenClawConfig });
const discordResult = getCapabilities(discordPlugin, {} as OpenClawConfig);
expect(discordResult).toEqual(["interactive", "components"]);
expect(discordGetCapabilitiesMock).toHaveBeenCalledWith({ cfg: {} });
expect(discordDescribeMessageToolMock).toHaveBeenCalledWith({ cfg: {} });
});
it("exposes configured channel capabilities only when required credentials are present", () => {
@@ -139,18 +147,12 @@ describe("channel action capability matrix", () => {
},
} as OpenClawConfig;
expect(mattermostPlugin.actions?.getCapabilities?.({ cfg: configuredCfg })).toEqual([
"buttons",
]);
expect(mattermostPlugin.actions?.getCapabilities?.({ cfg: unconfiguredCfg })).toEqual([]);
expect(feishuPlugin.actions?.getCapabilities?.({ cfg: configuredFeishuCfg })).toEqual([
"cards",
]);
expect(feishuPlugin.actions?.getCapabilities?.({ cfg: disabledFeishuCfg })).toEqual([]);
expect(msteamsPlugin.actions?.getCapabilities?.({ cfg: configuredMsteamsCfg })).toEqual([
"cards",
]);
expect(msteamsPlugin.actions?.getCapabilities?.({ cfg: disabledMsteamsCfg })).toEqual([]);
expect(getCapabilities(mattermostPlugin, configuredCfg)).toEqual(["buttons"]);
expect(getCapabilities(mattermostPlugin, unconfiguredCfg)).toEqual([]);
expect(getCapabilities(feishuPlugin, configuredFeishuCfg)).toEqual(["cards"]);
expect(getCapabilities(feishuPlugin, disabledFeishuCfg)).toEqual([]);
expect(getCapabilities(msteamsPlugin, configuredMsteamsCfg)).toEqual(["cards"]);
expect(getCapabilities(msteamsPlugin, disabledMsteamsCfg)).toEqual([]);
});
it("keeps Zalo actions on the empty capability set", () => {
@@ -163,6 +165,6 @@ describe("channel action capability matrix", () => {
},
} as OpenClawConfig;
expect(zaloPlugin.actions?.getCapabilities?.({ cfg })).toEqual([]);
expect(getCapabilities(zaloPlugin, cfg)).toEqual([]);
});
});

View File

@@ -1,13 +0,0 @@
import type { ChannelMessageActionAdapter } from "./types.js";
export function createLegacyMessageToolDiscoveryMethods(
describeMessageTool: NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>,
): Pick<ChannelMessageActionAdapter, "listActions" | "getCapabilities" | "getToolSchema"> {
const describe = (ctx: Parameters<typeof describeMessageTool>[0]) =>
describeMessageTool(ctx) ?? null;
return {
listActions: (ctx) => [...(describe(ctx)?.actions ?? [])],
getCapabilities: (ctx) => [...(describe(ctx)?.capabilities ?? [])],
getToolSchema: (ctx) => describe(ctx)?.schema ?? null,
};
}

View File

@@ -10,7 +10,6 @@ import {
resolveSlackChannelId,
handleSlackMessageAction,
} from "../../plugin-sdk/slack.js";
import { createLegacyMessageToolDiscoveryMethods } from "./message-tool-legacy.js";
import { createSlackMessageToolBlocksSchema } from "./message-tool-schema.js";
import type { ChannelMessageActionAdapter, ChannelMessageToolDiscovery } from "./types.js";
@@ -52,7 +51,6 @@ export function createSlackActions(
return {
describeMessageTool,
...createLegacyMessageToolDiscoveryMethods(describeMessageTool),
extractToolSend: ({ args }) => extractSlackToolSend(args),
handleAction: async (ctx) => {
return await handleSlackMessageAction({

View File

@@ -1,5 +1,9 @@
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
import { getChannelPlugin, listChannelPlugins } from "../../channels/plugins/index.js";
import {
createMessageActionDiscoveryContext,
resolveMessageActionDiscoveryForPlugin,
} from "../../channels/plugins/message-action-discovery.js";
import type {
ChannelCapabilities,
ChannelCapabilitiesDiagnostics,
@@ -133,10 +137,6 @@ async function resolveChannelReports(params: {
: [resolveChannelDefaultAccountId({ plugin, cfg, accountIds: ids })];
})();
const reports: ChannelCapabilitiesReport[] = [];
const listedActions = plugin.actions?.listActions?.({ cfg }) ?? [];
const actions = Array.from(
new Set<string>(["send", "broadcast", ...listedActions.map((action) => String(action))]),
);
for (const accountId of accountIds) {
const resolvedAccount = plugin.config.resolveAccount(cfg, accountId);
@@ -169,6 +169,18 @@ async function resolveChannelReports(params: {
target: params.target,
})
: undefined;
const discoveredActions = resolveMessageActionDiscoveryForPlugin({
pluginId: plugin.id,
actions: plugin.actions,
context: createMessageActionDiscoveryContext({
cfg,
accountId,
}),
includeActions: true,
}).actions;
const actions = Array.from(
new Set<string>(["send", "broadcast", ...discoveredActions.map((action) => String(action))]),
);
reports.push({
channel: plugin.id,

View File

@@ -23,15 +23,3 @@ export * from "../agents/vllm-defaults.js";
// Intentional public runtime surface: channel plugins use ingress agent helpers directly.
export * from "../agents/agent-command.js";
export * from "../tts/tts.js";
// Legacy channel action runtime re-exports. New bundled plugin code should use
// local extension-owned modules instead of adding more public SDK surface here.
export {
handleDiscordAction,
readDiscordParentIdParam,
isDiscordModerationAction,
readDiscordModerationCommand,
} from "../../extensions/discord/runtime-api.js";
export {
handleTelegramAction,
readTelegramButtons,
} from "../../extensions/telegram/runtime-api.js";

View File

@@ -34,7 +34,6 @@ export type * from "../channels/plugins/types.js";
export * from "../channels/plugins/config-writes.js";
export * from "../channels/plugins/directory-config.js";
export * from "../channels/plugins/media-payload.js";
export * from "../channels/plugins/message-tool-legacy.js";
export * from "../channels/plugins/message-tool-schema.js";
export * from "../channels/plugins/normalize/signal.js";
export * from "../channels/plugins/normalize/whatsapp.js";

View File

@@ -27,5 +27,5 @@ export type { RuntimeEnv } from "../runtime.js";
export { formatDocsLink } from "../terminal/links.js";
export type { WizardPrompter } from "../wizard/prompts.js";
export { createLoggerBackedRuntime } from "./runtime.js";
export { tlonSetupAdapter } from "../../extensions/tlon/api.js";
export { tlonSetupWizard } from "../../extensions/tlon/api.js";
export { tlonSetupAdapter } from "../../extensions/tlon/src/setup-core.js";
export { tlonSetupWizard } from "../../extensions/tlon/src/setup-surface.js";

View File

@@ -68,7 +68,7 @@ let webLoginQrPromise: Promise<
> | null = null;
let webChannelPromise: Promise<typeof import("../../channels/web/index.js")> | null = null;
let whatsappActionsPromise: Promise<
typeof import("../../../extensions/whatsapp/runtime-api.js")
typeof import("../../../extensions/whatsapp/action-runtime.runtime.js")
> | null = null;
function loadWebLoginQr() {
@@ -82,7 +82,7 @@ function loadWebChannel() {
}
function loadWhatsAppActions() {
whatsappActionsPromise ??= import("../../../extensions/whatsapp/runtime-api.js");
whatsappActionsPromise ??= import("../../../extensions/whatsapp/action-runtime.runtime.js");
return whatsappActionsPromise;
}

View File

@@ -217,7 +217,7 @@ export type PluginRuntimeChannel = {
startWebLoginWithQr: typeof import("../../../extensions/whatsapp/login-qr-api.js").startWebLoginWithQr;
waitForWebLogin: typeof import("../../../extensions/whatsapp/login-qr-api.js").waitForWebLogin;
monitorWebChannel: typeof import("../../channels/web/index.js").monitorWebChannel;
handleWhatsAppAction: typeof import("../../../extensions/whatsapp/runtime-api.js").handleWhatsAppAction;
handleWhatsAppAction: typeof import("../../../extensions/whatsapp/action-runtime.runtime.js").handleWhatsAppAction;
createLoginTool: typeof import("./runtime-whatsapp-login-tool.js").createRuntimeWhatsAppLoginTool;
};
line: {