@cxyhhhhh/openclaw-qqbot 1.6.7-alpha.1

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 +470 -0
  3. package/README.zh.md +465 -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 +264 -0
  10. package/dist/src/api.js +777 -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 +2021 -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 +86 -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 +926 -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 +70 -0
  42. package/dist/src/ref-index-store.js +250 -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 +18 -0
  46. package/dist/src/request-context.js +30 -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 +1461 -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 +250 -0
  56. package/dist/src/streaming.js +914 -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 +248 -0
  63. package/dist/src/types.d.ts +364 -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 +34 -0
  68. package/dist/src/update-checker.js +160 -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 +59 -0
  72. package/dist/src/utils/chunked-upload.js +289 -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 +148 -0
  78. package/dist/src/utils/media-send.js +456 -0
  79. package/dist/src/utils/media-tags.d.ts +14 -0
  80. package/dist/src/utils/media-tags.js +164 -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 +51 -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 +32 -0
  90. package/dist/src/utils/text-parsing.js +69 -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 +13 -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 +69 -0
  156. package/node_modules/ws/wrapper.mjs +8 -0
  157. package/openclaw.plugin.json +17 -0
  158. package/package.json +67 -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 +113 -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 +451 -0
  168. package/scripts/upgrade-via-npm.sh +528 -0
  169. package/scripts/upgrade-via-source.sh +916 -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 +149 -0
  174. package/src/admin-resolver.ts +181 -0
  175. package/src/api.ts +1138 -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 +2257 -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 +349 -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 +1119 -0
  191. package/src/proactive.ts +530 -0
  192. package/src/ref-index-store.ts +335 -0
  193. package/src/reply-dispatcher.ts +334 -0
  194. package/src/request-context.ts +39 -0
  195. package/src/runtime.ts +14 -0
  196. package/src/session-store.ts +303 -0
  197. package/src/slash-commands.ts +1615 -0
  198. package/src/startup-greeting.ts +120 -0
  199. package/src/streaming.ts +1102 -0
  200. package/src/stt.ts +86 -0
  201. package/src/tools/channel.ts +281 -0
  202. package/src/tools/remind.ts +300 -0
  203. package/src/types.ts +386 -0
  204. package/src/typing-keepalive.ts +59 -0
  205. package/src/update-checker.ts +174 -0
  206. package/src/utils/audio-convert.ts +859 -0
  207. package/src/utils/chunked-upload.ts +419 -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 +585 -0
  211. package/src/utils/media-tags.ts +182 -0
  212. package/src/utils/payload.ts +265 -0
  213. package/src/utils/pkg-version.ts +54 -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 +75 -0
  217. package/src/utils/upload-cache.ts +128 -0
  218. package/tsconfig.json +16 -0
