🔌 适配器开发指南
本指南将帮助你为 Zhin.js 开发新的平台适配器,连接各种聊天平台。
生态说明:Zhin.js 开箱即用支持控制台适配器、HTTP 服务、Web 控制台、SQLite 数据库。Telegram、Discord、QQ、KOOK、OneBot v11、MySQL、PostgreSQL 等需手动安装扩展包。建议适配器开发优先兼容主仓库内置平台,跨平台请注明依赖。
🎯 适配器核心概念
适配器是连接不同聊天平台的桥梁,是 Zhin 多平台支持的核心。一个优秀的适配器应该:
- 🔗 平台连接管理 - 稳定的连接建立和维护
- 💬 消息双向传输 - 接收和发送消息的完整支持
- 📡 事件处理 - 处理平台特有的事件类型
- 🔄 会话管理 - 维护用户和群组的会话状态
- 🛡️ 错误恢复 - 自动重连和异常处理
🏗️ 适配器架构
mermaid
graph TD
A[聊天平台] --> B[适配器]
B --> C[Bot实例]
C --> D[消息转换]
D --> E[Zhin核心]
E --> F[插件系统]
F --> G[消息处理]
G --> H[回复消息]
H --> D
D --> C
C --> B
B --> A
📁 现代适配器结构
基于实际项目的适配器实现:
typescript
import {
Adapter,
Bot,
BotConfig,
SendOptions,
Plugin
} from 'zhin.js'
// 🔧 Bot配置接口
interface MyBotConfig extends BotConfig {
name: string // 机器人名称
context: string // 适配器上下文名
token: string // 平台访问令牌
endpoint?: string // 可选的API端点
options?: {
reconnect?: boolean // 是否自动重连
timeout?: number // 连接超时时间
}
}
// 🤖 Bot实现
class MyBot implements Bot<MyBotConfig> {
public connected = false
private client: any
constructor(
private plugin: Plugin,
public config: MyBotConfig
) {}
async connect() {
try {
// 🔗 建立平台连接
this.client = await this.createConnection()
this.setupEventHandlers()
this.connected = true
this.plugin.logger.info(`机器人 ${this.config.name} 连接成功`)
} catch (error) {
this.plugin.logger.error('连接失败:', error)
throw error
}
}
async disconnect() {
try {
if (this.client) {
await this.client.disconnect()
}
this.connected = false
this.plugin.logger.info(`机器人 ${this.config.name} 已断开连接`)
} catch (error) {
this.plugin.logger.error('断开连接失败:', error)
}
}
async sendMessage(options: SendOptions) {
if (!this.connected) {
throw new Error('机器人未连接')
}
try {
// 🔄 转换消息格式
const platformMessage = this.convertToPlatformFormat(options)
// 📤 发送消息
await this.client.sendMessage(platformMessage)
this.plugin.logger.debug('消息发送成功:', options)
} catch (error) {
this.plugin.logger.error('消息发送失败:', error)
throw error
}
}
// 🔧 私有方法
private async createConnection() {
// 实现具体的连接逻辑
return await createPlatformClient(this.config)
}
private setupEventHandlers() {
// 📡 设置事件监听
this.client.on('message', this.handleMessage.bind(this))
this.client.on('error', this.handleError.bind(this))
this.client.on('disconnect', this.handleDisconnect.bind(this))
}
private handleMessage(rawMessage: any) {
// 💬 处理收到的消息
const message = this.convertFromPlatformFormat(rawMessage)
// 🎯 触发消息事件
this.plugin.emit('message.receive', message)
// 📋 根据消息类型触发特定事件
if (message.type === 'group') {
this.plugin.emit('message.group.receive', message)
} else {
this.plugin.emit('message.private.receive', message)
}
}
private handleError(error: any) {
this.plugin.logger.error('平台错误:', error)
// 🔄 自动重连逻辑
if (this.config.options?.reconnect && this.shouldReconnect(error)) {
this.reconnect()
}
}
private async reconnect() {
try {
await this.disconnect()
await new Promise(resolve => setTimeout(resolve, 5000)) // 等待5秒
await this.connect()
} catch (error) {
this.plugin.logger.error('重连失败:', error)
}
}
}
// 🔌 适配器实现
export class MyAdapter extends Adapter {
constructor() {
super('my-platform', (plugin, config) => new MyBot(plugin, config))
}
// 可以重写适配器方法来自定义行为
async start() {
await super.start()
this.plugin.logger.info(`${this.name} 适配器启动完成`)
}
async stop() {
await super.stop()
this.plugin.logger.info(`${this.name} 适配器已停止`)
}
}
🎯 实际适配器示例
📱 基于现有适配器的学习
让我们看看项目中已有的适配器实现:
1️⃣ Process 适配器(控制台交互)
typescript
// 基于 adapters/process/index.ts
import { Adapter, Bot, BotConfig, SendOptions } from 'zhin.js'
interface ProcessBotConfig extends BotConfig {
name: string
context: 'process'
}
class ProcessBot implements Bot<ProcessBotConfig> {
connected = false
constructor(
private plugin: Plugin,
public config: ProcessBotConfig
) {}
async connect() {
this.connected = true
// 🎯 监听控制台输入
process.stdin.on('data', (data) => {
const content = data.toString().trim()
if (content) {
this.handleConsoleInput(content)
}
})
this.plugin.logger.info('控制台机器人已就绪,可以直接输入消息')
}
async disconnect() {
this.connected = false
process.stdin.removeAllListeners('data')
}
async sendMessage(options: SendOptions) {
// 🖨️ 输出到控制台
console.log(`[${new Date().toLocaleTimeString()}] ${options.content}`)
}
private handleConsoleInput(content: string) {
const message = {
id: Date.now().toString(),
type: 'private' as const,
raw: content,
content: [{ type: 'text', data: { text: content } }],
sender: {
id: 'console-user',
name: '控制台用户'
},
reply: async (replyContent: string) => {
await this.sendMessage({
context: this.config.context,
bot: this.config.name,
id: 'console',
type: 'private',
content: replyContent
})
}
}
// 触发消息事件
this.plugin.emit('message.receive', message)
}
}
export class ProcessAdapter extends Adapter {
constructor() {
super('process', (plugin, config) => new ProcessBot(plugin, config))
}
}
2️⃣ WebSocket 适配器(OneBot v11)
typescript
// 基于 adapters/onebot11/index.ts
import WebSocket from 'ws'
interface OneBot11Config extends BotConfig {
name: string
context: 'onebot11'
url: string
access_token?: string
}
class OneBot11Bot implements Bot<OneBot11Config> {
private ws?: WebSocket
private heartbeatInterval?: NodeJS.Timeout
connected = false
async connect() {
const wsUrl = new URL('/ws', this.config.url)
if (this.config.access_token) {
wsUrl.searchParams.set('access_token', this.config.access_token)
}
this.ws = new WebSocket(wsUrl.toString())
this.ws.on('open', () => {
this.connected = true
this.startHeartbeat()
this.plugin.logger.info('OneBot WebSocket 连接成功')
})
this.ws.on('message', (data) => {
try {
const payload = JSON.parse(data.toString())
this.handleWebSocketMessage(payload)
} catch (error) {
this.plugin.logger.error('消息解析失败:', error)
}
})
this.ws.on('close', () => {
this.connected = false
this.stopHeartbeat()
// 🔄 自动重连
setTimeout(() => {
if (!this.connected) {
this.connect()
}
}, 5000)
})
}
async sendMessage(options: SendOptions) {
if (!this.ws || !this.connected) {
throw new Error('WebSocket 未连接')
}
const apiCall = {
action: options.type === 'group' ? 'send_group_msg' : 'send_private_msg',
params: {
[options.type === 'group' ? 'group_id' : 'user_id']: options.id,
message: options.content
}
}
this.ws.send(JSON.stringify(apiCall))
}
private startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws && this.connected) {
this.ws.ping()
}
}, 30000)
}
private stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
}
}
}
3️⃣ HTTP API 适配器(KOOK)
typescript
// 基于 adapters/kook/index.ts
import { Client as KookClient } from 'kook-client'
interface KookBotConfig extends BotConfig {
name: string
context: 'kook'
token: string
mode: 'websocket' | 'webhook'
logLevel?: 'off' | 'info' | 'debug'
ignore?: 'bot' | 'none'
}
class KookBot implements Bot<KookBotConfig> {
private client: KookClient
connected = false
constructor(
private plugin: Plugin,
public config: KookBotConfig
) {
this.client = new KookClient({
token: config.token,
mode: config.mode,
logLevel: config.logLevel || 'info'
})
}
async connect() {
// 🎯 设置事件监听
this.client.on('message', this.handleKookMessage.bind(this))
this.client.on('ready', () => {
this.connected = true
this.plugin.logger.info(`KOOK机器人 ${this.config.name} 已就绪`)
})
// 🔗 连接到KOOK
await this.client.connect()
}
async disconnect() {
await this.client.disconnect()
this.connected = false
}
async sendMessage(options: SendOptions) {
const channel = options.type === 'group' ? options.id : null
const userId = options.type === 'private' ? options.id : null
if (channel) {
await this.client.sendChannelMessage(channel, options.content)
} else if (userId) {
await this.client.sendDirectMessage(userId, options.content)
}
}
private handleKookMessage(kookMsg: any) {
// 🔄 转换KOOK消息格式
const message = {
id: kookMsg.msgId,
type: kookMsg.channelType === 'GROUP' ? 'group' : 'private',
raw: kookMsg.content,
content: [{ type: 'text', data: { text: kookMsg.content } }],
sender: {
id: kookMsg.authorId,
name: kookMsg.author?.nickname || '未知用户'
},
channel: kookMsg.channelType === 'GROUP' ? {
id: kookMsg.targetId,
name: kookMsg.extra?.guild?.name || '未知频道'
} : undefined
}
this.plugin.emit('message.receive', message)
}
}
🚀 适配器最佳实践
💡 错误处理策略
typescript
import { useLogger } from 'zhin.js'
class MyBot implements Bot {
private logger = useLogger()
// 🛡️ 安全调用包装器
private async safeCall<T>(
action: () => Promise<T>,
errorMessage: string,
fallback?: T
): Promise<T> {
try {
return await action()
} catch (error) {
this.logger.error(`${errorMessage}:`, error)
if (fallback !== undefined) {
return fallback
}
throw error
}
}
// 🔄 重试机制
private async withRetry<T>(
action: () => Promise<T>,
maxRetries = 3,
delay = 1000
): Promise<T> {
let lastError: Error
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await action()
} catch (error) {
lastError = error as Error
if (attempt === maxRetries) {
throw lastError
}
// 指数退避延迟
const backoffDelay = delay * Math.pow(2, attempt - 1)
await new Promise(resolve => setTimeout(resolve, backoffDelay))
this.logger.warn(`第 ${attempt} 次尝试失败,${backoffDelay}ms 后重试:`, error)
}
}
throw lastError!
}
async sendMessage(options: SendOptions) {
return this.withRetry(
() => this.safeCall(
() => this.client.sendMessage(options),
'发送消息失败'
),
3,
1000
)
}
}
🧹 资源管理
typescript
class MyBot implements Bot {
private cleanupTasks: (() => Promise<void>)[] = []
private timers: NodeJS.Timeout[] = []
private connections: any[] = []
async connect() {
// 创建连接
const connection = await this.createConnection()
this.connections.push(connection)
// 注册清理任务
this.addCleanupTask(async () => {
await connection.close()
})
// 启动心跳
const heartbeat = setInterval(() => {
this.sendHeartbeat()
}, 30000)
this.timers.push(heartbeat)
}
async disconnect() {
// 清理定时器
this.timers.forEach(timer => clearInterval(timer))
this.timers = []
// 执行所有清理任务
await Promise.allSettled(
this.cleanupTasks.map(task => task())
)
this.cleanupTasks = []
// 关闭连接
await Promise.allSettled(
this.connections.map(conn => conn.close())
)
this.connections = []
this.connected = false
}
private addCleanupTask(task: () => Promise<void>) {
this.cleanupTasks.push(task)
}
}
🔧 配置验证
typescript
import { z } from 'zod'
// 配置模式定义
const BotConfigSchema = z.object({
name: z.string().min(1),
token: z.string().min(1),
endpoint: z.string().url().optional(),
options: z.object({
reconnect: z.boolean().default(true),
timeout: z.number().min(1000).default(5000),
retries: z.number().min(1).max(10).default(3)
}).default({})
})
class MyBot implements Bot {
private validatedConfig: z.infer<typeof BotConfigSchema>
constructor(plugin: Plugin, config: any) {
// ✅ 验证配置
this.validatedConfig = BotConfigSchema.parse(config)
this.plugin = plugin
}
async connect() {
const { token, endpoint, options } = this.validatedConfig
// 使用验证过的配置
this.client = await createClient(token, {
endpoint,
timeout: options.timeout,
reconnect: options.reconnect
})
}
}
📊 性能监控
typescript
class MyBot implements Bot {
private stats = {
messagesReceived: 0,
messagesSent: 0,
errors: 0,
connectionUptime: Date.now()
}
async sendMessage(options: SendOptions) {
const start = Date.now()
try {
await this.client.sendMessage(options)
this.stats.messagesSent++
const duration = Date.now() - start
this.logger.debug(`消息发送成功 (${duration}ms)`)
} catch (error) {
this.stats.errors++
throw error
}
}
private handleMessage(message: any) {
this.stats.messagesReceived++
// 处理消息...
this.plugin.emit('message.receive', convertedMessage)
}
getStats() {
return {
...this.stats,
uptime: Date.now() - this.stats.connectionUptime
}
}
}
🎯 适配器注册和使用
📝 注册适配器
typescript
// adapters/my-platform/index.ts
export { MyAdapter as default } from './adapter'
// 或者在插件中注册
import { registerAdapter } from 'zhin.js'
import { MyAdapter } from './my-adapter'
registerAdapter(new MyAdapter())
⚙️ 配置使用
typescript
// zhin.config.ts
export default defineConfig(async (env) => {
return {
bots: [
{
name: 'my-bot',
context: 'my-platform', // 对应适配器名称
token: env.MY_PLATFORM_TOKEN,
endpoint: env.MY_PLATFORM_ENDPOINT,
options: {
reconnect: true,
timeout: 10000
}
}
],
plugins: [
'adapter-my-platform', // 启用适配器插件
'my-other-plugins'
]
}
})
🧪 测试适配器
单元测试示例
typescript
// tests/adapter.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { MyAdapter, MyBot } from '../src/adapter'
describe('MyAdapter', () => {
let adapter: MyAdapter
let mockPlugin: any
beforeEach(() => {
mockPlugin = {
logger: {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn()
},
emit: vi.fn()
}
adapter = new MyAdapter()
})
it('should create bot instance correctly', () => {
const config = {
name: 'test-bot',
context: 'my-platform',
token: 'test-token'
}
const bot = adapter.createBot(mockPlugin, config)
expect(bot).toBeInstanceOf(MyBot)
expect(bot.config).toEqual(config)
})
it('should handle connection success', async () => {
const bot = adapter.createBot(mockPlugin, {
name: 'test-bot',
context: 'my-platform',
token: 'valid-token'
})
await bot.connect()
expect(bot.connected).toBe(true)
expect(mockPlugin.logger.info).toHaveBeenCalledWith(
expect.stringContaining('连接成功')
)
})
it('should handle connection failure', async () => {
const bot = adapter.createBot(mockPlugin, {
name: 'test-bot',
context: 'my-platform',
token: 'invalid-token'
})
await expect(bot.connect()).rejects.toThrow()
expect(mockPlugin.logger.error).toHaveBeenCalled()
})
})
🌍 生态系统与扩展
� 开箱即用
- 控制台适配器(@zhin.js/adapter-process,默认内置)
- HTTP 服务(@zhin.js/http)
- Web 控制台(@zhin.js/console)
- SQLite 数据库(默认)
🔌 可选扩展(需手动安装)
- Telegram(@zhin.js/adapter-telegram)
- Discord(@zhin.js/adapter-discord)
- QQ(@zhin.js/adapter-qq)
- KOOK(@zhin.js/adapter-kook)
- OneBot v11(@zhin.js/adapter-onebot11)
- MySQL(@zhin.js/database-mysql)
- PostgreSQL(@zhin.js/database-pg)
�📚 更多资源
🎉 恭喜! 你现在已经掌握了 Zhin 适配器开发的完整技能,可以为任何聊天平台创建高质量的适配器了!