@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
package/src/types.ts ADDED
@@ -0,0 +1,386 @@
1
+ /**
2
+ * QQ Bot 配置类型
3
+ */
4
+ export interface QQBotConfig {
5
+ appId: string;
6
+ clientSecret?: string;
7
+ clientSecretFile?: string;
8
+ }
9
+
10
+ /**
11
+ * 解析后的 QQ Bot 账户
12
+ */
13
+ export interface ResolvedQQBotAccount {
14
+ accountId: string;
15
+ name?: string;
16
+ enabled: boolean;
17
+ appId: string;
18
+ clientSecret: string;
19
+ secretSource: "config" | "file" | "env" | "none";
20
+ /** 系统提示词 */
21
+ systemPrompt?: string;
22
+ /** 图床服务器公网地址 */
23
+ imageServerBaseUrl?: string;
24
+ /** 是否支持 markdown 消息(默认 true) */
25
+ markdownSupport: boolean;
26
+ config: QQBotAccountConfig;
27
+ }
28
+
29
+ /** 群消息策略:open=全响应 | allowlist=白名单 | disabled=不响应 */
30
+ export type GroupPolicy = "open" | "allowlist" | "disabled";
31
+
32
+ /** 工具策略:full=全部 | restricted=限制敏感工具 | none=禁止 */
33
+ export type ToolPolicy = "full" | "restricted" | "none";
34
+
35
+ /** 单个群的配置 */
36
+ export interface GroupConfig {
37
+ /** 是否需要 @机器人才响应(默认 true) */
38
+ requireMention?: boolean;
39
+ /**
40
+ * 是否忽略 @了其他用户但没有 @机器人的消息(默认 false)。
41
+ * 开启后,消息中 @了其他人但未 @bot 时直接丢弃(不记录历史、不触发 AI)。
42
+ */
43
+ ignoreOtherMentions?: boolean;
44
+ /** 群聊中 AI 可使用的工具范围(默认 restricted) */
45
+ toolPolicy?: ToolPolicy;
46
+ /** 群名称 */
47
+ name?: string;
48
+ /** 群消息行为 PE(未配置时使用内置默认值) */
49
+ prompt?: string;
50
+ /** 群历史消息缓存条数(0 禁用,默认 20) */
51
+ historyLimit?: number;
52
+ }
53
+
54
+ /**
55
+ * QQ Bot 账户配置
56
+ */
57
+ export interface QQBotAccountConfig {
58
+ enabled?: boolean;
59
+ name?: string;
60
+ appId?: string;
61
+ clientSecret?: string;
62
+ clientSecretFile?: string;
63
+ dmPolicy?: "open" | "pairing" | "allowlist";
64
+ allowFrom?: string[];
65
+ /** 群消息策略(默认 allowlist) */
66
+ groupPolicy?: GroupPolicy;
67
+ /** 群白名单(groupPolicy 为 allowlist 时生效) */
68
+ groupAllowFrom?: string[];
69
+ /** 群配置映射(按 groupOpenid 索引,"*" 为默认) */
70
+ groups?: Record<string, GroupConfig>;
71
+ /** 系统提示词,会添加在用户消息前面 */
72
+ systemPrompt?: string;
73
+ /** 图床服务器公网地址,用于发送图片,例如 http://your-ip:18765 */
74
+ imageServerBaseUrl?: string;
75
+ /** 是否支持 markdown 消息(默认 true,设为 false 可禁用) */
76
+ markdownSupport?: boolean;
77
+ /**
78
+ * @deprecated 请使用 audioFormatPolicy.uploadDirectFormats
79
+ * 可直接上传的音频格式(不转换为 SILK),向后兼容
80
+ */
81
+ voiceDirectUploadFormats?: string[];
82
+ /**
83
+ * 音频格式策略配置
84
+ * 统一管理入站(STT)和出站(上传)的音频格式转换行为
85
+ */
86
+ audioFormatPolicy?: AudioFormatPolicy;
87
+ /**
88
+ * 是否启用公网 URL 直传 QQ 平台(默认 true)
89
+ * 启用时:公网 URL 先直传给 QQ 开放平台的富媒体 API,平台自行拉取;失败后自动 fallback 到插件下载再 Base64 上传
90
+ * 禁用时:公网 URL 始终由插件先下载到本地,再以 Base64 上传(适用于 QQ 平台无法访问目标 URL 的场景)
91
+ */
92
+ urlDirectUpload?: boolean;
93
+ /**
94
+ * /bot-upgrade 指令返回的升级指引网址
95
+ * 默认: https://doc.weixin.qq.com/doc/w3_AKEAGQaeACgCNHrh1CbHzTAKtT2gB?scode=AJEAIQdfAAozxFEnLZAKEAGQaeACg
96
+ */
97
+ upgradeUrl?: string;
98
+ /**
99
+ * /bot-upgrade 指令的行为模式
100
+ * - "doc":展示升级文档链接(默认,安全模式)
101
+ * - "hot-reload":检测到新版本时直接执行 npm 升级脚本进行热更新
102
+ */
103
+ upgradeMode?: "doc" | "hot-reload";
104
+ /**
105
+ * 出站消息合并回复(debounce)配置
106
+ * 当短时间内收到多次 deliver 时,将文本合并为一条消息发送,避免消息轰炸
107
+ */
108
+ deliverDebounce?: DeliverDebounceConfig;
109
+ /**
110
+ * 是否启用流式消息(默认 false)
111
+ * 启用后,AI 的回复会以流式形式逐步显示在 QQ 聊天中,
112
+ * 用户可以看到文字逐字出现的打字机效果。
113
+ * 设置为 true 可开启流式消息。
114
+ *
115
+ * 注意:仅 C2C(私聊)支持流式消息 API。
116
+ */
117
+ streaming?: boolean;
118
+ }
119
+
120
+ /**
121
+ * 出站消息合并回复配置
122
+ */
123
+ export interface DeliverDebounceConfig {
124
+ /**
125
+ * 是否启用合并回复(默认 true)
126
+ */
127
+ enabled?: boolean;
128
+ /**
129
+ * 合并窗口时长(毫秒),在此时间内的连续 deliver 会被合并
130
+ * 默认 1500ms
131
+ */
132
+ windowMs?: number;
133
+ /**
134
+ * 最大等待时长(毫秒),从第一条 deliver 开始计算,超过此时间强制发送
135
+ * 防止持续有新 deliver 导致一直不发送
136
+ * 默认 8000ms
137
+ */
138
+ maxWaitMs?: number;
139
+ /**
140
+ * 合并文本之间的分隔符
141
+ * 默认 "\n\n---\n\n"
142
+ */
143
+ separator?: string;
144
+ }
145
+
146
+ /**
147
+ * 音频格式策略:控制哪些格式可跳过转换
148
+ */
149
+ export interface AudioFormatPolicy {
150
+ /**
151
+ * STT 模型直接支持的音频格式(入站:跳过 SILK→WAV 转换)
152
+ * 如果 STT 服务支持直接处理某些格式(如 silk/amr),可将其加入此列表
153
+ * 例如: [".silk", ".amr", ".wav", ".mp3", ".ogg"]
154
+ * 默认为空(所有语音都先转换为 WAV 再送 STT)
155
+ */
156
+ sttDirectFormats?: string[];
157
+ /**
158
+ * QQ 平台支持直传的音频格式(出站:跳过→SILK 转换)
159
+ * 默认为 [".wav", ".mp3", ".silk"](QQ Bot API 原生支持的三种格式)
160
+ * 仅当需要覆盖默认值时才配置此项
161
+ */
162
+ uploadDirectFormats?: string[];
163
+ /**
164
+ * 是否启用语音转码(默认 true)
165
+ * 设为 false 可在环境无 ffmpeg 时跳过转码,直接以文件形式发送
166
+ * 当禁用时,非原生格式的音频会 fallback 到 sendDocument(文件发送)
167
+ */
168
+ transcodeEnabled?: boolean;
169
+ }
170
+
171
+ /**
172
+ * 富媒体附件
173
+ */
174
+ export interface MessageAttachment {
175
+ content_type: string; // 如 "image/png"
176
+ filename?: string;
177
+ height?: number;
178
+ width?: number;
179
+ size?: number;
180
+ url: string;
181
+ voice_wav_url?: string; // QQ 提供的 WAV 格式语音直链,有值时优先使用以避免 SILK→WAV 转换
182
+ asr_refer_text?: string; // QQ 事件内置 ASR 语音识别文本
183
+ }
184
+
185
+ /**
186
+ * C2C 消息事件
187
+ */
188
+ export interface C2CMessageEvent {
189
+ author: {
190
+ id: string;
191
+ union_openid: string;
192
+ user_openid: string;
193
+ };
194
+ content: string;
195
+ id: string;
196
+ timestamp: string;
197
+ message_scene?: {
198
+ source: string;
199
+ /** ext 数组,可能包含 ref_msg_idx=REFIDX_xxx(引用的消息)和 msg_idx=REFIDX_xxx(自身索引) */
200
+ ext?: string[];
201
+ };
202
+ attachments?: MessageAttachment[];
203
+ }
204
+
205
+ /**
206
+ * 频道 AT 消息事件
207
+ */
208
+ export interface GuildMessageEvent {
209
+ id: string;
210
+ channel_id: string;
211
+ guild_id: string;
212
+ content: string;
213
+ timestamp: string;
214
+ author: {
215
+ id: string;
216
+ username?: string;
217
+ bot?: boolean;
218
+ };
219
+ member?: {
220
+ nick?: string;
221
+ joined_at?: string;
222
+ };
223
+ attachments?: MessageAttachment[];
224
+ }
225
+
226
+ /**
227
+ * 群聊 AT 消息事件
228
+ */
229
+ export interface GroupMessageEvent {
230
+ author: {
231
+ id: string;
232
+ member_openid: string;
233
+ username?: string;
234
+ bot?: boolean;
235
+ };
236
+ content: string;
237
+ id: string;
238
+ timestamp: string;
239
+ group_id: string;
240
+ group_openid: string;
241
+ message_scene?: {
242
+ source: string;
243
+ ext?: string[];
244
+ };
245
+ attachments?: MessageAttachment[];
246
+ /** @提及列表 */
247
+ mentions?: Array<{
248
+ scope?: "all" | "single";
249
+ id?: string;
250
+ user_openid?: string;
251
+ member_openid?: string;
252
+ nickname?: string;
253
+ bot?: boolean;
254
+ /** 是否 @机器人自身 */
255
+ is_you?: boolean;
256
+ }>;
257
+ }
258
+
259
+ /**
260
+ * 按钮交互事件(INTERACTION_CREATE)
261
+ */
262
+ export interface InteractionEvent {
263
+ /** 事件 ID,用于回应交互(PUT /interactions/{id}) */
264
+ id: string;
265
+ /** 事件类型:11=消息按钮 12=单聊快捷菜单 */
266
+ type: number;
267
+ /** 场景:c2c / group / guild */
268
+ scene?: string;
269
+ /** 场景类型:0=频道 1=群聊 2=单聊 */
270
+ chat_type?: number;
271
+ /** 触发时间 RFC3339 */
272
+ timestamp?: string;
273
+ /** 频道 openid(仅频道场景) */
274
+ guild_id?: string;
275
+ /** 子频道 openid(仅频道场景) */
276
+ channel_id?: string;
277
+ /** 单聊用户 openid(仅 c2c 场景) */
278
+ user_openid?: string;
279
+ /** 群 openid(仅群聊场景) */
280
+ group_openid?: string;
281
+ /** 群内触发用户 openid(仅群聊场景) */
282
+ group_member_openid?: string;
283
+ version: number;
284
+ data: {
285
+ type: number;
286
+ resolved: {
287
+ /** 按钮 action.data 值 */
288
+ button_data?: string;
289
+ /** 按钮 id */
290
+ button_id?: string;
291
+ /** 操作用户 userid(仅频道场景) */
292
+ user_id?: string;
293
+ /** 自定义菜单 id(仅菜单场景) */
294
+ feature_id?: string;
295
+ /** 操作的消息 id(仅频道场景) */
296
+ message_id?: string;
297
+ /** 配置更新:群消息模式 "mention"=@机器人时激活 "always"=总是激活 */
298
+ require_mention?: string;
299
+ /** 配置更新:群消息策略 */
300
+ group_policy?: GroupPolicy;
301
+ /** 配置更新:@文本的名称提及BOT名,多个使用,分隔 */
302
+ mention_patterns?: string;
303
+ };
304
+ };
305
+ }
306
+
307
+ /**
308
+ * WebSocket 事件负载
309
+ */
310
+ export interface WSPayload {
311
+ op: number;
312
+ d?: unknown;
313
+ s?: number;
314
+ t?: string;
315
+ }
316
+
317
+
318
+
319
+ // ---- 流式消息常量 ----
320
+
321
+ /** 流式消息输入模式 */
322
+ export const StreamInputMode = {
323
+ /** 每次发送的 content_raw 替换整条消息内容 */
324
+ REPLACE: "replace",
325
+ } as const;
326
+ export type StreamInputMode = (typeof StreamInputMode)[keyof typeof StreamInputMode];
327
+
328
+ /** 流式消息输入状态 */
329
+ export const StreamInputState = {
330
+ /** 正文生成中 */
331
+ GENERATING: 1,
332
+ /** 正文生成结束(终结状态) */
333
+ DONE: 10,
334
+ } as const;
335
+ export type StreamInputState = (typeof StreamInputState)[keyof typeof StreamInputState];
336
+
337
+ /** 流式消息内容类型 */
338
+ export const StreamContentType = {
339
+ MARKDOWN: "markdown",
340
+ } as const;
341
+ export type StreamContentType = (typeof StreamContentType)[keyof typeof StreamContentType];
342
+
343
+ /**
344
+ * 流式消息请求体
345
+ * 对应 StreamReq proto
346
+ */
347
+ export interface StreamMessageRequest {
348
+ /** 输入模式 */
349
+ input_mode: StreamInputMode;
350
+ /** 输入状态 */
351
+ input_state: StreamInputState;
352
+ /** 内容类型 */
353
+ content_type: StreamContentType;
354
+ /** markdown 内容 */
355
+ content_raw: string;
356
+ /** 事件 ID */
357
+ event_id: string;
358
+ /** 原始消息 ID */
359
+ msg_id: string;
360
+ /** 流式消息 ID,首次发送后返回,后续分片需携带 */
361
+ stream_msg_id?: string;
362
+ /** 递增序号 */
363
+ msg_seq: number;
364
+ /** 同一条流式会话内的发送索引,从 0 开始,每次发送前递增;新流式会话重新从 0 开始 */
365
+ index: number;
366
+ }
367
+
368
+ /**
369
+ * 流式消息响应体
370
+ * 对应 StreamRsp proto
371
+ *
372
+ * 成功时返回:{ id, timestamp, extInfo }(无 code/message)
373
+ * 失败时返回:{ code, message }(code > 0)
374
+ */
375
+ export interface StreamMessageResponse {
376
+ /** 错误码,仅失败时存在(> 0 表示失败);成功时不存在 */
377
+ code?: number;
378
+ /** 错误信息,仅失败时存在 */
379
+ message?: string;
380
+ /** 流式消息 ID */
381
+ id?: string;
382
+ /** 时间戳 */
383
+ timestamp?: string;
384
+ /** 扩展信息 */
385
+ extInfo?: Record<string, unknown>;
386
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 输入状态自动续期
3
+ * 在消息处理期间定时续发 "正在输入" 状态通知,确保用户持续看到 bot 在处理中。
4
+ * 仅 C2C 私聊有效(QQ 群聊 API 不支持输入状态通知)。
5
+ */
6
+
7
+ import { sendC2CInputNotify } from "./api.js";
8
+
9
+ // 每 50 秒续发一次(QQ API input_second=60,留 10s 余量)
10
+ export const TYPING_INTERVAL_MS = 50_000;
11
+ export const TYPING_INPUT_SECOND = 60;
12
+
13
+ export class TypingKeepAlive {
14
+ private timer: ReturnType<typeof setInterval> | null = null;
15
+ private stopped = false;
16
+
17
+ constructor(
18
+ private readonly getToken: () => Promise<string>,
19
+ private readonly clearCache: () => void,
20
+ private readonly openid: string,
21
+ private readonly msgId: string | undefined,
22
+ private readonly log?: { info: (msg: string) => void; error: (msg: string) => void; debug?: (msg: string) => void },
23
+ private readonly logPrefix = "[qqbot]",
24
+ ) {}
25
+
26
+ /** 启动定时续期(首次发送由调用方自行处理,这里只负责后续续期) */
27
+ start(): void {
28
+ if (this.stopped) return;
29
+ this.timer = setInterval(() => {
30
+ if (this.stopped) { this.stop(); return; }
31
+ this.send().catch(() => {});
32
+ }, TYPING_INTERVAL_MS);
33
+ }
34
+
35
+ /** 停止续期 */
36
+ stop(): void {
37
+ this.stopped = true;
38
+ if (this.timer) {
39
+ clearInterval(this.timer);
40
+ this.timer = null;
41
+ }
42
+ }
43
+
44
+ private async send(): Promise<void> {
45
+ try {
46
+ const token = await this.getToken();
47
+ await sendC2CInputNotify(token, this.openid, this.msgId, TYPING_INPUT_SECOND);
48
+ this.log?.debug?.(`${this.logPrefix} Typing keep-alive sent to ${this.openid}`);
49
+ } catch (err) {
50
+ try {
51
+ this.clearCache();
52
+ const token = await this.getToken();
53
+ await sendC2CInputNotify(token, this.openid, this.msgId, TYPING_INPUT_SECOND);
54
+ } catch {
55
+ this.log?.debug?.(`${this.logPrefix} Typing keep-alive failed for ${this.openid}: ${err}`);
56
+ }
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * 版本检查器
3
+ *
4
+ * - triggerUpdateCheck(): gateway 启动时调用,后台预热缓存
5
+ * - getUpdateInfo(): 每次实时查询 npm registry,返回最新结果
6
+ *
7
+ * 使用 HTTPS 直接请求 npm registry API(不依赖 npm CLI),
8
+ * 支持多 registry fallback:npmjs.org → npmmirror.com,解决国内网络问题。
9
+ */
10
+
11
+ import { createRequire } from "node:module";
12
+ import https from "node:https";
13
+ import { getPackageVersion } from "./utils/pkg-version.js";
14
+
15
+ const require = createRequire(import.meta.url);
16
+
17
+ const PKG_NAME = "@tencent-connect/openclaw-qqbot";
18
+ const ENCODED_PKG = encodeURIComponent(PKG_NAME);
19
+
20
+ const REGISTRIES = [
21
+ `https://registry.npmjs.org/${ENCODED_PKG}`,
22
+ `https://registry.npmmirror.com/${ENCODED_PKG}`,
23
+ ];
24
+
25
+ let CURRENT_VERSION = getPackageVersion(import.meta.url);
26
+
27
+ export interface UpdateInfo {
28
+ current: string;
29
+ /** 最佳升级目标(prerelease 用户优先 alpha,稳定版用户取 latest) */
30
+ latest: string | null;
31
+ /** 稳定版 dist-tag */
32
+ stable: string | null;
33
+ /** alpha dist-tag */
34
+ alpha: string | null;
35
+ hasUpdate: boolean;
36
+ checkedAt: number;
37
+ error?: string;
38
+ }
39
+
40
+ let _log: { info: (msg: string) => void; error: (msg: string) => void; debug?: (msg: string) => void } | undefined;
41
+
42
+ function fetchJson(url: string, timeoutMs: number): Promise<any> {
43
+ return new Promise((resolve, reject) => {
44
+ const req = https.get(url, { timeout: timeoutMs, headers: { Accept: "application/json" } }, (res) => {
45
+ if (res.statusCode !== 200) {
46
+ res.resume();
47
+ reject(new Error(`HTTP ${res.statusCode} from ${url}`));
48
+ return;
49
+ }
50
+ let data = "";
51
+ res.on("data", (chunk: string) => { data += chunk; });
52
+ res.on("end", () => {
53
+ try { resolve(JSON.parse(data)); } catch (e) { reject(e); }
54
+ });
55
+ });
56
+ req.on("error", reject);
57
+ req.on("timeout", () => { req.destroy(); reject(new Error(`timeout fetching ${url}`)); });
58
+ });
59
+ }
60
+
61
+ async function fetchDistTags(): Promise<Record<string, string>> {
62
+ for (const url of REGISTRIES) {
63
+ try {
64
+ const json = await fetchJson(url, 10_000);
65
+ const tags = json["dist-tags"];
66
+ if (tags && typeof tags === "object") return tags;
67
+ } catch (e: any) {
68
+ _log?.debug?.(`[qqbot:update-checker] ${url} failed: ${e.message}`);
69
+ }
70
+ }
71
+ throw new Error("all registries failed");
72
+ }
73
+
74
+ function buildUpdateInfo(tags: Record<string, string>): UpdateInfo {
75
+ const currentIsPrerelease = CURRENT_VERSION.includes("-");
76
+ const stableTag = tags.latest || null;
77
+ const alphaTag = tags.alpha || null;
78
+
79
+ // 严格隔离:alpha 只跟 alpha 比,正式版只跟正式版比,不交叉
80
+ const compareTarget = currentIsPrerelease ? alphaTag : stableTag;
81
+
82
+ const hasUpdate = typeof compareTarget === "string"
83
+ && compareTarget !== CURRENT_VERSION
84
+ && compareVersions(compareTarget, CURRENT_VERSION) > 0;
85
+
86
+ return {
87
+ current: CURRENT_VERSION,
88
+ latest: compareTarget,
89
+ stable: stableTag,
90
+ alpha: alphaTag,
91
+ hasUpdate,
92
+ checkedAt: Date.now(),
93
+ };
94
+ }
95
+
96
+ /** gateway 启动时调用,保存 log 引用 */
97
+ export function triggerUpdateCheck(log?: {
98
+ info: (msg: string) => void;
99
+ error: (msg: string) => void;
100
+ debug?: (msg: string) => void;
101
+ }): void {
102
+ if (log) _log = log;
103
+ // 预热:fire-and-forget
104
+ getUpdateInfo().then((info) => {
105
+ if (info.hasUpdate) {
106
+ _log?.info?.(`[qqbot:update-checker] new version available: ${info.latest} (current: ${CURRENT_VERSION})`);
107
+ }
108
+ }).catch(() => {});
109
+ }
110
+
111
+ /** 每次实时查询 npm registry */
112
+ export async function getUpdateInfo(): Promise<UpdateInfo> {
113
+ try {
114
+ const tags = await fetchDistTags();
115
+ return buildUpdateInfo(tags);
116
+ } catch (err: any) {
117
+ _log?.debug?.(`[qqbot:update-checker] check failed: ${err.message}`);
118
+ return { current: CURRENT_VERSION, latest: null, stable: null, alpha: null, hasUpdate: false, checkedAt: Date.now(), error: err.message };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * 检查指定版本是否存在于 npm registry
124
+ * 用于 /bot-upgrade --version 的前置校验
125
+ */
126
+ export async function checkVersionExists(version: string): Promise<boolean> {
127
+ for (const baseUrl of REGISTRIES) {
128
+ try {
129
+ const url = `${baseUrl}/${version}`;
130
+ const json = await fetchJson(url, 10_000);
131
+ if (json && json.version === version) return true;
132
+ } catch {
133
+ // try next registry
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+
139
+ function compareVersions(a: string, b: string): number {
140
+ const parse = (v: string) => {
141
+ const clean = v.replace(/^v/, "");
142
+ const [main, pre] = clean.split("-", 2);
143
+ return { parts: main.split(".").map(Number), pre: pre || null };
144
+ };
145
+ const pa = parse(a);
146
+ const pb = parse(b);
147
+ // 先比主版本号
148
+ for (let i = 0; i < 3; i++) {
149
+ const diff = (pa.parts[i] || 0) - (pb.parts[i] || 0);
150
+ if (diff !== 0) return diff;
151
+ }
152
+ // 主版本号相同:正式版 > prerelease
153
+ if (!pa.pre && pb.pre) return 1;
154
+ if (pa.pre && !pb.pre) return -1;
155
+ if (!pa.pre && !pb.pre) return 0;
156
+ // 都是 prerelease:按段逐一比较(alpha.1 vs alpha.2)
157
+ const aParts = pa.pre!.split(".");
158
+ const bParts = pb.pre!.split(".");
159
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
160
+ const aP = aParts[i] ?? "";
161
+ const bP = bParts[i] ?? "";
162
+ const aNum = Number(aP);
163
+ const bNum = Number(bP);
164
+ // 都是数字则按数字比较
165
+ if (!isNaN(aNum) && !isNaN(bNum)) {
166
+ if (aNum !== bNum) return aNum - bNum;
167
+ } else {
168
+ // 字符串比较
169
+ if (aP < bP) return -1;
170
+ if (aP > bP) return 1;
171
+ }
172
+ }
173
+ return 0;
174
+ }