@leoqlin/openclaw-qqbot 1.6.7-alpha1

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 (218) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +484 -0
  3. package/README.zh.md +479 -0
  4. package/bin/qqbot-cli.js +243 -0
  5. package/dist/index.d.ts +17 -0
  6. package/dist/index.js +26 -0
  7. package/dist/src/admin-resolver.d.ts +33 -0
  8. package/dist/src/admin-resolver.js +157 -0
  9. package/dist/src/api.d.ts +301 -0
  10. package/dist/src/api.js +890 -0
  11. package/dist/src/channel.d.ts +29 -0
  12. package/dist/src/channel.js +452 -0
  13. package/dist/src/config.d.ts +56 -0
  14. package/dist/src/config.js +278 -0
  15. package/dist/src/credential-backup.d.ts +31 -0
  16. package/dist/src/credential-backup.js +66 -0
  17. package/dist/src/deliver-debounce.d.ts +74 -0
  18. package/dist/src/deliver-debounce.js +174 -0
  19. package/dist/src/gateway.d.ts +18 -0
  20. package/dist/src/gateway.js +2005 -0
  21. package/dist/src/group-history.d.ts +136 -0
  22. package/dist/src/group-history.js +226 -0
  23. package/dist/src/image-server.d.ts +87 -0
  24. package/dist/src/image-server.js +570 -0
  25. package/dist/src/inbound-attachments.d.ts +60 -0
  26. package/dist/src/inbound-attachments.js +248 -0
  27. package/dist/src/known-users.d.ts +100 -0
  28. package/dist/src/known-users.js +263 -0
  29. package/dist/src/message-gating.d.ts +53 -0
  30. package/dist/src/message-gating.js +107 -0
  31. package/dist/src/message-queue.d.ts +89 -0
  32. package/dist/src/message-queue.js +257 -0
  33. package/dist/src/onboarding.d.ts +10 -0
  34. package/dist/src/onboarding.js +203 -0
  35. package/dist/src/outbound-deliver.d.ts +48 -0
  36. package/dist/src/outbound-deliver.js +392 -0
  37. package/dist/src/outbound.d.ts +205 -0
  38. package/dist/src/outbound.js +938 -0
  39. package/dist/src/proactive.d.ts +170 -0
  40. package/dist/src/proactive.js +399 -0
  41. package/dist/src/ref-index-store.d.ts +101 -0
  42. package/dist/src/ref-index-store.js +298 -0
  43. package/dist/src/reply-dispatcher.d.ts +35 -0
  44. package/dist/src/reply-dispatcher.js +311 -0
  45. package/dist/src/request-context.d.ts +25 -0
  46. package/dist/src/request-context.js +37 -0
  47. package/dist/src/runtime.d.ts +3 -0
  48. package/dist/src/runtime.js +10 -0
  49. package/dist/src/session-store.d.ts +52 -0
  50. package/dist/src/session-store.js +254 -0
  51. package/dist/src/slash-commands.d.ts +77 -0
  52. package/dist/src/slash-commands.js +1866 -0
  53. package/dist/src/startup-greeting.d.ts +30 -0
  54. package/dist/src/startup-greeting.js +97 -0
  55. package/dist/src/streaming.d.ts +247 -0
  56. package/dist/src/streaming.js +899 -0
  57. package/dist/src/stt.d.ts +21 -0
  58. package/dist/src/stt.js +70 -0
  59. package/dist/src/tools/channel.d.ts +16 -0
  60. package/dist/src/tools/channel.js +234 -0
  61. package/dist/src/tools/remind.d.ts +2 -0
  62. package/dist/src/tools/remind.js +256 -0
  63. package/dist/src/types.d.ts +367 -0
  64. package/dist/src/types.js +17 -0
  65. package/dist/src/typing-keepalive.d.ts +27 -0
  66. package/dist/src/typing-keepalive.js +64 -0
  67. package/dist/src/update-checker.d.ts +36 -0
  68. package/dist/src/update-checker.js +171 -0
  69. package/dist/src/utils/audio-convert.d.ts +98 -0
  70. package/dist/src/utils/audio-convert.js +755 -0
  71. package/dist/src/utils/chunked-upload.d.ts +68 -0
  72. package/dist/src/utils/chunked-upload.js +341 -0
  73. package/dist/src/utils/file-utils.d.ts +61 -0
  74. package/dist/src/utils/file-utils.js +172 -0
  75. package/dist/src/utils/image-size.d.ts +51 -0
  76. package/dist/src/utils/image-size.js +234 -0
  77. package/dist/src/utils/media-send.d.ts +158 -0
  78. package/dist/src/utils/media-send.js +499 -0
  79. package/dist/src/utils/media-tags.d.ts +14 -0
  80. package/dist/src/utils/media-tags.js +165 -0
  81. package/dist/src/utils/payload.d.ts +112 -0
  82. package/dist/src/utils/payload.js +186 -0
  83. package/dist/src/utils/pkg-version.d.ts +5 -0
  84. package/dist/src/utils/pkg-version.js +61 -0
  85. package/dist/src/utils/platform.d.ts +137 -0
  86. package/dist/src/utils/platform.js +390 -0
  87. package/dist/src/utils/ssrf-guard.d.ts +25 -0
  88. package/dist/src/utils/ssrf-guard.js +91 -0
  89. package/dist/src/utils/text-parsing.d.ts +36 -0
  90. package/dist/src/utils/text-parsing.js +75 -0
  91. package/dist/src/utils/upload-cache.d.ts +34 -0
  92. package/dist/src/utils/upload-cache.js +93 -0
  93. package/index.ts +31 -0
  94. package/node_modules/@eshaz/web-worker/LICENSE +201 -0
  95. package/node_modules/@eshaz/web-worker/README.md +134 -0
  96. package/node_modules/@eshaz/web-worker/browser.js +17 -0
  97. package/node_modules/@eshaz/web-worker/cjs/browser.js +16 -0
  98. package/node_modules/@eshaz/web-worker/cjs/node.js +219 -0
  99. package/node_modules/@eshaz/web-worker/index.d.ts +4 -0
  100. package/node_modules/@eshaz/web-worker/node.js +223 -0
  101. package/node_modules/@eshaz/web-worker/package.json +54 -0
  102. package/node_modules/@wasm-audio-decoders/common/index.js +5 -0
  103. package/node_modules/@wasm-audio-decoders/common/package.json +36 -0
  104. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +231 -0
  105. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +129 -0
  106. package/node_modules/@wasm-audio-decoders/common/src/puff/README +67 -0
  107. package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +31 -0
  108. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +863 -0
  109. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +35 -0
  110. package/node_modules/@wasm-audio-decoders/common/src/utilities.js +3 -0
  111. package/node_modules/@wasm-audio-decoders/common/types.d.ts +7 -0
  112. package/node_modules/mpg123-decoder/README.md +265 -0
  113. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +185 -0
  114. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +1 -0
  115. package/node_modules/mpg123-decoder/index.js +8 -0
  116. package/node_modules/mpg123-decoder/package.json +58 -0
  117. package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +464 -0
  118. package/node_modules/mpg123-decoder/src/MPEGDecoder.js +200 -0
  119. package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +21 -0
  120. package/node_modules/mpg123-decoder/types.d.ts +30 -0
  121. package/node_modules/silk-wasm/LICENSE +21 -0
  122. package/node_modules/silk-wasm/README.md +85 -0
  123. package/node_modules/silk-wasm/lib/index.cjs +16 -0
  124. package/node_modules/silk-wasm/lib/index.d.ts +70 -0
  125. package/node_modules/silk-wasm/lib/index.mjs +16 -0
  126. package/node_modules/silk-wasm/lib/silk.wasm +0 -0
  127. package/node_modules/silk-wasm/lib/utils.d.ts +4 -0
  128. package/node_modules/silk-wasm/package.json +39 -0
  129. package/node_modules/simple-yenc/.github/FUNDING.yml +1 -0
  130. package/node_modules/simple-yenc/.prettierignore +1 -0
  131. package/node_modules/simple-yenc/LICENSE +7 -0
  132. package/node_modules/simple-yenc/README.md +163 -0
  133. package/node_modules/simple-yenc/dist/esm.js +1 -0
  134. package/node_modules/simple-yenc/dist/index.js +1 -0
  135. package/node_modules/simple-yenc/package.json +50 -0
  136. package/node_modules/simple-yenc/rollup.config.js +27 -0
  137. package/node_modules/simple-yenc/src/simple-yenc.js +302 -0
  138. package/node_modules/ws/LICENSE +20 -0
  139. package/node_modules/ws/README.md +548 -0
  140. package/node_modules/ws/browser.js +8 -0
  141. package/node_modules/ws/index.js +22 -0
  142. package/node_modules/ws/lib/buffer-util.js +131 -0
  143. package/node_modules/ws/lib/constants.js +19 -0
  144. package/node_modules/ws/lib/event-target.js +292 -0
  145. package/node_modules/ws/lib/extension.js +203 -0
  146. package/node_modules/ws/lib/limiter.js +55 -0
  147. package/node_modules/ws/lib/permessage-deflate.js +528 -0
  148. package/node_modules/ws/lib/receiver.js +706 -0
  149. package/node_modules/ws/lib/sender.js +602 -0
  150. package/node_modules/ws/lib/stream.js +161 -0
  151. package/node_modules/ws/lib/subprotocol.js +62 -0
  152. package/node_modules/ws/lib/validation.js +152 -0
  153. package/node_modules/ws/lib/websocket-server.js +554 -0
  154. package/node_modules/ws/lib/websocket.js +1393 -0
  155. package/node_modules/ws/package.json +70 -0
  156. package/node_modules/ws/wrapper.mjs +21 -0
  157. package/openclaw.plugin.json +17 -0
  158. package/package.json +70 -0
  159. package/preload.cjs +33 -0
  160. package/scripts/cleanup-legacy-plugins.sh +124 -0
  161. package/scripts/link-sdk-core.cjs +185 -0
  162. package/scripts/postinstall-link-sdk.js +126 -0
  163. package/scripts/proactive-api-server.ts +369 -0
  164. package/scripts/send-proactive.ts +293 -0
  165. package/scripts/set-markdown.sh +156 -0
  166. package/scripts/test-sendmedia.ts +116 -0
  167. package/scripts/upgrade-via-npm.ps1 +460 -0
  168. package/scripts/upgrade-via-npm.sh +652 -0
  169. package/scripts/upgrade-via-source.sh +1026 -0
  170. package/skills/qqbot-channel/SKILL.md +263 -0
  171. package/skills/qqbot-channel/references/api_references.md +521 -0
  172. package/skills/qqbot-media/SKILL.md +60 -0
  173. package/skills/qqbot-remind/SKILL.md +159 -0
  174. package/src/admin-resolver.ts +181 -0
  175. package/src/api.ts +1284 -0
  176. package/src/channel.ts +477 -0
  177. package/src/config.ts +347 -0
  178. package/src/credential-backup.ts +72 -0
  179. package/src/deliver-debounce.ts +229 -0
  180. package/src/gateway.ts +2245 -0
  181. package/src/group-history.ts +328 -0
  182. package/src/image-server.ts +675 -0
  183. package/src/inbound-attachments.ts +321 -0
  184. package/src/known-users.ts +353 -0
  185. package/src/message-gating.ts +190 -0
  186. package/src/message-queue.ts +352 -0
  187. package/src/onboarding.ts +274 -0
  188. package/src/openclaw-plugin-sdk.d.ts +587 -0
  189. package/src/outbound-deliver.ts +473 -0
  190. package/src/outbound.ts +1131 -0
  191. package/src/proactive.ts +530 -0
  192. package/src/ref-index-store.ts +412 -0
  193. package/src/reply-dispatcher.ts +334 -0
  194. package/src/request-context.ts +49 -0
  195. package/src/runtime.ts +14 -0
  196. package/src/session-store.ts +303 -0
  197. package/src/slash-commands.ts +2030 -0
  198. package/src/startup-greeting.ts +120 -0
  199. package/src/streaming.ts +1077 -0
  200. package/src/stt.ts +86 -0
  201. package/src/tools/channel.ts +281 -0
  202. package/src/tools/remind.ts +308 -0
  203. package/src/types.ts +391 -0
  204. package/src/typing-keepalive.ts +59 -0
  205. package/src/update-checker.ts +186 -0
  206. package/src/utils/audio-convert.ts +859 -0
  207. package/src/utils/chunked-upload.ts +483 -0
  208. package/src/utils/file-utils.ts +193 -0
  209. package/src/utils/image-size.ts +266 -0
  210. package/src/utils/media-send.ts +631 -0
  211. package/src/utils/media-tags.ts +183 -0
  212. package/src/utils/payload.ts +265 -0
  213. package/src/utils/pkg-version.ts +64 -0
  214. package/src/utils/platform.ts +435 -0
  215. package/src/utils/ssrf-guard.ts +102 -0
  216. package/src/utils/text-parsing.ts +85 -0
  217. package/src/utils/upload-cache.ts +128 -0
  218. package/tsconfig.json +16 -0
@@ -0,0 +1,248 @@
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
+ }
@@ -0,0 +1,100 @@
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[];
@@ -0,0 +1,263 @@
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
+ }
@@ -0,0 +1,53 @@
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;