@tencent-connect/openclaw-qqbot 1.7.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/README.md +206 -4
  2. package/README.zh.md +207 -4
  3. package/bin/qqbot-cli.js +5 -2
  4. package/dist/index.cjs +15549 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +890 -0
  7. package/index.ts +56 -18
  8. package/openclaw.plugin.json +19 -3
  9. package/package.json +14 -23
  10. package/preload.cjs +17 -14
  11. package/scripts/link-sdk-core.cjs +133 -50
  12. package/scripts/upgrade-via-npm.sh +400 -73
  13. package/scripts/upgrade-via-source.sh +126 -89
  14. package/skills/qqbot-channel/SKILL.md +34 -12
  15. package/src/adapter/contract.ts +69 -0
  16. package/src/adapter/gateway.ts +61 -0
  17. package/src/adapter/index.ts +15 -0
  18. package/src/adapter/lint.ts +144 -0
  19. package/src/adapter/media.ts +112 -0
  20. package/src/adapter/pairing.ts +99 -0
  21. package/src/adapter/resolve.ts +333 -0
  22. package/src/adapter/setup.ts +55 -0
  23. package/src/adapter/webhook.ts +248 -0
  24. package/src/adapter/workspace.ts +29 -0
  25. package/src/bot-instance.ts +60 -0
  26. package/src/channel.ts +202 -413
  27. package/src/commands/bot-approve.ts +143 -0
  28. package/src/commands/bot-clear-storage.ts +114 -0
  29. package/src/commands/bot-group-always.ts +62 -0
  30. package/src/commands/bot-help.ts +40 -0
  31. package/src/commands/bot-logs.ts +248 -0
  32. package/src/commands/bot-me.ts +18 -0
  33. package/src/commands/bot-pairing.ts +57 -0
  34. package/src/commands/bot-ping.ts +33 -0
  35. package/src/commands/bot-streaming.ts +55 -0
  36. package/src/commands/bot-upgrade.ts +56 -0
  37. package/src/commands/bot-version.ts +41 -0
  38. package/src/commands/config-util.ts +96 -0
  39. package/src/commands/index.ts +49 -0
  40. package/src/config.ts +59 -19
  41. package/src/dispatch/body-assembler.ts +356 -0
  42. package/src/dispatch/ctx-builder.ts +110 -0
  43. package/src/dispatch/dispatch.ts +352 -0
  44. package/src/dispatch/envelope-builder.ts +112 -0
  45. package/src/dispatch/index.ts +2 -0
  46. package/src/{approval-handler.ts → features/approval-handler.ts} +40 -94
  47. package/src/features/approval-utils.ts +41 -0
  48. package/src/features/credential-backup.ts +82 -0
  49. package/src/features/history-store.ts +19 -0
  50. package/src/features/msgid-cache.ts +55 -0
  51. package/src/features/onboarding.ts +38 -0
  52. package/src/{proactive.ts → features/proactive.ts} +68 -49
  53. package/src/features/ref-index-store.ts +282 -0
  54. package/src/{update-checker.ts → features/update-checker.ts} +15 -35
  55. package/src/gateway/event-handlers.ts +241 -0
  56. package/src/gateway/index.ts +3 -0
  57. package/src/gateway/lifecycle.ts +221 -0
  58. package/src/gateway/middleware-setup.ts +139 -0
  59. package/src/gateway/qqbot-gateway.ts +342 -0
  60. package/src/middleware/access-control.ts +130 -0
  61. package/src/middleware/attachment.ts +314 -0
  62. package/src/middleware/policy-injector.ts +66 -0
  63. package/src/openclaw-plugin-sdk.d.ts +81 -10
  64. package/src/outbound/cron-scheduler.ts +130 -0
  65. package/src/outbound/debounce.ts +102 -0
  66. package/src/outbound/deliver-pipeline.ts +235 -0
  67. package/src/outbound/image-size.ts +123 -0
  68. package/src/outbound/index.ts +3 -0
  69. package/src/outbound/local-file-router.ts +145 -0
  70. package/src/outbound/media-send.ts +377 -0
  71. package/src/outbound/outbound-service.ts +222 -0
  72. package/src/outbound/reply-limiter.ts +128 -0
  73. package/src/outbound/sanitize.ts +32 -0
  74. package/src/outbound/streaming-controller.ts +228 -0
  75. package/src/outbound/target.ts +63 -0
  76. package/src/outbound/tts-provider.ts +84 -0
  77. package/src/request-context.ts +9 -8
  78. package/src/runtime.ts +31 -6
  79. package/src/setup/account-key.ts +41 -0
  80. package/src/setup/finalize.ts +110 -0
  81. package/src/setup/login.ts +197 -0
  82. package/src/setup/surface.ts +40 -0
  83. package/src/tools/platform.ts +149 -0
  84. package/src/tools/remind.ts +1 -1
  85. package/src/types-augment.d.ts +54 -0
  86. package/src/types.ts +71 -8
  87. package/src/typings/openclaw-webhook-ingress.d.ts +66 -0
  88. package/src/utils/mention.ts +52 -0
  89. package/src/utils/pkg-version.ts +84 -47
  90. package/src/utils/platform.ts +31 -7
  91. package/src/utils/plugin-logger.ts +104 -0
  92. package/src/utils/ssrf-guard.ts +32 -2
  93. package/src/utils/stt.ts +137 -0
  94. package/src/utils/voice-text.ts +61 -0
  95. package/tsconfig.json +2 -1
  96. package/tsup.config.ts +57 -0
  97. package/dist/index.d.ts +0 -17
  98. package/dist/index.js +0 -26
  99. package/dist/src/admin-resolver.d.ts +0 -33
  100. package/dist/src/admin-resolver.js +0 -157
  101. package/dist/src/api.d.ts +0 -307
  102. package/dist/src/api.js +0 -910
  103. package/dist/src/approval-handler.d.ts +0 -47
  104. package/dist/src/approval-handler.js +0 -372
  105. package/dist/src/channel.d.ts +0 -29
  106. package/dist/src/channel.js +0 -528
  107. package/dist/src/config.d.ts +0 -56
  108. package/dist/src/config.js +0 -278
  109. package/dist/src/credential-backup.d.ts +0 -31
  110. package/dist/src/credential-backup.js +0 -66
  111. package/dist/src/deliver-debounce.d.ts +0 -74
  112. package/dist/src/deliver-debounce.js +0 -174
  113. package/dist/src/gateway.d.ts +0 -18
  114. package/dist/src/gateway.js +0 -2063
  115. package/dist/src/group-history.d.ts +0 -136
  116. package/dist/src/group-history.js +0 -226
  117. package/dist/src/image-server.d.ts +0 -87
  118. package/dist/src/image-server.js +0 -570
  119. package/dist/src/inbound-attachments.d.ts +0 -60
  120. package/dist/src/inbound-attachments.js +0 -248
  121. package/dist/src/known-users.d.ts +0 -100
  122. package/dist/src/known-users.js +0 -263
  123. package/dist/src/message-gating.d.ts +0 -53
  124. package/dist/src/message-gating.js +0 -107
  125. package/dist/src/message-queue.d.ts +0 -91
  126. package/dist/src/message-queue.js +0 -257
  127. package/dist/src/onboarding.d.ts +0 -10
  128. package/dist/src/onboarding.js +0 -203
  129. package/dist/src/outbound-deliver.d.ts +0 -48
  130. package/dist/src/outbound-deliver.js +0 -392
  131. package/dist/src/outbound.d.ts +0 -220
  132. package/dist/src/outbound.js +0 -975
  133. package/dist/src/proactive.d.ts +0 -170
  134. package/dist/src/proactive.js +0 -399
  135. package/dist/src/ref-index-store.d.ts +0 -101
  136. package/dist/src/ref-index-store.js +0 -298
  137. package/dist/src/reply-dispatcher.d.ts +0 -35
  138. package/dist/src/reply-dispatcher.js +0 -311
  139. package/dist/src/request-context.d.ts +0 -25
  140. package/dist/src/request-context.js +0 -37
  141. package/dist/src/runtime.d.ts +0 -3
  142. package/dist/src/runtime.js +0 -13
  143. package/dist/src/session-store.d.ts +0 -52
  144. package/dist/src/session-store.js +0 -254
  145. package/dist/src/slash-commands.d.ts +0 -82
  146. package/dist/src/slash-commands.js +0 -2117
  147. package/dist/src/startup-greeting.d.ts +0 -30
  148. package/dist/src/startup-greeting.js +0 -97
  149. package/dist/src/streaming.d.ts +0 -247
  150. package/dist/src/streaming.js +0 -899
  151. package/dist/src/stt.d.ts +0 -21
  152. package/dist/src/stt.js +0 -70
  153. package/dist/src/tools/channel.d.ts +0 -16
  154. package/dist/src/tools/channel.js +0 -234
  155. package/dist/src/tools/remind.d.ts +0 -2
  156. package/dist/src/tools/remind.js +0 -256
  157. package/dist/src/types.d.ts +0 -443
  158. package/dist/src/types.js +0 -22
  159. package/dist/src/typing-keepalive.d.ts +0 -27
  160. package/dist/src/typing-keepalive.js +0 -64
  161. package/dist/src/update-checker.d.ts +0 -36
  162. package/dist/src/update-checker.js +0 -171
  163. package/dist/src/utils/audio-convert.d.ts +0 -98
  164. package/dist/src/utils/audio-convert.js +0 -755
  165. package/dist/src/utils/chunked-upload.d.ts +0 -68
  166. package/dist/src/utils/chunked-upload.js +0 -341
  167. package/dist/src/utils/file-utils.d.ts +0 -61
  168. package/dist/src/utils/file-utils.js +0 -172
  169. package/dist/src/utils/image-size.d.ts +0 -51
  170. package/dist/src/utils/image-size.js +0 -234
  171. package/dist/src/utils/media-send.d.ts +0 -158
  172. package/dist/src/utils/media-send.js +0 -502
  173. package/dist/src/utils/media-tags.d.ts +0 -14
  174. package/dist/src/utils/media-tags.js +0 -165
  175. package/dist/src/utils/payload.d.ts +0 -112
  176. package/dist/src/utils/payload.js +0 -186
  177. package/dist/src/utils/pkg-version.d.ts +0 -5
  178. package/dist/src/utils/pkg-version.js +0 -61
  179. package/dist/src/utils/platform.d.ts +0 -137
  180. package/dist/src/utils/platform.js +0 -390
  181. package/dist/src/utils/ssrf-guard.d.ts +0 -25
  182. package/dist/src/utils/ssrf-guard.js +0 -91
  183. package/dist/src/utils/text-parsing.d.ts +0 -31
  184. package/dist/src/utils/text-parsing.js +0 -74
  185. package/dist/src/utils/upload-cache.d.ts +0 -34
  186. package/dist/src/utils/upload-cache.js +0 -93
  187. package/node_modules/@eshaz/web-worker/LICENSE +0 -201
  188. package/node_modules/@eshaz/web-worker/README.md +0 -134
  189. package/node_modules/@eshaz/web-worker/browser.js +0 -17
  190. package/node_modules/@eshaz/web-worker/cjs/browser.js +0 -16
  191. package/node_modules/@eshaz/web-worker/cjs/node.js +0 -219
  192. package/node_modules/@eshaz/web-worker/index.d.ts +0 -4
  193. package/node_modules/@eshaz/web-worker/node.js +0 -223
  194. package/node_modules/@eshaz/web-worker/package.json +0 -54
  195. package/node_modules/@wasm-audio-decoders/common/index.js +0 -5
  196. package/node_modules/@wasm-audio-decoders/common/package.json +0 -36
  197. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +0 -231
  198. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +0 -129
  199. package/node_modules/@wasm-audio-decoders/common/src/puff/README +0 -67
  200. package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +0 -31
  201. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +0 -863
  202. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +0 -35
  203. package/node_modules/@wasm-audio-decoders/common/src/utilities.js +0 -3
  204. package/node_modules/@wasm-audio-decoders/common/types.d.ts +0 -7
  205. package/node_modules/mpg123-decoder/README.md +0 -265
  206. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +0 -185
  207. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +0 -1
  208. package/node_modules/mpg123-decoder/index.js +0 -8
  209. package/node_modules/mpg123-decoder/package.json +0 -58
  210. package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +0 -464
  211. package/node_modules/mpg123-decoder/src/MPEGDecoder.js +0 -200
  212. package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +0 -21
  213. package/node_modules/mpg123-decoder/types.d.ts +0 -30
  214. package/node_modules/silk-wasm/LICENSE +0 -21
  215. package/node_modules/silk-wasm/README.md +0 -85
  216. package/node_modules/silk-wasm/lib/index.cjs +0 -16
  217. package/node_modules/silk-wasm/lib/index.d.ts +0 -70
  218. package/node_modules/silk-wasm/lib/index.mjs +0 -16
  219. package/node_modules/silk-wasm/lib/silk.wasm +0 -0
  220. package/node_modules/silk-wasm/lib/utils.d.ts +0 -4
  221. package/node_modules/silk-wasm/package.json +0 -39
  222. package/node_modules/simple-yenc/.github/FUNDING.yml +0 -1
  223. package/node_modules/simple-yenc/.prettierignore +0 -1
  224. package/node_modules/simple-yenc/LICENSE +0 -7
  225. package/node_modules/simple-yenc/README.md +0 -163
  226. package/node_modules/simple-yenc/dist/esm.js +0 -1
  227. package/node_modules/simple-yenc/dist/index.js +0 -1
  228. package/node_modules/simple-yenc/package.json +0 -50
  229. package/node_modules/simple-yenc/rollup.config.js +0 -27
  230. package/node_modules/simple-yenc/src/simple-yenc.js +0 -302
  231. package/node_modules/ws/LICENSE +0 -20
  232. package/node_modules/ws/README.md +0 -548
  233. package/node_modules/ws/browser.js +0 -8
  234. package/node_modules/ws/index.js +0 -13
  235. package/node_modules/ws/lib/buffer-util.js +0 -131
  236. package/node_modules/ws/lib/constants.js +0 -19
  237. package/node_modules/ws/lib/event-target.js +0 -292
  238. package/node_modules/ws/lib/extension.js +0 -203
  239. package/node_modules/ws/lib/limiter.js +0 -55
  240. package/node_modules/ws/lib/permessage-deflate.js +0 -528
  241. package/node_modules/ws/lib/receiver.js +0 -706
  242. package/node_modules/ws/lib/sender.js +0 -602
  243. package/node_modules/ws/lib/stream.js +0 -161
  244. package/node_modules/ws/lib/subprotocol.js +0 -62
  245. package/node_modules/ws/lib/validation.js +0 -152
  246. package/node_modules/ws/lib/websocket-server.js +0 -554
  247. package/node_modules/ws/lib/websocket.js +0 -1393
  248. package/node_modules/ws/package.json +0 -69
  249. package/node_modules/ws/wrapper.mjs +0 -8
  250. package/scripts/postinstall-link-sdk.js +0 -126
  251. package/skills/qqbot-media/SKILL.md +0 -60
  252. package/src/admin-resolver.ts +0 -181
  253. package/src/api.ts +0 -1319
  254. package/src/credential-backup.ts +0 -72
  255. package/src/deliver-debounce.ts +0 -229
  256. package/src/gateway.ts +0 -2304
  257. package/src/group-history.ts +0 -328
  258. package/src/image-server.ts +0 -675
  259. package/src/inbound-attachments.ts +0 -321
  260. package/src/known-users.ts +0 -353
  261. package/src/message-gating.ts +0 -190
  262. package/src/message-queue.ts +0 -354
  263. package/src/onboarding.ts +0 -274
  264. package/src/outbound-deliver.ts +0 -486
  265. package/src/outbound.ts +0 -1179
  266. package/src/ref-index-store.ts +0 -412
  267. package/src/reply-dispatcher.ts +0 -334
  268. package/src/session-store.ts +0 -303
  269. package/src/slash-commands.ts +0 -2297
  270. package/src/startup-greeting.ts +0 -120
  271. package/src/streaming.ts +0 -1077
  272. package/src/stt.ts +0 -86
  273. package/src/tools/channel.ts +0 -281
  274. package/src/typing-keepalive.ts +0 -59
  275. package/src/utils/audio-convert.ts +0 -859
  276. package/src/utils/chunked-upload.ts +0 -483
  277. package/src/utils/file-utils.ts +0 -193
  278. package/src/utils/image-size.ts +0 -266
  279. package/src/utils/media-send.ts +0 -636
  280. package/src/utils/media-tags.ts +0 -183
  281. package/src/utils/payload.ts +0 -265
  282. package/src/utils/text-parsing.ts +0 -85
  283. package/src/utils/upload-cache.ts +0 -128
@@ -1,412 +0,0 @@
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
- }