@karinjs/adapter-qqbot 1.3.1 → 2.1.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.
@@ -0,0 +1,5 @@
1
+ //#region src/apps/cleanup.d.ts
2
+ /** 每 10 分钟清理一次过期的 getMsg/getHistoryMsg 本地缓存。 */
3
+ declare const clearMessageCache: import("node-karin").Task | undefined;
4
+ //#endregion
5
+ export { clearMessageCache };
@@ -0,0 +1,22 @@
1
+ import { n as config } from "../config-DKa7596f.js";
2
+ import { t as getMessageStore } from "../message-O73AeL_P.js";
3
+ import karin from "node-karin";
4
+
5
+ //#region src/apps/cleanup.ts
6
+ /**
7
+ * 判断是否有任意 QQBot 配置开启消息缓存。
8
+ *
9
+ * @returns true 表示需要注册消息缓存清理任务。
10
+ */
11
+ const enabledMessageCache = () => {
12
+ return config().some((item) => item.messageCache.enable);
13
+ };
14
+ /** 每 10 分钟清理一次过期的 getMsg/getHistoryMsg 本地缓存。 */
15
+ const clearMessageCache = enabledMessageCache() ? karin.task("qqbot 适配器消息缓存清理", "*/10 * * * *", async () => await getMessageStore().cleanupExpired(), {
16
+ name: "qqbot 适配器消息缓存清理任务",
17
+ log: false,
18
+ type: "skip"
19
+ }) : void 0;
20
+
21
+ //#endregion
22
+ export { clearMessageCache };
@@ -0,0 +1,18 @@
1
+ //#region src/apps/login.d.ts
2
+ /**
3
+ * #QQ登录 指令
4
+ *
5
+ * 仅主人可用。匹配 `#QQ登录` 或 `#qqlogin`(大小写均可)。
6
+ * 必须带 `#` 前缀。
7
+ *
8
+ * 流程:
9
+ * 1. 立即回复"开始扫码"
10
+ * 2. 触发扫码授权流程
11
+ * 3. 每次生成新二维码 → 把 PNG 图片回复给触发者
12
+ * 4. 授权成功 → 回复结果,watch 回调自动初始化 bot
13
+ *
14
+ * ⚠ 注意:扫码授权会刷新该 appId 的 secret,旧 secret 立即失效
15
+ */
16
+ declare const qqLogin: import("node-karin").Command<keyof import("node-karin").MessageEventMap>;
17
+ //#endregion
18
+ export { qqLogin };
@@ -0,0 +1,106 @@
1
+ import { c as log } from "../config-DKa7596f.js";
2
+ import { n as runQrOnboard } from "../onboard-DPWgX39H.js";
3
+ import karin, { segment } from "node-karin";
4
+
5
+ //#region src/apps/login.ts
6
+ /**
7
+ * #QQ登录 指令
8
+ *
9
+ * 仅主人可用。匹配 `#QQ登录` 或 `#qqlogin`(大小写均可)。
10
+ * 必须带 `#` 前缀。
11
+ *
12
+ * 流程:
13
+ * 1. 立即回复"开始扫码"
14
+ * 2. 触发扫码授权流程
15
+ * 3. 每次生成新二维码 → 把 PNG 图片回复给触发者
16
+ * 4. 授权成功 → 回复结果,watch 回调自动初始化 bot
17
+ *
18
+ * ⚠ 注意:扫码授权会刷新该 appId 的 secret,旧 secret 立即失效
19
+ */
20
+ /** 互斥锁:避免并发扫码 */
21
+ let busy = false;
22
+ /**
23
+ * 判断触发 #QQ登录 的上游是否为 Karin 控制台适配器。
24
+ * Console 适配器无法直接在聊天窗口展示二维码图片,因此需要额外打印终端二维码。
25
+ */
26
+ const isConsoleAdapter = (bot) => {
27
+ const selfId = String(bot.selfId || "").toLowerCase();
28
+ const name = String(bot.adapter.name || "").toLowerCase();
29
+ const protocol = String(bot.adapter.protocol || "").toLowerCase();
30
+ return selfId === "console" || name === "console" || name.endsWith("/console") || protocol === "console";
31
+ };
32
+ /**
33
+ * 记录一次回复返回的消息 ID,便于扫码流程结束后清理二维码、链接等临时信息。
34
+ * @param result 回复返回值。
35
+ * @param messageIds 已登记的消息 ID 集合。
36
+ */
37
+ const trackReply = (result, messageIds) => {
38
+ const messageId = result?.messageId;
39
+ if (messageId && !messageIds.includes(messageId)) messageIds.push(messageId);
40
+ };
41
+ /**
42
+ * 撤回扫码流程中发出的临时消息。
43
+ * @param bot 触发登录的机器人实例。
44
+ * @param contact 回复所在会话。
45
+ * @param messageIds 需要撤回的消息 ID。
46
+ */
47
+ const recallTrackedReplies = async (bot, contact, messageIds) => {
48
+ for (const messageId of messageIds) try {
49
+ await bot.recallMsg(contact, messageId);
50
+ } catch (err) {
51
+ log("warn", `[扫码登录] 撤回临时消息失败: ${messageId}`, err);
52
+ }
53
+ messageIds.length = 0;
54
+ };
55
+ const qqLogin = karin.command(/^#(?:qq登录|qqlogin)$/i, async (e) => {
56
+ if (busy) {
57
+ await e.reply("已有扫码授权在进行中,请稍候再试");
58
+ return;
59
+ }
60
+ busy = true;
61
+ const messageIds = [];
62
+ /**
63
+ * 发送并登记扫码流程的临时回复。
64
+ * @param elements 回复内容。
65
+ */
66
+ const replyAndTrack = async (elements) => {
67
+ const result = await e.reply(elements);
68
+ trackReply(result, messageIds);
69
+ return result;
70
+ };
71
+ try {
72
+ await replyAndTrack([segment.text("已开始 QQ 扫码登录授权\n⚠ 扫码完成会刷新该机器人的 secret,旧 secret 立即失效")]);
73
+ log("info", `[扫码登录] 由用户 ${e.userId} 触发`);
74
+ const ok = await runQrOnboard({
75
+ printTerminal: isConsoleAdapter(e.bot),
76
+ onQr: async ({ base64, url, refresh }) => {
77
+ const head = refresh === 0 ? "请使用手机 QQ 扫描二维码完成授权(二维码 60s 内有效,过期会自动刷新最多 3 次):" : `上一张二维码已过期,已自动刷新(第 ${refresh}/3 次,仍为 60s 有效期):`;
78
+ try {
79
+ await replyAndTrack([
80
+ segment.text(head),
81
+ segment.image(base64),
82
+ segment.text(`或在手机 QQ 打开链接:\n${url}`)
83
+ ]);
84
+ } catch (err) {
85
+ log("warn", "[扫码登录] 推送二维码失败:", err);
86
+ }
87
+ }
88
+ });
89
+ await recallTrackedReplies(e.bot, e.contact, messageIds);
90
+ if (ok) await e.reply("扫码授权成功,配置已保存,QQBot 实例正在初始化…");
91
+ else await e.reply("扫码授权失败或超时,请重试 #QQ登录");
92
+ } catch (err) {
93
+ log("error", "[扫码登录] 异常:", err);
94
+ await recallTrackedReplies(e.bot, e.contact, messageIds);
95
+ await e.reply("扫码授权过程中发生异常,详情见服务端日志");
96
+ } finally {
97
+ busy = false;
98
+ }
99
+ }, {
100
+ name: "qqbot:login",
101
+ permission: "master",
102
+ authFailMsg: "#QQ登录 仅限主人使用"
103
+ });
104
+
105
+ //#endregion
106
+ export { qqLogin };
@@ -0,0 +1,49 @@
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 GetUrls from "@karinjs/geturls";
6
+
7
+ //#region src/utils/common.ts
8
+ /** 睡眠 */
9
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10
+ /** 随机数 */
11
+ const random = (min, max) => lodash.random(min, max);
12
+ /**
13
+ * 伪事件日志(红字)
14
+ */
15
+ const fakeEvent = (log) => {
16
+ logger.fatal(`${logger.red("[QQ Official Bot][虚假事件]")} ${log.replace(/\n/g, "\\n")}`);
17
+ };
18
+ /**
19
+ * 提取文本中的 URL 列表
20
+ * 中文不符合 URL 规范,先替换为 `|` 再扫描
21
+ */
22
+ const handleUrl = (text, exclude = []) => {
23
+ const cleaned = text.replace(/[一-龥]/g, "|");
24
+ return GetUrls.getUrls(cleaned, {
25
+ exclude,
26
+ stripWWW: false,
27
+ normalizeProtocol: false,
28
+ removeQueryParameters: false,
29
+ removeSingleSlash: false,
30
+ sortQueryParameters: false,
31
+ stripAuthentication: false,
32
+ stripTextFragment: false,
33
+ removeTrailingSlash: false,
34
+ normalize: false
35
+ });
36
+ };
37
+ /**
38
+ * 读取图片宽高
39
+ */
40
+ const getImageSize = async (url) => {
41
+ const { width = 100, height = 100 } = size(await common.buffer(url));
42
+ return {
43
+ width,
44
+ height
45
+ };
46
+ };
47
+
48
+ //#endregion
49
+ export { sleep as a, random as i, getImageSize as n, handleUrl as r, fakeEvent as t };
@@ -0,0 +1,170 @@
1
+ import { common, karinPathBase, logger, requireFileSync, watch } from "node-karin";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import fs from "node:fs";
5
+
6
+ //#region src/root.ts
7
+ /** 当前文件的绝对路径 */
8
+ const filePath = fileURLToPath(import.meta.url);
9
+ /** 插件包绝对路径 */
10
+ const dirPath = path.resolve(filePath, "../..");
11
+ /** 插件包的名称 */
12
+ const basename = path.basename(dirPath);
13
+
14
+ //#endregion
15
+ //#region src/utils/logger.ts
16
+ const PREFIX = "[QQ Official Bot]";
17
+ const m = {
18
+ trace: (...a) => logger.trace(PREFIX, ...a),
19
+ debug: (...a) => logger.debug(PREFIX, ...a),
20
+ info: (...a) => logger.info(PREFIX, ...a),
21
+ warn: (...a) => logger.warn(PREFIX, ...a),
22
+ error: (...a) => logger.error(PREFIX, ...a),
23
+ fatal: (...a) => logger.fatal(PREFIX, ...a),
24
+ mark: (...a) => logger.mark(PREFIX, ...a)
25
+ };
26
+ /**
27
+ * QQBot 日志函数
28
+ * - log('info', '消息')
29
+ * - log.info('消息')
30
+ *
31
+ * 日志级别由 karin 框架的 log.level 控制
32
+ */
33
+ function log(level, ...args) {
34
+ m[level](...args);
35
+ }
36
+ Object.assign(log, m);
37
+
38
+ //#endregion
39
+ //#region src/utils/plugin.ts
40
+ /**
41
+ * 读取当前插件的 package.json。
42
+ *
43
+ * @returns package.json 内容。
44
+ */
45
+ const pkg = () => requireFileSync(`${dirPath}/package.json`);
46
+ /** 当前插件 npm 包名,如 @karinjs/adapter-qqbot。 */
47
+ const pluginName = pkg().name;
48
+ /** 文件系统目录名:把 npm 包名里的 `/` 规范化为 `-`,与 Karin 框架对齐。 */
49
+ const pluginDirName = pluginName.replace(/\//g, "-");
50
+
51
+ //#endregion
52
+ //#region src/utils/config.ts
53
+ let cache;
54
+ const cacheMap = {};
55
+ /** 延迟注入,避免与 core/index.ts 循环依赖 */
56
+ let _createBot;
57
+ let _destroyBot;
58
+ const bindHandlers = (createBot, destroyBot) => {
59
+ _createBot = createBot;
60
+ _destroyBot = destroyBot;
61
+ };
62
+ /** 配置目录:${karinPathBase}/@karinjs-adapter-qqbot/config */
63
+ const dirConfig = `${karinPathBase}/${pluginDirName}/config`;
64
+ /**
65
+ * 读取配置
66
+ */
67
+ const config = () => {
68
+ const cfgPath = `${dirConfig}/config.json`;
69
+ if (!fs.existsSync(cfgPath)) {
70
+ fs.mkdirSync(dirConfig, { recursive: true });
71
+ fs.writeFileSync(cfgPath, JSON.stringify([], null, 2));
72
+ }
73
+ if (cache) return cache;
74
+ const result = formatConfig(requireFileSync(cfgPath));
75
+ cache = result;
76
+ result.forEach((v) => {
77
+ cacheMap[v.appId] = v;
78
+ });
79
+ return result;
80
+ };
81
+ /**
82
+ * 获取单 bot 配置
83
+ */
84
+ const getConfig = (appid) => cacheMap[appid];
85
+ /**
86
+ * 写入配置
87
+ */
88
+ const writeConfig = (data) => {
89
+ fs.mkdirSync(dirConfig, { recursive: true });
90
+ fs.writeFileSync(`${dirConfig}/config.json`, JSON.stringify(data, null, 2));
91
+ };
92
+ /**
93
+ * 用默认值补齐用户配置
94
+ */
95
+ const formatConfig = (user) => {
96
+ const def = getDefaultConfig()[0];
97
+ return user.map((item) => ({
98
+ ...def,
99
+ ...item,
100
+ keyboard: {
101
+ ...def.keyboard,
102
+ ...item.keyboard
103
+ },
104
+ messageCache: {
105
+ ...def.messageCache,
106
+ ...item.messageCache
107
+ },
108
+ event: {
109
+ ...def.event,
110
+ ...item.event
111
+ }
112
+ }));
113
+ };
114
+ /**
115
+ * 默认配置
116
+ */
117
+ const getDefaultConfig = () => [{
118
+ name: "",
119
+ appId: "",
120
+ secret: "",
121
+ prodApi: "https://api.sgroup.qq.com",
122
+ sandboxApi: "https://sandbox.api.sgroup.qq.com",
123
+ tokenApi: "https://bots.qq.com/app/getAppAccessToken",
124
+ sandbox: false,
125
+ qqEnable: true,
126
+ guildEnable: true,
127
+ guildMode: 0,
128
+ regex: [{
129
+ reg: "^/",
130
+ rep: "#"
131
+ }],
132
+ keyboard: { enable: true },
133
+ messageCache: {
134
+ enable: false,
135
+ self: false
136
+ },
137
+ event: { type: 2 }
138
+ }];
139
+ /**
140
+ * 监听配置文件,比较前后差异决定重建 / 销毁 bot
141
+ */
142
+ setTimeout(() => {
143
+ watch(`${dirConfig}/config.json`, (old, now) => {
144
+ cache = formatConfig(now);
145
+ cache.forEach((v) => {
146
+ cacheMap[v.appId] = v;
147
+ });
148
+ const diff = common.diffArray(old, now);
149
+ diff.removed.forEach((v) => _destroyBot?.(v.appId));
150
+ if (diff.added.length > 0) {
151
+ const ids = new Set(diff.added.map((v) => v.appId));
152
+ cache.forEach((v) => {
153
+ if (!ids.has(v.appId)) return;
154
+ if (v.event.type === 0) return;
155
+ _createBot?.(v);
156
+ });
157
+ }
158
+ diff.common.forEach((curr) => {
159
+ const prev = old.find((c) => c.appId === curr.appId);
160
+ if (!prev) return;
161
+ if (JSON.stringify(prev) === JSON.stringify(curr)) return;
162
+ logger.info(`[QQ Official Bot][配置监听] 配置已变更: ${curr.appId},重新初始化`);
163
+ _destroyBot?.(prev.appId);
164
+ if (curr.event.type !== 0) _createBot?.(curr);
165
+ });
166
+ });
167
+ }, 2e3);
168
+
169
+ //#endregion
170
+ export { writeConfig as a, log as c, getDefaultConfig as i, basename as l, config as n, pkg as o, getConfig as r, pluginDirName as s, bindHandlers as t };