@tencent-connect/openclaw-qqbot 1.7.2 → 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 (291) hide show
  1. package/README.md +25 -8
  2. package/README.zh.md +24 -8
  3. package/bin/qqbot-cli.js +5 -2
  4. package/dist/index.cjs +15549 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +890 -0
  7. package/index.ts +56 -19
  8. package/openclaw.plugin.json +19 -3
  9. package/package.json +14 -23
  10. package/preload.cjs +17 -14
  11. package/scripts/link-sdk-core.cjs +133 -50
  12. package/scripts/upgrade-via-npm.sh +42 -11
  13. package/scripts/upgrade-via-source.sh +4 -4
  14. package/skills/qqbot-channel/SKILL.md +34 -12
  15. package/src/adapter/contract.ts +69 -0
  16. package/src/adapter/gateway.ts +61 -0
  17. package/src/adapter/index.ts +15 -0
  18. package/src/adapter/lint.ts +144 -0
  19. package/src/adapter/media.ts +112 -0
  20. package/src/adapter/pairing.ts +99 -0
  21. package/src/adapter/resolve.ts +333 -0
  22. package/src/adapter/setup.ts +55 -0
  23. package/src/adapter/webhook.ts +248 -0
  24. package/src/adapter/workspace.ts +29 -0
  25. package/src/bot-instance.ts +60 -0
  26. package/src/channel.ts +202 -413
  27. package/src/commands/bot-approve.ts +143 -0
  28. package/src/commands/bot-clear-storage.ts +114 -0
  29. package/src/commands/bot-group-always.ts +62 -0
  30. package/src/commands/bot-help.ts +40 -0
  31. package/src/commands/bot-logs.ts +248 -0
  32. package/src/commands/bot-me.ts +18 -0
  33. package/src/commands/bot-pairing.ts +57 -0
  34. package/src/commands/bot-ping.ts +33 -0
  35. package/src/commands/bot-streaming.ts +55 -0
  36. package/src/commands/bot-upgrade.ts +56 -0
  37. package/src/commands/bot-version.ts +41 -0
  38. package/src/commands/config-util.ts +96 -0
  39. package/src/commands/index.ts +49 -0
  40. package/src/config.ts +46 -19
  41. package/src/dispatch/body-assembler.ts +356 -0
  42. package/src/dispatch/ctx-builder.ts +110 -0
  43. package/src/dispatch/dispatch.ts +352 -0
  44. package/src/dispatch/envelope-builder.ts +112 -0
  45. package/src/dispatch/index.ts +2 -0
  46. package/src/{approval-handler.ts → features/approval-handler.ts} +40 -94
  47. package/src/features/approval-utils.ts +41 -0
  48. package/src/features/credential-backup.ts +82 -0
  49. package/src/features/history-store.ts +19 -0
  50. package/src/features/msgid-cache.ts +55 -0
  51. package/src/features/onboarding.ts +38 -0
  52. package/src/{proactive.ts → features/proactive.ts} +68 -49
  53. package/src/features/ref-index-store.ts +282 -0
  54. package/src/{update-checker.ts → features/update-checker.ts} +15 -35
  55. package/src/gateway/event-handlers.ts +241 -0
  56. package/src/gateway/index.ts +3 -0
  57. package/src/gateway/lifecycle.ts +221 -0
  58. package/src/gateway/middleware-setup.ts +139 -0
  59. package/src/gateway/qqbot-gateway.ts +342 -0
  60. package/src/middleware/access-control.ts +130 -0
  61. package/src/middleware/attachment.ts +314 -0
  62. package/src/middleware/policy-injector.ts +66 -0
  63. package/src/openclaw-plugin-sdk.d.ts +81 -10
  64. package/src/outbound/cron-scheduler.ts +130 -0
  65. package/src/outbound/debounce.ts +102 -0
  66. package/src/outbound/deliver-pipeline.ts +235 -0
  67. package/src/outbound/image-size.ts +123 -0
  68. package/src/outbound/index.ts +3 -0
  69. package/src/outbound/local-file-router.ts +145 -0
  70. package/src/outbound/media-send.ts +377 -0
  71. package/src/outbound/outbound-service.ts +222 -0
  72. package/src/outbound/reply-limiter.ts +128 -0
  73. package/src/outbound/sanitize.ts +32 -0
  74. package/src/outbound/streaming-controller.ts +228 -0
  75. package/src/outbound/target.ts +63 -0
  76. package/src/outbound/tts-provider.ts +84 -0
  77. package/src/request-context.ts +9 -8
  78. package/src/runtime.ts +31 -6
  79. package/src/setup/account-key.ts +41 -0
  80. package/src/setup/finalize.ts +110 -0
  81. package/src/setup/login.ts +197 -0
  82. package/src/setup/surface.ts +40 -0
  83. package/src/tools/platform.ts +149 -0
  84. package/src/tools/remind.ts +2 -0
  85. package/src/types-augment.d.ts +54 -0
  86. package/src/types.ts +51 -9
  87. package/src/utils/mention.ts +52 -0
  88. package/src/utils/pkg-version.ts +84 -47
  89. package/src/utils/platform.ts +31 -7
  90. package/src/utils/plugin-logger.ts +104 -0
  91. package/src/utils/ssrf-guard.ts +32 -2
  92. package/src/utils/stt.ts +137 -0
  93. package/src/utils/voice-text.ts +61 -0
  94. package/tsconfig.json +2 -1
  95. package/tsup.config.ts +57 -0
  96. package/dist/index.d.ts +0 -18
  97. package/dist/index.js +0 -27
  98. package/dist/src/admin-resolver.d.ts +0 -33
  99. package/dist/src/admin-resolver.js +0 -157
  100. package/dist/src/api.d.ts +0 -309
  101. package/dist/src/api.js +0 -923
  102. package/dist/src/approval-handler.d.ts +0 -47
  103. package/dist/src/approval-handler.js +0 -372
  104. package/dist/src/channel.d.ts +0 -29
  105. package/dist/src/channel.js +0 -528
  106. package/dist/src/config.d.ts +0 -60
  107. package/dist/src/config.js +0 -288
  108. package/dist/src/credential-backup.d.ts +0 -31
  109. package/dist/src/credential-backup.js +0 -66
  110. package/dist/src/deliver-debounce.d.ts +0 -74
  111. package/dist/src/deliver-debounce.js +0 -174
  112. package/dist/src/gateway.d.ts +0 -18
  113. package/dist/src/gateway.js +0 -2025
  114. package/dist/src/group-history.d.ts +0 -136
  115. package/dist/src/group-history.js +0 -226
  116. package/dist/src/image-server.d.ts +0 -87
  117. package/dist/src/image-server.js +0 -570
  118. package/dist/src/inbound-attachments.d.ts +0 -60
  119. package/dist/src/inbound-attachments.js +0 -248
  120. package/dist/src/known-users.d.ts +0 -100
  121. package/dist/src/known-users.js +0 -263
  122. package/dist/src/message-gating.d.ts +0 -53
  123. package/dist/src/message-gating.js +0 -107
  124. package/dist/src/message-queue.d.ts +0 -91
  125. package/dist/src/message-queue.js +0 -257
  126. package/dist/src/onboarding.d.ts +0 -10
  127. package/dist/src/onboarding.js +0 -203
  128. package/dist/src/outbound-deliver.d.ts +0 -48
  129. package/dist/src/outbound-deliver.js +0 -392
  130. package/dist/src/outbound.d.ts +0 -220
  131. package/dist/src/outbound.js +0 -975
  132. package/dist/src/proactive.d.ts +0 -170
  133. package/dist/src/proactive.js +0 -399
  134. package/dist/src/ref-index-store.d.ts +0 -101
  135. package/dist/src/ref-index-store.js +0 -298
  136. package/dist/src/reply-dispatcher.d.ts +0 -35
  137. package/dist/src/reply-dispatcher.js +0 -311
  138. package/dist/src/request-context.d.ts +0 -25
  139. package/dist/src/request-context.js +0 -37
  140. package/dist/src/runtime.d.ts +0 -3
  141. package/dist/src/runtime.js +0 -13
  142. package/dist/src/session-store.d.ts +0 -52
  143. package/dist/src/session-store.js +0 -254
  144. package/dist/src/slash-commands.d.ts +0 -82
  145. package/dist/src/slash-commands.js +0 -2233
  146. package/dist/src/startup-greeting.d.ts +0 -30
  147. package/dist/src/startup-greeting.js +0 -97
  148. package/dist/src/streaming.d.ts +0 -247
  149. package/dist/src/streaming.js +0 -899
  150. package/dist/src/stt.d.ts +0 -21
  151. package/dist/src/stt.js +0 -70
  152. package/dist/src/tools/channel.d.ts +0 -16
  153. package/dist/src/tools/channel.js +0 -231
  154. package/dist/src/tools/remind.d.ts +0 -2
  155. package/dist/src/tools/remind.js +0 -255
  156. package/dist/src/transport/index.d.ts +0 -10
  157. package/dist/src/transport/index.js +0 -9
  158. package/dist/src/transport/webhook-transport.d.ts +0 -67
  159. package/dist/src/transport/webhook-transport.js +0 -245
  160. package/dist/src/transport/webhook-verify.d.ts +0 -48
  161. package/dist/src/transport/webhook-verify.js +0 -98
  162. package/dist/src/types.d.ts +0 -462
  163. package/dist/src/types.js +0 -22
  164. package/dist/src/typing-keepalive.d.ts +0 -27
  165. package/dist/src/typing-keepalive.js +0 -64
  166. package/dist/src/update-checker.d.ts +0 -36
  167. package/dist/src/update-checker.js +0 -171
  168. package/dist/src/utils/audio-convert.d.ts +0 -98
  169. package/dist/src/utils/audio-convert.js +0 -783
  170. package/dist/src/utils/chunked-upload.d.ts +0 -68
  171. package/dist/src/utils/chunked-upload.js +0 -341
  172. package/dist/src/utils/file-utils.d.ts +0 -61
  173. package/dist/src/utils/file-utils.js +0 -172
  174. package/dist/src/utils/image-size.d.ts +0 -51
  175. package/dist/src/utils/image-size.js +0 -234
  176. package/dist/src/utils/media-send.d.ts +0 -158
  177. package/dist/src/utils/media-send.js +0 -502
  178. package/dist/src/utils/media-tags.d.ts +0 -14
  179. package/dist/src/utils/media-tags.js +0 -165
  180. package/dist/src/utils/payload.d.ts +0 -112
  181. package/dist/src/utils/payload.js +0 -186
  182. package/dist/src/utils/pkg-version.d.ts +0 -5
  183. package/dist/src/utils/pkg-version.js +0 -61
  184. package/dist/src/utils/platform.d.ts +0 -137
  185. package/dist/src/utils/platform.js +0 -390
  186. package/dist/src/utils/ssrf-guard.d.ts +0 -25
  187. package/dist/src/utils/ssrf-guard.js +0 -91
  188. package/dist/src/utils/text-parsing.d.ts +0 -31
  189. package/dist/src/utils/text-parsing.js +0 -74
  190. package/dist/src/utils/upload-cache.d.ts +0 -34
  191. package/dist/src/utils/upload-cache.js +0 -93
  192. package/node_modules/@eshaz/web-worker/LICENSE +0 -201
  193. package/node_modules/@eshaz/web-worker/README.md +0 -134
  194. package/node_modules/@eshaz/web-worker/browser.js +0 -17
  195. package/node_modules/@eshaz/web-worker/cjs/browser.js +0 -16
  196. package/node_modules/@eshaz/web-worker/cjs/node.js +0 -219
  197. package/node_modules/@eshaz/web-worker/index.d.ts +0 -4
  198. package/node_modules/@eshaz/web-worker/node.js +0 -223
  199. package/node_modules/@eshaz/web-worker/package.json +0 -54
  200. package/node_modules/@wasm-audio-decoders/common/index.js +0 -5
  201. package/node_modules/@wasm-audio-decoders/common/package.json +0 -36
  202. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +0 -231
  203. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +0 -129
  204. package/node_modules/@wasm-audio-decoders/common/src/puff/README +0 -67
  205. package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +0 -31
  206. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +0 -863
  207. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +0 -35
  208. package/node_modules/@wasm-audio-decoders/common/src/utilities.js +0 -3
  209. package/node_modules/@wasm-audio-decoders/common/types.d.ts +0 -7
  210. package/node_modules/mpg123-decoder/README.md +0 -265
  211. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +0 -185
  212. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +0 -1
  213. package/node_modules/mpg123-decoder/index.js +0 -8
  214. package/node_modules/mpg123-decoder/package.json +0 -58
  215. package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +0 -464
  216. package/node_modules/mpg123-decoder/src/MPEGDecoder.js +0 -200
  217. package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +0 -21
  218. package/node_modules/mpg123-decoder/types.d.ts +0 -30
  219. package/node_modules/silk-wasm/LICENSE +0 -21
  220. package/node_modules/silk-wasm/README.md +0 -85
  221. package/node_modules/silk-wasm/lib/index.cjs +0 -16
  222. package/node_modules/silk-wasm/lib/index.d.ts +0 -70
  223. package/node_modules/silk-wasm/lib/index.mjs +0 -16
  224. package/node_modules/silk-wasm/lib/silk.wasm +0 -0
  225. package/node_modules/silk-wasm/lib/utils.d.ts +0 -4
  226. package/node_modules/silk-wasm/package.json +0 -39
  227. package/node_modules/simple-yenc/.github/FUNDING.yml +0 -1
  228. package/node_modules/simple-yenc/.prettierignore +0 -1
  229. package/node_modules/simple-yenc/LICENSE +0 -7
  230. package/node_modules/simple-yenc/README.md +0 -163
  231. package/node_modules/simple-yenc/dist/esm.js +0 -1
  232. package/node_modules/simple-yenc/dist/index.js +0 -1
  233. package/node_modules/simple-yenc/package.json +0 -50
  234. package/node_modules/simple-yenc/rollup.config.js +0 -27
  235. package/node_modules/simple-yenc/src/simple-yenc.js +0 -302
  236. package/node_modules/ws/LICENSE +0 -20
  237. package/node_modules/ws/README.md +0 -548
  238. package/node_modules/ws/browser.js +0 -8
  239. package/node_modules/ws/index.js +0 -13
  240. package/node_modules/ws/lib/buffer-util.js +0 -131
  241. package/node_modules/ws/lib/constants.js +0 -19
  242. package/node_modules/ws/lib/event-target.js +0 -292
  243. package/node_modules/ws/lib/extension.js +0 -203
  244. package/node_modules/ws/lib/limiter.js +0 -55
  245. package/node_modules/ws/lib/permessage-deflate.js +0 -528
  246. package/node_modules/ws/lib/receiver.js +0 -706
  247. package/node_modules/ws/lib/sender.js +0 -602
  248. package/node_modules/ws/lib/stream.js +0 -161
  249. package/node_modules/ws/lib/subprotocol.js +0 -62
  250. package/node_modules/ws/lib/validation.js +0 -152
  251. package/node_modules/ws/lib/websocket-server.js +0 -554
  252. package/node_modules/ws/lib/websocket.js +0 -1393
  253. package/node_modules/ws/package.json +0 -69
  254. package/node_modules/ws/wrapper.mjs +0 -8
  255. package/scripts/postinstall-link-sdk.js +0 -170
  256. package/skills/qqbot-media/SKILL.md +0 -60
  257. package/src/admin-resolver.ts +0 -181
  258. package/src/api.ts +0 -1333
  259. package/src/credential-backup.ts +0 -72
  260. package/src/deliver-debounce.ts +0 -229
  261. package/src/gateway.ts +0 -2272
  262. package/src/group-history.ts +0 -328
  263. package/src/image-server.ts +0 -675
  264. package/src/inbound-attachments.ts +0 -321
  265. package/src/known-users.ts +0 -353
  266. package/src/message-gating.ts +0 -190
  267. package/src/message-queue.ts +0 -354
  268. package/src/onboarding.ts +0 -282
  269. package/src/outbound-deliver.ts +0 -486
  270. package/src/outbound.ts +0 -1179
  271. package/src/ref-index-store.ts +0 -412
  272. package/src/reply-dispatcher.ts +0 -334
  273. package/src/session-store.ts +0 -303
  274. package/src/slash-commands.ts +0 -2431
  275. package/src/startup-greeting.ts +0 -120
  276. package/src/streaming.ts +0 -1077
  277. package/src/stt.ts +0 -86
  278. package/src/tools/channel.ts +0 -275
  279. package/src/transport/index.ts +0 -11
  280. package/src/transport/webhook-transport.ts +0 -332
  281. package/src/transport/webhook-verify.ts +0 -119
  282. package/src/typing-keepalive.ts +0 -59
  283. package/src/utils/audio-convert.ts +0 -887
  284. package/src/utils/chunked-upload.ts +0 -483
  285. package/src/utils/file-utils.ts +0 -193
  286. package/src/utils/image-size.ts +0 -266
  287. package/src/utils/media-send.ts +0 -636
  288. package/src/utils/media-tags.ts +0 -183
  289. package/src/utils/payload.ts +0 -265
  290. package/src/utils/text-parsing.ts +0 -85
  291. package/src/utils/upload-cache.ts +0 -128
@@ -1,190 +0,0 @@
1
- /**
2
- * 群消息门控 — 统一入口。
3
- *
4
- * 将 ignoreOtherMentions / shouldBlock / mentionGating 三层判断收敛到
5
- * 一个纯函数 resolveGroupMessageGate() 中,让 gateway 主流程只关心一个结果。
6
- *
7
- * 按优先级串行检查:
8
- * 1. ignoreOtherMentions — @了其他人但未 @bot → 丢弃(记历史)
9
- * 2. shouldBlock — 未授权控制命令静默拦截
10
- * 3. mentionGating — requireMention 门控 + 命令旁路
11
- */
12
-
13
- // ────────────────────── Types ──────────────────────
14
-
15
- export type MentionGateResult = {
16
- effectiveWasMentioned: boolean;
17
- shouldSkip: boolean;
18
- };
19
-
20
- export type MentionGateWithBypassResult = MentionGateResult & {
21
- shouldBypassMention: boolean;
22
- };
23
-
24
- export type GroupMessageGateAction =
25
- /** @了其他人但未 @bot,丢弃并记录历史 */
26
- | "drop_other_mention"
27
- /** 未授权控制命令,静默拦截 */
28
- | "block_unauthorized_command"
29
- /** 非 @bot 消息,记录历史后跳过 AI */
30
- | "skip_no_mention"
31
- /** 正常放行,交给 AI */
32
- | "pass";
33
-
34
- export type GroupMessageGateResult = {
35
- action: GroupMessageGateAction;
36
- /** 仅 action=pass|skip_no_mention 时有值 */
37
- effectiveWasMentioned: boolean;
38
- shouldBypassMention: boolean;
39
- };
40
-
41
- export type GroupMessageGateParams = {
42
- // ── ignoreOtherMentions 层 ──
43
- ignoreOtherMentions: boolean;
44
- hasAnyMention: boolean;
45
- wasMentioned: boolean;
46
- implicitMention: boolean;
47
-
48
- // ── shouldBlock 层 ──
49
- allowTextCommands: boolean;
50
- isControlCommand: boolean;
51
- commandAuthorized: boolean;
52
-
53
- // ── mentionGating 层 ──
54
- requireMention: boolean;
55
- canDetectMention: boolean;
56
- };
57
-
58
- // ────────────────────── Core Logic ──────────────────────
59
-
60
- /**
61
- * 基础 mention 门控纯函数。
62
- * effectiveWasMentioned = wasMentioned || implicitMention || shouldBypassMention
63
- * shouldSkip = requireMention && canDetectMention && !effectiveWasMentioned
64
- */
65
- function resolveMentionGating(params: {
66
- requireMention: boolean;
67
- canDetectMention: boolean;
68
- wasMentioned: boolean;
69
- implicitMention?: boolean;
70
- shouldBypassMention?: boolean;
71
- }): MentionGateResult {
72
- const implicit = params.implicitMention === true;
73
- const bypass = params.shouldBypassMention === true;
74
- const effectiveWasMentioned = params.wasMentioned || implicit || bypass;
75
- const shouldSkip = params.requireMention && params.canDetectMention && !effectiveWasMentioned;
76
- return { effectiveWasMentioned, shouldSkip };
77
- }
78
-
79
- /**
80
- * 带命令旁路的 mention 门控。
81
- *
82
- * shouldBypassMention 条件(全部满足时才旁路):
83
- * 1. requireMention — 开启了 mention 要求
84
- * 2. !wasMentioned — 没有被直接 mention
85
- * 3. !hasAnyMention — 消息中没有任何 @(防止 @ 其他人的消息误 bypass)
86
- * 4. allowTextCommands — 文本命令已启用
87
- * 5. commandAuthorized — 发送者有命令权限
88
- * 6. hasControlCommand — 消息是合法控制命令
89
- */
90
- function resolveMentionGatingWithBypass(params: {
91
- requireMention: boolean;
92
- canDetectMention: boolean;
93
- wasMentioned: boolean;
94
- implicitMention?: boolean;
95
- hasAnyMention?: boolean;
96
- allowTextCommands: boolean;
97
- hasControlCommand: boolean;
98
- commandAuthorized: boolean;
99
- }): MentionGateWithBypassResult {
100
- const shouldBypassMention =
101
- params.requireMention &&
102
- !params.wasMentioned &&
103
- !(params.hasAnyMention ?? false) &&
104
- params.allowTextCommands &&
105
- params.commandAuthorized &&
106
- params.hasControlCommand;
107
- return {
108
- ...resolveMentionGating({
109
- requireMention: params.requireMention,
110
- canDetectMention: params.canDetectMention,
111
- wasMentioned: params.wasMentioned,
112
- implicitMention: params.implicitMention,
113
- shouldBypassMention,
114
- }),
115
- shouldBypassMention,
116
- };
117
- }
118
-
119
- // ────────────────────── Unified Gate ──────────────────────
120
-
121
- /**
122
- * 群消息统一门控,按优先级串行判定:
123
- *
124
- * 1. ignoreOtherMentions — @了其他人但未 @bot → drop_other_mention
125
- * 2. shouldBlock — 未授权控制命令 → block_unauthorized_command
126
- * 3. mentionGating — 未满足 @bot 条件 → skip_no_mention
127
- * 4. 通过所有检查 → pass
128
- */
129
- export function resolveGroupMessageGate(params: GroupMessageGateParams): GroupMessageGateResult {
130
- const {
131
- ignoreOtherMentions,
132
- hasAnyMention,
133
- wasMentioned,
134
- implicitMention,
135
- allowTextCommands,
136
- isControlCommand,
137
- commandAuthorized,
138
- requireMention,
139
- canDetectMention,
140
- } = params;
141
-
142
- // ── Layer 1: ignoreOtherMentions ──
143
- if (
144
- ignoreOtherMentions &&
145
- hasAnyMention &&
146
- !wasMentioned &&
147
- !implicitMention
148
- ) {
149
- return {
150
- action: "drop_other_mention",
151
- effectiveWasMentioned: false,
152
- shouldBypassMention: false,
153
- };
154
- }
155
-
156
- // ── Layer 2: shouldBlock 未授权控制命令 ──
157
- if (allowTextCommands && isControlCommand && !commandAuthorized) {
158
- return {
159
- action: "block_unauthorized_command",
160
- effectiveWasMentioned: false,
161
- shouldBypassMention: false,
162
- };
163
- }
164
-
165
- // ── Layer 3: mention 门控 + 命令旁路 ──
166
- const mentionGate = resolveMentionGatingWithBypass({
167
- requireMention,
168
- canDetectMention,
169
- wasMentioned,
170
- implicitMention,
171
- hasAnyMention,
172
- allowTextCommands,
173
- hasControlCommand: isControlCommand,
174
- commandAuthorized,
175
- });
176
-
177
- if (mentionGate.shouldSkip) {
178
- return {
179
- action: "skip_no_mention",
180
- effectiveWasMentioned: mentionGate.effectiveWasMentioned,
181
- shouldBypassMention: mentionGate.shouldBypassMention,
182
- };
183
- }
184
-
185
- return {
186
- action: "pass",
187
- effectiveWasMentioned: mentionGate.effectiveWasMentioned,
188
- shouldBypassMention: mentionGate.shouldBypassMention,
189
- };
190
- }
@@ -1,354 +0,0 @@
1
- import type { QueueSnapshot } from "./slash-commands.js";
2
- import type { MsgElement } from "./types.js";
3
-
4
- // ── 消息队列默认配置 ──
5
- const DEFAULT_GLOBAL_QUEUE_SIZE = 1000;
6
- const DEFAULT_PER_PEER_QUEUE_SIZE = 20;
7
- const DEFAULT_GROUP_QUEUE_SIZE = 50;
8
- const DEFAULT_MAX_CONCURRENT_USERS = 10;
9
-
10
- /**
11
- * 消息队列项类型(用于异步处理消息,防止阻塞心跳)
12
- */
13
- export interface QueuedMessage {
14
- type: "c2c" | "guild" | "dm" | "group";
15
- senderId: string;
16
- senderName?: string;
17
- content: string;
18
- messageId: string;
19
- timestamp: string;
20
- channelId?: string;
21
- guildId?: string;
22
- groupOpenid?: string;
23
- attachments?: Array<{ content_type: string; url: string; filename?: string; voice_wav_url?: string; asr_refer_text?: string }>;
24
- /** 被引用消息的 refIdx(用户引用了哪条历史消息) */
25
- refMsgIdx?: string;
26
- /** 当前消息自身的 refIdx(供将来被引用) */
27
- msgIdx?: string;
28
- /** 事件类型(如 GROUP_AT_MESSAGE_CREATE),用于群消息合并时判断是否有 @ */
29
- eventType?: string;
30
- /** 发送者是否为机器人 */
31
- senderIsBot?: boolean;
32
- /** @ 提及列表(群消息合并时需要去重合并) */
33
- mentions?: Array<{ scope?: "all" | "single"; id?: string; user_openid?: string; member_openid?: string; username?: string; bot?: boolean; is_you?: boolean }>;
34
- /** 消息场景(来源、扩展字段) */
35
- messageScene?: { source?: string; ext?: string[] };
36
- /** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
37
- msgElements?: MsgElement[];
38
- /** 消息类型,参见 MSG_TYPE_* */
39
- msgType?: number;
40
- /** 群消息合并标记:记录合并了多少条原始消息 */
41
- _mergedCount?: number;
42
- /** 合并前的原始消息列表(用于 gateway 侧逐条格式化信封) */
43
- _mergedMessages?: QueuedMessage[];
44
- }
45
-
46
- export interface MessageQueueContext {
47
- accountId: string;
48
- log?: {
49
- info: (msg: string) => void;
50
- error: (msg: string) => void;
51
- debug?: (msg: string) => void;
52
- };
53
- /** 外部提供的 abort 状态检查 */
54
- isAborted: () => boolean;
55
- /** 群聊队列上限(默认 50) */
56
- groupQueueSize?: number;
57
- /** 私聊/DM 队列上限(默认 20) */
58
- peerQueueSize?: number;
59
- /** 全局队列总量上限(默认 1000) */
60
- globalQueueSize?: number;
61
- /** 最大并发处理用户数(默认 10) */
62
- maxConcurrentUsers?: number;
63
- }
64
-
65
- export interface MessageQueue {
66
- enqueue: (msg: QueuedMessage) => void;
67
- startProcessor: (handleMessageFn: (msg: QueuedMessage) => Promise<void>) => void;
68
- getSnapshot: (senderPeerId: string) => QueueSnapshot;
69
- getMessagePeerId: (msg: QueuedMessage) => string;
70
- /** 清空指定用户的排队消息,返回被丢弃的消息数 */
71
- clearUserQueue: (peerId: string) => number;
72
- /** 立即执行一条消息(绕过队列),用于紧急命令 */
73
- executeImmediate: (msg: QueuedMessage) => void;
74
- }
75
-
76
- // ── 群消息合并工具函数 ──
77
-
78
- /** 判断 peerId 是否属于群聊 */
79
- const isGroupPeer = (peerId: string): boolean =>
80
- peerId.startsWith("group:") || peerId.startsWith("guild:");
81
-
82
- /**
83
- * 将多条群消息合并为一条,用于群聊场景下排队消息的批量处理。
84
- * - content 拼接为多行,每行带发送者前缀
85
- * - 附件合并
86
- * - messageId / msgIdx / timestamp 取最后一条(用于回复引用)
87
- * - mentions 合并去重
88
- * - 如果有任意一条 @了你(is_you),合并结果也标记 @你
89
- * - senderIsBot 只要有一条不是 bot 就算非 bot
90
- */
91
- function mergeGroupMessages(batch: QueuedMessage[]): QueuedMessage {
92
- if (batch.length === 1) return batch[0];
93
-
94
- const last = batch[batch.length - 1];
95
- const first = batch[0];
96
-
97
- // 拼接内容:每条消息带发送者前缀
98
- const mergedContent = batch
99
- .map((m) => {
100
- const name = m.senderName ?? m.senderId;
101
- return `[${name}]: ${m.content}`;
102
- })
103
- .join("\n");
104
-
105
- // 合并附件
106
- const mergedAttachments: QueuedMessage["attachments"] = [];
107
- for (const m of batch) {
108
- if (m.attachments?.length) {
109
- mergedAttachments.push(...m.attachments);
110
- }
111
- }
112
-
113
- // 合并 mentions(去重 by member_openid/id)
114
- const seenMentionIds = new Set<string>();
115
- const mergedMentions: NonNullable<QueuedMessage["mentions"]> = [];
116
- let hasAtYouEvent = false;
117
- for (const m of batch) {
118
- if (m.eventType === "GROUP_AT_MESSAGE_CREATE") {
119
- hasAtYouEvent = true;
120
- }
121
- if (m.mentions) {
122
- for (const mt of m.mentions) {
123
- const key = mt.member_openid ?? mt.id ?? mt.user_openid ?? "";
124
- if (key && seenMentionIds.has(key)) continue;
125
- if (key) seenMentionIds.add(key);
126
- mergedMentions.push(mt);
127
- }
128
- }
129
- }
130
-
131
- // senderIsBot: 只要有一条来自非 bot 用户,就算非 bot
132
- const allFromBot = batch.every((m) => m.senderIsBot);
133
-
134
- return {
135
- type: last.type,
136
- senderId: last.senderId,
137
- senderName: last.senderName,
138
- senderIsBot: allFromBot,
139
- content: mergedContent,
140
- messageId: last.messageId,
141
- timestamp: last.timestamp,
142
- channelId: last.channelId,
143
- guildId: last.guildId,
144
- groupOpenid: last.groupOpenid,
145
- attachments: mergedAttachments.length > 0 ? mergedAttachments : undefined,
146
- refMsgIdx: first.refMsgIdx,
147
- msgIdx: last.msgIdx,
148
- eventType: hasAtYouEvent ? "GROUP_AT_MESSAGE_CREATE" : last.eventType,
149
- mentions: mergedMentions.length > 0 ? mergedMentions : undefined,
150
- messageScene: last.messageScene,
151
- _mergedCount: batch.length,
152
- _mergedMessages: batch.length > 1 ? batch : undefined,
153
- };
154
- }
155
-
156
- /**
157
- * 创建按用户并发的消息队列(同用户串行,跨用户并行)
158
- *
159
- * 内置群消息增强:
160
- * - 群聊 / 私聊使用不同队列上限
161
- * - 群聊溢出时优先丢弃 bot 消息
162
- * - drain 时自动合并群聊排队消息(斜杠命令单独处理)
163
- */
164
- export function createMessageQueue(ctx: MessageQueueContext): MessageQueue {
165
- const { accountId, log } = ctx;
166
- const globalQueueSize = ctx.globalQueueSize ?? DEFAULT_GLOBAL_QUEUE_SIZE;
167
- const peerQueueSize = ctx.peerQueueSize ?? DEFAULT_PER_PEER_QUEUE_SIZE;
168
- const groupQueueSize = ctx.groupQueueSize ?? DEFAULT_GROUP_QUEUE_SIZE;
169
- const maxConcurrentUsers = ctx.maxConcurrentUsers ?? DEFAULT_MAX_CONCURRENT_USERS;
170
-
171
- const userQueues = new Map<string, QueuedMessage[]>();
172
- const activeUsers = new Set<string>();
173
- let handleMessageFnRef: ((msg: QueuedMessage) => Promise<void>) | null = null;
174
- let totalEnqueued = 0;
175
-
176
- const getMessagePeerId = (msg: QueuedMessage): string => {
177
- if (msg.type === "guild") return `guild:${msg.channelId ?? "unknown"}`;
178
- if (msg.type === "group") return `group:${msg.groupOpenid ?? "unknown"}`;
179
- return `dm:${msg.senderId}`;
180
- };
181
-
182
- /** 从满队列中淘汰一条消息(群聊优先丢弃 bot 消息,否则丢弃最旧) */
183
- const evictOne = (queue: QueuedMessage[], isGroup: boolean): QueuedMessage | undefined => {
184
- if (isGroup) {
185
- const botIdx = queue.findIndex(m => m.senderIsBot);
186
- if (botIdx >= 0) return queue.splice(botIdx, 1)[0];
187
- }
188
- return queue.shift();
189
- };
190
-
191
- /** 判断消息是否为斜杠指令 */
192
- const isSlashCommand = (msg: QueuedMessage): boolean =>
193
- (msg.content ?? "").trim().startsWith("/");
194
-
195
- /** 处理单条消息,捕获异常并记录日志 */
196
- const processOne = async (
197
- msg: QueuedMessage,
198
- peerId: string,
199
- label: string,
200
- ): Promise<boolean> => {
201
- try {
202
- await handleMessageFnRef!(msg);
203
- return true;
204
- } catch (err) {
205
- log?.error(`[qqbot:${accountId}] ${label} error for ${peerId}: ${err}`);
206
- return false;
207
- }
208
- };
209
-
210
- /** 批量处理群聊排队消息:斜杠指令逐条处理,普通消息合并后处理 */
211
- const drainGroupBatch = async (all: QueuedMessage[], peerId: string): Promise<void> => {
212
- const commands: QueuedMessage[] = [];
213
- const normal: QueuedMessage[] = [];
214
- for (const m of all) {
215
- (isSlashCommand(m) ? commands : normal).push(m);
216
- }
217
-
218
- // 指令消息逐条处理
219
- for (const cmd of commands) {
220
- log?.info(`[qqbot:${accountId}] Processing command independently for ${peerId}: ${(cmd.content ?? "").trim().slice(0, 50)}`);
221
- await processOne(cmd, peerId, "Command processor");
222
- }
223
-
224
- // 普通消息合并后处理
225
- if (normal.length > 0) {
226
- const merged = mergeGroupMessages(normal);
227
- if (normal.length > 1) {
228
- log?.info(`[qqbot:${accountId}] Merged ${normal.length} queued group messages for ${peerId} into one`);
229
- }
230
- await processOne(merged, peerId, `Message processor (merged batch of ${normal.length})`);
231
- }
232
- };
233
-
234
- /** 处理指定 peer 队列中的消息(串行) */
235
- const drainUserQueue = async (peerId: string): Promise<void> => {
236
- if (activeUsers.has(peerId)) return;
237
- if (activeUsers.size >= maxConcurrentUsers) {
238
- log?.info(`[qqbot:${accountId}] Max concurrent users (${maxConcurrentUsers}) reached, ${peerId} will wait`);
239
- return;
240
- }
241
-
242
- const queue = userQueues.get(peerId);
243
- if (!queue || queue.length === 0) {
244
- userQueues.delete(peerId);
245
- return;
246
- }
247
-
248
- activeUsers.add(peerId);
249
- const isGroup = isGroupPeer(peerId);
250
-
251
- try {
252
- while (queue.length > 0 && !ctx.isAborted()) {
253
- // 群聊排队 > 1 条:批量处理
254
- if (isGroup && queue.length > 1 && handleMessageFnRef) {
255
- const all = queue.splice(0, queue.length);
256
- totalEnqueued = Math.max(0, totalEnqueued - all.length);
257
- await drainGroupBatch(all, peerId);
258
- continue;
259
- }
260
-
261
- // 非群聊 或 队列只剩 1 条:逐条处理
262
- const msg = queue.shift()!;
263
- totalEnqueued = Math.max(0, totalEnqueued - 1);
264
- if (handleMessageFnRef) {
265
- await processOne(msg, peerId, "Message processor");
266
- }
267
- }
268
- } finally {
269
- activeUsers.delete(peerId);
270
- userQueues.delete(peerId);
271
- // 尽量填满并发槽位
272
- for (const [waitingPeerId, waitingQueue] of userQueues) {
273
- if (activeUsers.size >= maxConcurrentUsers) break;
274
- if (waitingQueue.length > 0 && !activeUsers.has(waitingPeerId)) {
275
- drainUserQueue(waitingPeerId);
276
- }
277
- }
278
- }
279
- };
280
-
281
- const enqueue = (msg: QueuedMessage): void => {
282
- const peerId = getMessagePeerId(msg);
283
- const isGroup = isGroupPeer(peerId);
284
- let queue = userQueues.get(peerId);
285
- if (!queue) {
286
- queue = [];
287
- userQueues.set(peerId, queue);
288
- }
289
-
290
- // 群聊和非群聊使用不同的队列上限
291
- const maxSize = isGroup ? groupQueueSize : peerQueueSize;
292
-
293
- // 队列溢出:淘汰一条旧消息
294
- if (queue.length >= maxSize) {
295
- const dropped = evictOne(queue, isGroup);
296
- totalEnqueued = Math.max(0, totalEnqueued - 1);
297
- if (isGroup && dropped?.senderIsBot) {
298
- log?.info(`[qqbot:${accountId}] Queue full for ${peerId}, dropping bot message ${dropped.messageId}`);
299
- } else {
300
- log?.error(`[qqbot:${accountId}] Queue full for ${peerId}, dropping oldest message ${dropped?.messageId}`);
301
- }
302
- }
303
-
304
- // 全局总量保护
305
- totalEnqueued++;
306
- if (totalEnqueued > globalQueueSize) {
307
- log?.error(`[qqbot:${accountId}] Global queue limit reached (${totalEnqueued}), message from ${peerId} may be delayed`);
308
- }
309
-
310
- queue.push(msg);
311
- log?.debug?.(`[qqbot:${accountId}] Message enqueued for ${peerId}, user queue: ${queue.length}, active users: ${activeUsers.size}`);
312
-
313
- // 如果该用户没有正在处理的消息,立即启动处理
314
- drainUserQueue(peerId);
315
- };
316
-
317
- const startProcessor = (handleMessageFn: (msg: QueuedMessage) => Promise<void>): void => {
318
- handleMessageFnRef = handleMessageFn;
319
- log?.info(`[qqbot:${accountId}] Message processor started (per-user concurrency, max ${maxConcurrentUsers} users)`);
320
- };
321
-
322
- const getSnapshot = (senderPeerId: string): QueueSnapshot => {
323
- let totalPending = 0;
324
- for (const [, q] of userQueues) {
325
- totalPending += q.length;
326
- }
327
- const senderQueue = userQueues.get(senderPeerId);
328
- return {
329
- totalPending,
330
- activeUsers: activeUsers.size,
331
- maxConcurrentUsers,
332
- senderPending: senderQueue ? senderQueue.length : 0,
333
- };
334
- };
335
-
336
- const clearUserQueue = (peerId: string): number => {
337
- const queue = userQueues.get(peerId);
338
- if (!queue || queue.length === 0) return 0;
339
- const droppedCount = queue.length;
340
- queue.length = 0;
341
- totalEnqueued = Math.max(0, totalEnqueued - droppedCount);
342
- return droppedCount;
343
- };
344
-
345
- const executeImmediate = (msg: QueuedMessage): void => {
346
- if (handleMessageFnRef) {
347
- handleMessageFnRef(msg).catch(err => {
348
- log?.error(`[qqbot:${accountId}] Immediate execution error: ${err}`);
349
- });
350
- }
351
- };
352
-
353
- return { enqueue, startProcessor, getSnapshot, getMessagePeerId, clearUserQueue, executeImmediate };
354
- }