@karinjs/adapter-qqbot 3.0.8 → 3.2.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 +4 -2
- package/dist/apps/qqbot-file-to-url.d.ts +4 -0
- package/dist/apps/qqbot-file-to-url.js +94 -0
- package/dist/common-OHGAoohl.js +42 -0
- package/dist/index.d.ts +991 -15
- package/dist/index.js +1346 -317
- package/dist/utils-C0oZi6PR.js +5 -0
- package/dist/web.config.js +1 -1
- package/package.json +1 -1
- package/dist/utils-QpIUJhxK.js +0 -32
package/README.md
CHANGED
|
@@ -119,9 +119,9 @@ export const menuKeyboard = karin.button(/^菜单(?:\s+\d+)?$/, (next, args) =>
|
|
|
119
119
|
- 较大的图片或视频直接交给 QQ 发送时,QQ 客户端可能会把它显示成群文件,而不是图片或视频卡片;配置 `fileToUrl` 后通常可以避免这种显示问题。
|
|
120
120
|
- 直接交给 QQ 只是兜底方案,生产环境仍然建议准备自己的文件服务。
|
|
121
121
|
|
|
122
|
-
> **注意:Markdown
|
|
122
|
+
> **注意:Markdown 里的图片依赖 `fileToUrl`。**
|
|
123
123
|
>
|
|
124
|
-
>
|
|
124
|
+
> Markdown 里的图片只能写成 QQ 能访问的链接,适配器不能把 Markdown 文本里的图片自动当成附件上传。QQ 单聊和群聊有下面提到的内置临时图床可以兜住,频道场景则必须自己配置 `fileToUrl`。
|
|
125
125
|
|
|
126
126
|
你可以在自己的 Karin 插件中编写并注册 `fileToUrl` Handler:
|
|
127
127
|
|
|
@@ -154,6 +154,8 @@ export const uploadResource = karin.handler('fileToUrl', async (args) => {
|
|
|
154
154
|
|
|
155
155
|
建议生产环境都配置 `fileToUrl`。这样图片、视频和文件都可以先上传到你自己的文件服务,再交给 QQ 发送,成功率和可控性都会更好。
|
|
156
156
|
|
|
157
|
+
没有自己的图床也不至于完全用不了 Markdown 图片:适配器内置了一个基于 QQ 官方分片上传的临时图床,QQ 单聊和群聊里会自动生效,把本地图片、截图、Base64 图片换成临时直链后嵌进 Markdown。它排在你自己的 `fileToUrl` 之后,只在你没有配置、或者你的图床这次没接手时才出手,拿到的地址会过期,因此仅供当次发送使用。频道场景和文件类型用不了它,仍然需要自备 `fileToUrl`。
|
|
158
|
+
|
|
157
159
|
## 事件
|
|
158
160
|
|
|
159
161
|
- 支持 QQ 单聊、群聊、频道、频道私信和按钮回调。
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { n as getImageSize } from "../common-OHGAoohl.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,42 @@
|
|
|
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
|
+
//#endregion
|
|
16
|
+
//#region src/utils/common.ts
|
|
17
|
+
/** 随机数 */
|
|
18
|
+
const random = (min, max) => lodash.random(min, max);
|
|
19
|
+
/**
|
|
20
|
+
* 伪事件日志(红字)
|
|
21
|
+
*/
|
|
22
|
+
const fakeEvent = (log) => {
|
|
23
|
+
logger.fatal(`${logger.red("[QQ Official Bot][虚假事件]")} ${log.replace(/\n/g, "\\n")}`);
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* 读取图片宽高
|
|
27
|
+
*
|
|
28
|
+
* @param source 图片来源,支持 URL、本地路径、base64://、完整 data URL 与已在内存中的 Buffer。
|
|
29
|
+
* @returns 图片宽高;解析失败时回落到 100x100。
|
|
30
|
+
*/
|
|
31
|
+
const getImageSize = async (source) => {
|
|
32
|
+
/** karin 的 common.buffer 不认识带 MIME 头的 data URL,先归一为 base64:// 形式 */
|
|
33
|
+
if (typeof source === "string" && DATA_URL_BASE64_RE.test(source)) source = source.replace(DATA_URL_BASE64_RE, "base64://");
|
|
34
|
+
const { width = 100, height = 100 } = size(await common.buffer(source));
|
|
35
|
+
return {
|
|
36
|
+
width,
|
|
37
|
+
height
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { DATA_URL_BASE64_RE as a, BASE64_PREFIX_RE as i, getImageSize as n, HTTP_URL_RE as o, random as r, fakeEvent as t };
|