@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
@@ -0,0 +1,149 @@
1
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
2
+ import { listQQBotAccountIds } from '../config.js';
3
+ import { getBotForAccount } from '../bot-instance.js';
4
+ import { getRequestAccountId } from '../request-context.js';
5
+
6
+ // ========== JSON Schema ==========
7
+
8
+ const PlatformApiSchema = {
9
+ type: 'object',
10
+ properties: {
11
+ method: {
12
+ type: 'string',
13
+ description:
14
+ 'HTTP 请求方法。可选值:GET, POST, PUT, PATCH, DELETE',
15
+ enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
16
+ },
17
+ path: {
18
+ type: 'string',
19
+ description:
20
+ 'API 路径(不含域名),占位符需替换为实际值。' +
21
+ '示例:/users/@me/guilds, /guilds/{guild_id}/channels, ' +
22
+ '/v2/groups/{group_id}/bot_state',
23
+ },
24
+ body: {
25
+ type: 'object',
26
+ description:
27
+ '请求体(JSON),用于 POST/PUT/PATCH 请求。' +
28
+ 'GET/DELETE 请求不需要此参数。',
29
+ },
30
+ query: {
31
+ type: 'object',
32
+ description:
33
+ 'URL 查询参数(键值对),会拼接到路径后面。' +
34
+ '如 { "limit": "100", "after": "0" } 会拼接为 ?limit=100&after=0',
35
+ additionalProperties: { type: 'string' },
36
+ },
37
+ },
38
+ required: ['method', 'path'],
39
+ } as const;
40
+
41
+ // ========== 工具函数 ==========
42
+
43
+ function json(data: unknown) {
44
+ return {
45
+ content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }],
46
+ details: data,
47
+ };
48
+ }
49
+
50
+ function validatePath(path: string): string | null {
51
+ if (!path.startsWith('/')) return "path 必须以 / 开头";
52
+ if (path.includes('..') || path.includes('//')) return "path 不允许包含 .. 或 //";
53
+ if (!/^\/[a-zA-Z0-9\-._~:@!$&'()*+,;=/%]+$/.test(path) && path !== '/') {
54
+ return 'path 包含非法字符';
55
+ }
56
+ return null;
57
+ }
58
+
59
+ // ========== 注册入口 ==========
60
+
61
+ /**
62
+ * 注册 QQBot 平台统一 API 网关工具。
63
+ * 通过 SDK 的 bot.api 网关代理所有 QQ 开放平台 HTTP 接口 — 自动鉴权、重试、结构化错误。
64
+ *
65
+ * 覆盖范围:
66
+ * - 频道(Guild/Channel/Member/Announce/Forum/Schedule)→ 见 qqbot-channel skill
67
+ * - 群(Group/Member)→ 见 qqbot-group skill
68
+ */
69
+ export function registerPlatformTool(api: OpenClawPluginApi): void {
70
+ const cfg = api.config;
71
+ if (!cfg) return;
72
+
73
+ const accountIds = listQQBotAccountIds(cfg);
74
+ if (accountIds.length === 0) return;
75
+
76
+ api.registerTool(
77
+ {
78
+ name: 'qqbot_platform_api',
79
+ label: 'QQBot Platform API Gateway',
80
+ description:
81
+ 'QQ 开放平台统一 HTTP API 网关,自动填充鉴权 Token。' +
82
+ '常用接口速查:' +
83
+ '【频道】GET /users/@me/guilds | /guilds/{guild_id}/channels | /channels/{channel_id} | ' +
84
+ '【群】GET /v2/groups/{group_id}/bot_state | /v2/groups/{group_id}/members/{member_id} | /v2/groups/{group_id}/info。' +
85
+ '更多接口和参数详情请阅读 qqbot-channel 和 qqbot-group skill。',
86
+ parameters: PlatformApiSchema,
87
+ async execute(_toolCallId, params) {
88
+ const p = params as {
89
+ method: string;
90
+ path: string;
91
+ body?: Record<string, unknown>;
92
+ query?: Record<string, string>;
93
+ };
94
+
95
+ if (!p.method) return json({ error: 'method 为必填参数' });
96
+ if (!p.path) return json({ error: 'path 为必填参数' });
97
+
98
+ const method = p.method.toUpperCase();
99
+ if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
100
+ return json({ error: `不支持的 HTTP 方法: ${method}` });
101
+ }
102
+
103
+ const pathError = validatePath(p.path);
104
+ if (pathError) return json({ error: pathError });
105
+
106
+ const accountId = getRequestAccountId();
107
+ if (!accountId) {
108
+ return json({ error: '无法获取当前请求的账号信息,此工具仅支持在消息会话中使用' });
109
+ }
110
+
111
+ try {
112
+ const bot = getBotForAccount(accountId);
113
+ const apiGateway = bot.api;
114
+
115
+ let data: unknown;
116
+ switch (method) {
117
+ case 'GET':
118
+ data = await apiGateway.get(p.path, p.query);
119
+ break;
120
+ case 'POST':
121
+ data = await apiGateway.post(p.path, p.body);
122
+ break;
123
+ case 'PUT':
124
+ data = await apiGateway.put(p.path, p.body);
125
+ break;
126
+ case 'PATCH':
127
+ data = await apiGateway.patch(p.path, p.body);
128
+ break;
129
+ case 'DELETE':
130
+ data = await apiGateway.delete(p.path);
131
+ break;
132
+ }
133
+
134
+ return json(data);
135
+ } catch (err: unknown) {
136
+ const errMsg = err instanceof Error ? err.message : String(err);
137
+ const apiErr = err as { httpStatus?: number; bizCode?: number; path?: string };
138
+ return json({
139
+ error: errMsg,
140
+ status: apiErr.httpStatus,
141
+ code: apiErr.bizCode,
142
+ path: p.path,
143
+ });
144
+ }
145
+ },
146
+ },
147
+ { name: 'qqbot_platform_api' },
148
+ );
149
+ }
@@ -304,5 +304,5 @@ export function registerRemindTool(api: OpenClawPluginApi): void {
304
304
  { name: "qqbot_remind" },
305
305
  );
