@karinjs/adapter-qqbot 3.1.0 → 3.3.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.
- package/README.md +12 -4
- package/dist/apps/cleanup.js +1 -1
- package/dist/apps/login.js +1 -1
- package/dist/apps/qqbot-file-to-url.d.ts +4 -0
- package/dist/apps/qqbot-file-to-url.js +94 -0
- package/dist/common-DwVsAvIo.js +47 -0
- package/dist/{config-D1_LqrcJ.js → config-BgCU6apn.js} +2 -6
- package/dist/index.d.ts +357 -29
- package/dist/index.js +904 -389
- package/dist/{message-CVJVFAGR.js → message-CydR-wrX.js} +1 -1
- package/dist/utils-u17tvC2U.js +5 -0
- package/dist/web.config.d.ts +0 -1
- package/dist/web.config.js +4 -12
- package/package.json +8 -6
- package/dist/utils-QpIUJhxK.js +0 -32
package/README.md
CHANGED
|
@@ -25,7 +25,6 @@ pnpm add @karinjs/adapter-qqbot
|
|
|
25
25
|
"qqEnable": true,
|
|
26
26
|
"guildEnable": true,
|
|
27
27
|
"guildMode": 0,
|
|
28
|
-
"markdown": { "enable": true },
|
|
29
28
|
"event": { "type": 2 }
|
|
30
29
|
}
|
|
31
30
|
]
|
|
@@ -33,7 +32,14 @@ pnpm add @karinjs/adapter-qqbot
|
|
|
33
32
|
|
|
34
33
|
- `event.type`: `0` 关闭、`1` Webhook、`2` WebSocket(默认)。
|
|
35
34
|
- `guildMode`: `0` 公域,只收 @ 消息;`1` 私域,接收全部频道消息。
|
|
36
|
-
|
|
35
|
+
|
|
36
|
+
> 旧版本里的 `markdown.enable` 已移除。发送通道现在由平台能力决定,不再需要用户配置:
|
|
37
|
+
> QQ 单聊和群聊的自定义 Markdown 已对所有机器人开放,固定走 Markdown 通道;频道场景仍需向 QQ
|
|
38
|
+
> 申请内邀开通,适配器会先尝试 Markdown,被平台以权限错误拒绝后自动降级为普通消息,并记住这个
|
|
39
|
+
> 机器人,本次运行内不再重试。开通后重启 Karin(或改一次配置)即可恢复。配置文件里残留的
|
|
40
|
+
> `markdown` 字段会被忽略,下次保存配置时自动清除。
|
|
41
|
+
>
|
|
42
|
+
> 频道降级为普通消息后按钮无法发送——平台的 `keyboard` 只能挂在 Markdown 消息上。
|
|
37
43
|
|
|
38
44
|
## 按钮
|
|
39
45
|
|
|
@@ -119,9 +125,9 @@ export const menuKeyboard = karin.button(/^菜单(?:\s+\d+)?$/, (next, args) =>
|
|
|
119
125
|
- 较大的图片或视频直接交给 QQ 发送时,QQ 客户端可能会把它显示成群文件,而不是图片或视频卡片;配置 `fileToUrl` 后通常可以避免这种显示问题。
|
|
120
126
|
- 直接交给 QQ 只是兜底方案,生产环境仍然建议准备自己的文件服务。
|
|
121
127
|
|
|
122
|
-
> **注意:Markdown
|
|
128
|
+
> **注意:Markdown 里的图片依赖 `fileToUrl`。**
|
|
123
129
|
>
|
|
124
|
-
>
|
|
130
|
+
> Markdown 里的图片只能写成 QQ 能访问的链接,适配器不能把 Markdown 文本里的图片自动当成附件上传。QQ 单聊和群聊有下面提到的内置临时图床可以兜住,频道场景则必须自己配置 `fileToUrl`。
|
|
125
131
|
|
|
126
132
|
你可以在自己的 Karin 插件中编写并注册 `fileToUrl` Handler:
|
|
127
133
|
|
|
@@ -154,6 +160,8 @@ export const uploadResource = karin.handler('fileToUrl', async (args) => {
|
|
|
154
160
|
|
|
155
161
|
建议生产环境都配置 `fileToUrl`。这样图片、视频和文件都可以先上传到你自己的文件服务,再交给 QQ 发送,成功率和可控性都会更好。
|
|
156
162
|
|
|
163
|
+
没有自己的图床也不至于完全用不了 Markdown 图片:适配器内置了一个基于 QQ 官方分片上传的临时图床,QQ 单聊和群聊里会自动生效,把本地图片、截图、Base64 图片换成临时直链后嵌进 Markdown。它排在你自己的 `fileToUrl` 之后,只在你没有配置、或者你的图床这次没接手时才出手,拿到的地址会过期,因此仅供当次发送使用。频道场景和文件类型用不了它,仍然需要自备 `fileToUrl`。
|
|
164
|
+
|
|
157
165
|
## 事件
|
|
158
166
|
|
|
159
167
|
- 支持 QQ 单聊、群聊、频道、频道私信和按钮回调。
|
package/dist/apps/cleanup.js
CHANGED
package/dist/apps/login.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as getDefaultConfig, n as config, o as writeConfig } from "../config-
|
|
1
|
+
import { i as getDefaultConfig, n as config, o as writeConfig } from "../config-BgCU6apn.js";
|
|
2
2
|
import karin, { segment } from "node-karin";
|
|
3
3
|
import qrcode from "qrcode";
|
|
4
4
|
import axios from "node-karin/axios";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { n as getImageSize } from "../common-DwVsAvIo.js";
|
|
2
|
+
import karin, { getAllBot } from "node-karin";
|
|
3
|
+
|
|
4
|
+
//#region src/apps/qqbot-file-to-url.ts
|
|
5
|
+
/**
|
|
6
|
+
* 内置 `fileToUrl` 处理器:用 QQ 官方分片上传充当临时图床。
|
|
7
|
+
*
|
|
8
|
+
* 分片上传完成后,合并响应会带上资源的下载直链。有了这个地址,本地图片、截图、
|
|
9
|
+
* base64 图片就能以 markdown 图片语法进入消息正文,和文本、按钮合并成一条消息,
|
|
10
|
+
* 而不是退化成一条独立的富媒体消息。
|
|
11
|
+
*
|
|
12
|
+
* 无需任何配置:适配器发送消息时会把执行发送的 bot 和真实发送目标一并交给本
|
|
13
|
+
* 处理器,上传因此总是发生在同一个 bot、同一个会话下。QQ 的 openid 按 bot
|
|
14
|
+
* 隔离,这一点不能靠猜。上传只借用会话的上传通道,不会往会话里发任何消息。
|
|
15
|
+
*
|
|
16
|
+
* 只在以下条件全部满足时接手,其余情况一律放行给链路上的其他处理器:
|
|
17
|
+
* - 资源要被写进 markdown 正文(富媒体上传不需要先换成 URL);
|
|
18
|
+
* - 类型是图片 / 视频 / 语音(QQ 不为文件类型返回直链);
|
|
19
|
+
* - 能确定执行上传的 bot 与归属会话(频道场景没有可用 openid)。
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* 判断已注册的 bot 是否由本插件提供。
|
|
23
|
+
*
|
|
24
|
+
* @param bot Karin 注册表里的 bot 实例。
|
|
25
|
+
* @returns 是否为 QQBot 适配器。
|
|
26
|
+
*/
|
|
27
|
+
const isQQBot = (bot) => {
|
|
28
|
+
const candidate = bot;
|
|
29
|
+
return candidate?.adapter?.protocol === "qqbot" && !!candidate.super?.media;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 按 appId 取出对应的 QQBot。
|
|
33
|
+
*
|
|
34
|
+
* 不做「随便挑一个」的兜底:openid 按 bot 隔离,用错 bot 上传要么失败,要么把
|
|
35
|
+
* 文件传到了另一个账号的会话下,只能定位到确切的那一个。
|
|
36
|
+
*
|
|
37
|
+
* @param selfId 目标 bot 的 appId。
|
|
38
|
+
* @returns 对应的 QQBot 适配器,未注册时返回 undefined。
|
|
39
|
+
*/
|
|
40
|
+
const findBot = (selfId) => {
|
|
41
|
+
return getAllBot().filter(isQQBot).find((bot) => bot.selfId === selfId);
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* 把处理器收到的文件参数归一成 media 接口可用的来源字符串。
|
|
45
|
+
*
|
|
46
|
+
* @param file 处理器入参里的文件数据。
|
|
47
|
+
* @returns 路径 / URL 字符串,或 base64:// 形式的内容。
|
|
48
|
+
*/
|
|
49
|
+
const toUploadSource = (file) => {
|
|
50
|
+
if (typeof file === "string") return file;
|
|
51
|
+
return `base64://${Buffer.from(file).toString("base64")}`;
|
|
52
|
+
};
|
|
53
|
+
const qqbotFileToUrl = karin.handler("fileToUrl", async (payload, next) => {
|
|
54
|
+
/** fileToUrl 把第四个参数原样放在 args 字段下。 */
|
|
55
|
+
const { file, type, filename, args: extra } = payload;
|
|
56
|
+
/**
|
|
57
|
+
* 富媒体上传接受本地文件与 base64,先换成 URL 再交给 QQ 反而多一次往返,
|
|
58
|
+
* 而且那次往返仍然是 QQ 自己下载转存,没有收益。
|
|
59
|
+
*/
|
|
60
|
+
if (extra?.purpose !== "markdown") return next();
|
|
61
|
+
/** QQ 只对图片 / 视频 / 语音返回下载直链。 */
|
|
62
|
+
if (type === "file") return next();
|
|
63
|
+
/** 频道场景用 channel_id,没有上传接口要求的 openid。 */
|
|
64
|
+
const origin = extra.origin;
|
|
65
|
+
if (!extra.selfId || !origin) return next();
|
|
66
|
+
const bot = findBot(extra.selfId);
|
|
67
|
+
if (!bot) return next();
|
|
68
|
+
try {
|
|
69
|
+
const { url, response } = await bot.super.media.uploadForUrl(origin.scene, origin.peer, type, toUploadSource(file), filename);
|
|
70
|
+
bot.logger("debug", `[内置图床] 已取得直链,有效期 ${response.ttl}s: ${url}`);
|
|
71
|
+
if (type !== "image") return { url };
|
|
72
|
+
/** markdown 图片语法需要宽高;解析不出来时 getImageSize 会给出保守默认值。 */
|
|
73
|
+
const { width, height } = await getImageSize(file);
|
|
74
|
+
return {
|
|
75
|
+
url,
|
|
76
|
+
width,
|
|
77
|
+
height
|
|
78
|
+
};
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
81
|
+
bot.logger("debug", `[内置图床] 取直链失败,交给后续处理器: ${reason}`);
|
|
82
|
+
return next();
|
|
83
|
+
}
|
|
84
|
+
}, {
|
|
85
|
+
name: "QQBot 分片上传图床",
|
|
86
|
+
/**
|
|
87
|
+
* rank 升序执行,默认 10000。这里排在默认值之后:本处理器给出的是会过期的
|
|
88
|
+
* 临时地址,使用方自建的图床通常是长期地址,应当优先生效。
|
|
89
|
+
*/
|
|
90
|
+
rank: 2e4
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
export { qqbotFileToUrl };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { common, logger } from "node-karin";
|
|
2
|
+
import qrcode from "qrcode";
|
|
3
|
+
import size from "image-size";
|
|
4
|
+
import lodash from "node-karin/lodash";
|
|
5
|
+
import "@karinjs/geturls";
|
|
6
|
+
|
|
7
|
+
//#region src/core/constants/regex.ts
|
|
8
|
+
/** HTTP(S) 资源已经是可直接传给 QQ 平台的公网地址。 */
|
|
9
|
+
const HTTP_URL_RE = /^https?:\/\//i;
|
|
10
|
+
/** Karin 常见 base64 图片/媒体前缀。 */
|
|
11
|
+
const BASE64_PREFIX_RE = /^base64:\/\//i;
|
|
12
|
+
/** 标准 data URL 的 base64 前缀。 */
|
|
13
|
+
const DATA_URL_BASE64_RE = /^data:[^;,]+;base64,/i;
|
|
14
|
+
/**
|
|
15
|
+
* 纯数字且 5-11 位:一定是 QQ 号/群号。
|
|
16
|
+
* openid 是 32 位十六进制(或带连字符的 UUID 形态),不可能匹配此规则。
|
|
17
|
+
*/
|
|
18
|
+
const QQ_NUMBER_RE = /^\d{5,11}$/;
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/utils/common.ts
|
|
22
|
+
/** 随机数 */
|
|
23
|
+
const random = (min, max) => lodash.random(min, max);
|
|
24
|
+
/**
|
|
25
|
+
* 伪事件日志(红字)
|
|
26
|
+
*/
|
|
27
|
+
const fakeEvent = (log) => {
|
|
28
|
+
logger.fatal(`${logger.red("[QQ Official Bot][虚假事件]")} ${log.replace(/\n/g, "\\n")}`);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 读取图片宽高
|
|
32
|
+
*
|
|
33
|
+
* @param source 图片来源,支持 URL、本地路径、base64://、完整 data URL 与已在内存中的 Buffer。
|
|
34
|
+
* @returns 图片宽高;解析失败时回落到 100x100。
|
|
35
|
+
*/
|
|
36
|
+
const getImageSize = async (source) => {
|
|
37
|
+
/** karin 的 common.buffer 不认识带 MIME 头的 data URL,先归一为 base64:// 形式 */
|
|
38
|
+
if (typeof source === "string" && DATA_URL_BASE64_RE.test(source)) source = source.replace(DATA_URL_BASE64_RE, "base64://");
|
|
39
|
+
const { width = 100, height = 100 } = size(await common.buffer(source));
|
|
40
|
+
return {
|
|
41
|
+
width,
|
|
42
|
+
height
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { DATA_URL_BASE64_RE as a, BASE64_PREFIX_RE as i, getImageSize as n, HTTP_URL_RE as o, random as r, QQ_NUMBER_RE as s, fakeEvent as t };
|
|
@@ -80,19 +80,16 @@ const writeConfig = (data) => {
|
|
|
80
80
|
syncCache(normalized);
|
|
81
81
|
};
|
|
82
82
|
/**
|
|
83
|
-
*
|
|
83
|
+
* 用默认值补齐用户配置,并清理已废弃字段
|
|
84
84
|
*/
|
|
85
85
|
const formatConfig = (user) => {
|
|
86
86
|
const def = getDefaultConfig()[0];
|
|
87
87
|
return user.map((item) => {
|
|
88
|
+
/** markdown 是历史字段,解构出来即丢弃,见 {@link LegacyQQBotConfig}。 */
|
|
88
89
|
const { markdown, messageCache, event, ...rest } = item;
|
|
89
90
|
return {
|
|
90
91
|
...def,
|
|
91
92
|
...rest,
|
|
92
|
-
markdown: {
|
|
93
|
-
...def.markdown,
|
|
94
|
-
...markdown
|
|
95
|
-
},
|
|
96
93
|
messageCache: normalizeMessageCache(def.messageCache, messageCache),
|
|
97
94
|
event: {
|
|
98
95
|
...def.event,
|
|
@@ -153,7 +150,6 @@ const getDefaultConfig = () => [{
|
|
|
153
150
|
reg: "^/",
|
|
154
151
|
rep: "#"
|
|
155
152
|
}],
|
|
156
|
-
markdown: { enable: true },
|
|
157
153
|
messageCache: {
|
|
158
154
|
enable: false,
|
|
159
155
|
self: false,
|