@zhin.js/adapter-onebot11 1.0.98 → 1.1.0

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 (83) hide show
  1. package/CHANGELOG.md +650 -3
  2. package/README.md +58 -368
  3. package/adapters/onebot11.js +40 -0
  4. package/adapters/onebot11.ts +53 -0
  5. package/agent/PERMITS.md +14 -0
  6. package/{skills/onebot11/SKILL.md → agent/skills/onebot11.md} +4 -4
  7. package/agent/tools/set_title.ts +24 -0
  8. package/commands/endpoint/add/[id].js +3 -0
  9. package/commands/endpoint/add/[id].ts +3 -0
  10. package/commands/endpoint/list.js +3 -0
  11. package/commands/endpoint/list.ts +3 -0
  12. package/commands/endpoint/remove/[id].js +3 -0
  13. package/commands/endpoint/remove/[id].ts +3 -0
  14. package/lib/client.d.ts +20 -0
  15. package/lib/client.js +30 -0
  16. package/lib/content-port.d.ts +4 -0
  17. package/lib/content-port.js +63 -0
  18. package/lib/endpoint-management.d.ts +15 -0
  19. package/lib/endpoint-management.js +47 -0
  20. package/lib/index.d.ts +10 -16
  21. package/lib/index.js +7 -53
  22. package/lib/onebot11-endpoint-commands.d.ts +1 -0
  23. package/lib/onebot11-endpoint-commands.js +24 -0
  24. package/lib/onebot11-runtime-state.d.ts +1 -0
  25. package/lib/onebot11-runtime-state.js +6 -0
  26. package/lib/protocol.d.ts +160 -0
  27. package/lib/protocol.js +316 -0
  28. package/lib/side-event-dispatch.d.ts +7 -0
  29. package/lib/side-event-dispatch.js +36 -0
  30. package/lib/ws-endpoint.d.ts +22 -0
  31. package/lib/ws-endpoint.js +209 -0
  32. package/lib/ws-transport.d.ts +13 -0
  33. package/lib/ws-transport.js +71 -0
  34. package/lib/ws-types.d.ts +18 -0
  35. package/lib/ws-types.js +1 -0
  36. package/lib/wss-auth.d.ts +2 -0
  37. package/lib/wss-auth.js +16 -0
  38. package/lib/wss-endpoint.d.ts +25 -0
  39. package/lib/wss-endpoint.js +173 -0
  40. package/package.json +69 -22
  41. package/plugin.js +18 -0
  42. package/schema.json +97 -0
  43. package/src/client.ts +78 -0
  44. package/src/content-port.ts +66 -0
  45. package/src/endpoint-management.ts +72 -0
  46. package/src/index.ts +62 -65
  47. package/src/onebot11-endpoint-commands.ts +23 -0
  48. package/src/onebot11-runtime-state.ts +7 -0
  49. package/src/protocol.ts +459 -0
  50. package/src/side-event-dispatch.ts +48 -0
  51. package/src/ws-endpoint.ts +272 -0
  52. package/src/ws-transport.ts +100 -0
  53. package/src/ws-types.ts +21 -0
  54. package/src/wss-auth.ts +17 -0
  55. package/src/wss-endpoint.ts +228 -0
  56. package/lib/adapter.d.ts +0 -32
  57. package/lib/adapter.d.ts.map +0 -1
  58. package/lib/adapter.js +0 -92
  59. package/lib/adapter.js.map +0 -1
  60. package/lib/bot-ws-client.d.ts +0 -40
  61. package/lib/bot-ws-client.d.ts.map +0 -1
  62. package/lib/bot-ws-client.js +0 -397
  63. package/lib/bot-ws-client.js.map +0 -1
  64. package/lib/bot-ws-server.d.ts +0 -42
  65. package/lib/bot-ws-server.d.ts.map +0 -1
  66. package/lib/bot-ws-server.js +0 -344
  67. package/lib/bot-ws-server.js.map +0 -1
  68. package/lib/index.d.ts.map +0 -1
  69. package/lib/index.js.map +0 -1
  70. package/lib/onebot-get-msg.d.ts +0 -3
  71. package/lib/onebot-get-msg.d.ts.map +0 -1
  72. package/lib/onebot-get-msg.js +0 -27
  73. package/lib/onebot-get-msg.js.map +0 -1
  74. package/lib/types.d.ts +0 -54
  75. package/lib/types.d.ts.map +0 -1
  76. package/lib/types.js +0 -5
  77. package/lib/types.js.map +0 -1
  78. package/plugin.yml +0 -3
  79. package/src/adapter.ts +0 -104
  80. package/src/bot-ws-client.ts +0 -421
  81. package/src/bot-ws-server.ts +0 -382
  82. package/src/onebot-get-msg.ts +0 -33
  83. package/src/types.ts +0 -58
