@rdjksp/node-napcat-ts 0.4.20

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.
@@ -0,0 +1,58 @@
1
+ import type { EventHandleMap, EventKey, HandlerResMap, NCWebsocketOptions, WSSendParam, WSSendReturn } from './Interfaces.js';
2
+ export declare class NCWebsocketBase {
3
+ #private;
4
+ constructor(NCWebsocketOptions: NCWebsocketOptions, debug?: boolean);
5
+ /**
6
+ * await connect() 等待 ws 连接
7
+ */
8
+ connect(): Promise<void>;
9
+ disconnect(): void;
10
+ reconnect(): Promise<void>;
11
+ /**
12
+ * 发送API请求
13
+ * @param method API 端点
14
+ * @param params 请求参数
15
+ */
16
+ send<T extends keyof WSSendParam>(method: T, params: WSSendParam[T]): Promise<WSSendReturn[T]>;
17
+ /**
18
+ * 注册监听方法
19
+ * @param event
20
+ * @param handle
21
+ * @returns 返回自身引用
22
+ */
23
+ on<T extends EventKey>(event: T, handle: EventHandleMap[T]): this;
24
+ /**
25
+ * 只执行一次
26
+ * @param event
27
+ * @param handle
28
+ * @returns 返回自身引用
29
+ */
30
+ once<T extends EventKey>(event: T, handle: EventHandleMap[T]): this;
31
+ /**
32
+ * 解除监听方法
33
+ * @param event
34
+ * @param handle
35
+ * @returns 返回自身引用
36
+ */
37
+ off<T extends EventKey>(event: T, handle: EventHandleMap[T]): this;
38
+ /**
39
+ * effect风格的订阅 效果同on
40
+ * @param event
41
+ * @param handle
42
+ * @returns 返回执行后取消订阅的函数
43
+ */
44
+ subscribe<T extends EventKey>(event: T, handle: EventHandleMap[T]): () => void;
45
+ /**
46
+ * effect风格的订阅 效果同once
47
+ * @param event
48
+ * @param handle
49
+ * @returns 返回执行后取消订阅的函数
50
+ */
51
+ subscribeOnce<T extends EventKey>(event: T, handle: EventHandleMap[T]): () => void;
52
+ /**
53
+ * 手动模拟触发某个事件
54
+ * @param type
55
+ * @param context
56
+ */
57
+ emit<T extends EventKey>(type: T, context: HandlerResMap[T]): this;
58
+ }
@@ -0,0 +1,276 @@
1
+ import WebSocket from 'isomorphic-ws';
2
+ import { nanoid } from 'nanoid';
3
+ import { NCEventBus } from './NCEventBus.js';
4
+ import { convertCQCodeToJSON, CQCodeDecode, logger } from './Utils.js';
5
+ export class NCWebsocketBase {
6
+ #debug;
7
+ #baseUrl;
8
+ #accessToken;
9
+ #throwPromise;
10
+ #reconnection;
11
+ #socket;
12
+ #eventBus;
13
+ #echoMap;
14
+ constructor(NCWebsocketOptions, debug = false) {
15
+ this.#accessToken = NCWebsocketOptions.accessToken ?? '';
16
+ this.#throwPromise = NCWebsocketOptions.throwPromise ?? false;
17
+ if ('baseUrl' in NCWebsocketOptions) {
18
+ this.#baseUrl = NCWebsocketOptions.baseUrl;
19
+ }
20
+ else if ('protocol' in NCWebsocketOptions &&
21
+ 'host' in NCWebsocketOptions &&
22
+ 'port' in NCWebsocketOptions) {
23
+ const { protocol, host, port } = NCWebsocketOptions;
24
+ this.#baseUrl = protocol + '://' + host + ':' + port;
25
+ }
26
+ else {
27
+ throw new Error('NCWebsocketOptions must contain either "protocol && host && port" or "baseUrl"');
28
+ }
29
+ // 整理重连参数
30
+ const { enable = true, attempts = 10, delay = 5000 } = NCWebsocketOptions.reconnection ?? {};
31
+ this.#reconnection = { enable, attempts, delay, nowAttempts: 1 };
32
+ this.#debug = debug;
33
+ this.#eventBus = new NCEventBus(this);
34
+ this.#echoMap = new Map();
35
+ }
36
+ // ==================WebSocket操作=============================
37
+ /**
38
+ * await connect() 等待 ws 连接
39
+ */
40
+ async connect() {
41
+ return new Promise((resolve, reject) => {
42
+ this.#eventBus.emit('socket.connecting', { reconnection: this.#reconnection });
43
+ const socket = new WebSocket(`${this.#baseUrl}?access_token=${this.#accessToken}`);
44
+ socket.onopen = () => {
45
+ this.#eventBus.emit('socket.open', { reconnection: this.#reconnection });
46
+ this.#reconnection.nowAttempts = 1;
47
+ resolve();
48
+ };
49
+ socket.onclose = async (event) => {
50
+ this.#eventBus.emit('socket.close', {
51
+ code: event.code,
52
+ reason: event.reason,
53
+ reconnection: this.#reconnection,
54
+ });
55
+ if (this.#reconnection.enable &&
56
+ this.#reconnection.nowAttempts < this.#reconnection.attempts) {
57
+ this.#reconnection.nowAttempts++;
58
+ setTimeout(async () => {
59
+ try {
60
+ await this.reconnect();
61
+ }
62
+ catch (error) {
63
+ reject(error);
64
+ }
65
+ }, this.#reconnection.delay);
66
+ }
67
+ };
68
+ socket.onmessage = (event) => this.#message(event.data);
69
+ socket.onerror = (event) => {
70
+ this.#eventBus.emit('socket.error', {
71
+ reconnection: this.#reconnection,
72
+ error_type: 'connect_error',
73
+ errors: event?.error?.errors ?? [event?.error ?? null],
74
+ });
75
+ if (this.#throwPromise) {
76
+ if (this.#reconnection.enable &&
77
+ this.#reconnection.nowAttempts < this.#reconnection.attempts) {
78
+ // 重连未到最后一次,等待继续重连,不抛出错误
79
+ return;
80
+ }
81
+ reject({
82
+ reconnection: this.#reconnection,
83
+ error_type: 'connect_error',
84
+ errors: event?.error?.errors ?? [event?.error ?? null],
85
+ });
86
+ }
87
+ };
88
+ this.#socket = socket;
89
+ });
90
+ }
91
+ disconnect() {
92
+ if (this.#socket) {
93
+ this.#socket.close(1000);
94
+ this.#socket = undefined;
95
+ }
96
+ }
97
+ async reconnect() {
98
+ this.disconnect();
99
+ await this.connect();
100
+ }
101
+ #message(data) {
102
+ let strData;
103
+ try {
104
+ strData = data.toString();
105
+ // 检查数据是否看起来像有效的JSON (以 { 或 [ 开头)
106
+ if (!(strData.trim().startsWith('{') || strData.trim().startsWith('['))) {
107
+ logger.warn('[node-napcat-ts]', '[socket]', 'received non-JSON data:', strData);
108
+ return;
109
+ }
110
+ let json = JSON.parse(strData);
111
+ if (json.post_type === 'message' || json.post_type === 'message_sent') {
112
+ if (json.message_format === 'string') {
113
+ // 直接处理message字段,而不是整个json对象
114
+ json.message = convertCQCodeToJSON(CQCodeDecode(json.message));
115
+ json.message_format = 'array';
116
+ }
117
+ if (typeof json.raw_message === 'string') {
118
+ json.raw_message = CQCodeDecode(json.raw_message);
119
+ }
120
+ }
121
+ if (this.#debug) {
122
+ logger.debug('[node-napcat-ts]', '[socket]', 'receive data');
123
+ logger.dir(json);
124
+ }
125
+ if (json.echo) {
126
+ const handler = this.#echoMap.get(json.echo);
127
+ if (handler) {
128
+ if (json.retcode === 0) {
129
+ this.#eventBus.emit('api.response.success', json);
130
+ handler.onSuccess(json);
131
+ }
132
+ else {
133
+ this.#eventBus.emit('api.response.failure', json);
134
+ handler.onFailure(json);
135
+ }
136
+ }
137
+ }
138
+ else {
139
+ if (json?.status === 'failed' && json?.echo === null) {
140
+ this.#reconnection.enable = false;
141
+ this.#eventBus.emit('socket.error', {
142
+ reconnection: this.#reconnection,
143
+ error_type: 'response_error',
144
+ info: {
145
+ url: this.#baseUrl,
146
+ errno: json.retcode,
147
+ message: json.message,
148
+ },
149
+ });
150
+ if (this.#throwPromise)
151
+ throw new Error(json.message);
152
+ this.disconnect();
153
+ return;
154
+ }
155
+ this.#eventBus.parseMessage(json);
156
+ }
157
+ }
158
+ catch (error) {
159
+ logger.warn('[node-napcat-ts]', '[socket]', 'failed to parse JSON');
160
+ logger.dir(error);
161
+ return;
162
+ }
163
+ }
164
+ // ==================事件绑定=============================
165
+ /**
166
+ * 发送API请求
167
+ * @param method API 端点
168
+ * @param params 请求参数
169
+ */
170
+ send(method, params) {
171
+ const echo = nanoid();
172
+ const message = {
173
+ action: method,
174
+ params: params,
175
+ echo,
176
+ };
177
+ if (this.#debug) {
178
+ logger.debug('[node-open-napcat] send request');
179
+ logger.dir(message);
180
+ }
181
+ return new Promise((resolve, reject) => {
182
+ const onSuccess = (response) => {
183
+ this.#echoMap.delete(echo);
184
+ return resolve(response.data);
185
+ };
186
+ const onFailure = (reason) => {
187
+ this.#echoMap.delete(echo);
188
+ return reject(reason);
189
+ };
190
+ this.#echoMap.set(echo, {
191
+ message,
192
+ onSuccess,
193
+ onFailure,
194
+ });
195
+ this.#eventBus.emit('api.preSend', message);
196
+ if (this.#socket === undefined) {
197
+ reject({
198
+ status: 'failed',
199
+ retcode: -1,
200
+ data: null,
201
+ message: 'api socket is not connected',
202
+ echo: '',
203
+ });
204
+ }
205
+ else if (this.#socket.readyState === WebSocket.CLOSING) {
206
+ reject({
207
+ status: 'failed',
208
+ retcode: -1,
209
+ data: null,
210
+ message: 'api socket is closed',
211
+ echo: '',
212
+ });
213
+ }
214
+ else {
215
+ this.#socket.send(JSON.stringify(message));
216
+ }
217
+ });
218
+ }
219
+ /**
220
+ * 注册监听方法
221
+ * @param event
222
+ * @param handle
223
+ * @returns 返回自身引用
224
+ */
225
+ on(event, handle) {
226
+ this.#eventBus.on(event, handle);
227
+ return this;
228
+ }
229
+ /**
230
+ * 只执行一次
231
+ * @param event
232
+ * @param handle
233
+ * @returns 返回自身引用
234
+ */
235
+ once(event, handle) {
236
+ this.#eventBus.once(event, handle);
237
+ return this;
238
+ }
239
+ /**
240
+ * 解除监听方法
241
+ * @param event
242
+ * @param handle
243
+ * @returns 返回自身引用
244
+ */
245
+ off(event, handle) {
246
+ this.#eventBus.off(event, handle);
247
+ return this;
248
+ }
249
+ /**
250
+ * effect风格的订阅 效果同on
251
+ * @param event
252
+ * @param handle
253
+ * @returns 返回执行后取消订阅的函数
254
+ */
255
+ subscribe(event, handle) {
256
+ return this.#eventBus.subscribe(event, handle);
257
+ }
258
+ /**
259
+ * effect风格的订阅 效果同once
260
+ * @param event
261
+ * @param handle
262
+ * @returns 返回执行后取消订阅的函数
263
+ */
264
+ subscribeOnce(event, handle) {
265
+ return this.#eventBus.subscribeOnce(event, handle);
266
+ }
267
+ /**
268
+ * 手动模拟触发某个事件
269
+ * @param type
270
+ * @param context
271
+ */
272
+ emit(type, context) {
273
+ this.#eventBus.emit(type, context);
274
+ return this;
275
+ }
276
+ }
@@ -0,0 +1,379 @@
1
+ export interface UnSafeStruct {
2
+ type: string;
3
+ data: {
4
+ [k: string]: any;
5
+ };
6
+ }
7
+ export interface Receive {
8
+ text: {
9
+ type: 'text';
10
+ data: {
11
+ text: string;
12
+ };
13
+ };
14
+ at: {
15
+ type: 'at';
16
+ data: {
17
+ qq: string | 'all';
18
+ };
19
+ };
20
+ image: {
21
+ type: 'image';
22
+ data: {
23
+ summary: string;
24
+ file: string;
25
+ sub_type: number;
26
+ url: string;
27
+ file_size: string;
28
+ } | {
29
+ summary: string;
30
+ file: string;
31
+ sub_type: string;
32
+ url: string;
33
+ key: string;
34
+ emoji_id: string;
35
+ emoji_package_id: number;
36
+ };
37
+ };
38
+ file: {
39
+ type: 'file';
40
+ data: {
41
+ file: string;
42
+ file_id: string;
43
+ file_size: string;
44
+ };
45
+ };
46
+ poke: {
47
+ type: 'poke';
48
+ data: {
49
+ type: string;
50
+ id: string;
51
+ };
52
+ };
53
+ dice: {
54
+ type: 'dice';
55
+ data: {
56
+ result: string;
57
+ };
58
+ };
59
+ rps: {
60
+ type: 'rps';
61
+ data: {
62
+ result: string;
63
+ };
64
+ };
65
+ face: {
66
+ type: 'face';
67
+ data: {
68
+ id: string;
69
+ raw: {
70
+ faceIndex?: number;
71
+ faceText?: string;
72
+ faceType?: number;
73
+ packId?: string;
74
+ stickerId?: string;
75
+ sourceType?: number;
76
+ stickerType?: number;
77
+ resultId?: string;
78
+ surpriseId?: string;
79
+ randomType?: number;
80
+ imageType?: number;
81
+ pokeType?: number;
82
+ spokeSummary?: string;
83
+ doubleHit?: number;
84
+ vaspokeId?: number;
85
+ vaspokeName?: string;
86
+ vaspokeMinver?: number;
87
+ pokeStrength?: number;
88
+ msgType?: number;
89
+ faceBubbleCount?: number;
90
+ oldVersionStr?: string;
91
+ pokeFlag?: number;
92
+ chainCount?: number;
93
+ };
94
+ resultId: string | null;
95
+ chainCount: number | null;
96
+ };
97
+ };
98
+ reply: {
99
+ type: 'reply';
100
+ data: {
101
+ id: string;
102
+ };
103
+ };
104
+ video: {
105
+ type: 'video';
106
+ data: {
107
+ file: string;
108
+ url: string;
109
+ file_size: string;
110
+ };
111
+ };
112
+ record: {
113
+ type: 'record';
114
+ data: {
115
+ file: string;
116
+ file_size: string;
117
+ };
118
+ };
119
+ forward: {
120
+ type: 'forward';
121
+ data: {
122
+ id: string;
123
+ content?: Receive[keyof Receive][];
124
+ };
125
+ };
126
+ json: {
127
+ type: 'json';
128
+ data: {
129
+ data: string;
130
+ };
131
+ };
132
+ markdown: {
133
+ type: 'markdown';
134
+ data: {
135
+ content: string;
136
+ };
137
+ };
138
+ }
139
+ interface BaseSegment<T extends string, D> {
140
+ type: T;
141
+ data: D;
142
+ }
143
+ export interface TextSegment extends BaseSegment<'text', {
144
+ text: string;
145
+ }> {
146
+ }
147
+ export interface AtSegment extends BaseSegment<'at', {
148
+ qq: string | 'all';
149
+ }> {
150
+ }
151
+ export interface ReplySegment extends BaseSegment<'reply', {
152
+ id: string;
153
+ }> {
154
+ }
155
+ export interface FaceSegment extends BaseSegment<'face', {
156
+ id: string;
157
+ }> {
158
+ }
159
+ export interface MFaceSegment extends BaseSegment<'mface', {
160
+ emoji_id: string;
161
+ emoji_package_id: string;
162
+ key: string;
163
+ summary?: string;
164
+ }> {
165
+ }
166
+ export interface ImageSegment extends BaseSegment<'image', {
167
+ file: string;
168
+ summary?: string;
169
+ sub_type?: string;
170
+ }> {
171
+ }
172
+ export interface FileSegment extends BaseSegment<'file', {
173
+ file: string;
174
+ name?: string;
175
+ }> {
176
+ }
177
+ export interface VideoSegment extends BaseSegment<'video', {
178
+ file: string;
179
+ name?: string;
180
+ thumb?: string;
181
+ }> {
182
+ }
183
+ export interface RecordSegment extends BaseSegment<'record', {
184
+ file: string;
185
+ }> {
186
+ }
187
+ export interface JsonSegment extends BaseSegment<'json', {
188
+ data: string;
189
+ }> {
190
+ }
191
+ export interface DiceSegment extends BaseSegment<'dice', any> {
192
+ }
193
+ export interface RPSSegment extends BaseSegment<'rps', any> {
194
+ }
195
+ export interface MarkdownSegment extends BaseSegment<'markdown', {
196
+ content: string;
197
+ }> {
198
+ }
199
+ export interface CloudMusicSegment extends BaseSegment<'music', {
200
+ type: 'qq' | '163' | 'kugou' | 'kuwo' | 'migu';
201
+ id: string;
202
+ }> {
203
+ }
204
+ export interface MusicSegmentCustom extends BaseSegment<'music', {
205
+ type: 'qq' | '163' | 'kugou' | 'kuwo' | 'migu' | 'custom';
206
+ url: string;
207
+ image: string;
208
+ audio?: string;
209
+ title?: string;
210
+ singer?: string;
211
+ }> {
212
+ }
213
+ export type MusicSegment = CloudMusicSegment | MusicSegmentCustom;
214
+ export interface NodeSegment extends BaseSegment<'node', ({
215
+ content: SendMessageSegment[];
216
+ } | {
217
+ id: string;
218
+ }) & {
219
+ user_id?: string;
220
+ nickname?: string;
221
+ source?: string;
222
+ news?: {
223
+ text: string;
224
+ }[];
225
+ summary?: string;
226
+ prompt?: string;
227
+ time?: string;
228
+ }> {
229
+ }
230
+ export interface ForwardSegment extends BaseSegment<'forward', {
231
+ id: string;
232
+ }> {
233
+ }
234
+ export interface ContactSegment extends BaseSegment<'contact', {
235
+ type: 'qq' | 'group';
236
+ id: string;
237
+ }> {
238
+ }
239
+ export type SendMessageSegment = TextSegment | AtSegment | ReplySegment | FaceSegment | MFaceSegment | ImageSegment | FileSegment | VideoSegment | RecordSegment | JsonSegment | DiceSegment | RPSSegment | MarkdownSegment | MusicSegment | NodeSegment | ForwardSegment | ContactSegment;
240
+ export declare const Structs: {
241
+ /**
242
+ * 发送文字消息
243
+ * @param text 要发送的文字
244
+ * @returns { type: 'text', data: { text } }
245
+ */
246
+ text: (text: string) => TextSegment;
247
+ /**
248
+ * @某人
249
+ * @param qq at的QQ号
250
+ * @returns { type: 'at', data: { qq } }
251
+ */
252
+ at: (qq: string | "all" | number) => AtSegment;
253
+ /**
254
+ * 回复消息
255
+ * @param id 回复的消息id
256
+ * @returns { type: 'reply', data: { id } }
257
+ */
258
+ reply: (id: string | number) => ReplySegment;
259
+ /**
260
+ * 发送QQ表情
261
+ * @param id QQ 表情 ID
262
+ * @returns { type: 'face', data: { id, resultId, chainCount } }
263
+ */
264
+ face: (id: string | number) => FaceSegment;
265
+ /**
266
+ * 发送QQ表情包
267
+ * @param emoji_id 表情id
268
+ * @param emoji_package_id 表情包id
269
+ * @param key 未知(必要)
270
+ * @param summary 表情简介,可选
271
+ * @returns { type: 'mface', data: { summary, emoji_id, emoji_package_id, key } }
272
+ */
273
+ mface: (emoji_id: string | number, emoji_package_id: string | number, key: string, summary?: string) => MFaceSegment;
274
+ /**
275
+ * 发送图片
276
+ * @param file 网络图片地址, 文件路径或者Buffer
277
+ * @param name 图片名
278
+ * @param summary 图片简介
279
+ * @param sub_type 图片类型
280
+ * @returns { type: 'image', data: { file, summary, sub_type } }
281
+ */
282
+ image: (file: string | Buffer, summary?: string, sub_type?: string | number) => ImageSegment;
283
+ /**
284
+ * 发文件
285
+ * @param file 网络文件地址, 文件路径或者Buffer
286
+ * @param name 文件名
287
+ * @returns { type: 'file', data: { file, name } }
288
+ */
289
+ file: (file: string | Buffer, name?: string) => FileSegment;
290
+ /**
291
+ * 发视频
292
+ * @param file 网络视频地址, 文件路径或者Buffer
293
+ * @param name 视频名
294
+ * @param thumb 预览图
295
+ * @returns { type: 'video', data: { file, name, thumb } }
296
+ */
297
+ video: (file: string | Buffer, name?: string, thumb?: string) => VideoSegment;
298
+ /**
299
+ * 发语音
300
+ * @param file 网络语音地址, 文件路径或者Buffer
301
+ * @param name 语音备注
302
+ * @returns { type: 'record', data: { file, name } }
303
+ */
304
+ record: (file: string | Buffer) => RecordSegment;
305
+ /**
306
+ * 发送json消息
307
+ * @param data json信息(序列化后)
308
+ * @returns { type: 'json', data: { data } }
309
+ */
310
+ json: (data: string) => JsonSegment;
311
+ /**
312
+ * 发送骰子魔法表情
313
+ * @returns { type: 'dice', data: {} }
314
+ */
315
+ dice: () => DiceSegment;
316
+ /**
317
+ * 发送猜拳魔法
318
+ * @returns { type: 'rps', data: {} }
319
+ */
320
+ rps: () => RPSSegment;
321
+ /**
322
+ * 发送markdown
323
+ * @param data markdown内容
324
+ * @returns { type: 'markdown', data: {} }
325
+ */
326
+ markdown: (content: string) => MarkdownSegment;
327
+ /**
328
+ * 音乐分享
329
+ * @param type QQ音乐或网易云音乐QQ音乐
330
+ * @param id 音乐id
331
+ * @returns { type: 'music', data: { type, id } }
332
+ */
333
+ music: (type: "qq" | "163" | "kugou" | "migu" | "kuwo", id: string | number) => CloudMusicSegment;
334
+ /**
335
+ * 分享非qq、网易云音乐 需要配置签名服务器
336
+ * @param url 点击后跳转目标 URL
337
+ * @param audio 音乐 URL
338
+ * @param title 标题
339
+ * @param image 发送时可选,内容描述
340
+ * @param singer 发送时可选,图片 URL
341
+ * @returns { type: 'music', data: { type: 'custom', url, audio, title, image, singer } }
342
+ */
343
+ customMusic: (type: "qq" | "163" | "kugou" | "migu" | "kuwo" | "custom", url: string, image: string, audio?: string, title?: string, singer?: string) => MusicSegmentCustom;
344
+ /**
345
+ * 转发消息节点
346
+ * @param id 消息id
347
+ * @param user_id 消息id
348
+ * @param nickname 消息id
349
+ * @param source 消息id
350
+ * @param id 消息id
351
+ * @param id 消息id
352
+ * @returns { type: 'node', data: { id } }
353
+ */
354
+ node: (id: string | number, user_id?: number | string, nickname?: string, source?: string, news?: {
355
+ text: string;
356
+ }[], summary?: string, prompt?: string, time?: string | number) => NodeSegment;
357
+ /**
358
+ * 自定义转发消息节点
359
+ * @param content 消息内容
360
+ * @returns { type: 'node', data: { content } }
361
+ */
362
+ customNode: (content: SendMessageSegment[], user_id?: number | string, nickname?: string, source?: string, news?: {
363
+ text: string;
364
+ }[], summary?: string, prompt?: string, time?: string | number) => NodeSegment;
365
+ /**
366
+ * 转发消息
367
+ * @param message_id 消息id
368
+ * @return { type: 'forward', data: { id }}
369
+ */
370
+ forward: (message_id: number) => ForwardSegment;
371
+ /**
372
+ * 发送名片
373
+ * @param type 名片类型
374
+ * @param id 联系人QQ号
375
+ * @returns { type: 'contact', data: { id } }
376
+ */
377
+ contact: (type: "qq" | "group", id: number | string) => ContactSegment;
378
+ };
379
+ export {};