@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.
@@ -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 };
@@ -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 = {
@@ -1,9 +1,8 @@
1
- import { a as readConfigFile, f as pkg, i as getDefaultConfig, n as config, o as writeConfig, s as buildFallbackWsUrlFromApi, u as normalizeProxyConfig } from "./config-B_Gc9JKZ.js";
2
- import "./utils-DIq0lGMt.js";
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.name || item.appId,
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
- ...getBotProxyInput(item)
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.divider.horizontal("bot-proxy-section", {
234
- description: "连接代理(高级设置:通常只在使用 Webhook 转 WebSocket 服务时填写,除非你知道你在做什么,否则不建议更改)",
235
- descPosition: 5
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("proxy:ws", {
238
- label: "WebSocket 接入地址",
239
- description: "事件接收方式为 WebSocket 时连接的地址。常见用途是填写第三方 Webhook 转 WebSocket 服务地址,因为 QQ 后台开启 Webhook 后通常无法再直连官方 WebSocket。不填会使用当前环境的官方默认网关。"
194
+ components.input.create("messageCache:ttlHours", {
195
+ label: "缓存保留小时数",
196
+ description: "超过该时长的消息会被清理任务删除。范围 1~720 小时,默认 24。多个机器人同时开启缓存时按最大值生效。"
240
197
  }),
241
- components.input.create("proxy:api", {
242
- label: "OpenAPI 反代地址",
243
- description: "用于发送消息、上传媒体、查询网关和获取机器人资料的 QQ OpenAPI 根地址。多数 Webhook 转 WebSocket 场景不用改这里;只有 OpenAPI 请求也需要走反代时才填写。"
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": "2.2.1",
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.2",
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.3",
45
- "tsx": "^4.23.0",
47
+ "tsdown": "^0.22.13",
48
+ "tsx": "^4.23.1",
46
49
  "typescript": "^6.0.3"
47
50
  },
48
51
  "publishConfig": {
@@ -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 };
@@ -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 };