@zhin.js/adapter-onebot11 4.0.1 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +40 -370
  3. package/adapters/onebot11.ts +39 -0
  4. package/agent/tools/set_title.ts +21 -0
  5. package/lib/index.d.ts +7 -16
  6. package/lib/index.js +6 -56
  7. package/lib/onebot11-agent-deps.d.ts +15 -0
  8. package/lib/onebot11-agent-deps.js +30 -0
  9. package/lib/protocol.d.ts +144 -0
  10. package/lib/protocol.js +235 -0
  11. package/lib/ws-endpoint.d.ts +28 -0
  12. package/lib/ws-endpoint.js +210 -0
  13. package/lib/ws-transport.d.ts +13 -0
  14. package/lib/ws-transport.js +74 -0
  15. package/lib/ws-types.d.ts +17 -0
  16. package/lib/ws-types.js +1 -0
  17. package/lib/wss-auth.d.ts +2 -0
  18. package/lib/wss-auth.js +16 -0
  19. package/lib/wss-endpoint.d.ts +26 -0
  20. package/lib/wss-endpoint.js +143 -0
  21. package/package.json +52 -22
  22. package/plugin.ts +13 -0
  23. package/schema.json +36 -0
  24. package/src/index.ts +54 -68
  25. package/src/onebot11-agent-deps.ts +49 -0
  26. package/src/protocol.ts +370 -0
  27. package/src/ws-endpoint.ts +266 -0
  28. package/src/ws-transport.ts +101 -0
  29. package/src/ws-types.ts +20 -0
  30. package/src/wss-auth.ts +17 -0
  31. package/src/wss-endpoint.ts +189 -0
  32. package/lib/adapter.d.ts +0 -41
  33. package/lib/adapter.d.ts.map +0 -1
  34. package/lib/adapter.js +0 -101
  35. package/lib/adapter.js.map +0 -1
  36. package/lib/endpoint-ws-client.d.ts +0 -41
  37. package/lib/endpoint-ws-client.d.ts.map +0 -1
  38. package/lib/endpoint-ws-client.js +0 -408
  39. package/lib/endpoint-ws-client.js.map +0 -1
  40. package/lib/endpoint-ws-server.d.ts +0 -43
  41. package/lib/endpoint-ws-server.d.ts.map +0 -1
  42. package/lib/endpoint-ws-server.js +0 -355
  43. package/lib/endpoint-ws-server.js.map +0 -1
  44. package/lib/index.d.ts.map +0 -1
  45. package/lib/index.js.map +0 -1
  46. package/lib/onebot-get-msg.d.ts +0 -3
  47. package/lib/onebot-get-msg.d.ts.map +0 -1
  48. package/lib/onebot-get-msg.js +0 -27
  49. package/lib/onebot-get-msg.js.map +0 -1
  50. package/lib/segment-mapper.d.ts +0 -2
  51. package/lib/segment-mapper.d.ts.map +0 -1
  52. package/lib/segment-mapper.js +0 -2
  53. package/lib/segment-mapper.js.map +0 -1
  54. package/lib/types.d.ts +0 -61
  55. package/lib/types.d.ts.map +0 -1
  56. package/lib/types.js +0 -5
  57. package/lib/types.js.map +0 -1
  58. package/plugin.yml +0 -3
  59. package/src/adapter.ts +0 -114
  60. package/src/endpoint-ws-client.ts +0 -435
  61. package/src/endpoint-ws-server.ts +0 -396
  62. package/src/onebot-get-msg.ts +0 -33
  63. package/src/segment-mapper.ts +0 -1
  64. package/src/types.ts +0 -60
  65. /package/{skills/onebot11 → agent}/PERMITS.md +0 -0
  66. /package/{skills/onebot11/SKILL.md → agent/skills/onebot11.md} +0 -0
