@karinjs/adapter-qqbot 3.0.0 → 3.0.1
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/dist/apps/cleanup.js +1 -1
- package/dist/apps/login.d.ts +2 -16
- package/dist/apps/login.js +138 -85
- package/dist/{config-BUuU6FnQ.js → config-D1_LqrcJ.js} +2 -99
- package/dist/index.d.ts +10 -29
- package/dist/index.js +102 -115
- package/dist/logger-sza6kuy3.js +27 -0
- package/dist/{message-BTGL3TSn.js → message-CVJVFAGR.js} +2 -1
- package/dist/utils-lb9ZdWay.js +32 -0
- package/dist/web.config.d.ts +0 -5
- package/dist/web.config.js +4 -87
- package/package.json +3 -2
- package/dist/common-by1GpV0b.js +0 -49
- package/dist/onboard-vIEoe9dB.js +0 -252
- package/dist/utils-N9PNiHjE.js +0 -4
package/dist/web.config.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { a as readConfigFile,
|
|
2
|
-
import "./utils-
|
|
1
|
+
import { a as readConfigFile, n as config, o as writeConfig, s as pkg } from "./config-D1_LqrcJ.js";
|
|
2
|
+
import "./utils-lb9ZdWay.js";
|
|
3
3
|
import { components, defineConfig } from "node-karin";
|
|
4
4
|
|
|
5
5
|
//#region src/web.config.ts
|
|
6
|
-
const defaultProxy = () => getDefaultConfig()[0].proxy;
|
|
7
6
|
const unwrapValue = (value) => {
|
|
8
7
|
if (value && typeof value === "object" && "value" in value) return value.value;
|
|
9
8
|
return value;
|
|
@@ -42,51 +41,6 @@ const toStringList = (value) => {
|
|
|
42
41
|
if (!Array.isArray(raw)) return [];
|
|
43
42
|
return raw.map((item) => toStringValue(item)).filter(Boolean);
|
|
44
43
|
};
|
|
45
|
-
const proxyKeys = (sandbox) => sandbox ? {
|
|
46
|
-
api: "sandboxApi",
|
|
47
|
-
ws: "sandboxWs"
|
|
48
|
-
} : {
|
|
49
|
-
api: "prodApi",
|
|
50
|
-
ws: "prodWs"
|
|
51
|
-
};
|
|
52
|
-
const getBotProxyInput = (item) => {
|
|
53
|
-
const keys = proxyKeys(!!item.sandbox);
|
|
54
|
-
const proxy = item.proxy || defaultProxy();
|
|
55
|
-
return {
|
|
56
|
-
"proxy:api": proxy[keys.api] || defaultProxy()[keys.api],
|
|
57
|
-
"proxy:ws": proxy[keys.ws] || defaultProxy()[keys.ws]
|
|
58
|
-
};
|
|
59
|
-
};
|
|
60
|
-
const proxyFromInput = (item, previous) => {
|
|
61
|
-
const def = defaultProxy();
|
|
62
|
-
const sandbox = toBool(item.sandbox, previous?.sandbox ?? false);
|
|
63
|
-
const previousSandbox = previous?.sandbox ?? sandbox;
|
|
64
|
-
const keys = proxyKeys(sandbox);
|
|
65
|
-
const previousVisibleKeys = proxyKeys(previousSandbox);
|
|
66
|
-
const previousProxy = previous?.proxy;
|
|
67
|
-
const base = {
|
|
68
|
-
...def,
|
|
69
|
-
...previousProxy
|
|
70
|
-
};
|
|
71
|
-
const envChanged = !!previous && previousSandbox !== sandbox;
|
|
72
|
-
const rawApi = toStringValue(item["proxy:api"]).trim();
|
|
73
|
-
const rawWs = toStringValue(item["proxy:ws"]).trim();
|
|
74
|
-
const previousVisibleApi = previousProxy?.[previousVisibleKeys.api] || def[previousVisibleKeys.api];
|
|
75
|
-
const previousVisibleWs = previousProxy?.[previousVisibleKeys.ws] || def[previousVisibleKeys.ws];
|
|
76
|
-
const api = envChanged && rawApi === previousVisibleApi ? base[keys.api] || def[keys.api] : rawApi || base[keys.api] || def[keys.api];
|
|
77
|
-
const previousApi = previousProxy?.[keys.api] || def[keys.api];
|
|
78
|
-
const previousWs = previousProxy?.[keys.ws] || def[keys.ws];
|
|
79
|
-
const wsInput = envChanged && rawWs === previousVisibleWs ? base[keys.ws] || def[keys.ws] : rawWs;
|
|
80
|
-
let ws = wsInput || previousWs || def[keys.ws];
|
|
81
|
-
const previousAutoWs = buildFallbackWsUrlFromApi(previousApi);
|
|
82
|
-
if (!wsInput || wsInput === previousWs && previousWs === previousAutoWs && api !== previousApi) ws = buildFallbackWsUrlFromApi(api);
|
|
83
|
-
return normalizeProxyConfig({
|
|
84
|
-
...base,
|
|
85
|
-
[keys.api]: api,
|
|
86
|
-
tokenApi: base.tokenApi || def.tokenApi,
|
|
87
|
-
[keys.ws]: ws
|
|
88
|
-
});
|
|
89
|
-
};
|
|
90
44
|
var web_config_default = defineConfig({
|
|
91
45
|
info: {
|
|
92
46
|
id: pkg().name,
|
|
@@ -102,25 +56,21 @@ var web_config_default = defineConfig({
|
|
|
102
56
|
const data = [];
|
|
103
57
|
config().forEach((item) => {
|
|
104
58
|
data.push({
|
|
105
|
-
title: item.
|
|
59
|
+
title: item.appId,
|
|
106
60
|
subtitle: item.appId,
|
|
107
|
-
name: item.name,
|
|
108
61
|
appId: item.appId,
|
|
109
62
|
secret: item.secret,
|
|
110
63
|
"event:type": String(item.event?.type ?? 2),
|
|
111
|
-
sandbox: item.sandbox,
|
|
112
64
|
qqEnable: item.qqEnable,
|
|
113
65
|
guildEnable: item.guildEnable,
|
|
114
66
|
guildMode: item.guildMode === 1,
|
|
115
67
|
regex: item.regex.map((r) => `${r.reg} ${r.rep}`),
|
|
116
|
-
"keyboard:enable": item.keyboard?.enable !== false,
|
|
117
68
|
"markdown:enable": item.markdown?.enable !== false,
|
|
118
69
|
"messageCache:enable": item.messageCache?.enable === true,
|
|
119
70
|
"messageCache:self": item.messageCache?.self === true,
|
|
120
71
|
"messageCache:level": item.messageCache?.level || "standard",
|
|
121
72
|
"messageCache:ttlHours": String(item.messageCache?.ttlHours ?? 24),
|
|
122
|
-
"messageCache:maxRows": String(item.messageCache?.maxRows ?? 2e5)
|
|
123
|
-
...getBotProxyInput(item)
|
|
73
|
+
"messageCache:maxRows": String(item.messageCache?.maxRows ?? 2e5)
|
|
124
74
|
});
|
|
125
75
|
});
|
|
126
76
|
return [components.divider.horizontal("bot-section", {
|
|
@@ -158,10 +108,6 @@ var web_config_default = defineConfig({
|
|
|
158
108
|
description: "基础信息",
|
|
159
109
|
descPosition: 5
|
|
160
110
|
}),
|
|
161
|
-
components.input.create("name", {
|
|
162
|
-
label: "机器人显示名称",
|
|
163
|
-
description: "只用于 Karin 配置页和日志里识别这个机器人,不影响 QQ 平台资料。扫码登录写入配置时通常会自动填充。"
|
|
164
|
-
}),
|
|
165
111
|
components.input.create("appId", {
|
|
166
112
|
label: "机器人 AppID",
|
|
167
113
|
description: "QQ 开放平台机器人应用的 AppID,是识别机器人账号的唯一编号。填错会导致鉴权和收发消息失败。",
|
|
@@ -176,11 +122,6 @@ var web_config_default = defineConfig({
|
|
|
176
122
|
description: "场景能力",
|
|
177
123
|
descPosition: 5
|
|
178
124
|
}),
|
|
179
|
-
components.switch.create("sandbox", {
|
|
180
|
-
label: "使用沙盒环境",
|
|
181
|
-
description: "开启后这个机器人会使用沙盒环境的 OpenAPI 和 WebSocket 地址;关闭时使用正式环境地址。正式上线机器人请关闭。",
|
|
182
|
-
defaultSelected: false
|
|
183
|
-
}),
|
|
184
125
|
components.switch.create("qqEnable", {
|
|
185
126
|
label: "启用 QQ 私聊 / 群聊",
|
|
186
127
|
description: "开启后会处理 QQ 单聊和群聊里的消息事件;如果这个机器人只用于频道,可以关闭。",
|
|
@@ -209,11 +150,6 @@ var web_config_default = defineConfig({
|
|
|
209
150
|
placeholder: "^/ #"
|
|
210
151
|
})
|
|
211
152
|
}),
|
|
212
|
-
components.switch.create("keyboard:enable", {
|
|
213
|
-
label: "把链接自动转成按钮",
|
|
214
|
-
description: "开启后,发送文本里出现的 http(s) 链接会尽量转成 QQ keyboard 按钮,用户可以直接点按钮打开链接。该能力依赖 Markdown 通道。",
|
|
215
|
-
defaultSelected: true
|
|
216
|
-
}),
|
|
217
153
|
components.switch.create("markdown:enable", {
|
|
218
154
|
label: "启用 Markdown 发送通道",
|
|
219
155
|
description: "开启后普通文本和图片优先合并为 QQ Markdown 消息,Markdown 里的图片会以内嵌图片形式发送;关闭后普通图片会按官方富媒体单独上传发送。插件显式传入的 segment.markdown 不受此开关影响。",
|
|
@@ -262,18 +198,6 @@ var web_config_default = defineConfig({
|
|
|
262
198
|
components.input.create("messageCache:maxRows", {
|
|
263
199
|
label: "缓存最大消息条数",
|
|
264
200
|
description: "数据库消息行数硬上限,超出后从最旧的消息开始删除,保证磁盘占用可预期。范围 1000~5000000,默认 200000。"
|
|
265
|
-
}),
|
|
266
|
-
components.divider.horizontal("bot-proxy-section", {
|
|
267
|
-
description: "连接代理(高级设置:通常只在使用 Webhook 转 WebSocket 服务时填写,除非你知道你在做什么,否则不建议更改)",
|
|
268
|
-
descPosition: 5
|
|
269
|
-
}),
|
|
270
|
-
components.input.create("proxy:ws", {
|
|
271
|
-
label: "WebSocket 接入地址",
|
|
272
|
-
description: "事件接收方式为 WebSocket 时连接的地址。常见用途是填写第三方 Webhook 转 WebSocket 服务地址,因为 QQ 后台开启 Webhook 后通常无法再直连官方 WebSocket。不填会使用当前环境的官方默认网关。"
|
|
273
|
-
}),
|
|
274
|
-
components.input.create("proxy:api", {
|
|
275
|
-
label: "OpenAPI 反代地址",
|
|
276
|
-
description: "用于发送消息、上传媒体、查询网关和获取机器人资料的 QQ OpenAPI 根地址。多数 Webhook 转 WebSocket 场景不用改这里;只有 OpenAPI 请求也需要走反代时才填写。"
|
|
277
201
|
})
|
|
278
202
|
]
|
|
279
203
|
}
|
|
@@ -289,12 +213,10 @@ var web_config_default = defineConfig({
|
|
|
289
213
|
try {
|
|
290
214
|
data = input.qqbot.map((item) => {
|
|
291
215
|
const previous = prevConfig.find((cfg) => cfg.appId === toStringValue(item.appId));
|
|
292
|
-
const sandbox = toBool(item.sandbox, previous?.sandbox ?? false);
|
|
293
216
|
const qqEnable = toBool(item.qqEnable, previous?.qqEnable ?? true);
|
|
294
217
|
const guildEnable = toBool(item.guildEnable, previous?.guildEnable ?? true);
|
|
295
218
|
const guildMode = toBool(item.guildMode, previous?.guildMode === 1) ? 1 : 0;
|
|
296
219
|
const eventType = Number(toStringValue(item["event:type"], String(previous?.event?.type ?? 0)));
|
|
297
|
-
const keyboardEnable = toBool(item["keyboard:enable"], previous?.keyboard?.enable ?? true);
|
|
298
220
|
const markdownEnable = toBool(item["markdown:enable"], previous?.markdown?.enable ?? true);
|
|
299
221
|
const messageCacheEnable = toBool(item["messageCache:enable"], previous?.messageCache?.enable ?? false);
|
|
300
222
|
const messageCacheSelf = toBool(item["messageCache:self"], previous?.messageCache?.self ?? false);
|
|
@@ -312,18 +234,13 @@ var web_config_default = defineConfig({
|
|
|
312
234
|
rep: parts[1]?.replace(/^<|>$/g, "") || ""
|
|
313
235
|
};
|
|
314
236
|
});
|
|
315
|
-
const proxy = proxyFromInput(item, previous);
|
|
316
237
|
return {
|
|
317
|
-
name: toStringValue(item.name),
|
|
318
238
|
appId: toStringValue(item.appId),
|
|
319
239
|
secret: toStringValue(item.secret),
|
|
320
|
-
proxy,
|
|
321
|
-
sandbox,
|
|
322
240
|
qqEnable,
|
|
323
241
|
guildEnable,
|
|
324
242
|
guildMode,
|
|
325
243
|
regex,
|
|
326
|
-
keyboard: { enable: keyboardEnable },
|
|
327
244
|
markdown: { enable: markdownEnable },
|
|
328
245
|
messageCache: {
|
|
329
246
|
enable: messageCacheEnable,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karinjs/adapter-qqbot",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "karin adapter for QQ Official Bot (2.0)",
|
|
5
5
|
"homepage": "https://github.com/KarinJS/karin-plugin-adapter-qqbot",
|
|
6
6
|
"bugs": {
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"lint:fix": "eslint src --fix",
|
|
26
26
|
"pub": "npm publish --access public",
|
|
27
27
|
"sort": "npx sort-package-json",
|
|
28
|
-
"test": "node tests/schema-sql.test.mjs && node tests/codec.test.mjs && node tests/store.test.mjs"
|
|
28
|
+
"test": "node tests/schema-sql.test.mjs && node tests/codec.test.mjs && node tests/store.test.mjs",
|
|
29
|
+
"typecheck": "npx tsc --noEmit"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@karinjs/geturls": "^1.0.2",
|
package/dist/common-by1GpV0b.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
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 };
|
package/dist/onboard-vIEoe9dB.js
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { i as getDefaultConfig, m as log, n as config, o as writeConfig } from "./config-BUuU6FnQ.js";
|
|
2
|
-
import { a as sleep } from "./common-by1GpV0b.js";
|
|
3
|
-
import qrcode from "qrcode";
|
|
4
|
-
import axios from "node-karin/axios";
|
|
5
|
-
import crypto from "node:crypto";
|
|
6
|
-
|
|
7
|
-
//#region src/core/onboard/crypto.ts
|
|
8
|
-
/**
|
|
9
|
-
* 生成 256-bit 随机 AES 密钥(base64)
|
|
10
|
-
*/
|
|
11
|
-
const generateBindKey = () => crypto.randomBytes(32).toString("base64");
|
|
12
|
-
/**
|
|
13
|
-
* 解密 bot_encrypt_secret(AES-256-GCM)
|
|
14
|
-
*
|
|
15
|
-
* 密文布局:IV (12 bytes) || ciphertext (N bytes) || AuthTag (16 bytes)
|
|
16
|
-
*/
|
|
17
|
-
const decryptSecret = (encryptedBase64, keyBase64) => {
|
|
18
|
-
const key = Buffer.from(keyBase64, "base64");
|
|
19
|
-
if (key.length !== 32) throw new Error(`AES key length must be 32 bytes, got ${key.length}`);
|
|
20
|
-
const raw = Buffer.from(encryptedBase64, "base64");
|
|
21
|
-
const iv = raw.subarray(0, 12);
|
|
22
|
-
const ciphertext = raw.subarray(12, raw.length - 16);
|
|
23
|
-
const authTag = raw.subarray(raw.length - 16);
|
|
24
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
25
|
-
decipher.setAuthTag(authTag);
|
|
26
|
-
let plaintext = decipher.update(ciphertext, void 0, "utf8");
|
|
27
|
-
plaintext += decipher.final("utf8");
|
|
28
|
-
return plaintext;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/core/onboard/portal.ts
|
|
33
|
-
/** QQ 开放平台扫码绑定服务域名 */
|
|
34
|
-
const PORTAL_HOST = "q.qq.com";
|
|
35
|
-
const CREATE_PATH = "/lite/create_bind_task";
|
|
36
|
-
const POLL_PATH = "/lite/poll_bind_result";
|
|
37
|
-
/** 扫码页面模板 */
|
|
38
|
-
const CONNECT_URL = "https://q.qq.com/qqbot/openclaw/connect.html?task_id={task_id}&_wv=2&source=karin";
|
|
39
|
-
const headers = {
|
|
40
|
-
"Content-Type": "application/json",
|
|
41
|
-
Accept: "application/json"
|
|
42
|
-
};
|
|
43
|
-
/** 绑定状态 */
|
|
44
|
-
const BindStatus = {
|
|
45
|
-
NONE: 0,
|
|
46
|
-
PENDING: 1,
|
|
47
|
-
COMPLETED: 2,
|
|
48
|
-
EXPIRED: 3
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* 创建绑定任务
|
|
52
|
-
* @returns task_id 与本地生成的 AES key
|
|
53
|
-
*/
|
|
54
|
-
const createBindTask = async () => {
|
|
55
|
-
const key = generateBindKey();
|
|
56
|
-
const { data } = await axios.post(`https://${PORTAL_HOST}${CREATE_PATH}`, { key }, { headers });
|
|
57
|
-
if (data.retcode !== 0) throw new Error(`create_bind_task: ${data.msg || "unknown error"}`);
|
|
58
|
-
const taskId = data.data?.task_id;
|
|
59
|
-
if (!taskId) throw new Error("create_bind_task: missing task_id");
|
|
60
|
-
return {
|
|
61
|
-
taskId,
|
|
62
|
-
aesKey: key
|
|
63
|
-
};
|
|
64
|
-
};
|
|
65
|
-
/**
|
|
66
|
-
* 轮询绑定结果
|
|
67
|
-
*/
|
|
68
|
-
const pollBindResult = async (taskId) => {
|
|
69
|
-
const { data } = await axios.post(`https://${PORTAL_HOST}${POLL_PATH}`, { task_id: taskId }, { headers });
|
|
70
|
-
if (data.retcode !== 0) throw new Error(`poll_bind_result: ${data.msg || "unknown error"}`);
|
|
71
|
-
const d = data.data ?? {};
|
|
72
|
-
return {
|
|
73
|
-
status: d.status ?? BindStatus.NONE,
|
|
74
|
-
appId: String(d.bot_appid ?? ""),
|
|
75
|
-
encryptedSecret: d.bot_encrypt_secret ?? "",
|
|
76
|
-
userOpenid: d.user_openid ?? ""
|
|
77
|
-
};
|
|
78
|
-
};
|
|
79
|
-
/**
|
|
80
|
-
* 构造扫码 URL
|
|
81
|
-
*/
|
|
82
|
-
const buildConnectUrl = (taskId) => CONNECT_URL.replace("{task_id}", encodeURIComponent(taskId));
|
|
83
|
-
/**
|
|
84
|
-
* 通过 appId + secret 直接调一次 @me,拿到 bot 昵称
|
|
85
|
-
* 用于扫码完成后的展示与写入配置
|
|
86
|
-
*/
|
|
87
|
-
const fetchBotName = async (appId, secret) => {
|
|
88
|
-
try {
|
|
89
|
-
const accessToken = (await axios.post("https://bots.qq.com/app/getAppAccessToken", {
|
|
90
|
-
appId,
|
|
91
|
-
clientSecret: secret
|
|
92
|
-
})).data?.access_token;
|
|
93
|
-
if (!accessToken) return "";
|
|
94
|
-
return (await axios.get("https://api.sgroup.qq.com/users/@me", { headers: { Authorization: `QQBot ${accessToken}` } })).data?.username || "";
|
|
95
|
-
} catch {
|
|
96
|
-
return "";
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
//#endregion
|
|
101
|
-
//#region src/core/onboard/qr.ts
|
|
102
|
-
/** 默认轮询间隔(毫秒) */
|
|
103
|
-
const POLL_INTERVAL = 2e3;
|
|
104
|
-
/** 默认总超时(秒) */
|
|
105
|
-
const DEFAULT_TIMEOUT_S = 600;
|
|
106
|
-
/** 单张二维码硬编码有效期:60s,到时主动判定过期触发刷新 */
|
|
107
|
-
const QR_TTL_MS = 6e4;
|
|
108
|
-
/** 二维码过期最大刷新次数 */
|
|
109
|
-
const MAX_REFRESHES = 3;
|
|
110
|
-
/**
|
|
111
|
-
* 生成二维码(同时拿到 ascii / base64)
|
|
112
|
-
*/
|
|
113
|
-
const renderQr = async (url) => {
|
|
114
|
-
return {
|
|
115
|
-
ascii: await qrcode.toString(url, {
|
|
116
|
-
type: "terminal",
|
|
117
|
-
small: true
|
|
118
|
-
}).catch(() => ""),
|
|
119
|
-
base64: `base64://${(await qrcode.toDataURL(url, {
|
|
120
|
-
width: 320,
|
|
121
|
-
margin: 2
|
|
122
|
-
})).split(",")[1]}`
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
/**
|
|
126
|
-
* 扫码登录主流程:
|
|
127
|
-
* 1. 创建绑定任务,调用 onQr 暴露二维码
|
|
128
|
-
* 2. 轮询直到 COMPLETED / 二维码过期刷新 / 超时
|
|
129
|
-
* 3. 解密返回 secret
|
|
130
|
-
*/
|
|
131
|
-
const qrRegister = async (opts = {}) => {
|
|
132
|
-
const { timeoutSeconds = DEFAULT_TIMEOUT_S, printTerminal = false, onQr } = opts;
|
|
133
|
-
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
134
|
-
for (let refresh = 0; refresh <= MAX_REFRESHES; refresh++) {
|
|
135
|
-
let task;
|
|
136
|
-
try {
|
|
137
|
-
task = await createBindTask();
|
|
138
|
-
} catch (err) {
|
|
139
|
-
log("error", "[扫码登录] 创建绑定任务失败:", err);
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
const url = buildConnectUrl(task.taskId);
|
|
143
|
-
const { ascii, base64 } = await renderQr(url);
|
|
144
|
-
const qrDeadline = Date.now() + QR_TTL_MS;
|
|
145
|
-
if (printTerminal) {
|
|
146
|
-
if (ascii) {
|
|
147
|
-
console.log();
|
|
148
|
-
console.log(ascii);
|
|
149
|
-
}
|
|
150
|
-
console.log(` 请使用 QQ 扫码(60s 内有效),或在手机 QQ 打开:\n ${url}\n`);
|
|
151
|
-
}
|
|
152
|
-
if (onQr) try {
|
|
153
|
-
await onQr({
|
|
154
|
-
url,
|
|
155
|
-
ascii,
|
|
156
|
-
base64,
|
|
157
|
-
refresh
|
|
158
|
-
});
|
|
159
|
-
} catch (err) {
|
|
160
|
-
log("warn", "[扫码登录] onQr 回调异常:", err);
|
|
161
|
-
}
|
|
162
|
-
let expired = false;
|
|
163
|
-
while (Date.now() < deadline) {
|
|
164
|
-
if (Date.now() >= qrDeadline) {
|
|
165
|
-
expired = true;
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
let result;
|
|
169
|
-
try {
|
|
170
|
-
result = await pollBindResult(task.taskId);
|
|
171
|
-
} catch {
|
|
172
|
-
await sleep(POLL_INTERVAL);
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (result.status === BindStatus.COMPLETED) {
|
|
176
|
-
if (!result.encryptedSecret) {
|
|
177
|
-
log("error", "[扫码登录] 绑定结果缺少加密密钥");
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
let secret;
|
|
181
|
-
try {
|
|
182
|
-
secret = decryptSecret(result.encryptedSecret, task.aesKey);
|
|
183
|
-
} catch (err) {
|
|
184
|
-
log("error", "[扫码登录] 解密 secret 失败:", err);
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
log("info", `[扫码登录] 授权成功,App ID: ${result.appId}`);
|
|
188
|
-
return {
|
|
189
|
-
appId: result.appId,
|
|
190
|
-
secret,
|
|
191
|
-
userOpenid: result.userOpenid
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
if (result.status === BindStatus.EXPIRED) {
|
|
195
|
-
expired = true;
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
await sleep(POLL_INTERVAL);
|
|
199
|
-
}
|
|
200
|
-
if (expired) {
|
|
201
|
-
if (refresh >= MAX_REFRESHES) {
|
|
202
|
-
log("error", `[扫码登录] 二维码已过期 4 次,终止流程`);
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
log("info", `[扫码登录] 二维码 60s 内未完成扫码,正在刷新 (${refresh + 1}/${MAX_REFRESHES})`);
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
log("error", `[扫码登录] 等待扫码总超时(${timeoutSeconds}s)`);
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
//#endregion
|
|
215
|
-
//#region src/core/onboard/index.ts
|
|
216
|
-
/** 是否需要触发扫码引导:完全没有 bot 配置时为 true */
|
|
217
|
-
const needQrOnboard = () => config().length === 0;
|
|
218
|
-
/**
|
|
219
|
-
* 执行扫码登录并把结果写入 config.json
|
|
220
|
-
*
|
|
221
|
-
* - 同 appId 已存在 → 覆盖 secret / name
|
|
222
|
-
* - 不存在 → 追加新配置
|
|
223
|
-
*
|
|
224
|
-
* 支持 `opts.onQr` 把新二维码透出给调用方(如 #QQ登录 指令把图片回给用户)
|
|
225
|
-
*/
|
|
226
|
-
const runQrOnboard = async (opts = {}) => {
|
|
227
|
-
const result = await qrRegister(opts);
|
|
228
|
-
if (!result) return false;
|
|
229
|
-
const name = await fetchBotName(result.appId, result.secret);
|
|
230
|
-
if (name) log("info", `[扫码登录] Bot 名称: ${name}`);
|
|
231
|
-
const list = config();
|
|
232
|
-
const idx = list.findIndex((item) => item.appId === result.appId);
|
|
233
|
-
if (idx >= 0) {
|
|
234
|
-
list[idx].secret = result.secret;
|
|
235
|
-
if (name) list[idx].name = name;
|
|
236
|
-
log("info", `[扫码登录] 已更新已有配置: ${result.appId}`);
|
|
237
|
-
} else {
|
|
238
|
-
const defaults = getDefaultConfig()[0];
|
|
239
|
-
list.push({
|
|
240
|
-
...defaults,
|
|
241
|
-
name: name || "",
|
|
242
|
-
appId: result.appId,
|
|
243
|
-
secret: result.secret
|
|
244
|
-
});
|
|
245
|
-
log("info", `[扫码登录] 已添加新配置: ${result.appId}`);
|
|
246
|
-
}
|
|
247
|
-
writeConfig(list);
|
|
248
|
-
return true;
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
//#endregion
|
|
252
|
-
export { runQrOnboard as n, needQrOnboard as t };
|
package/dist/utils-N9PNiHjE.js
DELETED