@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,412 @@
1
+ /**
2
+ * QQ Bot 引用索引持久化存储
3
+ *
4
+ * QQ Bot 使用 REFIDX_xxx 索引体系做引用消息,
5
+ * 入站事件只有索引值,无 API 可回查内容。
6
+ * 采用 内存缓存 + JSONL 追加写持久化 方案,确保重启后历史引用仍可命中。
7
+ *
8
+ * 存储位置:~/.openclaw/qqbot/data/ref-index.jsonl
9
+ *
10
+ * 每行格式:{"k":"REFIDX_xxx","v":{...},"t":1709000000}
11
+ * - k = refIdx 键
12
+ * - v = 消息数据
13
+ * - t = 写入时间(用于 TTL 淘汰和 compact)
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { getQQBotDataDir } from "./utils/platform.js";
19
+ import { formatAttachmentTags } from "./group-history.js";
20
+ import { parseFaceTags, buildAttachmentSummaries } from "./utils/text-parsing.js";
21
+ import { processAttachments, formatVoiceText } from "./inbound-attachments.js";
22
+
23
+ // ============ 存储的消息摘要 ============
24
+
25
+ export interface RefIndexEntry {
26
+ /** 消息文本内容(完整保存) */
27
+ content: string;
28
+ /** 发送者 ID */
29
+ senderId: string;
30
+ /** 发送者名称 */
31
+ senderName?: string;
32
+ /** 消息时间戳 (ms) */
33
+ timestamp: number;
34
+ /** 是否是 bot 发出的消息 */
35
+ isBot?: boolean;
36
+ /** 附件摘要(图片/语音/视频/文件等) */
37
+ attachments?: RefAttachmentSummary[];
38
+ }
39
+
40
+ /** 附件摘要:存本地路径、在线 URL 和类型描述 */
41
+ export interface RefAttachmentSummary {
42
+ /** 附件类型 */
43
+ type: "image" | "voice" | "video" | "file" | "unknown";
44
+ /** 文件名(如有) */
45
+ filename?: string;
46
+ /** MIME 类型 */
47
+ contentType?: string;
48
+ /** 语音转录文本(入站:STT/ASR识别结果;出站:TTS原文本) */
49
+ transcript?: string;
50
+ /** 语音转录来源:stt=本地STT、asr=平台ASR、tts=TTS原文本、fallback=兜底文案 */
51
+ transcriptSource?: "stt" | "asr" | "tts" | "fallback";
52
+ /** 已下载到本地的文件路径(持久化后可供引用时访问) */
53
+ localPath?: string;
54
+ /** 在线来源 URL(公网图片/文件等) */
55
+ url?: string;
56
+ }
57
+
58
+ // ============ 配置 ============
59
+
60
+ const STORAGE_DIR = getQQBotDataDir("data");
61
+ const REF_INDEX_FILE = path.join(STORAGE_DIR, "ref-index.jsonl");
62
+ const MAX_ENTRIES = 50000; // 内存中最大缓存条目数
63
+ const TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 天
64
+ const COMPACT_THRESHOLD_RATIO = 2; // 文件行数超过有效条目 N 倍时 compact
65
+
66
+ // ============ JSONL 行格式 ============
67
+
68
+ interface RefIndexLine {
69
+ /** refIdx 键 */
70
+ k: string;
71
+ /** 消息数据 */
72
+ v: RefIndexEntry;
73
+ /** 写入时间 (ms) */
74
+ t: number;
75
+ }
76
+
77
+ // ============ 内存缓存 ============
78
+
79
+ let cache: Map<string, RefIndexEntry & { _createdAt: number }> | null = null;
80
+ let totalLinesOnDisk = 0; // 磁盘文件总行数(含过期 / 被覆盖的)
81
+
82
+ /**
83
+ * 从 JSONL 文件加载到内存(懒加载,首次访问时触发)
84
+ */
85
+ function loadFromFile(): Map<string, RefIndexEntry & { _createdAt: number }> {
86
+ if (cache !== null) return cache;
87
+
88
+ cache = new Map();
89
+ totalLinesOnDisk = 0;
90
+
91
+ try {
92
+ if (!fs.existsSync(REF_INDEX_FILE)) {
93
+ return cache;
94
+ }
95
+
96
+ const raw = fs.readFileSync(REF_INDEX_FILE, "utf-8");
97
+ const lines = raw.split("\n");
98
+ const now = Date.now();
99
+ let expired = 0;
100
+
101
+ for (const line of lines) {
102
+ const trimmed = line.trim();
103
+ if (!trimmed) continue;
104
+ totalLinesOnDisk++;
105
+
106
+ try {
107
+ const entry = JSON.parse(trimmed) as RefIndexLine;
108
+ if (!entry.k || !entry.v || !entry.t) continue;
109
+
110
+ // 跳过过期条目
111
+ if (now - entry.t > TTL_MS) {
112
+ expired++;
113
+ continue;
114
+ }
115
+
116
+ cache.set(entry.k, {
117
+ ...entry.v,
118
+ _createdAt: entry.t,
119
+ });
120
+ } catch {
121
+ // 跳过损坏的行
122
+ }
123
+ }
124
+
125
+ console.log(
126
+ `[ref-index-store] Loaded ${cache.size} entries from ${totalLinesOnDisk} lines (${expired} expired)`,
127
+ );
128
+
129
+ // 启动时检查是否需要 compact
130
+ if (shouldCompact()) {
131
+ compactFile();
132
+ }
133
+ } catch (err) {
134
+ console.error(`[ref-index-store] Failed to load: ${err}`);
135
+ cache = new Map();
136
+ }
137
+
138
+ return cache;
139
+ }
140
+
141
+ // ============ JSONL 追加写入 ============
142
+
143
+ /**
144
+ * 追加一行到 JSONL 文件
145
+ */
146
+ function appendLine(line: RefIndexLine): void {
147
+ try {
148
+ ensureDir();
149
+ fs.appendFileSync(REF_INDEX_FILE, JSON.stringify(line) + "\n", "utf-8");
150
+ totalLinesOnDisk++;
151
+ } catch (err) {
152
+ console.error(`[ref-index-store] Failed to append: ${err}`);
153
+ }
154
+ }
155
+
156
+ function ensureDir(): void {
157
+ if (!fs.existsSync(STORAGE_DIR)) {
158
+ fs.mkdirSync(STORAGE_DIR, { recursive: true });
159
+ }
160
+ }
161
+
162
+ // ============ Compact:重写文件,去除过期和被覆盖的条目 ============
163
+
164
+ function shouldCompact(): boolean {
165
+ if (!cache) return false;
166
+ // 文件行数远超有效条目数时 compact
167
+ return totalLinesOnDisk > cache.size * COMPACT_THRESHOLD_RATIO && totalLinesOnDisk > 1000;
168
+ }
169
+
170
+ function compactFile(): void {
171
+ if (!cache) return;
172
+
173
+ const before = totalLinesOnDisk;
174
+ try {
175
+ ensureDir();
176
+ const tmpPath = REF_INDEX_FILE + ".tmp";
177
+ const lines: string[] = [];
178
+
179
+ for (const [key, entry] of cache) {
180
+ const line: RefIndexLine = {
181
+ k: key,
182
+ v: {
183
+ content: entry.content,
184
+ senderId: entry.senderId,
185
+ senderName: entry.senderName,
186
+ timestamp: entry.timestamp,
187
+ isBot: entry.isBot,
188
+ attachments: entry.attachments,
189
+ },
190
+ t: entry._createdAt,
191
+ };
192
+ lines.push(JSON.stringify(line));
193
+ }
194
+
195
+ fs.writeFileSync(tmpPath, lines.join("\n") + "\n", "utf-8");
196
+ fs.renameSync(tmpPath, REF_INDEX_FILE);
197
+ totalLinesOnDisk = cache.size;
198
+ console.log(`[ref-index-store] Compacted: ${before} lines → ${totalLinesOnDisk} lines`);
199
+ } catch (err) {
200
+ console.error(`[ref-index-store] Compact failed: ${err}`);
201
+ }
202
+ }
203
+
204
+ // ============ 溢出淘汰 ============
205
+
206
+ function evictIfNeeded(): void {
207
+ if (!cache || cache.size < MAX_ENTRIES) return;
208
+
209
+ const now = Date.now();
210
+ // 第一轮:清理过期
211
+ for (const [key, entry] of cache) {
212
+ if (now - entry._createdAt > TTL_MS) {
213
+ cache.delete(key);
214
+ }
215
+ }
216
+
217
+ // 第二轮:仍超限,按时间删最旧
218
+ if (cache.size >= MAX_ENTRIES) {
219
+ const sorted = [...cache.entries()].sort((a, b) => a[1]._createdAt - b[1]._createdAt);
220
+ const toRemove = sorted.slice(0, cache.size - MAX_ENTRIES + 1000);
221
+ for (const [key] of toRemove) {
222
+ cache.delete(key);
223
+ }
224
+ console.log(`[ref-index-store] Evicted ${toRemove.length} oldest entries`);
225
+ }
226
+ }
227
+
228
+ // ============ 公共 API ============
229
+
230
+ /**
231
+ * 存储一条消息的 refIdx 映射
232
+ */
233
+ export function setRefIndex(refIdx: string, entry: RefIndexEntry): void {
234
+ const store = loadFromFile();
235
+ evictIfNeeded();
236
+
237
+ const now = Date.now();
238
+ store.set(refIdx, {
239
+ content: entry.content,
240
+ senderId: entry.senderId,
241
+ senderName: entry.senderName,
242
+ timestamp: entry.timestamp,
243
+ isBot: entry.isBot,
244
+ attachments: entry.attachments,
245
+ _createdAt: now,
246
+ });
247
+
248
+ // 追加写入 JSONL
249
+ appendLine({
250
+ k: refIdx,
251
+ v: {
252
+ content: entry.content,
253
+ senderId: entry.senderId,
254
+ senderName: entry.senderName,
255
+ timestamp: entry.timestamp,
256
+ isBot: entry.isBot,
257
+ attachments: entry.attachments,
258
+ },
259
+ t: now,
260
+ });
261
+
262
+ // 检查是否需要 compact
263
+ if (shouldCompact()) {
264
+ compactFile();
265
+ }
266
+ }
267
+
268
+ /**
269
+ * 查找被引用消息
270
+ */
271
+ export function getRefIndex(refIdx: string): RefIndexEntry | null {
272
+ const store = loadFromFile();
273
+ const entry = store.get(refIdx);
274
+ if (!entry) return null;
275
+
276
+ // 检查过期
277
+ if (Date.now() - entry._createdAt > TTL_MS) {
278
+ store.delete(refIdx);
279
+ return null;
280
+ }
281
+
282
+ return {
283
+ content: entry.content,
284
+ senderId: entry.senderId,
285
+ senderName: entry.senderName,
286
+ timestamp: entry.timestamp,
287
+ isBot: entry.isBot,
288
+ attachments: entry.attachments,
289
+ };
290
+ }
291
+
292
+ /**
293
+ * 将引用消息内容格式化为人类可读的描述(供 AI 上下文注入)
294
+ */
295
+ export function formatRefEntryForAgent(entry: RefIndexEntry): string {
296
+ const parts: string[] = [];
297
+
298
+ // 文本内容
299
+ if (entry.content.trim()) {
300
+ parts.push(entry.content);
301
+ }
302
+
303
+ // 附件描述(委托 formatAttachmentTags 统一格式化)
304
+ const attachmentDesc = formatAttachmentTags(entry.attachments);
305
+ if (attachmentDesc) {
306
+ parts.push(attachmentDesc);
307
+ }
308
+
309
+ return parts.join("\n");
310
+ }
311
+
312
+ /**
313
+ * 将 QQ 推送事件中的 message_reference 结构格式化为人类可读的描述(供 AI 上下文注入)
314
+ *
315
+ * 完整参考 gateway 中对当前消息的处理流程:
316
+ * 1. 调用 processAttachments 下载附件到本地、语音转录
317
+ * 2. 调用 formatVoiceText 格式化语音转录文本
318
+ * 3. 调用 parseFaceTags 解析 QQ 表情标签
319
+ * 4. 按 gateway 中 userContent 的拼接逻辑组合最终文本
320
+ */
321
+ export async function formatMessageReferenceForAgent(
322
+ ref: {
323
+ content: string;
324
+ attachments?: Array<{
325
+ content_type: string;
326
+ url: string;
327
+ filename?: string;
328
+ height?: number;
329
+ width?: number;
330
+ size?: number;
331
+ voice_wav_url?: string;
332
+ asr_refer_text?: string;
333
+ }>;
334
+ } | undefined,
335
+ ctx: {
336
+ appId: string;
337
+ peerId?: string;
338
+ cfg: unknown;
339
+ log?: {
340
+ info: (msg: string) => void;
341
+ error: (msg: string) => void;
342
+ debug?: (msg: string) => void;
343
+ };
344
+ },
345
+ ): Promise<string> {
346
+ if (!ref) return "";
347
+
348
+ // 处理附件(图片等)- 下载到本地供 openclaw 访问(参考 gateway 中 processAttachments 调用)
349
+ const processed = await processAttachments(ref.attachments, ctx);
350
+ const { attachmentInfo, voiceTranscripts, voiceTranscriptSources, attachmentLocalPaths } = processed;
351
+
352
+ // 语音转录文本注入(参考 gateway 中 formatVoiceText 调用)
353
+ const voiceText = formatVoiceText(voiceTranscripts);
354
+
355
+ // 解析 QQ 表情标签,将 <faceType=...,ext="base64"> 替换为 【表情: 中文名】
356
+ const parsedContent = parseFaceTags(ref.content ?? "");
357
+
358
+ // 最终组合(参考 gateway 中 userContent 的拼接逻辑)
359
+ const userContent = voiceText
360
+ ? (parsedContent.trim() ? `${parsedContent}\n${voiceText}` : voiceText) + attachmentInfo
361
+ : parsedContent + attachmentInfo;
362
+
363
+ // 构建附件摘要并通过 formatAttachmentTags 统一生成标签
364
+ // 与缓存命中路径 (formatRefEntryForAgent → formatAttachmentTags) 格式完全一致
365
+ const attSummaries = buildAttachmentSummaries(ref.attachments, attachmentLocalPaths);
366
+ if (attSummaries && voiceTranscripts.length > 0) {
367
+ let voiceIdx = 0;
368
+ for (const att of attSummaries) {
369
+ if (att.type === "voice" && voiceIdx < voiceTranscripts.length) {
370
+ att.transcript = voiceTranscripts[voiceIdx];
371
+ if (voiceIdx < voiceTranscriptSources.length) {
372
+ att.transcriptSource = voiceTranscriptSources[voiceIdx];
373
+ }
374
+ voiceIdx++;
375
+ }
376
+ }
377
+ }
378
+ const attachmentDesc = formatAttachmentTags(attSummaries);
379
+
380
+ const parts: string[] = [];
381
+ if (userContent.trim()) parts.push(userContent.trim());
382
+ if (attachmentDesc) parts.push(attachmentDesc);
383
+
384
+ return parts.join(" ");
385
+ }
386
+
387
+ /**
388
+ * 进程退出前强制 compact(确保数据一致性)
389
+ */
390
+ export function flushRefIndex(): void {
391
+ if (cache && shouldCompact()) {
392
+ compactFile();
393
+ }
394
+ }
395
+
396
+ /**
397
+ * 缓存统计(调试用)
398
+ */
399
+ export function getRefIndexStats(): {
400
+ size: number;
401
+ maxEntries: number;
402
+ totalLinesOnDisk: number;
403
+ filePath: string;
404
+ } {
405
+ const store = loadFromFile();
406
+ return {
407
+ size: store.size,
408
+ maxEntries: MAX_ENTRIES,
409
+ totalLinesOnDisk,
410
+ filePath: REF_INDEX_FILE,
411
+ };
412
+ }