@karinjs/adapter-qqbot 1.2.10 → 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.
Files changed (65) hide show
  1. package/README.md +326 -85
  2. package/dist/apps/login.d.ts +20 -0
  3. package/dist/apps/login.js +57 -0
  4. package/dist/config-H7LrvlNU.js +197 -0
  5. package/dist/index.d.ts +670 -0
  6. package/dist/index.js +2847 -0
  7. package/dist/onboard-BX8g8Wik.js +249 -0
  8. package/dist/utils-DO4gEkVN.js +3 -0
  9. package/dist/web.config.d.ts +22 -0
  10. package/dist/web.config.js +173 -0
  11. package/package.json +34 -21
  12. package/config/comment/config.json +0 -3
  13. package/config/config/config.yaml +0 -53
  14. package/lib/cli/pr.d.ts +0 -2
  15. package/lib/cli/pr.js +0 -73
  16. package/lib/core/adapter/adapter.d.ts +0 -91
  17. package/lib/core/adapter/adapter.js +0 -144
  18. package/lib/core/adapter/handler.d.ts +0 -49
  19. package/lib/core/adapter/handler.js +0 -140
  20. package/lib/core/adapter/markdown.d.ts +0 -38
  21. package/lib/core/adapter/markdown.js +0 -220
  22. package/lib/core/adapter/normal.d.ts +0 -30
  23. package/lib/core/adapter/normal.js +0 -263
  24. package/lib/core/api/index.d.ts +0 -167
  25. package/lib/core/api/index.js +0 -180
  26. package/lib/core/api/sign.d.ts +0 -22
  27. package/lib/core/api/sign.js +0 -40
  28. package/lib/core/api/types.d.ts +0 -236
  29. package/lib/core/api/types.js +0 -1
  30. package/lib/core/event/conver.d.ts +0 -10
  31. package/lib/core/event/conver.js +0 -57
  32. package/lib/core/event/index.d.ts +0 -9
  33. package/lib/core/event/index.js +0 -98
  34. package/lib/core/event/message.d.ts +0 -27
  35. package/lib/core/event/message.js +0 -95
  36. package/lib/core/event/notice.d.ts +0 -50
  37. package/lib/core/event/notice.js +0 -133
  38. package/lib/core/event/types.d.ts +0 -473
  39. package/lib/core/event/types.js +0 -1
  40. package/lib/core/internal/axios.d.ts +0 -25
  41. package/lib/core/internal/axios.js +0 -46
  42. package/lib/core/internal/sign.d.ts +0 -22
  43. package/lib/core/internal/sign.js +0 -40
  44. package/lib/core/webhook/c2c.d.ts +0 -2
  45. package/lib/core/webhook/c2c.js +0 -14
  46. package/lib/core/webhook/http.d.ts +0 -2
  47. package/lib/core/webhook/http.js +0 -35
  48. package/lib/core/webhook/index.d.ts +0 -17
  49. package/lib/core/webhook/index.js +0 -87
  50. package/lib/core/webhook/ws.d.ts +0 -2
  51. package/lib/core/webhook/ws.js +0 -79
  52. package/lib/index.d.ts +0 -1
  53. package/lib/index.js +0 -5
  54. package/lib/root.d.ts +0 -5
  55. package/lib/root.js +0 -9
  56. package/lib/types/config.d.ts +0 -53
  57. package/lib/types/config.js +0 -1
  58. package/lib/utils/common.d.ts +0 -80
  59. package/lib/utils/common.js +0 -198
  60. package/lib/utils/config.d.ts +0 -9
  61. package/lib/utils/config.js +0 -54
  62. package/lib/utils/dir.d.ts +0 -1
  63. package/lib/utils/dir.js +0 -1
  64. package/lib/utils/index.d.ts +0 -4
  65. package/lib/utils/index.js +0 -4
