refactor(plugin-runtime): remove plugin-specific core seams

This commit is contained in:
Peter Steinberger 2026-04-03 13:07:04 +01:00
parent 4846ebce12
commit f59d0eac68
No known key found for this signature in database
79 changed files with 1062 additions and 2628 deletions

View file

@ -127,7 +127,7 @@
"exportName": "ChannelConfiguredBindingConversationRef",
"kind": "type",
"source": {
"line": 659,
"line": 675,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -136,7 +136,7 @@
"exportName": "ChannelConfiguredBindingMatch",
"kind": "type",
"source": {
"line": 664,
"line": 680,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -145,7 +145,7 @@
"exportName": "ChannelConfiguredBindingProvider",
"kind": "type",
"source": {
"line": 680,
"line": 696,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -154,7 +154,7 @@
"exportName": "ChannelGatewayContext",
"kind": "type",
"source": {
"line": 271,
"line": 275,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -172,7 +172,7 @@
"exportName": "ChannelMessageActionAdapter",
"kind": "type",
"source": {
"line": 556,
"line": 560,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -181,7 +181,7 @@
"exportName": "ChannelMessageActionContext",
"kind": "type",
"source": {
"line": 520,
"line": 524,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -1240,7 +1240,7 @@
"exportName": "BlockStreamingCoalesceSchema",
"kind": "const",
"source": {
"line": 341,
"line": 414,
"path": "src/config/zod-schema.core.ts"
}
},
@ -1249,7 +1249,7 @@
"exportName": "ContextVisibilityModeSchema",
"kind": "const",
"source": {
"line": 339,
"line": 412,
"path": "src/config/zod-schema.core.ts"
}
},
@ -1267,7 +1267,7 @@
"exportName": "DmConfigSchema",
"kind": "const",
"source": {
"line": 294,
"line": 367,
"path": "src/config/zod-schema.core.ts"
}
},
@ -1276,7 +1276,7 @@
"exportName": "DmPolicySchema",
"kind": "const",
"source": {
"line": 338,
"line": 411,
"path": "src/config/zod-schema.core.ts"
}
},
@ -1294,7 +1294,7 @@
"exportName": "GroupPolicySchema",
"kind": "const",
"source": {
"line": 336,
"line": 409,
"path": "src/config/zod-schema.core.ts"
}
},
@ -1312,7 +1312,7 @@
"exportName": "MarkdownConfigSchema",
"kind": "const",
"source": {
"line": 374,
"line": 447,
"path": "src/config/zod-schema.core.ts"
}
},
@ -1330,7 +1330,7 @@
"exportName": "ReplyRuntimeConfigSchemaShape",
"kind": "const",
"source": {
"line": 349,
"line": 422,
"path": "src/config/zod-schema.core.ts"
}
},
@ -1339,7 +1339,7 @@
"exportName": "requireOpenAllowFrom",
"kind": "const",
"source": {
"line": 489,
"line": 562,
"path": "src/config/zod-schema.core.ts"
}
},
@ -1404,7 +1404,7 @@
"exportName": "BaseProbeResult",
"kind": "type",
"source": {
"line": 599,
"line": 603,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -1413,7 +1413,7 @@
"exportName": "BaseTokenResolution",
"kind": "type",
"source": {
"line": 605,
"line": 609,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -1440,7 +1440,7 @@
"exportName": "ChannelApprovalAdapter",
"kind": "type",
"source": {
"line": 597,
"line": 613,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -1449,7 +1449,7 @@
"exportName": "ChannelApprovalCapability",
"kind": "type",
"source": {
"line": 592,
"line": 602,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -1458,7 +1458,7 @@
"exportName": "ChannelCommandConversationContext",
"kind": "type",
"source": {
"line": 668,
"line": 684,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -1467,7 +1467,7 @@
"exportName": "ChannelDirectoryAdapter",
"kind": "type",
"source": {
"line": 454,
"line": 459,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -1485,7 +1485,7 @@
"exportName": "ChannelMessageActionAdapter",
"kind": "type",
"source": {
"line": 556,
"line": 560,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -1494,7 +1494,7 @@
"exportName": "ChannelMessageActionContext",
"kind": "type",
"source": {
"line": 520,
"line": 524,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -1941,7 +1941,7 @@
"exportName": "BaseProbeResult",
"kind": "type",
"source": {
"line": 599,
"line": 603,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -1950,7 +1950,7 @@
"exportName": "BaseTokenResolution",
"kind": "type",
"source": {
"line": 605,
"line": 609,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -1986,7 +1986,7 @@
"exportName": "ChannelAgentPromptAdapter",
"kind": "type",
"source": {
"line": 493,
"line": 497,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -2013,7 +2013,7 @@
"exportName": "ChannelAllowlistAdapter",
"kind": "type",
"source": {
"line": 603,
"line": 619,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2022,7 +2022,7 @@
"exportName": "ChannelApprovalAdapter",
"kind": "type",
"source": {
"line": 597,
"line": 613,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2031,7 +2031,7 @@
"exportName": "ChannelApprovalCapability",
"kind": "type",
"source": {
"line": 592,
"line": 602,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2058,7 +2058,7 @@
"exportName": "ChannelAuthAdapter",
"kind": "type",
"source": {
"line": 395,
"line": 399,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2103,7 +2103,7 @@
"exportName": "ChannelCommandAdapter",
"kind": "type",
"source": {
"line": 492,
"line": 497,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2112,7 +2112,7 @@
"exportName": "ChannelCommandConversationContext",
"kind": "type",
"source": {
"line": 668,
"line": 684,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2130,7 +2130,7 @@
"exportName": "ChannelConfiguredBindingConversationRef",
"kind": "type",
"source": {
"line": 659,
"line": 675,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2139,7 +2139,7 @@
"exportName": "ChannelConfiguredBindingMatch",
"kind": "type",
"source": {
"line": 664,
"line": 680,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2148,7 +2148,7 @@
"exportName": "ChannelConfiguredBindingProvider",
"kind": "type",
"source": {
"line": 680,
"line": 696,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2157,7 +2157,7 @@
"exportName": "ChannelConversationBindingSupport",
"kind": "type",
"source": {
"line": 696,
"line": 712,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2166,7 +2166,7 @@
"exportName": "ChannelDirectoryAdapter",
"kind": "type",
"source": {
"line": 454,
"line": 459,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2175,7 +2175,7 @@
"exportName": "ChannelDirectoryEntry",
"kind": "type",
"source": {
"line": 507,
"line": 511,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -2184,7 +2184,7 @@
"exportName": "ChannelDirectoryEntryKind",
"kind": "type",
"source": {
"line": 505,
"line": 509,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -2193,7 +2193,7 @@
"exportName": "ChannelElevatedAdapter",
"kind": "type",
"source": {
"line": 485,
"line": 490,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2202,7 +2202,7 @@
"exportName": "ChannelGatewayAdapter",
"kind": "type",
"source": {
"line": 379,
"line": 383,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2211,7 +2211,7 @@
"exportName": "ChannelGatewayContext",
"kind": "type",
"source": {
"line": 271,
"line": 275,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2220,7 +2220,7 @@
"exportName": "ChannelGroupAdapter",
"kind": "type",
"source": {
"line": 129,
"line": 133,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2238,7 +2238,7 @@
"exportName": "ChannelHeartbeatAdapter",
"kind": "type",
"source": {
"line": 420,
"line": 425,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2265,7 +2265,7 @@
"exportName": "ChannelLifecycleAdapter",
"kind": "type",
"source": {
"line": 497,
"line": 502,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2274,7 +2274,7 @@
"exportName": "ChannelLoginWithQrStartResult",
"kind": "type",
"source": {
"line": 350,
"line": 354,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2283,7 +2283,7 @@
"exportName": "ChannelLoginWithQrWaitResult",
"kind": "type",
"source": {
"line": 355,
"line": 359,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2292,7 +2292,7 @@
"exportName": "ChannelLogoutContext",
"kind": "type",
"source": {
"line": 360,
"line": 364,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2301,7 +2301,7 @@
"exportName": "ChannelLogoutResult",
"kind": "type",
"source": {
"line": 344,
"line": 348,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2328,7 +2328,7 @@
"exportName": "ChannelMessageActionAdapter",
"kind": "type",
"source": {
"line": 556,
"line": 560,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -2337,7 +2337,7 @@
"exportName": "ChannelMessageActionContext",
"kind": "type",
"source": {
"line": 520,
"line": 524,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -2409,7 +2409,7 @@
"exportName": "ChannelOutboundAdapter",
"kind": "type",
"source": {
"line": 175,
"line": 179,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2418,7 +2418,7 @@
"exportName": "ChannelOutboundContext",
"kind": "type",
"source": {
"line": 135,
"line": 139,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2427,7 +2427,7 @@
"exportName": "ChannelOutboundPayloadHint",
"kind": "type",
"source": {
"line": 160,
"line": 164,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2445,7 +2445,7 @@
"exportName": "ChannelOutboundTargetRef",
"kind": "type",
"source": {
"line": 164,
"line": 168,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2454,7 +2454,7 @@
"exportName": "ChannelPairingAdapter",
"kind": "type",
"source": {
"line": 368,
"line": 372,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2472,7 +2472,7 @@
"exportName": "ChannelPollContext",
"kind": "type",
"source": {
"line": 587,
"line": 591,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -2481,7 +2481,7 @@
"exportName": "ChannelPollResult",
"kind": "type",
"source": {
"line": 578,
"line": 582,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -2490,7 +2490,7 @@
"exportName": "ChannelResolveKind",
"kind": "type",
"source": {
"line": 465,
"line": 470,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2499,7 +2499,7 @@
"exportName": "ChannelResolverAdapter",
"kind": "type",
"source": {
"line": 475,
"line": 480,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2508,7 +2508,7 @@
"exportName": "ChannelResolveResult",
"kind": "type",
"source": {
"line": 467,
"line": 472,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2517,7 +2517,7 @@
"exportName": "ChannelSecurityAdapter",
"kind": "type",
"source": {
"line": 727,
"line": 743,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2562,7 +2562,7 @@
"exportName": "ChannelStatusAdapter",
"kind": "type",
"source": {
"line": 217,
"line": 221,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@ -2625,7 +2625,7 @@
"exportName": "ChannelToolSend",
"kind": "type",
"source": {
"line": 549,
"line": 553,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -3237,11 +3237,11 @@
}
},
{
"declaration": "export function resolveStoredModelOverride(params: { sessionEntry?: SessionEntry | undefined; sessionStore?: Record<string, SessionEntry> | undefined; sessionKey?: string | undefined; parentSessionKey?: string | undefined; }): StoredModelOverride | null;",
"declaration": "export function resolveStoredModelOverride(params: { sessionEntry?: SessionEntry | undefined; sessionStore?: Record<string, SessionEntry> | undefined; sessionKey?: string | undefined; parentSessionKey?: string | undefined; defaultProvider: string; }): StoredModelOverride | null;",
"exportName": "resolveStoredModelOverride",
"kind": "function",
"source": {
"line": 156,
"line": 145,
"path": "src/auto-reply/reply/model-selection.ts"
}
},
@ -3502,7 +3502,7 @@
"exportName": "StoredModelOverride",
"kind": "type",
"source": {
"line": 123,
"line": 124,
"path": "src/auto-reply/reply/model-selection.ts"
}
}
@ -3954,7 +3954,7 @@
"exportName": "ChannelMessageActionContext",
"kind": "type",
"source": {
"line": 520,
"line": 524,
"path": "src/channels/plugins/types.core.ts"
}
},
@ -5308,7 +5308,7 @@
"exportName": "ModelApi",
"kind": "type",
"source": {
"line": 16,
"line": 17,
"path": "src/config/types.models.ts"
}
},
@ -5317,7 +5317,7 @@
"exportName": "ModelDefinitionConfig",
"kind": "type",
"source": {
"line": 49,
"line": 50,
"path": "src/config/types.models.ts"
}
},
@ -5326,7 +5326,7 @@
"exportName": "ModelProviderConfig",
"kind": "type",
"source": {
"line": 67,
"line": 68,
"path": "src/config/types.models.ts"
}
},
@ -6099,7 +6099,7 @@
"exportName": "ChannelGatewayContext",
"kind": "type",
"source": {
"line": 271,
"line": 275,
"path": "src/channels/plugins/types.adapters.ts"
}
},

File diff suppressed because one or more lines are too long

View file

@ -30,10 +30,12 @@ import {
} from "./accounts.js";
import { getDiscordApprovalCapability } from "./approval-native.js";
import { auditDiscordChannelPermissions, collectDiscordAuditChannelIds } from "./audit.js";
import { discordMessageActions } from "./channel-actions.js";
import {
listDiscordDirectoryGroupsFromConfig,
listDiscordDirectoryPeersFromConfig,
} from "./directory-config.js";
import { listDiscordDirectoryGroupsLive, listDiscordDirectoryPeersLive } from "./directory-live.js";
import { shouldSuppressLocalDiscordExecApprovalPrompt } from "./exec-approvals.js";
import {
resolveDiscordGroupRequireMention,
@ -46,10 +48,10 @@ import {
} from "./normalize.js";
import { resolveDiscordOutboundSessionRoute } from "./outbound-session-route.js";
import type { DiscordProbe } from "./probe.js";
import { resolveDiscordChannelAllowlist } from "./resolve-channels.js";
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
import {
buildTokenChannelStatusSummary,
type ChannelMessageActionAdapter,
type ChannelPlugin,
DEFAULT_ACCOUNT_ID,
getChatChannelMeta,
@ -59,16 +61,15 @@ import {
type OpenClawConfig,
} from "./runtime-api.js";
import { getDiscordRuntime } from "./runtime.js";
import { fetchChannelPermissionsDiscord } from "./send.js";
import { fetchChannelPermissionsDiscord, sendMessageDiscord, sendPollDiscord } from "./send.js";
import { normalizeExplicitDiscordSessionKey } from "./session-key-normalization.js";
import { discordSetupAdapter } from "./setup-core.js";
import { createDiscordPluginBase, discordConfigAdapter } from "./shared.js";
import { collectDiscordStatusIssues } from "./status-issues.js";
import { parseDiscordTarget } from "./targets.js";
import { DiscordUiContainer } from "./ui.js";
type DiscordSendFn = ReturnType<
typeof getDiscordRuntime
>["channel"]["discord"]["sendMessageDiscord"];
type DiscordSendFn = typeof sendMessageDiscord;
let discordProviderRuntimePromise:
| Promise<typeof import("./monitor/provider.runtime.js")>
@ -128,22 +129,6 @@ function formatDiscordIntents(intents?: {
].join(" ");
}
const discordMessageActions: ChannelMessageActionAdapter = {
describeMessageTool: (ctx) =>
getDiscordRuntime().channel.discord.messageActions?.describeMessageTool?.(ctx) ?? null,
extractToolSend: (ctx) =>
getDiscordRuntime().channel.discord.messageActions?.extractToolSend?.(ctx) ?? null,
handleAction: async (ctx) => {
const ma = getDiscordRuntime().channel.discord.messageActions;
if (!ma?.handleAction) {
throw new Error("Discord message actions not available");
}
return ma.handleAction(ctx);
},
requiresTrustedRequesterSender: ({ action, toolContext }) =>
Boolean(toolContext && (action === "timeout" || action === "kick" || action === "ban")),
};
function buildDiscordCrossContextComponents(params: {
originLabel: string;
message: string;
@ -329,6 +314,8 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
},
messaging: {
normalizeTarget: normalizeDiscordMessagingTarget,
normalizeExplicitSessionKey: ({ sessionKey, ctx }) =>
normalizeExplicitDiscordSessionKey(sessionKey, ctx),
resolveSessionTarget: ({ id }) => normalizeDiscordMessagingTarget(`channel:${id}`),
parseExplicitTarget: ({ raw }) => parseDiscordExplicitTarget(raw),
inferTargetChatType: ({ to }) => parseDiscordExplicitTarget(to)?.chatType,
@ -344,7 +331,10 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
listPeers: async (params) => listDiscordDirectoryPeersFromConfig(params),
listGroups: async (params) => listDiscordDirectoryGroupsFromConfig(params),
...createRuntimeDirectoryLiveAdapter({
getRuntime: () => getDiscordRuntime().channel.discord,
getRuntime: () => ({
listDirectoryGroupsLive: listDiscordDirectoryGroupsLive,
listDirectoryPeersLive: listDiscordDirectoryPeersLive,
}),
listPeersLive: (runtime) => runtime.listDirectoryPeersLive,
listGroupsLive: (runtime) => runtime.listDirectoryGroupsLive,
}),
@ -358,10 +348,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
inputs,
missingTokenNote: "missing Discord token",
resolveWithToken: ({ token, inputs }) =>
getDiscordRuntime().channel.discord.resolveChannelAllowlist({
token,
entries: inputs,
}),
resolveDiscordChannelAllowlist({ token, entries: inputs }),
mapResolved: (entry) => ({
input: entry.input,
resolved: entry.resolved,
@ -379,10 +366,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
inputs,
missingTokenNote: "missing Discord token",
resolveWithToken: ({ token, inputs }) =>
getDiscordRuntime().channel.discord.resolveUserAllowlist({
token,
entries: inputs,
}),
resolveDiscordUserAllowlist({ token, entries: inputs }),
mapResolved: (entry) => ({
input: entry.input,
resolved: entry.resolved,
@ -638,7 +622,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
message: PAIRING_APPROVED_MESSAGE,
normalizeAllowEntry: createPairingPrefixStripper(/^(discord|user):/i),
notify: async ({ id, message }) => {
await getDiscordRuntime().channel.discord.sendMessageDiscord(`user:${id}`, message);
await sendMessageDiscord(`user:${id}`, message);
},
},
},
@ -666,9 +650,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
attachedResults: {
channel: "discord",
sendText: async ({ cfg, to, text, accountId, deps, replyToId, silent }) => {
const send =
resolveOutboundSendDep<DiscordSendFn>(deps, "discord") ??
getDiscordRuntime().channel.discord.sendMessageDiscord;
const send = resolveOutboundSendDep<DiscordSendFn>(deps, "discord") ?? sendMessageDiscord;
return await send(to, text, {
verbose: false,
cfg,
@ -688,9 +670,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
replyToId,
silent,
}) => {
const send =
resolveOutboundSendDep<DiscordSendFn>(deps, "discord") ??
getDiscordRuntime().channel.discord.sendMessageDiscord;
const send = resolveOutboundSendDep<DiscordSendFn>(deps, "discord") ?? sendMessageDiscord;
return await send(to, text, {
verbose: false,
cfg,
@ -702,7 +682,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
});
},
sendPoll: async ({ cfg, to, poll, accountId, silent }) =>
await getDiscordRuntime().channel.discord.sendPollDiscord(to, poll, {
await sendPollDiscord(to, poll, {
cfg,
accountId: accountId ?? undefined,
silent: silent ?? undefined,

View file

@ -37,7 +37,6 @@ function createRuntime(): { runtime: PluginRuntime; mocks: LineRuntimeMocks } {
const runtime = {
config: { writeConfigFile },
channel: { line: { resolveLineAccount } },
} as unknown as PluginRuntime;
return { runtime, mocks: { writeConfigFile, resolveLineAccount } };

View file

@ -3,11 +3,12 @@ import { createRestrictSendersChannelSecurity } from "openclaw/plugin-sdk/channe
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
import { type ChannelPlugin, type ResolvedLineAccount } from "../api.js";
import { resolveLineAccount } from "./accounts.js";
import { lineChannelPluginCommon } from "./channel-shared.js";
import { lineGatewayAdapter } from "./gateway.js";
import { resolveLineGroupRequireMention } from "./group-policy.js";
import { lineOutboundAdapter } from "./outbound.js";
import { getLineRuntime } from "./runtime.js";
import { pushMessageLine } from "./send.js";
import { lineSetupAdapter } from "./setup-core.js";
import { lineSetupWizard } from "./setup-surface.js";
import { lineStatusAdapter } from "./status.js";
@ -157,12 +158,11 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = createChatChannelP
message: "OpenClaw: your access has been approved.",
normalizeAllowEntry: createPairingPrefixStripper(/^line:(?:user:)?/i),
notify: async ({ cfg, id, message }) => {
const line = getLineRuntime().channel.line;
const account = line.resolveLineAccount({ cfg });
const account = resolveLineAccount({ cfg });
if (!account.channelAccessToken) {
throw new Error("LINE channel access token not configured");
}
await line.pushMessageLine(id, message, {
await pushMessageLine(id, message, {
accountId: account.accountId,
channelAccessToken: account.channelAccessToken,
});

View file

@ -6,6 +6,9 @@ import {
type OpenClawConfig,
type ResolvedLineAccount,
} from "../api.js";
import { resolveLineAccount } from "./accounts.js";
import { monitorLineProvider } from "./monitor.js";
import { probeLineBot } from "./probe.js";
import { getLineRuntime } from "./runtime.js";
export const lineGatewayAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>["gateway"]> = {
@ -26,7 +29,7 @@ export const lineGatewayAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>[
let lineBotLabel = "";
try {
const probe = await getLineRuntime().channel.line.probeLineBot(token, 2500);
const probe = await probeLineBot(token, 2500);
const displayName = probe.ok ? probe.bot?.displayName?.trim() : null;
if (displayName) {
lineBotLabel = ` (${displayName})`;
@ -39,7 +42,7 @@ export const lineGatewayAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>[
ctx.log?.info(`[${account.accountId}] starting LINE provider${lineBotLabel}`);
return await getLineRuntime().channel.line.monitorLineProvider({
return await monitorLineProvider({
channelAccessToken: token,
channelSecret: secret,
accountId: account.accountId,
@ -106,7 +109,7 @@ export const lineGatewayAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>[
await getLineRuntime().config.writeConfigFile(nextCfg);
}
const resolved = getLineRuntime().channel.line.resolveLineAccount({
const resolved = resolveLineAccount({
cfg: changed ? nextCfg : cfg,
accountId,
});

View file

@ -11,6 +11,17 @@ import {
} from "../api.js";
import { resolveLineOutboundMedia, type LineOutboundMediaResolved } from "./outbound-media.js";
import { getLineRuntime } from "./runtime.js";
import {
createQuickReplyItems,
pushFlexMessage,
pushLocationMessage,
pushMessageLine,
pushMessagesLine,
pushTemplateMessage,
pushTextMessageWithQuickReplies,
sendMessageLine,
} from "./send.js";
import { buildTemplateMessageFromPayload } from "./template-messages.js";
type LineChannelDataWithMedia = LineChannelData & {
mediaKind?: "image" | "video" | "audio";
@ -77,14 +88,13 @@ export const lineOutboundAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>
sendPayload: async ({ to, payload, accountId, cfg }) => {
const runtime = getLineRuntime();
const lineData = (payload.channelData?.line as LineChannelDataWithMedia | undefined) ?? {};
const sendText = runtime.channel.line.pushMessageLine;
const sendBatch = runtime.channel.line.pushMessagesLine;
const sendFlex = runtime.channel.line.pushFlexMessage;
const sendTemplate = runtime.channel.line.pushTemplateMessage;
const sendLocation = runtime.channel.line.pushLocationMessage;
const sendQuickReplies = runtime.channel.line.pushTextMessageWithQuickReplies;
const buildTemplate = runtime.channel.line.buildTemplateMessageFromPayload;
const createQuickReplyItems = runtime.channel.line.createQuickReplyItems;
const sendText = pushMessageLine;
const sendBatch = pushMessagesLine;
const sendFlex = pushFlexMessage;
const sendTemplate = pushTemplateMessage;
const sendLocation = pushLocationMessage;
const sendQuickReplies = pushTextMessageWithQuickReplies;
const buildTemplate = buildTemplateMessageFromPayload;
let lastResult: { messageId: string; chatId: string } | null = null;
const quickReplies = lineData.quickReplies ?? [];
@ -129,7 +139,7 @@ export const lineOutboundAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>
continue;
}
if (!useLineSpecificMedia) {
lastResult = await runtime.channel.line.sendMessageLine(to, "", {
lastResult = await sendMessageLine(to, "", {
verbose: false,
mediaUrl: trimmed,
cfg,
@ -143,7 +153,7 @@ export const lineOutboundAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>
durationMs: lineData.durationMs,
trackingId: lineData.trackingId,
});
lastResult = await runtime.channel.line.sendMessageLine(to, "", {
lastResult = await sendMessageLine(to, "", {
verbose: false,
mediaUrl: resolved.mediaUrl,
mediaKind: resolved.mediaKind,
@ -294,8 +304,8 @@ export const lineOutboundAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>
channel: "line",
sendText: async ({ cfg, to, text, accountId }) => {
const runtime = getLineRuntime();
const sendText = runtime.channel.line.pushMessageLine;
const sendFlex = runtime.channel.line.pushFlexMessage;
const sendText = pushMessageLine;
const sendFlex = pushFlexMessage;
const processed = processLineMessage(text);
let result: { messageId: string; chatId: string };
if (processed.text.trim()) {
@ -318,7 +328,7 @@ export const lineOutboundAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>
return result;
},
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) =>
await getLineRuntime().channel.line.sendMessageLine(to, text, {
await sendMessageLine(to, text, {
verbose: false,
mediaUrl,
cfg,

View file

@ -14,6 +14,7 @@ import { resolveSignalAccount, type ResolvedSignalAccount } from "./accounts.js"
import { signalApprovalAuth } from "./approval-auth.js";
import { markdownToSignalTextChunks } from "./format.js";
import { signalMessageActions } from "./message-actions.js";
import { monitorSignalProvider } from "./monitor.js";
import { looksLikeSignalTargetId, normalizeSignalMessagingTarget } from "./normalize.js";
import { resolveSignalOutboundTarget } from "./outbound-session.js";
import { probeSignal, type SignalProbe } from "./probe.js";
@ -29,7 +30,7 @@ import {
resolveChannelMediaMaxBytes,
type ChannelPlugin,
} from "./runtime-api.js";
import { getSignalRuntime } from "./runtime.js";
import { sendMessageSignal } from "./send.js";
import { signalSetupAdapter } from "./setup-core.js";
import {
signalConfigAdapter,
@ -37,16 +38,14 @@ import {
signalSecurityAdapter,
signalSetupWizard,
} from "./shared.js";
type SignalSendFn = ReturnType<typeof getSignalRuntime>["channel"]["signal"]["sendMessageSignal"];
type SignalSendFn = typeof sendMessageSignal;
function resolveSignalSendContext(params: {
cfg: Parameters<typeof resolveSignalAccount>[0]["cfg"];
accountId?: string;
deps?: { [channelId: string]: unknown };
}) {
const send =
resolveOutboundSendDep<SignalSendFn>(params.deps, "signal") ??
getSignalRuntime().channel.signal.sendMessageSignal;
const send = resolveOutboundSendDep<SignalSendFn>(params.deps, "signal") ?? sendMessageSignal;
const maxBytes = resolveChannelMediaMaxBytes({
cfg: params.cfg,
resolveChannelLimitMb: ({ cfg, accountId }) =>
@ -299,7 +298,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount, SignalProbe> =
});
ctx.log?.info(`[${account.accountId}] starting provider (${account.baseUrl})`);
// Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
return getSignalRuntime().channel.signal.monitorSignalProvider({
return monitorSignalProvider({
accountId: account.accountId,
config: ctx.cfg,
runtime: ctx.runtime,
@ -315,7 +314,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount, SignalProbe> =
message: PAIRING_APPROVED_MESSAGE,
normalizeAllowEntry: createPairingPrefixStripper(/^signal:/i),
notify: async ({ id, message }) => {
await getSignalRuntime().channel.signal.sendMessageSignal(id, message);
await sendMessageSignal(id, message);
},
},
},

View file

@ -1,5 +1,6 @@
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { slackPlugin } from "./src/channel.js";
import { registerSlackPluginHttpRoutes } from "./src/http/plugin-routes.js";
import { setSlackRuntime } from "./src/runtime.js";
export { slackPlugin } from "./src/channel.js";
@ -11,4 +12,5 @@ export default defineChannelPluginEntry({
description: "Slack channel plugin",
plugin: slackPlugin,
setRuntime: setSlackRuntime,
registerFull: registerSlackPluginHttpRoutes,
});

View file

@ -36,7 +36,7 @@ import {
resolveSlackReplyToMode,
type ResolvedSlackAccount,
} from "./accounts.js";
import type { SlackActionContext } from "./action-runtime.js";
import { handleSlackAction, type SlackActionContext } from "./action-runtime.js";
import { resolveSlackAutoThreadId } from "./action-threading.js";
import { slackApprovalCapability } from "./approval-native.js";
import { createSlackActions } from "./channel-actions.js";
@ -45,13 +45,16 @@ import {
listSlackDirectoryGroupsFromConfig,
listSlackDirectoryPeersFromConfig,
} from "./directory-config.js";
import { listSlackDirectoryGroupsLive, listSlackDirectoryPeersLive } from "./directory-live.js";
import { shouldSuppressLocalSlackExecApprovalPrompt } from "./exec-approvals.js";
import { resolveSlackGroupRequireMention, resolveSlackGroupToolPolicy } from "./group-policy.js";
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
import { SLACK_TEXT_LIMIT } from "./limits.js";
import { monitorSlackProvider } from "./monitor.js";
import { slackOutbound } from "./outbound-adapter.js";
import { probeSlack, type SlackProbe } from "./probe.js";
import { resolveSlackReplyBlocks } from "./reply-blocks.js";
import { resolveSlackChannelAllowlist } from "./resolve-channels.js";
import { resolveSlackUserAllowlist } from "./resolve-users.js";
import {
DEFAULT_ACCOUNT_ID,
@ -65,6 +68,7 @@ import {
} from "./runtime-api.js";
import { getSlackRuntime } from "./runtime.js";
import { fetchSlackScopes } from "./scopes.js";
import { sendMessageSlack } from "./send.js";
import { slackSetupAdapter } from "./setup-core.js";
import { slackSetupWizard } from "./setup-surface.js";
import {
@ -89,14 +93,7 @@ const resolveSlackDmPolicy = createScopedDmSecurityResolver<ResolvedSlackAccount
});
function resolveSlackProbe() {
try {
return getSlackRuntime().channel.slack.probeSlack;
} catch (error) {
if (error instanceof Error && error.message === "Slack runtime not initialized") {
return probeSlack;
}
throw error;
}
return probeSlack;
}
// Select the appropriate Slack token for read/write operations.
@ -116,7 +113,7 @@ function getTokenForOperation(
return botToken ?? userToken;
}
type SlackSendFn = ReturnType<typeof getSlackRuntime>["channel"]["slack"]["sendMessageSlack"];
type SlackSendFn = typeof sendMessageSlack;
function resolveSlackSendContext(params: {
cfg: Parameters<typeof resolveSlackAccount>[0]["cfg"];
@ -125,9 +122,7 @@ function resolveSlackSendContext(params: {
replyToId?: string | number | null;
threadId?: string | number | null;
}) {
const send =
resolveOutboundSendDep<SlackSendFn>(params.deps, "slack") ??
getSlackRuntime().channel.slack.sendMessageSlack;
const send = resolveOutboundSendDep<SlackSendFn>(params.deps, "slack") ?? sendMessageSlack;
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
const token = getTokenForOperation(account, "write");
const botToken = account.botToken?.trim();
@ -323,7 +318,10 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
listPeers: async (params) => listSlackDirectoryPeersFromConfig(params),
listGroups: async (params) => listSlackDirectoryGroupsFromConfig(params),
...createRuntimeDirectoryLiveAdapter({
getRuntime: () => getSlackRuntime().channel.slack,
getRuntime: () => ({
listDirectoryGroupsLive: listSlackDirectoryGroupsLive,
listDirectoryPeersLive: listSlackDirectoryPeersLive,
}),
listPeersLive: (runtime) => runtime.listDirectoryPeersLive,
listGroupsLive: (runtime) => runtime.listDirectoryGroupsLive,
}),
@ -349,10 +347,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
inputs,
missingTokenNote: "missing Slack token",
resolveWithToken: ({ token, inputs }) =>
getSlackRuntime().channel.slack.resolveChannelAllowlist({
token,
entries: inputs,
}),
resolveSlackChannelAllowlist({ token, entries: inputs }),
mapResolved: (entry) =>
toResolvedTarget(entry, entry.archived ? "archived" : undefined),
});
@ -362,17 +357,14 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
inputs,
missingTokenNote: "missing Slack token",
resolveWithToken: ({ token, inputs }) =>
getSlackRuntime().channel.slack.resolveUserAllowlist({
token,
entries: inputs,
}),
resolveSlackUserAllowlist({ token, entries: inputs }),
mapResolved: (entry) => toResolvedTarget(entry, entry.note),
});
},
},
actions: createSlackActions(SLACK_CHANNEL, {
invoke: async (action, cfg, toolContext) =>
await getSlackRuntime().channel.slack.handleSlackAction(
await handleSlackAction(
action,
cfg as OpenClawConfig,
toolContext as SlackActionContext | undefined,
@ -450,7 +442,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
const botToken = account.botToken?.trim();
const appToken = account.appToken?.trim();
ctx.log?.info(`[${account.accountId}] starting provider`);
return getSlackRuntime().channel.slack.monitorSlackProvider({
return monitorSlackProvider({
botToken: botToken ?? "",
appToken: appToken ?? "",
accountId: account.accountId,
@ -480,11 +472,11 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
const botToken = account.botToken?.trim();
const tokenOverride = token && token !== botToken ? token : undefined;
if (tokenOverride) {
await getSlackRuntime().channel.slack.sendMessageSlack(`user:${id}`, message, {
await sendMessageSlack(`user:${id}`, message, {
token: tokenOverride,
});
} else {
await getSlackRuntime().channel.slack.sendMessageSlack(`user:${id}`, message);
await sendMessageSlack(`user:${id}`, message);
}
},
},

View file

@ -0,0 +1,23 @@
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { listSlackAccountIds, resolveSlackAccount } from "../accounts.js";
import { handleSlackHttpRequest, normalizeSlackWebhookPath } from "./registry.js";
export function registerSlackPluginHttpRoutes(api: OpenClawPluginApi): void {
const accountIds = new Set<string>([DEFAULT_ACCOUNT_ID, ...listSlackAccountIds(api.config)]);
const registeredPaths = new Set<string>();
for (const accountId of accountIds) {
const account = resolveSlackAccount({ cfg: api.config, accountId });
registeredPaths.add(normalizeSlackWebhookPath(account.config.webhookPath));
}
if (registeredPaths.size === 0) {
registeredPaths.add(normalizeSlackWebhookPath());
}
for (const path of registeredPaths) {
api.registerHttpRoute({
path,
auth: "plugin",
handler: async (req, res) => await handleSlackHttpRequest(req, res),
});
}
}

View file

@ -4,6 +4,7 @@ import {
createApproverRestrictedNativeApprovalCapability,
splitChannelApprovalCapability,
} from "openclaw/plugin-sdk/approval-runtime";
import type { ChannelApprovalCapability } from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { ExecApprovalRequest, PluginApprovalRequest } from "openclaw/plugin-sdk/infra-runtime";
import { listTelegramAccountIds } from "./accounts.js";
@ -84,7 +85,7 @@ const resolveTelegramApproverDmTargets = createChannelApproverDmTargetResolver({
mapApprover: (approver) => ({ to: approver }),
});
export const telegramApprovalCapability = createApproverRestrictedNativeApprovalCapability({
const telegramNativeApprovalCapability = createApproverRestrictedNativeApprovalCapability({
channel: "telegram",
channelLabel: "Telegram",
listAccountIds: listTelegramAccountIds,
@ -105,6 +106,32 @@ export const telegramApprovalCapability = createApproverRestrictedNativeApproval
resolveApproverDmTargets: resolveTelegramApproverDmTargets,
});
const resolveTelegramApproveCommandBehavior: NonNullable<
ChannelApprovalCapability["resolveApproveCommandBehavior"]
> = ({ cfg, accountId, senderId, approvalKind }) => {
if (approvalKind !== "exec") {
return undefined;
}
if (isTelegramExecApprovalClientEnabled({ cfg, accountId })) {
return undefined;
}
if (
isTelegramExecApprovalAuthorizedSender({ cfg, accountId, senderId }) &&
!isTelegramExecApprovalApprover({ cfg, accountId, senderId })
) {
return { kind: "ignore" };
}
return {
kind: "reply",
text: "❌ Telegram exec approvals are not enabled for this bot account.",
};
};
export const telegramApprovalCapability: ChannelApprovalCapability = {
...telegramNativeApprovalCapability,
resolveApproveCommandBehavior: resolveTelegramApproveCommandBehavior,
};
export const telegramNativeApprovalAdapter = splitChannelApprovalCapability(
telegramApprovalCapability,
);

View file

@ -14,6 +14,7 @@ import {
listWhatsAppAccountIds,
resolveDefaultWhatsAppAccountId,
resolveWhatsAppAccount,
hasAnyWhatsAppAuth,
type ResolvedWhatsAppAccount,
} from "./accounts.js";
import { WhatsAppChannelConfigSchema } from "./config-schema.js";
@ -141,6 +142,7 @@ export function createWhatsAppPluginBase(params: {
isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false,
disabledReason: () => "disabled",
isConfigured: params.isConfigured,
hasPersistedAuthState: ({ cfg }) => hasAnyWhatsAppAuth(cfg),
unconfiguredReason: () => "not linked",
describeAccount: (account) =>
describeAccountSnapshot({

View file

@ -40,154 +40,6 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
"resolveAnthropicVertexRegionFromBaseUrl",
],
},
{
subpath: "discord-account",
source: pluginSource("discord", "api.js"),
exports: ["resolveDiscordAccount", "ResolvedDiscordAccount"],
typeExports: ["ResolvedDiscordAccount"],
},
{
subpath: "discord-runtime-surface",
source: pluginSource("discord", "runtime-api.js"),
// Runtime entrypoints should be blocked until the owning plugin is active.
loadPolicy: "activated",
exports: [
"addRoleDiscord",
"auditDiscordChannelPermissions",
"banMemberDiscord",
"collectDiscordAuditChannelIds",
"createChannelDiscord",
"createScheduledEventDiscord",
"createThreadDiscord",
"deleteChannelDiscord",
"deleteMessageDiscord",
"discordMessageActions",
"editChannelDiscord",
"editDiscordComponentMessage",
"editMessageDiscord",
"fetchChannelInfoDiscord",
"fetchChannelPermissionsDiscord",
"fetchMemberInfoDiscord",
"fetchMessageDiscord",
"fetchReactionsDiscord",
"fetchRoleInfoDiscord",
"fetchVoiceStatusDiscord",
"getGateway",
"getPresence",
"hasAnyGuildPermissionDiscord",
"kickMemberDiscord",
"listDiscordDirectoryGroupsLive",
"listDiscordDirectoryPeersLive",
"listGuildChannelsDiscord",
"listGuildEmojisDiscord",
"listPinsDiscord",
"listScheduledEventsDiscord",
"listThreadsDiscord",
"monitorDiscordProvider",
"moveChannelDiscord",
"pinMessageDiscord",
"probeDiscord",
"reactMessageDiscord",
"readMessagesDiscord",
"registerBuiltDiscordComponentMessage",
"removeChannelPermissionDiscord",
"removeOwnReactionsDiscord",
"removeReactionDiscord",
"removeRoleDiscord",
"resolveDiscordChannelAllowlist",
"resolveDiscordOutboundSessionRoute",
"resolveDiscordUserAllowlist",
"searchMessagesDiscord",
"sendDiscordComponentMessage",
"sendMessageDiscord",
"sendPollDiscord",
"sendStickerDiscord",
"sendTypingDiscord",
"sendVoiceMessageDiscord",
"setChannelPermissionDiscord",
"timeoutMemberDiscord",
"unpinMessageDiscord",
"uploadEmojiDiscord",
"uploadStickerDiscord",
],
},
{
subpath: "discord-session-key",
source: pluginSource("discord", "session-key-api.js"),
exports: ["normalizeExplicitDiscordSessionKey"],
},
{
subpath: "discord-surface",
source: pluginSource("discord", "api.js"),
exports: [
"buildDiscordComponentMessage",
"collectDiscordStatusIssues",
"createDiscordActionGate",
"DiscordComponentMessageSpec",
"DiscordSendComponents",
"DiscordSendEmbeds",
"DiscordSendResult",
"handleDiscordMessageAction",
"inspectDiscordAccount",
"isDiscordExecApprovalApprover",
"isDiscordExecApprovalClientEnabled",
"InspectedDiscordAccount",
"listDiscordAccountIds",
"listDiscordDirectoryGroupsFromConfig",
"listDiscordDirectoryPeersFromConfig",
"looksLikeDiscordTargetId",
"normalizeDiscordMessagingTarget",
"normalizeDiscordOutboundTarget",
"readDiscordComponentSpec",
"ResolvedDiscordAccount",
"resolveDefaultDiscordAccountId",
"resolveDiscordAccount",
"resolveDiscordChannelId",
"resolveDiscordRuntimeGroupPolicy",
"resolveDiscordGroupRequireMention",
"resolveDiscordGroupToolPolicy",
],
typeExports: [
"DiscordComponentMessageSpec",
"DiscordProbe",
"DiscordSendComponents",
"DiscordSendEmbeds",
"DiscordSendResult",
"DiscordTokenResolution",
"InspectedDiscordAccount",
"ResolvedDiscordAccount",
],
},
{
subpath: "discord-thread-bindings",
source: pluginSource("discord", "runtime-api.js"),
loadPolicy: "activated",
directExports: {
unbindThreadBindingsBySessionKey: "./discord-maintenance.js",
},
exports: [
"autoBindSpawnedDiscordSubagent",
"createThreadBindingManager",
"getThreadBindingManager",
"listThreadBindingsBySessionKey",
"resolveThreadBindingIdleTimeoutMs",
"resolveThreadBindingInactivityExpiresAt",
"resolveThreadBindingMaxAgeExpiresAt",
"resolveThreadBindingMaxAgeMs",
"setThreadBindingIdleTimeoutBySessionKey",
"setThreadBindingMaxAgeBySessionKey",
"ThreadBindingManager",
"ThreadBindingRecord",
"ThreadBindingTargetKind",
"unbindThreadBindingsBySessionKey",
],
typeExports: ["ThreadBindingManager", "ThreadBindingRecord", "ThreadBindingTargetKind"],
},
{
subpath: "discord-timeouts",
source: pluginSource("discord", "timeouts.js"),
exports: ["DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS", "DISCORD_DEFAULT_LISTENER_TIMEOUT_MS"],
},
{
subpath: "anthropic-cli",
source: pluginSource("anthropic", "api.js"),
@ -836,32 +688,6 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
source: pluginSource("qianfan", "api.js"),
exports: ["QIANFAN_BASE_URL", "QIANFAN_DEFAULT_MODEL_ID", "buildQianfanProvider"],
},
{
subpath: "signal-account",
source: pluginSource("signal", "api.js"),
exports: ["resolveSignalAccount", "ResolvedSignalAccount"],
typeExports: ["ResolvedSignalAccount"],
},
{
subpath: "signal-surface",
source: pluginSource("signal", "api.js"),
exports: [
"isSignalSenderAllowed",
"listEnabledSignalAccounts",
"listSignalAccountIds",
"monitorSignalProvider",
"probeSignal",
"removeReactionSignal",
"ResolvedSignalAccount",
"resolveDefaultSignalAccountId",
"resolveSignalReactionLevel",
"sendMessageSignal",
"sendReactionSignal",
"signalMessageActions",
"SignalSender",
],
typeExports: ["ResolvedSignalAccount", "SignalProbe", "SignalSender"],
},
{
subpath: "provider-reasoning",
source: pluginSource("ollama", "api.js"),
@ -938,84 +764,6 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
"SYNTHETIC_MODEL_CATALOG",
],
},
{
subpath: "slack-target-parser",
source: pluginSource("slack", "api.js"),
exports: ["parseSlackTarget", "resolveSlackChannelId"],
},
{
subpath: "slack-account",
source: pluginSource("slack", "api.js"),
exports: ["resolveSlackAccount", "ResolvedSlackAccount"],
typeExports: ["ResolvedSlackAccount"],
},
{
subpath: "slack-runtime-surface",
source: pluginSource("slack", "runtime-api.js"),
loadPolicy: "activated",
exports: [
"handleSlackAction",
"listSlackDirectoryGroupsLive",
"listSlackDirectoryPeersLive",
"monitorSlackProvider",
"probeSlack",
"resolveSlackChannelAllowlist",
"resolveSlackUserAllowlist",
"sendMessageSlack",
"SlackActionContext",
],
typeExports: ["SlackActionContext"],
},
{
subpath: "slack-surface",
source: pluginSource("slack", "api.js"),
functionExports: [
"listSlackAccountIds",
"listSlackDirectoryGroupsFromConfig",
"listSlackDirectoryPeersFromConfig",
"resolveDefaultSlackAccountId",
"resolveSlackRuntimeGroupPolicy",
],
exports: [
"buildSlackThreadingToolContext",
"createSlackWebClient",
"deleteSlackMessage",
"downloadSlackFile",
"editSlackMessage",
"extractSlackToolSend",
"getSlackMemberInfo",
"handleSlackHttpRequest",
"inspectSlackAccount",
"InspectedSlackAccount",
"isSlackInteractiveRepliesEnabled",
"listEnabledSlackAccounts",
"listSlackAccountIds",
"listSlackDirectoryGroupsFromConfig",
"listSlackDirectoryPeersFromConfig",
"listSlackEmojis",
"listSlackMessageActions",
"listSlackPins",
"listSlackReactions",
"normalizeAllowListLower",
"parseSlackBlocksInput",
"recordSlackThreadParticipation",
"resolveDefaultSlackAccountId",
"resolveSlackAutoThreadId",
"resolveSlackGroupRequireMention",
"resolveSlackRuntimeGroupPolicy",
"resolveSlackGroupToolPolicy",
"resolveSlackReplyToMode",
"ResolvedSlackAccount",
"sendSlackMessage",
"pinSlackMessage",
"reactSlackMessage",
"readSlackMessages",
"removeOwnSlackReactions",
"removeSlackReaction",
"unpinSlackMessage",
],
typeExports: ["InspectedSlackAccount", "ResolvedSlackAccount", "SlackProbe"],
},
{
subpath: "together",
source: pluginSource("together", "api.js"),
@ -1040,71 +788,6 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
"VENICE_MODEL_CATALOG",
],
},
{
subpath: "telegram-account",
source: pluginSource("telegram", "api.js"),
exports: ["resolveTelegramAccount", "ResolvedTelegramAccount"],
typeExports: ["ResolvedTelegramAccount"],
},
{
subpath: "telegram-allow-from",
source: pluginSource("telegram", "api.js"),
exports: ["isNumericTelegramUserId", "normalizeTelegramAllowFromEntry"],
},
{
subpath: "telegram-surface",
source: pluginSource("telegram", "api.js"),
exports: [
"buildBrowseProvidersButton",
"buildModelsKeyboard",
"buildProviderKeyboard",
"buildTelegramGroupPeerId",
"calculateTotalPages",
"createTelegramActionGate",
"fetchTelegramChatId",
"getCacheStats",
"getModelsPageSize",
"inspectTelegramAccount",
"InspectedTelegramAccount",
"isTelegramExecApprovalApprover",
"isTelegramExecApprovalAuthorizedSender",
"isTelegramExecApprovalClientEnabled",
"isTelegramExecApprovalTargetRecipient",
"listTelegramAccountIds",
"listTelegramDirectoryGroupsFromConfig",
"listTelegramDirectoryPeersFromConfig",
"looksLikeTelegramTargetId",
"lookupTelegramChatId",
"normalizeTelegramMessagingTarget",
"parseTelegramReplyToMessageId",
"parseTelegramTarget",
"parseTelegramThreadId",
"ProviderInfo",
"ResolvedTelegramAccount",
"resolveTelegramAutoThreadId",
"resolveTelegramGroupRequireMention",
"resolveTelegramGroupToolPolicy",
"resolveTelegramInlineButtonsScope",
"resolveTelegramPollActionGateState",
"resolveTelegramReactionLevel",
"resolveTelegramTargetChatType",
"searchStickers",
"sendTelegramPayloadMessages",
"StickerMetadata",
"TelegramButtonStyle",
"TelegramInlineButtons",
],
typeExports: [
"InspectedTelegramAccount",
"ProviderInfo",
"ResolvedTelegramAccount",
"StickerMetadata",
"TelegramButtonStyle",
"TelegramInlineButtons",
"TelegramProbe",
"TelegramTokenResolution",
],
},
{
subpath: "vercel-ai-gateway",
source: pluginSource("vercel-ai-gateway", "api.js"),
@ -1193,36 +876,6 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
"ZAI_GLOBAL_BASE_URL",
],
},
{
subpath: "whatsapp-targets",
source: pluginSource("whatsapp", "api.js"),
exports: ["isWhatsAppGroupJid", "isWhatsAppUserTarget", "normalizeWhatsAppTarget"],
},
{
subpath: "whatsapp-surface",
source: pluginSource("whatsapp", "api.js"),
exportSources: {
DEFAULT_WEB_MEDIA_BYTES: pluginSource("whatsapp", "constants.js"),
},
exports: [
"DEFAULT_WEB_MEDIA_BYTES",
"hasAnyWhatsAppAuth",
"listEnabledWhatsAppAccounts",
"listWhatsAppDirectoryGroupsFromConfig",
"listWhatsAppDirectoryPeersFromConfig",
"resolveWhatsAppAccount",
"resolveWhatsAppGroupRequireMention",
"resolveWhatsAppGroupToolPolicy",
"resolveWhatsAppOutboundTarget",
"whatsappAccessControlTesting",
],
typeExports: [
"WebChannelStatus",
"WebInboundMessage",
"WebListenerCloseReason",
"WebMonitorTuning",
],
},
{
subpath: "zalo-setup",
source: pluginSource("zalo", "api.js"),

View file

@ -11,6 +11,7 @@ const acquireSessionWriteLockMock = vi.hoisted(() =>
vi.mock("../session-write-lock.js", () => ({
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
resolveSessionLockMaxHoldFromTimeout: () => 1,
}));
let truncateToolResultText: typeof import("./tool-result-truncation.js").truncateToolResultText;
@ -28,6 +29,7 @@ async function loadFreshToolResultTruncationModuleForTest() {
vi.resetModules();
vi.doMock("../session-write-lock.js", () => ({
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
resolveSessionLockMaxHoldFromTimeout: () => 1,
}));
({ onSessionTranscriptUpdate } = await import("../../sessions/transcript-events.js"));
({

View file

@ -9,6 +9,7 @@ const acquireSessionWriteLockMock = vi.hoisted(() =>
vi.mock("../session-write-lock.js", () => ({
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
resolveSessionLockMaxHoldFromTimeout: () => 1,
}));
let rewriteTranscriptEntriesInSessionFile: typeof import("./transcript-rewrite.js").rewriteTranscriptEntriesInSessionFile;
@ -20,6 +21,7 @@ async function loadFreshTranscriptRewriteModuleForTest() {
vi.resetModules();
vi.doMock("../session-write-lock.js", () => ({
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
resolveSessionLockMaxHoldFromTimeout: () => 1,
}));
({ onSessionTranscriptUpdate } = await import("../../sessions/transcript-events.js"));
({ installSessionToolResultGuard } = await import("../session-tool-result-guard.js"));

View file

@ -141,7 +141,7 @@ export function installReplyRuntimeMocks(mocks: ReplyRuntimeMocks) {
listSkillCommandsForWorkspace: () => [],
}));
vi.mock("../plugins/runtime/runtime-web-channel-boundary.js", () => ({
vi.mock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({
webAuthExists: mocks.webAuthExists,
getWebAuthAgeMs: mocks.getWebAuthAgeMs,
readWebSelfId: mocks.readWebSelfId,

View file

@ -1,7 +1,7 @@
import {
isTelegramExecApprovalAuthorizedSender,
isTelegramExecApprovalClientEnabled,
} from "../../../extensions/telegram/api.js";
getChannelPlugin,
resolveChannelApprovalCapability,
} from "../../channels/plugins/index.js";
import { callGateway } from "../../gateway/call.js";
import { ErrorCodes } from "../../gateway/protocol/index.js";
import { logVerbose } from "../../globals.js";
@ -77,17 +77,6 @@ function buildResolvedByLabel(params: Parameters<CommandHandler>[0]): string {
return `${channel}:${sender}`;
}
function isAuthorizedTelegramExecSender(params: Parameters<CommandHandler>[0]): boolean {
if (params.command.channel !== "telegram") {
return false;
}
return isTelegramExecApprovalAuthorizedSender({
cfg: params.cfg,
accountId: params.ctx.AccountId,
senderId: params.command.senderId,
});
}
function readErrorCode(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value : null;
}
@ -178,7 +167,21 @@ export const handleApproveCommand: CommandHandler = async (params, allowTextComm
}
const isPluginId = parsed.id.startsWith("plugin:");
const telegramExecAuthorizedSender = isAuthorizedTelegramExecSender(params);
const approvalCapability = resolveChannelApprovalCapability(
getChannelPlugin(params.command.channel),
);
const approveCommandBehavior = approvalCapability?.resolveApproveCommandBehavior?.({
cfg: params.cfg,
accountId: params.ctx.AccountId,
senderId: params.command.senderId,
approvalKind: isPluginId ? "plugin" : "exec",
});
if (approveCommandBehavior?.kind === "ignore") {
return { shouldContinue: false };
}
if (approveCommandBehavior?.kind === "reply") {
return { shouldContinue: false, reply: { text: approveCommandBehavior.text } };
}
const execApprovalAuthorization = resolveApprovalCommandAuthorization({
cfg: params.cfg,
channel: params.command.channel,
@ -203,18 +206,6 @@ export const handleApproveCommand: CommandHandler = async (params, allowTextComm
return { shouldContinue: false };
}
if (
params.command.channel === "telegram" &&
!isPluginId &&
!telegramExecAuthorizedSender &&
!isTelegramExecApprovalClientEnabled({ cfg: params.cfg, accountId: params.ctx.AccountId })
) {
return {
shouldContinue: false,
reply: { text: "❌ Telegram exec approvals are not enabled for this bot account." },
};
}
const missingScope = requireGatewayClientScopeForInternalChannel(params, {
label: "/approve",
allowedScopes: ["operator.approvals", "operator.admin"],

View file

@ -1,4 +1,8 @@
import { resolveFastModeState } from "../../agents/fast-mode.js";
import {
setChannelConversationBindingIdleTimeoutBySessionKey,
setChannelConversationBindingMaxAgeBySessionKey,
} from "../../channels/plugins/conversation-bindings.js";
import { formatThreadBindingDurationLabel } from "../../channels/thread-bindings-messages.js";
import { parseDurationMs } from "../../cli/parse-duration.js";
import { isRestartEnabled } from "../../config/commands.js";
@ -7,7 +11,6 @@ import { getSessionBindingService } from "../../infra/outbound/session-binding-s
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
import { scheduleGatewaySigusr1Restart, triggerOpenClawRestart } from "../../infra/restart.js";
import { loadCostUsageSummary, loadSessionCostSummary } from "../../infra/session-cost-usage.js";
import { createPluginRuntime } from "../../plugins/runtime/index.js";
import { formatTokenCount, formatUsd } from "../../utils/usage-format.js";
import { parseActivationCommand } from "../group-activation.js";
import { parseSendPolicyCommand } from "../send-policy.js";
@ -32,12 +35,6 @@ const SESSION_COMMAND_PREFIX = "/session";
const SESSION_DURATION_OFF_VALUES = new Set(["off", "disable", "disabled", "none", "0"]);
const SESSION_ACTION_IDLE = "idle";
const SESSION_ACTION_MAX_AGE = "max-age";
let cachedChannelRuntime: ReturnType<typeof createPluginRuntime>["channel"] | undefined;
function getChannelRuntime() {
cachedChannelRuntime ??= createPluginRuntime().channel;
return cachedChannelRuntime;
}
function resolveSessionCommandUsage() {
return "Usage: /session idle <duration|off> | /session max-age <duration|off> (example: /session idle 24h)";
@ -462,20 +459,14 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
})
: undefined;
const telegramConversationId = onTelegram ? resolveTelegramConversationId(params) : undefined;
const channelRuntime = getChannelRuntime();
const discordManager = onDiscord
? channelRuntime.discord.threadBindings.getManager(accountId)
: null;
if (onDiscord && !discordManager) {
return {
shouldContinue: false,
reply: { text: "⚠️ Discord thread bindings are unavailable for this account." },
};
}
const discordBinding =
onDiscord && threadId ? discordManager?.getByThreadId(threadId) : undefined;
onDiscord && threadId
? sessionBindingService.resolveByConversation({
channel: "discord",
accountId,
conversationId: threadId,
})
: null;
const telegramBinding =
onTelegram && telegramConversationId
? sessionBindingService.resolveByConversation({
@ -538,39 +529,18 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
};
}
const idleTimeoutMs = onDiscord
? channelRuntime.discord.threadBindings.resolveIdleTimeoutMs({
record: discordBinding!,
defaultIdleTimeoutMs: discordManager!.getIdleTimeoutMs(),
})
: resolveSessionBindingDurationMs(
(onMatrix ? matrixBinding : telegramBinding)!,
"idleTimeoutMs",
24 * 60 * 60 * 1000,
);
const idleExpiresAt = onDiscord
? channelRuntime.discord.threadBindings.resolveInactivityExpiresAt({
record: discordBinding!,
defaultIdleTimeoutMs: discordManager!.getIdleTimeoutMs(),
})
: idleTimeoutMs > 0
? resolveSessionBindingLastActivityAt((onMatrix ? matrixBinding : telegramBinding)!) +
idleTimeoutMs
: undefined;
const maxAgeMs = onDiscord
? channelRuntime.discord.threadBindings.resolveMaxAgeMs({
record: discordBinding!,
defaultMaxAgeMs: discordManager!.getMaxAgeMs(),
})
: resolveSessionBindingDurationMs((onMatrix ? matrixBinding : telegramBinding)!, "maxAgeMs", 0);
const maxAgeExpiresAt = onDiscord
? channelRuntime.discord.threadBindings.resolveMaxAgeExpiresAt({
record: discordBinding!,
defaultMaxAgeMs: discordManager!.getMaxAgeMs(),
})
: maxAgeMs > 0
? (onMatrix ? matrixBinding : telegramBinding)!.boundAt + maxAgeMs
const activeBinding = (onDiscord ? discordBinding : onMatrix ? matrixBinding : telegramBinding)!;
const idleTimeoutMs = resolveSessionBindingDurationMs(
activeBinding,
"idleTimeoutMs",
24 * 60 * 60 * 1000,
);
const idleExpiresAt =
idleTimeoutMs > 0
? resolveSessionBindingLastActivityAt(activeBinding) + idleTimeoutMs
: undefined;
const maxAgeMs = resolveSessionBindingDurationMs(activeBinding, "maxAgeMs", 0);
const maxAgeExpiresAt = maxAgeMs > 0 ? activeBinding.boundAt + maxAgeMs : undefined;
const durationArgRaw = tokens.slice(1).join("");
if (!durationArgRaw) {
@ -612,9 +582,7 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
}
const senderId = params.command.senderId?.trim() || "";
const boundBy = onDiscord
? discordBinding!.boundBy
: resolveSessionBindingBoundBy((onMatrix ? matrixBinding : telegramBinding)!);
const boundBy = resolveSessionBindingBoundBy(activeBinding);
if (boundBy && boundBy !== "system" && senderId && senderId !== boundBy) {
return {
shouldContinue: false,
@ -638,47 +606,21 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
};
}
const updatedBindings = (() => {
if (onDiscord) {
return action === SESSION_ACTION_IDLE
? channelRuntime.discord.threadBindings.setIdleTimeoutBySessionKey({
targetSessionKey: discordBinding!.targetSessionKey,
accountId,
idleTimeoutMs: durationMs,
})
: channelRuntime.discord.threadBindings.setMaxAgeBySessionKey({
targetSessionKey: discordBinding!.targetSessionKey,
accountId,
maxAgeMs: durationMs,
});
}
if (onMatrix) {
return action === SESSION_ACTION_IDLE
? channelRuntime.matrix.threadBindings.setIdleTimeoutBySessionKey({
targetSessionKey: matrixBinding!.targetSessionKey,
accountId,
idleTimeoutMs: durationMs,
})
: channelRuntime.matrix.threadBindings.setMaxAgeBySessionKey({
targetSessionKey: matrixBinding!.targetSessionKey,
accountId,
maxAgeMs: durationMs,
});
}
return action === SESSION_ACTION_IDLE
? channelRuntime.threadBindings.setIdleTimeoutBySessionKey({
channelId: "telegram",
targetSessionKey: telegramBinding!.targetSessionKey,
const channelId = onDiscord ? "discord" : onMatrix ? "matrix" : "telegram";
const updatedBindings =
action === SESSION_ACTION_IDLE
? setChannelConversationBindingIdleTimeoutBySessionKey({
channelId,
targetSessionKey: activeBinding.targetSessionKey,
accountId,
idleTimeoutMs: durationMs,
})
: channelRuntime.threadBindings.setMaxAgeBySessionKey({
channelId: "telegram",
targetSessionKey: telegramBinding!.targetSessionKey,
: setChannelConversationBindingMaxAgeBySessionKey({
channelId,
targetSessionKey: activeBinding.targetSessionKey,
accountId,
maxAgeMs: durationMs,
});
})();
if (updatedBindings.length === 0) {
return {
shouldContinue: false,

View file

@ -491,6 +491,26 @@ const telegramCommandTestPlugin: ChannelPlugin = {
formatAllowFrom: normalizeTelegramAllowFromEntries,
}),
auth: telegramNativeApprovalAdapter.auth,
approvalCapability: {
resolveApproveCommandBehavior: ({ cfg, accountId, senderId, approvalKind }) => {
if (approvalKind !== "exec") {
return undefined;
}
if (isTelegramExecApprovalClientEnabled({ cfg, accountId })) {
return undefined;
}
if (
isTelegramExecApprovalAuthorizedSender({ cfg, accountId, senderId }) &&
!getTelegramExecApprovalApprovers({ cfg, accountId }).includes(senderId?.trim() ?? "")
) {
return { kind: "ignore" } as const;
}
return {
kind: "reply",
text: "❌ Telegram exec approvals are not enabled for this bot account.",
} as const;
},
},
pairing: {
idLabel: "telegramUserId",
},

View file

@ -229,6 +229,7 @@ async function loadFreshFollowupRunnerModuleForTest() {
acquireSessionWriteLock: vi.fn(async () => ({
release: async () => {},
})),
resolveSessionLockMaxHoldFromTimeout: vi.fn(() => 1),
}));
vi.doMock("../../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn(async () => false),

View file

@ -28,6 +28,7 @@ import { initSessionState } from "./session.js";
// Perf: session-store locks are exercised elsewhere; most session tests don't need FS lock files.
vi.mock("../../agents/session-write-lock.js", () => ({
acquireSessionWriteLock: async () => ({ release: async () => {} }),
resolveSessionLockMaxHoldFromTimeout: () => 1,
}));
vi.mock("../../agents/model-catalog.js", () => ({

View file

@ -1,12 +1,12 @@
// Barrel exports for the web channel pieces. Splitting the original 900+ line
// module keeps responsibilities small and testable.
import { resolveWaWebAuthDir } from "./plugins/runtime/runtime-web-channel-boundary.js";
import { resolveWebChannelAuthDir } from "./plugins/runtime/runtime-web-channel-plugin.js";
export { HEARTBEAT_PROMPT } from "./auto-reply/heartbeat.js";
export { HEARTBEAT_TOKEN } from "./auto-reply/tokens.js";
export { loadWebMedia, optimizeImageToJpeg } from "./media/web-media.js";
export {
createWaSocket,
createWebChannelSocket as createWaSocket,
extractMediaPlaceholder,
extractText,
formatError,
@ -19,19 +19,19 @@ export {
pickWebChannel,
resolveHeartbeatRecipients,
runWebHeartbeatOnce,
sendMessageWhatsApp,
sendReactionWhatsApp,
waitForWaConnection,
sendWebChannelMessage as sendMessageWhatsApp,
sendWebChannelReaction as sendReactionWhatsApp,
waitForWebChannelConnection as waitForWaConnection,
webAuthExists,
} from "./plugins/runtime/runtime-web-channel-boundary.js";
} from "./plugins/runtime/runtime-web-channel-plugin.js";
// Keep the historic constant surface available, but resolve it through the
// plugin boundary only when a caller actually coerces the value to string.
class LazyWhatsAppAuthDir {
// web-channel plugin boundary only when a caller actually coerces the value to string.
class LazyWebChannelAuthDir {
#value: string | null = null;
#read(): string {
this.#value ??= resolveWaWebAuthDir();
this.#value ??= resolveWebChannelAuthDir();
return this.#value;
}
@ -48,4 +48,4 @@ class LazyWhatsAppAuthDir {
}
}
export const WA_WEB_AUTH_DIR = new LazyWhatsAppAuthDir() as unknown as string;
export const WA_WEB_AUTH_DIR = new LazyWebChannelAuthDir() as unknown as string;

View file

@ -29,15 +29,15 @@ describe("normalizeChatType", () => {
describe("WA_WEB_AUTH_DIR", () => {
afterEach(() => {
vi.doUnmock("../plugins/runtime/runtime-web-channel-boundary.js");
vi.doUnmock("../plugins/runtime/runtime-web-channel-plugin.js");
});
it("resolves lazily and caches across the legacy and channels/web entrypoints", async () => {
const resolveWaWebAuthDir = vi.fn(() => "/tmp/openclaw-whatsapp-auth");
const resolveWebChannelAuthDir = vi.fn(() => "/tmp/openclaw-whatsapp-auth");
vi.resetModules();
vi.doMock("../plugins/runtime/runtime-web-channel-boundary.js", () => ({
createWaSocket: vi.fn(),
vi.doMock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({
createWebChannelSocket: vi.fn(),
extractMediaPlaceholder: vi.fn(),
extractText: vi.fn(),
formatError: vi.fn(),
@ -49,20 +49,20 @@ describe("WA_WEB_AUTH_DIR", () => {
monitorWebInbox: vi.fn(),
pickWebChannel: vi.fn(),
resolveHeartbeatRecipients: vi.fn(),
resolveWaWebAuthDir,
resolveWebChannelAuthDir,
runWebHeartbeatOnce: vi.fn(),
sendMessageWhatsApp: vi.fn(),
sendReactionWhatsApp: vi.fn(),
waitForWaConnection: vi.fn(),
sendWebChannelMessage: vi.fn(),
sendWebChannelReaction: vi.fn(),
waitForWebChannelConnection: vi.fn(),
webAuthExists: vi.fn(),
}));
const channelWeb = await import("../channel-web.js");
const webEntry = await import("./web/index.js");
expect(resolveWaWebAuthDir).not.toHaveBeenCalled();
expect(resolveWebChannelAuthDir).not.toHaveBeenCalled();
expect(String(channelWeb.WA_WEB_AUTH_DIR)).toBe("/tmp/openclaw-whatsapp-auth");
expect(String(webEntry.WA_WEB_AUTH_DIR)).toBe("/tmp/openclaw-whatsapp-auth");
expect(resolveWaWebAuthDir).toHaveBeenCalledTimes(1);
expect(resolveWebChannelAuthDir).toHaveBeenCalledTimes(1);
});
});

View file

@ -5,10 +5,12 @@ function buildApprovalCapabilityFromLegacyPlugin(
): ChannelApprovalCapability | undefined {
const authorizeActorAction = plugin?.auth?.authorizeActorAction;
const getActionAvailabilityState = plugin?.auth?.getActionAvailabilityState;
const resolveApproveCommandBehavior = plugin?.auth?.resolveApproveCommandBehavior;
const approvals = plugin?.approvals;
if (
!authorizeActorAction &&
!getActionAvailabilityState &&
!resolveApproveCommandBehavior &&
!approvals?.delivery &&
!approvals?.render &&
!approvals?.native
@ -18,6 +20,7 @@ function buildApprovalCapabilityFromLegacyPlugin(
return {
authorizeActorAction,
getActionAvailabilityState,
resolveApproveCommandBehavior,
delivery: approvals?.delivery,
render: approvals?.render,
native: approvals?.native,
@ -39,6 +42,8 @@ export function resolveChannelApprovalCapability(
authorizeActorAction: capability.authorizeActorAction ?? legacyCapability.authorizeActorAction,
getActionAvailabilityState:
capability.getActionAvailabilityState ?? legacyCapability.getActionAvailabilityState,
resolveApproveCommandBehavior:
capability.resolveApproveCommandBehavior ?? legacyCapability.resolveApproveCommandBehavior,
delivery: capability.delivery ?? legacyCapability.delivery,
render: capability.render ?? legacyCapability.render,
native: capability.native ?? legacyCapability.native,

View file

@ -120,6 +120,7 @@ export type ChannelConfigAdapter<ResolvedAccount> = {
accountId?: string | null;
allowFrom: Array<string | number>;
}) => string[];
hasPersistedAuthState?: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean;
resolveDefaultTo?: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
@ -415,6 +416,7 @@ export type ChannelAuthAdapter = {
accountId?: string | null;
action: "approve";
}) => ChannelActionAvailabilityState;
resolveApproveCommandBehavior?: ChannelApprovalCapability["resolveApproveCommandBehavior"];
};
export type ChannelHeartbeatAdapter = {
@ -520,6 +522,11 @@ export type ChannelApprovalDeliveryAdapter = {
export type ChannelApprovalKind = "exec" | "plugin";
export type ChannelApproveCommandBehavior =
| { kind: "allow" }
| { kind: "ignore" }
| { kind: "reply"; text: string };
export type ChannelApprovalNativeSurface = "origin" | "approver-dm";
export type ChannelApprovalNativeTarget = {
@ -592,6 +599,12 @@ export type ChannelApprovalRenderAdapter = {
export type ChannelApprovalCapability = ChannelApprovalAdapter & {
authorizeActorAction?: ChannelAuthAdapter["authorizeActorAction"];
getActionAvailabilityState?: ChannelAuthAdapter["getActionAvailabilityState"];
resolveApproveCommandBehavior?: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
senderId?: string | null;
approvalKind: ChannelApprovalKind;
}) => ChannelApproveCommandBehavior | undefined;
};
export type ChannelApprovalAdapter = {

View file

@ -397,6 +397,10 @@ export type ChannelThreadingToolContext = {
/** Channel-owned messaging helpers for target parsing, routing, and payload shaping. */
export type ChannelMessagingAdapter = {
normalizeTarget?: (raw: string) => string | undefined;
normalizeExplicitSessionKey?: (params: {
sessionKey: string;
ctx: MsgContext;
}) => string | undefined;
/**
* Canonical plugin-owned session conversation grammar.
* Use this when the provider encodes thread or scoped-conversation semantics

View file

@ -1,11 +0,0 @@
import { inspectDiscordAccount as inspectDiscordAccountImpl } from "../../extensions/discord/api.js";
export type { InspectedDiscordAccount } from "../../extensions/discord/api.js";
type InspectDiscordAccount = typeof import("../../extensions/discord/api.js").inspectDiscordAccount;
export function inspectDiscordAccount(
...args: Parameters<InspectDiscordAccount>
): ReturnType<InspectDiscordAccount> {
return inspectDiscordAccountImpl(...args);
}

View file

@ -1,11 +0,0 @@
import { inspectSlackAccount as inspectSlackAccountImpl } from "../../extensions/slack/api.js";
export type { InspectedSlackAccount } from "../../extensions/slack/api.js";
type InspectSlackAccount = typeof import("../../extensions/slack/api.js").inspectSlackAccount;
export function inspectSlackAccount(
...args: Parameters<InspectSlackAccount>
): ReturnType<InspectSlackAccount> {
return inspectSlackAccountImpl(...args);
}

View file

@ -19,15 +19,6 @@ const sendFns = vi.hoisted(() => ({
imessage: vi.fn(async () => ({ messageId: "i1", chatId: "imessage:1" })),
}));
const whatsappBoundaryLoads = vi.hoisted(() => vi.fn());
vi.mock("../plugins/runtime/runtime-web-channel-boundary.js", async (importOriginal) => {
whatsappBoundaryLoads();
return await importOriginal<
typeof import("../plugins/runtime/runtime-web-channel-boundary.js")
>();
});
vi.mock("./send-runtime/whatsapp.js", () => {
moduleLoads.whatsapp();
return { runtimeSend: { sendMessage: sendFns.whatsapp } };
@ -112,13 +103,4 @@ describe("createDefaultDeps", () => {
expect(moduleLoads.discord).toHaveBeenCalledTimes(1);
expect(sendFns.discord).toHaveBeenCalledTimes(2);
});
it("does not import the whatsapp runtime boundary on deps module load", async () => {
await importFreshModule<typeof import("./deps.js")>(
import.meta.url,
"./deps.js?scope=no-whatsapp-runtime-on-import",
);
expect(whatsappBoundaryLoads).not.toHaveBeenCalled();
});
});

View file

@ -1,9 +1,7 @@
import { sendMessageDiscord as sendMessageDiscordImpl } from "../../../extensions/discord/runtime-api.js";
import { createPluginBoundaryRuntimeSend } from "./plugin-boundary-send.js";
type RuntimeSend = {
sendMessage: typeof import("../../../extensions/discord/runtime-api.js").sendMessageDiscord;
};
export const runtimeSend = {
sendMessage: sendMessageDiscordImpl,
} satisfies RuntimeSend;
export const runtimeSend = createPluginBoundaryRuntimeSend({
pluginId: "discord",
exportName: "sendMessageDiscord",
missingLabel: "Discord plugin runtime",
});

View file

@ -0,0 +1,37 @@
import { createCachedPluginBoundaryModuleLoader } from "../../plugins/runtime/runtime-plugin-boundary.js";
type RuntimeSendModule = Record<string, unknown>;
export type RuntimeSend = {
sendMessage: (...args: unknown[]) => Promise<unknown>;
};
function resolveRuntimeExport(
module: RuntimeSendModule | null,
pluginId: string,
exportName: string,
): (...args: unknown[]) => Promise<unknown> {
const candidate = module?.[exportName];
if (typeof candidate !== "function") {
throw new Error(`${pluginId} plugin runtime is unavailable: missing export '${exportName}'`);
}
return candidate as (...args: unknown[]) => Promise<unknown>;
}
export function createPluginBoundaryRuntimeSend(params: {
pluginId: string;
exportName: string;
missingLabel: string;
}): RuntimeSend {
const loadRuntimeModuleSync = createCachedPluginBoundaryModuleLoader<RuntimeSendModule>({
pluginId: params.pluginId,
entryBaseName: "runtime-api",
required: true,
missingLabel: params.missingLabel,
});
return {
sendMessage: (...args) =>
resolveRuntimeExport(loadRuntimeModuleSync(), params.pluginId, params.exportName)(...args),
};
}

View file

@ -1,9 +1,7 @@
import { sendMessageSignal as sendMessageSignalImpl } from "../../../extensions/signal/runtime-api.js";
import { createPluginBoundaryRuntimeSend } from "./plugin-boundary-send.js";
type RuntimeSend = {
sendMessage: typeof import("../../../extensions/signal/runtime-api.js").sendMessageSignal;
};
export const runtimeSend = {
sendMessage: sendMessageSignalImpl,
} satisfies RuntimeSend;
export const runtimeSend = createPluginBoundaryRuntimeSend({
pluginId: "signal",
exportName: "sendMessageSignal",
missingLabel: "Signal plugin runtime",
});

View file

@ -1,9 +1,7 @@
import { sendMessageSlack as sendMessageSlackImpl } from "../../../extensions/slack/runtime-api.js";
import { createPluginBoundaryRuntimeSend } from "./plugin-boundary-send.js";
type RuntimeSend = {
sendMessage: typeof import("../../../extensions/slack/runtime-api.js").sendMessageSlack;
};
export const runtimeSend = {
sendMessage: sendMessageSlackImpl,
} satisfies RuntimeSend;
export const runtimeSend = createPluginBoundaryRuntimeSend({
pluginId: "slack",
exportName: "sendMessageSlack",
missingLabel: "Slack plugin runtime",
});

View file

@ -1,9 +1,7 @@
import { sendMessageWhatsApp as sendMessageWhatsAppImpl } from "../../plugins/runtime/runtime-web-channel-boundary.js";
import { createPluginBoundaryRuntimeSend } from "./plugin-boundary-send.js";
type RuntimeSend = {
sendMessage: typeof import("../../plugins/runtime/runtime-web-channel-boundary.js").sendMessageWhatsApp;
};
export const runtimeSend = {
sendMessage: sendMessageWhatsAppImpl,
} satisfies RuntimeSend;
export const runtimeSend = createPluginBoundaryRuntimeSend({
pluginId: "whatsapp",
exportName: "sendMessageWhatsApp",
missingLabel: "WhatsApp plugin runtime",
});

View file

@ -41,7 +41,7 @@ async function loadFreshHealthModulesForTest() {
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
updateLastRoute: vi.fn().mockResolvedValue(undefined),
}));
vi.doMock("../plugins/runtime/runtime-web-channel-boundary.js", () => ({
vi.doMock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({
webAuthExists: vi.fn(async () => true),
getWebAuthAgeMs: vi.fn(() => 1234),
readWebSelfId: vi.fn(() => ({ e164: null, jid: null })),

View file

@ -383,7 +383,7 @@ vi.mock("../channels/plugins/index.js", () => ({
},
] as unknown,
}));
vi.mock("../plugins/runtime/runtime-web-channel-boundary.js", () => ({
vi.mock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({
webAuthExists: mocks.webAuthExists,
getWebAuthAgeMs: mocks.getWebAuthAgeMs,
readWebSelfId: mocks.readWebSelfId,

View file

@ -1,5 +1,5 @@
import { hasAnyWhatsAppAuth } from "../../extensions/whatsapp/auth-presence.js";
import { hasMeaningfulChannelConfig } from "../channels/config-presence.js";
import { getChannelPlugin } from "../channels/plugins/index.js";
import { isRecord } from "../utils.js";
import type { OpenClawConfig } from "./config.js";
@ -127,17 +127,6 @@ function isStructuredChannelConfigured(
return hasMeaningfulChannelConfig(entry);
}
function isWhatsAppConfigured(cfg: OpenClawConfig): boolean {
if (hasAnyWhatsAppAuth(cfg)) {
return true;
}
const entry = resolveChannelConfig(cfg, "whatsapp");
if (!entry) {
return false;
}
return hasMeaningfulChannelConfig(entry);
}
function isGenericChannelConfigured(cfg: OpenClawConfig, channelId: string): boolean {
const entry = resolveChannelConfig(cfg, channelId);
return hasMeaningfulChannelConfig(entry);
@ -148,8 +137,12 @@ export function isChannelConfigured(
channelId: string,
env: NodeJS.ProcessEnv = process.env,
): boolean {
if (channelId === "whatsapp") {
return isWhatsAppConfigured(cfg);
const pluginConfigured = getChannelPlugin(channelId)?.config.hasPersistedAuthState?.({
cfg,
env,
});
if (pluginConfigured) {
return true;
}
const spec = STRUCTURED_CHANNEL_CONFIG_SPECS[channelId];
if (spec) {

View file

@ -1,5 +1,11 @@
import { describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { MsgContext } from "../../auto-reply/templating.js";
import type { ChannelPlugin } from "../../channels/plugins/types.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
import {
createChannelTestPluginBase,
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import { normalizeExplicitSessionKey } from "./explicit-session-key-normalization.js";
function makeCtx(overrides: Partial<MsgContext>): MsgContext {
@ -11,6 +17,54 @@ function makeCtx(overrides: Partial<MsgContext>): MsgContext {
} as MsgContext;
}
beforeEach(() => {
const discordPlugin: ChannelPlugin = {
...createChannelTestPluginBase({
id: "discord",
label: "Discord",
docsPath: "/channels/discord",
}),
messaging: {
normalizeExplicitSessionKey: ({ sessionKey, ctx }) => {
const normalizedChatType = ctx.ChatType?.trim().toLowerCase();
let normalized = sessionKey.trim().toLowerCase();
if (normalizedChatType !== "direct" && normalizedChatType !== "dm") {
return normalized;
}
normalized = normalized.replace(/^(discord:)dm:/, "$1direct:");
normalized = normalized.replace(/^(agent:[^:]+:discord:)dm:/, "$1direct:");
const match = normalized.match(/^((?:agent:[^:]+:)?)discord:channel:([^:]+)$/);
if (!match) {
return normalized;
}
const from = (ctx.From ?? "").trim().toLowerCase();
const senderId = (ctx.SenderId ?? "").trim().toLowerCase();
const fromDiscordId =
from.startsWith("discord:") && !from.includes(":channel:") && !from.includes(":group:")
? from.slice("discord:".length)
: "";
const directId = senderId || fromDiscordId;
return directId && directId === match[2]
? `${match[1]}discord:direct:${match[2]}`
: normalized;
},
},
};
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "discord",
plugin: discordPlugin,
source: "test",
},
]),
);
});
afterEach(() => {
resetPluginRuntimeStateForTest();
});
describe("normalizeExplicitSessionKey", () => {
it("dispatches discord keys through the provider normalizer", () => {
expect(

View file

@ -1,50 +1,44 @@
import { normalizeExplicitDiscordSessionKey } from "../../../extensions/discord/session-key-api.js";
import type { MsgContext } from "../../auto-reply/templating.js";
import { getChannelPlugin, listChannelPlugins } from "../../channels/plugins/index.js";
import { normalizeMessageChannel } from "../../utils/message-channel.js";
type ExplicitSessionKeyNormalizer = (sessionKey: string, ctx: MsgContext) => string;
type ExplicitSessionKeyNormalizerEntry = {
provider: string;
normalize: ExplicitSessionKeyNormalizer;
matches: (params: {
sessionKey: string;
provider?: string;
surface?: string;
from: string;
}) => boolean;
};
const EXPLICIT_SESSION_KEY_NORMALIZERS: ExplicitSessionKeyNormalizerEntry[] = [
{
provider: "discord",
normalize: normalizeExplicitDiscordSessionKey,
matches: ({ sessionKey, provider, surface, from }) =>
surface === "discord" ||
provider === "discord" ||
from.startsWith("discord:") ||
sessionKey.startsWith("discord:") ||
sessionKey.includes(":discord:"),
},
];
function resolveExplicitSessionKeyNormalizer(
function resolveExplicitSessionKeyNormalizerCandidates(
sessionKey: string,
ctx: Pick<MsgContext, "From" | "Provider" | "Surface">,
): ExplicitSessionKeyNormalizer | undefined {
): string[] {
const normalizedProvider = ctx.Provider?.trim().toLowerCase();
const normalizedSurface = ctx.Surface?.trim().toLowerCase();
const normalizedFrom = (ctx.From ?? "").trim().toLowerCase();
return EXPLICIT_SESSION_KEY_NORMALIZERS.find((entry) =>
entry.matches({
sessionKey,
provider: normalizedProvider,
surface: normalizedSurface,
from: normalizedFrom,
}),
)?.normalize;
const candidates = new Set<string>();
const maybeAdd = (value?: string | null) => {
const normalized = normalizeMessageChannel(value);
if (normalized) {
candidates.add(normalized);
}
};
maybeAdd(normalizedSurface);
maybeAdd(normalizedProvider);
maybeAdd(normalizedFrom.split(":", 1)[0]);
for (const plugin of listChannelPlugins()) {
const pluginId = normalizeMessageChannel(plugin.id);
if (!pluginId) {
continue;
}
if (sessionKey.startsWith(`${pluginId}:`) || sessionKey.includes(`:${pluginId}:`)) {
candidates.add(pluginId);
}
}
return [...candidates];
}
export function normalizeExplicitSessionKey(sessionKey: string, ctx: MsgContext): string {
const normalized = sessionKey.trim().toLowerCase();
const normalize = resolveExplicitSessionKeyNormalizer(normalized, ctx);
return normalize ? normalize(normalized, ctx) : normalized;
for (const channelId of resolveExplicitSessionKeyNormalizerCandidates(normalized, ctx)) {
const normalize = getChannelPlugin(channelId)?.messaging?.normalizeExplicitSessionKey;
const next = normalize?.({ sessionKey: normalized, ctx });
if (typeof next === "string" && next.trim()) {
return next.trim().toLowerCase();
}
}
return normalized;
}

View file

@ -1,5 +1,11 @@
import { describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { MsgContext } from "../../auto-reply/templating.js";
import type { ChannelPlugin } from "../../channels/plugins/types.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
import {
createChannelTestPluginBase,
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import { resolveSessionKey } from "./session-key.js";
function makeCtx(overrides: Partial<MsgContext>): MsgContext {
@ -11,6 +17,54 @@ function makeCtx(overrides: Partial<MsgContext>): MsgContext {
} as MsgContext;
}
beforeEach(() => {
const discordPlugin: ChannelPlugin = {
...createChannelTestPluginBase({
id: "discord",
label: "Discord",
docsPath: "/channels/discord",
}),
messaging: {
normalizeExplicitSessionKey: ({ sessionKey, ctx }) => {
const normalizedChatType = ctx.ChatType?.trim().toLowerCase();
let normalized = sessionKey.trim().toLowerCase();
if (normalizedChatType !== "direct" && normalizedChatType !== "dm") {
return normalized;
}
normalized = normalized.replace(/^(discord:)dm:/, "$1direct:");
normalized = normalized.replace(/^(agent:[^:]+:discord:)dm:/, "$1direct:");
const match = normalized.match(/^((?:agent:[^:]+:)?)discord:channel:([^:]+)$/);
if (!match) {
return normalized;
}
const from = (ctx.From ?? "").trim().toLowerCase();
const senderId = (ctx.SenderId ?? "").trim().toLowerCase();
const fromDiscordId =
from.startsWith("discord:") && !from.includes(":channel:") && !from.includes(":group:")
? from.slice("discord:".length)
: "";
const directId = senderId || fromDiscordId;
return directId && directId === match[2]
? `${match[1]}discord:direct:${match[2]}`
: normalized;
},
},
};
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "discord",
plugin: discordPlugin,
source: "test",
},
]),
);
});
afterEach(() => {
resetPluginRuntimeStateForTest();
});
describe("resolveSessionKey", () => {
describe("Discord DM session key normalization", () => {
it("passes through correct discord:direct keys unchanged", () => {

View file

@ -4,10 +4,6 @@ import type { OpenClawConfig } from "../../config/config.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
const whatsappAccountMocks = vi.hoisted(() => ({
resolveWhatsAppAccount: vi.fn<() => { allowFrom: string[] }>(() => ({ allowFrom: [] })),
}));
vi.mock("../../config/sessions.js", () => ({
loadSessionStore: vi.fn().mockReturnValue({}),
resolveAgentMainSessionKey: vi.fn().mockReturnValue("agent:test:main"),
@ -28,16 +24,11 @@ vi.mock("../../pairing/pairing-store.js", () => ({
readChannelAllowFromStoreSync: vi.fn(() => []),
}));
vi.mock("../../../extensions/whatsapp/api.js", () => ({
resolveWhatsAppAccount: whatsappAccountMocks.resolveWhatsAppAccount,
}));
const mockedModuleIds = [
"../../config/sessions.js",
"../../infra/outbound/channel-selection.js",
"../../infra/outbound/target-resolver.js",
"../../pairing/pairing-store.js",
"../../../extensions/whatsapp/api.js",
];
import { loadSessionStore } from "../../config/sessions.js";
@ -65,6 +56,22 @@ function createStubOutbound(label: string): ChannelOutboundAdapter {
};
}
function createAllowlistAwareStubOutbound(label: string): ChannelOutboundAdapter {
return {
deliveryMode: "gateway",
resolveTarget: ({ to, allowFrom }) => {
const trimmed = typeof to === "string" ? to.trim() : "";
if (!trimmed) {
return { ok: false, error: new Error(`${label} requires target`) };
}
if (allowFrom && allowFrom.length > 0 && !allowFrom.includes(trimmed)) {
return { ok: false, error: new Error(`${label} target blocked`) };
}
return { ok: true, to: trimmed };
},
};
}
beforeEach(() => {
resetPluginRuntimeStateForTest();
setActivePluginRegistry(
@ -79,10 +86,18 @@ beforeEach(() => {
},
{
pluginId: "whatsapp",
plugin: createOutboundTestPlugin({
id: "whatsapp",
outbound: createStubOutbound("WhatsApp"),
}),
plugin: {
...createOutboundTestPlugin({
id: "whatsapp",
outbound: createAllowlistAwareStubOutbound("WhatsApp"),
}),
config: {
listAccountIds: () => [],
resolveAccount: () => ({}),
resolveAllowFrom: ({ cfg }: { cfg: OpenClawConfig }) =>
(cfg.channels?.whatsapp as { allowFrom?: string[] } | undefined)?.allowFrom,
},
},
source: "test",
},
]),
@ -142,10 +157,6 @@ function setLastSessionEntry(params: {
});
}
function setWhatsAppAllowFrom(allowFrom: string[]) {
vi.mocked(whatsappAccountMocks.resolveWhatsAppAccount).mockReturnValue({ allowFrom });
}
function setStoredWhatsAppAllowFrom(allowFrom: string[]) {
vi.mocked(readChannelAllowFromStoreSync).mockReturnValue(allowFrom);
}
@ -176,10 +187,9 @@ describe("resolveDeliveryTarget", () => {
lastChannel: "whatsapp",
lastTo: "+15550000099",
});
setWhatsAppAllowFrom([]);
setStoredWhatsAppAllowFrom(["+15550000001"]);
const cfg = makeCfg({ bindings: [] });
const cfg = makeCfg({ bindings: [], channels: { whatsapp: { allowFrom: [] } } });
const result = await resolveLastTarget(cfg);
expect(result.channel).toBe("whatsapp");
@ -192,10 +202,9 @@ describe("resolveDeliveryTarget", () => {
lastChannel: "whatsapp",
lastTo: "+15550000099",
});
setWhatsAppAllowFrom([]);
setStoredWhatsAppAllowFrom(["+15550000001"]);
const cfg = makeCfg({ bindings: [] });
const cfg = makeCfg({ bindings: [], channels: { whatsapp: { allowFrom: [] } } });
const result = await resolveDeliveryTarget(cfg, AGENT_ID, {
channel: "whatsapp",
to: "+15550000099",

View file

@ -1,5 +1,4 @@
import { resolveWhatsAppAccount } from "../../../extensions/whatsapp/api.js";
import { normalizeWhatsAppTarget } from "../../channels/plugins/normalize/whatsapp.js";
import { getChannelPlugin } from "../../channels/plugins/index.js";
import type { ChannelId } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
@ -15,6 +14,7 @@ import {
resolveSessionDeliveryTarget,
} from "../../infra/outbound/targets.js";
import { readChannelAllowFromStoreSync } from "../../pairing/pairing-store.js";
import { mapAllowFromEntries } from "../../plugin-sdk/channel-config-helpers.js";
import { buildChannelAccountBindings } from "../../routing/bindings.js";
import { normalizeAccountId, normalizeAgentId } from "../../routing/session-key.js";
@ -152,26 +152,32 @@ export async function resolveDeliveryTarget(
};
}
let allowFromOverride: string[] | undefined;
if (channel === "whatsapp") {
const resolvedAccountId = normalizeAccountId(accountId);
const configuredAllowFromRaw =
resolveWhatsAppAccount({ cfg, accountId: resolvedAccountId }).allowFrom ?? [];
const configuredAllowFrom = configuredAllowFromRaw
.map((entry) => String(entry).trim())
.filter((entry) => entry && entry !== "*")
.map((entry) => normalizeWhatsAppTarget(entry))
.filter((entry): entry is string => Boolean(entry));
const storeAllowFrom = readChannelAllowFromStoreSync("whatsapp", process.env, resolvedAccountId)
.map((entry) => normalizeWhatsAppTarget(entry))
.filter((entry): entry is string => Boolean(entry));
allowFromOverride = [...new Set([...configuredAllowFrom, ...storeAllowFrom])];
const channelPlugin = getChannelPlugin(channel);
const resolvedAccountId = normalizeAccountId(accountId);
const configuredAllowFromRaw = channelPlugin?.config.resolveAllowFrom?.({
cfg,
accountId: resolvedAccountId,
});
const configuredAllowFrom = configuredAllowFromRaw
? mapAllowFromEntries(configuredAllowFromRaw)
: [];
const storeAllowFrom = mapAllowFromEntries(
readChannelAllowFromStoreSync(channel, process.env, resolvedAccountId),
);
const allowFromOverride = [...new Set([...configuredAllowFrom, ...storeAllowFrom])];
const effectiveAllowFrom = mode === "implicit" ? allowFromOverride : undefined;
if (toCandidate && mode === "implicit" && allowFromOverride.length > 0) {
const normalizedCurrentTarget = normalizeWhatsAppTarget(toCandidate);
if (!normalizedCurrentTarget || !allowFromOverride.includes(normalizedCurrentTarget)) {
toCandidate = allowFromOverride[0];
}
if (toCandidate && mode === "implicit" && allowFromOverride.length > 0) {
const currentTargetResolution = resolveOutboundTarget({
channel,
to: toCandidate,
cfg,
accountId,
mode,
allowFrom: effectiveAllowFrom,
});
if (!currentTargetResolution.ok) {
toCandidate = allowFromOverride[0];
}
}
@ -181,7 +187,7 @@ export async function resolveDeliveryTarget(
cfg,
accountId,
mode,
allowFrom: allowFromOverride,
allowFrom: effectiveAllowFrom,
});
if (!docked.ok) {
return {

View file

@ -8,7 +8,6 @@ import {
import { createServer as createHttpsServer } from "node:https";
import type { TlsOptions } from "node:tls";
import type { WebSocketServer } from "ws";
import { handleSlackHttpRequest } from "../../extensions/slack/api.js";
import { resolveAgentAvatar } from "../agents/identity-avatar.js";
import { CANVAS_WS_PATH, handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
import type { CanvasHostHandler } from "../canvas-host/server.js";
@ -325,9 +324,8 @@ export async function runGatewayHttpRequestStages(
} catch (err) {
// Log and skip the failing stage so subsequent stages (control-ui,
// gateway-probes, etc.) remain reachable. A common trigger is a
// bundled-plugin facade that fails to load because an optional
// dependency is missing (e.g. @slack/bolt after the lazy-facade
// refactor).
// plugin-owned route/runtime code can still fail to load when an
// optional dependency is missing. Keep later stages reachable.
console.error(`[gateway-http] stage "${stage.name}" threw — skipping:`, err);
}
}
@ -898,10 +896,6 @@ export function createGatewayHttpServer(opts: {
rateLimiter,
}),
},
{
name: "slack",
run: () => handleSlackHttpRequest(req, res),
},
];
if (openResponsesEnabled) {
requestStages.push({

View file

@ -142,21 +142,16 @@ vi.mock("../plugins/hook-runner-global.js", async (importOriginal) => {
};
});
vi.mock("../plugins/runtime/runtime-discord.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../plugins/runtime/runtime-discord.js")>();
vi.mock("../infra/outbound/session-binding-service.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../infra/outbound/session-binding-service.js")>();
return {
...actual,
createRuntimeDiscord: () => {
const runtime = actual.createRuntimeDiscord();
return {
...runtime,
threadBindings: {
...runtime.threadBindings,
unbindBySessionKey: (params: unknown) =>
threadBindingMocks.unbindThreadBindingsBySessionKey(params),
},
};
},
getSessionBindingService: () => ({
...actual.getSessionBindingService(),
unbind: async (params: unknown) =>
threadBindingMocks.unbindThreadBindingsBySessionKey(params),
}),
};
});
@ -1862,9 +1857,7 @@ describe("gateway server sessions", () => {
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledTimes(1);
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledWith({
targetSessionKey: "agent:main:discord:group:dev",
targetKind: "acp",
reason: "session-delete",
sendFarewell: true,
});
ws.close();
@ -2022,9 +2015,7 @@ describe("gateway server sessions", () => {
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledTimes(1);
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledWith({
targetSessionKey: "agent:main:subagent:worker",
targetKind: "subagent",
reason: "session-delete",
sendFarewell: true,
});
ws.close();
@ -2053,9 +2044,7 @@ describe("gateway server sessions", () => {
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledTimes(1);
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledWith({
targetSessionKey: "agent:main:subagent:worker",
targetKind: "subagent",
reason: "session-delete",
sendFarewell: true,
});
ws.close();
@ -2083,9 +2072,7 @@ describe("gateway server sessions", () => {
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledTimes(1);
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledWith({
targetSessionKey: "agent:main:subagent:worker",
targetKind: "subagent",
reason: "session-delete",
sendFarewell: true,
});
ws.close();
@ -2140,9 +2127,7 @@ describe("gateway server sessions", () => {
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledTimes(1);
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledWith({
targetSessionKey: "agent:main:main",
targetKind: "acp",
reason: "session-reset",
sendFarewell: true,
});
ws.close();
@ -2308,9 +2293,7 @@ describe("gateway server sessions", () => {
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledTimes(1);
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledWith({
targetSessionKey: "agent:main:subagent:worker",
targetKind: "subagent",
reason: "session-reset",
sendFarewell: true,
});
ws.close();
@ -2338,9 +2321,7 @@ describe("gateway server sessions", () => {
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledTimes(1);
expect(threadBindingMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledWith({
targetSessionKey: "agent:main:main",
targetKind: "acp",
reason: "session-reset",
sendFarewell: true,
});
ws.close();

View file

@ -21,9 +21,9 @@ import {
import { resolveSessionFilePath, resolveSessionFilePathOptions } from "../config/sessions/paths.js";
import { logVerbose } from "../globals.js";
import { createInternalHookEvent, triggerInternalHook } from "../hooks/internal-hooks.js";
import { getSessionBindingService } from "../infra/outbound/session-binding-service.js";
import { closeTrackedBrowserTabsForSessions } from "../plugin-sdk/browser-maintenance.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
import { createPluginRuntime } from "../plugins/runtime/index.js";
import {
isSubagentSessionKey,
normalizeAgentId,
@ -44,12 +44,6 @@ import {
} from "./session-utils.js";
const ACP_RUNTIME_CLEANUP_TIMEOUT_MS = 15_000;
let cachedChannelRuntime: ReturnType<typeof createPluginRuntime>["channel"] | undefined;
function getChannelRuntime() {
cachedChannelRuntime ??= createPluginRuntime().channel;
return cachedChannelRuntime;
}
function stripRuntimeModelState(entry?: SessionEntry): SessionEntry | undefined {
if (!entry) {
@ -164,12 +158,9 @@ export async function emitSessionUnboundLifecycleEvent(params: {
emitHooks?: boolean;
}) {
const targetKind = isSubagentSessionKey(params.targetSessionKey) ? "subagent" : "acp";
const channelRuntime = getChannelRuntime();
channelRuntime.discord.threadBindings.unbindBySessionKey({
await getSessionBindingService().unbind({
targetSessionKey: params.targetSessionKey,
targetKind,
reason: params.reason,
sendFarewell: true,
});
if (params.emitHooks === false) {

View file

@ -821,12 +821,12 @@ vi.mock("../plugins/loader.js", async () => {
loadOpenClawPlugins: () => pluginRegistryState.registry,
};
});
vi.mock("../plugins/runtime/runtime-web-channel-boundary.js", () => ({
sendMessageWhatsApp: (...args: unknown[]) =>
vi.mock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({
sendWebChannelMessage: (...args: unknown[]) =>
(hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args),
}));
vi.mock("/src/plugins/runtime/runtime-web-channel-boundary.js", () => ({
sendMessageWhatsApp: (...args: unknown[]) =>
vi.mock("/src/plugins/runtime/runtime-web-channel-plugin.js", () => ({
sendWebChannelMessage: (...args: unknown[]) =>
(hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args),
}));

View file

@ -18,75 +18,6 @@ export interface PluginSdkFacadeTypeMap {
};
types: {};
};
"discord-account": {
module: typeof import("@openclaw/discord/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/discord/api.js");
};
};
types: {
ResolvedDiscordAccount: import("@openclaw/discord/api.js").ResolvedDiscordAccount;
};
};
"discord-runtime-surface": {
module: typeof import("@openclaw/discord/runtime-api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/discord/runtime-api.js");
};
};
types: {};
};
"discord-session-key": {
module: typeof import("@openclaw/discord/session-key-api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/discord/session-key-api.js");
};
};
types: {};
};
"discord-surface": {
module: typeof import("@openclaw/discord/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/discord/api.js");
};
};
types: {
DiscordComponentMessageSpec: import("@openclaw/discord/api.js").DiscordComponentMessageSpec;
DiscordProbe: import("@openclaw/discord/api.js").DiscordProbe;
DiscordSendComponents: import("@openclaw/discord/api.js").DiscordSendComponents;
DiscordSendEmbeds: import("@openclaw/discord/api.js").DiscordSendEmbeds;
DiscordSendResult: import("@openclaw/discord/api.js").DiscordSendResult;
DiscordTokenResolution: import("@openclaw/discord/api.js").DiscordTokenResolution;
InspectedDiscordAccount: import("@openclaw/discord/api.js").InspectedDiscordAccount;
ResolvedDiscordAccount: import("@openclaw/discord/api.js").ResolvedDiscordAccount;
};
};
"discord-thread-bindings": {
module: typeof import("@openclaw/discord/runtime-api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/discord/runtime-api.js");
};
};
types: {
ThreadBindingManager: import("@openclaw/discord/runtime-api.js").ThreadBindingManager;
ThreadBindingRecord: import("@openclaw/discord/runtime-api.js").ThreadBindingRecord;
ThreadBindingTargetKind: import("@openclaw/discord/runtime-api.js").ThreadBindingTargetKind;
};
};
"discord-timeouts": {
module: typeof import("@openclaw/discord/timeouts.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/discord/timeouts.js");
};
};
types: {};
};
"anthropic-cli": {
module: typeof import("@openclaw/anthropic/api.js");
sourceModules: {
@ -510,30 +441,6 @@ export interface PluginSdkFacadeTypeMap {
};
types: {};
};
"signal-account": {
module: typeof import("@openclaw/signal/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/signal/api.js");
};
};
types: {
ResolvedSignalAccount: import("@openclaw/signal/api.js").ResolvedSignalAccount;
};
};
"signal-surface": {
module: typeof import("@openclaw/signal/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/signal/api.js");
};
};
types: {
ResolvedSignalAccount: import("@openclaw/signal/api.js").ResolvedSignalAccount;
SignalProbe: import("@openclaw/signal/api.js").SignalProbe;
SignalSender: import("@openclaw/signal/api.js").SignalSender;
};
};
"provider-reasoning": {
module: typeof import("@openclaw/ollama/api.js");
sourceModules: {
@ -578,50 +485,6 @@ export interface PluginSdkFacadeTypeMap {
};
types: {};
};
"slack-target-parser": {
module: typeof import("@openclaw/slack/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/slack/api.js");
};
};
types: {};
};
"slack-account": {
module: typeof import("@openclaw/slack/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/slack/api.js");
};
};
types: {
ResolvedSlackAccount: import("@openclaw/slack/api.js").ResolvedSlackAccount;
};
};
"slack-runtime-surface": {
module: typeof import("@openclaw/slack/runtime-api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/slack/runtime-api.js");
};
};
types: {
SlackActionContext: import("@openclaw/slack/runtime-api.js").SlackActionContext;
};
};
"slack-surface": {
module: typeof import("@openclaw/slack/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/slack/api.js");
};
};
types: {
InspectedSlackAccount: import("@openclaw/slack/api.js").InspectedSlackAccount;
ResolvedSlackAccount: import("@openclaw/slack/api.js").ResolvedSlackAccount;
SlackProbe: import("@openclaw/slack/api.js").SlackProbe;
};
};
together: {
module: typeof import("@openclaw/together/api.js");
sourceModules: {
@ -640,44 +503,6 @@ export interface PluginSdkFacadeTypeMap {
};
types: {};
};
"telegram-account": {
module: typeof import("@openclaw/telegram/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/telegram/api.js");
};
};
types: {
ResolvedTelegramAccount: import("@openclaw/telegram/api.js").ResolvedTelegramAccount;
};
};
"telegram-allow-from": {
module: typeof import("@openclaw/telegram/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/telegram/api.js");
};
};
types: {};
};
"telegram-surface": {
module: typeof import("@openclaw/telegram/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/telegram/api.js");
};
};
types: {
InspectedTelegramAccount: import("@openclaw/telegram/api.js").InspectedTelegramAccount;
ProviderInfo: import("@openclaw/telegram/api.js").ProviderInfo;
ResolvedTelegramAccount: import("@openclaw/telegram/api.js").ResolvedTelegramAccount;
StickerMetadata: import("@openclaw/telegram/api.js").StickerMetadata;
TelegramButtonStyle: import("@openclaw/telegram/api.js").TelegramButtonStyle;
TelegramInlineButtons: import("@openclaw/telegram/api.js").TelegramInlineButtons;
TelegramProbe: import("@openclaw/telegram/api.js").TelegramProbe;
TelegramTokenResolution: import("@openclaw/telegram/api.js").TelegramTokenResolution;
};
};
"vercel-ai-gateway": {
module: typeof import("@openclaw/vercel-ai-gateway/api.js");
sourceModules: {
@ -732,32 +557,6 @@ export interface PluginSdkFacadeTypeMap {
};
types: {};
};
"whatsapp-targets": {
module: typeof import("@openclaw/whatsapp/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/whatsapp/api.js");
};
};
types: {};
};
"whatsapp-surface": {
module: typeof import("@openclaw/whatsapp/api.js");
sourceModules: {
source1: {
module: typeof import("@openclaw/whatsapp/api.js");
};
source2: {
module: typeof import("@openclaw/whatsapp/constants.js");
};
};
types: {
WebChannelStatus: import("@openclaw/whatsapp/api.js").WebChannelStatus;
WebInboundMessage: import("@openclaw/whatsapp/api.js").WebInboundMessage;
WebListenerCloseReason: import("@openclaw/whatsapp/api.js").WebListenerCloseReason;
WebMonitorTuning: import("@openclaw/whatsapp/api.js").WebMonitorTuning;
};
};
"zalo-setup": {
module: typeof import("@openclaw/zalo/api.js");
sourceModules: {

View file

@ -47,7 +47,6 @@ describe("tsdown config", () => {
"index",
"commands/status.summary.runtime",
"plugins/provider-runtime.runtime",
"plugins/runtime/runtime-line.contract",
"plugins/runtime/index",
"plugin-sdk/compat",
"plugin-sdk/index",

View file

@ -10,7 +10,7 @@ const lazyRuntimeSpecifiers = [
"./cli/prompt.js",
"./infra/binaries.js",
"./process/exec.js",
"./plugins/runtime/runtime-web-channel-boundary.js",
"./plugins/runtime/runtime-web-channel-plugin.js",
] as const;
function readLibraryModuleImports() {

View file

@ -14,7 +14,7 @@ import {
handlePortError,
PortInUseError,
} from "./infra/ports.js";
import type { monitorWebChannel as monitorWebChannelRuntime } from "./plugins/runtime/runtime-web-channel-boundary.js";
import type { monitorWebChannel as monitorWebChannelRuntime } from "./plugins/runtime/runtime-web-channel-plugin.js";
import type {
runCommandWithTimeout as runCommandWithTimeoutRuntime,
runExec as runExecRuntime,
@ -32,8 +32,8 @@ let replyRuntimePromise: Promise<typeof import("./auto-reply/reply.runtime.js")>
let promptRuntimePromise: Promise<typeof import("./cli/prompt.js")> | null = null;
let binariesRuntimePromise: Promise<typeof import("./infra/binaries.js")> | null = null;
let execRuntimePromise: Promise<typeof import("./process/exec.js")> | null = null;
let whatsappRuntimePromise: Promise<
typeof import("./plugins/runtime/runtime-web-channel-boundary.js")
let webChannelRuntimePromise: Promise<
typeof import("./plugins/runtime/runtime-web-channel-plugin.js")
> | null = null;
function loadReplyRuntime() {
@ -56,9 +56,9 @@ function loadExecRuntime() {
return execRuntimePromise;
}
function loadWhatsAppRuntime() {
whatsappRuntimePromise ??= import("./plugins/runtime/runtime-web-channel-boundary.js");
return whatsappRuntimePromise;
function loadWebChannelRuntime() {
webChannelRuntimePromise ??= import("./plugins/runtime/runtime-web-channel-plugin.js");
return webChannelRuntimePromise;
}
export const getReplyFromConfig: GetReplyFromConfig = async (...args) =>
@ -71,7 +71,7 @@ export const runExec: RunExec = async (...args) => (await loadExecRuntime()).run
export const runCommandWithTimeout: RunCommandWithTimeout = async (...args) =>
(await loadExecRuntime()).runCommandWithTimeout(...args);
export const monitorWebChannel: MonitorWebChannel = async (...args) =>
(await loadWhatsAppRuntime()).monitorWebChannel(...args);
(await loadWebChannelRuntime()).monitorWebChannel(...args);
export {
assertWebChannel,

View file

@ -45,12 +45,6 @@ describe("plugin activation boundary", () => {
}>
| undefined;
let browserAmbientImportsPromise: Promise<void> | undefined;
let discordMaintenancePromise:
| Promise<{
unbindThreadBindingsBySessionKey: typeof import("./plugin-sdk/discord-maintenance.js").unbindThreadBindingsBySessionKey;
}>
| undefined;
function importAmbientModules() {
ambientImportsPromise ??= Promise.all([
import("./agents/cli-session.js"),
@ -110,13 +104,6 @@ describe("plugin activation boundary", () => {
return browserAmbientImportsPromise;
}
function importDiscordMaintenance() {
discordMaintenancePromise ??= import("./plugin-sdk/discord-maintenance.js").then((module) => ({
unbindThreadBindingsBySessionKey: module.unbindThreadBindingsBySessionKey,
}));
return discordMaintenancePromise;
}
it("does not load bundled provider plugins on ambient command imports", async () => {
await importAmbientModules();
@ -187,17 +174,16 @@ describe("plugin activation boundary", () => {
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
});
it("keeps discord cleanup helpers cold when discord is disabled", async () => {
const discord = await importDiscordMaintenance();
it("keeps generic session-binding cleanup helpers cold when plugins are disabled", async () => {
const { getSessionBindingService } =
await import("./infra/outbound/session-binding-service.js");
expect(
discord.unbindThreadBindingsBySessionKey({
await expect(
getSessionBindingService().unbind({
targetSessionKey: "agent:main:test",
targetKind: "acp",
reason: "session-reset",
sendFarewell: true,
}),
).toEqual([]);
).resolves.toEqual([]);
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
});

View file

@ -169,11 +169,13 @@ export function createApproverRestrictedNativeApprovalAdapter(
export function createChannelApprovalCapability(params: {
authorizeActorAction?: ChannelApprovalCapability["authorizeActorAction"];
getActionAvailabilityState?: ChannelApprovalCapability["getActionAvailabilityState"];
resolveApproveCommandBehavior?: ChannelApprovalCapability["resolveApproveCommandBehavior"];
approvals?: Pick<ChannelApprovalCapability, "delivery" | "render" | "native">;
}): ChannelApprovalCapability {
return {
authorizeActorAction: params.authorizeActorAction,
getActionAvailabilityState: params.getActionAvailabilityState,
resolveApproveCommandBehavior: params.resolveApproveCommandBehavior,
delivery: params.approvals?.delivery,
render: params.approvals?.render,
native: params.approvals?.native,
@ -184,6 +186,7 @@ export function splitChannelApprovalCapability(capability: ChannelApprovalCapabi
auth: {
authorizeActorAction?: ChannelApprovalCapability["authorizeActorAction"];
getActionAvailabilityState?: ChannelApprovalCapability["getActionAvailabilityState"];
resolveApproveCommandBehavior?: ChannelApprovalCapability["resolveApproveCommandBehavior"];
};
delivery: ChannelApprovalCapability["delivery"];
render: ChannelApprovalCapability["render"];
@ -193,6 +196,7 @@ export function splitChannelApprovalCapability(capability: ChannelApprovalCapabi
auth: {
authorizeActorAction: capability.authorizeActorAction,
getActionAvailabilityState: capability.getActionAvailabilityState,
resolveApproveCommandBehavior: capability.resolveApproveCommandBehavior,
},
delivery: capability.delivery,
render: capability.render,

View file

@ -1,16 +0,0 @@
import { tryLoadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
type DiscordThreadBindingsModule = typeof import("../../extensions/discord/runtime-api.js");
export const unbindThreadBindingsBySessionKey: DiscordThreadBindingsModule["unbindThreadBindingsBySessionKey"] =
((...args) => {
// Session cleanup always attempts Discord thread unbinds, even when Discord is disabled.
// Keep that path a no-op unless the Discord runtime is actually active.
const unbindThreadBindings = tryLoadActivatedBundledPluginPublicSurfaceModuleSync<
Pick<DiscordThreadBindingsModule, "unbindThreadBindingsBySessionKey">
>({
dirName: "discord",
artifactBasename: "runtime-api.js",
})?.unbindThreadBindingsBySessionKey;
return typeof unbindThreadBindings === "function" ? unbindThreadBindings(...args) : [];
}) as DiscordThreadBindingsModule["unbindThreadBindingsBySessionKey"];

View file

@ -1,185 +0,0 @@
import {
createLazyFacadeObjectValue,
loadBundledPluginPublicSurfaceModuleSync,
} from "./facade-runtime.js";
type DiscordRuntimeModule = typeof import("../../extensions/discord/runtime-api.js");
type DiscordRuntimeSurface = Pick<
DiscordRuntimeModule,
| "auditDiscordChannelPermissions"
| "createThreadDiscord"
| "deleteMessageDiscord"
| "discordMessageActions"
| "editChannelDiscord"
| "editMessageDiscord"
| "getThreadBindingManager"
| "listDiscordDirectoryGroupsLive"
| "listDiscordDirectoryPeersLive"
| "monitorDiscordProvider"
| "pinMessageDiscord"
| "probeDiscord"
| "resolveDiscordChannelAllowlist"
| "resolveDiscordUserAllowlist"
| "resolveThreadBindingIdleTimeoutMs"
| "resolveThreadBindingInactivityExpiresAt"
| "resolveThreadBindingMaxAgeExpiresAt"
| "resolveThreadBindingMaxAgeMs"
| "sendDiscordComponentMessage"
| "sendMessageDiscord"
| "sendPollDiscord"
| "sendTypingDiscord"
| "setThreadBindingIdleTimeoutBySessionKey"
| "setThreadBindingMaxAgeBySessionKey"
| "unbindThreadBindingsBySessionKey"
| "unpinMessageDiscord"
>;
function loadDiscordRuntimeSurface(): DiscordRuntimeSurface {
return loadBundledPluginPublicSurfaceModuleSync<DiscordRuntimeSurface>({
dirName: "discord",
artifactBasename: "runtime-api.js",
});
}
export const discordMessageActions: DiscordRuntimeModule["discordMessageActions"] =
createLazyFacadeObjectValue(() => loadDiscordRuntimeSurface().discordMessageActions);
export const auditDiscordChannelPermissions: DiscordRuntimeModule["auditDiscordChannelPermissions"] =
((...args) =>
loadDiscordRuntimeSurface().auditDiscordChannelPermissions(
...args,
)) as DiscordRuntimeModule["auditDiscordChannelPermissions"];
export const createThreadDiscord: DiscordRuntimeModule["createThreadDiscord"] = ((...args) =>
loadDiscordRuntimeSurface().createThreadDiscord(
...args,
)) as DiscordRuntimeModule["createThreadDiscord"];
export const deleteMessageDiscord: DiscordRuntimeModule["deleteMessageDiscord"] = ((...args) =>
loadDiscordRuntimeSurface().deleteMessageDiscord(
...args,
)) as DiscordRuntimeModule["deleteMessageDiscord"];
export const editChannelDiscord: DiscordRuntimeModule["editChannelDiscord"] = ((...args) =>
loadDiscordRuntimeSurface().editChannelDiscord(
...args,
)) as DiscordRuntimeModule["editChannelDiscord"];
export const editMessageDiscord: DiscordRuntimeModule["editMessageDiscord"] = ((...args) =>
loadDiscordRuntimeSurface().editMessageDiscord(
...args,
)) as DiscordRuntimeModule["editMessageDiscord"];
export const getThreadBindingManager: DiscordRuntimeModule["getThreadBindingManager"] = ((
...args
) =>
loadDiscordRuntimeSurface().getThreadBindingManager(
...args,
)) as DiscordRuntimeModule["getThreadBindingManager"];
export const listDiscordDirectoryGroupsLive: DiscordRuntimeModule["listDiscordDirectoryGroupsLive"] =
((...args) =>
loadDiscordRuntimeSurface().listDiscordDirectoryGroupsLive(
...args,
)) as DiscordRuntimeModule["listDiscordDirectoryGroupsLive"];
export const listDiscordDirectoryPeersLive: DiscordRuntimeModule["listDiscordDirectoryPeersLive"] =
((...args) =>
loadDiscordRuntimeSurface().listDiscordDirectoryPeersLive(
...args,
)) as DiscordRuntimeModule["listDiscordDirectoryPeersLive"];
export const monitorDiscordProvider: DiscordRuntimeModule["monitorDiscordProvider"] = ((...args) =>
loadDiscordRuntimeSurface().monitorDiscordProvider(
...args,
)) as DiscordRuntimeModule["monitorDiscordProvider"];
export const pinMessageDiscord: DiscordRuntimeModule["pinMessageDiscord"] = ((...args) =>
loadDiscordRuntimeSurface().pinMessageDiscord(
...args,
)) as DiscordRuntimeModule["pinMessageDiscord"];
export const probeDiscord: DiscordRuntimeModule["probeDiscord"] = ((...args) =>
loadDiscordRuntimeSurface().probeDiscord(...args)) as DiscordRuntimeModule["probeDiscord"];
export const resolveDiscordChannelAllowlist: DiscordRuntimeModule["resolveDiscordChannelAllowlist"] =
((...args) =>
loadDiscordRuntimeSurface().resolveDiscordChannelAllowlist(
...args,
)) as DiscordRuntimeModule["resolveDiscordChannelAllowlist"];
export const resolveDiscordUserAllowlist: DiscordRuntimeModule["resolveDiscordUserAllowlist"] = ((
...args
) =>
loadDiscordRuntimeSurface().resolveDiscordUserAllowlist(
...args,
)) as DiscordRuntimeModule["resolveDiscordUserAllowlist"];
export const resolveThreadBindingIdleTimeoutMs: DiscordRuntimeModule["resolveThreadBindingIdleTimeoutMs"] =
((...args) =>
loadDiscordRuntimeSurface().resolveThreadBindingIdleTimeoutMs(
...args,
)) as DiscordRuntimeModule["resolveThreadBindingIdleTimeoutMs"];
export const resolveThreadBindingInactivityExpiresAt: DiscordRuntimeModule["resolveThreadBindingInactivityExpiresAt"] =
((...args) =>
loadDiscordRuntimeSurface().resolveThreadBindingInactivityExpiresAt(
...args,
)) as DiscordRuntimeModule["resolveThreadBindingInactivityExpiresAt"];
export const resolveThreadBindingMaxAgeExpiresAt: DiscordRuntimeModule["resolveThreadBindingMaxAgeExpiresAt"] =
((...args) =>
loadDiscordRuntimeSurface().resolveThreadBindingMaxAgeExpiresAt(
...args,
)) as DiscordRuntimeModule["resolveThreadBindingMaxAgeExpiresAt"];
export const resolveThreadBindingMaxAgeMs: DiscordRuntimeModule["resolveThreadBindingMaxAgeMs"] = ((
...args
) =>
loadDiscordRuntimeSurface().resolveThreadBindingMaxAgeMs(
...args,
)) as DiscordRuntimeModule["resolveThreadBindingMaxAgeMs"];
export const sendDiscordComponentMessage: DiscordRuntimeModule["sendDiscordComponentMessage"] = ((
...args
) =>
loadDiscordRuntimeSurface().sendDiscordComponentMessage(
...args,
)) as DiscordRuntimeModule["sendDiscordComponentMessage"];
export const sendMessageDiscord: DiscordRuntimeModule["sendMessageDiscord"] = ((...args) =>
loadDiscordRuntimeSurface().sendMessageDiscord(
...args,
)) as DiscordRuntimeModule["sendMessageDiscord"];
export const sendPollDiscord: DiscordRuntimeModule["sendPollDiscord"] = ((...args) =>
loadDiscordRuntimeSurface().sendPollDiscord(...args)) as DiscordRuntimeModule["sendPollDiscord"];
export const sendTypingDiscord: DiscordRuntimeModule["sendTypingDiscord"] = ((...args) =>
loadDiscordRuntimeSurface().sendTypingDiscord(
...args,
)) as DiscordRuntimeModule["sendTypingDiscord"];
export const setThreadBindingIdleTimeoutBySessionKey: DiscordRuntimeModule["setThreadBindingIdleTimeoutBySessionKey"] =
((...args) =>
loadDiscordRuntimeSurface().setThreadBindingIdleTimeoutBySessionKey(
...args,
)) as DiscordRuntimeModule["setThreadBindingIdleTimeoutBySessionKey"];
export const setThreadBindingMaxAgeBySessionKey: DiscordRuntimeModule["setThreadBindingMaxAgeBySessionKey"] =
((...args) =>
loadDiscordRuntimeSurface().setThreadBindingMaxAgeBySessionKey(
...args,
)) as DiscordRuntimeModule["setThreadBindingMaxAgeBySessionKey"];
export const unbindThreadBindingsBySessionKey: DiscordRuntimeModule["unbindThreadBindingsBySessionKey"] =
((...args) =>
loadDiscordRuntimeSurface().unbindThreadBindingsBySessionKey(
...args,
)) as DiscordRuntimeModule["unbindThreadBindingsBySessionKey"];
export const unpinMessageDiscord: DiscordRuntimeModule["unpinMessageDiscord"] = ((...args) =>
loadDiscordRuntimeSurface().unpinMessageDiscord(
...args,
)) as DiscordRuntimeModule["unpinMessageDiscord"];

View file

@ -1,42 +0,0 @@
import type { DiscordSendResult } from "../../extensions/discord/api.js";
import type { OutboundMediaAccess } from "../media/load-options.js";
import { attachChannelToResult } from "./channel-send-result.js";
type DiscordSendOptionInput = {
replyToId?: string | null;
accountId?: string | null;
silent?: boolean;
};
type DiscordSendMediaOptionInput = DiscordSendOptionInput & {
mediaUrl?: string;
mediaAccess?: OutboundMediaAccess;
mediaLocalRoots?: readonly string[];
mediaReadFile?: (filePath: string) => Promise<Buffer>;
};
/** Build the common Discord send options from SDK-level reply payload fields. */
export function buildDiscordSendOptions(input: DiscordSendOptionInput) {
return {
verbose: false,
replyTo: input.replyToId ?? undefined,
accountId: input.accountId ?? undefined,
silent: input.silent ?? undefined,
};
}
/** Extend the base Discord send options with media-specific fields. */
export function buildDiscordSendMediaOptions(input: DiscordSendMediaOptionInput) {
return {
...buildDiscordSendOptions(input),
mediaUrl: input.mediaUrl,
mediaAccess: input.mediaAccess,
mediaLocalRoots: input.mediaLocalRoots,
mediaReadFile: input.mediaReadFile,
};
}
/** Stamp raw Discord send results with the channel id expected by shared outbound flows. */
export function tagDiscordChannelResult(result: DiscordSendResult) {
return attachChannelToResult("discord", result);
}

View file

@ -1,34 +0,0 @@
import {
createLazyFacadeObjectValue,
loadBundledPluginPublicSurfaceModuleSync,
} from "./facade-runtime.js";
type SignalRuntimeModule = typeof import("../../extensions/signal/runtime-api.js");
type SignalRuntimeSurface = Pick<
SignalRuntimeModule,
"monitorSignalProvider" | "probeSignal" | "sendMessageSignal" | "signalMessageActions"
>;
function loadSignalRuntimeSurface(): SignalRuntimeSurface {
return loadBundledPluginPublicSurfaceModuleSync<SignalRuntimeSurface>({
dirName: "signal",
artifactBasename: "runtime-api.js",
});
}
export const signalMessageActions: SignalRuntimeModule["signalMessageActions"] =
createLazyFacadeObjectValue(() => loadSignalRuntimeSurface().signalMessageActions);
export const monitorSignalProvider: SignalRuntimeModule["monitorSignalProvider"] = ((...args) =>
loadSignalRuntimeSurface().monitorSignalProvider(
...args,
)) as SignalRuntimeModule["monitorSignalProvider"];
export const probeSignal: SignalRuntimeModule["probeSignal"] = ((...args) =>
loadSignalRuntimeSurface().probeSignal(...args)) as SignalRuntimeModule["probeSignal"];
export const sendMessageSignal: SignalRuntimeModule["sendMessageSignal"] = ((...args) =>
loadSignalRuntimeSurface().sendMessageSignal(
...args,
)) as SignalRuntimeModule["sendMessageSignal"];

View file

@ -1,64 +0,0 @@
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
type SlackRuntimeModule = typeof import("../../extensions/slack/runtime-api.js");
type SlackRuntimeSurface = Pick<
SlackRuntimeModule,
| "handleSlackAction"
| "listSlackDirectoryGroupsLive"
| "listSlackDirectoryPeersLive"
| "monitorSlackProvider"
| "probeSlack"
| "resolveSlackChannelAllowlist"
| "resolveSlackUserAllowlist"
| "sendMessageSlack"
>;
function loadSlackRuntimeSurface(): SlackRuntimeSurface {
return loadBundledPluginPublicSurfaceModuleSync<SlackRuntimeSurface>({
dirName: "slack",
artifactBasename: "runtime-api.js",
});
}
export const handleSlackAction: SlackRuntimeModule["handleSlackAction"] = ((...args) =>
loadSlackRuntimeSurface().handleSlackAction(...args)) as SlackRuntimeModule["handleSlackAction"];
export const listSlackDirectoryGroupsLive: SlackRuntimeModule["listSlackDirectoryGroupsLive"] = ((
...args
) =>
loadSlackRuntimeSurface().listSlackDirectoryGroupsLive(
...args,
)) as SlackRuntimeModule["listSlackDirectoryGroupsLive"];
export const listSlackDirectoryPeersLive: SlackRuntimeModule["listSlackDirectoryPeersLive"] = ((
...args
) =>
loadSlackRuntimeSurface().listSlackDirectoryPeersLive(
...args,
)) as SlackRuntimeModule["listSlackDirectoryPeersLive"];
export const monitorSlackProvider: SlackRuntimeModule["monitorSlackProvider"] = ((...args) =>
loadSlackRuntimeSurface().monitorSlackProvider(
...args,
)) as SlackRuntimeModule["monitorSlackProvider"];
export const probeSlack: SlackRuntimeModule["probeSlack"] = ((...args) =>
loadSlackRuntimeSurface().probeSlack(...args)) as SlackRuntimeModule["probeSlack"];
export const resolveSlackChannelAllowlist: SlackRuntimeModule["resolveSlackChannelAllowlist"] = ((
...args
) =>
loadSlackRuntimeSurface().resolveSlackChannelAllowlist(
...args,
)) as SlackRuntimeModule["resolveSlackChannelAllowlist"];
export const resolveSlackUserAllowlist: SlackRuntimeModule["resolveSlackUserAllowlist"] = ((
...args
) =>
loadSlackRuntimeSurface().resolveSlackUserAllowlist(
...args,
)) as SlackRuntimeModule["resolveSlackUserAllowlist"];
export const sendMessageSlack: SlackRuntimeModule["sendMessageSlack"] = ((...args) =>
loadSlackRuntimeSurface().sendMessageSlack(...args)) as SlackRuntimeModule["sendMessageSlack"];

View file

@ -375,7 +375,7 @@ describe("plugin-sdk subpath exports", () => {
/plugins\/runtime\/runtime-(?:discord|imessage|line|signal|slack|telegram|whatsapp)(?:[-.][^"']*)?\.js/u,
exclude: [
"src/plugins/runtime/runtime-plugin-boundary.ts",
"src/plugins/runtime/runtime-web-channel-boundary.ts",
"src/plugins/runtime/runtime-web-channel-plugin.ts",
],
excludeFilesMatching: [/\.test\.ts$/u, /\.test-harness\.ts$/u],
});

View file

@ -29,17 +29,17 @@ describe("loadSiblingRuntimeModuleSync", () => {
it("loads a sibling runtime module from the caller directory", () => {
const root = createTempDir();
const moduleUrl = pathToFileURL(
path.join(root, "src", "plugins", "runtime", "runtime-line.js"),
path.join(root, "src", "plugins", "runtime", "runtime.js"),
).href;
writeFile(
path.join(root, "src", "plugins", "runtime", "runtime-line.contract.js"),
path.join(root, "src", "plugins", "runtime", "runtime.contract.js"),
"module.exports = { runtimeLine: { source: 'sibling' } };",
);
const loaded = loadSiblingRuntimeModuleSync<{ runtimeLine: { source: string } }>({
moduleUrl,
relativeBase: "./runtime-line.contract",
relativeBase: "./runtime.contract",
});
expect(loaded.runtimeLine.source).toBe("sibling");
@ -50,13 +50,13 @@ describe("loadSiblingRuntimeModuleSync", () => {
const moduleUrl = pathToFileURL(path.join(root, "dist", "runtime-9DLN_Ai5.js")).href;
writeFile(
path.join(root, "dist", "plugins", "runtime", "runtime-line.contract.js"),
path.join(root, "dist", "plugins", "runtime", "runtime.contract.js"),
"module.exports = { runtimeLine: { source: 'dist-runtime' } };",
);
const loaded = loadSiblingRuntimeModuleSync<{ runtimeLine: { source: string } }>({
moduleUrl,
relativeBase: "./runtime-line.contract",
relativeBase: "./runtime.contract",
});
expect(loaded.runtimeLine.source).toBe("dist-runtime");
@ -69,8 +69,8 @@ describe("loadSiblingRuntimeModuleSync", () => {
expect(() =>
loadSiblingRuntimeModuleSync({
moduleUrl,
relativeBase: "./runtime-line.contract",
relativeBase: "./runtime.contract",
}),
).toThrow("Unable to resolve runtime module ./runtime-line.contract");
).toThrow("Unable to resolve runtime module ./runtime.contract");
});
});

View file

@ -61,17 +61,7 @@ import {
readChannelAllowFromStore,
upsertChannelPairingRequest,
} from "../../pairing/pairing-store.js";
import {
setThreadBindingIdleTimeoutBySessionKey,
setThreadBindingMaxAgeBySessionKey,
} from "../../plugin-sdk/discord-runtime-surface.js";
import { buildAgentSessionKey, resolveAgentRoute } from "../../routing/resolve-route.js";
import { defineCachedValue } from "./runtime-cache.js";
import { createRuntimeDiscord } from "./runtime-discord.js";
import { createRuntimeLine } from "./runtime-line.js";
import { createRuntimeMatrix } from "./runtime-matrix.js";
import { createRuntimeSignal } from "./runtime-signal.js";
import { createRuntimeSlack } from "./runtime-slack.js";
import type { PluginRuntime } from "./types.js";
export function createRuntimeChannel(): PluginRuntime["channel"] {
@ -161,63 +151,22 @@ export function createRuntimeChannel(): PluginRuntime["channel"] {
loadAdapter: loadChannelOutboundAdapter,
},
threadBindings: {
setIdleTimeoutBySessionKey: ({ channelId, targetSessionKey, accountId, idleTimeoutMs }) => {
switch (channelId) {
case "discord":
return setThreadBindingIdleTimeoutBySessionKey({
targetSessionKey,
accountId,
idleTimeoutMs,
});
case "matrix":
return setChannelConversationBindingIdleTimeoutBySessionKey({
channelId,
targetSessionKey,
accountId: accountId ?? "",
idleTimeoutMs,
});
case "telegram":
return setChannelConversationBindingIdleTimeoutBySessionKey({
channelId,
targetSessionKey,
accountId,
idleTimeoutMs,
});
}
},
setMaxAgeBySessionKey: ({ channelId, targetSessionKey, accountId, maxAgeMs }) => {
switch (channelId) {
case "discord":
return setThreadBindingMaxAgeBySessionKey({
targetSessionKey,
accountId,
maxAgeMs,
});
case "matrix":
return setChannelConversationBindingMaxAgeBySessionKey({
channelId,
targetSessionKey,
accountId: accountId ?? "",
maxAgeMs,
});
case "telegram":
return setChannelConversationBindingMaxAgeBySessionKey({
channelId,
targetSessionKey,
accountId,
maxAgeMs,
});
}
},
setIdleTimeoutBySessionKey: ({ channelId, targetSessionKey, accountId, idleTimeoutMs }) =>
setChannelConversationBindingIdleTimeoutBySessionKey({
channelId,
targetSessionKey,
accountId,
idleTimeoutMs,
}),
setMaxAgeBySessionKey: ({ channelId, targetSessionKey, accountId, maxAgeMs }) =>
setChannelConversationBindingMaxAgeBySessionKey({
channelId,
targetSessionKey,
accountId,
maxAgeMs,
}),
},
} satisfies Omit<PluginRuntime["channel"], "discord" | "slack" | "matrix" | "signal" | "line"> &
Partial<Pick<PluginRuntime["channel"], "discord" | "slack" | "matrix" | "signal" | "line">>;
defineCachedValue(channelRuntime, "discord", createRuntimeDiscord);
defineCachedValue(channelRuntime, "slack", createRuntimeSlack);
defineCachedValue(channelRuntime, "matrix", createRuntimeMatrix);
defineCachedValue(channelRuntime, "signal", createRuntimeSignal);
defineCachedValue(channelRuntime, "line", createRuntimeLine);
} satisfies PluginRuntime["channel"];
return channelRuntime as PluginRuntime["channel"];
}

View file

@ -1,64 +0,0 @@
import {
auditDiscordChannelPermissions as auditDiscordChannelPermissionsImpl,
listDiscordDirectoryGroupsLive as listDiscordDirectoryGroupsLiveImpl,
listDiscordDirectoryPeersLive as listDiscordDirectoryPeersLiveImpl,
monitorDiscordProvider as monitorDiscordProviderImpl,
probeDiscord as probeDiscordImpl,
resolveDiscordChannelAllowlist as resolveDiscordChannelAllowlistImpl,
resolveDiscordUserAllowlist as resolveDiscordUserAllowlistImpl,
createThreadDiscord as createThreadDiscordImpl,
deleteMessageDiscord as deleteMessageDiscordImpl,
editChannelDiscord as editChannelDiscordImpl,
editMessageDiscord as editMessageDiscordImpl,
pinMessageDiscord as pinMessageDiscordImpl,
sendDiscordComponentMessage as sendDiscordComponentMessageImpl,
sendMessageDiscord as sendMessageDiscordImpl,
sendPollDiscord as sendPollDiscordImpl,
sendTypingDiscord as sendTypingDiscordImpl,
unpinMessageDiscord as unpinMessageDiscordImpl,
} from "../../plugin-sdk/discord-runtime-surface.js";
import type { PluginRuntimeChannel } from "./types-channel.js";
type RuntimeDiscordOps = Pick<
PluginRuntimeChannel["discord"],
| "auditChannelPermissions"
| "listDirectoryGroupsLive"
| "listDirectoryPeersLive"
| "probeDiscord"
| "resolveChannelAllowlist"
| "resolveUserAllowlist"
| "sendComponentMessage"
| "sendMessageDiscord"
| "sendPollDiscord"
| "monitorDiscordProvider"
> & {
typing: Pick<PluginRuntimeChannel["discord"]["typing"], "pulse">;
conversationActions: Pick<
PluginRuntimeChannel["discord"]["conversationActions"],
"editMessage" | "deleteMessage" | "pinMessage" | "unpinMessage" | "createThread" | "editChannel"
>;
};
export const runtimeDiscordOps = {
auditChannelPermissions: auditDiscordChannelPermissionsImpl,
listDirectoryGroupsLive: listDiscordDirectoryGroupsLiveImpl,
listDirectoryPeersLive: listDiscordDirectoryPeersLiveImpl,
probeDiscord: probeDiscordImpl,
resolveChannelAllowlist: resolveDiscordChannelAllowlistImpl,
resolveUserAllowlist: resolveDiscordUserAllowlistImpl,
sendComponentMessage: sendDiscordComponentMessageImpl,
sendMessageDiscord: sendMessageDiscordImpl,
sendPollDiscord: sendPollDiscordImpl,
monitorDiscordProvider: monitorDiscordProviderImpl,
typing: {
pulse: sendTypingDiscordImpl,
},
conversationActions: {
editMessage: editMessageDiscordImpl,
deleteMessage: deleteMessageDiscordImpl,
pinMessage: pinMessageDiscordImpl,
unpinMessage: unpinMessageDiscordImpl,
createThread: createThreadDiscordImpl,
editChannel: editChannelDiscordImpl,
},
} satisfies RuntimeDiscordOps;

View file

@ -1,122 +0,0 @@
import {
discordMessageActions,
getThreadBindingManager,
resolveThreadBindingIdleTimeoutMs,
resolveThreadBindingInactivityExpiresAt,
resolveThreadBindingMaxAgeExpiresAt,
resolveThreadBindingMaxAgeMs,
setThreadBindingIdleTimeoutBySessionKey,
setThreadBindingMaxAgeBySessionKey,
unbindThreadBindingsBySessionKey,
} from "../../plugin-sdk/discord-runtime-surface.js";
import {
createLazyRuntimeMethodBinder,
createLazyRuntimeSurface,
} from "../../shared/lazy-runtime.js";
import { createDiscordTypingLease } from "./runtime-discord-typing.js";
import type { PluginRuntimeChannel } from "./types-channel.js";
const loadRuntimeDiscordOps = createLazyRuntimeSurface(
() => import("./runtime-discord-ops.runtime.js"),
({ runtimeDiscordOps }) => runtimeDiscordOps,
);
const bindDiscordRuntimeMethod = createLazyRuntimeMethodBinder(loadRuntimeDiscordOps);
const auditChannelPermissionsLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.auditChannelPermissions,
);
const listDirectoryGroupsLiveLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.listDirectoryGroupsLive,
);
const listDirectoryPeersLiveLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.listDirectoryPeersLive,
);
const probeDiscordLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.probeDiscord,
);
const resolveChannelAllowlistLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.resolveChannelAllowlist,
);
const resolveUserAllowlistLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.resolveUserAllowlist,
);
const sendComponentMessageLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.sendComponentMessage,
);
const sendMessageDiscordLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.sendMessageDiscord,
);
const sendPollDiscordLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.sendPollDiscord,
);
const monitorDiscordProviderLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.monitorDiscordProvider,
);
const sendTypingDiscordLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.typing.pulse,
);
const editMessageDiscordLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.conversationActions.editMessage,
);
const deleteMessageDiscordLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.conversationActions.deleteMessage,
);
const pinMessageDiscordLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.conversationActions.pinMessage,
);
const unpinMessageDiscordLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.conversationActions.unpinMessage,
);
const createThreadDiscordLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.conversationActions.createThread,
);
const editChannelDiscordLazy = bindDiscordRuntimeMethod(
(runtimeDiscordOps) => runtimeDiscordOps.conversationActions.editChannel,
);
export function createRuntimeDiscord(): PluginRuntimeChannel["discord"] {
return {
messageActions: discordMessageActions,
auditChannelPermissions: auditChannelPermissionsLazy,
listDirectoryGroupsLive: listDirectoryGroupsLiveLazy,
listDirectoryPeersLive: listDirectoryPeersLiveLazy,
probeDiscord: probeDiscordLazy,
resolveChannelAllowlist: resolveChannelAllowlistLazy,
resolveUserAllowlist: resolveUserAllowlistLazy,
sendComponentMessage: sendComponentMessageLazy,
sendMessageDiscord: sendMessageDiscordLazy,
sendPollDiscord: sendPollDiscordLazy,
monitorDiscordProvider: monitorDiscordProviderLazy,
threadBindings: {
getManager: getThreadBindingManager,
resolveIdleTimeoutMs: resolveThreadBindingIdleTimeoutMs,
resolveInactivityExpiresAt: resolveThreadBindingInactivityExpiresAt,
resolveMaxAgeMs: resolveThreadBindingMaxAgeMs,
resolveMaxAgeExpiresAt: resolveThreadBindingMaxAgeExpiresAt,
setIdleTimeoutBySessionKey: setThreadBindingIdleTimeoutBySessionKey,
setMaxAgeBySessionKey: setThreadBindingMaxAgeBySessionKey,
unbindBySessionKey: unbindThreadBindingsBySessionKey,
},
typing: {
pulse: sendTypingDiscordLazy,
start: async ({ channelId, accountId, cfg, intervalMs }) =>
await createDiscordTypingLease({
channelId,
accountId,
cfg,
intervalMs,
pulse: async ({ channelId, accountId, cfg }) =>
void (await sendTypingDiscordLazy(channelId, { accountId, cfg })),
}),
},
conversationActions: {
editMessage: editMessageDiscordLazy,
deleteMessage: deleteMessageDiscordLazy,
pinMessage: pinMessageDiscordLazy,
unpinMessage: unpinMessageDiscordLazy,
createThread: createThreadDiscordLazy,
editChannel: editChannelDiscordLazy,
},
};
}

View file

@ -1,38 +0,0 @@
import {
buildTemplateMessageFromPayload,
createQuickReplyItems,
monitorLineProvider,
probeLineBot,
pushFlexMessage,
pushLocationMessage,
pushMessageLine,
pushMessagesLine,
pushTemplateMessage,
pushTextMessageWithQuickReplies,
sendMessageLine,
} from "../../plugin-sdk/line-runtime.js";
import {
listLineAccountIds,
normalizeAccountId,
resolveDefaultLineAccountId,
resolveLineAccount,
} from "../../plugin-sdk/line.js";
import type { PluginRuntimeChannel } from "./types-channel.js";
export const runtimeLine = {
listLineAccountIds,
resolveDefaultLineAccountId,
resolveLineAccount,
normalizeAccountId,
probeLineBot,
sendMessageLine,
pushMessageLine,
pushMessagesLine,
pushFlexMessage,
pushTemplateMessage,
pushLocationMessage,
pushTextMessageWithQuickReplies,
createQuickReplyItems,
buildTemplateMessageFromPayload,
monitorLineProvider,
} satisfies PluginRuntimeChannel["line"];

View file

@ -1,11 +0,0 @@
export {
monitorLineProvider,
probeLineBot,
pushFlexMessage,
pushLocationMessage,
pushMessageLine,
pushMessagesLine,
pushTemplateMessage,
pushTextMessageWithQuickReplies,
sendMessageLine,
} from "../../plugin-sdk/line-runtime.js";

View file

@ -1,46 +0,0 @@
import { loadSiblingRuntimeModuleSync } from "./local-runtime-module.js";
import type { PluginRuntimeChannel } from "./types-channel.js";
type RuntimeLineModule = {
runtimeLine: PluginRuntimeChannel["line"];
};
let cachedRuntimeLineModule: RuntimeLineModule | null = null;
function loadRuntimeLineModule(): RuntimeLineModule {
cachedRuntimeLineModule ??= loadSiblingRuntimeModuleSync<RuntimeLineModule>({
moduleUrl: import.meta.url,
relativeBase: "./runtime-line.contract",
});
return cachedRuntimeLineModule;
}
export function createRuntimeLine(): PluginRuntimeChannel["line"] {
return {
listLineAccountIds: (...args) =>
loadRuntimeLineModule().runtimeLine.listLineAccountIds(...args),
resolveDefaultLineAccountId: (...args) =>
loadRuntimeLineModule().runtimeLine.resolveDefaultLineAccountId(...args),
resolveLineAccount: (...args) =>
loadRuntimeLineModule().runtimeLine.resolveLineAccount(...args),
normalizeAccountId: (...args) =>
loadRuntimeLineModule().runtimeLine.normalizeAccountId(...args),
probeLineBot: (...args) => loadRuntimeLineModule().runtimeLine.probeLineBot(...args),
sendMessageLine: (...args) => loadRuntimeLineModule().runtimeLine.sendMessageLine(...args),
pushMessageLine: (...args) => loadRuntimeLineModule().runtimeLine.pushMessageLine(...args),
pushMessagesLine: (...args) => loadRuntimeLineModule().runtimeLine.pushMessagesLine(...args),
pushFlexMessage: (...args) => loadRuntimeLineModule().runtimeLine.pushFlexMessage(...args),
pushTemplateMessage: (...args) =>
loadRuntimeLineModule().runtimeLine.pushTemplateMessage(...args),
pushLocationMessage: (...args) =>
loadRuntimeLineModule().runtimeLine.pushLocationMessage(...args),
pushTextMessageWithQuickReplies: (...args) =>
loadRuntimeLineModule().runtimeLine.pushTextMessageWithQuickReplies(...args),
createQuickReplyItems: (...args) =>
loadRuntimeLineModule().runtimeLine.createQuickReplyItems(...args),
buildTemplateMessageFromPayload: (...args) =>
loadRuntimeLineModule().runtimeLine.buildTemplateMessageFromPayload(...args),
monitorLineProvider: (...args) =>
loadRuntimeLineModule().runtimeLine.monitorLineProvider(...args),
};
}

View file

@ -1,68 +0,0 @@
import { createJiti } from "jiti";
import type { MatrixRuntimeBoundaryModule } from "./runtime-matrix-surface.js";
import {
loadPluginBoundaryModuleWithJiti,
resolvePluginRuntimeModulePath,
resolvePluginRuntimeRecord,
} from "./runtime-plugin-boundary.js";
const MATRIX_PLUGIN_ID = "matrix";
type MatrixPluginRecord = {
rootDir?: string;
source: string;
};
let cachedModulePath: string | null = null;
let cachedModule: MatrixRuntimeBoundaryModule | null = null;
const jitiLoaders = new Map<boolean, ReturnType<typeof createJiti>>();
function resolveMatrixPluginRecord(): MatrixPluginRecord | null {
return resolvePluginRuntimeRecord(MATRIX_PLUGIN_ID) as MatrixPluginRecord | null;
}
function resolveMatrixRuntimeModulePath(record: MatrixPluginRecord): string | null {
return resolvePluginRuntimeModulePath(record, "runtime-api");
}
function loadMatrixModule(): MatrixRuntimeBoundaryModule | null {
const record = resolveMatrixPluginRecord();
if (!record) {
return null;
}
const modulePath = resolveMatrixRuntimeModulePath(record);
if (!modulePath) {
return null;
}
if (cachedModule && cachedModulePath === modulePath) {
return cachedModule;
}
const loaded = loadPluginBoundaryModuleWithJiti<MatrixRuntimeBoundaryModule>(
modulePath,
jitiLoaders,
);
cachedModulePath = modulePath;
cachedModule = loaded;
return loaded;
}
export function setMatrixThreadBindingIdleTimeoutBySessionKey(
...args: Parameters<MatrixRuntimeBoundaryModule["setMatrixThreadBindingIdleTimeoutBySessionKey"]>
): ReturnType<MatrixRuntimeBoundaryModule["setMatrixThreadBindingIdleTimeoutBySessionKey"]> {
const fn = loadMatrixModule()?.setMatrixThreadBindingIdleTimeoutBySessionKey;
if (typeof fn !== "function") {
return [];
}
return fn(...args);
}
export function setMatrixThreadBindingMaxAgeBySessionKey(
...args: Parameters<MatrixRuntimeBoundaryModule["setMatrixThreadBindingMaxAgeBySessionKey"]>
): ReturnType<MatrixRuntimeBoundaryModule["setMatrixThreadBindingMaxAgeBySessionKey"]> {
const fn = loadMatrixModule()?.setMatrixThreadBindingMaxAgeBySessionKey;
if (typeof fn !== "function") {
return [];
}
return fn(...args);
}

View file

@ -1,178 +0,0 @@
// Narrow plugin-sdk surface for the bundled Matrix plugin.
// Keep this list additive and scoped to the runtime contract only.
import { createOptionalChannelSetupSurface } from "../../plugin-sdk/channel-setup.js";
export {
createActionGate,
jsonResult,
readNumberParam,
readReactionParams,
readStringArrayParam,
readStringParam,
} from "../../agents/tools/common.js";
export type { ReplyPayload } from "../../auto-reply/types.js";
export { resolveAckReaction } from "../../agents/identity.js";
export {
compileAllowlist,
resolveCompiledAllowlistMatch,
resolveAllowlistCandidates,
resolveAllowlistMatchByCandidates,
} from "../../channels/allowlist-match.js";
export {
addAllowlistUserEntriesFromConfigEntry,
buildAllowlistResolutionSummary,
canonicalizeAllowlistWithResolvedIds,
mergeAllowlist,
patchAllowlistUsersInConfigEntries,
summarizeMapping,
} from "../../channels/allowlists/resolve-utils.js";
export { ensureConfiguredAcpBindingReady } from "../../acp/persistent-bindings.lifecycle.js";
export { resolveConfiguredAcpBindingRecord } from "../../acp/persistent-bindings.resolve.js";
export { resolveControlCommandGate } from "../../channels/command-gating.js";
export type { NormalizedLocation } from "../../channels/location.js";
export { formatLocationText, toLocationContext } from "../../channels/location.js";
export { logInboundDrop, logTypingFailure } from "../../channels/logging.js";
export type { AllowlistMatch } from "../../channels/plugins/allowlist-match.js";
export { formatAllowlistMatchMeta } from "../../channels/plugins/allowlist-match.js";
export {
buildChannelKeyCandidates,
resolveChannelEntryMatch,
} from "../../channels/plugins/channel-config.js";
export { createAccountListHelpers } from "../../channels/plugins/account-helpers.js";
export {
deleteAccountFromConfigSection,
setAccountEnabledInConfigSection,
} from "../../channels/plugins/config-helpers.js";
export { buildChannelConfigSchema } from "../../channels/plugins/config-schema.js";
export { formatPairingApproveHint } from "../../channels/plugins/helpers.js";
export {
buildSingleChannelSecretPromptState,
addWildcardAllowFrom,
mergeAllowFromEntries,
promptAccountId,
promptSingleChannelSecretInput,
setTopLevelChannelGroupPolicy,
} from "../../channels/plugins/setup-wizard-helpers.js";
export { promptChannelAccessConfig } from "../../channels/plugins/setup-group-access.js";
export { PAIRING_APPROVED_MESSAGE } from "../../channels/plugins/pairing-message.js";
export {
applyAccountNameToChannelSection,
moveSingleAccountChannelSectionToDefaultAccount,
} from "../../channels/plugins/setup-helpers.js";
export type {
BaseProbeResult,
ChannelDirectoryEntry,
ChannelGroupContext,
ChannelMessageActionAdapter,
ChannelMessageActionContext,
ChannelMessageActionName,
ChannelMessageToolDiscovery,
ChannelMessageToolSchemaContribution,
ChannelOutboundAdapter,
ChannelResolveKind,
ChannelResolveResult,
ChannelSetupInput,
ChannelToolSend,
} from "../../channels/plugins/types.js";
export type { ChannelPlugin } from "../../channels/plugins/types.plugin.js";
export { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
export { resolveThreadBindingFarewellText } from "../../channels/thread-bindings-messages.js";
export {
resolveThreadBindingIdleTimeoutMsForChannel,
resolveThreadBindingMaxAgeMsForChannel,
} from "../../channels/thread-bindings-policy.js";
export {
setMatrixThreadBindingIdleTimeoutBySessionKey,
setMatrixThreadBindingMaxAgeBySessionKey,
} from "../../plugin-sdk/matrix-thread-bindings.js";
export { createTypingCallbacks } from "../../channels/typing.js";
export { createChannelReplyPipeline } from "../../plugin-sdk/channel-reply-pipeline.js";
export type { OpenClawConfig } from "../../config/config.js";
export {
GROUP_POLICY_BLOCKED_LABEL,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
} from "../../config/runtime-group-policy.js";
export type {
DmPolicy,
GroupPolicy,
GroupToolPolicyConfig,
MarkdownTableMode,
} from "../../config/types.js";
export type { SecretInput } from "../../plugin-sdk/secret-input.js";
export {
buildSecretInputSchema,
hasConfiguredSecretInput,
normalizeResolvedSecretInputString,
normalizeSecretInputString,
} from "../../plugin-sdk/secret-input.js";
export { ToolPolicySchema } from "../../config/zod-schema.agent-runtime.js";
export { MarkdownConfigSchema } from "../../config/zod-schema.core.js";
export { formatZonedTimestamp } from "../../infra/format-time/format-datetime.js";
export { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js";
export { maybeCreateMatrixMigrationSnapshot } from "../../infra/matrix-migration-snapshot.js";
export {
getSessionBindingService,
registerSessionBindingAdapter,
unregisterSessionBindingAdapter,
} from "../../infra/outbound/session-binding-service.js";
export { resolveOutboundSendDep } from "../../infra/outbound/send-deps.js";
export type {
BindingTargetKind,
SessionBindingRecord,
} from "../../infra/outbound/session-binding-service.js";
export { isPrivateOrLoopbackHost } from "../../gateway/net.js";
export { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
export { emptyPluginConfigSchema } from "../config-schema.js";
export type { PluginRuntime, RuntimeLogger } from "./types.js";
export type { OpenClawPluginApi } from "../types.js";
export type { PollInput } from "../../polls.js";
export { normalizePollInput } from "../../polls.js";
export {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
normalizeOptionalAccountId,
resolveAgentIdFromSessionKey,
} from "../../routing/session-key.js";
export type { RuntimeEnv } from "../../runtime.js";
export { normalizeStringEntries } from "../../shared/string-normalization.js";
export { formatDocsLink } from "../../terminal/links.js";
export { redactSensitiveText } from "../../logging/redact.js";
export type { WizardPrompter } from "../../wizard/prompts.js";
export {
evaluateGroupRouteAccessForPolicy,
resolveSenderScopedGroupPolicy,
} from "../../plugin-sdk/group-access.js";
export { createChannelPairingController } from "../../plugin-sdk/channel-pairing.js";
export { readJsonFileWithFallback, writeJsonFileAtomically } from "../../plugin-sdk/json-store.js";
export { formatResolvedUnresolvedNote } from "../../plugin-sdk/resolution-notes.js";
export { runPluginCommandWithTimeout } from "../../plugin-sdk/run-command.js";
export { createLoggerBackedRuntime, resolveRuntimeEnv } from "../../plugin-sdk/runtime.js";
export { dispatchReplyFromConfigWithSettledDispatcher } from "../../plugin-sdk/inbound-reply-dispatch.js";
export {
buildProbeChannelStatusSummary,
collectStatusIssuesFromLastError,
} from "../../plugin-sdk/status-helpers.js";
export {
resolveMatrixAccountStorageRoot,
resolveMatrixCredentialsDir,
resolveMatrixCredentialsPath,
resolveMatrixLegacyFlatStoragePaths,
} from "../../plugin-sdk/matrix-helper.js";
export { getMatrixScopedEnvVarNames } from "../../plugin-sdk/matrix-helper.js";
export {
requiresExplicitMatrixDefaultAccount,
resolveMatrixDefaultOrOnlyAccountId,
} from "../../plugin-sdk/matrix-helper.js";
const matrixSetup = createOptionalChannelSetupSurface({
channel: "matrix",
label: "Matrix",
npmSpec: "@openclaw/matrix",
docsPath: "/channels/matrix",
});
export const matrixSetupWizard = matrixSetup.setupWizard;
export const matrixSetupAdapter = matrixSetup.setupAdapter;

View file

@ -1,22 +0,0 @@
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
export type MatrixThreadBindingIdleTimeoutParams = {
accountId: string;
targetSessionKey: string;
idleTimeoutMs: number;
};
export type MatrixThreadBindingMaxAgeParams = {
accountId: string;
targetSessionKey: string;
maxAgeMs: number;
};
export type MatrixRuntimeBoundaryModule = {
setMatrixThreadBindingIdleTimeoutBySessionKey: (
params: MatrixThreadBindingIdleTimeoutParams,
) => SessionBindingRecord[];
setMatrixThreadBindingMaxAgeBySessionKey: (
params: MatrixThreadBindingMaxAgeParams,
) => SessionBindingRecord[];
};

View file

@ -1,14 +0,0 @@
import {
setMatrixThreadBindingIdleTimeoutBySessionKey,
setMatrixThreadBindingMaxAgeBySessionKey,
} from "./runtime-matrix-boundary.js";
import type { PluginRuntimeChannel } from "./types-channel.js";
export function createRuntimeMatrix(): PluginRuntimeChannel["matrix"] {
return {
threadBindings: {
setIdleTimeoutBySessionKey: setMatrixThreadBindingIdleTimeoutBySessionKey,
setMaxAgeBySessionKey: setMatrixThreadBindingMaxAgeBySessionKey,
},
};
}

View file

@ -16,6 +16,13 @@ type PluginRuntimeRecord = {
source: string;
};
type CachedPluginBoundaryLoaderParams = {
pluginId: string;
entryBaseName: string;
required?: boolean;
missingLabel?: string;
};
export function readPluginBoundaryConfigSafely() {
try {
return loadConfig();
@ -104,3 +111,47 @@ export function loadPluginBoundaryModuleWithJiti<TModule>(
): TModule {
return getPluginBoundaryJiti(modulePath, loaders)(modulePath) as TModule;
}
export function createCachedPluginBoundaryModuleLoader<TModule>(
params: CachedPluginBoundaryLoaderParams,
): () => TModule | null {
let cachedModulePath: string | null = null;
let cachedModule: TModule | null = null;
const loaders = new Map<boolean, ReturnType<typeof createJiti>>();
return () => {
const missingLabel = params.missingLabel ?? `${params.pluginId} plugin runtime`;
const record = resolvePluginRuntimeRecord(
params.pluginId,
params.required
? () => {
throw new Error(`${missingLabel} is unavailable: missing plugin '${params.pluginId}'`);
}
: undefined,
);
if (!record) {
return null;
}
const modulePath = resolvePluginRuntimeModulePath(
record,
params.entryBaseName,
params.required
? () => {
throw new Error(
`${missingLabel} is unavailable: missing ${params.entryBaseName} for plugin '${params.pluginId}'`,
);
}
: undefined,
);
if (!modulePath) {
return null;
}
if (cachedModule && cachedModulePath === modulePath) {
return cachedModule;
}
const loaded = loadPluginBoundaryModuleWithJiti<TModule>(modulePath, loaders);
cachedModulePath = modulePath;
cachedModule = loaded;
return loaded;
};
}

View file

@ -1,16 +0,0 @@
import {
monitorSignalProvider,
probeSignal,
signalMessageActions,
sendMessageSignal,
} from "../../plugin-sdk/signal-runtime-surface.js";
import type { PluginRuntimeChannel } from "./types-channel.js";
export function createRuntimeSignal(): PluginRuntimeChannel["signal"] {
return {
probeSignal,
sendMessageSignal,
monitorSignalProvider,
messageActions: signalMessageActions,
};
}

View file

@ -1,34 +0,0 @@
import {
listSlackDirectoryGroupsLive as listSlackDirectoryGroupsLiveImpl,
listSlackDirectoryPeersLive as listSlackDirectoryPeersLiveImpl,
monitorSlackProvider as monitorSlackProviderImpl,
probeSlack as probeSlackImpl,
resolveSlackChannelAllowlist as resolveSlackChannelAllowlistImpl,
resolveSlackUserAllowlist as resolveSlackUserAllowlistImpl,
sendMessageSlack as sendMessageSlackImpl,
handleSlackAction as handleSlackActionImpl,
} from "../../plugin-sdk/slack-runtime-surface.js";
import type { PluginRuntimeChannel } from "./types-channel.js";
type RuntimeSlackOps = Pick<
PluginRuntimeChannel["slack"],
| "listDirectoryGroupsLive"
| "listDirectoryPeersLive"
| "probeSlack"
| "resolveChannelAllowlist"
| "resolveUserAllowlist"
| "sendMessageSlack"
| "monitorSlackProvider"
| "handleSlackAction"
>;
export const runtimeSlackOps = {
listDirectoryGroupsLive: listSlackDirectoryGroupsLiveImpl,
listDirectoryPeersLive: listSlackDirectoryPeersLiveImpl,
probeSlack: probeSlackImpl,
resolveChannelAllowlist: resolveSlackChannelAllowlistImpl,
resolveUserAllowlist: resolveSlackUserAllowlistImpl,
sendMessageSlack: sendMessageSlackImpl,
monitorSlackProvider: monitorSlackProviderImpl,
handleSlackAction: handleSlackActionImpl,
} satisfies RuntimeSlackOps;

View file

@ -1,48 +0,0 @@
import {
createLazyRuntimeMethodBinder,
createLazyRuntimeSurface,
} from "../../shared/lazy-runtime.js";
import type { PluginRuntimeChannel } from "./types-channel.js";
const loadRuntimeSlackOps = createLazyRuntimeSurface(
() => import("./runtime-slack-ops.runtime.js"),
({ runtimeSlackOps }) => runtimeSlackOps,
);
const bindSlackRuntimeMethod = createLazyRuntimeMethodBinder(loadRuntimeSlackOps);
const listDirectoryGroupsLiveLazy = bindSlackRuntimeMethod(
(runtimeSlackOps) => runtimeSlackOps.listDirectoryGroupsLive,
);
const listDirectoryPeersLiveLazy = bindSlackRuntimeMethod(
(runtimeSlackOps) => runtimeSlackOps.listDirectoryPeersLive,
);
const probeSlackLazy = bindSlackRuntimeMethod((runtimeSlackOps) => runtimeSlackOps.probeSlack);
const resolveChannelAllowlistLazy = bindSlackRuntimeMethod(
(runtimeSlackOps) => runtimeSlackOps.resolveChannelAllowlist,
);
const resolveUserAllowlistLazy = bindSlackRuntimeMethod(
(runtimeSlackOps) => runtimeSlackOps.resolveUserAllowlist,
);
const sendMessageSlackLazy = bindSlackRuntimeMethod(
(runtimeSlackOps) => runtimeSlackOps.sendMessageSlack,
);
const monitorSlackProviderLazy = bindSlackRuntimeMethod(
(runtimeSlackOps) => runtimeSlackOps.monitorSlackProvider,
);
const handleSlackActionLazy = bindSlackRuntimeMethod(
(runtimeSlackOps) => runtimeSlackOps.handleSlackAction,
);
export function createRuntimeSlack(): PluginRuntimeChannel["slack"] {
return {
listDirectoryGroupsLive: listDirectoryGroupsLiveLazy,
listDirectoryPeersLive: listDirectoryPeersLiveLazy,
probeSlack: probeSlackLazy,
resolveChannelAllowlist: resolveChannelAllowlistLazy,
resolveUserAllowlist: resolveUserAllowlistLazy,
sendMessageSlack: sendMessageSlackLazy,
monitorSlackProvider: monitorSlackProviderLazy,
handleSlackAction: handleSlackActionLazy,
};
}

View file

@ -1,286 +0,0 @@
import { createJiti } from "jiti";
type WhatsAppHeavyRuntimeModule = typeof import("@openclaw/whatsapp/runtime-api.js");
type WhatsAppLightRuntimeModule = typeof import("@openclaw/whatsapp/light-runtime-api.js");
import {
getDefaultLocalRoots as getDefaultLocalRootsImpl,
loadWebMedia as loadWebMediaImpl,
loadWebMediaRaw as loadWebMediaRawImpl,
optimizeImageToJpeg as optimizeImageToJpegImpl,
} from "../../media/web-media.js";
import {
loadPluginBoundaryModuleWithJiti,
resolvePluginRuntimeModulePath,
resolvePluginRuntimeRecord,
} from "./runtime-plugin-boundary.js";
const WHATSAPP_PLUGIN_ID = "whatsapp";
type WhatsAppPluginRecord = {
origin: string;
rootDir?: string;
source: string;
};
let cachedHeavyModulePath: string | null = null;
let cachedHeavyModule: WhatsAppHeavyRuntimeModule | null = null;
let cachedLightModulePath: string | null = null;
let cachedLightModule: WhatsAppLightRuntimeModule | null = null;
const jitiLoaders = new Map<boolean, ReturnType<typeof createJiti>>();
function resolveWhatsAppPluginRecord(): WhatsAppPluginRecord {
return resolvePluginRuntimeRecord(WHATSAPP_PLUGIN_ID, () => {
throw new Error(
`WhatsApp plugin runtime is unavailable: missing plugin '${WHATSAPP_PLUGIN_ID}'`,
);
}) as WhatsAppPluginRecord;
}
function resolveWhatsAppRuntimeModulePath(
record: WhatsAppPluginRecord,
entryBaseName: "light-runtime-api" | "runtime-api",
): string {
const modulePath = resolvePluginRuntimeModulePath(record, entryBaseName, () => {
throw new Error(
`WhatsApp plugin runtime is unavailable: missing ${entryBaseName} for plugin '${WHATSAPP_PLUGIN_ID}'`,
);
});
if (!modulePath) {
throw new Error(
`WhatsApp plugin runtime is unavailable: missing ${entryBaseName} for plugin '${WHATSAPP_PLUGIN_ID}'`,
);
}
return modulePath;
}
function loadCurrentHeavyModuleSync(): WhatsAppHeavyRuntimeModule {
const modulePath = resolveWhatsAppRuntimeModulePath(resolveWhatsAppPluginRecord(), "runtime-api");
return loadPluginBoundaryModuleWithJiti<WhatsAppHeavyRuntimeModule>(modulePath, jitiLoaders);
}
function loadWhatsAppLightModule(): WhatsAppLightRuntimeModule {
const modulePath = resolveWhatsAppRuntimeModulePath(
resolveWhatsAppPluginRecord(),
"light-runtime-api",
);
if (cachedLightModule && cachedLightModulePath === modulePath) {
return cachedLightModule;
}
const loaded = loadPluginBoundaryModuleWithJiti<WhatsAppLightRuntimeModule>(
modulePath,
jitiLoaders,
);
cachedLightModulePath = modulePath;
cachedLightModule = loaded;
return loaded;
}
async function loadWhatsAppHeavyModule(): Promise<WhatsAppHeavyRuntimeModule> {
const record = resolveWhatsAppPluginRecord();
const modulePath = resolveWhatsAppRuntimeModulePath(record, "runtime-api");
if (cachedHeavyModule && cachedHeavyModulePath === modulePath) {
return cachedHeavyModule;
}
const loaded = loadPluginBoundaryModuleWithJiti<WhatsAppHeavyRuntimeModule>(
modulePath,
jitiLoaders,
);
cachedHeavyModulePath = modulePath;
cachedHeavyModule = loaded;
return loaded;
}
function getLightExport<K extends keyof WhatsAppLightRuntimeModule>(
exportName: K,
): NonNullable<WhatsAppLightRuntimeModule[K]> {
const loaded = loadWhatsAppLightModule();
const value = loaded[exportName];
if (value == null) {
throw new Error(`WhatsApp plugin runtime is missing export '${String(exportName)}'`);
}
return value as NonNullable<WhatsAppLightRuntimeModule[K]>;
}
async function getHeavyExport<K extends keyof WhatsAppHeavyRuntimeModule>(
exportName: K,
): Promise<NonNullable<WhatsAppHeavyRuntimeModule[K]>> {
const loaded = await loadWhatsAppHeavyModule();
const value = loaded[exportName];
if (value == null) {
throw new Error(`WhatsApp plugin runtime is missing export '${String(exportName)}'`);
}
return value as NonNullable<WhatsAppHeavyRuntimeModule[K]>;
}
export function getActiveWebListener(
...args: Parameters<WhatsAppLightRuntimeModule["getActiveWebListener"]>
): ReturnType<WhatsAppLightRuntimeModule["getActiveWebListener"]> {
return getLightExport("getActiveWebListener")(...args);
}
export function getWebAuthAgeMs(
...args: Parameters<WhatsAppLightRuntimeModule["getWebAuthAgeMs"]>
): ReturnType<WhatsAppLightRuntimeModule["getWebAuthAgeMs"]> {
return getLightExport("getWebAuthAgeMs")(...args);
}
export function logWebSelfId(
...args: Parameters<WhatsAppLightRuntimeModule["logWebSelfId"]>
): ReturnType<WhatsAppLightRuntimeModule["logWebSelfId"]> {
return getLightExport("logWebSelfId")(...args);
}
export function loginWeb(
...args: Parameters<WhatsAppHeavyRuntimeModule["loginWeb"]>
): ReturnType<WhatsAppHeavyRuntimeModule["loginWeb"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.loginWeb(...args));
}
export function logoutWeb(
...args: Parameters<WhatsAppLightRuntimeModule["logoutWeb"]>
): ReturnType<WhatsAppLightRuntimeModule["logoutWeb"]> {
return getLightExport("logoutWeb")(...args);
}
export function readWebSelfId(
...args: Parameters<WhatsAppLightRuntimeModule["readWebSelfId"]>
): ReturnType<WhatsAppLightRuntimeModule["readWebSelfId"]> {
return getLightExport("readWebSelfId")(...args);
}
export function webAuthExists(
...args: Parameters<WhatsAppLightRuntimeModule["webAuthExists"]>
): ReturnType<WhatsAppLightRuntimeModule["webAuthExists"]> {
return getLightExport("webAuthExists")(...args);
}
export function sendMessageWhatsApp(
...args: Parameters<WhatsAppHeavyRuntimeModule["sendMessageWhatsApp"]>
): ReturnType<WhatsAppHeavyRuntimeModule["sendMessageWhatsApp"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.sendMessageWhatsApp(...args));
}
export function sendPollWhatsApp(
...args: Parameters<WhatsAppHeavyRuntimeModule["sendPollWhatsApp"]>
): ReturnType<WhatsAppHeavyRuntimeModule["sendPollWhatsApp"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.sendPollWhatsApp(...args));
}
export function sendReactionWhatsApp(
...args: Parameters<WhatsAppHeavyRuntimeModule["sendReactionWhatsApp"]>
): ReturnType<WhatsAppHeavyRuntimeModule["sendReactionWhatsApp"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.sendReactionWhatsApp(...args));
}
export function createRuntimeWhatsAppLoginTool(
...args: Parameters<WhatsAppLightRuntimeModule["createWhatsAppLoginTool"]>
): ReturnType<WhatsAppLightRuntimeModule["createWhatsAppLoginTool"]> {
return getLightExport("createWhatsAppLoginTool")(...args);
}
export function createWaSocket(
...args: Parameters<WhatsAppHeavyRuntimeModule["createWaSocket"]>
): ReturnType<WhatsAppHeavyRuntimeModule["createWaSocket"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.createWaSocket(...args));
}
export function formatError(
...args: Parameters<WhatsAppLightRuntimeModule["formatError"]>
): ReturnType<WhatsAppLightRuntimeModule["formatError"]> {
return getLightExport("formatError")(...args);
}
export function getStatusCode(
...args: Parameters<WhatsAppLightRuntimeModule["getStatusCode"]>
): ReturnType<WhatsAppLightRuntimeModule["getStatusCode"]> {
return getLightExport("getStatusCode")(...args);
}
export function pickWebChannel(
...args: Parameters<WhatsAppLightRuntimeModule["pickWebChannel"]>
): ReturnType<WhatsAppLightRuntimeModule["pickWebChannel"]> {
return getLightExport("pickWebChannel")(...args);
}
export function resolveWaWebAuthDir(): WhatsAppLightRuntimeModule["WA_WEB_AUTH_DIR"] {
return getLightExport("WA_WEB_AUTH_DIR");
}
export async function handleWhatsAppAction(
...args: Parameters<WhatsAppHeavyRuntimeModule["handleWhatsAppAction"]>
): ReturnType<WhatsAppHeavyRuntimeModule["handleWhatsAppAction"]> {
return (await getHeavyExport("handleWhatsAppAction"))(...args);
}
export async function loadWebMedia(
...args: Parameters<typeof loadWebMediaImpl>
): ReturnType<typeof loadWebMediaImpl> {
return await loadWebMediaImpl(...args);
}
export async function loadWebMediaRaw(
...args: Parameters<typeof loadWebMediaRawImpl>
): ReturnType<typeof loadWebMediaRawImpl> {
return await loadWebMediaRawImpl(...args);
}
export function monitorWebChannel(
...args: Parameters<WhatsAppHeavyRuntimeModule["monitorWebChannel"]>
): ReturnType<WhatsAppHeavyRuntimeModule["monitorWebChannel"]> {
return loadWhatsAppHeavyModule().then((loaded) => loaded.monitorWebChannel(...args));
}
export async function monitorWebInbox(
...args: Parameters<WhatsAppHeavyRuntimeModule["monitorWebInbox"]>
): ReturnType<WhatsAppHeavyRuntimeModule["monitorWebInbox"]> {
return (await getHeavyExport("monitorWebInbox"))(...args);
}
export async function optimizeImageToJpeg(
...args: Parameters<typeof optimizeImageToJpegImpl>
): ReturnType<typeof optimizeImageToJpegImpl> {
return await optimizeImageToJpegImpl(...args);
}
export async function runWebHeartbeatOnce(
...args: Parameters<WhatsAppHeavyRuntimeModule["runWebHeartbeatOnce"]>
): ReturnType<WhatsAppHeavyRuntimeModule["runWebHeartbeatOnce"]> {
return (await getHeavyExport("runWebHeartbeatOnce"))(...args);
}
export async function startWebLoginWithQr(
...args: Parameters<WhatsAppHeavyRuntimeModule["startWebLoginWithQr"]>
): ReturnType<WhatsAppHeavyRuntimeModule["startWebLoginWithQr"]> {
return (await getHeavyExport("startWebLoginWithQr"))(...args);
}
export async function waitForWaConnection(
...args: Parameters<WhatsAppHeavyRuntimeModule["waitForWaConnection"]>
): ReturnType<WhatsAppHeavyRuntimeModule["waitForWaConnection"]> {
return (await getHeavyExport("waitForWaConnection"))(...args);
}
export async function waitForWebLogin(
...args: Parameters<WhatsAppHeavyRuntimeModule["waitForWebLogin"]>
): ReturnType<WhatsAppHeavyRuntimeModule["waitForWebLogin"]> {
return (await getHeavyExport("waitForWebLogin"))(...args);
}
export const extractMediaPlaceholder = (
...args: Parameters<WhatsAppHeavyRuntimeModule["extractMediaPlaceholder"]>
) => loadCurrentHeavyModuleSync().extractMediaPlaceholder(...args);
export const extractText = (...args: Parameters<WhatsAppHeavyRuntimeModule["extractText"]>) =>
loadCurrentHeavyModuleSync().extractText(...args);
export function getDefaultLocalRoots(
...args: Parameters<typeof getDefaultLocalRootsImpl>
): ReturnType<typeof getDefaultLocalRootsImpl> {
return getDefaultLocalRootsImpl(...args);
}
export function resolveHeartbeatRecipients(
...args: Parameters<WhatsAppHeavyRuntimeModule["resolveHeartbeatRecipients"]>
): ReturnType<WhatsAppHeavyRuntimeModule["resolveHeartbeatRecipients"]> {
return loadCurrentHeavyModuleSync().resolveHeartbeatRecipients(...args);
}

View file

@ -0,0 +1,289 @@
import { createJiti } from "jiti";
type WebChannelHeavyRuntimeModule = typeof import("@openclaw/whatsapp/runtime-api.js");
type WebChannelLightRuntimeModule = typeof import("@openclaw/whatsapp/light-runtime-api.js");
import {
getDefaultLocalRoots as getDefaultLocalRootsImpl,
loadWebMedia as loadWebMediaImpl,
loadWebMediaRaw as loadWebMediaRawImpl,
optimizeImageToJpeg as optimizeImageToJpegImpl,
} from "../../media/web-media.js";
import {
loadPluginBoundaryModuleWithJiti,
resolvePluginRuntimeModulePath,
resolvePluginRuntimeRecord,
} from "./runtime-plugin-boundary.js";
const WEB_CHANNEL_PLUGIN_ID = "whatsapp";
type WebChannelPluginRecord = {
origin: string;
rootDir?: string;
source: string;
};
let cachedHeavyModulePath: string | null = null;
let cachedHeavyModule: WebChannelHeavyRuntimeModule | null = null;
let cachedLightModulePath: string | null = null;
let cachedLightModule: WebChannelLightRuntimeModule | null = null;
const jitiLoaders = new Map<boolean, ReturnType<typeof createJiti>>();
function resolveWebChannelPluginRecord(): WebChannelPluginRecord {
return resolvePluginRuntimeRecord(WEB_CHANNEL_PLUGIN_ID, () => {
throw new Error(
`web channel plugin runtime is unavailable: missing plugin '${WEB_CHANNEL_PLUGIN_ID}'`,
);
}) as WebChannelPluginRecord;
}
function resolveWebChannelRuntimeModulePath(
record: WebChannelPluginRecord,
entryBaseName: "light-runtime-api" | "runtime-api",
): string {
const modulePath = resolvePluginRuntimeModulePath(record, entryBaseName, () => {
throw new Error(
`web channel plugin runtime is unavailable: missing ${entryBaseName} for plugin '${WEB_CHANNEL_PLUGIN_ID}'`,
);
});
if (!modulePath) {
throw new Error(
`web channel plugin runtime is unavailable: missing ${entryBaseName} for plugin '${WEB_CHANNEL_PLUGIN_ID}'`,
);
}
return modulePath;
}
function loadCurrentHeavyModuleSync(): WebChannelHeavyRuntimeModule {
const modulePath = resolveWebChannelRuntimeModulePath(
resolveWebChannelPluginRecord(),
"runtime-api",
);
return loadPluginBoundaryModuleWithJiti<WebChannelHeavyRuntimeModule>(modulePath, jitiLoaders);
}
function loadWebChannelLightModule(): WebChannelLightRuntimeModule {
const modulePath = resolveWebChannelRuntimeModulePath(
resolveWebChannelPluginRecord(),
"light-runtime-api",
);
if (cachedLightModule && cachedLightModulePath === modulePath) {
return cachedLightModule;
}
const loaded = loadPluginBoundaryModuleWithJiti<WebChannelLightRuntimeModule>(
modulePath,
jitiLoaders,
);
cachedLightModulePath = modulePath;
cachedLightModule = loaded;
return loaded;
}
async function loadWebChannelHeavyModule(): Promise<WebChannelHeavyRuntimeModule> {
const record = resolveWebChannelPluginRecord();
const modulePath = resolveWebChannelRuntimeModulePath(record, "runtime-api");
if (cachedHeavyModule && cachedHeavyModulePath === modulePath) {
return cachedHeavyModule;
}
const loaded = loadPluginBoundaryModuleWithJiti<WebChannelHeavyRuntimeModule>(
modulePath,
jitiLoaders,
);
cachedHeavyModulePath = modulePath;
cachedHeavyModule = loaded;
return loaded;
}
function getLightExport<K extends keyof WebChannelLightRuntimeModule>(
exportName: K,
): NonNullable<WebChannelLightRuntimeModule[K]> {
const loaded = loadWebChannelLightModule();
const value = loaded[exportName];
if (value == null) {
throw new Error(`web channel plugin runtime is missing export '${String(exportName)}'`);
}
return value as NonNullable<WebChannelLightRuntimeModule[K]>;
}
async function getHeavyExport<K extends keyof WebChannelHeavyRuntimeModule>(
exportName: K,
): Promise<NonNullable<WebChannelHeavyRuntimeModule[K]>> {
const loaded = await loadWebChannelHeavyModule();
const value = loaded[exportName];
if (value == null) {
throw new Error(`web channel plugin runtime is missing export '${String(exportName)}'`);
}
return value as NonNullable<WebChannelHeavyRuntimeModule[K]>;
}
export function getActiveWebListener(
...args: Parameters<WebChannelLightRuntimeModule["getActiveWebListener"]>
): ReturnType<WebChannelLightRuntimeModule["getActiveWebListener"]> {
return getLightExport("getActiveWebListener")(...args);
}
export function getWebAuthAgeMs(
...args: Parameters<WebChannelLightRuntimeModule["getWebAuthAgeMs"]>
): ReturnType<WebChannelLightRuntimeModule["getWebAuthAgeMs"]> {
return getLightExport("getWebAuthAgeMs")(...args);
}
export function logWebSelfId(
...args: Parameters<WebChannelLightRuntimeModule["logWebSelfId"]>
): ReturnType<WebChannelLightRuntimeModule["logWebSelfId"]> {
return getLightExport("logWebSelfId")(...args);
}
export function loginWeb(
...args: Parameters<WebChannelHeavyRuntimeModule["loginWeb"]>
): ReturnType<WebChannelHeavyRuntimeModule["loginWeb"]> {
return loadWebChannelHeavyModule().then((loaded) => loaded.loginWeb(...args));
}
export function logoutWeb(
...args: Parameters<WebChannelLightRuntimeModule["logoutWeb"]>
): ReturnType<WebChannelLightRuntimeModule["logoutWeb"]> {
return getLightExport("logoutWeb")(...args);
}
export function readWebSelfId(
...args: Parameters<WebChannelLightRuntimeModule["readWebSelfId"]>
): ReturnType<WebChannelLightRuntimeModule["readWebSelfId"]> {
return getLightExport("readWebSelfId")(...args);
}
export function webAuthExists(
...args: Parameters<WebChannelLightRuntimeModule["webAuthExists"]>
): ReturnType<WebChannelLightRuntimeModule["webAuthExists"]> {
return getLightExport("webAuthExists")(...args);
}
export function sendWebChannelMessage(
...args: Parameters<WebChannelHeavyRuntimeModule["sendMessageWhatsApp"]>
): ReturnType<WebChannelHeavyRuntimeModule["sendMessageWhatsApp"]> {
return loadWebChannelHeavyModule().then((loaded) => loaded.sendMessageWhatsApp(...args));
}
export function sendWebChannelPoll(
...args: Parameters<WebChannelHeavyRuntimeModule["sendPollWhatsApp"]>
): ReturnType<WebChannelHeavyRuntimeModule["sendPollWhatsApp"]> {
return loadWebChannelHeavyModule().then((loaded) => loaded.sendPollWhatsApp(...args));
}
export function sendWebChannelReaction(
...args: Parameters<WebChannelHeavyRuntimeModule["sendReactionWhatsApp"]>
): ReturnType<WebChannelHeavyRuntimeModule["sendReactionWhatsApp"]> {
return loadWebChannelHeavyModule().then((loaded) => loaded.sendReactionWhatsApp(...args));
}
export function createRuntimeWebChannelLoginTool(
...args: Parameters<WebChannelLightRuntimeModule["createWhatsAppLoginTool"]>
): ReturnType<WebChannelLightRuntimeModule["createWhatsAppLoginTool"]> {
return getLightExport("createWhatsAppLoginTool")(...args);
}
export function createWebChannelSocket(
...args: Parameters<WebChannelHeavyRuntimeModule["createWaSocket"]>
): ReturnType<WebChannelHeavyRuntimeModule["createWaSocket"]> {
return loadWebChannelHeavyModule().then((loaded) => loaded.createWaSocket(...args));
}
export function formatError(
...args: Parameters<WebChannelLightRuntimeModule["formatError"]>
): ReturnType<WebChannelLightRuntimeModule["formatError"]> {
return getLightExport("formatError")(...args);
}
export function getStatusCode(
...args: Parameters<WebChannelLightRuntimeModule["getStatusCode"]>
): ReturnType<WebChannelLightRuntimeModule["getStatusCode"]> {
return getLightExport("getStatusCode")(...args);
}
export function pickWebChannel(
...args: Parameters<WebChannelLightRuntimeModule["pickWebChannel"]>
): ReturnType<WebChannelLightRuntimeModule["pickWebChannel"]> {
return getLightExport("pickWebChannel")(...args);
}
export function resolveWebChannelAuthDir(): WebChannelLightRuntimeModule["WA_WEB_AUTH_DIR"] {
return getLightExport("WA_WEB_AUTH_DIR");
}
export async function handleWebChannelAction(
...args: Parameters<WebChannelHeavyRuntimeModule["handleWhatsAppAction"]>
): ReturnType<WebChannelHeavyRuntimeModule["handleWhatsAppAction"]> {
return (await getHeavyExport("handleWhatsAppAction"))(...args);
}
export async function loadWebMedia(
...args: Parameters<typeof loadWebMediaImpl>
): ReturnType<typeof loadWebMediaImpl> {
return await loadWebMediaImpl(...args);
}
export async function loadWebMediaRaw(
...args: Parameters<typeof loadWebMediaRawImpl>
): ReturnType<typeof loadWebMediaRawImpl> {
return await loadWebMediaRawImpl(...args);
}
export function monitorWebChannel(
...args: Parameters<WebChannelHeavyRuntimeModule["monitorWebChannel"]>
): ReturnType<WebChannelHeavyRuntimeModule["monitorWebChannel"]> {
return loadWebChannelHeavyModule().then((loaded) => loaded.monitorWebChannel(...args));
}
export async function monitorWebInbox(
...args: Parameters<WebChannelHeavyRuntimeModule["monitorWebInbox"]>
): ReturnType<WebChannelHeavyRuntimeModule["monitorWebInbox"]> {
return (await getHeavyExport("monitorWebInbox"))(...args);
}
export async function optimizeImageToJpeg(
...args: Parameters<typeof optimizeImageToJpegImpl>
): ReturnType<typeof optimizeImageToJpegImpl> {
return await optimizeImageToJpegImpl(...args);
}
export async function runWebHeartbeatOnce(
...args: Parameters<WebChannelHeavyRuntimeModule["runWebHeartbeatOnce"]>
): ReturnType<WebChannelHeavyRuntimeModule["runWebHeartbeatOnce"]> {
return (await getHeavyExport("runWebHeartbeatOnce"))(...args);
}
export async function startWebLoginWithQr(
...args: Parameters<WebChannelHeavyRuntimeModule["startWebLoginWithQr"]>
): ReturnType<WebChannelHeavyRuntimeModule["startWebLoginWithQr"]> {
return (await getHeavyExport("startWebLoginWithQr"))(...args);
}
export async function waitForWebChannelConnection(
...args: Parameters<WebChannelHeavyRuntimeModule["waitForWaConnection"]>
): ReturnType<WebChannelHeavyRuntimeModule["waitForWaConnection"]> {
return (await getHeavyExport("waitForWaConnection"))(...args);
}
export async function waitForWebLogin(
...args: Parameters<WebChannelHeavyRuntimeModule["waitForWebLogin"]>
): ReturnType<WebChannelHeavyRuntimeModule["waitForWebLogin"]> {
return (await getHeavyExport("waitForWebLogin"))(...args);
}
export const extractMediaPlaceholder = (
...args: Parameters<WebChannelHeavyRuntimeModule["extractMediaPlaceholder"]>
) => loadCurrentHeavyModuleSync().extractMediaPlaceholder(...args);
export const extractText = (...args: Parameters<WebChannelHeavyRuntimeModule["extractText"]>) =>
loadCurrentHeavyModuleSync().extractText(...args);
export function getDefaultLocalRoots(
...args: Parameters<typeof getDefaultLocalRootsImpl>
): ReturnType<typeof getDefaultLocalRootsImpl> {
return getDefaultLocalRootsImpl(...args);
}
export function resolveHeartbeatRecipients(
...args: Parameters<WebChannelHeavyRuntimeModule["resolveHeartbeatRecipients"]>
): ReturnType<WebChannelHeavyRuntimeModule["resolveHeartbeatRecipients"]> {
return loadCurrentHeavyModuleSync().resolveHeartbeatRecipients(...args);
}

View file

@ -1,17 +1,14 @@
/**
* Runtime helpers for native channel plugins.
*
* This surface exposes core and channel-specific helpers used by bundled
* plugins. Prefer hooks unless you need tight in-process coupling with the
* OpenClaw messaging/runtime stack.
* This surface exposes generic core helpers only. Plugin-owned behavior stays
* inside the owning plugin package instead of hanging off core runtime slots
* like `channel.discord` or `channel.slack`.
*/
type ReadChannelAllowFromStore =
typeof import("../../pairing/pairing-store.js").readChannelAllowFromStore;
type UpsertChannelPairingRequest =
typeof import("../../pairing/pairing-store.js").upsertChannelPairingRequest;
type DiscordRuntimeSurface = typeof import("../../../extensions/discord/runtime-api.js");
type DiscordThreadBindings = typeof import("../../../extensions/discord/runtime-api.js");
type MatrixThreadBindings = typeof import("../../../extensions/matrix/api.js");
type ReadChannelAllowFromStoreForAccount = (params: {
channel: Parameters<ReadChannelAllowFromStore>[0];
@ -110,98 +107,16 @@ export type PluginRuntimeChannel = {
};
threadBindings: {
setIdleTimeoutBySessionKey: (params: {
channelId: "discord" | "matrix" | "telegram";
channelId: string;
targetSessionKey: string;
accountId?: string;
idleTimeoutMs: number;
}) => RuntimeThreadBindingLifecycleRecord[];
setMaxAgeBySessionKey: (params: {
channelId: "discord" | "matrix" | "telegram";
channelId: string;
targetSessionKey: string;
accountId?: string;
maxAgeMs: number;
}) => RuntimeThreadBindingLifecycleRecord[];
};
discord: {
messageActions: DiscordRuntimeSurface["discordMessageActions"];
auditChannelPermissions: DiscordRuntimeSurface["auditDiscordChannelPermissions"];
listDirectoryGroupsLive: DiscordRuntimeSurface["listDiscordDirectoryGroupsLive"];
listDirectoryPeersLive: DiscordRuntimeSurface["listDiscordDirectoryPeersLive"];
probeDiscord: DiscordRuntimeSurface["probeDiscord"];
resolveChannelAllowlist: DiscordRuntimeSurface["resolveDiscordChannelAllowlist"];
resolveUserAllowlist: DiscordRuntimeSurface["resolveDiscordUserAllowlist"];
sendComponentMessage: DiscordRuntimeSurface["sendDiscordComponentMessage"];
sendMessageDiscord: DiscordRuntimeSurface["sendMessageDiscord"];
sendPollDiscord: DiscordRuntimeSurface["sendPollDiscord"];
monitorDiscordProvider: DiscordRuntimeSurface["monitorDiscordProvider"];
threadBindings: {
getManager: DiscordThreadBindings["getThreadBindingManager"];
resolveIdleTimeoutMs: DiscordThreadBindings["resolveThreadBindingIdleTimeoutMs"];
resolveInactivityExpiresAt: DiscordThreadBindings["resolveThreadBindingInactivityExpiresAt"];
resolveMaxAgeMs: DiscordThreadBindings["resolveThreadBindingMaxAgeMs"];
resolveMaxAgeExpiresAt: DiscordThreadBindings["resolveThreadBindingMaxAgeExpiresAt"];
setIdleTimeoutBySessionKey: DiscordThreadBindings["setThreadBindingIdleTimeoutBySessionKey"];
setMaxAgeBySessionKey: DiscordThreadBindings["setThreadBindingMaxAgeBySessionKey"];
unbindBySessionKey: DiscordThreadBindings["unbindThreadBindingsBySessionKey"];
};
typing: {
pulse: DiscordRuntimeSurface["sendTypingDiscord"];
start: (params: {
channelId: string;
accountId?: string;
cfg?: ReturnType<typeof import("../../config/config.js").loadConfig>;
intervalMs?: number;
}) => Promise<{
refresh: () => Promise<void>;
stop: () => void;
}>;
};
conversationActions: {
editMessage: DiscordRuntimeSurface["editMessageDiscord"];
deleteMessage: DiscordRuntimeSurface["deleteMessageDiscord"];
pinMessage: DiscordRuntimeSurface["pinMessageDiscord"];
unpinMessage: DiscordRuntimeSurface["unpinMessageDiscord"];
createThread: DiscordRuntimeSurface["createThreadDiscord"];
editChannel: DiscordRuntimeSurface["editChannelDiscord"];
};
};
slack: {
listDirectoryGroupsLive: typeof import("../../../extensions/slack/runtime-api.js").listSlackDirectoryGroupsLive;
listDirectoryPeersLive: typeof import("../../../extensions/slack/runtime-api.js").listSlackDirectoryPeersLive;
probeSlack: typeof import("../../../extensions/slack/runtime-api.js").probeSlack;
resolveChannelAllowlist: typeof import("../../../extensions/slack/runtime-api.js").resolveSlackChannelAllowlist;
resolveUserAllowlist: typeof import("../../../extensions/slack/runtime-api.js").resolveSlackUserAllowlist;
sendMessageSlack: typeof import("../../../extensions/slack/runtime-api.js").sendMessageSlack;
monitorSlackProvider: typeof import("../../../extensions/slack/runtime-api.js").monitorSlackProvider;
handleSlackAction: typeof import("../../../extensions/slack/runtime-api.js").handleSlackAction;
};
matrix: {
threadBindings: {
setIdleTimeoutBySessionKey: MatrixThreadBindings["setMatrixThreadBindingIdleTimeoutBySessionKey"];
setMaxAgeBySessionKey: MatrixThreadBindings["setMatrixThreadBindingMaxAgeBySessionKey"];
};
};
signal: {
probeSignal: typeof import("../../../extensions/signal/runtime-api.js").probeSignal;
sendMessageSignal: typeof import("../../../extensions/signal/runtime-api.js").sendMessageSignal;
monitorSignalProvider: typeof import("../../../extensions/signal/runtime-api.js").monitorSignalProvider;
messageActions: typeof import("../../../extensions/signal/runtime-api.js").signalMessageActions;
};
line: {
listLineAccountIds: typeof import("../../plugin-sdk/line.js").listLineAccountIds;
resolveDefaultLineAccountId: typeof import("../../plugin-sdk/line.js").resolveDefaultLineAccountId;
resolveLineAccount: typeof import("../../plugin-sdk/line.js").resolveLineAccount;
normalizeAccountId: typeof import("../../plugin-sdk/line.js").normalizeAccountId;
probeLineBot: typeof import("../../plugin-sdk/line-runtime.js").probeLineBot;
sendMessageLine: typeof import("../../plugin-sdk/line-runtime.js").sendMessageLine;
pushMessageLine: typeof import("../../plugin-sdk/line-runtime.js").pushMessageLine;
pushMessagesLine: typeof import("../../plugin-sdk/line-runtime.js").pushMessagesLine;
pushFlexMessage: typeof import("../../plugin-sdk/line-runtime.js").pushFlexMessage;
pushTemplateMessage: typeof import("../../plugin-sdk/line-runtime.js").pushTemplateMessage;
pushLocationMessage: typeof import("../../plugin-sdk/line-runtime.js").pushLocationMessage;
pushTextMessageWithQuickReplies: typeof import("../../plugin-sdk/line-runtime.js").pushTextMessageWithQuickReplies;
createQuickReplyItems: typeof import("../../plugin-sdk/line-runtime.js").createQuickReplyItems;
buildTemplateMessageFromPayload: typeof import("../../plugin-sdk/line-runtime.js").buildTemplateMessageFromPayload;
monitorLineProvider: typeof import("../../plugin-sdk/line-runtime.js").monitorLineProvider;
};
};

View file

@ -333,12 +333,7 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
setMaxAgeBySessionKey:
vi.fn() as unknown as PluginRuntime["channel"]["threadBindings"]["setMaxAgeBySessionKey"],
},
discord: {} as PluginRuntime["channel"]["discord"],
activity: {} as PluginRuntime["channel"]["activity"],
line: {} as PluginRuntime["channel"]["line"],
slack: {} as PluginRuntime["channel"]["slack"],
matrix: {} as PluginRuntime["channel"]["matrix"],
signal: {} as PluginRuntime["channel"]["signal"],
},
events: {
onAgentEvent: vi.fn(() => () => {}) as unknown as PluginRuntime["events"]["onAgentEvent"],

View file

@ -128,7 +128,6 @@ function buildCoreDistEntries(): Record<string, string> {
"agents/pi-model-discovery-runtime": "src/agents/pi-model-discovery-runtime.ts",
"commands/status.summary.runtime": "src/commands/status.summary.runtime.ts",
"plugins/provider-runtime.runtime": "src/plugins/provider-runtime.runtime.ts",
"plugins/runtime/runtime-line.contract": "src/plugins/runtime/runtime-line.contract.ts",
extensionAPI: "src/extensionAPI.ts",
"infra/warning-filter": "src/infra/warning-filter.ts",
"telegram/audit": bundledPluginFile("telegram", "src/audit.ts"),