@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 +7 -0
- package/lib/config.d.ts +2 -0
- package/lib/format.d.ts +5 -0
- package/lib/format.js +92 -0
- package/lib/sends.js +79 -3
- package/lib/types.d.ts +48 -0
- package/lib/types.js +1 -0
- package/package.json +1 -1
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];
|
package/lib/format.d.ts
ADDED
|
@@ -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
|
-
|
|
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
|
|
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
|
+
|