@@ -1,435 +0,0 @@
1
- /**
2
- * OneBot11 WebSocket 客户端连接 Endpoint
3
- */
4
- import WebSocket from 'ws';
5
- import { EventEmitter } from 'events';
6
- import { clearInterval } from 'node:timers';
7
- import { formatCompact, Endpoint, Message, Notice, Request, segment, SendContent, SendOptions, expandInteractiveSegmentsInContent, type QuotedMessagePayload, applyQqSenderRoleToMessageSender, type MessageElement,} from 'zhin.js';
8
- import { parseOneBotGetMsgResponse } from './onebot-get-msg.js';
9
- import { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
10
- import type {
11
- OneBot11WsClientConfig,
12
- OneBot11Message,
13
- ApiResponse,
14
- } from './types.js';
15
- import type { OneBot11Adapter } from './adapter.js';
16
-
17
-
18
- export class OneBot11WsClient extends EventEmitter implements Endpoint<OneBot11WsClientConfig, OneBot11Message> {
19
- $connected: boolean;
20
- private ws?: WebSocket;
21
- private reconnectTimer?: NodeJS.Timeout;
22
- private heartbeatTimer?: NodeJS.Timeout;
23
- private requestId = 0;
24
- private pendingRequests = new Map<string, {
25
- resolve: (value: any) => void;
26
- reject: (error: Error) => void;
27
- timeout: NodeJS.Timeout;
28
- }>();
29
- get logger() {
30
- return this.adapter.plugin.logger;
31
- }
32
-
33
- constructor(public adapter: OneBot11Adapter, public $config: OneBot11WsClientConfig) {
34
- super();
35
- this.$connected = false;
36
- }
37
-
38
- get $id() {
39
- return this.$config.name;
40
- }
41
-
42
- async $connect(): Promise<void> {
43
- return new Promise((resolve, reject) => {
44
- const headers: Record<string, string> = {};
45
- if (this.$config.access_token) {
46
- headers['Authorization'] = `Bearer ${this.$config.access_token}`;
47
- const url=new URL(this.$config.url);
48
- url.searchParams.set('access_token', this.$config.access_token);
49
- this.$config.url = url.toString();
50
- }
51
- this.ws = new WebSocket(this.$config.url, { headers });
52
-
53
- this.ws.on('open', () => {
54
- this.$connected = true;
55
- if (!this.$config.access_token) {
56
- this.logger.warn(formatCompact({ endpoint: this.$id, ok: false, error: 'missing access_token' }));
57
- }
58
- this.logger.info(formatCompact({ endpoint: this.$id, mode: 'ws' }));
59
- this.startHeartbeat();
60
- resolve();
61
- });
62
-
63
- this.ws.on('message', (data) => {
64
- try {
65
- const message = JSON.parse(data.toString());
66
- this.handleWebSocketMessage(message);
67
- } catch (error) {
68
- this.emit('error', error);
69
- }
70
- });
71
-
72
- this.ws.on('close', (code, reason) => {
73
- this.$connected = false;
74
- const reasonStr = reason?.toString?.() || String(reason);
75
- const codeHint = code === 1005 ? ' [无状态,多为服务端/代理未发 close 帧即断开]' : code === 1006 ? ' [异常关闭]' : '';
76
- this.logger.warn(formatCompact( {
77
- op: 'disconnect',
78
- endpoint: this.$config.name,
79
- code,
80
- error: `${reasonStr || 'closed'}${codeHint}`,
81
- reconnect_ms: this.$config.reconnect_interval || 5000,
82
- }));
83
- reject({ code, reason });
84
- this.scheduleReconnect();
85
- });
86
-
87
- this.ws.on('error', (error) => {
88
- this.logger.warn(formatCompact( {
89
- op: 'ws_error',
90
- endpoint: this.$config.name,
91
- ok: false,
92
- error: error instanceof Error ? error.message : String(error),
93
- }));
94
- reject(error);
95
- });
96
- });
97
- }
98
-
99
- async $disconnect(): Promise<void> {
100
- if (this.reconnectTimer) {
101
- clearTimeout(this.reconnectTimer);
102
- this.reconnectTimer = undefined;
103
- }
104
- if (this.heartbeatTimer) {
105
- clearInterval(this.heartbeatTimer);
106
- this.heartbeatTimer = undefined;
107
- }
108
- for (const [, request] of this.pendingRequests) {
109
- clearTimeout(request.timeout);
110
- request.reject(new Error('Connection closed'));
111
- }
112
- this.pendingRequests.clear();
113
- if (this.ws) {
114
- this.ws.close();
115
- this.ws = undefined;
116
- }
117
- }
118
-
119
- private toWireSendContent(content: SendContent): MessageElement[] {
120
- const items = (Array.isArray(content) ? content : [content]).map((item) =>
121
- typeof item === 'string' ? { type: 'text', data: { text: item } } : item as MessageElement,
122
- );
123
- return fromCanonicalSegments(toCanonicalSegments(items as never));
124
- }
125
-
126
- $formatMessage(onebotMsg: OneBot11Message) {
127
- const wire = Array.isArray(onebotMsg.message) ? [...onebotMsg.message] : [];
128
- const quoteId = Message.quoteIdFromContent(wire);
129
- Message.alignReplySegments(wire, quoteId);
130
- const content = toCanonicalSegments(wire as never);
131
-
132
- const message = Message.from(onebotMsg, {
133
- $id: onebotMsg.message_id.toString(),
134
- $adapter: 'onebot11',
135
- $endpoint: `${this.$config.name}`,
136
- $sender: (() => {
137
- const sender: { id: string; name: string; role?: string; permissions?: string[] } = {
138
- id: onebotMsg.user_id.toString(),
139
- name: onebotMsg.sender?.nickname || onebotMsg.sender?.card || onebotMsg.user_id.toString(),
140
- };
141
- applyQqSenderRoleToMessageSender(sender, onebotMsg.sender?.role);
142
- return sender;
143
- })(),
144
- $channel: {
145
- id: (onebotMsg.group_id || onebotMsg.user_id).toString(),
146
- type: onebotMsg.group_id ? 'group' : 'private',
147
- },
148
- $content: content,
149
- $quote_id: quoteId,
150
- $raw: onebotMsg.raw_message,
151
- $timestamp: onebotMsg.time,
152
- $recall: async () => {
153
- await this.$recallMessage(message.$id);
154
- },
155
- $reply: async (content: SendContent, quote?: boolean | string): Promise<string> => {
156
- if (!Array.isArray(content)) content = [content];
157
- if (quote) content.unshift({ type: 'reply', data: { message_id: message.$id } });
158
- return await this.adapter.sendMessage({
159
- ...message.$channel,
160
- context: 'onebot11',
161
- endpoint: `${this.$config.name}`,
162
- content,
163
- });
164
- },
165
- });
166
- return message;
167
- }
168
-
169
- async $sendMessage(options: SendOptions): Promise<string> {
170
- const content = this.toWireSendContent(expandInteractiveSegmentsInContent(options.content));
171
- const messageData: any = { message: content };
172
- if (options.type === 'group') {
173
- const result = await this.callApi('send_group_msg', {
174
- group_id: parseInt(options.id),
175
- ...messageData,
176
- });
177
- this.logger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
178
- return result.message_id.toString();
179
- }
180
- if (options.type === 'private') {
181
- const result = await this.callApi('send_private_msg', {
182
- user_id: parseInt(options.id),
183
- ...messageData,
184
- });
185
- this.logger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
186
- return result.message_id.toString();
187
- }
188
- throw new Error('Either group_id or user_id must be provided');
189
- }
190
-
191
- async $recallMessage(id: string): Promise<void> {
192
- await this.callApi('delete_msg', { message_id: parseInt(id) });
193
- }
194
-
195
- async $getMsg(messageId: string): Promise<QuotedMessagePayload> {
196
- const idParam = /^\d+$/.test(messageId) ? Number(messageId) : messageId;
197
- const data = await this.callApi('get_msg', { message_id: idParam });
198
- return parseOneBotGetMsgResponse(messageId, data);
199
- }
200
-
201
- async kickMember(groupId: number, userId: number, rejectAddRequest: boolean = false): Promise<boolean> {
202
- try {
203
- await this.callApi('set_group_kick', {
204
- group_id: groupId,
205
- user_id: userId,
206
- reject_add_request: rejectAddRequest,
207
- });
208
- this.logger.debug(formatCompact( { op: 'kick', endpoint: this.$id, group: groupId, user: userId }));
209
- return true;
210
- } catch (error) {
211
- this.logger.error(`OneBot11 Endpoint ${this.$id} 踢出成员失败:`, error);
212
- throw error;
213
- }
214
- }
215
-
216
- async muteMember(groupId: number, userId: number, duration: number = 600): Promise<boolean> {
217
- try {
218
- await this.callApi('set_group_ban', {
219
- group_id: groupId,
220
- user_id: userId,
221
- duration,
222
- });
223
- this.logger.debug(formatCompact( { op: duration > 0 ? 'mute' : 'unmute', endpoint: this.$id, group: groupId, user: userId, duration }));
224
- return true;
225
- } catch (error) {
226
- this.logger.error(`OneBot11 Endpoint ${this.$id} 禁言操作失败:`, error);
227
- throw error;
228
- }
229
- }
230
-
231
- async muteAll(groupId: number, enable: boolean = true): Promise<boolean> {
232
- try {
233
- await this.callApi('set_group_whole_ban', { group_id: groupId, enable });
234
- this.logger.debug(formatCompact( { op: 'mute_all', endpoint: this.$id, group: groupId, enable }));
235
- return true;
236
- } catch (error) {
237
- this.logger.error(`OneBot11 Endpoint ${this.$id} 全员禁言操作失败:`, error);
238
- throw error;
239
- }
240
- }
241
-
242
- async setAdmin(groupId: number, userId: number, enable: boolean = true): Promise<boolean> {
243
- try {
244
- await this.callApi('set_group_admin', { group_id: groupId, user_id: userId, enable });
245
- this.logger.debug(formatCompact( { op: 'set_admin', endpoint: this.$id, group: groupId, user: userId, enable }));
246
- return true;
247
- } catch (error) {
248
- this.logger.error(`OneBot11 Endpoint ${this.$id} 设置管理员失败:`, error);
249
- throw error;
250
- }
251
- }
252
-
253
- async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
254
- try {
255
- await this.callApi('set_group_card', { group_id: groupId, user_id: userId, card });
256
- this.logger.debug(formatCompact( { op: 'set_card', endpoint: this.$id, group: groupId, user: userId }));
257
- return true;
258
- } catch (error) {
259
- this.logger.error(`OneBot11 Endpoint ${this.$id} 设置群名片失败:`, error);
260
- throw error;
261
- }
262
- }
263
-
264
- async setTitle(groupId: number, userId: number, title: string, duration: number = -1): Promise<boolean> {
265
- try {
266
- await this.callApi('set_group_special_title', {
267
- group_id: groupId,
268
- user_id: userId,
269
- special_title: title,
270
- duration,
271
- });
272
- this.logger.debug(formatCompact( { op: 'set_title', endpoint: this.$id, group: groupId, user: userId }));
273
- return true;
274
- } catch (error) {
275
- this.logger.error(`OneBot11 Endpoint ${this.$id} 设置头衔失败:`, error);
276
- throw error;
277
- }
278
- }
279
-
280
- async setGroupName(groupId: number, name: string): Promise<boolean> {
281
- try {
282
- await this.callApi('set_group_name', { group_id: groupId, group_name: name });
283
- this.logger.debug(formatCompact( { op: 'set_group_name', endpoint: this.$id, group: groupId }));
284
- return true;
285
- } catch (error) {
286
- this.logger.error(`OneBot11 Endpoint ${this.$id} 设置群名失败:`, error);
287
- throw error;
288
- }
289
- }
290
-
291
- async getMemberList(groupId: number): Promise<any[]> {
292
- try {
293
- return await this.callApi('get_group_member_list', { group_id: groupId });
294
- } catch (error) {
295
- this.logger.error(`OneBot11 Endpoint ${this.$id} 获取群成员列表失败:`, error);
296
- throw error;
297
- }
298
- }
299
-
300
- async getGroupInfo(groupId: number): Promise<any> {
301
- try {
302
- return await this.callApi('get_group_info', { group_id: groupId });
303
- } catch (error) {
304
- this.logger.error(`OneBot11 Endpoint ${this.$id} 获取群信息失败:`, error);
305
- throw error;
306
- }
307
- }
308
-
309
- private async callApi(action: string, params: any = {}): Promise<any> {
310
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
311
- throw new Error('WebSocket is not connected');
312
- }
313
- const echo = `req_${++this.requestId}`;
314
- const message = { action, params, echo };
315
- return new Promise((resolve, reject) => {
316
- const timeout = setTimeout(() => {
317
- this.pendingRequests.delete(echo);
318
- reject(new Error(`API call timeout: ${action}`));
319
- }, 30000);
320
- this.pendingRequests.set(echo, { resolve, reject, timeout });
321
- this.ws!.send(JSON.stringify(message));
322
- });
323
- }
324
-
325
- private handleWebSocketMessage(message: any): void {
326
- if (message.echo && this.pendingRequests.has(message.echo)) {
327
- const request = this.pendingRequests.get(message.echo)!;
328
- this.pendingRequests.delete(message.echo);
329
- clearTimeout(request.timeout);
330
- const response = message as ApiResponse;
331
- if (response.status === 'ok') return request.resolve(response.data);
332
- return request.reject(new Error(`API error: ${response.retcode}`));
333
- }
334
- if (message.post_type === 'message') {
335
- this.handleOneBot11Message(message);
336
- } else if (message.post_type === 'notice') {
337
- this.handleOneBot11Notice(message);
338
- } else if (message.post_type === 'request') {
339
- this.handleOneBot11Request(message);
340
- }
341
- }
342
-
343
- private handleOneBot11Notice(event: any): void {
344
- const noticeTypeMap: Record<string, string> = {
345
- group_increase: 'group_member_increase',
346
- group_decrease: 'group_member_decrease',
347
- group_admin: 'group_admin_change',
348
- group_ban: 'group_ban',
349
- group_recall: 'group_recall',
350
- friend_recall: 'friend_recall',
351
- friend_add: 'friend_add',
352
- notify: event.sub_type === 'poke' ? (event.group_id ? 'group_poke' : 'friend_poke') : `notify_${event.sub_type}`,
353
- group_upload: 'group_upload',
354
- };
355
- const $type = noticeTypeMap[event.notice_type] || event.notice_type;
356
- const isGroup = !!event.group_id;
357
- const notice = Notice.from(event, {
358
- $id: `${event.time}_${event.notice_type}_${event.group_id || event.user_id}`,
359
- $adapter: 'onebot11',
360
- $endpoint: this.$config.name,
361
- $type,
362
- $subType: event.sub_type,
363
- $channel: {
364
- id: (event.group_id || event.user_id)?.toString() || '',
365
- type: isGroup ? 'group' : 'private',
366
- },
367
- $operator: event.operator_id ? { id: event.operator_id.toString(), name: event.operator_id.toString() } : undefined,
368
- $target: event.user_id ? { id: event.user_id.toString(), name: event.user_id.toString() } : undefined,
369
- $timestamp: event.time || Math.floor(Date.now() / 1000),
370
- });
371
- this.adapter.emit('notice.receive', notice);
372
- }
373
-
374
- private handleOneBot11Request(event: any): void {
375
- const typeMap: Record<string, string> = {
376
- friend: 'friend_add',
377
- group: event.sub_type === 'invite' ? 'group_invite' : 'group_add',
378
- };
379
- const $type = typeMap[event.request_type] || event.request_type;
380
- const request = Request.from(event, {
381
- $id: event.flag || `${event.time}_${event.request_type}_${event.user_id}`,
382
- $adapter: 'onebot11',
383
- $endpoint: this.$config.name,
384
- $type,
385
- $subType: event.sub_type,
386
- $channel: {
387
- id: (event.group_id || event.user_id)?.toString() || '',
388
- type: event.group_id ? 'group' : 'private',
389
- },
390
- $sender: { id: event.user_id?.toString() || '', name: event.user_id?.toString() || '' },
391
- $comment: event.comment,
392
- $timestamp: event.time || Math.floor(Date.now() / 1000),
393
- $approve: async (remark?: string) => {
394
- await this.callApi(
395
- event.request_type === 'friend' ? 'set_friend_add_request' : 'set_group_add_request',
396
- { flag: event.flag, approve: true, remark },
397
- );
398
- },
399
- $reject: async (reason?: string) => {
400
- await this.callApi(
401
- event.request_type === 'friend' ? 'set_friend_add_request' : 'set_group_add_request',
402
- { flag: event.flag, approve: false, reason },
403
- );
404
- },
405
- });
406
- this.adapter.emit('request.receive', request);
407
- }
408
-
409
- private handleOneBot11Message(onebotMsg: OneBot11Message): void {
410
- const message = this.$formatMessage(onebotMsg);
411
- this.adapter.emit('message.receive', message);
412
- this.logger.debug(`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`);
413
- }
414
-
415
- private startHeartbeat(): void {
416
- const interval = this.$config.heartbeat_interval || 30000;
417
- this.heartbeatTimer = setInterval(() => {
418
- if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.ping();
419
- }, interval);
420
- }
421
-
422
- private scheduleReconnect(): void {
423
- if (this.reconnectTimer) return;
424
- const interval = this.$config.reconnect_interval || 5000;
425
- this.reconnectTimer = setTimeout(async () => {
426
- this.reconnectTimer = undefined;
427
- try {
428
- await this.$connect();
429
- } catch (error) {
430
- this.emit('error', new Error(`Reconnection failed: ${error}`));
431
- this.scheduleReconnect();
432
- }
433
- }, interval);
434
- }
435
- }