@@ -1,382 +0,0 @@
1
- /**
2
- * OneBot11 WebSocket 服务端(反向 WS)Bot
3
- */
4
- import WebSocket, { WebSocketServer } from 'ws';
5
- import { EventEmitter } from 'events';
6
- import { clearInterval } from 'node:timers';
7
- import { IncomingMessage } from 'http';
8
- import { formatCompact, Bot, Message, Notice, Request, segment, SendOptions, type QuotedMessagePayload } from 'zhin.js';
9
- import { parseOneBotGetMsgResponse } from './onebot-get-msg.js';
10
- import type { Router } from '@zhin.js/host-router';
11
- import type {
12
- OneBot11WsServerConfig,
13
- OneBot11Message,
14
- ApiResponse,
15
- } from './types.js';
16
- import type { OneBot11Adapter } from './adapter.js';
17
-
18
- export class OneBot11WsServer extends EventEmitter implements Bot<OneBot11WsServerConfig, OneBot11Message> {
19
- $connected: boolean;
20
- #wss?: WebSocketServer;
21
- #clientMap: Map<string, WebSocket> = new Map();
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
-
30
- get logger() {
31
- return this.adapter.plugin.logger;
32
- }
33
-
34
- get $id() {
35
- return this.$config.name;
36
- }
37
-
38
- constructor(
39
- public adapter: OneBot11Adapter,
40
- public router: Router,
41
- public $config: OneBot11WsServerConfig,
42
- ) {
43
- super();
44
- this.$connected = false;
45
- }
46
-
47
- async $connect(): Promise<void> {
48
- if (!this.$config.access_token) {
49
- this.logger.warn(formatCompact({ bot: this.$id, ok: false, error: 'missing access_token' }));
50
- }
51
- this.#wss = this.router.ws(this.$config.path, {
52
- verifyClient: (info: { origin: string; secure: boolean; req: IncomingMessage }) => {
53
- const { req: { headers } } = info;
54
- const authorization = headers['authorization'] || '';
55
- if (this.$config.access_token && authorization !== `Bearer ${this.$config.access_token}`) {
56
- this.logger.error('鉴权失败');
57
- return false;
58
- }
59
- return true;
60
- },
61
- });
62
- this.logger.info(formatCompact( { op: 'listen', bot: this.$id, mode: 'wss', path: this.$config.path }));
63
- this.#wss.on('connection', (client, req) => {
64
- this.startHeartbeat();
65
- this.logger.info(formatCompact({ bot: this.$id, peer: req.socket.remoteAddress }));
66
- client.on('error', (err) => this.logger.warn(formatCompact( {
67
- op: 'ws_error',
68
- bot: this.$id,
69
- ok: false,
70
- error: err instanceof Error ? err.message : String(err),
71
- })));
72
- client.on('close', (code, reason) => {
73
- const reasonStr = reason?.toString?.() || String(reason ?? '');
74
- const codeHint = code === 1005 ? ' [无状态]' : code === 1006 ? ' [异常关闭]' : '';
75
- this.logger.warn(formatCompact( {
76
- op: 'disconnect',
77
- bot: this.$id,
78
- peer: req.socket.remoteAddress,
79
- code: code ?? '?',
80
- error: `${reasonStr || 'closed'}${codeHint}`,
81
- }));
82
- for (const [key, value] of this.#clientMap) {
83
- if (client === value) this.#clientMap.delete(key);
84
- }
85
- if (this.#clientMap.size === 0) {
86
- this.$connected = false;
87
- }
88
- });
89
- client.on('message', (data) => {
90
- try {
91
- const message = JSON.parse(data.toString());
92
- this.handleWebSocketMessage(client, message);
93
- } catch (error) {
94
- this.emit('error', error);
95
- }
96
- });
97
- });
98
- }
99
-
100
- async $disconnect(): Promise<void> {
101
- this.#wss?.close();
102
- if (this.heartbeatTimer) {
103
- clearInterval(this.heartbeatTimer);
104
- this.heartbeatTimer = undefined;
105
- }
106
- }
107
-
108
- $formatMessage(onebotMsg: OneBot11Message) {
109
- const msgId = [onebotMsg.self_id, onebotMsg.message_id].join(':');
110
- const channel = {
111
- id: [onebotMsg.self_id, (onebotMsg.group_id || onebotMsg.user_id)].join(':'),
112
- type: (onebotMsg.group_id ? 'group' : 'private') as 'group' | 'private',
113
- };
114
- const content = Array.isArray(onebotMsg.message) ? [...onebotMsg.message] : [];
115
- const quoteId = Message.quoteIdFromContent(content);
116
- Message.alignReplySegments(content, quoteId);
117
-
118
- const message = Message.from(onebotMsg, {
119
- $id: msgId,
120
- $adapter: 'onebot11',
121
- $bot: `${this.$config.name}`,
122
- $sender: {
123
- id: onebotMsg.user_id.toString(),
124
- name: onebotMsg.user_id.toString(),
125
- },
126
- $channel: channel,
127
- $content: content,
128
- $quote_id: quoteId,
129
- $raw: onebotMsg.raw_message,
130
- $timestamp: onebotMsg.time,
131
- $recall: async (): Promise<void> => await this.$recallMessage(msgId),
132
- $reply: async (content: any, quote?: boolean | string): Promise<string> => {
133
- if (!Array.isArray(content)) content = [content];
134
- if (quote) content.unshift({ type: 'reply', data: { message_id: msgId } });
135
- return await this.$sendMessage({
136
- ...channel,
137
- context: 'onebot11',
138
- bot: `${this.$config.name}`,
139
- content,
140
- });
141
- },
142
- });
143
- return message;
144
- }
145
-
146
- async $sendMessage(options: SendOptions): Promise<string> {
147
- const messageData: any = { message: options.content };
148
- if (options.type === 'group') {
149
- const [self_id, id] = options.id.split(':');
150
- const result = await this.callApi(self_id, 'send_group_msg', {
151
- group_id: parseInt(id),
152
- ...messageData,
153
- });
154
- this.logger.debug(`${this.$config.name} send ${options.type}(${id}):${segment.raw(options.content)}`);
155
- return result.message_id.toString();
156
- }
157
- if (options.type === 'private') {
158
- const [self_id, id] = options.id.split(':');
159
- const result = await this.callApi(self_id, 'send_private_msg', {
160
- user_id: parseInt(id),
161
- ...messageData,
162
- });
163
- this.logger.debug(`${this.$config.name} send ${options.type}(${id}):${segment.raw(options.content)}`);
164
- return result.message_id.toString();
165
- }
166
- throw new Error('Either group_id or user_id must be provided');
167
- }
168
-
169
- async $recallMessage(id: string): Promise<void> {
170
- const [self_id, message_id] = id.split(':');
171
- await this.callApi(self_id, 'delete_msg', { message_id: parseInt(message_id) });
172
- }
173
-
174
- async $getMsg(messageId: string): Promise<QuotedMessagePayload> {
175
- const selfId = this.getFirstSelfId();
176
- const idParam = /^\d+$/.test(messageId) ? Number(messageId) : messageId;
177
- const data = await this.callApi(selfId, 'get_msg', { message_id: idParam });
178
- return parseOneBotGetMsgResponse(messageId, data);
179
- }
180
-
181
- private getFirstSelfId(): string {
182
- const first = this.#clientMap.keys().next().value;
183
- if (!first) throw new Error('反向 WS 尚未有实现端连接');
184
- return first;
185
- }
186
-
187
- async kickMember(groupId: number, userId: number, rejectAddRequest: boolean = false): Promise<boolean> {
188
- const selfId = this.getFirstSelfId();
189
- await this.callApi(selfId, 'set_group_kick', { group_id: groupId, user_id: userId, reject_add_request: rejectAddRequest });
190
- return true;
191
- }
192
-
193
- async muteMember(groupId: number, userId: number, duration: number = 600): Promise<boolean> {
194
- const selfId = this.getFirstSelfId();
195
- await this.callApi(selfId, 'set_group_ban', { group_id: groupId, user_id: userId, duration });
196
- return true;
197
- }
198
-
199
- async muteAll(groupId: number, enable: boolean = true): Promise<boolean> {
200
- const selfId = this.getFirstSelfId();
201
- await this.callApi(selfId, 'set_group_whole_ban', { group_id: groupId, enable });
202
- return true;
203
- }
204
-
205
- async setAdmin(groupId: number, userId: number, enable: boolean = true): Promise<boolean> {
206
- const selfId = this.getFirstSelfId();
207
- await this.callApi(selfId, 'set_group_admin', { group_id: groupId, user_id: userId, enable });
208
- return true;
209
- }
210
-
211
- async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
212
- const selfId = this.getFirstSelfId();
213
- await this.callApi(selfId, 'set_group_card', { group_id: groupId, user_id: userId, card });
214
- return true;
215
- }
216
-
217
- async setGroupName(groupId: number, name: string): Promise<boolean> {
218
- const selfId = this.getFirstSelfId();
219
- await this.callApi(selfId, 'set_group_name', { group_id: groupId, group_name: name });
220
- return true;
221
- }
222
-
223
- async getMemberList(groupId: number): Promise<any[]> {
224
- const selfId = this.getFirstSelfId();
225
- return await this.callApi(selfId, 'get_group_member_list', { group_id: groupId });
226
- }
227
-
228
- async getGroupInfo(groupId: number): Promise<any> {
229
- const selfId = this.getFirstSelfId();
230
- return await this.callApi(selfId, 'get_group_info', { group_id: groupId });
231
- }
232
-
233
- async setTitle(groupId: number, userId: number, title: string, duration: number = -1): Promise<boolean> {
234
- const selfId = this.getFirstSelfId();
235
- await this.callApi(selfId, 'set_group_special_title', { group_id: groupId, user_id: userId, special_title: title, duration });
236
- return true;
237
- }
238
-
239
- private async callApi(self_id: string, action: string, params: any = {}): Promise<any> {
240
- const client = this.#clientMap.get(self_id);
241
- if (!client || client.readyState !== WebSocket.OPEN) {
242
- throw new Error('WebSocket is not connected');
243
- }
244
- const echo = `req_${++this.requestId}`;
245
- const message = { action, params, echo };
246
- return new Promise((resolve, reject) => {
247
- const timeout = setTimeout(() => {
248
- this.pendingRequests.delete(echo);
249
- reject(new Error(`API call timeout: ${action}`));
250
- }, 30000);
251
- this.pendingRequests.set(echo, { resolve, reject, timeout });
252
- client.send(JSON.stringify(message));
253
- });
254
- }
255
-
256
- private handleWebSocketMessage(client: WebSocket, message: any): void {
257
- // 自动注册 client 映射(修复:统一使用 String 类型 key,兼容不发 lifecycle:connect 的实现端)
258
- if (message.self_id != null) {
259
- const selfIdStr = String(message.self_id);
260
- if (!this.#clientMap.has(selfIdStr) || this.#clientMap.get(selfIdStr) !== client) {
261
- this.#clientMap.set(selfIdStr, client);
262
- if (!this.$connected) {
263
- this.$connected = true;
264
- }
265
- }
266
- }
267
- if (message.echo && this.pendingRequests.has(message.echo)) {
268
- const request = this.pendingRequests.get(message.echo)!;
269
- this.pendingRequests.delete(message.echo);
270
- clearTimeout(request.timeout);
271
- const response = message as ApiResponse;
272
- if (response.status === 'ok') return request.resolve(response.data);
273
- return request.reject(new Error(`API error: ${response.retcode}`));
274
- }
275
- switch (message.post_type) {
276
- case 'message':
277
- return this.handleMessage(message);
278
- case 'notice':
279
- return this.handleNotice(message);
280
- case 'request':
281
- return this.handleRequest(message);
282
- case 'meta_event':
283
- return this.handleMetaEvent(client, message);
284
- }
285
- }
286
-
287
- private handleMetaEvent(client: WebSocket, message: any): void {
288
- switch (message.sub_type) {
289
- case 'heartbeat':
290
- break;
291
- case 'connect':
292
- this.#clientMap.set(String(message.self_id), client);
293
- this.$connected = true;
294
- this.logger.info(formatCompact({ bot: this.$config.name, self_id: message.self_id }));
295
- break;
296
- }
297
- }
298
-
299
- private handleMessage(onebotMsg: OneBot11Message): void {
300
- const message = this.$formatMessage(onebotMsg);
301
- this.adapter.emit('message.receive', message);
302
- this.logger.debug(`${this.$config.name} recv ${message.$channel.type}(${onebotMsg.group_id || onebotMsg.user_id}):${segment.raw(message.$content)}`);
303
- }
304
-
305
- private handleNotice(event: any): void {
306
- const noticeTypeMap: Record<string, string> = {
307
- group_increase: 'group_member_increase',
308
- group_decrease: 'group_member_decrease',
309
- group_admin: 'group_admin_change',
310
- group_ban: 'group_ban',
311
- group_recall: 'group_recall',
312
- friend_recall: 'friend_recall',
313
- friend_add: 'friend_add',
314
- notify: event.sub_type === 'poke' ? (event.group_id ? 'group_poke' : 'friend_poke') : `notify_${event.sub_type}`,
315
- group_upload: 'group_upload',
316
- };
317
- const $type = noticeTypeMap[event.notice_type] || event.notice_type;
318
- const isGroup = !!event.group_id;
319
- const notice = Notice.from(event, {
320
- $id: `${event.self_id}:${event.time}_${event.notice_type}_${event.group_id || event.user_id}`,
321
- $adapter: 'onebot11',
322
- $bot: this.$config.name,
323
- $type,
324
- $subType: event.sub_type,
325
- $channel: {
326
- id: [event.self_id, (event.group_id || event.user_id)].join(':'),
327
- type: isGroup ? 'group' : 'private',
328
- },
329
- $operator: event.operator_id ? { id: event.operator_id.toString(), name: event.operator_id.toString() } : undefined,
330
- $target: event.user_id ? { id: event.user_id.toString(), name: event.user_id.toString() } : undefined,
331
- $timestamp: event.time || Math.floor(Date.now() / 1000),
332
- });
333
- this.adapter.emit('notice.receive', notice);
334
- }
335
-
336
- private handleRequest(event: any): void {
337
- const self_id = event.self_id?.toString() || '';
338
- const typeMap: Record<string, string> = {
339
- friend: 'friend_add',
340
- group: event.sub_type === 'invite' ? 'group_invite' : 'group_add',
341
- };
342
- const $type = typeMap[event.request_type] || event.request_type;
343
- const request = Request.from(event, {
344
- $id: event.flag || `${self_id}:${event.time}_${event.request_type}_${event.user_id}`,
345
- $adapter: 'onebot11',
346
- $bot: this.$config.name,
347
- $type,
348
- $subType: event.sub_type,
349
- $channel: {
350
- id: [self_id, (event.group_id || event.user_id)].join(':'),
351
- type: event.group_id ? 'group' : 'private',
352
- },
353
- $sender: { id: event.user_id?.toString() || '', name: event.user_id?.toString() || '' },
354
- $comment: event.comment,
355
- $timestamp: event.time || Math.floor(Date.now() / 1000),
356
- $approve: async (remark?: string) => {
357
- await this.callApi(
358
- self_id,
359
- event.request_type === 'friend' ? 'set_friend_add_request' : 'set_group_add_request',
360
- { flag: event.flag, approve: true, remark },
361
- );
362
- },
363
- $reject: async (reason?: string) => {
364
- await this.callApi(
365
- self_id,
366
- event.request_type === 'friend' ? 'set_friend_add_request' : 'set_group_add_request',
367
- { flag: event.flag, approve: false, reason },
368
- );
369
- },
370
- });
371
- this.adapter.emit('request.receive', request);
372
- }
373
-
374
- private startHeartbeat(): void {
375
- const interval = this.$config.heartbeat_interval || 30000;
376
- this.heartbeatTimer = setInterval(() => {
377
- for (const client of this.#wss?.clients || []) {
378
- if (client && client.readyState === WebSocket.OPEN) client.ping();
379
- }
380
- }, interval);
381
- }
382
- }
@@ -1,33 +0,0 @@
1
- import type { MessageSegment, QuotedMessagePayload } from 'zhin.js';
2
-
3
- export function parseOneBotGetMsgResponse(
4
- messageId: string,
5
- data: unknown,
6
- ): QuotedMessagePayload {
7
- const record =
8
- data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
9
- let content: QuotedMessagePayload['content'] = [];
10
- if (Array.isArray(record.message)) {
11
- content = record.message as MessageSegment[];
12
- } else if (typeof record.raw_message === 'string' && record.raw_message) {
13
- content = [{ type: 'text', data: { text: record.raw_message } }];
14
- }
15
-
16
- const senderRaw = record.sender;
17
- let sender: QuotedMessagePayload['sender'];
18
- if (senderRaw && typeof senderRaw === 'object') {
19
- const s = senderRaw as Record<string, unknown>;
20
- sender = {
21
- id: String(s.user_id ?? ''),
22
- name: String(s.nickname ?? s.card ?? ''),
23
- };
24
- }
25
-
26
- return {
27
- messageId,
28
- sender,
29
- content,
30
- raw: typeof record.raw_message === 'string' ? record.raw_message : undefined,
31
- time: typeof record.time === 'number' ? record.time : undefined,
32
- };
33
- }
package/src/types.ts DELETED
@@ -1,58 +0,0 @@
1
- /**
2
- * OneBot11 适配器类型与配置
3
- */
4
-
5
-
6
- export interface OneBot11SenderInfo {
7
- id: string;
8
- name: string;
9
- role?: 'owner' | 'admin' | 'member';
10
- isOwner?: boolean;
11
- isAdmin?: boolean;
12
- card?: string;
13
- title?: string;
14
- }
15
-
16
- /** 配置公共字段;单一适配器 context 均为 'onebot11',连接方式由 connection 区分 */
17
- export interface OneBot11ConfigBase {
18
- context: 'onebot11';
19
- name: string;
20
- access_token?: string;
21
- }
22
-
23
- /** 正向 WebSocket:应用连 OneBot 实现的 WS */
24
- export interface OneBot11WsClientConfig extends OneBot11ConfigBase {
25
- connection: 'ws';
26
- url: string;
27
- reconnect_interval?: number;
28
- heartbeat_interval?: number;
29
- }
30
-
31
- /** 反向 WebSocket:应用开 WS 服务端,实现连上来 */
32
- export interface OneBot11WsServerConfig extends OneBot11ConfigBase {
33
- connection: 'wss';
34
- path: string;
35
- heartbeat_interval?: number;
36
- }
37
-
38
- export type OneBot11BotConfig = OneBot11WsClientConfig | OneBot11WsServerConfig;
39
-
40
- export interface OneBot11Message {
41
- post_type: string;
42
- self_id: string;
43
- message_type?: string;
44
- sub_type?: string;
45
- message_id: number;
46
- user_id: number;
47
- group_id?: number;
48
- message: Array<{ type: string; data: Record<string, any> }>;
49
- raw_message: string;
50
- time: number;
51
- }
52
-
53
- export interface ApiResponse<T = any> {
54
- status: string;
55
- retcode: number;
56
- data: T;
57
- echo?: string;
58
- }