306
306
 
307
- console.log("[qqbot-remind] Registered QQBot remind tool");
308
307
  }
308
+
@@ -0,0 +1,54 @@
1
+ /**
2
+ * SDK MiddlewareState 类型扩展
3
+ *
4
+ * 通过 TypeScript module augmentation 为 SDK 的 MiddlewareState 添加
5
+ * 中间件填充的 well-known keys 类型声明。
6
+ *
7
+ * 这些字段由 SDK 内置中间件 / 项目自定义中间件填充:
8
+ * - envelope: envelopeFormatter 中间件 → 组装后的 agentBody(字符串)
9
+ * - assembledBody: envelopeFormatter format 注入函数 → 完整 AssembledBody 缓存
10
+ * - history: historyBuffer 中间件 → 群历史消息列表
11
+ * - quote: quoteRef 中间件 → 引用消息信息
12
+ * - mention: mentionGate 中间件 → @bot 判定结果
13
+ * - command: slashCommand 中间件 → 解析的命令
14
+ * - processedAttachments: attachmentProcessor 中间件 → 语音 STT、图片 URL
15
+ */
16
+ import '@tencent-connect/qqbot-nodejs';
17
+ import type { AssembledBody } from './dispatch/body-assembler.js';
18
+ import type { ProcessedAttachments } from './middleware/attachment.js';
19
+
20
+ declare module '@tencent-connect/qqbot-nodejs' {
21
+ interface MiddlewareState {
22
+ /** envelopeFormatter 输出的字符串(默认 = AssembledBody.agentBody) */
23
+ envelope?: string;
24
+ /** 项目自定义 format 注入的完整组装结果(dispatch 阶段直接消费) */
25
+ assembledBody?: AssembledBody;
26
+ /** historyBuffer 填充的群历史 */
27
+ history?: Array<{
28
+ role: string;
29
+ content: string;
30
+ senderId?: string;
31
+ senderName?: string;
32
+ timestamp?: number;
33
+ }>;
34
+ /** quoteRef 解析的引用信息 */
35
+ quote?: {
36
+ content: string;
37
+ senderId: string;
38
+ attachments?: unknown[];
39
+ messageId?: string;
40
+ };
41
+ /** mentionGate 判定结果 */
42
+ mention?: {
43
+ wasMentioned: boolean;
44
+ stripped?: string;
45
+ };
46
+ /** slashCommand 解析的命令(命令被匹配时存在) */
47
+ command?: {
48
+ name: string;
49
+ args: string;
50
+ };
51
+ /** attachmentProcessor 处理的附件结果 */
52
+ processedAttachments?: ProcessedAttachments;
53
+ }
54
+ }
package/src/types.ts CHANGED
@@ -25,10 +25,15 @@ export interface ResolvedQQBotAccount {
25
25
  secretSource: "config" | "file" | "env" | "none";
26
26
  /** 系统提示词 */
27
27
  systemPrompt?: string;
28
- /** 图床服务器公网地址 */
29
- imageServerBaseUrl?: string;
30
28
  /** 是否支持 markdown 消息(默认 true) */
31
29
  markdownSupport: boolean;
30
+ /** User-Agent 尾部追加内容 */
31
+ userAgentSuffix: string;
32
+ /**
33
+ * 单条消息最大处理时间(ms)。超时后 concurrencyGuard 会 abort 处理链,
34
+ * 释放锁并排空缓冲消息。0 表示不限制。默认 0(不限制)。
35
+ */
36
+ processingTimeoutMs: number;
32
37
  config: QQBotAccountConfig;
33
38
  }
