@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,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
- }