plugin-sdk: add channel subpaths and migrate bundled plugins

This commit is contained in:
Gustavo Madeira Santana 2026-03-03 22:07:03 -05:00
parent 1c200ca7ae
commit 1278ee9248
75 changed files with 808 additions and 174 deletions

View File

@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
- Plugins/startup loading: lazily initialize plugin runtime, split startup-critical plugin SDK imports into `openclaw/plugin-sdk/core` and `openclaw/plugin-sdk/telegram`, and preserve `api.runtime` reflection semantics for plugin compatibility. (#28620) thanks @hmemcpy.
- Build/lazy runtime boundaries: replace ineffective dynamic import sites with dedicated lazy runtime boundaries across Slack slash handling, Telegram audit, CLI send deps, memory fallback, and outbound delivery paths while preserving behavior. (#33690) thanks @gumadeiras.
- Config/heartbeat legacy-path handling: auto-migrate top-level `heartbeat` into `agents.defaults.heartbeat` (with merge semantics that preserve explicit defaults), and keep startup failures on non-migratable legacy entries in the detailed invalid-config path instead of generic migration-failed errors. (#32706) thanks @xiwan.
- Plugins/SDK subpath parity: add channel-specific plugin SDK subpaths for Discord, Slack, Signal, iMessage, WhatsApp, and LINE; migrate bundled plugin entrypoints to scoped subpaths/core with CI guardrails; and keep `openclaw/plugin-sdk` root import compatibility for existing external plugins. (#33737) thanks @gumadeiras.
- Security/auth labels: remove token and API-key snippets from user-facing auth status labels so `/status` and `/models` do not expose credential fragments. (#33262) thanks @cu1ch3n.
- Auth/credential semantics: align profile eligibility + probe diagnostics with SecretRef/expiry rules and harden browser download atomic writes. (#33733) thanks @joshavant.
- Security/audit denyCommands guidance: suggest likely exact node command IDs for unknown `gateway.nodes.denyCommands` entries so ineffective denylist entries are easier to correct. (#29713) thanks @liquidhorizon88-bot.

View File

@ -106,6 +106,26 @@ Notes:
- Uses core media-understanding audio configuration (`tools.media.audio`) and provider fallback order.
- Returns `{ text: undefined }` when no transcription output is produced (for example skipped/unsupported input).
## Plugin SDK import paths
Use SDK subpaths instead of the monolithic `openclaw/plugin-sdk` import when
authoring plugins:
- `openclaw/plugin-sdk/core` for generic plugin APIs, provider auth types, and shared helpers.
- `openclaw/plugin-sdk/telegram` for Telegram channel plugins.
- `openclaw/plugin-sdk/discord` for Discord channel plugins.
- `openclaw/plugin-sdk/slack` for Slack channel plugins.
- `openclaw/plugin-sdk/signal` for Signal channel plugins.
- `openclaw/plugin-sdk/imessage` for iMessage channel plugins.
- `openclaw/plugin-sdk/whatsapp` for WhatsApp channel plugins.
- `openclaw/plugin-sdk/line` for LINE channel plugins.
Compatibility note:
- `openclaw/plugin-sdk` remains supported for existing external plugins.
- New and migrated bundled plugins should use channel subpaths (or `core`) to
keep startup imports scoped.
## Discovery & precedence
OpenClaw scans, in order:

View File

@ -1,4 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { createAcpxPluginConfigSchema } from "./src/config.js";
import { createAcpxRuntimeService } from "./src/service.js";

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { bluebubblesPlugin } from "./src/channel.js";
import { setBlueBubblesRuntime } from "./src/runtime.js";

View File

@ -3,7 +3,7 @@ import {
type OpenClawPluginApi,
type ProviderAuthContext,
type ProviderAuthResult,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/core";
const DEFAULT_BASE_URL = "http://localhost:3000/v1";
const DEFAULT_API_KEY = "n/a";

View File

@ -1,7 +1,7 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { listDevicePairing } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { listDevicePairing } from "openclaw/plugin-sdk/core";
const NOTIFY_STATE_FILE = "device-pair-notify.json";
const NOTIFY_POLL_INTERVAL_MS = 10_000;

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { createDiagnosticsOtelService } from "./src/service.js";
const plugin = {

View File

@ -1,6 +1,6 @@
import path from "node:path";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/core";
import {
diffsPluginConfigSchema,
resolveDiffsPluginDefaults,

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/discord";
import { discordPlugin } from "./src/channel.js";
import { setDiscordRuntime } from "./src/runtime.js";
import { registerDiscordSubagentHooks } from "./src/subagent-hooks.js";

View File

@ -1,4 +1,4 @@
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/discord";
import { describe, expect, it, vi } from "vitest";
import { discordPlugin } from "./channel.js";
import { setDiscordRuntime } from "./runtime.js";

View File

@ -29,7 +29,7 @@ import {
type ChannelMessageActionAdapter,
type ChannelPlugin,
type ResolvedDiscordAccount,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/discord";
import { getDiscordRuntime } from "./runtime.js";
const meta = getChatChannelMeta("discord");

View File

@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import type { PluginRuntime } from "openclaw/plugin-sdk/discord";
let runtime: PluginRuntime | null = null;

View File

@ -1,4 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerDiscordSubagentHooks } from "./subagent-hooks.js";
@ -35,7 +35,7 @@ const hookMocks = vi.hoisted(() => ({
unbindThreadBindingsBySessionKey: vi.fn(() => []),
}));
vi.mock("openclaw/plugin-sdk", () => ({
vi.mock("openclaw/plugin-sdk/discord", () => ({
resolveDiscordAccount: hookMocks.resolveDiscordAccount,
autoBindSpawnedDiscordSubagent: hookMocks.autoBindSpawnedDiscordSubagent,
listThreadBindingsBySessionKey: hookMocks.listThreadBindingsBySessionKey,

View File

@ -1,10 +1,10 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
import {
autoBindSpawnedDiscordSubagent,
listThreadBindingsBySessionKey,
resolveDiscordAccount,
unbindThreadBindingsBySessionKey,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/discord";
function summarizeError(err: unknown): string {
if (err instanceof Error) {

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { registerFeishuBitableTools } from "./src/bitable.js";
import { feishuPlugin } from "./src/channel.js";
import { registerFeishuChatTools } from "./src/chat.js";

View File

@ -3,7 +3,7 @@ import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
type ProviderAuthContext,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/core";
import { loginGeminiCliOAuth } from "./oauth.js";
const PROVIDER_ID = "google-gemini-cli";

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { googlechatDock, googlechatPlugin } from "./src/channel.js";
import { setGoogleChatRuntime } from "./src/runtime.js";

View File

@ -8,7 +8,7 @@
"google-auth-library": "^10.6.1"
},
"peerDependencies": {
"openclaw": ">=2026.3.1"
"openclaw": ">=2026.3.2"
},
"openclaw": {
"extensions": [

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/imessage";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/imessage";
import { imessagePlugin } from "./src/channel.js";
import { setIMessageRuntime } from "./src/runtime.js";

View File

@ -26,7 +26,7 @@ import {
setAccountEnabledInConfigSection,
type ChannelPlugin,
type ResolvedIMessageAccount,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/imessage";
import { getIMessageRuntime } from "./runtime.js";
const meta = getChatChannelMeta("imessage");

View File

@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import type { PluginRuntime } from "openclaw/plugin-sdk/imessage";
let runtime: PluginRuntime | null = null;

View File

@ -1,5 +1,5 @@
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { ircPlugin } from "./src/channel.js";
import { setIrcRuntime } from "./src/runtime.js";

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/line";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/line";
import { registerLineCardCommand } from "./src/card-command.js";
import { linePlugin } from "./src/channel.js";
import { setLineRuntime } from "./src/runtime.js";

View File

@ -1,4 +1,4 @@
import type { LineChannelData, OpenClawPluginApi, ReplyPayload } from "openclaw/plugin-sdk";
import type { LineChannelData, OpenClawPluginApi, ReplyPayload } from "openclaw/plugin-sdk/line";
import {
createActionCard,
createImageCard,
@ -7,7 +7,7 @@ import {
createReceiptCard,
type CardAction,
type ListItem,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/line";
const CARD_USAGE = `Usage: /card <type> "title" "body" [options]

View File

@ -1,4 +1,4 @@
import type { OpenClawConfig, PluginRuntime, ResolvedLineAccount } from "openclaw/plugin-sdk";
import type { OpenClawConfig, PluginRuntime, ResolvedLineAccount } from "openclaw/plugin-sdk/line";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
import { linePlugin } from "./channel.js";

View File

@ -1,4 +1,4 @@
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/line";
import { describe, expect, it, vi } from "vitest";
import { linePlugin } from "./channel.js";
import { setLineRuntime } from "./runtime.js";

View File

@ -4,7 +4,7 @@ import type {
OpenClawConfig,
PluginRuntime,
ResolvedLineAccount,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/line";
import { describe, expect, it, vi } from "vitest";
import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
import { linePlugin } from "./channel.js";

View File

@ -12,7 +12,7 @@ import {
type LineConfig,
type LineChannelData,
type ResolvedLineAccount,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/line";
import { getLineRuntime } from "./runtime.js";
// LINE channel metadata

View File

@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import type { PluginRuntime } from "openclaw/plugin-sdk/line";
let runtime: PluginRuntime | null = null;

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { matrixPlugin } from "./src/channel.js";
import { ensureMatrixCryptoRuntime } from "./src/matrix/deps.js";
import { setMatrixRuntime } from "./src/runtime.js";

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { mattermostPlugin } from "./src/channel.js";
import { getSlashCommandState, registerSlashCommandRoute } from "./src/mattermost/slash-state.js";
import { setMattermostRuntime } from "./src/runtime.js";

View File

@ -5,7 +5,7 @@
"description": "OpenClaw core memory search plugin",
"type": "module",
"peerDependencies": {
"openclaw": ">=2026.3.1"
"openclaw": ">=2026.3.2"
},
"openclaw": {
"extensions": [

View File

@ -10,7 +10,7 @@ import { randomUUID } from "node:crypto";
import type * as LanceDB from "@lancedb/lancedb";
import { Type } from "@sinclair/typebox";
import OpenAI from "openai";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import {
DEFAULT_CAPTURE_MAX_CHARS,
MEMORY_CATEGORIES,

View File

@ -3,7 +3,7 @@ import {
type OpenClawPluginApi,
type ProviderAuthContext,
type ProviderAuthResult,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/core";
import { loginMiniMaxPortalOAuth, type MiniMaxRegion } from "./oauth.js";
const PROVIDER_ID = "minimax-portal";

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { msteamsPlugin } from "./src/channel.js";
import { setMSTeamsRuntime } from "./src/runtime.js";

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { nextcloudTalkPlugin } from "./src/channel.js";
import { setNextcloudTalkRuntime } from "./src/runtime.js";

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { nostrPlugin } from "./src/channel.js";
import type { NostrProfile } from "./src/config-schema.js";
import { createNostrProfileHttpHandler } from "./src/nostr-profile-http.js";

View File

@ -2,7 +2,7 @@ import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
type ProviderAuthContext,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/core";
import { loginQwenPortalOAuth } from "./oauth.js";
const PROVIDER_ID = "qwen-portal";

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/signal";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/signal";
import { signalPlugin } from "./src/channel.js";
import { setSignalRuntime } from "./src/runtime.js";

View File

@ -27,7 +27,7 @@ import {
type ChannelMessageActionAdapter,
type ChannelPlugin,
type ResolvedSignalAccount,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/signal";
import { getSignalRuntime } from "./runtime.js";
const signalMessageActions: ChannelMessageActionAdapter = {

View File

@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import type { PluginRuntime } from "openclaw/plugin-sdk/signal";
let runtime: PluginRuntime | null = null;

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/slack";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/slack";
import { slackPlugin } from "./src/channel.js";
import { setSlackRuntime } from "./src/runtime.js";

View File

@ -1,4 +1,4 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import type { OpenClawConfig } from "openclaw/plugin-sdk/slack";
import { describe, expect, it, vi } from "vitest";
const handleSlackActionMock = vi.fn();

View File

@ -29,7 +29,7 @@ import {
SlackConfigSchema,
type ChannelPlugin,
type ResolvedSlackAccount,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/slack";
import { getSlackRuntime } from "./runtime.js";
const meta = getChatChannelMeta("slack");

View File

@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import type { PluginRuntime } from "openclaw/plugin-sdk/slack";
let runtime: PluginRuntime | null = null;

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { createSynologyChatPlugin } from "./src/channel.js";
import { setSynologyRuntime } from "./src/runtime.js";

View File

@ -1,4 +1,4 @@
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/core";
type ThreadOwnershipConfig = {
forwarderUrl?: string;

View File

@ -2,8 +2,8 @@ import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { tlonPlugin } from "./src/channel.js";
import { setTlonRuntime } from "./src/runtime.js";

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { twitchPlugin } from "./src/plugin.js";
import { setTwitchRuntime } from "./src/runtime.js";

View File

@ -1,5 +1,5 @@
import { Type } from "@sinclair/typebox";
import type { GatewayRequestHandlerOptions, OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { GatewayRequestHandlerOptions, OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { registerVoiceCallCli } from "./src/cli.js";
import {
VoiceCallConfigSchema,

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/whatsapp";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/whatsapp";
import { whatsappPlugin } from "./src/channel.js";
import { setWhatsAppRuntime } from "./src/runtime.js";

View File

@ -33,7 +33,7 @@ import {
type ChannelMessageActionName,
type ChannelPlugin,
type ResolvedWhatsAppAccount,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/whatsapp";
import { getWhatsAppRuntime } from "./runtime.js";
const meta = getChatChannelMeta("whatsapp");

View File

@ -1,82 +1,60 @@
import { describe, expect, it, vi } from "vitest";
import { installCommonResolveTargetErrorCases } from "../../shared/resolve-target-test-helpers.js";
vi.mock("openclaw/plugin-sdk", () => ({
getChatChannelMeta: () => ({ id: "whatsapp", label: "WhatsApp" }),
normalizeWhatsAppTarget: (value: string) => {
vi.mock("openclaw/plugin-sdk/whatsapp", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/whatsapp")>(
"openclaw/plugin-sdk/whatsapp",
);
const normalizeWhatsAppTarget = (value: string) => {
if (value === "invalid-target") return null;
// Simulate E.164 normalization: strip leading + and whatsapp: prefix
// Simulate E.164 normalization: strip leading + and whatsapp: prefix.
const stripped = value.replace(/^whatsapp:/i, "").replace(/^\+/, "");
return stripped.includes("@g.us") ? stripped : `${stripped}@s.whatsapp.net`;
},
isWhatsAppGroupJid: (value: string) => value.endsWith("@g.us"),
resolveWhatsAppOutboundTarget: ({
to,
allowFrom,
mode,
}: {
to?: string;
allowFrom: string[];
mode: "explicit" | "implicit";
}) => {
const raw = typeof to === "string" ? to.trim() : "";
if (!raw) {
return { ok: false, error: new Error("missing target") };
}
const normalizeWhatsAppTarget = (value: string) => {
if (value === "invalid-target") return null;
const stripped = value.replace(/^whatsapp:/i, "").replace(/^\+/, "");
return stripped.includes("@g.us") ? stripped : `${stripped}@s.whatsapp.net`;
};
const normalized = normalizeWhatsAppTarget(raw);
if (!normalized) {
return { ok: false, error: new Error("invalid target") };
}
};
if (mode === "implicit" && !normalized.endsWith("@g.us")) {
const allowAll = allowFrom.includes("*");
const allowExact = allowFrom.some((entry) => {
if (!entry) {
return false;
}
const normalizedEntry = normalizeWhatsAppTarget(entry.trim());
return normalizedEntry?.toLowerCase() === normalized.toLowerCase();
});
if (!allowAll && !allowExact) {
return { ok: false, error: new Error("target not allowlisted") };
return {
...actual,
getChatChannelMeta: () => ({ id: "whatsapp", label: "WhatsApp" }),
normalizeWhatsAppTarget,
isWhatsAppGroupJid: (value: string) => value.endsWith("@g.us"),
resolveWhatsAppOutboundTarget: ({
to,
allowFrom,
mode,
}: {
to?: string;
allowFrom: string[];
mode: "explicit" | "implicit";
}) => {
const raw = typeof to === "string" ? to.trim() : "";
if (!raw) {
return { ok: false, error: new Error("missing target") };
}
const normalized = normalizeWhatsAppTarget(raw);
if (!normalized) {
return { ok: false, error: new Error("invalid target") };
}
}
return { ok: true, to: normalized };
},
missingTargetError: (provider: string, hint: string) =>
new Error(`Delivering to ${provider} requires target ${hint}`),
WhatsAppConfigSchema: {},
whatsappOnboardingAdapter: {},
resolveWhatsAppHeartbeatRecipients: vi.fn(),
buildChannelConfigSchema: vi.fn(),
collectWhatsAppStatusIssues: vi.fn(),
createActionGate: vi.fn(),
DEFAULT_ACCOUNT_ID: "default",
escapeRegExp: vi.fn(),
formatPairingApproveHint: vi.fn(),
listWhatsAppAccountIds: vi.fn(),
listWhatsAppDirectoryGroupsFromConfig: vi.fn(),
listWhatsAppDirectoryPeersFromConfig: vi.fn(),
looksLikeWhatsAppTargetId: vi.fn(),
migrateBaseNameToDefaultAccount: vi.fn(),
normalizeAccountId: vi.fn(),
normalizeE164: vi.fn(),
normalizeWhatsAppMessagingTarget: vi.fn(),
readStringParam: vi.fn(),
resolveDefaultWhatsAppAccountId: vi.fn(),
resolveWhatsAppAccount: vi.fn(),
resolveWhatsAppGroupIntroHint: vi.fn(),
resolveWhatsAppGroupRequireMention: vi.fn(),
resolveWhatsAppGroupToolPolicy: vi.fn(),
resolveWhatsAppMentionStripPatterns: vi.fn(() => []),
applyAccountNameToChannelSection: vi.fn(),
}));
if (mode === "implicit" && !normalized.endsWith("@g.us")) {
const allowAll = allowFrom.includes("*");
const allowExact = allowFrom.some((entry) => {
if (!entry) {
return false;
}
const normalizedEntry = normalizeWhatsAppTarget(entry.trim());
return normalizedEntry?.toLowerCase() === normalized.toLowerCase();
});
if (!allowAll && !allowExact) {
return { ok: false, error: new Error("target not allowlisted") };
}
}
return { ok: true, to: normalized };
},
missingTargetError: (provider: string, hint: string) =>
new Error(`Delivering to ${provider} requires target ${hint}`),
};
});
vi.mock("./runtime.js", () => ({
getWhatsAppRuntime: vi.fn(() => ({

View File

@ -1,4 +1,4 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import type { PluginRuntime } from "openclaw/plugin-sdk/whatsapp";
let runtime: PluginRuntime | null = null;

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { zaloDock, zaloPlugin } from "./src/channel.js";
import { setZaloRuntime } from "./src/runtime.js";

View File

@ -1,5 +1,5 @@
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
import { zalouserDock, zalouserPlugin } from "./src/channel.js";
import { setZalouserRuntime } from "./src/runtime.js";
import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";

View File

@ -48,6 +48,30 @@
"types": "./dist/plugin-sdk/telegram.d.ts",
"default": "./dist/plugin-sdk/telegram.js"
},
"./plugin-sdk/discord": {
"types": "./dist/plugin-sdk/discord.d.ts",
"default": "./dist/plugin-sdk/discord.js"
},
"./plugin-sdk/slack": {
"types": "./dist/plugin-sdk/slack.d.ts",
"default": "./dist/plugin-sdk/slack.js"
},
"./plugin-sdk/signal": {
"types": "./dist/plugin-sdk/signal.d.ts",
"default": "./dist/plugin-sdk/signal.js"
},
"./plugin-sdk/imessage": {
"types": "./dist/plugin-sdk/imessage.d.ts",
"default": "./dist/plugin-sdk/imessage.js"
},
"./plugin-sdk/whatsapp": {
"types": "./dist/plugin-sdk/whatsapp.d.ts",
"default": "./dist/plugin-sdk/whatsapp.js"
},
"./plugin-sdk/line": {
"types": "./dist/plugin-sdk/line.d.ts",
"default": "./dist/plugin-sdk/line.js"
},
"./plugin-sdk/account-id": {
"types": "./dist/plugin-sdk/account-id.d.ts",
"default": "./dist/plugin-sdk/account-id.js"
@ -71,7 +95,7 @@
"build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json",
"build:strict-smoke": "pnpm canvas:a2ui:bundle && tsdown && pnpm build:plugin-sdk:dts",
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
"check": "pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope && pnpm check:host-env-policy:swift",
"check": "pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope && pnpm check:host-env-policy:swift",
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-links",
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500",
@ -115,6 +139,7 @@
"lint:docs": "pnpm dlx markdownlint-cli2",
"lint:docs:fix": "pnpm dlx markdownlint-cli2 --fix",
"lint:fix": "oxlint --type-aware --fix && pnpm format",
"lint:plugins:no-monolithic-plugin-sdk-entry-imports": "node --import tsx scripts/check-no-monolithic-plugin-sdk-entry-imports.ts",
"lint:plugins:no-register-http-handler": "node scripts/check-no-register-http-handler.mjs",
"lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
"lint:tmp:channel-agnostic-boundaries": "node scripts/check-channel-agnostic-boundaries.mjs",

View File

@ -29,13 +29,13 @@ importers:
version: 3.1000.0
'@buape/carbon':
specifier: 0.0.0-beta-20260216184201
version: 0.0.0-beta-20260216184201(hono@4.11.10)(opusscript@0.1.1)
version: 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.11.10)(opusscript@0.1.1)
'@clack/prompts':
specifier: ^1.0.1
version: 1.0.1
'@discordjs/voice':
specifier: ^0.19.0
version: 0.19.0(opusscript@0.1.1)
version: 0.19.0(@discordjs/opus@0.10.0)(opusscript@0.1.1)
'@grammyjs/runner':
specifier: ^2.0.3
version: 2.0.3(grammy@1.41.0)
@ -341,8 +341,8 @@ importers:
specifier: ^10.6.1
version: 10.6.1
openclaw:
specifier: '>=2026.3.1'
version: 2026.3.1(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(hono@4.11.10)(node-llama-cpp@3.16.2(typescript@5.9.3))
specifier: '>=2026.3.2'
version: 2026.3.2(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(hono@4.11.10)(node-llama-cpp@3.16.2(typescript@5.9.3))
extensions/imessage: {}
@ -402,8 +402,8 @@ importers:
extensions/memory-core:
dependencies:
openclaw:
specifier: '>=2026.3.1'
version: 2026.3.1(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(hono@4.11.10)(node-llama-cpp@3.16.2(typescript@5.9.3))
specifier: '>=2026.3.2'
version: 2026.3.2(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(hono@4.11.10)(node-llama-cpp@3.16.2(typescript@5.9.3))
extensions/memory-lancedb:
dependencies:
@ -461,7 +461,7 @@ importers:
dependencies:
'@tloncorp/api':
specifier: github:tloncorp/api-beta#7eede1c1a756977b09f96aa14a92e2b06318ae87
version: https://codeload.github.com/tloncorp/api-beta/tar.gz/7eede1c1a756977b09f96aa14a92e2b06318ae87
version: git+https://git@github.com:tloncorp/api-beta.git#7eede1c1a756977b09f96aa14a92e2b06318ae87
'@tloncorp/tlon-skill':
specifier: 0.1.9
version: 0.1.9
@ -925,6 +925,10 @@ packages:
resolution: {integrity: sha512-YJOVVZ545x24mHzANfYoy0BJX5PDyeZlpiJjDkUBM/V/Ao7TFX9lcUvCN4nr0tbr5ubeaXxtEBILUrHtTphVeQ==}
hasBin: true
'@discordjs/opus@0.10.0':
resolution: {integrity: sha512-HHEnSNrSPmFEyndRdQBJN2YE6egyXS9JUnJWyP6jficK0Y+qKMEZXyYTgmzpjrxXP1exM/hKaNP7BRBUEWkU5w==}
engines: {node: '>=12.0.0'}
'@discordjs/voice@0.19.0':
resolution: {integrity: sha512-UyX6rGEXzVyPzb1yvjHtPfTlnLvB5jX/stAMdiytHhfoydX+98hfympdOwsnTktzr+IRvphxTbdErgYDJkEsvw==}
engines: {node: '>=22.12.0'}
@ -2933,8 +2937,8 @@ packages:
resolution: {integrity: sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA==}
engines: {node: '>=12.17.0'}
'@tloncorp/api@https://codeload.github.com/tloncorp/api-beta/tar.gz/7eede1c1a756977b09f96aa14a92e2b06318ae87':
resolution: {tarball: https://codeload.github.com/tloncorp/api-beta/tar.gz/7eede1c1a756977b09f96aa14a92e2b06318ae87}
'@tloncorp/api@git+https://git@github.com:tloncorp/api-beta.git#7eede1c1a756977b09f96aa14a92e2b06318ae87':
resolution: {commit: 7eede1c1a756977b09f96aa14a92e2b06318ae87, repo: git@github.com:tloncorp/api-beta.git, type: git}
version: 0.0.2
'@tloncorp/tlon-skill-darwin-arm64@0.1.9':
@ -4973,8 +4977,8 @@ packages:
zod:
optional: true
openclaw@2026.3.1:
resolution: {integrity: sha512-7Pt5ykhaYa8TYpLWnBhaMg6Lp6kfk3rMKgqJ3WWESKM9BizYu1fkH/rF9BLeXlsNASgZdLp4oR8H0XfvIIoXIg==}
openclaw@2026.3.2:
resolution: {integrity: sha512-Gkqx24m7PF1DUXPI968DuC9n52lTZ5hI3X5PIi0HosC7J7d6RLkgVppj1mxvgiQAWMp41E41elvoi/h4KBjFcQ==}
engines: {node: '>=22.12.0'}
hasBin: true
peerDependencies:
@ -5185,10 +5189,13 @@ packages:
prism-media@1.3.5:
resolution: {integrity: sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==}
peerDependencies:
'@discordjs/opus': '>=0.8.0 <1.0.0'
ffmpeg-static: ^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0
node-opus: ^0.3.3
opusscript: ^0.0.8
peerDependenciesMeta:
'@discordjs/opus':
optional: true
ffmpeg-static:
optional: true
node-opus:
@ -6813,18 +6820,19 @@ snapshots:
'@borewit/text-codec@0.2.1': {}
'@buape/carbon@0.0.0-beta-20260216184201(hono@4.11.10)(opusscript@0.1.1)':
'@buape/carbon@0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.11.10)(opusscript@0.1.1)':
dependencies:
'@types/node': 25.3.3
discord-api-types: 0.38.37
optionalDependencies:
'@cloudflare/workers-types': 4.20260120.0
'@discordjs/voice': 0.19.0(opusscript@0.1.1)
'@discordjs/voice': 0.19.0(@discordjs/opus@0.10.0)(opusscript@0.1.1)
'@hono/node-server': 1.19.9(hono@4.11.10)
'@types/bun': 1.3.9
'@types/ws': 8.18.1
ws: 8.19.0
transitivePeerDependencies:
- '@discordjs/opus'
- bufferutil
- ffmpeg-static
- hono
@ -6959,14 +6967,24 @@ snapshots:
- supports-color
optional: true
'@discordjs/voice@0.19.0(opusscript@0.1.1)':
'@discordjs/opus@0.10.0':
dependencies:
'@discordjs/node-pre-gyp': 0.4.5
node-addon-api: 8.5.0
transitivePeerDependencies:
- encoding
- supports-color
optional: true
'@discordjs/voice@0.19.0(@discordjs/opus@0.10.0)(opusscript@0.1.1)':
dependencies:
'@types/ws': 8.18.1
discord-api-types: 0.38.40
prism-media: 1.3.5(opusscript@0.1.1)
prism-media: 1.3.5(@discordjs/opus@0.10.0)(opusscript@0.1.1)
tslib: 2.8.1
ws: 8.19.0
transitivePeerDependencies:
- '@discordjs/opus'
- bufferutil
- ffmpeg-static
- node-opus
@ -8889,7 +8907,7 @@ snapshots:
'@tinyhttp/content-disposition@2.2.4': {}
'@tloncorp/api@https://codeload.github.com/tloncorp/api-beta/tar.gz/7eede1c1a756977b09f96aa14a92e2b06318ae87':
'@tloncorp/api@git+https://git@github.com:tloncorp/api-beta.git#7eede1c1a756977b09f96aa14a92e2b06318ae87':
dependencies:
'@aws-sdk/client-s3': 3.1000.0
'@aws-sdk/s3-request-presigner': 3.1000.0
@ -11171,13 +11189,13 @@ snapshots:
ws: 8.19.0
zod: 4.3.6
openclaw@2026.3.1(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(hono@4.11.10)(node-llama-cpp@3.16.2(typescript@5.9.3)):
openclaw@2026.3.2(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(audio-decode@2.2.3)(hono@4.11.10)(node-llama-cpp@3.16.2(typescript@5.9.3)):
dependencies:
'@agentclientprotocol/sdk': 0.14.1(zod@4.3.6)
'@aws-sdk/client-bedrock': 3.1000.0
'@buape/carbon': 0.0.0-beta-20260216184201(hono@4.11.10)(opusscript@0.1.1)
'@buape/carbon': 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.11.10)(opusscript@0.1.1)
'@clack/prompts': 1.0.1
'@discordjs/voice': 0.19.0(opusscript@0.1.1)
'@discordjs/voice': 0.19.0(@discordjs/opus@0.10.0)(opusscript@0.1.1)
'@grammyjs/runner': 2.0.3(grammy@1.41.0)
'@grammyjs/transformer-throttler': 1.2.1(grammy@1.41.0)
'@homebridge/ciao': 1.3.5
@ -11226,12 +11244,15 @@ snapshots:
qrcode-terminal: 0.12.0
sharp: 0.34.5
sqlite-vec: 0.1.7-alpha.2
strip-ansi: 7.2.0
tar: 7.5.9
tslog: 4.10.2
undici: 7.22.0
ws: 8.19.0
yaml: 2.8.2
zod: 4.3.6
optionalDependencies:
'@discordjs/opus': 0.10.0
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- '@types/express'
@ -11485,8 +11506,9 @@ snapshots:
dependencies:
parse-ms: 4.0.0
prism-media@1.3.5(opusscript@0.1.1):
prism-media@1.3.5(@discordjs/opus@0.10.0)(opusscript@0.1.1):
optionalDependencies:
'@discordjs/opus': 0.10.0
opusscript: 0.1.1
process-nextick-args@2.0.1: {}

View File

@ -0,0 +1,50 @@
import fs from "node:fs";
import path from "node:path";
import { discoverOpenClawPlugins } from "../src/plugins/discovery.js";
const ROOT_IMPORT_PATTERNS = [
/\b(?:import|export)\b[\s\S]*?\bfrom\s+["']openclaw\/plugin-sdk["']/,
/\bimport\s+["']openclaw\/plugin-sdk["']/,
/\bimport\s*\(\s*["']openclaw\/plugin-sdk["']\s*\)/,
/\brequire\s*\(\s*["']openclaw\/plugin-sdk["']\s*\)/,
];
function hasMonolithicRootImport(content: string): boolean {
return ROOT_IMPORT_PATTERNS.some((pattern) => pattern.test(content));
}
function main() {
const discovery = discoverOpenClawPlugins({});
const bundledEntryFiles = [
...new Set(discovery.candidates.filter((c) => c.origin === "bundled").map((c) => c.source)),
];
const offenders: string[] = [];
for (const entryFile of bundledEntryFiles) {
let content = "";
try {
content = fs.readFileSync(entryFile, "utf8");
} catch {
continue;
}
if (hasMonolithicRootImport(content)) {
offenders.push(entryFile);
}
}
if (offenders.length > 0) {
console.error("Bundled plugin entrypoints must not import monolithic openclaw/plugin-sdk.");
for (const file of offenders.toSorted()) {
const relative = path.relative(process.cwd(), file) || file;
console.error(`- ${relative}`);
}
console.error("Use openclaw/plugin-sdk/<channel> for channel plugins or /core for others.");
process.exit(1);
}
console.log(
`OK: bundled entrypoints use scoped plugin-sdk subpaths (${bundledEntryFiles.length} checked).`,
);
}
main();

View File

@ -41,6 +41,18 @@ const exportedNames = exportMatch[1]
const exportSet = new Set(exportedNames);
const requiredSubpathEntries = [
"core",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"whatsapp",
"line",
"account-id",
];
// Critical functions that channel extension plugins import from openclaw/plugin-sdk.
// If any of these are missing, plugins will fail at runtime with:
// TypeError: (0 , _pluginSdk.<name>) is not a function
@ -76,10 +88,25 @@ for (const name of requiredExports) {
}
}
for (const entry of requiredSubpathEntries) {
const jsPath = resolve(__dirname, "..", "dist", "plugin-sdk", `${entry}.js`);
const dtsPath = resolve(__dirname, "..", "dist", "plugin-sdk", `${entry}.d.ts`);
if (!existsSync(jsPath)) {
console.error(`MISSING SUBPATH JS: dist/plugin-sdk/${entry}.js`);
missing += 1;
}
if (!existsSync(dtsPath)) {
console.error(`MISSING SUBPATH DTS: dist/plugin-sdk/${entry}.d.ts`);
missing += 1;
}
}
if (missing > 0) {
console.error(`\nERROR: ${missing} required export(s) missing from dist/plugin-sdk/index.js.`);
console.error(
`\nERROR: ${missing} required plugin-sdk artifact(s) missing (named exports or subpath files).`,
);
console.error("This will break channel extension plugins at runtime.");
console.error("Check src/plugin-sdk/index.ts and rebuild.");
console.error("Check src/plugin-sdk/index.ts, subpath entries, and rebuild.");
process.exit(1);
}

View File

@ -14,6 +14,22 @@ const requiredPathGroups = [
["dist/entry.js", "dist/entry.mjs"],
"dist/plugin-sdk/index.js",
"dist/plugin-sdk/index.d.ts",
"dist/plugin-sdk/core.js",
"dist/plugin-sdk/core.d.ts",
"dist/plugin-sdk/telegram.js",
"dist/plugin-sdk/telegram.d.ts",
"dist/plugin-sdk/discord.js",
"dist/plugin-sdk/discord.d.ts",
"dist/plugin-sdk/slack.js",
"dist/plugin-sdk/slack.d.ts",
"dist/plugin-sdk/signal.js",
"dist/plugin-sdk/signal.d.ts",
"dist/plugin-sdk/imessage.js",
"dist/plugin-sdk/imessage.d.ts",
"dist/plugin-sdk/whatsapp.js",
"dist/plugin-sdk/whatsapp.d.ts",
"dist/plugin-sdk/line.js",
"dist/plugin-sdk/line.d.ts",
"dist/build-info.json",
];
const forbiddenPrefixes = ["dist/OpenClaw.app/"];

View File

@ -6,7 +6,18 @@ import path from "node:path";
//
// Our package export map points subpath `types` at `dist/plugin-sdk/<entry>.d.ts`, so we
// generate stable entry d.ts files that re-export the real declarations.
const entrypoints = ["index", "core", "telegram", "account-id"] as const;
const entrypoints = [
"index",
"core",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"whatsapp",
"line",
"account-id",
] as const;
for (const entry of entrypoints) {
const out = path.join(process.cwd(), `dist/plugin-sdk/${entry}.d.ts`);
fs.mkdirSync(path.dirname(out), { recursive: true });

View File

@ -1,8 +1,17 @@
export type { OpenClawPluginApi, OpenClawPluginService } from "../plugins/types.js";
export type {
AnyAgentTool,
OpenClawPluginApi,
OpenClawPluginService,
ProviderAuthContext,
ProviderAuthResult,
} from "../plugins/types.js";
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export type { OpenClawConfig } from "../config/config.js";
export type { GatewayRequestHandlerOptions } from "../gateway/server-methods/types.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { buildOauthProviderAuthResult } from "./provider-auth-result.js";
export {
approveDevicePairing,
@ -15,6 +24,7 @@ export {
type PluginCommandRunOptions,
type PluginCommandRunResult,
} from "./run-command.js";
export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
export { resolveGatewayBindUrl } from "../shared/gateway-bind-url.js";
export type { GatewayBindUrlResult } from "../shared/gateway-bind-url.js";

60
src/plugin-sdk/discord.ts Normal file
View File

@ -0,0 +1,60 @@
export type { ChannelMessageActionAdapter } from "../channels/plugins/types.js";
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type { OpenClawConfig } from "../config/config.js";
export type { ResolvedDiscordAccount } from "../discord/accounts.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export type { OpenClawPluginApi } from "../plugins/types.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export {
applyAccountNameToChannelSection,
migrateBaseNameToDefaultAccount,
} from "../channels/plugins/setup-helpers.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export {
deleteAccountFromConfigSection,
setAccountEnabledInConfigSection,
} from "../channels/plugins/config-helpers.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
export { getChatChannelMeta } from "../channels/registry.js";
export {
listDiscordAccountIds,
resolveDefaultDiscordAccountId,
resolveDiscordAccount,
} from "../discord/accounts.js";
export {
listDiscordDirectoryGroupsFromConfig,
listDiscordDirectoryPeersFromConfig,
} from "../channels/plugins/directory-config.js";
export {
looksLikeDiscordTargetId,
normalizeDiscordMessagingTarget,
normalizeDiscordOutboundTarget,
} from "../channels/plugins/normalize/discord.js";
export { collectDiscordAuditChannelIds } from "../discord/audit.js";
export { collectDiscordStatusIssues } from "../channels/plugins/status-issues/discord.js";
export {
resolveDefaultGroupPolicy,
resolveOpenProviderRuntimeGroupPolicy,
} from "../config/runtime-group-policy.js";
export {
resolveDiscordGroupRequireMention,
resolveDiscordGroupToolPolicy,
} from "../channels/plugins/group-mentions.js";
export { discordOnboardingAdapter } from "../channels/plugins/onboarding/discord.js";
export { DiscordConfigSchema } from "../config/zod-schema.providers-core.js";
export {
autoBindSpawnedDiscordSubagent,
listThreadBindingsBySessionKey,
unbindThreadBindingsBySessionKey,
} from "../discord/monitor/thread-bindings.js";
export { buildTokenChannelStatusSummary } from "./status-helpers.js";

View File

@ -0,0 +1,49 @@
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type { ResolvedIMessageAccount } from "../imessage/accounts.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export type { OpenClawPluginApi } from "../plugins/types.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export {
applyAccountNameToChannelSection,
migrateBaseNameToDefaultAccount,
} from "../channels/plugins/setup-helpers.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export {
deleteAccountFromConfigSection,
setAccountEnabledInConfigSection,
} from "../channels/plugins/config-helpers.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
export { getChatChannelMeta } from "../channels/registry.js";
export {
listIMessageAccountIds,
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
} from "../imessage/accounts.js";
export {
formatTrimmedAllowFromEntries,
resolveIMessageConfigAllowFrom,
resolveIMessageConfigDefaultTo,
} from "./channel-config-helpers.js";
export {
looksLikeIMessageTargetId,
normalizeIMessageMessagingTarget,
} from "../channels/plugins/normalize/imessage.js";
export {
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
} from "../config/runtime-group-policy.js";
export {
resolveIMessageGroupRequireMention,
resolveIMessageGroupToolPolicy,
} from "../channels/plugins/group-mentions.js";
export { imessageOnboardingAdapter } from "../channels/plugins/onboarding/imessage.js";
export { IMessageConfigSchema } from "../config/zod-schema.providers-core.js";
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";

36
src/plugin-sdk/line.ts Normal file
View File

@ -0,0 +1,36 @@
export type {
ChannelAccountSnapshot,
ChannelGatewayContext,
ChannelStatusIssue,
} from "../channels/plugins/types.js";
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type { OpenClawConfig } from "../config/config.js";
export type { ReplyPayload } from "../auto-reply/types.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export type { OpenClawPluginApi } from "../plugins/types.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export {
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
} from "../config/runtime-group-policy.js";
export { buildTokenChannelStatusSummary } from "./status-helpers.js";
export { LineConfigSchema } from "../line/config-schema.js";
export type { LineChannelData, LineConfig, ResolvedLineAccount } from "../line/types.js";
export {
createActionCard,
createImageCard,
createInfoCard,
createListCard,
createReceiptCard,
type CardAction,
type ListItem,
} from "../line/flex-templates.js";
export { processLineMessage } from "../line/markdown-to-line.js";

49
src/plugin-sdk/signal.ts Normal file
View File

@ -0,0 +1,49 @@
export type { ChannelMessageActionAdapter } from "../channels/plugins/types.js";
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type { ResolvedSignalAccount } from "../signal/accounts.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export type { OpenClawPluginApi } from "../plugins/types.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export {
applyAccountNameToChannelSection,
migrateBaseNameToDefaultAccount,
} from "../channels/plugins/setup-helpers.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export {
deleteAccountFromConfigSection,
setAccountEnabledInConfigSection,
} from "../channels/plugins/config-helpers.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
export { getChatChannelMeta } from "../channels/registry.js";
export {
listSignalAccountIds,
resolveDefaultSignalAccountId,
resolveSignalAccount,
} from "../signal/accounts.js";
export {
looksLikeSignalTargetId,
normalizeSignalMessagingTarget,
} from "../channels/plugins/normalize/signal.js";
export {
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
} from "../config/runtime-group-policy.js";
export { signalOnboardingAdapter } from "../channels/plugins/onboarding/signal.js";
export { SignalConfigSchema } from "../config/zod-schema.providers-core.js";
export { normalizeE164 } from "../utils.js";
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
export {
buildBaseAccountStatusSnapshot,
buildBaseChannelStatusSummary,
collectStatusIssuesFromLastError,
createDefaultChannelRuntimeState,
} from "./status-helpers.js";

52
src/plugin-sdk/slack.ts Normal file
View File

@ -0,0 +1,52 @@
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type { OpenClawConfig } from "../config/config.js";
export type { ResolvedSlackAccount } from "../slack/accounts.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export type { OpenClawPluginApi } from "../plugins/types.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export {
applyAccountNameToChannelSection,
migrateBaseNameToDefaultAccount,
} from "../channels/plugins/setup-helpers.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export {
deleteAccountFromConfigSection,
setAccountEnabledInConfigSection,
} from "../channels/plugins/config-helpers.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
export { getChatChannelMeta } from "../channels/registry.js";
export {
listSlackAccountIds,
resolveDefaultSlackAccountId,
resolveSlackAccount,
resolveSlackReplyToMode,
} from "../slack/accounts.js";
export {
listSlackDirectoryGroupsFromConfig,
listSlackDirectoryPeersFromConfig,
} from "../channels/plugins/directory-config.js";
export {
looksLikeSlackTargetId,
normalizeSlackMessagingTarget,
} from "../channels/plugins/normalize/slack.js";
export { extractSlackToolSend, listSlackMessageActions } from "../slack/message-actions.js";
export { buildSlackThreadingToolContext } from "../slack/threading-tool-context.js";
export {
resolveDefaultGroupPolicy,
resolveOpenProviderRuntimeGroupPolicy,
} from "../config/runtime-group-policy.js";
export {
resolveSlackGroupRequireMention,
resolveSlackGroupToolPolicy,
} from "../channels/plugins/group-mentions.js";
export { slackOnboardingAdapter } from "../channels/plugins/onboarding/slack.js";
export { SlackConfigSchema } from "../config/zod-schema.providers-core.js";
export { handleSlackMessageAction } from "./slack-message-actions.js";

View File

@ -0,0 +1,39 @@
import * as discordSdk from "openclaw/plugin-sdk/discord";
import * as imessageSdk from "openclaw/plugin-sdk/imessage";
import * as lineSdk from "openclaw/plugin-sdk/line";
import * as signalSdk from "openclaw/plugin-sdk/signal";
import * as slackSdk from "openclaw/plugin-sdk/slack";
import * as whatsappSdk from "openclaw/plugin-sdk/whatsapp";
import { describe, expect, it } from "vitest";
describe("plugin-sdk subpath exports", () => {
it("exports Discord helpers", () => {
expect(typeof discordSdk.resolveDiscordAccount).toBe("function");
expect(typeof discordSdk.discordOnboardingAdapter).toBe("object");
});
it("exports Slack helpers", () => {
expect(typeof slackSdk.resolveSlackAccount).toBe("function");
expect(typeof slackSdk.handleSlackMessageAction).toBe("function");
});
it("exports Signal helpers", () => {
expect(typeof signalSdk.resolveSignalAccount).toBe("function");
expect(typeof signalSdk.signalOnboardingAdapter).toBe("object");
});
it("exports iMessage helpers", () => {
expect(typeof imessageSdk.resolveIMessageAccount).toBe("function");
expect(typeof imessageSdk.imessageOnboardingAdapter).toBe("object");
});
it("exports WhatsApp helpers", () => {
expect(typeof whatsappSdk.resolveWhatsAppAccount).toBe("function");
expect(typeof whatsappSdk.whatsappOnboardingAdapter).toBe("object");
});
it("exports LINE helpers", () => {
expect(typeof lineSdk.processLineMessage).toBe("function");
expect(typeof lineSdk.createInfoCard).toBe("function");
});
});

View File

@ -0,0 +1,58 @@
export type { ChannelMessageActionName } from "../channels/plugins/types.js";
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export type { ResolvedWhatsAppAccount } from "../web/accounts.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export type { OpenClawPluginApi } from "../plugins/types.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export {
applyAccountNameToChannelSection,
migrateBaseNameToDefaultAccount,
} from "../channels/plugins/setup-helpers.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export { getChatChannelMeta } from "../channels/registry.js";
export {
listWhatsAppAccountIds,
resolveDefaultWhatsAppAccountId,
resolveWhatsAppAccount,
} from "../web/accounts.js";
export {
formatWhatsAppConfigAllowFromEntries,
resolveWhatsAppConfigAllowFrom,
resolveWhatsAppConfigDefaultTo,
} from "./channel-config-helpers.js";
export {
listWhatsAppDirectoryGroupsFromConfig,
listWhatsAppDirectoryPeersFromConfig,
} from "../channels/plugins/directory-config.js";
export {
looksLikeWhatsAppTargetId,
normalizeWhatsAppMessagingTarget,
} from "../channels/plugins/normalize/whatsapp.js";
export { resolveWhatsAppOutboundTarget } from "../whatsapp/resolve-outbound-target.js";
export {
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
} from "../config/runtime-group-policy.js";
export {
resolveWhatsAppGroupRequireMention,
resolveWhatsAppGroupToolPolicy,
} from "../channels/plugins/group-mentions.js";
export {
resolveWhatsAppGroupIntroHint,
resolveWhatsAppMentionStripPatterns,
} from "../channels/plugins/whatsapp-shared.js";
export { resolveWhatsAppHeartbeatRecipients } from "../channels/plugins/whatsapp-heartbeat.js";
export { whatsappOnboardingAdapter } from "../channels/plugins/onboarding/whatsapp.js";
export { collectWhatsAppStatusIssues } from "../channels/plugins/status-issues/whatsapp.js";
export { WhatsAppConfigSchema } from "../config/zod-schema.providers-whatsapp.js";
export { createActionGate, readStringParam } from "../agents/tools/common.js";
export { normalizeE164 } from "../utils.js";

View File

@ -1005,6 +1005,29 @@ describe("loadOpenClawPlugins", () => {
expect(record?.status).toBe("loaded");
});
it("supports legacy plugins importing monolithic plugin-sdk root", () => {
useNoBundledPlugins();
const plugin = writePlugin({
id: "legacy-root-import",
filename: "legacy-root-import.cjs",
body: `module.exports = {
id: "legacy-root-import",
configSchema: (require("openclaw/plugin-sdk").emptyPluginConfigSchema)(),
register() {},
};`,
});
const registry = loadRegistryFromSinglePlugin({
plugin,
pluginConfig: {
allow: ["legacy-root-import"],
},
});
const record = registry.plugins.find((entry) => entry.id === "legacy-root-import");
expect(record?.status).toBe("loaded");
});
it("prefers dist plugin-sdk alias when loader runs from dist", () => {
const { root, distFile } = createPluginSdkAliasFixture();

View File

@ -100,6 +100,30 @@ const resolvePluginSdkTelegramAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "telegram.ts", distFile: "telegram.js" });
};
const resolvePluginSdkDiscordAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "discord.ts", distFile: "discord.js" });
};
const resolvePluginSdkSlackAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "slack.ts", distFile: "slack.js" });
};
const resolvePluginSdkSignalAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "signal.ts", distFile: "signal.js" });
};
const resolvePluginSdkIMessageAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "imessage.ts", distFile: "imessage.js" });
};
const resolvePluginSdkWhatsAppAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "whatsapp.ts", distFile: "whatsapp.js" });
};
const resolvePluginSdkLineAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "line.ts", distFile: "line.js" });
};
export const __testing = {
resolvePluginSdkAliasFile,
};
@ -478,10 +502,22 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
const pluginSdkAccountIdAlias = resolvePluginSdkAccountIdAlias();
const pluginSdkCoreAlias = resolvePluginSdkCoreAlias();
const pluginSdkTelegramAlias = resolvePluginSdkTelegramAlias();
const pluginSdkDiscordAlias = resolvePluginSdkDiscordAlias();
const pluginSdkSlackAlias = resolvePluginSdkSlackAlias();
const pluginSdkSignalAlias = resolvePluginSdkSignalAlias();
const pluginSdkIMessageAlias = resolvePluginSdkIMessageAlias();
const pluginSdkWhatsAppAlias = resolvePluginSdkWhatsAppAlias();
const pluginSdkLineAlias = resolvePluginSdkLineAlias();
const aliasMap = {
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...(pluginSdkCoreAlias ? { "openclaw/plugin-sdk/core": pluginSdkCoreAlias } : {}),
...(pluginSdkTelegramAlias ? { "openclaw/plugin-sdk/telegram": pluginSdkTelegramAlias } : {}),
...(pluginSdkDiscordAlias ? { "openclaw/plugin-sdk/discord": pluginSdkDiscordAlias } : {}),
...(pluginSdkSlackAlias ? { "openclaw/plugin-sdk/slack": pluginSdkSlackAlias } : {}),
...(pluginSdkSignalAlias ? { "openclaw/plugin-sdk/signal": pluginSdkSignalAlias } : {}),
...(pluginSdkIMessageAlias ? { "openclaw/plugin-sdk/imessage": pluginSdkIMessageAlias } : {}),
...(pluginSdkWhatsAppAlias ? { "openclaw/plugin-sdk/whatsapp": pluginSdkWhatsAppAlias } : {}),
...(pluginSdkLineAlias ? { "openclaw/plugin-sdk/line": pluginSdkLineAlias } : {}),
...(pluginSdkAccountIdAlias
? { "openclaw/plugin-sdk/account-id": pluginSdkAccountIdAlias }
: {}),

View File

@ -14,6 +14,12 @@
"src/plugin-sdk/index.ts",
"src/plugin-sdk/core.ts",
"src/plugin-sdk/telegram.ts",
"src/plugin-sdk/discord.ts",
"src/plugin-sdk/slack.ts",
"src/plugin-sdk/signal.ts",
"src/plugin-sdk/imessage.ts",
"src/plugin-sdk/whatsapp.ts",
"src/plugin-sdk/line.ts",
"src/plugin-sdk/account-id.ts",
"src/plugin-sdk/keyed-async-queue.ts",
"src/types/**/*.d.ts"

View File

@ -69,6 +69,48 @@ export default defineConfig([
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/discord.ts",
outDir: "dist/plugin-sdk",
env,
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/slack.ts",
outDir: "dist/plugin-sdk",
env,
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/signal.ts",
outDir: "dist/plugin-sdk",
env,
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/imessage.ts",
outDir: "dist/plugin-sdk",
env,
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/whatsapp.ts",
outDir: "dist/plugin-sdk",
env,
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/line.ts",
outDir: "dist/plugin-sdk",
env,
fixedExtension: false,
platform: "node",
},
{
entry: "src/plugin-sdk/account-id.ts",
outDir: "dist/plugin-sdk",

View File

@ -25,6 +25,30 @@ export default defineConfig({
find: "openclaw/plugin-sdk/telegram",
replacement: path.join(repoRoot, "src", "plugin-sdk", "telegram.ts"),
},
{
find: "openclaw/plugin-sdk/discord",
replacement: path.join(repoRoot, "src", "plugin-sdk", "discord.ts"),
},
{
find: "openclaw/plugin-sdk/slack",
replacement: path.join(repoRoot, "src", "plugin-sdk", "slack.ts"),
},
{
find: "openclaw/plugin-sdk/signal",
replacement: path.join(repoRoot, "src", "plugin-sdk", "signal.ts"),
},
{
find: "openclaw/plugin-sdk/imessage",
replacement: path.join(repoRoot, "src", "plugin-sdk", "imessage.ts"),
},
{
find: "openclaw/plugin-sdk/whatsapp",
replacement: path.join(repoRoot, "src", "plugin-sdk", "whatsapp.ts"),
},
{
find: "openclaw/plugin-sdk/line",
replacement: path.join(repoRoot, "src", "plugin-sdk", "line.ts"),
},
{
find: "openclaw/plugin-sdk/keyed-async-queue",
replacement: path.join(repoRoot, "src", "plugin-sdk", "keyed-async-queue.ts"),