How to Add a New Chat Platform Plugin to AionUi's Channels Subsystem

To add a new chat platform plugin to AionUi, extend the abstract BasePlugin class, implement the required lifecycle methods, and register the constructor with PluginManager using a unique PluginType identifier.

AionUi's Channels subsystem provides a modular architecture for integrating external chat platforms like Telegram, Lark, and DingTalk. By implementing the BasePlugin abstract class and registering your plugin with the PluginManager, you can connect any messaging platform to AionUi's unified message pipeline. This guide walks through the complete implementation using the actual source code from the iOfficeAI/AionUi repository.

Understanding the Plugin Architecture

The Channels subsystem in AionUi relies on a plugin-based architecture where every external chat platform is represented by a class that extends BasePlugin. The PluginManager maintains a registry mapping PluginType strings to plugin constructors, instantiating them based on configurations stored in the SQLite channel_plugin table.

When the application starts, ChannelManager.initialize() creates a PluginManager instance and automatically loads all enabled plugins. The manager calls plugin.initialize(config) for credential validation, followed by plugin.start() to establish connections via polling or WebSocket. All inbound messages are converted to the unified message format (IUnifiedIncomingMessage), ensuring the rest of the system— including the ActionExecutor and tool call handlers—remains platform-agnostic.

Step-by-Step Implementation Guide

1. Create the Plugin Directory Structure

Create a dedicated folder for your platform under src/channels/plugins/. For a platform named "MyChat", establish the following structure:

src/channels/plugins/mychat/
├── MyChatPlugin.ts
└── adapters.ts          # Optional: message format converters

Export the class from the central plugins index at src/channels/plugins/index.ts for convenient imports:

export { MyChatPlugin } from './mychat/MyChatPlugin';

2. Extend BasePlugin and Implement Lifecycle Methods

Create MyChatPlugin.ts and extend the abstract BasePlugin class from src/channels/plugins/BasePlugin.ts. You must implement the following protected lifecycle hooks:

  • onInitialize(config) – Validate credentials and initialize the platform SDK
  • onStart() – Establish connections and subscribe to incoming messages
  • onStop() – Clean up connections and clear state

Additionally, implement the public messaging APIs:

  • sendMessage(chatId, message) – Send outgoing messages and return the platform-specific message ID
  • editMessage(chatId, messageId, message) – Update existing messages for streaming responses
  • getActiveUserCount() – Return the number of active users for the status UI
  • getBotInfo() – Return bot metadata including name and ID

3. Handle Message Conversion

Inside your plugin, convert raw platform messages to IUnifiedIncomingMessage format before passing them to the handler. This decouples platform-specific schemas from AionUi's core processing logic. Use background processing to avoid blocking the SDK's event loop:

private async handleIncoming(raw: any): Promise<void> {
  const unified = toUnifiedIncomingMessage(raw);
  if (unified && this.messageHandler) {
    void this.messageHandler(unified).catch(err =>
      console.error('[MyChatPlugin] Message handler error:', err),
    );
  }
}

4. Register the Plugin with PluginManager

In src/channels/core/ChannelManager.ts, import your plugin and register it using registerPlugin() within the constructor or initialization block (around lines 48-52):

import { MyChatPlugin } from '../plugins/mychat/MyChatPlugin';

// Inside ChannelManager initialization:
registerPlugin('mychat', MyChatPlugin);

The registerPlugin method accepts a unique type string and the plugin constructor, storing them in the PluginManager's internal registry (Map<PluginType, PluginConstructor>).

5. Update Type Definitions

Add your plugin type to the PluginType union in src/channels/types.ts to ensure type safety across the codebase:

export type PluginType = 'telegram' | 'lark' | 'dingtalk' | 'mychat';

Complete Code Example

The following implementation demonstrates a fully functional plugin skeleton for a fictitious "MyChat" platform:

// src/channels/plugins/mychat/MyChatPlugin.ts
import type {
  IChannelPluginConfig,
  IUnifiedIncomingMessage,
  IUnifiedOutgoingMessage,
  PluginType,
  BotInfo,
} from '../../types';
import { BasePlugin } from '../BasePlugin';

export class MyChatPlugin extends BasePlugin {
  readonly type: PluginType = 'mychat';
  private client: any = null;
  private activeUsers = new Set<string>();

  protected async onInitialize(config: IChannelPluginConfig): Promise<void> {
    const token = config.credentials?.token;
    if (!token) {
      throw new Error('MyChat token is required');
    }
    this.client = new MyChatSDK({ token });
  }

  protected async onStart(): Promise<void> {
    if (!this.client) {
      throw new Error('Client not initialised');
    }
    await this.client.connect();
    this.client.on('message', (msg: any) => this.handleIncoming(msg));
  }

