diff --git a/src/channels/plugins/types.adapters.ts b/src/channels/plugins/types.adapters.ts index ead7f68b2fa..f31f3b20284 100644 --- a/src/channels/plugins/types.adapters.ts +++ b/src/channels/plugins/types.adapters.ts @@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../../config/config.js"; import type { GroupToolPolicyConfig } from "../../config/types.tools.js"; import type { OutboundDeliveryResult, OutboundSendDeps } from "../../infra/outbound/deliver.js"; import type { OutboundIdentity } from "../../infra/outbound/identity.js"; +import type { PluginRuntime } from "../../plugins/runtime/types.js"; import type { RuntimeEnv } from "../../runtime.js"; import type { ChannelAccountSnapshot, @@ -172,6 +173,68 @@ export type ChannelGatewayContext = { log?: ChannelLogSink; getStatus: () => ChannelAccountSnapshot; setStatus: (next: ChannelAccountSnapshot) => void; + /** + * Optional channel runtime helpers for external channel plugins. + * + * This field provides access to advanced Plugin SDK features that are + * available to external plugins but not to built-in channels (which can + * directly import internal modules). + * + * ## Available Features + * + * - **reply**: AI response dispatching, formatting, and delivery + * - **routing**: Agent route resolution and matching + * - **text**: Text chunking, markdown processing, and control command detection + * - **session**: Session management and metadata tracking + * - **media**: Remote media fetching and buffer saving + * - **commands**: Command authorization and control command handling + * - **groups**: Group policy resolution and mention requirements + * - **pairing**: Channel pairing and allow-from management + * + * ## Use Cases + * + * External channel plugins (e.g., email, SMS, custom integrations) that need: + * - AI-powered response generation and delivery + * - Advanced text processing and formatting + * - Session tracking and management + * - Agent routing and policy resolution + * + * ## Example + * + * ```typescript + * const emailGatewayAdapter: ChannelGatewayAdapter = { + * startAccount: async (ctx) => { + * // Check availability (for backward compatibility) + * if (!ctx.channelRuntime) { + * ctx.log?.warn?.("channelRuntime not available - skipping AI features"); + * return; + * } + * + * // Use AI dispatch + * await ctx.channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({ + * ctx: { ... }, + * cfg: ctx.cfg, + * dispatcherOptions: { + * deliver: async (payload) => { + * // Send reply via email + * }, + * }, + * }); + * }, + * }; + * ``` + * + * ## Backward Compatibility + * + * - This field is **optional** - channels that don't need it can ignore it + * - Built-in channels (slack, discord, etc.) typically don't use this field + * because they can directly import internal modules + * - External plugins should check for undefined before using + * + * @since Plugin SDK 2026.2.19 + * @see {@link https://docs.openclaw.ai/plugins/developing-plugins | Plugin SDK documentation} + */ + channelRuntime?: PluginRuntime["channel"]; }; export type ChannelLogoutResult = { diff --git a/src/gateway/server-channels.ts b/src/gateway/server-channels.ts index c5a4064e2f1..6c291541369 100644 --- a/src/gateway/server-channels.ts +++ b/src/gateway/server-channels.ts @@ -6,6 +6,7 @@ import { type BackoffPolicy, computeBackoff, sleepWithAbort } from "../infra/bac import { formatErrorMessage } from "../infra/errors.js"; import { resetDirectoryCache } from "../infra/outbound/target-resolver.js"; import type { createSubsystemLogger } from "../logging/subsystem.js"; +import type { PluginRuntime } from "../plugins/runtime/types.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -59,6 +60,36 @@ type ChannelManagerOptions = { loadConfig: () => OpenClawConfig; channelLogs: Record; channelRuntimeEnvs: Record; + /** + * Optional channel runtime helpers for external channel plugins. + * + * When provided, this value is passed to all channel plugins via the + * `channelRuntime` field in `ChannelGatewayContext`, enabling external + * plugins to access advanced Plugin SDK features (AI dispatch, routing, + * text processing, etc.). + * + * Built-in channels (slack, discord, telegram) typically don't use this + * because they can directly import internal modules from the monorepo. + * + * This field is optional - omitting it maintains backward compatibility + * with existing channels. + * + * @example + * ```typescript + * import { createPluginRuntime } from "../plugins/runtime/index.js"; + * + * const channelManager = createChannelManager({ + * loadConfig, + * channelLogs, + * channelRuntimeEnvs, + * channelRuntime: createPluginRuntime().channel, + * }); + * ``` + * + * @since Plugin SDK 2026.2.19 + * @see {@link ChannelGatewayContext.channelRuntime} + */ + channelRuntime?: PluginRuntime["channel"]; }; type StartChannelOptions = { @@ -78,7 +109,7 @@ export type ChannelManager = { // Channel docking: lifecycle hooks (`plugin.gateway`) flow through this manager. export function createChannelManager(opts: ChannelManagerOptions): ChannelManager { - const { loadConfig, channelLogs, channelRuntimeEnvs } = opts; + const { loadConfig, channelLogs, channelRuntimeEnvs, channelRuntime } = opts; const channelStores = new Map(); // Tracks restart attempts per channel:account. Reset on successful start. @@ -199,6 +230,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage log, getStatus: () => getRuntime(channelId, id), setStatus: (next) => setRuntime(channelId, id, next), + ...(channelRuntime ? { channelRuntime } : {}), }); const trackedPromise = Promise.resolve(task) .catch((err) => { diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 65c84593202..88354131859 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -46,6 +46,7 @@ import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/di import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js"; import { getGlobalHookRunner, runGlobalGatewayStopSafely } from "../plugins/hook-runner-global.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { createPluginRuntime } from "../plugins/runtime/index.js"; import type { PluginServicesHandle } from "../plugins/services.js"; import { getTotalQueueSize } from "../process/command-queue.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -554,6 +555,7 @@ export async function startGatewayServer( loadConfig, channelLogs, channelRuntimeEnvs, + channelRuntime: createPluginRuntime().channel, }); const { getRuntimeSnapshot, startChannels, startChannel, stopChannel, markChannelLoggedOut } = channelManager;