UNPKG

@zhin.js/adapter-onebot11

Version:

zhin adapter for onebot11

415 lines 15.9 kB
import WebSocket from 'ws'; import { EventEmitter } from "events"; import { Adapter, usePlugin, Message, registerAdapter, segment, useContext } from 'zhin.js'; import { clearInterval } from "node:timers"; const plugin = usePlugin(); // ============================================================================ // OneBot11 适配器实现 // ============================================================================ export class OneBot11WsClient extends EventEmitter { $config; $connected; ws; reconnectTimer; heartbeatTimer; requestId = 0; pendingRequests = new Map(); constructor($config) { super(); this.$config = $config; this.$connected = false; } async $connect() { return new Promise((resolve, reject) => { let wsUrl = this.$config.url; const headers = {}; if (this.$config.access_token) { headers['Authorization'] = `Bearer ${this.$config.access_token}`; } this.ws = new WebSocket(wsUrl, { headers }); this.ws.on('open', () => { this.$connected = true; if (!this.$config.access_token) plugin.logger.warn(`missing 'access_token', your OneBot protocol is not safely`); this.startHeartbeat(); resolve(); }); this.ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); this.handleWebSocketMessage(message); } catch (error) { this.emit('error', error); } }); this.ws.on('close', (code, reason) => { this.$connected = false; reject({ code, reason }); this.scheduleReconnect(); }); this.ws.on('error', (error) => { reject(error); }); }); } async $disconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = undefined; } if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = undefined; } // 清理所有待处理的请求 for (const [id, request] of this.pendingRequests) { clearTimeout(request.timeout); request.reject(new Error('Connection closed')); } this.pendingRequests.clear(); if (this.ws) { this.ws.close(); this.ws = undefined; } } $formatMessage(onebotMsg) { const message = Message.from(onebotMsg, { $id: onebotMsg.message_id.toString(), $adapter: 'onebot11', $bot: `${this.$config.name}`, $sender: { id: onebotMsg.user_id.toString(), name: onebotMsg.user_id.toString() }, $channel: { id: (onebotMsg.group_id || onebotMsg.user_id).toString(), type: onebotMsg.group_id ? 'group' : 'private' }, $content: onebotMsg.message, $raw: onebotMsg.raw_message, $timestamp: onebotMsg.time, $recall: async () => { await this.$recallMessage(message.$id); }, $reply: async (content, quote) => { if (quote) content.unshift({ type: 'reply', data: { message_id: message.$id } }); return await this.$sendMessage({ ...message.$channel, context: 'onebot11', bot: `${this.$config.name}`, content }); } }); return message; } async $sendMessage(options) { options = await plugin.app.handleBeforeSend(options); const messageData = { message: options.content }; if (options.type === 'group') { const result = await this.callApi('send_group_msg', { group_id: parseInt(options.id), ...messageData }); plugin.logger.info(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`); return result.message_id.toString(); } else if (options.type === 'private') { const result = await this.callApi('send_private_msg', { user_id: parseInt(options.id), ...messageData }); plugin.logger.info(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`); return result.message_id.toString(); } else { throw new Error('Either group_id or user_id must be provided'); } return ''; } async $recallMessage(id) { await this.callApi('delete_msg', { message_id: parseInt(id) }); } async callApi(action, params = {}) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { throw new Error('WebSocket is not connected'); } const echo = `req_${++this.requestId}`; const message = { action, params, echo }; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(echo); reject(new Error(`API call timeout: ${action}`)); }, 30000); // 30秒超时 this.pendingRequests.set(echo, { resolve, reject, timeout }); this.ws.send(JSON.stringify(message)); }); } handleWebSocketMessage(message) { // 处理API响应 if (message.echo && this.pendingRequests.has(message.echo)) { const request = this.pendingRequests.get(message.echo); this.pendingRequests.delete(message.echo); clearTimeout(request.timeout); const response = message; if (response.status === 'ok') { return request.resolve(response.data); } return request.reject(new Error(`API error: ${response.retcode}`)); } // 处理事件消息 if (message.post_type === 'message') { this.handleOneBot11Message(message); } else if (message.post_type === 'meta_event' && message.meta_event_type === 'heartbeat') { // 心跳消息,暂时忽略 } } handleOneBot11Message(onebotMsg) { const message = this.$formatMessage(onebotMsg); plugin.dispatch('message.receive', message); plugin.logger.info(`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`); plugin.dispatch(`message.${message.$channel.type}.receive`, message); } startHeartbeat() { const interval = this.$config.heartbeat_interval || 30000; this.heartbeatTimer = setInterval(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.ping(); } }, interval); } scheduleReconnect() { if (this.reconnectTimer) { return; } const interval = this.$config.reconnect_interval || 5000; this.reconnectTimer = setTimeout(async () => { this.reconnectTimer = undefined; try { await this.$connect(); } catch (error) { this.emit('error', new Error(`Reconnection failed: ${error}`)); this.scheduleReconnect(); } }, interval); } } export class OneBot11WsServer extends EventEmitter { router; $config; $connected; #wss; #clientMap = new Map(); heartbeatTimer; requestId = 0; pendingRequests = new Map(); constructor(router, $config) { super(); this.router = router; this.$config = $config; this.$connected = false; } async $connect() { if (!this.$config.access_token) plugin.logger.warn(`missing 'access_token', your OneBot protocol is not safely`); this.#wss = this.router.ws(this.$config.path, { verifyClient: (info) => { const { req: { headers }, } = info; const authorization = headers['authorization'] || ''; if (this.$config.access_token && authorization !== `Bearer ${this.$config.access_token}`) { plugin.logger.error('鉴权失败'); return false; } return true; } }); this.$connected = true; plugin.logger.info(`ws server start at path:${this.$config.path}`); this.#wss.on('connection', (client, req) => { this.startHeartbeat(); plugin.logger.info(`已连接到协议端:${req.socket.remoteAddress}`); client.on('error', err => { plugin.logger.error('连接出错:', err); }); client.on('close', code => { plugin.logger.error(`与连接端(${req.socket.remoteAddress})断开,错误码:${code}`); for (const [key, value] of this.#clientMap) { if (client === value) this.#clientMap.delete(key); } }); client.on('message', (data) => { try { const message = JSON.parse(data.toString()); this.handleWebSocketMessage(client, message); } catch (error) { this.emit('error', error); } }); }); } async $disconnect() { this.#wss?.close(); if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); delete this.heartbeatTimer; } } $formatMessage(onebotMsg) { const message = Message.from(onebotMsg, { $id: onebotMsg.message_id.toString(), $adapter: 'onebot11', $bot: `${this.$config.name}`, $sender: { id: onebotMsg.user_id.toString(), name: onebotMsg.user_id.toString() }, $channel: { id: [onebotMsg.self_id, (onebotMsg.group_id || onebotMsg.user_id)].join(':'), type: onebotMsg.group_id ? 'group' : 'private' }, $content: onebotMsg.message, $raw: onebotMsg.raw_message, $timestamp: onebotMsg.time, $recall: async () => { await this.$recallMessage(message.$id); }, $reply: async (content, quote) => { if (!Array.isArray(content)) content = [content]; if (quote) content.unshift({ type: 'reply', data: { message_id: message.$id } }); return await this.$sendMessage({ ...message.$channel, context: 'onebot11', bot: `${this.$config.name}`, content }); } }); return message; } async $sendMessage(options) { options = await plugin.app.handleBeforeSend(options); const messageData = { message: options.content }; if (options.type === 'group') { const [self_id, id] = options.id.split(':'); const result = await this.callApi(self_id, 'send_group_msg', { group_id: parseInt(id), ...messageData }); plugin.logger.info(`${this.$config.name} send ${options.type}(${id}):${segment.raw(options.content)}`); return result.message_id.toString(); } else if (options.type === 'private') { const [self_id, id] = options.id.split(':'); const result = await this.callApi(self_id, 'send_private_msg', { user_id: parseInt(id), ...messageData }); plugin.logger.info(`${this.$config.name} send ${options.type}(${id}):${segment.raw(options.content)}`); return result.message_id.toString(); } else { throw new Error('Either group_id or user_id must be provided'); } return ''; } async $recallMessage(id) { const [self_id, message_id] = id.split(':'); await this.callApi(self_id, 'delete_msg', { message_id: parseInt(message_id) }); } async callApi(self_id, action, params = {}) { const client = this.#clientMap.get(self_id); if (!client || client.readyState !== WebSocket.OPEN) { throw new Error('WebSocket is not connected'); } const echo = `req_${++this.requestId}`; const message = { action, params, echo }; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(echo); reject(new Error(`API call timeout: ${action}`)); }, 30000); // 30秒超时 this.pendingRequests.set(echo, { resolve, reject, timeout }); client.send(JSON.stringify(message)); }); } handleWebSocketMessage(client, message) { // 处理API响应 if (message.echo && this.pendingRequests.has(message.echo)) { const request = this.pendingRequests.get(message.echo); this.pendingRequests.delete(message.echo); clearTimeout(request.timeout); const response = message; if (response.status === 'ok') { return request.resolve(response.data); } return request.reject(new Error(`API error: ${response.retcode}`)); } switch (message.post_type) { case 'message': return this.handleMessage(message); case 'meta_event': return this.handleMetaEvent(client, message); } // 处理事件消息 if (message.post_type === 'message') { } else if (message.post_type === 'meta_event' && message.meta_event_type === 'heartbeat') { // 心跳消息,暂时忽略 } } handleMetaEvent(client, message) { switch (message.sub_type) { case 'heartbeat': break; case 'connect': this.#clientMap.set(message.self_id, client); plugin.logger.info(`client ${message.self_id} of ${this.$config.name} by ${this.$config.context} connected`); break; } } handleMessage(onebotMsg) { const message = this.$formatMessage(onebotMsg); plugin.dispatch('message.receive', message); plugin.logger.info(`${this.$config.name} recv ${message.$channel.type}(${onebotMsg.group_id || onebotMsg.user_id}):${segment.raw(message.$content)}`); plugin.dispatch(`message.${message.$channel.type}.receive`, message); } startHeartbeat() { const interval = this.$config.heartbeat_interval || 30000; this.heartbeatTimer = setInterval(() => { for (const client of this.#wss?.clients || []) { if (client && client.readyState === WebSocket.OPEN) { client.ping(); } } }, interval); } } registerAdapter(new Adapter('onebot11', OneBot11WsClient)); useContext('router', (router) => { registerAdapter(new Adapter('onebot11.wss', (c) => new OneBot11WsServer(router, c))); }); //# sourceMappingURL=index.js.map