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

> Learn how to add a new chat platform plugin to AionUi's Channels subsystem by extending BasePlugin. Implement lifecycle methods and register with PluginManager for seamless integration.

- Repository: [OfficeAI/AionUi](https://github.com/iofficeai/aionui)
- Tags: how-to-guide
- Published: 2026-02-16

---

**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:

```bash
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`](https://github.com/iOfficeAI/AionUi/blob/main/src/channels/plugins/index.ts) for convenient imports:

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

```

### 2. Extend BasePlugin and Implement Lifecycle Methods

Create [`MyChatPlugin.ts`](https://github.com/iOfficeAI/AionUi/blob/main/MyChatPlugin.ts) and extend the abstract `BasePlugin` class from [`src/channels/plugins/BasePlugin.ts`](https://github.com/iOfficeAI/AionUi/blob/main/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:

```typescript
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`](https://github.com/iOfficeAI/AionUi/blob/main/src/channels/core/ChannelManager.ts), import your plugin and register it using `registerPlugin()` within the constructor or initialization block (around lines 48-52):

```typescript
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`](https://github.com/iOfficeAI/AionUi/blob/main/src/channels/types.ts) to ensure type safety across the codebase:

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

```

## Complete Code Example

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

```typescript
// 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`](https://github.com/iOfficeAI/AionUi/blob/main/src/channels/core/ChannelManager.ts) follows this pattern:

```typescript
// 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`](https://github.com/iOfficeAI/AionUi/blob/main/src/channels/plugins/telegram/TelegramPlugin.ts) for polling-based implementations or [`src/channels/plugins/lark/LarkPlugin.ts`](https://github.com/iOfficeAI/AionUi/blob/main/src/channels/plugins/lark/LarkPlugin.ts) for WebSocket handling and event deduplication patterns.

## Summary

- **Extend `BasePlugin`** from [`src/channels/plugins/BasePlugin.ts`](https://github.com/iOfficeAI/AionUi/blob/main/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`](https://github.com/iOfficeAI/AionUi/blob/main/src/channels/core/ChannelManager.ts)
- **Update `PluginType`** in [`src/channels/types.ts`](https://github.com/iOfficeAI/AionUi/blob/main/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`](https://github.com/iOfficeAI/AionUi/blob/main/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.