@alemonjs/qq-bot 2.1.0 → 2.1.2

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.
package/README.md CHANGED
@@ -44,4 +44,11 @@ qq-bot:
44
44
  # 4) 域名代理模式
45
45
  # base_url_gateway: https://[your addr]
46
46
  # base_url_app_access_token: https://[your addr]
47
+ # 启动md强制转换为text,特别是当没有md权限,但使用了md数据格式时
48
+ markdownToText: true
49
+ # 隐藏不支持的消息类型(可选,默认: false)
50
+ # 开启后,转为文本时不可读内容(如 [视频]、[音频]、[图片]、[附件] 等占位符)将被置空
51
+ # 可读内容(如标题、按钮文本、链接等)仍会保留为纯文本
52
+ # 转换后内容为空时,将跳过发送并输出 info 日志
53
+ hideUnsupported: true
47
54
  ```
package/lib/config.d.ts CHANGED
@@ -3,6 +3,8 @@ export declare const platform = "qq-bot";
3
3
  export type Options = {
4
4
  master_key?: string[];
5
5
  master_id?: string[];
6
+ markdownToText?: boolean;
7
+ hideUnsupported?: boolean;
6
8
  } & sdkOptions;
7
9
  export declare const getQQBotConfig: () => Options;
8
10
  export declare const getMaster: (UserId: string) => readonly [boolean, string];
@@ -0,0 +1,5 @@
1
+ import type { DataEnums, DataMarkDown } from 'alemonjs';
2
+ export declare const markdownToText: (items: DataMarkDown["value"], hideUnsupported?: boolean) => string;
3
+ export declare const buttonsToText: (rows: any[]) => string;
4
+ export declare const markdownRawToText: (raw: string, hideUnsupported?: boolean) => string;
5
+ export declare const dataEnumToText: (item: DataEnums, hideUnsupported?: boolean) => string;
package/lib/format.js ADDED
@@ -0,0 +1,92 @@
1
+ const markdownToText = (items, hideUnsupported) => {
2
+ return items
3
+ .map(item => {
4
+ switch (item.type) {
5
+ case 'MD.text':
6
+ return item.value;
7
+ case 'MD.title':
8
+ return hideUnsupported ? `${item.value}\n` : `【${item.value}】\n`;
9
+ case 'MD.subtitle':
10
+ return hideUnsupported ? `${item.value}\n` : `〖${item.value}〗\n`;
11
+ case 'MD.bold':
12
+ case 'MD.italic':
13
+ case 'MD.italicStar':
14
+ case 'MD.strikethrough':
15
+ return item.value;
16
+ case 'MD.link': {
17
+ const v = item.value;
18
+ return `${v.text}( ${v.url} )`;
19
+ }
20
+ case 'MD.image':
21
+ return hideUnsupported ? '' : '[图片]';
22
+ case 'MD.list':
23
+ return (item.value
24
+ .map(li => {
25
+ if (typeof li.value === 'object') {
26
+ return `${li.value.index}. ${li.value.text ?? ''}`;
27
+ }
28
+ return `· ${li.value}`;
29
+ })
30
+ .join('\n') + '\n');
31
+ case 'MD.blockquote':
32
+ return `> ${item.value}\n`;
33
+ case 'MD.divider':
34
+ return hideUnsupported ? '' : '————————\n';
35
+ case 'MD.newline':
36
+ return '\n';
37
+ case 'MD.code':
38
+ return item.value;
39
+ case 'MD.mention':
40
+ if (item.value === 'everyone') {
41
+ return '@全体成员';
42
+ }
43
+ return `@${item.value ?? ''}`;
44
+ case 'MD.content':
45
+ return item.value;
46
+ case 'MD.button':
47
+ return hideUnsupported ? String(item.value) : `[${item.value}]`;
48
+ default:
49
+ return String(item?.value ?? '');
50
+ }
51
+ })
52
+ .join('');
53
+ };
54
+ const buttonsToText = (rows) => {
55
+ return rows.map((row) => row.value.map((btn) => `[${btn.value}]`).join(' ')).join('\n');
56
+ };
57
+ const markdownRawToText = (raw, hideUnsupported) => {
58
+ let text = raw;
59
+ text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, hideUnsupported ? '' : '[图片]');
60
+ text = text.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1');
61
+ text = text.replace(/^#{1,6}\s+/gm, '');
62
+ text = text.replace(/(\*{3}|_{3})([^*_]+)\1/g, '$2');
63
+ text = text.replace(/(\*{2}|_{2})([^*_]+)\1/g, '$2');
64
+ text = text.replace(/(?<!\*)\*(?!\*)([^*]+)(?<!\*)\*(?!\*)/g, '$1');
65
+ text = text.replace(/(?<!_)_(?!_)([^_]+)(?<!_)_(?!_)/g, '$1');
66
+ text = text.replace(/~~([^~]+)~~/g, '$1');
67
+ text = text.replace(/`([^`]+)`/g, '$1');
68
+ text = text.replace(/```[\s\S]*?```/g, match => {
69
+ return match.replace(/```\w*\n?/g, '').trim();
70
+ });
71
+ text = text.replace(/^>\s+/gm, '');
72
+ text = text.replace(/^[-*_]{3,}\s*$/gm, hideUnsupported ? '' : '————————');
73
+ text = text.replace(/^[\s]*[-*+]\s+/gm, '· ');
74
+ text = text.replace(/^[\s]*(\d+)\.\s+/gm, '$1. ');
75
+ return text.trim();
76
+ };
77
+ const dataEnumToText = (item, hideUnsupported) => {
78
+ switch (item.type) {
79
+ case 'MarkdownOriginal':
80
+ return markdownRawToText(String(item.value), hideUnsupported);
81
+ case 'Attachment':
82
+ return hideUnsupported ? '' : `[附件${item.options?.filename ? ': ' + item.options.filename : ''}]`;
83
+ case 'Audio':
84
+ return hideUnsupported ? '' : '[音频]';
85
+ case 'Video':
86
+ return hideUnsupported ? '' : '[视频]';
87
+ default:
88
+ return '';
89
+ }
90
+ };
91
+
92
+ export { buttonsToText, dataEnumToText, markdownRawToText, markdownToText };
package/lib/sends.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { createResult, ResultCode } from 'alemonjs';
3
3
  import axios from 'axios';
4
+ import { dataEnumToText, markdownToText, buttonsToText } from './format.js';
5
+ import { getQQBotConfig } from './config.js';
4
6
 
5
7
  const MAX_BUTTON_ROWS = 5;
6
8
  const MAX_BUTTONS_PER_ROW = 5;
@@ -162,7 +164,21 @@ const formatMention = (item, mode) => {
162
164
  return '';
163
165
  };
164
166
  const extractContent = (val, mode) => {
165
- return val
167
+ const nativeTypes = new Set([
168
+ 'Mention',
169
+ 'Text',
170
+ 'Link',
171
+ 'Image',
172
+ 'ImageFile',
173
+ 'ImageURL',
174
+ 'Markdown',
175
+ 'BT.group',
176
+ 'ButtonTemplate',
177
+ 'Ark.list',
178
+ 'Ark.Card',
179
+ 'Ark.BigCard'
180
+ ]);
181
+ const nativeText = val
166
182
  .filter(item => item.type === 'Mention' || item.type === 'Text' || item.type === 'Link')
167
183
  .map(item => {
168
184
  if (item.type === 'Link') {
@@ -177,6 +193,14 @@ const extractContent = (val, mode) => {
177
193
  return '';
178
194
  })
179
195
  .join('');
196
+ const config = getQQBotConfig();
197
+ const hide = config.hideUnsupported === true;
198
+ const fallbackText = val
199
+ .filter(item => !nativeTypes.has(item.type))
200
+ .map(item => dataEnumToText(item, hide))
201
+ .filter(Boolean)
202
+ .join('\n');
203
+ return [nativeText, fallbackText].filter(Boolean).join('\n');
180
204
  };
181
205
  const buildBaseParams = (tag, messageId, interactionTag) => {
182
206
  if (tag === interactionTag) {
@@ -288,23 +312,57 @@ const resolveRichMediaUrl = async (images, uploadMedia) => {
288
312
  }
289
313
  return undefined;
290
314
  };
315
+ const flattenMdToText = (content, val) => {
316
+ const mdItems = val.filter(item => item.type === 'Markdown');
317
+ const btnItems = val.filter(item => item.type === 'BT.group');
318
+ const parts = [content];
319
+ for (const item of mdItems) {
320
+ if (item.type === 'Markdown' && typeof item.value !== 'string') {
321
+ parts.push(markdownToText(item.value));
322
+ }
323
+ }
324
+ for (const item of btnItems) {
325
+ if (item.type === 'BT.group' && typeof item.value !== 'string') {
326
+ parts.push(buttonsToText(item.value));
327
+ }
328
+ }
329
+ return parts.filter(Boolean).join('\n');
330
+ };
291
331
  const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMessage, label) => {
332
+ const config = getQQBotConfig();
333
+ const mdToText = config.markdownToText === true;
292
334
  const images = filterImages(val);
293
335
  if (images.length > 0) {
294
336
  const url = await resolveRichMediaUrl(images, uploadMedia);
295
337
  if (!url) {
296
338
  return [createResult(ResultCode.Fail, '图片上传失败', null)];
297
339
  }
340
+ const imgContent = flattenMdToText(content, val);
298
341
  const res = await sendMessage({
299
- content,
342
+ content: imgContent,
300
343
  media: { file_info: url },
301
344
  msg_type: 7,
302
345
  ...baseParams
303
346
  });
304
347
  return [createResult(ResultCode.Ok, label, { id: res.id })];
305
348
  }
349
+ if (config.hideUnsupported === true && !content && !buildMdAndButtonsParams(val) && !buildArkParams(val)) {
350
+ logger.info('[qq-bot] hideUnsupported: 消息内容转换后为空,跳过发送');
351
+ return [];
352
+ }
353
+ if (mdToText) {
354
+ const textContent = flattenMdToText(content, val);
355
+ if (textContent) {
356
+ const res = await sendMessage({ content: textContent, msg_type: 0, ...baseParams });
357
+ return [createResult(ResultCode.Ok, label, { id: res.id })];
358
+ }
359
+ return [];
360
+ }
306
361
  const mdParams = buildMdAndButtonsParams(val);
307
362
  if (mdParams) {
363
+ if (mdParams.markdown?.content && content) {
364
+ mdParams.markdown.content = content + '\n' + mdParams.markdown.content;
365
+ }
308
366
  const res = await sendMessage({ content, msg_type: 2, ...mdParams, ...baseParams });
309
367
  return [createResult(ResultCode.Ok, label, { id: res.id })];
310
368
  }
@@ -346,14 +404,32 @@ const resolveImageBuffer = async (images) => {
346
404
  return null;
347
405
  };
348
406
  const sendGuildMessage = async (content, val, baseParams, sendMessage, label) => {
407
+ const config = getQQBotConfig();
408
+ const mdToText = config.markdownToText === true;
349
409
  const images = filterImages(val);
350
410
  if (images.length > 0) {
351
411
  const imageBuffer = await resolveImageBuffer(images);
352
- const res = await sendMessage({ content, ...baseParams }, imageBuffer);
412
+ const imgContent = flattenMdToText(content, val);
413
+ const res = await sendMessage({ content: imgContent, ...baseParams }, imageBuffer);
353
414
  return [createResult(ResultCode.Ok, label, { id: res?.id })];
354
415
  }
416
+ if (config.hideUnsupported === true && !content && !buildMdAndButtonsParams(val) && !buildArkParams(val)) {
417
+ logger.info('[qq-bot] hideUnsupported: 消息内容转换后为空,跳过发送');
418
+ return [];
419
+ }
420
+ if (mdToText) {
421
+ const textContent = flattenMdToText(content, val);
422
+ if (textContent) {
423
+ const res = await sendMessage({ content: textContent, ...baseParams });
424
+ return [createResult(ResultCode.Ok, label, { id: res?.id })];
425
+ }
426
+ return [];
427
+ }
355
428
  const mdParams = buildMdAndButtonsParams(val);
356
429
  if (mdParams) {
430
+ if (mdParams.markdown?.content && content) {
431
+ mdParams.markdown.content = content + '\n' + mdParams.markdown.content;
432
+ }
357
433
  const res = await sendMessage({ content: '', ...mdParams, ...baseParams });
358
434
  return [createResult(ResultCode.Ok, label, { id: res.id })];
359
435
  }
package/lib/types.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ export type DataArkListTip = {
2
+ type: 'Ark.listTip';
3
+ value: {
4
+ desc: string;
5
+ prompt: string;
6
+ };
7
+ };
8
+ export type DataArkListItem = {
9
+ type: 'Ark.listItem';
10
+ value: string | {
11
+ title: string;
12
+ link: string;
13
+ };
14
+ };
15
+ export type DataArkListContent = {
16
+ type: 'Ark.listContent';
17
+ value: DataArkListItem[];
18
+ };
19
+ export type DataArkList = {
20
+ type: 'Ark.list';
21
+ value: [DataArkListTip, DataArkListContent];
22
+ };
23
+ export type DataArkCard = {
24
+ type: 'Ark.Card';
25
+ value: {
26
+ title: string;
27
+ cover: string;
28
+ link: string;
29
+ subtitle: string;
30
+ decs: string;
31
+ prompt: string;
32
+ metadecs: string;
33
+ };
34
+ };
35
+ export type DataArkBigCard = {
36
+ type: 'Ark.BigCard';
37
+ value: {
38
+ title: string;
39
+ subtitle: string;
40
+ cover: string;
41
+ link: string;
42
+ prompt: string;
43
+ };
44
+ };
45
+ export type DataButtonTemplate = {
46
+ type: 'ButtonTemplate';
47
+ value: string;
48
+ };
package/lib/types.js ADDED
@@ -0,0 +1 @@
1
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alemonjs/qq-bot",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "阿柠檬qq-bot平台连接",
5
5
  "author": "lemonade",
6
6
  "license": "MIT",