@karinjs/adapter-qqbot 1.3.1 → 2.0.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 +222 -766
- package/dist/apps/login.d.ts +20 -0
- package/dist/apps/login.js +57 -0
- package/dist/config-H7LrvlNU.js +197 -0
- package/dist/index.d.ts +589 -418
- package/dist/index.js +2844 -97
- package/dist/onboard-BX8g8Wik.js +249 -0
- package/dist/utils-DO4gEkVN.js +3 -0
- package/dist/web.config.d.ts +20 -23
- package/dist/web.config.js +170 -221
- package/package.json +24 -18
- package/dist/chunk-PYOUSZ6B.js +0 -1501
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as _$node_karin0 from "node-karin";
|
|
2
|
+
|
|
3
|
+
//#region src/apps/login.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* #QQ登录 指令
|
|
6
|
+
*
|
|
7
|
+
* 仅主人可用。匹配 `#QQ登录` 或 `#qqlogin`(大小写均可)。
|
|
8
|
+
* 必须带 `#` 前缀。
|
|
9
|
+
*
|
|
10
|
+
* 流程:
|
|
11
|
+
* 1. 立即回复"开始扫码"
|
|
12
|
+
* 2. 触发扫码授权流程
|
|
13
|
+
* 3. 每次生成新二维码 → 把 PNG 图片回复给触发者
|
|
14
|
+
* 4. 授权成功 → 回复结果,watch 回调自动初始化 bot
|
|
15
|
+
*
|
|
16
|
+
* ⚠ 注意:扫码授权会刷新该 appId 的 secret,旧 secret 立即失效
|
|
17
|
+
*/
|
|
18
|
+
declare const qqLogin: _$node_karin0.Command<keyof _$node_karin0.MessageEventMap>;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { qqLogin };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { f as log } from "../config-H7LrvlNU.js";
|
|
2
|
+
import { n as runQrOnboard } from "../onboard-BX8g8Wik.js";
|
|
3
|
+
import karin$1, { 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
|
+
const qqLogin = karin$1.command(/^#(?:qq登录|qqlogin)$/i, async (e) => {
|
|
23
|
+
if (busy) {
|
|
24
|
+
await e.reply("已有扫码授权在进行中,请稍候再试");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
busy = true;
|
|
28
|
+
try {
|
|
29
|
+
await e.reply([segment.text("已开始 QQ 扫码登录授权\n⚠ 扫码完成会刷新该机器人的 secret,旧 secret 立即失效")]);
|
|
30
|
+
log("info", `[扫码登录] 由用户 ${e.userId} 触发`);
|
|
31
|
+
if (await runQrOnboard({ onQr: async ({ base64, url, refresh }) => {
|
|
32
|
+
const head = refresh === 0 ? "请使用手机 QQ 扫描二维码完成授权(二维码 60s 内有效,过期会自动刷新最多 3 次):" : `上一张二维码已过期,已自动刷新(第 ${refresh}/3 次,仍为 60s 有效期):`;
|
|
33
|
+
try {
|
|
34
|
+
await e.reply([
|
|
35
|
+
segment.text(head),
|
|
36
|
+
segment.image(base64),
|
|
37
|
+
segment.text(`或在手机 QQ 打开链接:\n${url}`)
|
|
38
|
+
]);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
log("warn", "[扫码登录] 推送二维码失败:", err);
|
|
41
|
+
}
|
|
42
|
+
} })) await e.reply("扫码授权成功,配置已保存,QQBot 实例正在初始化…");
|
|
43
|
+
else await e.reply("扫码授权失败或超时,请重试 #QQ登录");
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log("error", "[扫码登录] 异常:", err);
|
|
46
|
+
await e.reply("扫码授权过程中发生异常,详情见服务端日志");
|
|
47
|
+
} finally {
|
|
48
|
+
busy = false;
|
|
49
|
+
}
|
|
50
|
+
}, {
|
|
51
|
+
name: "qqbot:login",
|
|
52
|
+
permission: "master",
|
|
53
|
+
authFailMsg: "#QQ登录 仅限主人使用"
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
export { qqLogin };
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { basePath, common, logger, requireFileSync, watch } from "node-karin";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import qrcode from "qrcode";
|
|
5
|
+
import size from "image-size";
|
|
6
|
+
import lodash from "node-karin/lodash";
|
|
7
|
+
import GetUrls from "@karinjs/geturls";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
|
|
10
|
+
//#region src/root.ts
|
|
11
|
+
/** 当前文件的绝对路径 */
|
|
12
|
+
const filePath = fileURLToPath(import.meta.url);
|
|
13
|
+
/** 插件包绝对路径 */
|
|
14
|
+
const dirPath = path.resolve(filePath, "../..");
|
|
15
|
+
/** 插件包的名称 */
|
|
16
|
+
const basename = path.basename(dirPath);
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/utils/logger.ts
|
|
20
|
+
const PREFIX = "[QQ Official Bot]";
|
|
21
|
+
const m = {
|
|
22
|
+
trace: (...a) => logger.trace(PREFIX, ...a),
|
|
23
|
+
debug: (...a) => logger.debug(PREFIX, ...a),
|
|
24
|
+
info: (...a) => logger.info(PREFIX, ...a),
|
|
25
|
+
warn: (...a) => logger.warn(PREFIX, ...a),
|
|
26
|
+
error: (...a) => logger.error(PREFIX, ...a),
|
|
27
|
+
fatal: (...a) => logger.fatal(PREFIX, ...a),
|
|
28
|
+
mark: (...a) => logger.mark(PREFIX, ...a)
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* QQBot 日志函数
|
|
32
|
+
* - log('info', '消息')
|
|
33
|
+
* - log.info('消息')
|
|
34
|
+
*
|
|
35
|
+
* 日志级别由 karin 框架的 log.level 控制
|
|
36
|
+
*/
|
|
37
|
+
function log(level, ...args) {
|
|
38
|
+
m[level](...args);
|
|
39
|
+
}
|
|
40
|
+
Object.assign(log, m);
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/utils/common.ts
|
|
44
|
+
/** 睡眠 */
|
|
45
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
46
|
+
/** 随机数 */
|
|
47
|
+
const random = (min, max) => lodash.random(min, max);
|
|
48
|
+
/**
|
|
49
|
+
* 伪事件日志(红字)
|
|
50
|
+
*/
|
|
51
|
+
const fakeEvent = (log) => {
|
|
52
|
+
logger.fatal(`${logger.red("[QQ Official Bot][虚假事件]")} ${log.replace(/\n/g, "\\n")}`);
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* 提取文本中的 URL 列表
|
|
56
|
+
* 中文不符合 URL 规范,先替换为 `|` 再扫描
|
|
57
|
+
*/
|
|
58
|
+
const handleUrl = (text, exclude = []) => {
|
|
59
|
+
const cleaned = text.replace(/[一-龥]/g, "|");
|
|
60
|
+
return GetUrls.getUrls(cleaned, {
|
|
61
|
+
exclude,
|
|
62
|
+
stripWWW: false,
|
|
63
|
+
normalizeProtocol: false,
|
|
64
|
+
removeQueryParameters: false,
|
|
65
|
+
removeSingleSlash: false,
|
|
66
|
+
sortQueryParameters: false,
|
|
67
|
+
stripAuthentication: false,
|
|
68
|
+
stripTextFragment: false,
|
|
69
|
+
removeTrailingSlash: false,
|
|
70
|
+
normalize: false
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* 读取图片宽高
|
|
75
|
+
*/
|
|
76
|
+
const getImageSize = async (url) => {
|
|
77
|
+
const { width = 100, height = 100 } = size(await common.buffer(url));
|
|
78
|
+
return {
|
|
79
|
+
width,
|
|
80
|
+
height
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/utils/config.ts
|
|
86
|
+
let cache;
|
|
87
|
+
const cacheMap = {};
|
|
88
|
+
/** 延迟注入,避免与 core/index.ts 循环依赖 */
|
|
89
|
+
let _createBot;
|
|
90
|
+
let _destroyBot;
|
|
91
|
+
const bindHandlers = (createBot, destroyBot) => {
|
|
92
|
+
_createBot = createBot;
|
|
93
|
+
_destroyBot = destroyBot;
|
|
94
|
+
};
|
|
95
|
+
/** package.json */
|
|
96
|
+
const pkg = () => requireFileSync(`${dirPath}/package.json`);
|
|
97
|
+
/** 配置目录:${basePath}/@karinjs-adapter-qqbot/config */
|
|
98
|
+
const dirConfig = `${basePath}/${pkg().name.replace(/\//g, "-")}/config`;
|
|
99
|
+
/**
|
|
100
|
+
* 读取配置
|
|
101
|
+
*/
|
|
102
|
+
const config = () => {
|
|
103
|
+
const cfgPath = `${dirConfig}/config.json`;
|
|
104
|
+
if (!fs.existsSync(cfgPath)) {
|
|
105
|
+
fs.mkdirSync(dirConfig, { recursive: true });
|
|
106
|
+
fs.writeFileSync(cfgPath, JSON.stringify([], null, 2));
|
|
107
|
+
}
|
|
108
|
+
if (cache) return cache;
|
|
109
|
+
const result = formatConfig(requireFileSync(cfgPath));
|
|
110
|
+
cache = result;
|
|
111
|
+
result.forEach((v) => {
|
|
112
|
+
cacheMap[v.appId] = v;
|
|
113
|
+
});
|
|
114
|
+
return result;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* 获取单 bot 配置
|
|
118
|
+
*/
|
|
119
|
+
const getConfig = (appid) => cacheMap[appid];
|
|
120
|
+
/**
|
|
121
|
+
* 写入配置
|
|
122
|
+
*/
|
|
123
|
+
const writeConfig = (data) => {
|
|
124
|
+
fs.mkdirSync(dirConfig, { recursive: true });
|
|
125
|
+
fs.writeFileSync(`${dirConfig}/config.json`, JSON.stringify(data, null, 2));
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* 用默认值补齐用户配置
|
|
129
|
+
*/
|
|
130
|
+
const formatConfig = (user) => {
|
|
131
|
+
const def = getDefaultConfig()[0];
|
|
132
|
+
return user.map((item) => ({
|
|
133
|
+
...def,
|
|
134
|
+
...item,
|
|
135
|
+
keyboard: {
|
|
136
|
+
...def.keyboard,
|
|
137
|
+
...item.keyboard
|
|
138
|
+
},
|
|
139
|
+
event: {
|
|
140
|
+
...def.event,
|
|
141
|
+
...item.event
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* 默认配置
|
|
147
|
+
*/
|
|
148
|
+
const getDefaultConfig = () => [{
|
|
149
|
+
name: "",
|
|
150
|
+
appId: "",
|
|
151
|
+
secret: "",
|
|
152
|
+
prodApi: "https://api.sgroup.qq.com",
|
|
153
|
+
sandboxApi: "https://sandbox.api.sgroup.qq.com",
|
|
154
|
+
tokenApi: "https://bots.qq.com/app/getAppAccessToken",
|
|
155
|
+
sandbox: false,
|
|
156
|
+
qqEnable: true,
|
|
157
|
+
guildEnable: true,
|
|
158
|
+
guildMode: 0,
|
|
159
|
+
regex: [{
|
|
160
|
+
reg: "^/",
|
|
161
|
+
rep: "#"
|
|
162
|
+
}],
|
|
163
|
+
keyboard: { enable: true },
|
|
164
|
+
event: { type: 2 }
|
|
165
|
+
}];
|
|
166
|
+
/**
|
|
167
|
+
* 监听配置文件,比较前后差异决定重建 / 销毁 bot
|
|
168
|
+
*/
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
watch(`${dirConfig}/config.json`, (old, now) => {
|
|
171
|
+
cache = formatConfig(now);
|
|
172
|
+
cache.forEach((v) => {
|
|
173
|
+
cacheMap[v.appId] = v;
|
|
174
|
+
});
|
|
175
|
+
const diff = common.diffArray(old, now);
|
|
176
|
+
diff.removed.forEach((v) => _destroyBot?.(v.appId));
|
|
177
|
+
if (diff.added.length > 0) {
|
|
178
|
+
const ids = new Set(diff.added.map((v) => v.appId));
|
|
179
|
+
cache.forEach((v) => {
|
|
180
|
+
if (!ids.has(v.appId)) return;
|
|
181
|
+
if (v.event.type === 0) return;
|
|
182
|
+
_createBot?.(v);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
diff.common.forEach((curr) => {
|
|
186
|
+
const prev = old.find((c) => c.appId === curr.appId);
|
|
187
|
+
if (!prev) return;
|
|
188
|
+
if (JSON.stringify(prev) === JSON.stringify(curr)) return;
|
|
189
|
+
logger.info(`[QQ Official Bot][配置监听] 配置已变更: ${curr.appId},重新初始化`);
|
|
190
|
+
_destroyBot?.(prev.appId);
|
|
191
|
+
if (curr.event.type !== 0) _createBot?.(curr);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}, 2e3);
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
export { pkg as a, getImageSize as c, sleep as d, log as f, getDefaultConfig as i, handleUrl as l, config as n, writeConfig as o, basename as p, getConfig as r, fakeEvent as s, bindHandlers as t, random as u };
|