feat(plugin-sdk): Add channelRuntime support for external channel plugins

## Overview

This PR enables external channel plugins (loaded via Plugin SDK) to access
advanced runtime features like AI response dispatching, which were previously
only available to built-in channels.

## Changes

### src/gateway/server-channels.ts
- Import PluginRuntime type
- Add optional channelRuntime parameter to ChannelManagerOptions
- Pass channelRuntime to channel startAccount calls via conditional spread
- Ensures backward compatibility (field is optional)

### src/gateway/server.impl.ts
- Import createPluginRuntime from plugins/runtime
- Create and pass channelRuntime to channel manager

### src/channels/plugins/types.adapters.ts
- Import PluginRuntime type
- Add comprehensive documentation for channelRuntime field
- Document available features, use cases, and examples
- Improve type safety (use imported PluginRuntime type vs inline import)

## Benefits

External channel plugins can now:
- Generate AI-powered responses using dispatchReplyWithBufferedBlockDispatcher
- Access routing, text processing, and session management utilities
- Use command authorization and group policy resolution
- Maintain feature parity with built-in channels

## Backward Compatibility

- channelRuntime field is optional in ChannelGatewayContext
- Conditional spread ensures it's only passed when explicitly provided
- Existing channels without channelRuntime support continue to work unchanged
- No breaking changes to channel plugin API

## Testing

- Email channel plugin successfully uses channelRuntime for AI responses
- All existing built-in channels (slack, discord, telegram, etc.) work unchanged
- Gateway loads and runs without errors when channelRuntime is provided
This commit is contained in:
Gu XiaoBo 2026-02-24 21:51:41 +08:00 committed by Peter Steinberger
parent 666073ee46
commit 469cd5b464
3 changed files with 98 additions and 1 deletions

View File

@ -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<ResolvedAccount = unknown> = {
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<EmailAccount> = {
* 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 = {

View File

@ -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<ChannelId, SubsystemLogger>;
channelRuntimeEnvs: Record<ChannelId, RuntimeEnv>;
/**
* 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<ChannelId, ChannelRuntimeStore>();
// 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) => {

View File

@ -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;