docs/development/basic/add-new-bot-platform.mdx
This guide walks through the steps to add a new bot platform to LobeHub's channel system. The platform architecture is modular — each platform is a self-contained directory under src/server/services/bot/platforms/.
src/server/services/bot/platforms/
├── types.ts # Core interfaces (FieldSchema, PlatformClient, ClientFactory, etc.)
├── registry.ts # PlatformRegistry class
├── index.ts # Singleton registry + platform registration
├── utils.ts # Shared utilities
├── discord/ # Example: Discord platform
│ ├── definition.ts # PlatformDefinition export
│ ├── schema.ts # FieldSchema[] for credentials & settings
│ ├── client.ts # ClientFactory + PlatformClient implementation
│ └── api.ts # Platform API helper class
└── <your-platform>/ # Your new platform
Key concepts:
Each platform requires a Chat SDK adapter that bridges the platform's webhook events into the unified Vercel Chat SDK (chat npm package). Before implementing the platform, determine which adapter to use:
Some platforms have official adapters published under @chat-adapter/*:
@chat-adapter/discord — Discord@chat-adapter/slack — Slack@chat-adapter/telegram — TelegramCheck npm with npm view @chat-adapter/<platform> to see if one exists.
packages/If no npm adapter exists, you need to create one as a workspace package. Reference the existing implementations:
packages/chat-adapter-feishu — Feishu/Lark adapter (@lobechat/chat-adapter-feishu)packages/chat-adapter-qq — QQ adapter (@lobechat/chat-adapter-qq)Each adapter package follows this structure:
packages/chat-adapter-<platform>/
├── package.json # name: @lobechat/chat-adapter-<platform>
├── tsconfig.json
├── tsup.config.ts
└── src/
├── index.ts # Public exports: createXxxAdapter, XxxApiClient, etc.
├── adapter.ts # Adapter class implementing chat SDK's Adapter interface
├── api.ts # Platform API client (webhook verification, message parsing)
├── crypto.ts # Request signature verification
├── format-converter.ts # Message format conversion (platform format ↔ chat SDK AST)
└── types.ts # Platform-specific type definitions
Key points for developing a custom adapter:
Adapter interface from the chat packagecreateXxxAdapter(config) factory function is what PlatformClient.createAdapter() will call"chat": "^4.14.0" as a dependency in package.jsonmkdir src/server/services/bot/platforms/<platform-name>
You will create four files:
| File | Purpose |
|---|---|
schema.ts | Credential and settings field definitions |
api.ts | Lightweight API client for outbound messaging |
client.ts | ClientFactory + PlatformClient implementation |
definition.ts | PlatformDefinition export |
schema.ts)The schema is an array of FieldSchema objects with two top-level sections: credentials and settings.
import type { FieldSchema } from '../types';
export const schema: FieldSchema[] = [
{
key: 'credentials',
label: 'channel.credentials',
properties: [
{
key: 'applicationId',
description: 'channel.applicationIdHint',
label: 'channel.applicationId',
required: true,
type: 'string',
},
{
key: 'botToken',
description: 'channel.botTokenEncryptedHint',
label: 'channel.botToken',
required: true,
type: 'password', // Encrypted in storage, masked in UI
},
],
type: 'object',
},
{
key: 'settings',
label: 'channel.settings',
properties: [
{
key: 'charLimit',
default: 4000,
description: 'channel.charLimitHint',
label: 'channel.charLimit',
minimum: 100,
type: 'number',
},
// Add platform-specific settings...
],
type: 'object',
},
];
Schema conventions:
type: 'password' fields are encrypted at rest and masked in the formchannel.botToken, channel.charLimit) for shared fieldschannel.<platform>.<key> for platform-specific i18n keysdevOnly: true fields only appear when NODE_ENV === 'development'applicationId — either an explicit applicationId field, an appId field, or a botToken from which the ID is derived (see resolveApplicationId in the channel detail page)api.ts)A lightweight class for outbound messaging operations used by the callback service (outside the Chat SDK adapter):
import debug from 'debug';
const log = debug('bot-platform:<platform>:client');
export const API_BASE = 'https://api.example.com';
export class PlatformApi {
private readonly token: string;
constructor(token: string) {
this.token = token;
}
async sendMessage(channelId: string, text: string): Promise<{ id: string }> {
log('sendMessage: channel=%s', channelId);
return this.call('messages.send', { channel: channelId, text });
}
async editMessage(channelId: string, messageId: string, text: string): Promise<void> {
log('editMessage: channel=%s, message=%s', channelId, messageId);
await this.call('messages.update', { channel: channelId, id: messageId, text });
}
// ... other operations (typing indicator, reactions, etc.)
private async call(method: string, body: Record<string, unknown>): Promise<any> {
const response = await fetch(`${API_BASE}/${method}`, {
body: JSON.stringify(body),
headers: {
Authorization: `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
method: 'POST',
});
if (!response.ok) {
const text = await response.text();
log('API error: method=%s, status=%d, body=%s', method, response.status, text);
throw new Error(`API ${method} failed: ${response.status} ${text}`);
}
return response.json();
}
}
client.ts)Implement PlatformClient and extend ClientFactory:
import { createPlatformAdapter } from '@chat-adapter/<platform>';
import debug from 'debug';
import {
type BotPlatformRuntimeContext,
type BotProviderConfig,
ClientFactory,
type PlatformClient,
type PlatformMessenger,
type ValidationResult,
} from '../types';
import { PlatformApi } from './api';
const log = debug('bot-platform:<platform>:bot');
class MyPlatformClient implements PlatformClient {
readonly id = '<platform>';
readonly applicationId: string;
private config: BotProviderConfig;
private context: BotPlatformRuntimeContext;
constructor(config: BotProviderConfig, context: BotPlatformRuntimeContext) {
this.config = config;
this.context = context;
this.applicationId = config.applicationId;
}
// --- Lifecycle ---
async start(): Promise<void> {
// Register webhook or start listening
// For webhook platforms: configure the webhook URL with the platform API
// For gateway platforms: open a persistent connection
}
async stop(): Promise<void> {
// Cleanup: remove webhook registration or close connection
}
// --- Runtime Operations ---
createAdapter(): Record<string, any> {
// Return a Chat SDK adapter instance for inbound message handling
return {
'<platform>': createPlatformAdapter({
botToken: this.config.credentials.botToken,
// ... adapter-specific config
}),
};
}
getMessenger(platformThreadId: string): PlatformMessenger {
const api = new PlatformApi(this.config.credentials.botToken);
const channelId = platformThreadId.split(':')[1];
return {
createMessage: (content) => api.sendMessage(channelId, content).then(() => {}),
editMessage: (messageId, content) => api.editMessage(channelId, messageId, content),
removeReaction: (messageId, emoji) => api.removeReaction(channelId, messageId, emoji),
triggerTyping: () => Promise.resolve(),
};
}
extractChatId(platformThreadId: string): string {
return platformThreadId.split(':')[1];
}
parseMessageId(compositeId: string): string {
return compositeId;
}
// --- Optional methods ---
// sanitizeUserInput(text: string): string { ... }
// shouldSubscribe(threadId: string): boolean { ... }
// formatReply(body: string, stats?: UsageStats): string { ... }
}
export class MyPlatformClientFactory extends ClientFactory {
createClient(config: BotProviderConfig, context: BotPlatformRuntimeContext): PlatformClient {
return new MyPlatformClient(config, context);
}
async validateCredentials(credentials: Record<string, string>): Promise<ValidationResult> {
// Call the platform API to verify the credentials are valid
try {
const res = await fetch('https://api.example.com/auth.test', {
headers: { Authorization: `Bearer ${credentials.botToken}` },
method: 'POST',
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return { valid: true };
} catch {
return {
errors: [{ field: 'botToken', message: 'Failed to authenticate' }],
valid: false,
};
}
}
}
Key interfaces to implement:
| Method | Purpose |
|---|---|
start() | Register webhook or start gateway listener |
stop() | Clean up resources on shutdown |
createAdapter() | Return Chat SDK adapter for inbound event handling |
getMessenger() | Return outbound messaging interface for a thread |
extractChatId() | Parse platform channel ID from composite thread ID |
parseMessageId() | Convert composite message ID to platform-native format |
sanitizeUserInput() | (Optional) Strip bot mention artifacts from user input |
shouldSubscribe() | (Optional) Control thread auto-subscription behavior |
formatReply() | (Optional) Append platform-specific formatting to replies |
definition.ts)import type { PlatformDefinition } from '../types';
import { MyPlatformClientFactory } from './client';
import { schema } from './schema';
export const myPlatform: PlatformDefinition = {
id: '<platform>',
name: 'Platform Name',
connectionMode: 'webhook', // 'webhook' | 'websocket' | 'polling'
description: 'Connect a Platform bot',
documentation: {
portalUrl: 'https://developers.example.com',
setupGuideUrl: 'https://lobehub.com/docs/usage/channels/<platform>',
},
schema,
showWebhookUrl: true, // Set to true if users need to manually copy the webhook URL
clientFactory: new MyPlatformClientFactory(),
};
showWebhookUrl: Set to true for platforms where the user must manually paste a webhook URL (e.g., Slack, Feishu). Set to false (or omit) for platforms that auto-register webhooks via API (e.g., Telegram).
Edit src/server/services/bot/platforms/index.ts:
import { myPlatform } from './<platform>/definition';
// Add to exports
export { myPlatform } from './<platform>/definition';
// Register
platformRegistry.register(myPlatform);
src/locales/default/agent.ts)Add platform-specific keys. Reuse generic keys where possible:
// Reusable (already exist):
// 'channel.botToken', 'channel.applicationId', 'channel.charLimit', etc.
// Platform-specific:
'channel.<platform>.description': 'Connect this assistant to Platform for ...',
'channel.<platform>.someFieldHint': 'Description of this field.',
locales/zh-CN/agent.json, locales/en-US/agent.json)Add corresponding translations for all new keys in both locale files.
Create setup guides in docs/usage/channels/:
<platform>.mdx — English guide<platform>.zh-CN.mdx — Chinese guideFollow the structure of existing docs (e.g., discord.mdx): Prerequisites → Create App → Configure in LobeHub → Configure Webhooks → Test Connection → Configuration Reference → Troubleshooting.
The frontend automatically generates the configuration form from the schema. No frontend code changes are needed unless your platform requires a custom icon. The icon resolution works by matching the platform name against known icons in @lobehub/ui/icons:
// src/routes/(main)/agent/channel/const.ts
const ICON_NAMES = ['Discord', 'GoogleChat', 'Lark', 'Slack', 'Telegram', ...];
If your platform's name matches an icon name (case-insensitive), the icon is used automatically. Otherwise, add an alias in ICON_ALIASES.
All platforms share the same webhook route:
POST /api/agent/webhooks/[platform]/[appId]
The BotMessageRouter handles routing, on-demand bot loading, and Chat SDK integration automatically.
@chat-adapter/* on npm or custom packages/chat-adapter-<platform>)src/server/services/bot/platforms/<platform>/
schema.ts — Field definitions for credentials and settingsapi.ts — Outbound API clientclient.ts — ClientFactory + PlatformClientdefinition.ts — PlatformDefinition exportsrc/server/services/bot/platforms/index.tssrc/locales/default/agent.tslocales/zh-CN/agent.json and locales/en-US/agent.jsondocs/usage/channels/<platform>.mdx (en + zh-CN)const.ts (or add alias)