@karinjs/adapter-qqbot 2.2.1 → 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.d.ts +6 -2
- package/dist/apps/cleanup.js +5 -10
- package/dist/apps/login.d.ts +2 -16
- package/dist/apps/login.js +138 -85
- package/dist/config-D1_LqrcJ.js +196 -0
- package/dist/index.d.ts +409 -234
- package/dist/index.js +347 -365
- package/dist/logger-sza6kuy3.js +27 -0
- package/dist/message-CVJVFAGR.js +1844 -0
- package/dist/utils-lb9ZdWay.js +32 -0
- package/dist/web.config.d.ts +3 -5
- package/dist/web.config.js +44 -84
- package/package.json +8 -5
- package/dist/common-by1GpV0b.js +0 -49
- package/dist/config-B_Gc9JKZ.js +0 -253
- package/dist/message-Dt4QkDYn.js +0 -1739
- package/dist/onboard-BXQ1cG8h.js +0 -252
- package/dist/utils-DIq0lGMt.js +0 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import "./config-D1_LqrcJ.js";
|
|
2
|
+
import "./logger-sza6kuy3.js";
|
|
3
|
+
import { common, logger } from "node-karin";
|
|
4
|
+
import qrcode from "qrcode";
|
|
5
|
+
import size from "image-size";
|
|
6
|
+
import lodash from "node-karin/lodash";
|
|
7
|
+
import "@karinjs/geturls";
|
|
8
|
+
|
|
9
|
+
//#region src/utils/common.ts
|
|
10
|
+
/** QQBot统一API地址 */
|
|
11
|
+
const QQAPIBASEURL = "https://api.bot.q.qq.com";
|
|
12
|
+
/** 随机数 */
|
|
13
|
+
const random = (min, max) => lodash.random(min, max);
|
|
14
|
+
/**
|
|
15
|
+
* 伪事件日志(红字)
|
|
16
|
+
*/
|
|
17
|
+
const fakeEvent = (log) => {
|
|
18
|
+
logger.fatal(`${logger.red("[QQ Official Bot][虚假事件]")} ${log.replace(/\n/g, "\\n")}`);
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* 读取图片宽高
|
|
22
|
+
*/
|
|
23
|
+
const getImageSize = async (url) => {
|
|
24
|
+
const { width = 100, height = 100 } = size(await common.buffer(url));
|
|
25
|
+
return {
|
|
26
|
+
width,
|
|
27
|
+
height
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { random as i, fakeEvent as n, getImageSize as r, QQAPIBASEURL as t };
|
package/dist/web.config.d.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
//#region src/web.config.d.ts
|
|
2
2
|
type WebQQBotInput = {
|
|
3
|
-
name: unknown;
|
|
4
3
|
appId: unknown;
|
|
5
4
|
secret: unknown;
|
|
6
|
-
'proxy:api': unknown;
|
|
7
|
-
'proxy:ws': unknown;
|
|
8
|
-
sandbox: unknown;
|
|
9
5
|
qqEnable: unknown;
|
|
10
6
|
guildEnable: unknown;
|
|
11
7
|
guildMode: unknown;
|
|
12
8
|
regex: unknown;
|
|
13
|
-
'keyboard:enable': unknown;
|
|
14
9
|
'markdown:enable': unknown;
|
|
15
10
|
'messageCache:enable': unknown;
|
|
16
11
|
'messageCache:self': unknown;
|
|
12
|
+
'messageCache:level': unknown;
|
|
13
|
+
'messageCache:ttlHours': unknown;
|
|
14
|
+
'messageCache:maxRows': unknown;
|
|
17
15
|
'event:type': unknown;
|
|
18
16
|
};
|
|
19
17
|
type WebConfigInput = {
|
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,22 +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",
|
|
72
|
+
"messageCache:ttlHours": String(item.messageCache?.ttlHours ?? 24),
|
|
73
|
+
"messageCache:maxRows": String(item.messageCache?.maxRows ?? 2e5)
|
|
121
74
|
});
|
|
122
75
|
});
|
|
123
76
|
return [components.divider.horizontal("bot-section", {
|
|
@@ -155,10 +108,6 @@ var web_config_default = defineConfig({
|
|
|
155
108
|
description: "基础信息",
|
|
156
109
|
descPosition: 5
|
|
157
110
|
}),
|
|
158
|
-
components.input.create("name", {
|
|
159
|
-
label: "机器人显示名称",
|
|
160
|
-
description: "只用于 Karin 配置页和日志里识别这个机器人,不影响 QQ 平台资料。扫码登录写入配置时通常会自动填充。"
|
|
161
|
-
}),
|
|
162
111
|
components.input.create("appId", {
|
|
163
112
|
label: "机器人 AppID",
|
|
164
113
|
description: "QQ 开放平台机器人应用的 AppID,是识别机器人账号的唯一编号。填错会导致鉴权和收发消息失败。",
|
|
@@ -173,11 +122,6 @@ var web_config_default = defineConfig({
|
|
|
173
122
|
description: "场景能力",
|
|
174
123
|
descPosition: 5
|
|
175
124
|
}),
|
|
176
|
-
components.switch.create("sandbox", {
|
|
177
|
-
label: "使用沙盒环境",
|
|
178
|
-
description: "开启后这个机器人会使用沙盒环境的 OpenAPI 和 WebSocket 地址;关闭时使用正式环境地址。正式上线机器人请关闭。",
|
|
179
|
-
defaultSelected: false
|
|
180
|
-
}),
|
|
181
125
|
components.switch.create("qqEnable", {
|
|
182
126
|
label: "启用 QQ 私聊 / 群聊",
|
|
183
127
|
description: "开启后会处理 QQ 单聊和群聊里的消息事件;如果这个机器人只用于频道,可以关闭。",
|
|
@@ -206,11 +150,6 @@ var web_config_default = defineConfig({
|
|
|
206
150
|
placeholder: "^/ #"
|
|
207
151
|
})
|
|
208
152
|
}),
|
|
209
|
-
components.switch.create("keyboard:enable", {
|
|
210
|
-
label: "把链接自动转成按钮",
|
|
211
|
-
description: "开启后,发送文本里出现的 http(s) 链接会尽量转成 QQ keyboard 按钮,用户可以直接点按钮打开链接。该能力依赖 Markdown 通道。",
|
|
212
|
-
defaultSelected: true
|
|
213
|
-
}),
|
|
214
153
|
components.switch.create("markdown:enable", {
|
|
215
154
|
label: "启用 Markdown 发送通道",
|
|
216
155
|
description: "开启后普通文本和图片优先合并为 QQ Markdown 消息,Markdown 里的图片会以内嵌图片形式发送;关闭后普通图片会按官方富媒体单独上传发送。插件显式传入的 segment.markdown 不受此开关影响。",
|
|
@@ -230,17 +169,35 @@ var web_config_default = defineConfig({
|
|
|
230
169
|
description: "只有开启“缓存收到的消息到数据库”后才生效。开启后,机器人发送成功的消息也会写入缓存,方便之后通过消息 ID 查询。",
|
|
231
170
|
defaultSelected: false
|
|
232
171
|
}),
|
|
233
|
-
components.
|
|
234
|
-
|
|
235
|
-
|
|
172
|
+
components.radio.group("messageCache:level", {
|
|
173
|
+
label: "缓存存储分级",
|
|
174
|
+
description: "控制每条消息落库的内容多少,直接影响数据库体积。撤回和引用解析在任何分级下都可用。",
|
|
175
|
+
defaultValue: "standard",
|
|
176
|
+
radio: [
|
|
177
|
+
components.radio.create("minimal", {
|
|
178
|
+
label: "最小",
|
|
179
|
+
value: "minimal",
|
|
180
|
+
description: "只记录消息 ID 映射与引用关系,不保存正文和媒体,体积最小。"
|
|
181
|
+
}),
|
|
182
|
+
components.radio.create("standard", {
|
|
183
|
+
label: "标准(推荐)",
|
|
184
|
+
value: "standard",
|
|
185
|
+
description: "保存文本、表情和媒体本地文件,不保存 markdown 原文。"
|
|
186
|
+
}),
|
|
187
|
+
components.radio.create("full", {
|
|
188
|
+
label: "完整",
|
|
189
|
+
value: "full",
|
|
190
|
+
description: "所有支持的消息段原样保存,包含 markdown 原文,体积最大。"
|
|
191
|
+
})
|
|
192
|
+
]
|
|
236
193
|
}),
|
|
237
|
-
components.input.create("
|
|
238
|
-
label: "
|
|
239
|
-
description: "
|
|
194
|
+
components.input.create("messageCache:ttlHours", {
|
|
195
|
+
label: "缓存保留小时数",
|
|
196
|
+
description: "超过该时长的消息会被清理任务删除。范围 1~720 小时,默认 24。多个机器人同时开启缓存时按最大值生效。"
|
|
240
197
|
}),
|
|
241
|
-
components.input.create("
|
|
242
|
-
label: "
|
|
243
|
-
description: "
|
|
198
|
+
components.input.create("messageCache:maxRows", {
|
|
199
|
+
label: "缓存最大消息条数",
|
|
200
|
+
description: "数据库消息行数硬上限,超出后从最旧的消息开始删除,保证磁盘占用可预期。范围 1000~5000000,默认 200000。"
|
|
244
201
|
})
|
|
245
202
|
]
|
|
246
203
|
}
|
|
@@ -256,15 +213,20 @@ var web_config_default = defineConfig({
|
|
|
256
213
|
try {
|
|
257
214
|
data = input.qqbot.map((item) => {
|
|
258
215
|
const previous = prevConfig.find((cfg) => cfg.appId === toStringValue(item.appId));
|
|
259
|
-
const sandbox = toBool(item.sandbox, previous?.sandbox ?? false);
|
|
260
216
|
const qqEnable = toBool(item.qqEnable, previous?.qqEnable ?? true);
|
|
261
217
|
const guildEnable = toBool(item.guildEnable, previous?.guildEnable ?? true);
|
|
262
218
|
const guildMode = toBool(item.guildMode, previous?.guildMode === 1) ? 1 : 0;
|
|
263
219
|
const eventType = Number(toStringValue(item["event:type"], String(previous?.event?.type ?? 0)));
|
|
264
|
-
const keyboardEnable = toBool(item["keyboard:enable"], previous?.keyboard?.enable ?? true);
|
|
265
220
|
const markdownEnable = toBool(item["markdown:enable"], previous?.markdown?.enable ?? true);
|
|
266
221
|
const messageCacheEnable = toBool(item["messageCache:enable"], previous?.messageCache?.enable ?? false);
|
|
267
222
|
const messageCacheSelf = toBool(item["messageCache:self"], previous?.messageCache?.self ?? false);
|
|
223
|
+
/** 枚举与数值范围由 writeConfig 内的 formatConfig 统一校验回落。 */
|
|
224
|
+
const messageCacheLevel = toStringValue(item["messageCache:level"], previous?.messageCache?.level ?? "standard");
|
|
225
|
+
/** 输入被清空时回落上次值/默认值,避免 Number('') = 0 被夹到范围下限。 */
|
|
226
|
+
const ttlHoursRaw = toStringValue(item["messageCache:ttlHours"]).trim();
|
|
227
|
+
const messageCacheTtlHours = ttlHoursRaw ? Number(ttlHoursRaw) : previous?.messageCache?.ttlHours ?? 24;
|
|
228
|
+
const maxRowsRaw = toStringValue(item["messageCache:maxRows"]).trim();
|
|
229
|
+
const messageCacheMaxRows = maxRowsRaw ? Number(maxRowsRaw) : previous?.messageCache?.maxRows ?? 2e5;
|
|
268
230
|
const regex = toStringList(item.regex).map((str) => {
|
|
269
231
|
const parts = str.split(" ");
|
|
270
232
|
return {
|
|
@@ -272,22 +234,20 @@ var web_config_default = defineConfig({
|
|
|
272
234
|
rep: parts[1]?.replace(/^<|>$/g, "") || ""
|
|
273
235
|
};
|
|
274
236
|
});
|
|
275
|
-
const proxy = proxyFromInput(item, previous);
|
|
276
237
|
return {
|
|
277
|
-
name: toStringValue(item.name),
|
|
278
238
|
appId: toStringValue(item.appId),
|
|
279
239
|
secret: toStringValue(item.secret),
|
|
280
|
-
proxy,
|
|
281
|
-
sandbox,
|
|
282
240
|
qqEnable,
|
|
283
241
|
guildEnable,
|
|
284
242
|
guildMode,
|
|
285
243
|
regex,
|
|
286
|
-
keyboard: { enable: keyboardEnable },
|
|
287
244
|
markdown: { enable: markdownEnable },
|
|
288
245
|
messageCache: {
|
|
289
246
|
enable: messageCacheEnable,
|
|
290
|
-
self: messageCacheSelf
|
|
247
|
+
self: messageCacheSelf,
|
|
248
|
+
level: messageCacheLevel,
|
|
249
|
+
ttlHours: messageCacheTtlHours,
|
|
250
|
+
maxRows: messageCacheMaxRows
|
|
291
251
|
},
|
|
292
252
|
event: { type: eventType }
|
|
293
253
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karinjs/adapter-qqbot",
|
|
3
|
-
"version": "
|
|
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": {
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"lint": "eslint src",
|
|
25
25
|
"lint:fix": "eslint src --fix",
|
|
26
26
|
"pub": "npm publish --access public",
|
|
27
|
-
"sort": "npx sort-package-json"
|
|
27
|
+
"sort": "npx sort-package-json",
|
|
28
|
+
"test": "node tests/schema-sql.test.mjs && node tests/codec.test.mjs && node tests/store.test.mjs",
|
|
29
|
+
"typecheck": "npx tsc --noEmit"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"@karinjs/geturls": "^1.0.2",
|
|
@@ -35,14 +37,15 @@
|
|
|
35
37
|
"tweetnacl": "^1.0.3"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
40
|
+
"@tencent-connect/qqbot-nodejs": "^1.0.4",
|
|
38
41
|
"@types/express": "5.0.6",
|
|
39
|
-
"@types/node": "^24.13.
|
|
42
|
+
"@types/node": "^24.13.3",
|
|
40
43
|
"@types/qrcode": "^1.5.6",
|
|
41
44
|
"eslint": "^9.39.4",
|
|
42
45
|
"neostandard": "^0.13.0",
|
|
43
46
|
"node-karin": "1.16.3",
|
|
44
|
-
"tsdown": "^0.22.
|
|
45
|
-
"tsx": "^4.23.
|
|
47
|
+
"tsdown": "^0.22.13",
|
|
48
|
+
"tsx": "^4.23.1",
|
|
46
49
|
"typescript": "^6.0.3"
|
|
47
50
|
},
|
|
48
51
|
"publishConfig": {
|
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/config-B_Gc9JKZ.js
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
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/proxy-url.ts
|
|
53
|
-
/**
|
|
54
|
-
* HTTP / token 地址只清理首尾空白。完整接口地址是否带尾斜杠可能有路由含义,不能替用户改。
|
|
55
|
-
*/
|
|
56
|
-
const normalizeHttpUrl = (raw) => raw.trim();
|
|
57
|
-
/**
|
|
58
|
-
* 拼接 OpenAPI 路径时再处理斜杠,避免配置值本身被改写。
|
|
59
|
-
*/
|
|
60
|
-
const joinHttpPath = (base, path) => {
|
|
61
|
-
return `${normalizeHttpUrl(base).replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
62
|
-
};
|
|
63
|
-
/**
|
|
64
|
-
* WebSocket 网关地址:
|
|
65
|
-
* - ws:// / wss:// 原样保留协议
|
|
66
|
-
* - http:// / https:// 映射为 ws:// / wss://
|
|
67
|
-
* - 未写协议时按 wss:// 处理
|
|
68
|
-
*
|
|
69
|
-
* 这里只做最小处理,不改 path / query / hash。自定义网关的尾斜杠和查询参数都可能有路由含义。
|
|
70
|
-
*/
|
|
71
|
-
const normalizeWsGatewayUrl = (raw) => {
|
|
72
|
-
const value = raw.trim();
|
|
73
|
-
if (!value) return "";
|
|
74
|
-
let normalized = value;
|
|
75
|
-
const scheme = value.match(/^([a-z][a-z\d+.-]*):\/\//i)?.[1]?.toLowerCase();
|
|
76
|
-
if (scheme === "http" || scheme === "https") normalized = value.replace(/^http/i, "ws");
|
|
77
|
-
else if (scheme && scheme !== "ws" && scheme !== "wss") throw new Error(`unsupported websocket protocol: ${scheme}:`);
|
|
78
|
-
else if (!scheme) normalized = `wss://${value}`;
|
|
79
|
-
const parsed = new URL(normalized);
|
|
80
|
-
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") throw new Error(`unsupported websocket protocol: ${parsed.protocol}`);
|
|
81
|
-
return normalized;
|
|
82
|
-
};
|
|
83
|
-
/**
|
|
84
|
-
* 只有从 OpenAPI 根地址推导官方 fallback 网关时,才主动拼 `/websocket/`。
|
|
85
|
-
*/
|
|
86
|
-
const buildFallbackWsUrlFromApi = (apiUrl) => {
|
|
87
|
-
const parsed = new URL(normalizeWsGatewayUrl(apiUrl));
|
|
88
|
-
parsed.pathname = `${parsed.pathname.replace(/\/+$/, "")}/websocket/`;
|
|
89
|
-
parsed.search = "";
|
|
90
|
-
parsed.hash = "";
|
|
91
|
-
return parsed.toString();
|
|
92
|
-
};
|
|
93
|
-
const normalizeProxyConfig = (proxy) => ({
|
|
94
|
-
prodApi: normalizeHttpUrl(proxy.prodApi),
|
|
95
|
-
sandboxApi: normalizeHttpUrl(proxy.sandboxApi),
|
|
96
|
-
tokenApi: normalizeHttpUrl(proxy.tokenApi),
|
|
97
|
-
prodWs: normalizeWsGatewayUrl(proxy.prodWs),
|
|
98
|
-
sandboxWs: normalizeWsGatewayUrl(proxy.sandboxWs)
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
//#endregion
|
|
102
|
-
//#region src/utils/config.ts
|
|
103
|
-
let cache;
|
|
104
|
-
const cacheMap = {};
|
|
105
|
-
/** 延迟注入,避免与 core/index.ts 循环依赖 */
|
|
106
|
-
let _createBot;
|
|
107
|
-
let _destroyBot;
|
|
108
|
-
const bindHandlers = (createBot, destroyBot) => {
|
|
109
|
-
_createBot = createBot;
|
|
110
|
-
_destroyBot = destroyBot;
|
|
111
|
-
};
|
|
112
|
-
/** 配置目录:${karinPathBase}/@karinjs-adapter-qqbot/config */
|
|
113
|
-
const dirConfig = `${karinPathBase}/${pluginDirName}/config`;
|
|
114
|
-
const configPath = `${dirConfig}/config.json`;
|
|
115
|
-
const syncCache = (data) => {
|
|
116
|
-
cache = data;
|
|
117
|
-
Object.keys(cacheMap).forEach((key) => delete cacheMap[key]);
|
|
118
|
-
data.forEach((v) => {
|
|
119
|
-
cacheMap[v.appId] = v;
|
|
120
|
-
});
|
|
121
|
-
};
|
|
122
|
-
/**
|
|
123
|
-
* 读取配置
|
|
124
|
-
*/
|
|
125
|
-
const config = () => {
|
|
126
|
-
if (!fs.existsSync(configPath)) {
|
|
127
|
-
fs.mkdirSync(dirConfig, { recursive: true });
|
|
128
|
-
fs.writeFileSync(configPath, JSON.stringify([], null, 2));
|
|
129
|
-
}
|
|
130
|
-
if (cache) return cache;
|
|
131
|
-
const result = formatConfig(requireFileSync(configPath));
|
|
132
|
-
syncCache(result);
|
|
133
|
-
return result;
|
|
134
|
-
};
|
|
135
|
-
/**
|
|
136
|
-
* 直接从磁盘读取配置,不使用内存缓存。
|
|
137
|
-
*/
|
|
138
|
-
const readConfigFile = () => {
|
|
139
|
-
if (!fs.existsSync(configPath)) return config();
|
|
140
|
-
return formatConfig(requireFileSync(configPath));
|
|
141
|
-
};
|
|
142
|
-
/**
|
|
143
|
-
* 获取单 bot 配置
|
|
144
|
-
*/
|
|
145
|
-
const getConfig = (appid) => cacheMap[appid];
|
|
146
|
-
/**
|
|
147
|
-
* 写入配置
|
|
148
|
-
*/
|
|
149
|
-
const writeConfig = (data) => {
|
|
150
|
-
fs.mkdirSync(dirConfig, { recursive: true });
|
|
151
|
-
const normalized = formatConfig(data);
|
|
152
|
-
fs.writeFileSync(configPath, JSON.stringify(normalized, null, 2));
|
|
153
|
-
syncCache(normalized);
|
|
154
|
-
};
|
|
155
|
-
/**
|
|
156
|
-
* 用默认值补齐用户配置
|
|
157
|
-
*/
|
|
158
|
-
const formatConfig = (user) => {
|
|
159
|
-
const def = getDefaultConfig()[0];
|
|
160
|
-
return user.map((item) => {
|
|
161
|
-
const { prodApi, sandboxApi, tokenApi, wsUrl, proxy, keyboard, markdown, messageCache, event, ...rest } = item;
|
|
162
|
-
return {
|
|
163
|
-
...def,
|
|
164
|
-
...rest,
|
|
165
|
-
proxy: normalizeProxyConfig({
|
|
166
|
-
...def.proxy,
|
|
167
|
-
...proxy,
|
|
168
|
-
prodApi: proxy?.prodApi || prodApi || def.proxy.prodApi,
|
|
169
|
-
sandboxApi: proxy?.sandboxApi || sandboxApi || def.proxy.sandboxApi,
|
|
170
|
-
tokenApi: proxy?.tokenApi || tokenApi || def.proxy.tokenApi,
|
|
171
|
-
prodWs: proxy?.prodWs || proxy?.wsUrl || wsUrl || def.proxy.prodWs,
|
|
172
|
-
sandboxWs: proxy?.sandboxWs || proxy?.wsUrl || wsUrl || def.proxy.sandboxWs
|
|
173
|
-
}),
|
|
174
|
-
keyboard: {
|
|
175
|
-
...def.keyboard,
|
|
176
|
-
...keyboard
|
|
177
|
-
},
|
|
178
|
-
markdown: {
|
|
179
|
-
...def.markdown,
|
|
180
|
-
...markdown
|
|
181
|
-
},
|
|
182
|
-
messageCache: {
|
|
183
|
-
...def.messageCache,
|
|
184
|
-
...messageCache
|
|
185
|
-
},
|
|
186
|
-
event: {
|
|
187
|
-
...def.event,
|
|
188
|
-
...event
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
});
|
|
192
|
-
};
|
|
193
|
-
/**
|
|
194
|
-
* 默认配置
|
|
195
|
-
*/
|
|
196
|
-
const getDefaultConfig = () => [{
|
|
197
|
-
name: "",
|
|
198
|
-
appId: "",
|
|
199
|
-
secret: "",
|
|
200
|
-
proxy: {
|
|
201
|
-
prodApi: "https://api.sgroup.qq.com",
|
|
202
|
-
sandboxApi: "https://sandbox.api.sgroup.qq.com",
|
|
203
|
-
tokenApi: "https://bots.qq.com/app/getAppAccessToken",
|
|
204
|
-
prodWs: "wss://api.sgroup.qq.com/websocket/",
|
|
205
|
-
sandboxWs: "wss://sandbox.api.sgroup.qq.com/websocket/"
|
|
206
|
-
},
|
|
207
|
-
sandbox: false,
|
|
208
|
-
qqEnable: true,
|
|
209
|
-
guildEnable: true,
|
|
210
|
-
guildMode: 0,
|
|
211
|
-
regex: [{
|
|
212
|
-
reg: "^/",
|
|
213
|
-
rep: "#"
|
|
214
|
-
}],
|
|
215
|
-
keyboard: { enable: true },
|
|
216
|
-
markdown: { enable: true },
|
|
217
|
-
messageCache: {
|
|
218
|
-
enable: false,
|
|
219
|
-
self: false
|
|
220
|
-
},
|
|
221
|
-
event: { type: 2 }
|
|
222
|
-
}];
|
|
223
|
-
/**
|
|
224
|
-
* 监听配置文件,比较前后差异决定重建 / 销毁 bot
|
|
225
|
-
*/
|
|
226
|
-
setTimeout(() => {
|
|
227
|
-
watch(`${dirConfig}/config.json`, (old, now) => {
|
|
228
|
-
const oldConfig = formatConfig(old);
|
|
229
|
-
const nowConfig = formatConfig(now);
|
|
230
|
-
syncCache(nowConfig);
|
|
231
|
-
const diff = common.diffArray(oldConfig, nowConfig);
|
|
232
|
-
diff.removed.forEach((v) => _destroyBot?.(v.appId));
|
|
233
|
-
if (diff.added.length > 0) {
|
|
234
|
-
const ids = new Set(diff.added.map((v) => v.appId));
|
|
235
|
-
nowConfig.forEach((v) => {
|
|
236
|
-
if (!ids.has(v.appId)) return;
|
|
237
|
-
if (v.event.type === 0) return;
|
|
238
|
-
_createBot?.(v);
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
diff.common.forEach((curr) => {
|
|
242
|
-
const prev = oldConfig.find((c) => c.appId === curr.appId);
|
|
243
|
-
if (!prev) return;
|
|
244
|
-
if (JSON.stringify(prev) === JSON.stringify(curr)) return;
|
|
245
|
-
logger.info(`[QQ Official Bot][配置监听] 配置已变更: ${curr.appId},重新初始化`);
|
|
246
|
-
_destroyBot?.(prev.appId);
|
|
247
|
-
if (curr.event.type !== 0) _createBot?.(curr);
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
}, 2e3);
|
|
251
|
-
|
|
252
|
-
//#endregion
|
|
253
|
-
export { readConfigFile as a, joinHttpPath as c, normalizeWsGatewayUrl as d, pkg as f, basename as h, getDefaultConfig as i, normalizeHttpUrl as l, log as m, config as n, writeConfig as o, pluginDirName as p, getConfig as r, buildFallbackWsUrlFromApi as s, bindHandlers as t, normalizeProxyConfig as u };
|