@alemonjs/qq-bot 2.1.30 → 2.1.32

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,56 @@
1
+ import { readFile, mkdir, writeFile, rename, unlink } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { join, dirname } from 'node:path';
4
+
5
+ class FileSessionStore {
6
+ root;
7
+ #queues = new Map();
8
+ #generations = new Map();
9
+ constructor(root = join(process.cwd(), '.data', 'qq-bot', 'sessions')) {
10
+ this.root = root;
11
+ }
12
+ #path(botId) {
13
+ return join(this.root, `${encodeURIComponent(botId)}.json`);
14
+ }
15
+ async load(botId) {
16
+ try {
17
+ const value = JSON.parse(await readFile(this.#path(botId), 'utf8'));
18
+ return value?.botId === botId && typeof value.sessionId === 'string' && Number.isFinite(value.sequence) ? value : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ async save(session) {
25
+ const generation = this.#generations.get(session.botId) || 0;
26
+ await this.#enqueue(session.botId, async () => {
27
+ if (generation !== (this.#generations.get(session.botId) || 0))
28
+ return;
29
+ const path = this.#path(session.botId);
30
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
31
+ await mkdir(dirname(path), { recursive: true });
32
+ await writeFile(temp, JSON.stringify(session), 'utf8');
33
+ await rename(temp, path);
34
+ });
35
+ }
36
+ async clear(botId) {
37
+ this.#generations.set(botId, (this.#generations.get(botId) || 0) + 1);
38
+ await this.#enqueue(botId, async () => {
39
+ await unlink(this.#path(botId)).catch(() => undefined);
40
+ });
41
+ }
42
+ #enqueue(botId, task) {
43
+ const previous = this.#queues.get(botId) || Promise.resolve();
44
+ const next = previous.catch(() => undefined).then(task);
45
+ this.#queues.set(botId, next);
46
+ void next
47
+ .finally(() => {
48
+ if (this.#queues.get(botId) === next)
49
+ this.#queues.delete(botId);
50
+ })
51
+ .catch(() => undefined);
52
+ return next;
53
+ }
54
+ }
55
+
56
+ export { FileSessionStore };
@@ -1,4 +1,5 @@
1
1
  import { IntentsEnum } from './intents';
2
+ import type { SessionStore } from './session.js';
2
3
  export type MessageType = 0 | 1 | 2 | 3 | 4 | 7;
3
4
  export type FileType = 1 | 2 | 3 | 4;
4
5
  export interface ButtonType {
@@ -68,4 +69,5 @@ export interface Options {
68
69
  gatewayURL?: string;
69
70
  base_url_gateway?: string;
70
71
  base_url_app_access_token?: string;
72
+ sessionStore?: SessionStore;
71
73
  }
package/lib/sends.js CHANGED
@@ -188,6 +188,9 @@ const extractContent = (val, mode) => {
188
188
  'Image',
189
189
  'ImageFile',
190
190
  'ImageURL',
191
+ 'Audio',
192
+ 'Video',
193
+ 'Attachment',
191
194
  'Markdown',
192
195
  'MarkdownOriginal',
193
196
  'BT.group',
@@ -301,14 +304,15 @@ const buildArkParams = (val) => {
301
304
  const filterImages = (val) => {
302
305
  return val.filter(item => item.type === 'Image' || item.type === 'ImageFile' || item.type === 'ImageURL');
303
306
  };
304
- const resolveRichMediaUrl = async (images, uploadMedia) => {
305
- for (const item of images) {
307
+ const resolveRichMediaUrl = async (items, uploadMedia) => {
308
+ for (const item of items) {
306
309
  let fileData;
307
310
  let fileInfo;
311
+ const fileType = item.type === 'Video' ? 2 : item.type === 'Audio' ? 3 : item.type === 'Attachment' ? 4 : 1;
308
312
  if (item.type === 'ImageURL') {
309
313
  fileData = await axios.get(item.value, { responseType: 'arraybuffer' }).then(res => Buffer.from(res.data, 'binary').toString('base64'));
310
314
  }
311
- else if (item.type === 'Image') {
315
+ else if (item.type === 'Image' || item.type === 'Audio' || item.type === 'Video' || item.type === 'Attachment') {
312
316
  if (typeof item.value === 'string' && (item.value.startsWith('https://') || item.value.startsWith('http://'))) {
313
317
  fileData = await axios.get(item.value, { responseType: 'arraybuffer' }).then(res => Buffer.from(res.data, 'binary').toString('base64'));
314
318
  }
@@ -322,6 +326,9 @@ const resolveRichMediaUrl = async (images, uploadMedia) => {
322
326
  else if (Buffer.isBuffer(item.value)) {
323
327
  fileData = item.value.toString('base64');
324
328
  }
329
+ else if (typeof item.value === 'string' && item.value.startsWith('buffer://')) {
330
+ fileData = item.value.replace('buffer://', '');
331
+ }
325
332
  else if (typeof item.value === 'string') {
326
333
  fileData = item.value;
327
334
  }
@@ -330,7 +337,7 @@ const resolveRichMediaUrl = async (images, uploadMedia) => {
330
337
  fileData = readFileSync(item.value, 'base64');
331
338
  }
332
339
  if (fileData) {
333
- fileInfo = await uploadMedia({ file_type: 1, file_data: fileData }).then(res => res?.file_info);
340
+ fileInfo = await uploadMedia({ file_type: fileType, file_data: fileData }).then(res => res?.file_info);
334
341
  }
335
342
  if (fileInfo) {
336
343
  return fileInfo;
@@ -361,6 +368,7 @@ const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMes
361
368
  const config = getQQBotConfig();
362
369
  const mdToText = config.markdownToText === true;
363
370
  const images = filterImages(val);
371
+ const richMedia = val.filter(item => item.type === 'Audio' || item.type === 'Video' || item.type === 'Attachment');
364
372
  if (images.length > 0) {
365
373
  const url = await resolveRichMediaUrl(images, uploadMedia);
366
374
  if (!url) {
@@ -375,6 +383,19 @@ const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMes
375
383
  });
376
384
  return [createResult(ResultCode.Ok, label, { id: res.id })];
377
385
  }
386
+ if (richMedia.length > 0) {
387
+ const fileInfo = await resolveRichMediaUrl(richMedia, uploadMedia);
388
+ if (!fileInfo) {
389
+ return [createResult(ResultCode.Fail, '富媒体上传失败', null)];
390
+ }
391
+ const res = await sendMessage({
392
+ content: flattenMdToText(content, val),
393
+ media: { file_info: fileInfo },
394
+ msg_type: 7,
395
+ ...baseParams
396
+ });
397
+ return [createResult(ResultCode.Ok, label, { id: res.id })];
398
+ }
378
399
  if (config.hideUnsupported === true && !content && !buildMdAndButtonsParams(val) && !buildArkParams(val)) {
379
400
  logger.info('[qq-bot] hideUnsupported: 消息内容转换后为空,跳过发送');
380
401
  return [];
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@alemonjs/qq-bot",
3
- "version": "2.1.30",
3
+ "version": "2.1.32",
4
4
  "description": "阿柠檬qq-bot平台连接",
5
5
  "author": "lemonade",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "main": "lib/index.js",
9
9
  "scripts": {
10
- "build": "lvy build"
10
+ "build": "lvy build",
11
+ "test:ws": "yarn build && node tests/websocket.integration.mjs"
11
12
  },
12
13
  "dependencies": {
13
14
  "@noble/curves": "^1.7.0",
@@ -22,6 +23,7 @@
22
23
  "mime-types": "^2.1.35",
23
24
  "public-ip": "^6.0.1",
24
25
  "qrcode": "^1.5.3",
26
+ "uuid": "^14.0.1",
25
27
  "ws": "^8.13.0"
26
28
  },
27
29
  "peerDependencies": {
@@ -92,5 +94,5 @@
92
94
  "type": "git",
93
95
  "url": "https://github.com/lemonade-lab/alemonjs.git"
94
96
  },
95
- "gitHead": "edc7f9c5c75f4f5eaf527bb1dfdb31882bde6d6c"
97
+ "gitHead": "ebea3f64e13a92c671a0ecd8af856c17ca44846d"
96
98
  }