@@ -0,0 +1,182 @@
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
+ "(?:\\s+[a-z_-]+\\s*=\\s*[\"']?[^\"'/>>>]*?[\"']?)*" +
84
+ "\\s*/?" +
85
+ "\\s*[>>>]" +
86
+ "`?",
87
+ "gi"
88
+ );
89
+
90
+ /**
91
+ * 构建一个宽容的正则,能匹配各种畸形标签写法:
92
+ *
93
+ * 常见错误模式:
94
+ * 1. 标签名拼错:<qq_img>, <qqimage>, <image>, <img>, <pic> ...
95
+ * 2. 标签内多余空格:<qqimg >, < qqimg>, <qqimg >
96
+ * 3. 闭合标签不匹配:<qqimg>url</qqvoice>, <qqimg>url</img>
97
+ * 4. 闭合标签缺失斜杠:<qqimg>url<qqimg> (用开头标签代替闭合标签)
98
+ * 5. 闭合标签缺失尖括号:<qqimg>url/qqimg>
99
+ * 6. 中文尖括号:<qqimg>url</qqimg> 或 <qqimg>url</qqimg>
100
+ * 7. 多余引号包裹路径:<qqimg>"path"</qqimg>
101
+ * 8. Markdown 代码块包裹:`<qqimg>path</qqimg>`
102
+ * 9. 自闭合属性语法:<qqmedia file="/path" /> (由 SELF_CLOSING_TAG_REGEX 处理)
103
+ */
104
+ const FUZZY_MEDIA_TAG_REGEX = new RegExp(
105
+ // 可选 Markdown 行内代码反引号
106
+ "`?" +
107
+ // 开头标签:允许中文/英文尖括号,标签名前后可有空格
108
+ "[<<<]\\s*(" + TAG_NAME_PATTERN + ")\\s*[>>>]" +
109
+ // 内容:非贪婪匹配,允许引号包裹
110
+ "[\"']?\\s*" +
111
+ "([^<<<>>\"'`]+?)" +
112
+ "\\s*[\"']?" +
113
+ // 闭合标签:允许各种不规范写法
114
+ "[<<<]\\s*/?\\s*(?:" + TAG_NAME_PATTERN + ")\\s*[>>>]" +
115
+ // 可选结尾反引号
116
+ "`?",
117
+ "gi"
118
+ );
119
+
120
+ /**
121
+ * 将标签名映射为标准名称
122
+ */
123
+ function resolveTagName(raw: string): typeof VALID_TAGS[number] {
124
+ const lower = raw.toLowerCase();
125
+ if ((VALID_TAGS as readonly string[]).includes(lower)) {
126
+ return lower as typeof VALID_TAGS[number];
127
+ }
128
+ return TAG_ALIASES[lower] ?? "qqimg";
129
+ }
130
+
131
+ /**
132
+ * 预清理:将富媒体标签内部的换行/回车/制表符压缩为单个空格。
133
+ *
134
+ * 部分模型会在标签内部插入 \n \r \t 等空白字符,例如:
135
+ * <qqimg>\n /path/to/file.png\n</qqimg>
136
+ * <qqimg>/path/to/\nfile.png</qqimg>
137
+ *
138
+ * 此正则匹配从开标签到闭标签之间的内容(允许跨行),
139
+ * 将内部所有 [\r\n\t] 替换为空格,然后压缩连续空格。
140
+ */
141
+ const MULTILINE_TAG_CLEANUP = new RegExp(
142
+ "([<<<]\\s*(?:" + TAG_NAME_PATTERN + ")\\s*[>>>])" +
143
+ "([\\s\\S]*?)" +
144
+ "([<<<]\\s*/?\\s*(?:" + TAG_NAME_PATTERN + ")\\s*[>>>])",
145
+ "gi"
146
+ );
147
+
148
+ /**
149
+ * 预处理 LLM 输出文本,将各种畸形/错误的富媒体标签修正为标准格式。
150
+ *
151
+ * 标准格式:<qqimg>/path/to/file</qqimg>
152
+ *
153
+ * @param text LLM 原始输出
154
+ * @returns 修正后的文本(如果没有匹配到任何标签则原样返回)
155
+ */
156
+ export function normalizeMediaTags(text: string): string {
157
+ // 第 0 步:将自闭合属性语法转换为标准包裹语法
158
+ // <qqmedia file="/path/to/file.png" /> → <qqmedia>/path/to/file.png</qqmedia>
159
+ let cleaned = text.replace(SELF_CLOSING_TAG_REGEX, (_match, rawTag: string, content: string) => {
160
+ const tag = resolveTagName(rawTag);
161
+ const trimmed = content.trim();
162
+ if (!trimmed) return _match;
163
+ const expanded = expandTilde(trimmed);
164
+ return `<${tag}>${expanded}</${tag}>`;
165
+ });
166
+
167
+ // 第 1 步:将标签内部的换行/回车/制表符压缩为空格
168
+ cleaned = cleaned.replace(MULTILINE_TAG_CLEANUP, (_m, open: string, body: string, close: string) => {
169
+ const flat = body.replace(/[\r\n\t]+/g, " ").replace(/ {2,}/g, " ");
170
+ return open + flat + close;
171
+ });
172
+
173
+ // 第 2 步:将各种畸形标签统一为标准格式
174
+ return cleaned.replace(FUZZY_MEDIA_TAG_REGEX, (_match, rawTag: string, content: string) => {
175
+ const tag = resolveTagName(rawTag);
176
+ const trimmed = content.trim();
177
+ if (!trimmed) return _match; // 空内容不处理
178
+ // 展开波浪线路径:~/Desktop/file.png → /Users/xxx/Desktop/file.png
179
+ const expanded = expandTilde(trimmed);
180
+ return `<${tag}>${expanded}</${tag}>`;
181
+ });
182
+ }
@@ -0,0 +1,265 @@
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
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 从 import.meta.url 向上遍历目录树查找 package.json 并读取 version。
3
+ * 不依赖硬编码的 "../" 层级,无论编译输出结构如何变化都能可靠找到。
4
+ */
5
+
6
+ import { fileURLToPath } from "node:url";
7
+ import { createRequire } from "node:module";
8
+ import path from "node:path";
9
+ import fs from "node:fs";
10
+
11
+ let _cached: string | null = null;
12
+
13
+ export function getPackageVersion(metaUrl?: string): string {
14
+ if (_cached !== null) return _cached;
15
+
16
+ // Strategy 1: 从调用者的 import.meta.url(或本模块)向上遍历找 package.json
17
+ const startFile = metaUrl ? fileURLToPath(metaUrl) : fileURLToPath(import.meta.url);
18
+ let dir = path.dirname(startFile);
19
+ const root = path.parse(dir).root;
20
+
21
+ while (dir !== root) {
22
+ const candidate = path.join(dir, "package.json");
23
+ try {
24
+ if (fs.existsSync(candidate)) {
25
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
26
+ // 确认是我们自己的包(避免找到其他 package.json)
27
+ if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
28
+ _cached = pkg.version as string;
29
+ return _cached;
30
+ }
31
+ }
32
+ } catch {
33
+ // ignore and try parent
34
+ }
35
+ dir = path.dirname(dir);
36
+ }
37
+
38
+ // Strategy 2: fallback 用 createRequire 尝试常见相对路径
39
+ try {
40
+ const require = createRequire(metaUrl ?? import.meta.url);
41
+ for (const rel of ["../../package.json", "../package.json", "./package.json"]) {
42
+ try {
43
+ const pkg = require(rel);
44
+ if (pkg?.version) {
45
+ _cached = pkg.version as string;
46
+ return _cached;
47
+ }
48
+ } catch { /* next */ }
49
+ }
50
+ } catch { /* fallback */ }
51
+
52
+ _cached = "unknown";
53
+ return _cached;
54
+ }