34
39
 
@@ -57,6 +62,15 @@ export interface GroupConfig {
57
62
  historyLimit?: number;
58
63
  }
59
64
 
65
+ /** 消息接收传输方式 */
66
+ export type TransportMode = "websocket" | "webhook";
67
+
68
+ /** Webhook 传输配置 */
69
+ export interface WebhookTransportConfig {
70
+ /** 监听路径(默认 /qqbot/webhook) */
71
+ path?: string;
72
+ }
73
+
60
74
  /**
61
75
  * QQ Bot 账户配置
62
76
  */
@@ -66,8 +80,12 @@ export interface QQBotAccountConfig {
66
80
  appId?: string;
67
81
  clientSecret?: string;
68
82
  clientSecretFile?: string;
69
- dmPolicy?: "open" | "pairing" | "allowlist";
83
+ dmPolicy?: "open" | "pairing" | "allowlist" | "disabled";
70
84
  allowFrom?: string[];
85
+ /** 消息接收传输方式:websocket(默认)| webhook */
86
+ transport?: TransportMode;
87
+ /** webhook 传输配置(transport="webhook" 时生效) */
88
+ webhook?: WebhookTransportConfig;
71
89
  /** 群消息策略(默认 allowlist) */
72
90
  groupPolicy?: GroupPolicy;
73
91
  /** 群白名单(groupPolicy 为 allowlist 时生效) */
@@ -76,8 +94,6 @@ export interface QQBotAccountConfig {
76
94
  groups?: Record<string, GroupConfig>;
77
95
  /** 系统提示词,会添加在用户消息前面 */
78
96
  systemPrompt?: string;
79
- /** 图床服务器公网地址,用于发送图片,例如 http://your-ip:18765 */
80
- imageServerBaseUrl?: string;
81
97
  /** 是否支持 markdown 消息(默认 true,设为 false 可禁用) */
82
98
  markdownSupport?: boolean;
83
99
  /**
@@ -114,6 +130,12 @@ export interface QQBotAccountConfig {
114
130
  * 示例: "ryantest/openclaw-qqbot"
115
131
  */
116
132
  upgradePkg?: string;
133
+ /**
134
+ * 群消息是否默认需要 @机器人才响应(默认 true)
135
+ * 优先级低于 groups.{groupId}.requireMention 和 groups."*".requireMention
136
+ * 设为 false 时,所有群默认无需 @ 即触发回复(仍可被群级配置覆盖)
137
+ */
138
+ defaultRequireMention?: boolean;
117
139
  /**
118
140
  * 出站消息合并回复(debounce)配置
119
141
  * 当短时间内收到多次 deliver 时,将文本合并为一条消息发送,避免消息轰炸
@@ -123,11 +145,36 @@ export interface QQBotAccountConfig {
123
145
  * 是否启用流式消息(默认 false)
124
146
  * 启用后,AI 的回复会以流式形式逐步显示在 QQ 聊天中,
125
147
  * 用户可以看到文字逐字出现的打字机效果。
126
- * 设置为 true 可开启流式消息。
127
- *
148
+ *
149
+ * 兼容布尔值和对象格式,对齐框架 schema:
150
+ * - true / false 旧版布尔格式(自动转换为对象)
151
+ * - { mode: "partial" } 开启(对齐 StreamingMode.partial)
152
+ * - { mode: "off" } 关闭
153
+ *
128
154
  * 注意:仅 C2C(私聊)支持流式消息 API。
129
155
  */
130
- streaming?: boolean;
156
+ streaming?: boolean | { mode: 'partial' | 'off' };
157
+ /**
158
+ * STT (语音转文字) 配置
159
+ * 配置后,收到语音消息时会自动调用 STT 服务转录为文字
160
+ */
161
+ stt?: STTChannelConfig;
162
+ /**
163
+ * 单条消息最大处理时间(毫秒)。
164
+ * 超时后 concurrencyGuard 会 abort 处理链(取消 LLM 调用、工具执行等),
165
+ * 释放并发锁并排空缓冲消息。
166
+ *
167
+ * 设为 0 表示不限制超时。默认 0(不限制)。
168
+ *
169
+ * 可通过环境变量 OPENCLAW_PROCESSING_TIMEOUT_MS 覆盖全局默认值,
170
+ * 账户级配置优先级高于环境变量。
171
+ */
172
+ processingTimeoutMs?: number;
173
+ /**
174
+ * User-Agent 尾部追加内容(用于私有化部署标识等场景)
175
+ * 追加在 `QQBotPlugin/{version} (Node/{nodeVersion}; {os}; OpenClaw/{version})` 之后
176
+ */
177
+ userAgentSuffix?: string;
131
178
  }
132
179
 
133
180
  /**
@@ -181,6 +228,22 @@ export interface AudioFormatPolicy {
181
228
  transcodeEnabled?: boolean;
182
229
  }
183
230
 
231
+ /**
232
+ * STT (语音转文字) 配置
233
+ */
234
+ export interface STTChannelConfig {
235
+ /** 是否启用 STT(默认 true,配置了 baseUrl+apiKey 即自动启用) */
236
+ enabled?: boolean;
237
+ /** STT 服务提供商 ID(对应 models.providers 中的 key,默认 "openai") */
238
+ provider?: string;
239
+ /** STT API 地址(如 https://api.openai.com/v1) */
240
+ baseUrl?: string;
241
+ /** STT API 密钥 */
242
+ apiKey?: string;
243
+ /** STT 模型名称(默认 "whisper-1") */
244
+ model?: string;
245
+ }
246
+
184
247
  /**
185
248
  * 富媒体附件
186
249
  */
@@ -0,0 +1,66 @@
1
+ declare module "openclaw/plugin-sdk/webhook-ingress" {
2
+ import type { IncomingMessage, ServerResponse } from "node:http";
3
+
4
+ export interface WebhookInFlightLimiter {
5
+ acquire(): boolean;
6
+ release(): void;
7
+ }
8
+
9
+ export interface FixedWindowRateLimiter {
10
+ check(key?: string): boolean;
11
+ }
12
+
13
+ export function createWebhookInFlightLimiter(opts: {
14
+ max?: number;
15
+ maxInFlightPerKey?: number;
16
+ maxTrackedKeys?: number;
17
+ }): WebhookInFlightLimiter;
18
+
19
+ export function createFixedWindowRateLimiter(opts: {
20
+ windowMs: number;
21
+ max?: number;
22
+ maxRequests?: number;
23
+ maxTrackedKeys?: number;
24
+ }): FixedWindowRateLimiter;
25
+
26
+ export function registerWebhookTargetWithPluginRoute<T extends { path: string }>(opts: {
27
+ targetsByPath: Map<string, T[]>;
28
+ target: T;
29
+ route: {
30
+ auth: string;
31
+ match: "exact" | "prefix";
32
+ pluginId: string;
33
+ source: string;
34
+ accountId: string;
35
+ replaceExisting?: boolean;
36
+ log?: (msg: string) => void;
37
+ handler: (req: IncomingMessage, res: ServerResponse) => Promise<boolean | void>;
38
+ };
39
+ onLastPathTargetRemoved?: () => void;
40
+ }): { unregister: () => void };
41
+
42
+ export function withResolvedWebhookRequestPipeline<T extends { path: string }>(opts: {
43
+ req: IncomingMessage;
44
+ res: ServerResponse;
45
+ targetsByPath: Map<string, T[]>;
46
+ rateLimiter: FixedWindowRateLimiter;
47
+ inFlightLimiter: WebhookInFlightLimiter;
48
+ requireJsonContentType?: boolean;
49
+ handle: (ctx: { targets: T[] }) => Promise<void> | void;
50
+ }): Promise<boolean>;
51
+
52
+ export function resolveWebhookTargetWithAuthOrRejectSync<T>(opts: {
53
+ targets: T[];
54
+ res: ServerResponse;
55
+ isMatch: (target: T) => boolean;
56
+ unauthorizedStatusCode?: number;
57
+ unauthorizedMessage?: string;
58
+ }): T | null;
59
+
60
+ export function readWebhookBodyOrReject(opts: {
61
+ req: IncomingMessage;
62
+ res: ServerResponse;
63
+ maxBytes: number;
64
+ timeoutMs: number;
65
+ }): Promise<{ ok: true; value: string } | { ok: false }>;
66
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Mention 工具函数
3
+ *
4
+ * @mention 检测与清理,供 channel.ts 和 gateway 层使用。
5
+ */
6
+
7
+ export interface MentionEntry {
8
+ member_openid?: string;
9
+ id?: string;
10
+ user_openid?: string;
11
+ is_you?: boolean;
12
+ nickname?: string;
13
+ username?: string;
14
+ }
15
+
16
+ /** 清理 @mention:替换 <@openid> 为 @用户名,去除 @机器人自身 */
17
+ export function stripMentionText(text: string, mentions?: MentionEntry[]): string {
18
+ if (!text || !mentions?.length) return text;
19
+ let cleaned = text;
20
+ for (const m of mentions) {
21
+ const openid = m.member_openid ?? m.id ?? m.user_openid;
22
+ if (!openid) continue;
23
+ if (m.is_you) {
24
+ cleaned = cleaned.replace(new RegExp(`<@!?${openid}>`, 'g'), '').trim();
25
+ } else {
26
+ const displayName = m.nickname ?? m.username;
27
+ if (displayName) {
28
+ cleaned = cleaned.replace(new RegExp(`<@!?${openid}>`, 'g'), `@${displayName}`);
29
+ }
30
+ }
31
+ }
32
+ return cleaned;
33
+ }
34
+
35
+ /** 检测消息是否 @了机器人 */
36
+ export function detectWasMentioned({ eventType, mentions, content, mentionPatterns }: {
37
+ eventType?: string;
38
+ mentions?: Array<{ is_you?: boolean }>;
39
+ content?: string;
40
+ mentionPatterns?: string[];
41
+ }): boolean {
42
+ if (mentions?.some((m) => m.is_you)) return true;
43
+ if (eventType === 'GROUP_AT_MESSAGE_CREATE') return true;
44
+ if (mentionPatterns?.length && content) {
45
+ for (const pattern of mentionPatterns) {
46
+ try {
47
+ if (new RegExp(pattern, 'i').test(content)) return true;
48
+ } catch { /* 无效正则,跳过 */ }
49
+ }
50
+ }
51
+ return false;
52
+ }
@@ -1,64 +1,101 @@
1
1
  /**
2
- * 从 import.meta.url 向上遍历目录树查找 package.json 并读取 version。
3
- * 不依赖硬编码的 "../" 层级,无论编译输出结构如何变化都能可靠找到。
2
+ * 版本号获取工具。
3
+ *
4
+ * - getPackageVersion(): 插件自身版本(@tencent-connect/openclaw-qqbot)
5
+ * - getOpenclawVersion(): OpenClaw 框架版本降级链
6
+ *
7
+ * 兼容 CJS(tsup bundle)环境,不依赖 require.resolve。
4
8
  */
5
9
 
6
- import { fileURLToPath } from "node:url";
7
- import { createRequire } from "node:module";
8
10
  import path from "node:path";
9
11
  import fs from "node:fs";
10
12
 
11
- /** 已定位到的 package.json 路径,避免重复遍历目录树 */
12
- let _resolvedPkgPath: string | null = null;
13
+ /** OpenClaw 框架版本缓存(一次查完不再重复 io) */
14
+ let _cachedOpenclawVersion: string | undefined;
13
15
 
14
- export function getPackageVersion(metaUrl?: string): string {
15
- // 如果之前已定位到 package.json 路径,直接重新读取(快速路径)
16
- if (_resolvedPkgPath) {
17
- try {
18
- const pkg = JSON.parse(fs.readFileSync(_resolvedPkgPath, "utf8"));
19
- if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
20
- return pkg.version as string;
21
- }
22
- } catch {
23
- // 文件可能已被删除(升级过程中),清除路径缓存,走完整查找
24
- _resolvedPkgPath = null;
25
- }
26
- }
16
+ declare const __PLUGIN_VERSION__: string;
27
17
 
28
- // Strategy 1: 从调用者的 import.meta.url(或本模块)向上遍历找 package.json
29
- const startFile = metaUrl ? fileURLToPath(metaUrl) : fileURLToPath(import.meta.url);
30
- let dir = path.dirname(startFile);
31
- const root = path.parse(dir).root;
18
+ /**
19
+ * 获取插件自身版本号。
20
+ * 编译时由 tsup define 注入,零运行时 IO。
21
+ */
22
+ export function getPackageVersion(): string {
23
+ return typeof __PLUGIN_VERSION__ !== 'undefined' ? __PLUGIN_VERSION__ : 'unknown';
24
+ }
32
25
 
33
- while (dir !== root) {
34
- const candidate = path.join(dir, "package.json");
35
- try {
36
- if (fs.existsSync(candidate)) {
37
- const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
38
- // 确认是我们自己的包(避免找到其他 package.json)
39
- if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
40
- _resolvedPkgPath = candidate;
41
- return pkg.version as string;
42
- }
43
- }
44
- } catch {
45
- // ignore and try parent
46
- }
47
- dir = path.dirname(dir);
26
+ // ── OpenClaw 框架版本 ──
27
+
28
+ /**
29
+ * OpenClaw 框架版本降级链(优先后)。
30
+ * 1. PluginRuntime.version(3.31+,非 "unknown"
31
+ * 2. OPENCLAW_VERSION / OPENCLAW_SERVICE_VERSION 环境变量
32
+ * 3. 文件系统搜索 openclaw/package.json
33
+ * 4. 兜底 "unknown"
34
+ */
35
+ export function getOpenclawVersion(runtimeVersion?: string): string {
36
+ if (_cachedOpenclawVersion) return _cachedOpenclawVersion;
37
+
38
+ // 1. runtime.version(排除兜底值)
39
+ if (runtimeVersion && runtimeVersion !== "unknown") {
40
+ return _cachedOpenclawVersion = runtimeVersion;
48
41
  }
42
+ // 2. 环境变量
43
+ if (process.env.OPENCLAW_VERSION) {
44
+ return _cachedOpenclawVersion = process.env.OPENCLAW_VERSION;
45
+ }
46
+ if (process.env.OPENCLAW_SERVICE_VERSION) {
47
+ return _cachedOpenclawVersion = process.env.OPENCLAW_SERVICE_VERSION;
48
+ }
49
+ // 3. 文件系统
50
+ const pkgVer = readOpenclawPackageVersion();
51
+ if (pkgVer) return _cachedOpenclawVersion = pkgVer;
52
+
53
+ return "unknown";
54
+ }
49
55
 
50
- // Strategy 2: fallback createRequire 尝试常见相对路径
56
+ function readOpenclawPackageVersion(): string | undefined {
51
57
  try {
52
- const require = createRequire(metaUrl ?? import.meta.url);
53
- for (const rel of ["../../package.json", "../package.json", "./package.json"]) {
58
+ const dirs = searchRoots();
59
+ for (const dir of dirs) {
60
+ const pkgPath = path.join(dir, "package.json");
54
61
  try {
55
- const pkg = require(rel);
56
- if (pkg?.version) {
57
- return pkg.version as string;
58
- }
62
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as { name?: string; version?: string };
63
+ if (pkg.name === "openclaw" && pkg.version) return pkg.version;
59
64
  } catch { /* next */ }
60
65
  }
61
- } catch { /* fallback */ }
66
+ } catch { /* fallthrough */ }
67
+ return undefined;
68
+ }
62
69
 
63
- return "unknown";
70
+ function searchRoots(): string[] {
71
+ const roots: string[] = [];
72
+
73
+ // 从插件自身路径向上查找
74
+ if (typeof __filename === "string") {
75
+ let dir = path.dirname(__filename);
76
+ for (let i = 0; i < 10; i++) {
77
+ roots.push(dir);
78
+ const parent = path.dirname(dir);
79
+ if (parent === dir) break;
80
+ dir = parent;
81
+ }
82
+ }
83
+
84
+ // STATE_DIR 指向的 openclaw 安装目录附近
85
+ for (const key of ["OPENCLAW_STATE_DIR", "CLAWDBOT_STATE_DIR", "MOLTBOT_STATE_DIR"]) {
86
+ const v = process.env[key];
87
+ if (v) {
88
+ roots.push(path.dirname(v));
89
+ roots.push(path.resolve(v, ".."));
90
+ }
91
+ }
92
+
93
+ // 常见安装路径
94
+ const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
95
+ for (const name of ["openclaw", "clawdbot", "moltbot"]) {
96
+ roots.push(path.join(home, `.${name}`));
97
+ }
98
+ roots.push(process.cwd());
99
+
100
+ return [...new Set(roots.filter((r) => typeof r === "string" && r.length > 0))];
64
101
  }
@@ -13,7 +13,6 @@
13
13
  import * as os from "node:os";
14
14
  import * as path from "node:path";
15
15
  import * as fs from "node:fs";
16
- import { execFile } from "node:child_process";
17
16
 
18
17
  // ============ 基础平台信息 ============
19
18
 
@@ -299,12 +298,35 @@ export function detectFfmpeg(): Promise<string | null> {
299
298
  return _ffmpegCheckPromise;
300
299
  }
301
300
 
302
- /** 测试可执行文件是否能正常运行 */
303
- function testExecutable(cmd: string, args: string[]): Promise<boolean> {
301
+ /** 测试可执行文件是否在系统 PATH 中可访问 */
302
+ function testExecutable(cmd: string, _args: string[]): Promise<boolean> {
304
303
  return new Promise((resolve) => {
305
- execFile(cmd, args, { timeout: 5000 }, (err) => {
306
- resolve(!err);
307
- });
304
+ // 直接路径检查
305
+ if (path.isAbsolute(cmd) || cmd.includes("/") || cmd.includes("\\")) {
306
+ try {
307
+ fs.accessSync(cmd, fs.constants.X_OK);
308
+ resolve(true);
309
+ return;
310
+ } catch {
311
+ resolve(false);
312
+ return;
313
+ }
314
+ }
315
+
316
+ // PATH 查找
317
+ const pathEnv = process.env.PATH || "";
318
+ const paths = pathEnv.split(path.delimiter);
319
+ for (const p of paths) {
320
+ const cmdPath = path.join(p, cmd);
321
+ try {
322
+ fs.accessSync(cmdPath, fs.constants.X_OK);
323
+ resolve(true);
324
+ return;
325
+ } catch {
326
+ // 继续搜索
327
+ }
328
+ }
329
+ resolve(false);
308
330
  });
309
331
  }
310
332
 
@@ -328,7 +350,9 @@ export async function checkSilkWasmAvailable(): Promise<boolean> {
328
350
  if (_silkWasmAvailable !== null) return _silkWasmAvailable;
329
351
 
330
352
  try {
331
- const { isSilk } = await import("silk-wasm");
353
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
354
+ const silkWasm = await import("silk-wasm" as string) as any;
355
+ const isSilk = silkWasm.isSilk;
332
356
  // 用一个空 buffer 快速测试 WASM 是否能加载
333
357
  isSilk(new Uint8Array(0));
334
358
  _silkWasmAvailable = true;