  protected async onStop(): Promise<void> {
    if (this.client) {
      await this.client.disconnect();
    }
    this.activeUsers.clear();
  }

  async sendMessage(
    chatId: string,
    message: IUnifiedOutgoingMessage,
  ): Promise<string> {
    if (!this.client) {
      throw new Error('Client not initialised');
    }
    const { text, options } = toMyChatSendParams(message);
    const result = await this.client.sendMessage(chatId, text, options);
    return result.messageId;
  }

  async editMessage(
    chatId: string,
    messageId: string,
    message: IUnifiedOutgoingMessage,
  ): Promise<void> {
    if (!this.client) {
      throw new Error('Client not initialised');
    }
    const { text, options } = toMyChatSendParams(message);
    await this.client.editMessage(chatId, messageId, text, options);
  }

  getActiveUserCount(): number {
    return this.activeUsers.size;
  }

  getBotInfo(): BotInfo | null {
    return this.client?.botInfo ?? null;
  }

  private async handleIncoming(raw: any): Promise<void> {
    const unified = toUnifiedIncomingMessage(raw);
    if (unified && this.messageHandler) {
      void this.messageHandler(unified).catch(err =>
        console.error('[MyChatPlugin] Message handler error:', err),
      );
    }
  }

  static async testConnection(token: string): Promise<{ success: boolean; error?: string }> {
    try {
      const client = new MyChatSDK({ token });
      await client.ping();
      return { success: true };
    } catch (e: any) {
      return { success: false, error: e.message };
    }
  }
}

Integration with ChannelManager

The ChannelManager orchestrates plugin lifecycle and registration. After adding your import and registration call, the system automatically instantiates your plugin when users enable it through the UI. The registration in src/channels/core/ChannelManager.ts follows this pattern:

// src/channels/core/ChannelManager.ts
import { MyChatPlugin } from '../plugins/mychat/MyChatPlugin';

export class ChannelManager {
  private constructor() {
    // Existing registrations...
    registerPlugin('mychat', MyChatPlugin);
  }
}

When ChannelManager.initialize() runs, it queries the channel_plugin table for rows with type='mychat', creates instances using the registered constructor, and calls initialize() followed by start() on each enabled plugin.

Key Implementation Details

BasePlugin defines common state including status, config, messageHandler, and confirmHandler. Your implementation inherits these properties while providing platform-specific logic for the abstract methods.

The unified message format requires converting all incoming payloads to IUnifiedIncomingMessage and outgoing messages from IUnifiedOutgoingMessage. This standardization allows AionUi's ActionExecutor to process commands, tool calls, and streaming updates without platform-specific code branches.

For real-world reference, examine src/channels/plugins/telegram/TelegramPlugin.ts for polling-based implementations or src/channels/plugins/lark/LarkPlugin.ts for WebSocket handling and event deduplication patterns.

Summary

  • Extend BasePlugin from src/channels/plugins/BasePlugin.ts to create a new chat platform integration
  • Implement lifecycle methods (onInitialize, onStart, onStop) and messaging APIs (sendMessage, editMessage)
  • Register the plugin using registerPlugin(type, Constructor) in src/channels/core/ChannelManager.ts
  • Update PluginType in src/channels/types.ts to include your new platform identifier
  • Convert messages to unified formats to maintain compatibility with AionUi's core processing pipeline
  • Add UI configuration optionally by following existing patterns for credential management in the Settings page

Frequently Asked Questions

What methods must I implement when extending BasePlugin?

You must implement three protected lifecycle methods (onInitialize, onStart, onStop) and four public APIs (sendMessage, editMessage, getActiveUserCount, getBotInfo). The lifecycle methods handle connection management, while the public APIs enable message transmission and status reporting required by the Channels UI.

How does AionUi handle incoming messages from custom plugins?

Your plugin receives raw messages through the platform's SDK, converts them to IUnifiedIncomingMessage format using an adapter function, and passes them to this.messageHandler. The PluginManager routes these unified messages to the global ActionExecutor, which processes commands and tool calls uniformly regardless of the source platform.

Where do I register a new chat platform plugin in AionUi?

Register your plugin in src/channels/core/ChannelManager.ts by importing your plugin class and calling registerPlugin('your-type', YourPluginClass) within the constructor. This registration maps the type string to your constructor, allowing PluginManager to instantiate your plugin when loading configurations from the database.

Can I add UI elements for plugin configuration?

Yes. Add UI elements in the Settings page by following the existing patterns used for Telegram or Lark. Store configuration data in the IChannelPluginConfig structure, which your plugin receives during the onInitialize call. You can also implement a static testConnection method to validate credentials before saving the configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →