@tencent-connect/openclaw-qqbot 1.7.1 → 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 (283) hide show
  1. package/README.md +206 -4
  2. package/README.zh.md +207 -4
  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 -18
  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 +400 -73
  13. package/scripts/upgrade-via-source.sh +126 -89
  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 +59 -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 +1 -1
  85. package/src/types-augment.d.ts +54 -0
  86. package/src/types.ts +71 -8
  87. package/src/typings/openclaw-webhook-ingress.d.ts +66 -0
  88. package/src/utils/mention.ts +52 -0
  89. package/src/utils/pkg-version.ts +84 -47
  90. package/src/utils/platform.ts +31 -7
  91. package/src/utils/plugin-logger.ts +104 -0
  92. package/src/utils/ssrf-guard.ts +32 -2
  93. package/src/utils/stt.ts +137 -0
  94. package/src/utils/voice-text.ts +61 -0
  95. package/tsconfig.json +2 -1
  96. package/tsup.config.ts +57 -0
  97. package/dist/index.d.ts +0 -17
  98. package/dist/index.js +0 -26
  99. package/dist/src/admin-resolver.d.ts +0 -33
  100. package/dist/src/admin-resolver.js +0 -157
  101. package/dist/src/api.d.ts +0 -307
  102. package/dist/src/api.js +0 -910
  103. package/dist/src/approval-handler.d.ts +0 -47
  104. package/dist/src/approval-handler.js +0 -372
  105. package/dist/src/channel.d.ts +0 -29
  106. package/dist/src/channel.js +0 -528
  107. package/dist/src/config.d.ts +0 -56
  108. package/dist/src/config.js +0 -278
  109. package/dist/src/credential-backup.d.ts +0 -31
  110. package/dist/src/credential-backup.js +0 -66
  111. package/dist/src/deliver-debounce.d.ts +0 -74
  112. package/dist/src/deliver-debounce.js +0 -174
  113. package/dist/src/gateway.d.ts +0 -18
  114. package/dist/src/gateway.js +0 -2063
  115. package/dist/src/group-history.d.ts +0 -136
  116. package/dist/src/group-history.js +0 -226
  117. package/dist/src/image-server.d.ts +0 -87
  118. package/dist/src/image-server.js +0 -570
  119. package/dist/src/inbound-attachments.d.ts +0 -60
  120. package/dist/src/inbound-attachments.js +0 -248
  121. package/dist/src/known-users.d.ts +0 -100
  122. package/dist/src/known-users.js +0 -263
  123. package/dist/src/message-gating.d.ts +0 -53
  124. package/dist/src/message-gating.js +0 -107
  125. package/dist/src/message-queue.d.ts +0 -91
  126. package/dist/src/message-queue.js +0 -257
  127. package/dist/src/onboarding.d.ts +0 -10
  128. package/dist/src/onboarding.js +0 -203
  129. package/dist/src/outbound-deliver.d.ts +0 -48
  130. package/dist/src/outbound-deliver.js +0 -392
  131. package/dist/src/outbound.d.ts +0 -220
  132. package/dist/src/outbound.js +0 -975
  133. package/dist/src/proactive.d.ts +0 -170
  134. package/dist/src/proactive.js +0 -399
  135. package/dist/src/ref-index-store.d.ts +0 -101
  136. package/dist/src/ref-index-store.js +0 -298
  137. package/dist/src/reply-dispatcher.d.ts +0 -35
  138. package/dist/src/reply-dispatcher.js +0 -311
  139. package/dist/src/request-context.d.ts +0 -25
  140. package/dist/src/request-context.js +0 -37
  141. package/dist/src/runtime.d.ts +0 -3
  142. package/dist/src/runtime.js +0 -13
  143. package/dist/src/session-store.d.ts +0 -52
  144. package/dist/src/session-store.js +0 -254
  145. package/dist/src/slash-commands.d.ts +0 -82
  146. package/dist/src/slash-commands.js +0 -2117
  147. package/dist/src/startup-greeting.d.ts +0 -30
  148. package/dist/src/startup-greeting.js +0 -97
  149. package/dist/src/streaming.d.ts +0 -247
  150. package/dist/src/streaming.js +0 -899
  151. package/dist/src/stt.d.ts +0 -21
  152. package/dist/src/stt.js +0 -70
  153. package/dist/src/tools/channel.d.ts +0 -16
  154. package/dist/src/tools/channel.js +0 -234
  155. package/dist/src/tools/remind.d.ts +0 -2
  156. package/dist/src/tools/remind.js +0 -256
  157. package/dist/src/types.d.ts +0 -443
  158. package/dist/src/types.js +0 -22
  159. package/dist/src/typing-keepalive.d.ts +0 -27
  160. package/dist/src/typing-keepalive.js +0 -64
  161. package/dist/src/update-checker.d.ts +0 -36
  162. package/dist/src/update-checker.js +0 -171
  163. package/dist/src/utils/audio-convert.d.ts +0 -98
  164. package/dist/src/utils/audio-convert.js +0 -755
  165. package/dist/src/utils/chunked-upload.d.ts +0 -68
  166. package/dist/src/utils/chunked-upload.js +0 -341
  167. package/dist/src/utils/file-utils.d.ts +0 -61
  168. package/dist/src/utils/file-utils.js +0 -172
  169. package/dist/src/utils/image-size.d.ts +0 -51
  170. package/dist/src/utils/image-size.js +0 -234
  171. package/dist/src/utils/media-send.d.ts +0 -158
  172. package/dist/src/utils/media-send.js +0 -502
  173. package/dist/src/utils/media-tags.d.ts +0 -14
  174. package/dist/src/utils/media-tags.js +0 -165
  175. package/dist/src/utils/payload.d.ts +0 -112
  176. package/dist/src/utils/payload.js +0 -186
  177. package/dist/src/utils/pkg-version.d.ts +0 -5
  178. package/dist/src/utils/pkg-version.js +0 -61
  179. package/dist/src/utils/platform.d.ts +0 -137
  180. package/dist/src/utils/platform.js +0 -390
  181. package/dist/src/utils/ssrf-guard.d.ts +0 -25
  182. package/dist/src/utils/ssrf-guard.js +0 -91
  183. package/dist/src/utils/text-parsing.d.ts +0 -31
  184. package/dist/src/utils/text-parsing.js +0 -74
  185. package/dist/src/utils/upload-cache.d.ts +0 -34
  186. package/dist/src/utils/upload-cache.js +0 -93
  187. package/node_modules/@eshaz/web-worker/LICENSE +0 -201
  188. package/node_modules/@eshaz/web-worker/README.md +0 -134
  189. package/node_modules/@eshaz/web-worker/browser.js +0 -17
  190. package/node_modules/@eshaz/web-worker/cjs/browser.js +0 -16
  191. package/node_modules/@eshaz/web-worker/cjs/node.js +0 -219
  192. package/node_modules/@eshaz/web-worker/index.d.ts +0 -4
  193. package/node_modules/@eshaz/web-worker/node.js +0 -223
  194. package/node_modules/@eshaz/web-worker/package.json +0 -54
  195. package/node_modules/@wasm-audio-decoders/common/index.js +0 -5
  196. package/node_modules/@wasm-audio-decoders/common/package.json +0 -36
  197. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +0 -231
  198. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +0 -129
  199. package/node_modules/@wasm-audio-decoders/common/src/puff/README +0 -67
  200. package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +0 -31
  201. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +0 -863
  202. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +0 -35
  203. package/node_modules/@wasm-audio-decoders/common/src/utilities.js +0 -3
  204. package/node_modules/@wasm-audio-decoders/common/types.d.ts +0 -7
  205. package/node_modules/mpg123-decoder/README.md +0 -265
  206. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +0 -185
  207. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +0 -1
  208. package/node_modules/mpg123-decoder/index.js +0 -8
  209. package/node_modules/mpg123-decoder/package.json +0 -58
  210. package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +0 -464
  211. package/node_modules/mpg123-decoder/src/MPEGDecoder.js +0 -200
  212. package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +0 -21
  213. package/node_modules/mpg123-decoder/types.d.ts +0 -30
  214. package/node_modules/silk-wasm/LICENSE +0 -21
  215. package/node_modules/silk-wasm/README.md +0 -85
  216. package/node_modules/silk-wasm/lib/index.cjs +0 -16
  217. package/node_modules/silk-wasm/lib/index.d.ts +0 -70
  218. package/node_modules/silk-wasm/lib/index.mjs +0 -16
  219. package/node_modules/silk-wasm/lib/silk.wasm +0 -0
  220. package/node_modules/silk-wasm/lib/utils.d.ts +0 -4
  221. package/node_modules/silk-wasm/package.json +0 -39
  222. package/node_modules/simple-yenc/.github/FUNDING.yml +0 -1
  223. package/node_modules/simple-yenc/.prettierignore +0 -1
  224. package/node_modules/simple-yenc/LICENSE +0 -7
  225. package/node_modules/simple-yenc/README.md +0 -163
  226. package/node_modules/simple-yenc/dist/esm.js +0 -1
  227. package/node_modules/simple-yenc/dist/index.js +0 -1
  228. package/node_modules/simple-yenc/package.json +0 -50
  229. package/node_modules/simple-yenc/rollup.config.js +0 -27
  230. package/node_modules/simple-yenc/src/simple-yenc.js +0 -302
  231. package/node_modules/ws/LICENSE +0 -20
  232. package/node_modules/ws/README.md +0 -548
  233. package/node_modules/ws/browser.js +0 -8
  234. package/node_modules/ws/index.js +0 -13
  235. package/node_modules/ws/lib/buffer-util.js +0 -131
  236. package/node_modules/ws/lib/constants.js +0 -19
  237. package/node_modules/ws/lib/event-target.js +0 -292
  238. package/node_modules/ws/lib/extension.js +0 -203
  239. package/node_modules/ws/lib/limiter.js +0 -55
  240. package/node_modules/ws/lib/permessage-deflate.js +0 -528
  241. package/node_modules/ws/lib/receiver.js +0 -706
  242. package/node_modules/ws/lib/sender.js +0 -602
  243. package/node_modules/ws/lib/stream.js +0 -161
  244. package/node_modules/ws/lib/subprotocol.js +0 -62
  245. package/node_modules/ws/lib/validation.js +0 -152
  246. package/node_modules/ws/lib/websocket-server.js +0 -554
  247. package/node_modules/ws/lib/websocket.js +0 -1393
  248. package/node_modules/ws/package.json +0 -69
  249. package/node_modules/ws/wrapper.mjs +0 -8
  250. package/scripts/postinstall-link-sdk.js +0 -126
  251. package/skills/qqbot-media/SKILL.md +0 -60
  252. package/src/admin-resolver.ts +0 -181
  253. package/src/api.ts +0 -1319
  254. package/src/credential-backup.ts +0 -72
  255. package/src/deliver-debounce.ts +0 -229
  256. package/src/gateway.ts +0 -2304
  257. package/src/group-history.ts +0 -328
  258. package/src/image-server.ts +0 -675
  259. package/src/inbound-attachments.ts +0 -321
  260. package/src/known-users.ts +0 -353
  261. package/src/message-gating.ts +0 -190
  262. package/src/message-queue.ts +0 -354
  263. package/src/onboarding.ts +0 -274
  264. package/src/outbound-deliver.ts +0 -486
  265. package/src/outbound.ts +0 -1179
  266. package/src/ref-index-store.ts +0 -412
  267. package/src/reply-dispatcher.ts +0 -334
  268. package/src/session-store.ts +0 -303
  269. package/src/slash-commands.ts +0 -2297
  270. package/src/startup-greeting.ts +0 -120
  271. package/src/streaming.ts +0 -1077
  272. package/src/stt.ts +0 -86
  273. package/src/tools/channel.ts +0 -281
  274. package/src/typing-keepalive.ts +0 -59
  275. package/src/utils/audio-convert.ts +0 -859
  276. package/src/utils/chunked-upload.ts +0 -483
  277. package/src/utils/file-utils.ts +0 -193
  278. package/src/utils/image-size.ts +0 -266
  279. package/src/utils/media-send.ts +0 -636
  280. package/src/utils/media-tags.ts +0 -183
  281. package/src/utils/payload.ts +0 -265
  282. package/src/utils/text-parsing.ts +0 -85
  283. package/src/utils/upload-cache.ts +0 -128
@@ -1,248 +0,0 @@
1
- /**
2
- * 入站附件处理模块
3
- *
4
- * 负责下载、转换、转录用户发送的附件(图片/语音/文件),
5
- * 并归类为统一的 ProcessedAttachments 结构供 gateway 消费。
6
- */
7
- import { downloadFile } from "./image-server.js";
8
- import { convertSilkToWav, isVoiceAttachment, formatDuration } from "./utils/audio-convert.js";
9
- import { transcribeAudio, resolveSTTConfig } from "./stt.js";
10
- import { getQQBotMediaDir } from "./utils/platform.js";
11
- // ============ 空结果常量 ============
12
- const EMPTY_RESULT = {
13
- attachmentInfo: "",
14
- imageUrls: [],
15
- imageMediaTypes: [],
16
- voiceAttachmentPaths: [],
17
- voiceAttachmentUrls: [],
18
- voiceAsrReferTexts: [],
19
- voiceTranscripts: [],
20
- voiceTranscriptSources: [],
21
- attachmentLocalPaths: [],
22
- };
23
- // ============ 主函数 ============
24
- /**
25
- * 处理入站消息的附件列表。
26
- *
27
- * 三阶段流水线:
28
- * 1. 并行下载所有附件到本地
29
- * 2. 并行处理语音转换 + STT 转录
30
- * 3. 按原始顺序归类结果
31
- */
32
- export async function processAttachments(attachments, ctx) {
33
- if (!attachments?.length)
34
- return EMPTY_RESULT;
35
- const { appId, peerId, cfg, log } = ctx;
36
- const subPaths = ["downloads", appId, ...(peerId ? [peerId] : [])];
37
- const downloadDir = getQQBotMediaDir(...subPaths);
38
- const prefix = `[qqbot:${appId}]`;
39
- // 结果收集
40
- const imageUrls = [];
41
- const imageMediaTypes = [];
42
- const voiceAttachmentPaths = [];
43
- const voiceAttachmentUrls = [];
44
- const voiceAsrReferTexts = [];
45
- const voiceTranscripts = [];
46
- const voiceTranscriptSources = [];
47
- const attachmentLocalPaths = [];
48
- const otherAttachments = [];
49
- // 入站附件下载:限制 2 分钟,不限大小
50
- const INBOUND_DOWNLOAD_TIMEOUT_MS = 2 * 60 * 1000; // 2 分钟
51
- // Phase 1: 并行下载所有附件
52
- const downloadTasks = attachments.map(async (att) => {
53
- const attUrl = att.url?.startsWith("//") ? `https:${att.url}` : att.url;
54
- const isVoice = isVoiceAttachment(att);
55
- const wavUrl = isVoice && att.voice_wav_url
56
- ? (att.voice_wav_url.startsWith("//") ? `https:${att.voice_wav_url}` : att.voice_wav_url)
57
- : "";
58
- let localPath = null;
59
- let audioPath = null;
60
- let dlError;
61
- if (isVoice && wavUrl) {
62
- const wavResult = await downloadFile(wavUrl, undefined, { destDir: downloadDir, timeoutMs: INBOUND_DOWNLOAD_TIMEOUT_MS });
63
- if (wavResult.filePath) {
64
- localPath = wavResult.filePath;
65
- audioPath = wavResult.filePath;
66
- log?.info(`${prefix} Voice attachment: ${att.filename}, downloaded WAV directly (skip SILK→WAV)`);
67
- }
68
- else {
69
- log?.error(`${prefix} Failed to download voice_wav_url (${wavResult.error}), falling back to original URL`);
70
- }
71
- }
72
- if (!localPath) {
73
- const dlResult = await downloadFile(attUrl, att.filename, { destDir: downloadDir, timeoutMs: INBOUND_DOWNLOAD_TIMEOUT_MS });
74
- localPath = dlResult.filePath;
75
- dlError = dlResult.error;
76
- }
77
- return { att, attUrl, isVoice, localPath, audioPath, dlError };
78
- });
79
- const downloadResults = await Promise.all(downloadTasks);
80
- // Phase 2: 并行处理语音转换 + 转录(非语音附件同步归类)
81
- const processTasks = downloadResults.map(async ({ att, attUrl, isVoice, localPath, audioPath, dlError }) => {
82
- const asrReferText = typeof att.asr_refer_text === "string" ? att.asr_refer_text.trim() : "";
83
- const wavUrl = isVoice && att.voice_wav_url
84
- ? (att.voice_wav_url.startsWith("//") ? `https:${att.voice_wav_url}` : att.voice_wav_url)
85
- : "";
86
- const voiceSourceUrl = wavUrl || attUrl;
87
- const meta = {
88
- voiceUrl: isVoice && voiceSourceUrl ? voiceSourceUrl : undefined,
89
- asrReferText: isVoice && asrReferText ? asrReferText : undefined,
90
- };
91
- if (localPath) {
92
- if (att.content_type?.startsWith("image/")) {
93
- log?.info(`${prefix} Downloaded attachment to: ${localPath}`);
94
- return { localPath, type: "image", contentType: att.content_type, meta };
95
- }
96
- else if (isVoice) {
97
- log?.info(`${prefix} Downloaded attachment to: ${localPath}`);
98
- return processVoiceAttachment(localPath, audioPath, att, asrReferText, cfg, downloadDir, log, prefix);
99
- }
100
- else {
101
- log?.info(`${prefix} Downloaded attachment to: ${localPath}`);
102
- return { localPath, type: "other", filename: att.filename, meta };
103
- }
104
- }
105
- else {
106
- log?.error(`${prefix} Failed to download: ${attUrl}`);
107
- if (att.content_type?.startsWith("image/")) {
108
- return { localPath: null, type: "image-fallback", attUrl, contentType: att.content_type, dlError, meta };
109
- }
110
- else if (isVoice && asrReferText) {
111
- log?.info(`${prefix} Voice attachment download failed, using asr_refer_text fallback`);
112
- return { localPath: null, type: "voice-fallback", transcript: asrReferText, meta };
113
- }
114
- else {
115
- return { localPath: null, type: "other-fallback", filename: att.filename ?? att.content_type, dlError, meta };
116
- }
117
- }
118
- });
119
- const processResults = await Promise.all(processTasks);
120
- // Phase 3: 按原始顺序归类结果
121
- for (const result of processResults) {
122
- if (result.meta.voiceUrl)
123
- voiceAttachmentUrls.push(result.meta.voiceUrl);
124
- if (result.meta.asrReferText)
125
- voiceAsrReferTexts.push(result.meta.asrReferText);
126
- if (result.type === "image" && result.localPath) {
127
- imageUrls.push(result.localPath);
128
- imageMediaTypes.push(result.contentType);
129
- attachmentLocalPaths.push(result.localPath);
130
- }
131
- else if (result.type === "voice" && result.localPath) {
132
- voiceAttachmentPaths.push(result.localPath);
133
- voiceTranscripts.push(result.transcript);
134
- voiceTranscriptSources.push(result.transcriptSource);
135
- attachmentLocalPaths.push(result.localPath);
136
- }
137
- else if (result.type === "other" && result.localPath) {
138
- otherAttachments.push(`[附件: ${result.localPath}]`);
139
- attachmentLocalPaths.push(result.localPath);
140
- }
141
- else if (result.type === "image-fallback") {
142
- imageUrls.push(result.attUrl);
143
- imageMediaTypes.push(result.contentType);
144
- attachmentLocalPaths.push(null);
145
- // 给模型一个明确的失败提示(和 other-fallback 对齐)
146
- const hint = result.dlError?.includes("超时")
147
- ? "(图片下载超时)"
148
- : "(图片下载失败)";
149
- otherAttachments.push(`[图片] ${hint}`);
150
- }
151
- else if (result.type === "voice-fallback") {
152
- voiceTranscripts.push(result.transcript);
153
- voiceTranscriptSources.push("asr");
154
- attachmentLocalPaths.push(null);
155
- }
156
- else if (result.type === "other-fallback") {
157
- const hint = result.dlError?.includes("超时")
158
- ? "(下载超时)"
159
- : "(下载失败)";
160
- otherAttachments.push(`[附件: ${result.filename}] ${hint}`);
161
- attachmentLocalPaths.push(null);
162
- }
163
- }
164
- const attachmentInfo = otherAttachments.length > 0 ? "\n" + otherAttachments.join("\n") : "";
165
- return {
166
- attachmentInfo,
167
- imageUrls,
168
- imageMediaTypes,
169
- voiceAttachmentPaths,
170
- voiceAttachmentUrls,
171
- voiceAsrReferTexts,
172
- voiceTranscripts,
173
- voiceTranscriptSources,
174
- attachmentLocalPaths,
175
- };
176
- }
177
- /**
178
- * 将语音转录结果组装为用户消息中的文本片段。
179
- */
180
- export function formatVoiceText(transcripts) {
181
- if (transcripts.length === 0)
182
- return "";
183
- return transcripts.length === 1
184
- ? `[语音消息] ${transcripts[0]}`
185
- : transcripts.map((t, i) => `[语音${i + 1}] ${t}`).join("\n");
186
- }
187
- async function processVoiceAttachment(localPath, audioPath, att, asrReferText, cfg, downloadDir, log, prefix) {
188
- const wavUrl = att.voice_wav_url
189
- ? (att.voice_wav_url.startsWith("//") ? `https:${att.voice_wav_url}` : att.voice_wav_url)
190
- : "";
191
- const attUrl = att.url?.startsWith("//") ? `https:${att.url}` : att.url;
192
- const voiceSourceUrl = wavUrl || attUrl;
193
- const meta = {
194
- voiceUrl: voiceSourceUrl || undefined,
195
- asrReferText: asrReferText || undefined,
196
- };
197
- const sttCfg = resolveSTTConfig(cfg);
198
- if (!sttCfg) {
199
- if (asrReferText) {
200
- log?.info(`${prefix} Voice attachment: ${att.filename} (STT not configured, using asr_refer_text fallback)`);
201
- return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
202
- }
203
- log?.info(`${prefix} Voice attachment: ${att.filename} (STT not configured, skipping transcription)`);
204
- return { localPath, type: "voice", transcript: "[语音消息 - 语音识别未配置,无法转录]", transcriptSource: "fallback", meta };
205
- }
206
- // SILK→WAV 转换
207
- if (!audioPath) {
208
- log?.info(`${prefix} Voice attachment: ${att.filename}, converting SILK→WAV...`);
209
- try {
210
- const wavResult = await convertSilkToWav(localPath, downloadDir);
211
- if (wavResult) {
212
- audioPath = wavResult.wavPath;
213
- log?.info(`${prefix} Voice converted: ${wavResult.wavPath} (${formatDuration(wavResult.duration)})`);
214
- }
215
- else {
216
- audioPath = localPath;
217
- }
218
- }
219
- catch (convertErr) {
220
- log?.error(`${prefix} Voice conversion failed: ${convertErr}`);
221
- if (asrReferText) {
222
- return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
223
- }
224
- return { localPath, type: "voice", transcript: "[语音消息 - 格式转换失败]", transcriptSource: "fallback", meta };
225
- }
226
- }
227
- // STT 转录
228
- try {
229
- const transcript = await transcribeAudio(audioPath, cfg);
230
- if (transcript) {
231
- log?.info(`${prefix} STT transcript: ${transcript.slice(0, 100)}...`);
232
- return { localPath, type: "voice", transcript, transcriptSource: "stt", meta };
233
- }
234
- if (asrReferText) {
235
- log?.info(`${prefix} STT returned empty result, using asr_refer_text fallback`);
236
- return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
237
- }
238
- log?.info(`${prefix} STT returned empty result`);
239
- return { localPath, type: "voice", transcript: "[语音消息 - 转录结果为空]", transcriptSource: "fallback", meta };
240
- }
241
- catch (sttErr) {
242
- log?.error(`${prefix} STT failed: ${sttErr}`);
243
- if (asrReferText) {
244
- return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
245
- }
246
- return { localPath, type: "voice", transcript: "[语音消息 - 转录失败]", transcriptSource: "fallback", meta };
247
- }
248
- }
@@ -1,100 +0,0 @@
1
- /**
2
- * 已知用户存储
3
- * 记录与机器人交互过的所有用户
4
- * 支持主动消息和批量通知功能
5
- */
6
- export interface KnownUser {
7
- /** 用户 openid(唯一标识) */
8
- openid: string;
9
- /** 消息类型:私聊用户 / 群组 */
10
- type: "c2c" | "group";
11
- /** 用户昵称(如有) */
12
- nickname?: string;
13
- /** 群组 openid(如果是群消息) */
14
- groupOpenid?: string;
15
- /** 关联的机器人账户 ID */
16
- accountId: string;
17
- /** 首次交互时间戳 */
18
- firstSeenAt: number;
19
- /** 最后交互时间戳 */
20
- lastSeenAt: number;
21
- /** 交互次数 */
22
- interactionCount: number;
23
- }
24
- /**
25
- * 强制立即保存(用于进程退出前)
26
- */
27
- export declare function flushKnownUsers(): void;
28
- /**
29
- * 记录已知用户(收到消息时调用)
30
- * @param user 用户信息(部分字段)
31
- */
32
- export declare function recordKnownUser(user: {
33
- openid: string;
34
- type: "c2c" | "group";
35
- nickname?: string;
36
- groupOpenid?: string;
37
- accountId: string;
38
- }): void;
39
- /**
40
- * 获取单个用户信息
41
- * @param accountId 机器人账户 ID
42
- * @param openid 用户 openid
43
- * @param type 消息类型
44
- * @param groupOpenid 群组 openid(可选)
45
- */
46
- export declare function getKnownUser(accountId: string, openid: string, type?: "c2c" | "group", groupOpenid?: string): KnownUser | undefined;
47
- /**
48
- * 列出所有已知用户
49
- * @param options 筛选选项
50
- */
51
- export declare function listKnownUsers(options?: {
52
- /** 筛选特定机器人账户的用户 */
53
- accountId?: string;
54
- /** 筛选消息类型 */
55
- type?: "c2c" | "group";
56
- /** 最近活跃时间(毫秒,如 86400000 表示最近 24 小时) */
57
- activeWithin?: number;
58
- /** 返回数量限制 */
59
- limit?: number;
60
- /** 排序方式 */
61
- sortBy?: "lastSeenAt" | "firstSeenAt" | "interactionCount";
62
- /** 排序方向 */
63
- sortOrder?: "asc" | "desc";
64
- }): KnownUser[];
65
- /**
66
- * 获取用户统计信息
67
- * @param accountId 机器人账户 ID(可选,不传则返回所有账户的统计)
68
- */
69
- export declare function getKnownUsersStats(accountId?: string): {
70
- totalUsers: number;
71
- c2cUsers: number;
72
- groupUsers: number;
73
- activeIn24h: number;
74
- activeIn7d: number;
75
- };
76
- /**
77
- * 删除用户记录
78
- * @param accountId 机器人账户 ID
79
- * @param openid 用户 openid
80
- * @param type 消息类型
81
- * @param groupOpenid 群组 openid(可选)
82
- */
83
- export declare function removeKnownUser(accountId: string, openid: string, type?: "c2c" | "group", groupOpenid?: string): boolean;
84
- /**
85
- * 清除所有用户记录
86
- * @param accountId 机器人账户 ID(可选,不传则清除所有)
87
- */
88
- export declare function clearKnownUsers(accountId?: string): number;
89
- /**
90
- * 获取用户的所有群组(某用户在哪些群里交互过)
91
- * @param accountId 机器人账户 ID
92
- * @param openid 用户 openid
93
- */
94
- export declare function getUserGroups(accountId: string, openid: string): string[];
95
- /**
96
- * 获取群组的所有成员
97
- * @param accountId 机器人账户 ID
98
- * @param groupOpenid 群组 openid
99
- */
100
- export declare function getGroupMembers(accountId: string, groupOpenid: string): KnownUser[];
@@ -1,263 +0,0 @@
1
- /**
2
- * 已知用户存储
3
- * 记录与机器人交互过的所有用户
4
- * 支持主动消息和批量通知功能
5
- */
6
- import fs from "node:fs";
7
- import path from "node:path";
8
- import { getQQBotDataDir } from "./utils/platform.js";
9
- // 存储文件路径
10
- const KNOWN_USERS_DIR = getQQBotDataDir("data");
11
- const KNOWN_USERS_FILE = path.join(KNOWN_USERS_DIR, "known-users.json");
12
- // 内存缓存
13
- let usersCache = null;
14
- // 写入节流配置
15
- const SAVE_THROTTLE_MS = 5000; // 5秒写入一次
16
- let saveTimer = null;
17
- let isDirty = false;
18
- /**
19
- * 确保目录存在
20
- */
21
- function ensureDir() {
22
- if (!fs.existsSync(KNOWN_USERS_DIR)) {
23
- fs.mkdirSync(KNOWN_USERS_DIR, { recursive: true });
24
- }
25
- }
26
- /**
27
- * 从文件加载用户数据到缓存
28
- */
29
- function loadUsersFromFile() {
30
- if (usersCache !== null) {
31
- return usersCache;
32
- }
33
- usersCache = new Map();
34
- try {
35
- if (fs.existsSync(KNOWN_USERS_FILE)) {
36
- const data = fs.readFileSync(KNOWN_USERS_FILE, "utf-8");
37
- const users = JSON.parse(data);
38
- for (const user of users) {
39
- // 使用复合键:accountId + type + openid(群组还要加 groupOpenid)
40
- const key = makeUserKey(user);
41
- usersCache.set(key, user);
42
- }
43
- console.log(`[known-users] Loaded ${usersCache.size} users`);
44
- }
45
- }
46
- catch (err) {
47
- console.error(`[known-users] Failed to load users: ${err}`);
48
- usersCache = new Map();
49
- }
50
- return usersCache;
51
- }
52
- /**
53
- * 保存用户数据到文件(节流版本)
54
- */
55
- function saveUsersToFile() {
56
- if (!isDirty)
57
- return;
58
- if (saveTimer) {
59
- return; // 已有定时器在等待
60
- }
61
- saveTimer = setTimeout(() => {
62
- saveTimer = null;
63
- doSaveUsersToFile();
64
- }, SAVE_THROTTLE_MS);
65
- }
66
- /**
67
- * 实际执行保存
68
- */
69
- function doSaveUsersToFile() {
70
- if (!usersCache || !isDirty)
71
- return;
72
- try {
73
- ensureDir();
74
- const users = Array.from(usersCache.values());
75
- fs.writeFileSync(KNOWN_USERS_FILE, JSON.stringify(users, null, 2), "utf-8");
76
- isDirty = false;
77
- }
78
- catch (err) {
79
- console.error(`[known-users] Failed to save users: ${err}`);
80
- }
81
- }
82
- /**
83
- * 强制立即保存(用于进程退出前)
84
- */
85
- export function flushKnownUsers() {
86
- if (saveTimer) {
87
- clearTimeout(saveTimer);
88
- saveTimer = null;
89
- }
90
- doSaveUsersToFile();
91
- }
92
- /**
93
- * 生成用户唯一键
94
- */
95
- function makeUserKey(user) {
96
- const base = `${user.accountId}:${user.type}:${user.openid}`;
97
- if (user.type === "group" && user.groupOpenid) {
98
- return `${base}:${user.groupOpenid}`;
99
- }
100
- return base;
101
- }
102
- /**
103
- * 记录已知用户(收到消息时调用)
104
- * @param user 用户信息(部分字段)
105
- */
106
- export function recordKnownUser(user) {
107
- const cache = loadUsersFromFile();
108
- const key = makeUserKey(user);
109
- const now = Date.now();
110
- const existing = cache.get(key);
111
- if (existing) {
112
- // 更新已存在的用户
113
- existing.lastSeenAt = now;
114
- existing.interactionCount++;
115
- if (user.nickname && user.nickname !== existing.nickname) {
116
- existing.nickname = user.nickname;
117
- }
118
- }
119
- else {
120
- // 新用户
121
- const newUser = {
122
- openid: user.openid,
123
- type: user.type,
124
- nickname: user.nickname,
125
- groupOpenid: user.groupOpenid,
126
- accountId: user.accountId,
127
- firstSeenAt: now,
128
- lastSeenAt: now,
129
- interactionCount: 1,
130
- };
131
- cache.set(key, newUser);
132
- console.log(`[known-users] New user: ${user.openid} (${user.type})`);
133
- }
134
- isDirty = true;
135
- saveUsersToFile();
136
- }
137
- /**
138
- * 获取单个用户信息
139
- * @param accountId 机器人账户 ID
140
- * @param openid 用户 openid
141
- * @param type 消息类型
142
- * @param groupOpenid 群组 openid(可选)
143
- */
144
- export function getKnownUser(accountId, openid, type = "c2c", groupOpenid) {
145
- const cache = loadUsersFromFile();
146
- const key = makeUserKey({ accountId, openid, type, groupOpenid });
147
- return cache.get(key);
148
- }
149
- /**
150
- * 列出所有已知用户
151
- * @param options 筛选选项
152
- */
153
- export function listKnownUsers(options) {
154
- const cache = loadUsersFromFile();
155
- let users = Array.from(cache.values());
156
- // 筛选
157
- if (options?.accountId) {
158
- users = users.filter(u => u.accountId === options.accountId);
159
- }
160
- if (options?.type) {
161
- users = users.filter(u => u.type === options.type);
162
- }
163
- if (options?.activeWithin) {
164
- const cutoff = Date.now() - options.activeWithin;
165
- users = users.filter(u => u.lastSeenAt >= cutoff);
166
- }
167
- // 排序
168
- const sortBy = options?.sortBy ?? "lastSeenAt";
169
- const sortOrder = options?.sortOrder ?? "desc";
170
- users.sort((a, b) => {
171
- const aVal = a[sortBy] ?? 0;
172
- const bVal = b[sortBy] ?? 0;
173
- return sortOrder === "asc" ? aVal - bVal : bVal - aVal;
174
- });
175
- // 限制数量
176
- if (options?.limit && options.limit > 0) {
177
- users = users.slice(0, options.limit);
178
- }
179
- return users;
180
- }
181
- /**
182
- * 获取用户统计信息
183
- * @param accountId 机器人账户 ID(可选,不传则返回所有账户的统计)
184
- */
185
- export function getKnownUsersStats(accountId) {
186
- let users = listKnownUsers({ accountId });
187
- const now = Date.now();
188
- const day = 24 * 60 * 60 * 1000;
189
- return {
190
- totalUsers: users.length,
191
- c2cUsers: users.filter(u => u.type === "c2c").length,
192
- groupUsers: users.filter(u => u.type === "group").length,
193
- activeIn24h: users.filter(u => now - u.lastSeenAt < day).length,
194
- activeIn7d: users.filter(u => now - u.lastSeenAt < 7 * day).length,
195
- };
196
- }
197
- /**
198
- * 删除用户记录
199
- * @param accountId 机器人账户 ID
200
- * @param openid 用户 openid
201
- * @param type 消息类型
202
- * @param groupOpenid 群组 openid(可选)
203
- */
204
- export function removeKnownUser(accountId, openid, type = "c2c", groupOpenid) {
205
- const cache = loadUsersFromFile();
206
- const key = makeUserKey({ accountId, openid, type, groupOpenid });
207
- if (cache.has(key)) {
208
- cache.delete(key);
209
- isDirty = true;
210
- saveUsersToFile();
211
- console.log(`[known-users] Removed user ${openid}`);
212
- return true;
213
- }
214
- return false;
215
- }
216
- /**
217
- * 清除所有用户记录
218
- * @param accountId 机器人账户 ID(可选,不传则清除所有)
219
- */
220
- export function clearKnownUsers(accountId) {
221
- const cache = loadUsersFromFile();
222
- let count = 0;
223
- if (accountId) {
224
- // 只清除指定账户的用户
225
- for (const [key, user] of cache.entries()) {
226
- if (user.accountId === accountId) {
227
- cache.delete(key);
228
- count++;
229
- }
230
- }
231
- }
232
- else {
233
- // 清除所有
234
- count = cache.size;
235
- cache.clear();
236
- }
237
- if (count > 0) {
238
- isDirty = true;
239
- doSaveUsersToFile(); // 立即保存
240
- console.log(`[known-users] Cleared ${count} users`);
241
- }
242
- return count;
243
- }
244
- /**
245
- * 获取用户的所有群组(某用户在哪些群里交互过)
246
- * @param accountId 机器人账户 ID
247
- * @param openid 用户 openid
248
- */
249
- export function getUserGroups(accountId, openid) {
250
- const users = listKnownUsers({ accountId, type: "group" });
251
- return users
252
- .filter(u => u.openid === openid && u.groupOpenid)
253
- .map(u => u.groupOpenid);
254
- }
255
- /**
256
- * 获取群组的所有成员
257
- * @param accountId 机器人账户 ID
258
- * @param groupOpenid 群组 openid
259
- */
260
- export function getGroupMembers(accountId, groupOpenid) {
261
- return listKnownUsers({ accountId, type: "group" })
262
- .filter(u => u.groupOpenid === groupOpenid);
263
- }
@@ -1,53 +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
- export type MentionGateResult = {
13
- effectiveWasMentioned: boolean;
14
- shouldSkip: boolean;
15
- };
16
- export type MentionGateWithBypassResult = MentionGateResult & {
17
- shouldBypassMention: boolean;
18
- };
19
- export type GroupMessageGateAction =
20
- /** @了其他人但未 @bot,丢弃并记录历史 */
21
- "drop_other_mention"
22
- /** 未授权控制命令,静默拦截 */
23
- | "block_unauthorized_command"
24
- /** 非 @bot 消息,记录历史后跳过 AI */
25
- | "skip_no_mention"
26
- /** 正常放行,交给 AI */
27
- | "pass";
28
- export type GroupMessageGateResult = {
29
- action: GroupMessageGateAction;
30
- /** 仅 action=pass|skip_no_mention 时有值 */
31
- effectiveWasMentioned: boolean;
32
- shouldBypassMention: boolean;
33
- };
34
- export type GroupMessageGateParams = {
35
- ignoreOtherMentions: boolean;
36
- hasAnyMention: boolean;
37
- wasMentioned: boolean;
38
- implicitMention: boolean;
39
- allowTextCommands: boolean;
40
- isControlCommand: boolean;
41
- commandAuthorized: boolean;
42
- requireMention: boolean;
43
- canDetectMention: boolean;
44
- };
45
- /**
46
- * 群消息统一门控,按优先级串行判定:
47
- *
48
- * 1. ignoreOtherMentions — @了其他人但未 @bot → drop_other_mention
49
- * 2. shouldBlock — 未授权控制命令 → block_unauthorized_command
50
- * 3. mentionGating — 未满足 @bot 条件 → skip_no_mention
51
- * 4. 通过所有检查 → pass
52
- */
53
- export declare function resolveGroupMessageGate(params: GroupMessageGateParams): GroupMessageGateResult;