package/README.md CHANGED
@@ -1,85 +1,326 @@
1
- # qaq
2
-
3
- 开发中...
4
-
5
- 如需使用:
6
-
7
- ```bash
8
- pnpm add @karinjs/adapter-qqbot -w
9
- ```
10
-
11
- 在`config\plugin\@karinjs\adapter-qqbot`创建`config.yaml`
12
- 输入以下内容,请自行配置账号。当前`Markdown`仅支持`旧图文模板`和`原生`
13
-
14
- ```yaml
15
- # 全局默认配置 以下所有配置均可在账号配置中单独配置
16
- default:
17
- # 获取调用凭证的api
18
- accessTokenApi: https://bots.qq.com/app/getAppAccessToken
19
- # 沙盒api
20
- sandBoxApi: https://sandbox.api.sgroup.qq.com
21
- # 正式环境api
22
- qqBotApi: https://api.sgroup.qq.com
23
- # 机器人发送模式 0-直接发送 1-原生Markdown 3-旧图文模板Markdown 4-纯文模板Markdown 5-自定义处理
24
- sendMode: 0
25
- # 是否开启沙盒环境
26
- sandBox: false
27
- # 是否启用频道机器人 未适配
28
- guild: false
29
- # 频道机器人模式 0-公域机器人 1-私域机器人
30
- guildMode: 0
31
-
32
- # 机器人Markdown模板ID 只有在sendMode为3或4时有效
33
- templateId: ""
34
-
35
- # 机器人旧图文模板Markdown配置
36
- oldTemplate:
37
- # 开头文字
38
- textStartKey: text_start
39
- # 图片描述
40
- imgDescKey: img_dec
41
- # 图片地址
42
- imgUrlKey: img_url
43
- # 结尾文字
44
- textEndKey: text_end
45
-
46
- # 机器人纯文模板Markdown配置
47
- textTemplate:
48
- - text_0
49
- - text_1
50
- - text_2
51
- - text_3
52
- - text_4
53
- - text_5
54
- - text_6
55
- - text_7
56
- - text_8
57
- - text_9
58
-
59
- # 文本中的url转二维码白名单 配置后将不转换这些url为二维码
60
- exclude: []
61
- # 接受到消息后对文本进行表达式处理
62
- regex: [
63
- {
64
- # 表达式
65
- reg: "^#",
66
- # 替换为
67
- rep: "",
68
- },
69
- ]
70
-
71
- # 账号配置列表
72
- accounts:
73
- default:
74
- # 机器人ID 必须为字符串
75
- appId: "123456789"
76
- # 机器人密钥
77
- secret: "123456789"
78
- # 机器人发送模式 0-直接发送 1-原生Markdown 3-旧图文模板Markdown 4-纯文模板Markdown 5-自定义处理
79
- sendMode: 0
80
- # 文本中的url转二维码白名单 配置后将不转换这些url为二维码 优先级高于全局配置
81
- exclude: []
82
- # 接受到消息后对文本进行表达式处理 优先级高于全局配置
83
- regex: []
84
-
85
- ```
1
+ # @karinjs/adapter-qqbot
2
+
3
+ [![npm](https://img.shields.io/npm/v/@karinjs/adapter-qqbot?style=flat-square)](https://www.npmjs.com/package/@karinjs/adapter-qqbot)
4
+ [![license](https://img.shields.io/npm/l/@karinjs/adapter-qqbot?style=flat-square)](./LICENSE)
5
+
6
+ 为 [Karin](https://github.com/KarinJS/Karin) 框架提供 QQ 官方机器人接入能力。
7
+
8
+ > **2.0 已发布**:完整重写,对齐 QQ 开放平台 2026 年起的新事件协议
9
+ > (`GROUP_MESSAGE_CREATE` 全量群聊消息 + `mentions` 数组),并适配
10
+ > Markdown / Keyboard 全量开放后的发送方式。
11
+
12
+ ---
13
+
14
+ ## 特性
15
+
16
+ - 🌐 **完整事件支持**:群聊(@/全量)、单聊、频道、频道私信、按钮点击、机器人入退群、主动消息开关
17
+ - 📨 **双发送通道**:自动选择「经典通道」(text + media)或「Markdown 通道」(msg_type=2 + keyboard),按消息内容与配置开关
18
+ - 🔌 **双接入方式**:WebSocket(推荐,无需公网)/ Webhook(QQ 主动推送)
19
+ - ♻️ **WebSocket Resume**:断网自动重连,携带 `session_id` / `seq` 通过 `op:6` 恢复,减少消息丢失
20
+ - 🔑 **扫码登录**:插件首次启动自动二维码引导,授权后写回 `config.json`
21
+ - 🧩 **按钮回调**:完整接入 `INTERACTION_CREATE`,自动 ack 并以普通消息事件投递给业务层
22
+ - 🛡️ **签名校验**:webhook 入口自动用 ed25519 校验
23
+ - 📐 **完整 TypeScript 类型**
24
+ - 🧹 **零 1.x 残留**:不再依赖 Markdown 模板 / keyboard 模板 / 中转服务
25
+
26
+ ---
27
+
28
+ ## 安装
29
+
30
+ ```bash
31
+ pnpm add @karinjs/adapter-qqbot
32
+ ```
33
+
34
+ > 要求 Node.js >= 18,Karin >= 1.15
35
+
36
+ ---
37
+
38
+ ## 快速开始
39
+
40
+ ### 方式 A:扫码登录(推荐)
41
+
42
+ 启动后没有任何已配置的 QQBot 时,控制台只打印一行提示:
43
+
44
+ ```
45
+ ==========================================
46
+ 未检测到 QQBot 配置
47
+ 请使用任意可用 bot(如 OneBot / Console 适配器)
48
+ 对机器人发送指令: #QQ登录
49
+ 扫码授权完成后会自动写入配置并完成初始化
50
+
51
+ ⚠ 注意:扫码授权会刷新该机器人的 secret,旧 secret 立即失效
52
+ ==========================================
53
+ ```
54
+
55
+ 按提示,在**任意已连接的 bot 适配器**(OneBot、Console、Discord …)下,给机器人发送:
56
+
57
+ ```
58
+ #QQ登录 # 或 #qq登录 / #qqlogin / #QQLogin
59
+ ```
60
+
61
+ 服务端控制台随后弹出 ASCII 二维码:
62
+
63
+ ```
64
+ ▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢
65
+ ▢ ASCII QR ▢▢▢▢▢▢▢
66
+ ▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢
67
+
68
+ 请使用 QQ 扫描上方二维码,或在手机 QQ 打开链接:
69
+ https://q.qq.com/qqbot/openclaw/connect.html?task_id=...
70
+ ```
71
+
72
+ 手机 QQ 扫码授权后,插件自动:
73
+
74
+ 1. 从平台拉取 `appId` + 加密的 `secret`
75
+ 2. 本地 AES-256-GCM 解密
76
+ 3. 调 `/users/@me` 获取昵称
77
+ 4. 写入 `${karin-base}/@karinjs-adapter-qqbot/config/config.json`
78
+ 5. `watch` 回调自动初始化 bot,无需重启
79
+
80
+ > 该指令同样可用于**新增账号**和**刷新已有账号的 secret**。
81
+
82
+ ### 方式 B:手动配置
83
+
84
+ 在 [QQ 开放平台](https://bot.q.qq.com/) 创建机器人,记录 `AppID` 与 `Secret`,然后编辑:
85
+
86
+ `@karinjs/adapter-qqbot/config/config.json`:
87
+
88
+ ```json
89
+ [
90
+ {
91
+ "name": "我的机器人",
92
+ "appId": "1234567890",
93
+ "secret": "your-secret",
94
+ "prodApi": "https://api.sgroup.qq.com",
95
+ "sandboxApi": "https://sandbox.api.sgroup.qq.com",
96
+ "tokenApi": "https://bots.qq.com/app/getAppAccessToken",
97
+ "sandbox": false,
98
+ "qqEnable": true,
99
+ "guildEnable": true,
100
+ "guildMode": 0,
101
+ "regex": [{ "reg": "^/", "rep": "#" }],
102
+ "markdown": { "enable": false },
103
+ "keyboard": { "enable": true },
104
+ "event": { "type": 2 }
105
+ }
106
+ ]
107
+ ```
108
+
109
+ 保存即生效(`watch` 自动重载)。
110
+
111
+ ---
112
+
113
+ ## 配置项详解
114
+
115
+ | 字段 | 类型 | 默认 | 说明 |
116
+ | --- | --- | --- | --- |
117
+ | `name` | string | `""` | 机器人显示名(扫码登录会自动获取) |
118
+ | `appId` | string | — | 机器人 AppID(**必填**) |
119
+ | `secret` | string | — | 机器人 Secret(**必填**) |
120
+ | `prodApi` | string | `https://api.sgroup.qq.com` | 正式环境 API 基址 |
121
+ | `sandboxApi` | string | `https://sandbox.api.sgroup.qq.com` | 沙盒环境 API 基址 |
122
+ | `tokenApi` | string | `https://bots.qq.com/app/getAppAccessToken` | access_token 获取接口 |
123
+ | `sandbox` | boolean | `false` | 是否启用沙盒环境 |
124
+ | `qqEnable` | boolean | `true` | 是否处理单聊 / 群聊 |
125
+ | `guildEnable` | boolean | `true` | 是否处理频道消息 |
126
+ | `guildMode` | `0` \| `1` | `0` | `0` 公域(只接 @ 消息)/ `1` 私域(所有消息) |
127
+ | `regex` | object[] | `[{reg:"^/", rep:"#"}]` | 收到消息后对文本执行的正则替换 |
128
+ | `keyboard.enable` | boolean | `true` | 是否将文本中的 URL **自动**转为 keyboard 按钮 |
129
+ | `event.type` | `0` \| `1` \| `2` | `2` | 事件接收方式:0 关闭 / 1 webhook / 2 WebSocket |
130
+
131
+ ---
132
+
133
+ ## 事件接收方式
134
+
135
+ ### WebSocket(推荐,`event.type: 2`)
136
+
137
+ - 主动连接 QQ 官方 `wss://api.sgroup.qq.com/websocket/`
138
+ - 无需公网 IP / HTTPS 证书
139
+ - intents 自动探测,从最大权限位逐级回退到可用集合
140
+ - **自动 Resume**:断网时记录 `session_id` / `seq`,下次连接走 `op:6` 让服务端补发丢失的事件;服务端拒绝 Resume 时降级 Identify
141
+ - 指数退避重连:`1.5s × 2^n + jitter`,上限 30s
142
+
143
+ ### Webhook(`event.type: 1`)
144
+
145
+ - QQ 主动推送 `POST /qqbot/webhook` 到你的服务
146
+ - 自动处理 `op:13` 鉴权回调(签名回包)
147
+ - 普通事件用 ed25519 校验签名
148
+ - 需要公网 HTTPS:在 QQ 开放平台后台配置「统一回调地址」为 `https://your-domain/qqbot/webhook`
149
+
150
+ ---
151
+
152
+ ## 发送通道
153
+
154
+ **所有 QQ / 频道消息一律走 `msg_type=2` Markdown 通道**(官方已全量开放),不再
155
+ 按内容选择通道:
156
+
157
+ - 文本 / `segment.at(...)` 通过 markdown content 渲染(at 用 `<qqbot-at-user id="..."/>` 内嵌标签)
158
+ - 图片合并为 `![karin #宽px #高px](url)` 内嵌到同一条 markdown
159
+ - `segment.button(...)` / `segment.keyboard(...)` 自动挂到 `keyboard.content.rows`,与 markdown 同条下发
160
+ - 文本中 URL(开启 `keyboard.enable` 时)自动 `extractUrlButtons` 转 keyboard 按钮
161
+ - 视频 / 语音 / 文件不能进 markdown,由 pipeline 自动以 `msg_type=7` 紧随 markdown 主消息**并发补发**
162
+
163
+ ---
164
+
165
+ ## 按钮回调(INTERACTION_CREATE)
166
+
167
+ 收到按钮点击时,适配器:
168
+
169
+ 1. 立即 `PUT /interactions/{id}` `code=0` 回 ack,防止客户端 loading
170
+ 2. 将事件转为对应场景的消息事件投递:
171
+ - `chat_type=0` → guild message
172
+ - `chat_type=1` → group message
173
+ - `chat_type=2` → friend message
174
+ 3. 消息 elements 结构:
175
+
176
+ ```ts
177
+ [
178
+ segment.at(selfId), // 群 / 单聊场景自动补
179
+ segment.text(button_data),
180
+ segment.json('{"tag":"qqbot-button-click","button_id":"...","button_data":"...",...}'),
181
+ // srcReply 末尾自动追加 segment.pasmsg(id, 'event')
182
+ ]
183
+ ```
184
+
185
+ 业务层通过 `e.rawEvent.t === 'INTERACTION_CREATE'` 区分普通消息与按钮点击。
186
+
187
+ ---
188
+
189
+ ## 群聊消息事件
190
+
191
+ QQ 官方 2026 起的新版 `GROUP_MESSAGE_CREATE` 报文:
192
+
193
+ ```json
194
+ {
195
+ "op": 0, "t": "GROUP_MESSAGE_CREATE",
196
+ "d": {
197
+ "author": {
198
+ "id": "...", "member_openid": "...", "union_openid": "",
199
+ "username": "小布丁qwq", "bot": false
200
+ },
201
+ "content": " /分布 ",
202
+ "group_id": "...", "group_openid": "...",
203
+ "id": "ROBOT1.0_...",
204
+ "mentions": [
205
+ { "is_you": true, "scope": "single", "username": "方舟生存飞升", "...": "..." }
206
+ ],
207
+ "message_scene": { "ext": ["msg_idx=REFIDX_..."], "source": "default" },
208
+ "message_type": 0,
209
+ "timestamp": "2026-05-08T13:24:53+08:00"
210
+ }
211
+ }
212
+ ```
213
+
214
+ 适配器行为:
215
+
216
+ - 当 `mentions[].is_you === true` 时,elements 头部自动补 `segment.at(self)`
217
+ - `author.username` 写入 `sender.nick`
218
+ - `cfg.regex` 应用到 content 拆分后的每个文本段
219
+ - 原始报文完整保留在 `e.rawEvent`,业务层可访问 `mentions` / `message_scene` / `message_type`
220
+
221
+ ---
222
+
223
+ ## 撤回 / 引用回复 / 被动消息
224
+
225
+ - **撤回**:`bot.recallMsg(contact, messageId)` —— 自动按场景调用 `/v2/users` / `/v2/groups` / `/channels` / `/dms`
226
+ - **引用回复**:传入 `segment.reply(messageId)`,仅附加在第一条消息上
227
+ - **被动消息白名单**:
228
+ - 单聊 `event_id` 接受:`INTERACTION_CREATE` / `C2C_MSG_RECEIVE` / `FRIEND_ADD`
229
+ - 群聊 `event_id` 接受:`INTERACTION_CREATE` / `GROUP_ADD_ROBOT` / `GROUP_MSG_RECEIVE`
230
+
231
+ ---
232
+
233
+ ## Web 配置面板
234
+
235
+ Karin 内置 Web 面板可视化编辑 `config.json`,本插件提供以下控件:
236
+
237
+ - 基础字段:name / appId / secret / prodApi / sandboxApi / tokenApi
238
+ - 沙盒、QQ 场景、频道场景、频道私域模式开关
239
+ - 正则规则列表(每行 `<regex> <replacement>`)
240
+ - **URL 自动按钮** 开关(`keyboard.enable`)
241
+ - 事件接收方式(关闭 / Webhook / WebSocket)
242
+
243
+ ---
244
+
245
+ ## 开发
246
+
247
+ ```bash
248
+ git clone https://github.com/KarinJS/karin-plugin-adapter-qqbot.git
249
+ cd karin-plugin-adapter-qqbot
250
+ pnpm install
251
+ pnpm dev # tsx watch
252
+ pnpm build # tsc + tsdown
253
+ ```
254
+
255
+ 源码结构(2.0):
256
+
257
+ ```
258
+ src/
259
+ ├── connection/
260
+ │ ├── routing.ts express 路由聚合
261
+ │ ├── webhook.ts POST /qqbot/webhook
262
+ │ ├── transport.ts 事件总线(appId → 业务)
263
+ │ └── ws/
264
+ │ ├── client.ts 单连接生命周期 + 心跳 + Resume
265
+ │ ├── intents.ts intents 探测与回退
266
+ │ └── manager.ts 多 bot 连接管理
267
+ ├── core/
268
+ │ ├── index.ts createBot / destroyBot / initQQBotAdapter
269
+ │ ├── api/ QQBotApi 门面 + http/messages/media/interaction/meta/builders
270
+ │ ├── internal/axios.ts access_token 缓存 + 自动刷新
271
+ │ ├── adapter/
272
+ │ │ ├── base.ts AdapterQQBot
273
+ │ │ ├── grouping.ts 消息归类
274
+ │ │ ├── pipeline-qq.ts QQ 单聊/群聊发送管线
275
+ │ │ ├── pipeline-guild.ts 频道发送管线
276
+ │ │ └── text-to-md.ts URL→ 按钮、图片→ markdown
277
+ │ ├── event/
278
+ │ │ ├── dispatcher.ts 事件分发器
279
+ │ │ ├── message.ts 群/单/频道/私信消息
280
+ │ │ ├── notice.ts 入退群/好友/主动消息开关
281
+ │ │ ├── interaction.ts 按钮回调
282
+ │ │ └── conver.ts 报文 → karin elements
283
+ │ └── onboard/
284
+ │ ├── crypto.ts AES-256-GCM 解密
285
+ │ ├── portal.ts q.qq.com 接口
286
+ │ ├── qr.ts 终端二维码 + 轮询
287
+ │ └── index.ts runQrOnboard / needQrOnboard
288
+ ├── apps/ karin app 插件
289
+ │ └── login.ts #QQ登录 指令
290
+ ├── types/ 事件、配置、opcode 类型
291
+ └── utils/ 日志、配置读写、文本工具
292
+
293
+ demo/ 示例 / 烟测 app(不进 npm 包,不自动加载)
294
+ └── qqtest.ts QQBot 2.0 真号烟测命令集合
295
+ ```
296
+
297
+ ### 调试
298
+
299
+ WS 入站消息、bus 派发、dispatcher 路由等关键链路均通过 `logger.debug` 输出。
300
+ 诊断「事件收不到」类问题时,把 karin 的 `log.level` 改成 `debug` 即可看到完整链路。
301
+
302
+ ---
303
+
304
+ ## 从 1.x 升级
305
+
306
+ 2.0 是破坏式重写,**不提供配置自动迁移**。需要手动调整:
307
+
308
+ | 1.x | 2.0 |
309
+ | --- | --- |
310
+ | `markdown.mode: 0/1/2/3/4/5`(模式选择 / 模板) | 全部删除,永远默认走 markdown 通道 |
311
+ | `event.wsUrl` / `event.wsToken` | 删除(中转方案已废弃) |
312
+ | 新增 | `keyboard.enable: true`(默认,URL 自动转按钮) |
313
+
314
+ 完整改动详见 [CHANGELOG.md](./CHANGELOG.md) 2.0.0 段落。
315
+
316
+ ---
317
+
318
+ ## 反馈
319
+
320
+ - Issue:https://github.com/KarinJS/karin-plugin-adapter-qqbot/issues
321
+
322
+ ---
323
+
324
+ ## License
325
+
326
+ MIT
@@ -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 };