@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,183 +0,0 @@
1
- /**
2
- * 富媒体标签预处理与纠错
3
- *
4
- * 小模型常见的标签拼写错误及变体,在正则匹配前统一修正为标准格式。
5
- */
6
-
7
- import { expandTilde } from "./platform.js";
8
-
9
- // 标准标签名(qqmedia = 统一标签,系统根据文件扩展名自动路由)
10
- const VALID_TAGS = ["qqimg", "qqvoice", "qqvideo", "qqfile", "qqmedia"] as const;
11
-
12
- // 开头标签别名映射(key 全部小写)
13
- const TAG_ALIASES: Record<string, typeof VALID_TAGS[number]> = {
14
- // ---- qqimg 变体 ----
15
- "qq_img": "qqimg",
16
- "qqimage": "qqimg",
17
- "qq_image": "qqimg",
18
- "qqpic": "qqimg",
19
- "qq_pic": "qqimg",
20
- "qqpicture": "qqimg",
21
- "qq_picture": "qqimg",
22
- "qqphoto": "qqimg",
23
- "qq_photo": "qqimg",
24
- "img": "qqimg",
25
- "image": "qqimg",
26
- "pic": "qqimg",
27
- "picture": "qqimg",
28
- "photo": "qqimg",
29
- // ---- qqvoice 变体 ----
30
- "qq_voice": "qqvoice",
31
- "qqaudio": "qqvoice",
32
- "qq_audio": "qqvoice",
33
- "voice": "qqvoice",
34
- "audio": "qqvoice",
35
- // ---- qqvideo 变体 ----
36
- "qq_video": "qqvideo",
37
- "video": "qqvideo",
38
- // ---- qqfile 变体 ----
39
- "qq_file": "qqfile",
40
- "qqdoc": "qqfile",
41
- "qq_doc": "qqfile",
42
- "file": "qqfile",
43
- "doc": "qqfile",
44
- "document": "qqfile",
45
- // ---- qqmedia 变体(统一标签,根据扩展名自动路由) ----
46
- "qq_media": "qqmedia",
47
- "media": "qqmedia",
48
- "attachment": "qqmedia",
49
- "attach": "qqmedia",
50
- "qqattachment": "qqmedia",
51
- "qq_attachment": "qqmedia",
52
- "qqsend": "qqmedia",
53
- "qq_send": "qqmedia",
54
- "send": "qqmedia",
55
- };
56
-
57
- // 构建所有可识别的标签名列表(标准名 + 别名)
58
- const ALL_TAG_NAMES = [...VALID_TAGS, ...Object.keys(TAG_ALIASES)];
59
- // 按长度降序排列,优先匹配更长的名称(避免 "img" 抢先匹配 "qqimg" 的子串)
60
- ALL_TAG_NAMES.sort((a, b) => b.length - a.length);
61
-
62
- const TAG_NAME_PATTERN = ALL_TAG_NAMES.join("|");
63
-
64
- /**
65
- * 自闭合属性语法的正则:
66
- * <qqmedia file="/path/to/file.png" />
67
- * <qqimg src="/path" />
68
- * <image file="..." />
69
- * <qqmedia type="file" path="/path/to/file.zip" /> ← 多属性
70
- * 支持 file= / src= / path= / url= 属性名,引号可选
71
- * 也支持前面有其他属性(如 type="file")的多属性写法
72
- */
73
- const SELF_CLOSING_TAG_REGEX = new RegExp(
74
- "`?" +
75
- "[<<<]\\s*(" + TAG_NAME_PATTERN + ")" +
76
- // 允许前面有任意其他属性(如 type="file"),非贪婪跳过
77
- "(?:\\s+(?!file|src|path|url)[a-z_-]+\\s*=\\s*[\"']?[^\"'/>>>]*?[\"']?)*" +
78
- "\\s+(?:file|src|path|url)\\s*=\\s*" +
79
- "[\"']?" +
80
- // 注意:文件路径包含 /,不能在字符类中排除 /
81
- "([^\"'>>]+?)" +
82
- "[\"']?" +
83
- // 允许后面还有其他属性
84
- "(?:\\s+[a-z_-]+\\s*=\\s*[\"']?[^\"'/>>>]*?[\"']?)*" +
85
- "\\s*/?" +
86
- "\\s*[>>>]" +
87
- "`?",
88
- "gi"
89
- );
90
-
91
- /**
92
- * 构建一个宽容的正则,能匹配各种畸形标签写法:
93
- *
94
- * 常见错误模式:
95
- * 1. 标签名拼错:<qq_img>, <qqimage>, <image>, <img>, <pic> ...
96
- * 2. 标签内多余空格:<qqimg >, < qqimg>, <qqimg >
97
- * 3. 闭合标签不匹配:<qqimg>url</qqvoice>, <qqimg>url</img>
98
- * 4. 闭合标签缺失斜杠:<qqimg>url<qqimg> (用开头标签代替闭合标签)
99
- * 5. 闭合标签缺失尖括号:<qqimg>url/qqimg>
100
- * 6. 中文尖括号:<qqimg>url</qqimg> 或 <qqimg>url</qqimg>
101
- * 7. 多余引号包裹路径:<qqimg>"path"</qqimg>
102
- * 8. Markdown 代码块包裹:`<qqimg>path</qqimg>`
103
- * 9. 自闭合属性语法:<qqmedia file="/path" /> (由 SELF_CLOSING_TAG_REGEX 处理)
104
- */
105
- const FUZZY_MEDIA_TAG_REGEX = new RegExp(
106
- // 可选 Markdown 行内代码反引号
107
- "`?" +
108
- // 开头标签:允许中文/英文尖括号,标签名前后可有空格
109
- "[<<<]\\s*(" + TAG_NAME_PATTERN + ")\\s*[>>>]" +
110
- // 内容:非贪婪匹配,允许引号包裹
111
- "[\"']?\\s*" +
112
- "([^<<<>>\"'`]+?)" +
113
- "\\s*[\"']?" +
114
- // 闭合标签:允许各种不规范写法
115
- "[<<<]\\s*/?\\s*(?:" + TAG_NAME_PATTERN + ")\\s*[>>>]" +
116
- // 可选结尾反引号
117
- "`?",
118
- "gi"
119
- );
120
-
121
- /**
122
- * 将标签名映射为标准名称
123
- */
124
- function resolveTagName(raw: string): typeof VALID_TAGS[number] {
125
- const lower = raw.toLowerCase();
126
- if ((VALID_TAGS as readonly string[]).includes(lower)) {
127
- return lower as typeof VALID_TAGS[number];
128
- }
129
- return TAG_ALIASES[lower] ?? "qqimg";
130
- }
131
-
132
- /**
133
- * 预清理:将富媒体标签内部的换行/回车/制表符压缩为单个空格。
134
- *
135
- * 部分模型会在标签内部插入 \n \r \t 等空白字符,例如:
136
- * <qqimg>\n /path/to/file.png\n</qqimg>
137
- * <qqimg>/path/to/\nfile.png</qqimg>
138
- *
139
- * 此正则匹配从开标签到闭标签之间的内容(允许跨行),
140
- * 将内部所有 [\r\n\t] 替换为空格,然后压缩连续空格。
141
- */
142
- const MULTILINE_TAG_CLEANUP = new RegExp(
143
- "([<<<]\\s*(?:" + TAG_NAME_PATTERN + ")\\s*[>>>])" +
144
- "([\\s\\S]*?)" +
145
- "([<<<]\\s*/?\\s*(?:" + TAG_NAME_PATTERN + ")\\s*[>>>])",
146
- "gi"
147
- );
148
-
149
- /**
150
- * 预处理 LLM 输出文本,将各种畸形/错误的富媒体标签修正为标准格式。
151
- *
152
- * 标准格式:<qqimg>/path/to/file</qqimg>
153
- *
154
- * @param text LLM 原始输出
155
- * @returns 修正后的文本(如果没有匹配到任何标签则原样返回)
156
- */
157
- export function normalizeMediaTags(text: string): string {
158
- // 第 0 步:将自闭合属性语法转换为标准包裹语法
159
- // <qqmedia file="/path/to/file.png" /> → <qqmedia>/path/to/file.png</qqmedia>
160
- let cleaned = text.replace(SELF_CLOSING_TAG_REGEX, (_match, rawTag: string, content: string) => {
161
- const tag = resolveTagName(rawTag);
162
- const trimmed = content.trim();
163
- if (!trimmed) return _match;
164
- const expanded = expandTilde(trimmed);
165
- return `<${tag}>${expanded}</${tag}>`;
166
- });
167
-
168
- // 第 1 步:将标签内部的换行/回车/制表符压缩为空格
169
- cleaned = cleaned.replace(MULTILINE_TAG_CLEANUP, (_m, open: string, body: string, close: string) => {
170
- const flat = body.replace(/[\r\n\t]+/g, " ").replace(/ {2,}/g, " ");
171
- return open + flat + close;
172
- });
173
-
174
- // 第 2 步:将各种畸形标签统一为标准格式
175
- return cleaned.replace(FUZZY_MEDIA_TAG_REGEX, (_match, rawTag: string, content: string) => {
176
- const tag = resolveTagName(rawTag);
177
- const trimmed = content.trim();
178
- if (!trimmed) return _match; // 空内容不处理
179
- // 展开波浪线路径:~/Desktop/file.png → /Users/xxx/Desktop/file.png
180
- const expanded = expandTilde(trimmed);
181
- return `<${tag}>${expanded}</${tag}>`;
182
- });
183
- }
@@ -1,265 +0,0 @@
1
- /**
2
- * QQBot 结构化消息载荷工具
3
- *
4
- * 用于处理 AI 输出的结构化消息载荷,包括:
5
- * - 定时提醒载荷 (cron_reminder)
6
- * - 媒体消息载荷 (media)
7
- */
8
-
9
- // ============================================
10
- // 类型定义
11
- // ============================================
12
-
13
- /**
14
- * 定时提醒载荷
15
- */
16
- export interface CronReminderPayload {
17
- type: 'cron_reminder';
18
- /** 提醒内容 */
19
- content: string;
20
- /** 目标类型:c2c (私聊) 或 group (群聊) */
21
- targetType: 'c2c' | 'group';
22
- /** 目标地址:user_openid 或 group_openid */
23
- targetAddress: string;
24
- /** 原始消息 ID(可选) */
25
- originalMessageId?: string;
26
- }
27
-
28
- /**
29
- * 媒体消息载荷
30
- */
31
- export interface MediaPayload {
32
- type: 'media';
33
- /** 媒体类型:image, audio, video, file */
34
- mediaType: 'image' | 'audio' | 'video' | 'file';
35
- /** 来源类型:url 或 file */
36
- source: 'url' | 'file';
37
- /** 媒体路径或 URL */
38
- path: string;
39
- /** 媒体描述(可选) */
40
- caption?: string;
41
- }
42
-
43
- /**
44
- * QQBot 载荷联合类型
45
- */
46
- export type QQBotPayload = CronReminderPayload | MediaPayload;
47
-
48
- /**
49
- * 解析结果
50
- */
51
- export interface ParseResult {
52
- /** 是否为结构化载荷 */
53
- isPayload: boolean;
54
- /** 解析后的载荷对象(如果是结构化载荷) */
55
- payload?: QQBotPayload;
56
- /** 原始文本(如果不是结构化载荷) */
57
- text?: string;
58
- /** 解析错误信息(如果解析失败) */
59
- error?: string;
60
- }
61
-
62
- // ============================================
63
- // 常量定义
64
- // ============================================
65
-
66
- /** AI 输出的结构化载荷前缀 */
67
- const PAYLOAD_PREFIX = 'QQBOT_PAYLOAD:';
68
-
69
- /** Cron 消息存储的前缀 */
70
- const CRON_PREFIX = 'QQBOT_CRON:';
71
-
72
- // ============================================
73
- // 解析函数
74
- // ============================================
75
-
76
- /**
77
- * 解析 AI 输出的结构化载荷
78
- *
79
- * 检测消息是否以 QQBOT_PAYLOAD: 前缀开头,如果是则提取并解析 JSON
80
- *
81
- * @param text AI 输出的原始文本
82
- * @returns 解析结果
83
- *
84
- * @example
85
- * const result = parseQQBotPayload('QQBOT_PAYLOAD:\n{"type": "media", "mediaType": "image", ...}');
86
- * if (result.isPayload && result.payload) {
87
- * // 处理结构化载荷
88
- * }
89
- */
90
- export function parseQQBotPayload(text: string): ParseResult {
91
- const trimmedText = text.trim();
92
-
93
- // 检查是否以 QQBOT_PAYLOAD: 开头
94
- if (!trimmedText.startsWith(PAYLOAD_PREFIX)) {
95
- return {
96
- isPayload: false,
97
- text: text
98
- };
99
- }
100
-
101
- // 提取 JSON 内容(去掉前缀)
102
- const jsonContent = trimmedText.slice(PAYLOAD_PREFIX.length).trim();
103
-
104
- if (!jsonContent) {
105
- return {
106
- isPayload: true,
107
- error: '载荷内容为空'
108
- };
109
- }
110
-
111
- try {
112
- const payload = JSON.parse(jsonContent) as QQBotPayload;
113
-
114
- // 验证必要字段
115
- if (!payload.type) {
116
- return {
117
- isPayload: true,
118
- error: '载荷缺少 type 字段'
119
- };
120
- }
121
-
122
- // 根据 type 进行额外验证
123
- if (payload.type === 'cron_reminder') {
124
- if (!payload.content || !payload.targetType || !payload.targetAddress) {
125
- return {
126
- isPayload: true,
127
- error: 'cron_reminder 载荷缺少必要字段 (content, targetType, targetAddress)'
128
- };
129
- }
130
- } else if (payload.type === 'media') {
131
- if (!payload.mediaType || !payload.source || !payload.path) {
132
- return {
133
- isPayload: true,
134
- error: 'media 载荷缺少必要字段 (mediaType, source, path)'
135
- };
136
- }
137
- }
138
-
139
- return {
140
- isPayload: true,
141
- payload
142
- };
143
- } catch (e) {
144
- return {
145
- isPayload: true,
146
- error: `JSON 解析失败: ${e instanceof Error ? e.message : String(e)}`
147
- };
148
- }
149
- }
150
-
151
- // ============================================
152
- // Cron 编码/解码函数
153
- // ============================================
154
-
155
- /**
156
- * 将定时提醒载荷编码为 Cron 消息格式
157
- *
158
- * 将 JSON 编码为 Base64,并添加 QQBOT_CRON: 前缀
159
- *
160
- * @param payload 定时提醒载荷
161
- * @returns 编码后的消息字符串,格式为 QQBOT_CRON:{base64}
162
- *
163
- * @example
164
- * const message = encodePayloadForCron({
165
- * type: 'cron_reminder',
166
- * content: '喝水时间到!',
167
- * targetType: 'c2c',
168
- * targetAddress: 'user_openid_xxx'
169
- * });
170
- * // 返回: QQBOT_CRON:eyJ0eXBlIjoiY3Jvbl9yZW1pbmRlciIs...
171
- */
172
- export function encodePayloadForCron(payload: CronReminderPayload): string {
173
- const jsonString = JSON.stringify(payload);
174
- const base64 = Buffer.from(jsonString, 'utf-8').toString('base64');
175
- return `${CRON_PREFIX}${base64}`;
176
- }
177
-
178
- /**
179
- * 解码 Cron 消息中的载荷
180
- *
181
- * 检测 QQBOT_CRON: 前缀,解码 Base64 并解析 JSON
182
- *
183
- * @param message Cron 触发时收到的消息
184
- * @returns 解码结果,包含是否为 Cron 载荷、解析后的载荷对象或错误信息
185
- *
186
- * @example
187
- * const result = decodeCronPayload('QQBOT_CRON:eyJ0eXBlIjoiY3Jvbl9yZW1pbmRlciIs...');
188
- * if (result.isCronPayload && result.payload) {
189
- * // 处理定时提醒
190
- * }
191
- */
192
- export function decodeCronPayload(message: string): {
193
- isCronPayload: boolean;
194
- payload?: CronReminderPayload;
195
- error?: string;
196
- } {
197
- const trimmedMessage = message.trim();
198
-
199
- // 检查是否以 QQBOT_CRON: 开头
200
- if (!trimmedMessage.startsWith(CRON_PREFIX)) {
201
- return {
202
- isCronPayload: false
203
- };
204
- }
205
-
206
- // 提取 Base64 内容
207
- const base64Content = trimmedMessage.slice(CRON_PREFIX.length);
208
-
209
- if (!base64Content) {
210
- return {
211
- isCronPayload: true,
212
- error: 'Cron 载荷内容为空'
213
- };
214
- }
215
-
216
- try {
217
- // Base64 解码
218
- const jsonString = Buffer.from(base64Content, 'base64').toString('utf-8');
219
- const payload = JSON.parse(jsonString) as CronReminderPayload;
220
-
221
- // 验证类型
222
- if (payload.type !== 'cron_reminder') {
223
- return {
224
- isCronPayload: true,
225
- error: `期望 type 为 cron_reminder,实际为 ${payload.type}`
226
- };
227
- }
228
-
229
- // 验证必要字段
230
- if (!payload.content || !payload.targetType || !payload.targetAddress) {
231
- return {
232
- isCronPayload: true,
233
- error: 'Cron 载荷缺少必要字段'
234
- };
235
- }
236
-
237
- return {
238
- isCronPayload: true,
239
- payload
240
- };
241
- } catch (e) {
242
- return {
243
- isCronPayload: true,
244
- error: `Cron 载荷解码失败: ${e instanceof Error ? e.message : String(e)}`
245
- };
246
- }
247
- }
248
-
249
- // ============================================
250
- // 辅助函数
251
- // ============================================
252
-
253
- /**
254
- * 判断载荷是否为定时提醒类型
255
- */
256
- export function isCronReminderPayload(payload: QQBotPayload): payload is CronReminderPayload {
257
- return payload.type === 'cron_reminder';
258
- }
259
-
260
- /**
261
- * 判断载荷是否为媒体消息类型
262
- */
263
- export function isMediaPayload(payload: QQBotPayload): payload is MediaPayload {
264
- return payload.type === 'media';
265
- }
@@ -1,85 +0,0 @@
1
- /**
2
- * QQ Bot 文本解析工具函数
3
- */
4
-
5
- import type { RefAttachmentSummary } from "../ref-index-store.js";
6
- import { inferAttachmentType } from "../group-history.js";
7
- import { MSG_TYPE_QUOTE } from "../types.js";
8
-
9
- /**
10
- * 解析 QQ 表情标签,将 <faceType=1,faceId="13",ext="base64..."> 格式
11
- * 替换为 【表情: 中文名】 格式
12
- * ext 字段为 Base64 编码的 JSON,格式如 {"text":"呲牙"}
13
- */
14
- export function parseFaceTags(text: string): string {
15
- if (!text) return text;
16
-
17
- return text.replace(/<faceType=\d+,faceId="[^"]*",ext="([^"]*)">/g, (_match, ext: string) => {
18
- try {
19
- const decoded = Buffer.from(ext, "base64").toString("utf-8");
20
- const parsed = JSON.parse(decoded);
21
- const faceName = parsed.text || "未知表情";
22
- return `【表情: ${faceName}】`;
23
- } catch {
24
- return _match;
25
- }
26
- });
27
- }
28
-
29
- /**
30
- * 过滤内部标记(如 [[reply_to: xxx]])
31
- * 这些标记可能被 AI 错误地学习并输出,需要在发送前移除
32
- */
33
- export function filterInternalMarkers(text: string): string {
34
- if (!text) return text;
35
-
36
- let result = text.replace(/\[\[[a-z_]+:\s*[^\]]*\]\]/gi, "");
37
- // 过滤框架内部图片引用标记:@image:image_xxx.png、@voice:voice_xxx.silk 等
38
- result = result.replace(/@(?:image|voice|video|file):[a-zA-Z0-9_.-]+/g, "");
39
- result = result.replace(/\n{3,}/g, "\n\n").trim();
40
-
41
- return result;
42
- }
43
-
44
- /** 从 ext 和 msg_elements 中解析引用索引,仅 MSG_TYPE_QUOTE 时取 msg_elements */
45
- export function parseRefIndices(
46
- ext?: string[],
47
- messageType?: number,
48
- msgElements?: Array<{ msg_idx?: string }>,
49
- ): { refMsgIdx?: string; msgIdx?: string } {
50
- let refMsgIdx: string | undefined;
51
- let msgIdx: string | undefined;
52
- if (ext && ext.length > 0) {
53
- for (const item of ext) {
54
- if (item.startsWith("ref_msg_idx=")) {
55
- refMsgIdx = item.slice("ref_msg_idx=".length);
56
- } else if (item.startsWith("msg_idx=")) {
57
- msgIdx = item.slice("msg_idx=".length);
58
- }
59
- }
60
- }
61
- // 仅当 message_type=MSG_TYPE_QUOTE(引用消息)时,msg_elements[0].msg_idx 更权威,有值时覆盖 ext 解析结果
62
- if (messageType === MSG_TYPE_QUOTE) {
63
- const refElement = msgElements?.[0];
64
- if (refElement?.msg_idx) {
65
- refMsgIdx = refElement.msg_idx;
66
- }
67
- }
68
- return { refMsgIdx, msgIdx };
69
- }
70
-
71
- /**
72
- * 从附件列表中构建附件摘要(用于引用索引缓存)
73
- */
74
- export function buildAttachmentSummaries(
75
- attachments?: Array<{ content_type: string; url: string; filename?: string; voice_wav_url?: string }>,
76
- localPaths?: Array<string | null>,
77
- ): RefAttachmentSummary[] | undefined {
78
- if (!attachments || attachments.length === 0) return undefined;
79
- return attachments.map((att, idx) => ({
80
- type: inferAttachmentType(att.content_type),
81
- filename: att.filename,
82
- contentType: att.content_type,
83
- localPath: localPaths?.[idx] ?? undefined,
84
- }));
85
- }
@@ -1,128 +0,0 @@
1
- /**
2
- * file_info 缓存 — 借鉴 Telegram file_id 机制
3
- *
4
- * QQ Bot API 上传文件后返回 file_info + ttl,在 TTL 内相同文件可直接复用 file_info
5
- * 避免重复上传同一文件,节省带宽和时间。
6
- *
7
- * 缓存 key = md5(fileContent) + targetType(c2c/group) + targetId + fileType
8
- */
9
-
10
- import * as crypto from "node:crypto";
11
- import * as fs from "node:fs";
12
-
13
- interface CacheEntry {
14
- fileInfo: string;
15
- fileUuid: string;
16
- /** 过期时间戳(ms),比 API 返回的 TTL 提前 60 秒失效 */
17
- expiresAt: number;
18
- }
19
-
20
- // 内存缓存,key 格式:`${contentHash}:${scope}:${targetId}:${fileType}`
21
- const cache = new Map<string, CacheEntry>();
22
-
23
- // 最大缓存条目数,防止内存泄漏
24
- const MAX_CACHE_SIZE = 500;
25
-
26
- /**
27
- * 计算文件内容的 MD5 hash(用于缓存 key)
28
- * 对于 Base64 数据直接 hash,对于文件路径读取后 hash
29
- */
30
- export function computeFileHash(data: string | Buffer): string {
31
- const content = typeof data === "string" ? data : data;
32
- return crypto.createHash("md5").update(content).digest("hex");
33
- }
34
-
35
- /**
36
- * 构建缓存 key
37
- * @param contentHash - 文件内容 hash
38
- * @param scope - "c2c" | "group"
39
- * @param targetId - 用户 openid 或群 openid
40
- * @param fileType - 1=IMAGE, 2=VIDEO, 3=VOICE, 4=FILE
41
- */
42
- function buildCacheKey(contentHash: string, scope: string, targetId: string, fileType: number): string {
43
- return `${contentHash}:${scope}:${targetId}:${fileType}`;
44
- }
45
-
46
- /**
47
- * 从缓存获取 file_info
48
- * @returns file_info 字符串,未命中或已过期返回 null
49
- */
50
- export function getCachedFileInfo(
51
- contentHash: string,
52
- scope: "c2c" | "group",
53
- targetId: string,
54
- fileType: number,
55
- ): string | null {
56
- const key = buildCacheKey(contentHash, scope, targetId, fileType);
57
- const entry = cache.get(key);
58
-
59
- if (!entry) return null;
60
-
61
- // 检查是否过期
62
- if (Date.now() >= entry.expiresAt) {
63
- cache.delete(key);
64
- return null;
65
- }
66
-
67
- console.log(`[upload-cache] Cache HIT: key=${key.slice(0, 40)}..., fileUuid=${entry.fileUuid}`);
68
- return entry.fileInfo;
69
- }
70
-
71
- /**
72
- * 将上传结果写入缓存
73
- * @param ttl - API 返回的 TTL(秒),缓存会提前 60 秒失效
74
- */
75
- export function setCachedFileInfo(
76
- contentHash: string,
77
- scope: "c2c" | "group",
78
- targetId: string,
79
- fileType: number,
80
- fileInfo: string,
81
- fileUuid: string,
82
- ttl: number,
83
- ): void {
84
- // 清理过期条目(惰性清理)
85
- if (cache.size >= MAX_CACHE_SIZE) {
86
- const now = Date.now();
87
- for (const [k, v] of cache) {
88
- if (now >= v.expiresAt) {
89
- cache.delete(k);
90
- }
91
- }
92
- // 如果清理后仍然超限,删除最早的一半
93
- if (cache.size >= MAX_CACHE_SIZE) {
94
- const keys = Array.from(cache.keys());
95
- for (let i = 0; i < keys.length / 2; i++) {
96
- cache.delete(keys[i]!);
97
- }
98
- }
99
- }
100
-
101
- const key = buildCacheKey(contentHash, scope, targetId, fileType);
102
- // 提前 60 秒失效,避免临界点过期
103
- const safetyMargin = 60;
104
- const effectiveTtl = Math.max(ttl - safetyMargin, 10);
105
-
106
- cache.set(key, {
107
- fileInfo,
108
- fileUuid,
109
- expiresAt: Date.now() + effectiveTtl * 1000,
110
- });
111
-
112
- console.log(`[upload-cache] Cache SET: key=${key.slice(0, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`);
113
- }
114
-
115
- /**
116
- * 获取缓存统计
117
- */
118
- export function getUploadCacheStats(): { size: number; maxSize: number } {
119
- return { size: cache.size, maxSize: MAX_CACHE_SIZE };
120
- }
121
-
122
- /**
123
- * 清除所有缓存
124
- */
125
- export function clearUploadCache(): void {
126
- cache.clear();
127
- console.log(`[upload-cache] Cache cleared`);
128
- }