mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-10 00:31:22 +00:00
Merge branch 'main' into feat/deepseek-provider
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
1
extensions/whatsapp/action-runtime.runtime.ts
Normal file
1
extensions/whatsapp/action-runtime.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { handleWhatsAppAction } from "./src/action-runtime.js";
|
||||
@@ -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");
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user