@karinjs/adapter-qqbot 2.1.0 → 2.2.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/README.md +10 -0
- package/dist/apps/cleanup.js +2 -2
- package/dist/apps/login.js +2 -2
- package/dist/config-B_Gc9JKZ.js +253 -0
- package/dist/index.d.ts +21 -6
- package/dist/index.js +223 -36
- package/dist/{message-O73AeL_P.js → message-Dt4QkDYn.js} +1 -1
- package/dist/{onboard-DPWgX39H.js → onboard-BXQ1cG8h.js} +1 -1
- package/dist/utils-DIq0lGMt.js +4 -0
- package/dist/web.config.d.ts +21 -19
- package/dist/web.config.js +224 -96
- package/package.json +3 -3
- package/dist/config-DKa7596f.js +0 -170
- package/dist/utils-BPbnNAix.js +0 -4
package/README.md
CHANGED
|
@@ -26,7 +26,15 @@ pnpm add @karinjs/adapter-qqbot
|
|
|
26
26
|
"qqEnable": true,
|
|
27
27
|
"guildEnable": true,
|
|
28
28
|
"guildMode": 0,
|
|
29
|
+
"proxy": {
|
|
30
|
+
"prodApi": "https://api.sgroup.qq.com",
|
|
31
|
+
"sandboxApi": "https://sandbox.api.sgroup.qq.com",
|
|
32
|
+
"tokenApi": "https://bots.qq.com/app/getAppAccessToken",
|
|
33
|
+
"prodWs": "wss://api.sgroup.qq.com/websocket/",
|
|
34
|
+
"sandboxWs": "wss://sandbox.api.sgroup.qq.com/websocket/"
|
|
35
|
+
},
|
|
29
36
|
"keyboard": { "enable": true },
|
|
37
|
+
"markdown": { "enable": true },
|
|
30
38
|
"event": { "type": 2 }
|
|
31
39
|
}
|
|
32
40
|
]
|
|
@@ -34,6 +42,8 @@ pnpm add @karinjs/adapter-qqbot
|
|
|
34
42
|
|
|
35
43
|
- `event.type`: `0` 关闭、`1` Webhook、`2` WebSocket(默认)。
|
|
36
44
|
- `guildMode`: `0` 公域,只收 @ 消息;`1` 私域,接收全部频道消息。
|
|
45
|
+
- `proxy`: 自定义连接代理地址。`prodApi` / `sandboxApi` 作为 OpenAPI 根地址使用,后端拼接路径时会兼容末尾 `/`;`tokenApi` 是完整接口地址;`prodWs` / `sandboxWs` 是完整 WebSocket 网关地址,支持 `ws://` 和 `wss://`,路径和查询参数会按配置原样连接。
|
|
46
|
+
- `markdown.enable`: 是否让普通文本和图片自动合并为 Markdown 消息,默认开启。关闭后普通图片会改为单独上传发送;插件显式传入的 `segment.markdown` 不受此开关影响。
|
|
37
47
|
|
|
38
48
|
## 按钮
|
|
39
49
|
|
package/dist/apps/cleanup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as config } from "../config-
|
|
2
|
-
import { t as getMessageStore } from "../message-
|
|
1
|
+
import { n as config } from "../config-B_Gc9JKZ.js";
|
|
2
|
+
import { t as getMessageStore } from "../message-Dt4QkDYn.js";
|
|
3
3
|
import karin from "node-karin";
|
|
4
4
|
|
|
5
5
|
//#region src/apps/cleanup.ts
|
package/dist/apps/login.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as runQrOnboard } from "../onboard-
|
|
1
|
+
import { m as log } from "../config-B_Gc9JKZ.js";
|
|
2
|
+
import { n as runQrOnboard } from "../onboard-BXQ1cG8h.js";
|
|
3
3
|
import karin, { segment } from "node-karin";
|
|
4
4
|
|
|
5
5
|
//#region src/apps/login.ts
|
|
@@ -0,0 +1,253 @@
|
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -817,6 +817,21 @@ declare class MessageStore {
|
|
|
817
817
|
}
|
|
818
818
|
//#endregion
|
|
819
819
|
//#region src/types/config.d.ts
|
|
820
|
+
/**
|
|
821
|
+
* 自定义连接代理配置
|
|
822
|
+
*/
|
|
823
|
+
interface QQBotProxyConfig {
|
|
824
|
+
/** 正式环境 API */
|
|
825
|
+
prodApi: string;
|
|
826
|
+
/** 测试环境 API */
|
|
827
|
+
sandboxApi: string;
|
|
828
|
+
/** 调用凭证 API */
|
|
829
|
+
tokenApi: string;
|
|
830
|
+
/** 正式环境 WebSocket 网关 */
|
|
831
|
+
prodWs: string;
|
|
832
|
+
/** 测试环境 WebSocket 网关 */
|
|
833
|
+
sandboxWs: string;
|
|
834
|
+
}
|
|
820
835
|
/**
|
|
821
836
|
* 单个 QQBot 配置
|
|
822
837
|
*/
|
|
@@ -827,12 +842,8 @@ interface QQBotConfig {
|
|
|
827
842
|
appId: string;
|
|
828
843
|
/** 机器人密钥 */
|
|
829
844
|
secret: string;
|
|
830
|
-
/**
|
|
831
|
-
|
|
832
|
-
/** 测试环境 API */
|
|
833
|
-
sandboxApi: string;
|
|
834
|
-
/** 调用凭证 API */
|
|
835
|
-
tokenApi: string;
|
|
845
|
+
/** 自定义连接代理 */
|
|
846
|
+
proxy: QQBotProxyConfig;
|
|
836
847
|
/** 开启沙盒环境 */
|
|
837
848
|
sandbox: boolean;
|
|
838
849
|
/** 开启 QQ 场景能力(单聊 / 群聊) */
|
|
@@ -850,6 +861,10 @@ interface QQBotConfig {
|
|
|
850
861
|
keyboard: {
|
|
851
862
|
/** 是否将文本中的 URL 自动转换为 keyboard 按钮 */enable: boolean;
|
|
852
863
|
};
|
|
864
|
+
/** Markdown 通道 */
|
|
865
|
+
markdown: {
|
|
866
|
+
/** 是否启用 Markdown 通道发送消息 */enable: boolean;
|
|
867
|
+
};
|
|
853
868
|
/** 消息缓存配置 */
|
|
854
869
|
messageCache: {
|
|
855
870
|
/** 是否启用数据库消息缓存,用于 bot.getMsg */enable: boolean; /** 是否缓存机器人自己发送的消息 */
|