@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,321 +0,0 @@
1
- /**
2
- * 入站附件处理模块
3
- *
4
- * 负责下载、转换、转录用户发送的附件(图片/语音/文件),
5
- * 并归类为统一的 ProcessedAttachments 结构供 gateway 消费。
6
- */
7
-
8
- import { downloadFile } from "./image-server.js";
9
- import { convertSilkToWav, isVoiceAttachment, formatDuration } from "./utils/audio-convert.js";
10
- import { transcribeAudio, resolveSTTConfig } from "./stt.js";
11
- import { getQQBotMediaDir } from "./utils/platform.js";
12
-
13
- // ============ 类型定义 ============
14
-
15
- export interface RawAttachment {
16
- content_type: string;
17
- url: string;
18
- filename?: string;
19
- voice_wav_url?: string;
20
- asr_refer_text?: string;
21
- }
22
-
23
- export type TranscriptSource = "stt" | "asr" | "fallback";
24
-
25
- /** processAttachments 的返回值 */
26
- export interface ProcessedAttachments {
27
- /** 附件描述文本(其它类型附件) */
28
- attachmentInfo: string;
29
- /** 图片本地路径或远程 URL */
30
- imageUrls: string[];
31
- /** 图片 MIME 类型(与 imageUrls 一一对应) */
32
- imageMediaTypes: string[];
33
- /** 语音本地路径 */
34
- voiceAttachmentPaths: string[];
35
- /** 语音远程 URL */
36
- voiceAttachmentUrls: string[];
37
- /** QQ ASR 原始识别文本 */
38
- voiceAsrReferTexts: string[];
39
- /** 语音转录文本 */
40
- voiceTranscripts: string[];
41
- /** 转录来源 */
42
- voiceTranscriptSources: TranscriptSource[];
43
- /** 每个附件的本地路径(与原始 attachments 数组一一对应,未下载的为 null) */
44
- attachmentLocalPaths: Array<string | null>;
45
- }
46
-
47
- interface ProcessContext {
48
- appId: string;
49
- /** 对话 ID:群聊传 groupOpenid,私聊传 senderId(用于按群/用户隔离下载目录) */
50
- peerId?: string;
51
- cfg: unknown;
52
- log?: {
53
- info: (msg: string) => void;
54
- error: (msg: string) => void;
55
- debug?: (msg: string) => void;
56
- };
57
- }
58
-
59
- // ============ 空结果常量 ============
60
-
61
- const EMPTY_RESULT: ProcessedAttachments = {
62
- attachmentInfo: "",
63
- imageUrls: [],
64
- imageMediaTypes: [],
65
- voiceAttachmentPaths: [],
66
- voiceAttachmentUrls: [],
67
- voiceAsrReferTexts: [],
68
- voiceTranscripts: [],
69
- voiceTranscriptSources: [],
70
- attachmentLocalPaths: [],
71
- };
72
-
73
- // ============ 主函数 ============
74
-
75
- /**
76
- * 处理入站消息的附件列表。
77
- *
78
- * 三阶段流水线:
79
- * 1. 并行下载所有附件到本地
80
- * 2. 并行处理语音转换 + STT 转录
81
- * 3. 按原始顺序归类结果
82
- */
83
- export async function processAttachments(
84
- attachments: RawAttachment[] | undefined,
85
- ctx: ProcessContext,
86
- ): Promise<ProcessedAttachments> {
87
- if (!attachments?.length) return EMPTY_RESULT;
88
-
89
- const { appId, peerId, cfg, log } = ctx;
90
- const subPaths = ["downloads", appId, ...(peerId ? [peerId] : [])];
91
- const downloadDir = getQQBotMediaDir(...subPaths);
92
- const prefix = `[qqbot:${appId}]`;
93
-
94
- // 结果收集
95
- const imageUrls: string[] = [];
96
- const imageMediaTypes: string[] = [];
97
- const voiceAttachmentPaths: string[] = [];
98
- const voiceAttachmentUrls: string[] = [];
99
- const voiceAsrReferTexts: string[] = [];
100
- const voiceTranscripts: string[] = [];
101
- const voiceTranscriptSources: TranscriptSource[] = [];
102
- const attachmentLocalPaths: Array<string | null> = [];
103
- const otherAttachments: string[] = [];
104
-
105
- // 入站附件下载:限制 2 分钟,不限大小
106
- const INBOUND_DOWNLOAD_TIMEOUT_MS = 2 * 60 * 1000; // 2 分钟
107
-
108
- // Phase 1: 并行下载所有附件
109
- const downloadTasks = attachments.map(async (att) => {
110
- const attUrl = att.url?.startsWith("//") ? `https:${att.url}` : att.url;
111
- const isVoice = isVoiceAttachment(att);
112
- const wavUrl = isVoice && att.voice_wav_url
113
- ? (att.voice_wav_url.startsWith("//") ? `https:${att.voice_wav_url}` : att.voice_wav_url)
114
- : "";
115
-
116
- let localPath: string | null = null;
117
- let audioPath: string | null = null;
118
- let dlError: string | undefined;
119
-
120
- if (isVoice && wavUrl) {
121
- const wavResult = await downloadFile(wavUrl, undefined, { destDir: downloadDir, timeoutMs: INBOUND_DOWNLOAD_TIMEOUT_MS });
122
- if (wavResult.filePath) {
123
- localPath = wavResult.filePath;
124
- audioPath = wavResult.filePath;
125
- log?.info(`${prefix} Voice attachment: ${att.filename}, downloaded WAV directly (skip SILK→WAV)`);
126
- } else {
127
- log?.error(`${prefix} Failed to download voice_wav_url (${wavResult.error}), falling back to original URL`);
128
- }
129
- }
130
-
131
- if (!localPath) {
132
- const dlResult = await downloadFile(attUrl, att.filename, { destDir: downloadDir, timeoutMs: INBOUND_DOWNLOAD_TIMEOUT_MS });
133
- localPath = dlResult.filePath;
134
- dlError = dlResult.error;
135
- }
136
-
137
- return { att, attUrl, isVoice, localPath, audioPath, dlError };
138
- });
139
-
140
- const downloadResults = await Promise.all(downloadTasks);
141
-
142
- // Phase 2: 并行处理语音转换 + 转录(非语音附件同步归类)
143
- const processTasks = downloadResults.map(async ({ att, attUrl, isVoice, localPath, audioPath, dlError }) => {
144
- const asrReferText = typeof att.asr_refer_text === "string" ? att.asr_refer_text.trim() : "";
145
- const wavUrl = isVoice && att.voice_wav_url
146
- ? (att.voice_wav_url.startsWith("//") ? `https:${att.voice_wav_url}` : att.voice_wav_url)
147
- : "";
148
- const voiceSourceUrl = wavUrl || attUrl;
149
-
150
- const meta = {
151
- voiceUrl: isVoice && voiceSourceUrl ? voiceSourceUrl : undefined,
152
- asrReferText: isVoice && asrReferText ? asrReferText : undefined,
153
- };
154
-
155
- if (localPath) {
156
- if (att.content_type?.startsWith("image/")) {
157
- log?.info(`${prefix} Downloaded attachment to: ${localPath}`);
158
- return { localPath, type: "image" as const, contentType: att.content_type, meta };
159
- } else if (isVoice) {
160
- log?.info(`${prefix} Downloaded attachment to: ${localPath}`);
161
- return processVoiceAttachment(localPath, audioPath, att, asrReferText, cfg, downloadDir, log, prefix);
162
- } else {
163
- log?.info(`${prefix} Downloaded attachment to: ${localPath}`);
164
- return { localPath, type: "other" as const, filename: att.filename, meta };
165
- }
166
- } else {
167
- log?.error(`${prefix} Failed to download: ${attUrl}`);
168
- if (att.content_type?.startsWith("image/")) {
169
- return { localPath: null, type: "image-fallback" as const, attUrl, contentType: att.content_type, dlError, meta };
170
- } else if (isVoice && asrReferText) {
171
- log?.info(`${prefix} Voice attachment download failed, using asr_refer_text fallback`);
172
- return { localPath: null, type: "voice-fallback" as const, transcript: asrReferText, meta };
173
- } else {
174
- return { localPath: null, type: "other-fallback" as const, filename: att.filename ?? att.content_type, dlError, meta };
175
- }
176
- }
177
- });
178
-
179
- const processResults = await Promise.all(processTasks);
180
-
181
- // Phase 3: 按原始顺序归类结果
182
- for (const result of processResults) {
183
- if (result.meta.voiceUrl) voiceAttachmentUrls.push(result.meta.voiceUrl);
184
- if (result.meta.asrReferText) voiceAsrReferTexts.push(result.meta.asrReferText);
185
-
186
- if (result.type === "image" && result.localPath) {
187
- imageUrls.push(result.localPath);
188
- imageMediaTypes.push(result.contentType);
189
- attachmentLocalPaths.push(result.localPath);
190
- } else if (result.type === "voice" && result.localPath) {
191
- voiceAttachmentPaths.push(result.localPath);
192
- voiceTranscripts.push(result.transcript);
193
- voiceTranscriptSources.push(result.transcriptSource);
194
- attachmentLocalPaths.push(result.localPath);
195
- } else if (result.type === "other" && result.localPath) {
196
- otherAttachments.push(`[附件: ${result.localPath}]`);
197
- attachmentLocalPaths.push(result.localPath);
198
- } else if (result.type === "image-fallback") {
199
- imageUrls.push(result.attUrl);
200
- imageMediaTypes.push(result.contentType);
201
- attachmentLocalPaths.push(null);
202
- // 给模型一个明确的失败提示(和 other-fallback 对齐)
203
- const hint = result.dlError?.includes("超时")
204
- ? "(图片下载超时)"
205
- : "(图片下载失败)";
206
- otherAttachments.push(`[图片] ${hint}`);
207
- } else if (result.type === "voice-fallback") {
208
- voiceTranscripts.push(result.transcript);
209
- voiceTranscriptSources.push("asr");
210
- attachmentLocalPaths.push(null);
211
- } else if (result.type === "other-fallback") {
212
- const hint = result.dlError?.includes("超时")
213
- ? "(下载超时)"
214
- : "(下载失败)";
215
- otherAttachments.push(`[附件: ${result.filename}] ${hint}`);
216
- attachmentLocalPaths.push(null);
217
- }
218
- }
219
-
220
- const attachmentInfo = otherAttachments.length > 0 ? "\n" + otherAttachments.join("\n") : "";
221
-
222
- return {
223
- attachmentInfo,
224
- imageUrls,
225
- imageMediaTypes,
226
- voiceAttachmentPaths,
227
- voiceAttachmentUrls,
228
- voiceAsrReferTexts,
229
- voiceTranscripts,
230
- voiceTranscriptSources,
231
- attachmentLocalPaths,
232
- };
233
- }
234
-
235
- /**
236
- * 将语音转录结果组装为用户消息中的文本片段。
237
- */
238
- export function formatVoiceText(transcripts: string[]): string {
239
- if (transcripts.length === 0) return "";
240
- return transcripts.length === 1
241
- ? `[语音消息] ${transcripts[0]}`
242
- : transcripts.map((t, i) => `[语音${i + 1}] ${t}`).join("\n");
243
- }
244
-
245
- // ============ 内部辅助 ============
246
-
247
- type VoiceResult =
248
- | { localPath: string; type: "voice"; transcript: string; transcriptSource: TranscriptSource; meta: { voiceUrl?: string; asrReferText?: string } }
249
- | { localPath: string; type: "voice"; transcript: string; transcriptSource: TranscriptSource; meta: { voiceUrl?: string; asrReferText?: string } };
250
-
251
- async function processVoiceAttachment(
252
- localPath: string,
253
- audioPath: string | null,
254
- att: RawAttachment,
255
- asrReferText: string,
256
- cfg: unknown,
257
- downloadDir: string,
258
- log: ProcessContext["log"],
259
- prefix: string,
260
- ): Promise<VoiceResult> {
261
- const wavUrl = att.voice_wav_url
262
- ? (att.voice_wav_url.startsWith("//") ? `https:${att.voice_wav_url}` : att.voice_wav_url)
263
- : "";
264
- const attUrl = att.url?.startsWith("//") ? `https:${att.url}` : att.url;
265
- const voiceSourceUrl = wavUrl || attUrl;
266
- const meta = {
267
- voiceUrl: voiceSourceUrl || undefined,
268
- asrReferText: asrReferText || undefined,
269
- };
270
-
271
- const sttCfg = resolveSTTConfig(cfg as Record<string, unknown>);
272
- if (!sttCfg) {
273
- if (asrReferText) {
274
- log?.info(`${prefix} Voice attachment: ${att.filename} (STT not configured, using asr_refer_text fallback)`);
275
- return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
276
- }
277
- log?.info(`${prefix} Voice attachment: ${att.filename} (STT not configured, skipping transcription)`);
278
- return { localPath, type: "voice", transcript: "[语音消息 - 语音识别未配置,无法转录]", transcriptSource: "fallback", meta };
279
- }
280
-
281
- // SILK→WAV 转换
282
- if (!audioPath) {
283
- log?.info(`${prefix} Voice attachment: ${att.filename}, converting SILK→WAV...`);
284
- try {
285
- const wavResult = await convertSilkToWav(localPath, downloadDir);
286
- if (wavResult) {
287
- audioPath = wavResult.wavPath;
288
- log?.info(`${prefix} Voice converted: ${wavResult.wavPath} (${formatDuration(wavResult.duration)})`);
289
- } else {
290
- audioPath = localPath;
291
- }
292
- } catch (convertErr) {
293
- log?.error(`${prefix} Voice conversion failed: ${convertErr}`);
294
- if (asrReferText) {
295
- return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
296
- }
297
- return { localPath, type: "voice", transcript: "[语音消息 - 格式转换失败]", transcriptSource: "fallback", meta };
298
- }
299
- }
300
-
301
- // STT 转录
302
- try {
303
- const transcript = await transcribeAudio(audioPath!, cfg as Record<string, unknown>);
304
- if (transcript) {
305
- log?.info(`${prefix} STT transcript: ${transcript.slice(0, 100)}...`);
306
- return { localPath, type: "voice", transcript, transcriptSource: "stt", meta };
307
- }
308
- if (asrReferText) {
309
- log?.info(`${prefix} STT returned empty result, using asr_refer_text fallback`);
310
- return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
311
- }
312
- log?.info(`${prefix} STT returned empty result`);
313
- return { localPath, type: "voice", transcript: "[语音消息 - 转录结果为空]", transcriptSource: "fallback", meta };
314
- } catch (sttErr) {
315
- log?.error(`${prefix} STT failed: ${sttErr}`);
316
- if (asrReferText) {
317
- return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
318
- }
319
- return { localPath, type: "voice", transcript: "[语音消息 - 转录失败]", transcriptSource: "fallback", meta };
320
- }
321
- }
@@ -1,353 +0,0 @@
1
- /**
2
- * 已知用户存储
3
- * 记录与机器人交互过的所有用户
4
- * 支持主动消息和批量通知功能
5
- */
6
-
7
- import fs from "node:fs";
8
- import path from "node:path";
9
-
10
- // 已知用户信息接口
11
- export interface KnownUser {
12
- /** 用户 openid(唯一标识) */
13
- openid: string;
14
- /** 消息类型:私聊用户 / 群组 */
15
- type: "c2c" | "group";
16
- /** 用户昵称(如有) */
17
- nickname?: string;
18
- /** 群组 openid(如果是群消息) */
19
- groupOpenid?: string;
20
- /** 关联的机器人账户 ID */
21
- accountId: string;
22
- /** 首次交互时间戳 */
23
- firstSeenAt: number;
24
- /** 最后交互时间戳 */
25
- lastSeenAt: number;
26
- /** 交互次数 */
27
- interactionCount: number;
28
- }
29
-
30
- import { getQQBotDataDir } from "./utils/platform.js";
31
-
32
- // 存储文件路径
33
- const KNOWN_USERS_DIR = getQQBotDataDir("data");
34
- const KNOWN_USERS_FILE = path.join(KNOWN_USERS_DIR, "known-users.json");
35
-
36
- // 内存缓存
37
- let usersCache: Map<string, KnownUser> | null = null;
38
-
39
- // 写入节流配置
40
- const SAVE_THROTTLE_MS = 5000; // 5秒写入一次
41
- let saveTimer: ReturnType<typeof setTimeout> | null = null;
42
- let isDirty = false;
43
-
44
- /**
45
- * 确保目录存在
46
- */
47
- function ensureDir(): void {
48
- if (!fs.existsSync(KNOWN_USERS_DIR)) {
49
- fs.mkdirSync(KNOWN_USERS_DIR, { recursive: true });
50
- }
51
- }
52
-
53
- /**
54
- * 从文件加载用户数据到缓存
55
- */
56
- function loadUsersFromFile(): Map<string, KnownUser> {
57
- if (usersCache !== null) {
58
- return usersCache;
59
- }
60
-
61
- usersCache = new Map();
62
-
63
- try {
64
- if (fs.existsSync(KNOWN_USERS_FILE)) {
65
- const data = fs.readFileSync(KNOWN_USERS_FILE, "utf-8");
66
- const users = JSON.parse(data) as KnownUser[];
67
-
68
- for (const user of users) {
69
- // 使用复合键:accountId + type + openid(群组还要加 groupOpenid)
70
- const key = makeUserKey(user);
71
- usersCache.set(key, user);
72
- }
73
-
74
- console.log(`[known-users] Loaded ${usersCache.size} users`);
75
- }
76
- } catch (err) {
77
- console.error(`[known-users] Failed to load users: ${err}`);
78
- usersCache = new Map();
79
- }
80
-
81
- return usersCache;
82
- }
83
-
84
- /**
85
- * 保存用户数据到文件(节流版本)
86
- */
87
- function saveUsersToFile(): void {
88
- if (!isDirty) return;
89
-
90
- if (saveTimer) {
91
- return; // 已有定时器在等待
92
- }
93
-
94
- saveTimer = setTimeout(() => {
95
- saveTimer = null;
96
- doSaveUsersToFile();
97
- }, SAVE_THROTTLE_MS);
98
- }
99
-
100
- /**
101
- * 实际执行保存
102
- */
103
- function doSaveUsersToFile(): void {
104
- if (!usersCache || !isDirty) return;
105
-
106
- try {
107
- ensureDir();
108
- const users = Array.from(usersCache.values());
109
- fs.writeFileSync(KNOWN_USERS_FILE, JSON.stringify(users, null, 2), "utf-8");
110
- isDirty = false;
111
- } catch (err) {
112
- console.error(`[known-users] Failed to save users: ${err}`);
113
- }
114
- }
115
-
116
- /**
117
- * 强制立即保存(用于进程退出前)
118
- */
119
- export function flushKnownUsers(): void {
120
- if (saveTimer) {
121
- clearTimeout(saveTimer);
122
- saveTimer = null;
123
- }
124
- doSaveUsersToFile();
125
- }
126
-
127
- /**
128
- * 生成用户唯一键
129
- */
130
- function makeUserKey(user: Partial<KnownUser>): string {
131
- const base = `${user.accountId}:${user.type}:${user.openid}`;
132
- if (user.type === "group" && user.groupOpenid) {
133
- return `${base}:${user.groupOpenid}`;
134
- }
135
- return base;
136
- }
137
-
138
- /**
139
- * 记录已知用户(收到消息时调用)
140
- * @param user 用户信息(部分字段)
141
- */
142
- export function recordKnownUser(user: {
143
- openid: string;
144
- type: "c2c" | "group";
145
- nickname?: string;
146
- groupOpenid?: string;
147
- accountId: string;
148
- }): void {
149
- const cache = loadUsersFromFile();
150
- const key = makeUserKey(user);
151
- const now = Date.now();
152
-
153
- const existing = cache.get(key);
154
-
155
- if (existing) {
156
- // 更新已存在的用户
157
- existing.lastSeenAt = now;
158
- existing.interactionCount++;
159
- if (user.nickname && user.nickname !== existing.nickname) {
160
- existing.nickname = user.nickname;
161
- }
162
- } else {
163
- // 新用户
164
- const newUser: KnownUser = {
165
- openid: user.openid,
166
- type: user.type,
167
- nickname: user.nickname,
168
- groupOpenid: user.groupOpenid,
169
- accountId: user.accountId,
170
- firstSeenAt: now,
171
- lastSeenAt: now,
172
- interactionCount: 1,
173
- };
174
- cache.set(key, newUser);
175
- console.log(`[known-users] New user: ${user.openid} (${user.type})`);
176
- }
177
-
178
- isDirty = true;
179
- saveUsersToFile();
180
- }
181
-
182
- /**
183
- * 获取单个用户信息
184
- * @param accountId 机器人账户 ID
185
- * @param openid 用户 openid
186
- * @param type 消息类型
187
- * @param groupOpenid 群组 openid(可选)
188
- */
189
- export function getKnownUser(
190
- accountId: string,
191
- openid: string,
192
- type: "c2c" | "group" = "c2c",
193
- groupOpenid?: string
194
- ): KnownUser | undefined {
195
- const cache = loadUsersFromFile();
196
- const key = makeUserKey({ accountId, openid, type, groupOpenid });
197
- return cache.get(key);
198
- }
199
-
200
- /**
201
- * 列出所有已知用户
202
- * @param options 筛选选项
203
- */
204
- export function listKnownUsers(options?: {
205
- /** 筛选特定机器人账户的用户 */
206
- accountId?: string;
207
- /** 筛选消息类型 */
208
- type?: "c2c" | "group";
209
- /** 最近活跃时间(毫秒,如 86400000 表示最近 24 小时) */
210
- activeWithin?: number;
211
- /** 返回数量限制 */
212
- limit?: number;
213
- /** 排序方式 */
214
- sortBy?: "lastSeenAt" | "firstSeenAt" | "interactionCount";
215
- /** 排序方向 */
216
- sortOrder?: "asc" | "desc";
217
- }): KnownUser[] {
218
- const cache = loadUsersFromFile();
219
- let users = Array.from(cache.values());
220
-
221
- // 筛选
222
- if (options?.accountId) {
223
- users = users.filter(u => u.accountId === options.accountId);
224
- }
225
- if (options?.type) {
226
- users = users.filter(u => u.type === options.type);
227
- }
228
- if (options?.activeWithin) {
229
- const cutoff = Date.now() - options.activeWithin;
230
- users = users.filter(u => u.lastSeenAt >= cutoff);
231
- }
232
-
233
- // 排序
234
- const sortBy = options?.sortBy ?? "lastSeenAt";
235
- const sortOrder = options?.sortOrder ?? "desc";
236
- users.sort((a, b) => {
237
- const aVal = a[sortBy] ?? 0;
238
- const bVal = b[sortBy] ?? 0;
239
- return sortOrder === "asc" ? aVal - bVal : bVal - aVal;
240
- });
241
-
242
- // 限制数量
243
- if (options?.limit && options.limit > 0) {
244
- users = users.slice(0, options.limit);
245
- }
246
-
247
- return users;
248
- }
249
-
250
- /**
251
- * 获取用户统计信息
252
- * @param accountId 机器人账户 ID(可选,不传则返回所有账户的统计)
253
- */
254
- export function getKnownUsersStats(accountId?: string): {
255
- totalUsers: number;
256
- c2cUsers: number;
257
- groupUsers: number;
258
- activeIn24h: number;
259
- activeIn7d: number;
260
- } {
261
- let users = listKnownUsers({ accountId });
262
-
263
- const now = Date.now();
264
- const day = 24 * 60 * 60 * 1000;
265
-
266
- return {
267
- totalUsers: users.length,
268
- c2cUsers: users.filter(u => u.type === "c2c").length,
269
- groupUsers: users.filter(u => u.type === "group").length,
270
- activeIn24h: users.filter(u => now - u.lastSeenAt < day).length,
271
- activeIn7d: users.filter(u => now - u.lastSeenAt < 7 * day).length,
272
- };
273
- }
274
-
275
- /**
276
- * 删除用户记录
277
- * @param accountId 机器人账户 ID
278
- * @param openid 用户 openid
279
- * @param type 消息类型
280
- * @param groupOpenid 群组 openid(可选)
281
- */
282
- export function removeKnownUser(
283
- accountId: string,
284
- openid: string,
285
- type: "c2c" | "group" = "c2c",
286
- groupOpenid?: string
287
- ): boolean {
288
- const cache = loadUsersFromFile();
289
- const key = makeUserKey({ accountId, openid, type, groupOpenid });
290
-
291
- if (cache.has(key)) {
292
- cache.delete(key);
293
- isDirty = true;
294
- saveUsersToFile();
295
- console.log(`[known-users] Removed user ${openid}`);
296
- return true;
297
- }
298
-
299
- return false;
300
- }
301
-
302
- /**
303
- * 清除所有用户记录
304
- * @param accountId 机器人账户 ID(可选,不传则清除所有)
305
- */
306
- export function clearKnownUsers(accountId?: string): number {
307
- const cache = loadUsersFromFile();
308
- let count = 0;
309
-
310
- if (accountId) {
311
- // 只清除指定账户的用户
312
- for (const [key, user] of cache.entries()) {
313
- if (user.accountId === accountId) {
314
- cache.delete(key);
315
- count++;
316
- }
317
- }
318
- } else {
319
- // 清除所有
320
- count = cache.size;
321
- cache.clear();
322
- }
323
-
324
- if (count > 0) {
325
- isDirty = true;
326
- doSaveUsersToFile(); // 立即保存
327
- console.log(`[known-users] Cleared ${count} users`);
328
- }
329
-
330
- return count;
331
- }
332
-
333
- /**
334
- * 获取用户的所有群组(某用户在哪些群里交互过)
335
- * @param accountId 机器人账户 ID
336
- * @param openid 用户 openid
337
- */
338
- export function getUserGroups(accountId: string, openid: string): string[] {
339
- const users = listKnownUsers({ accountId, type: "group" });
340
- return users
341
- .filter(u => u.openid === openid && u.groupOpenid)
342
- .map(u => u.groupOpenid!);
343
- }
344
-
345
- /**
346
- * 获取群组的所有成员
347
- * @param accountId 机器人账户 ID
348
- * @param groupOpenid 群组 openid
349
- */
350
- export function getGroupMembers(accountId: string, groupOpenid: string): KnownUser[] {
351
- return listKnownUsers({ accountId, type: "group" })
352
- .filter(u => u.groupOpenid === groupOpenid);
353
- }