@tencent-connect/openclaw-qqbot 1.7.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +25 -8
  2. package/README.zh.md +24 -8
  3. package/bin/qqbot-cli.js +5 -2
  4. package/dist/index.cjs +15549 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +890 -0
  7. package/index.ts +56 -19
  8. package/openclaw.plugin.json +19 -3
  9. package/package.json +14 -23
  10. package/preload.cjs +17 -14
  11. package/scripts/link-sdk-core.cjs +133 -50
  12. package/scripts/upgrade-via-npm.sh +42 -11
  13. package/scripts/upgrade-via-source.sh +4 -4
  14. package/skills/qqbot-channel/SKILL.md +34 -12
  15. package/src/adapter/contract.ts +69 -0
  16. package/src/adapter/gateway.ts +61 -0
  17. package/src/adapter/index.ts +15 -0
  18. package/src/adapter/lint.ts +144 -0
  19. package/src/adapter/media.ts +112 -0
  20. package/src/adapter/pairing.ts +99 -0
  21. package/src/adapter/resolve.ts +333 -0
  22. package/src/adapter/setup.ts +55 -0
  23. package/src/adapter/webhook.ts +248 -0
  24. package/src/adapter/workspace.ts +29 -0
  25. package/src/bot-instance.ts +60 -0
  26. package/src/channel.ts +202 -413
  27. package/src/commands/bot-approve.ts +143 -0
  28. package/src/commands/bot-clear-storage.ts +114 -0
  29. package/src/commands/bot-group-always.ts +62 -0
  30. package/src/commands/bot-help.ts +40 -0
  31. package/src/commands/bot-logs.ts +248 -0
  32. package/src/commands/bot-me.ts +18 -0
  33. package/src/commands/bot-pairing.ts +57 -0
  34. package/src/commands/bot-ping.ts +33 -0
  35. package/src/commands/bot-streaming.ts +55 -0
  36. package/src/commands/bot-upgrade.ts +56 -0
  37. package/src/commands/bot-version.ts +41 -0
  38. package/src/commands/config-util.ts +96 -0
  39. package/src/commands/index.ts +49 -0
  40. package/src/config.ts +46 -19
  41. package/src/dispatch/body-assembler.ts +356 -0
  42. package/src/dispatch/ctx-builder.ts +110 -0
  43. package/src/dispatch/dispatch.ts +352 -0
  44. package/src/dispatch/envelope-builder.ts +112 -0
  45. package/src/dispatch/index.ts +2 -0
  46. package/src/{approval-handler.ts → features/approval-handler.ts} +40 -94
  47. package/src/features/approval-utils.ts +41 -0
  48. package/src/features/credential-backup.ts +82 -0
  49. package/src/features/history-store.ts +19 -0
  50. package/src/features/msgid-cache.ts +55 -0
  51. package/src/features/onboarding.ts +38 -0
  52. package/src/{proactive.ts → features/proactive.ts} +68 -49
  53. package/src/features/ref-index-store.ts +282 -0
  54. package/src/{update-checker.ts → features/update-checker.ts} +15 -35
  55. package/src/gateway/event-handlers.ts +241 -0
  56. package/src/gateway/index.ts +3 -0
  57. package/src/gateway/lifecycle.ts +221 -0
  58. package/src/gateway/middleware-setup.ts +139 -0
  59. package/src/gateway/qqbot-gateway.ts +342 -0
  60. package/src/middleware/access-control.ts +130 -0
  61. package/src/middleware/attachment.ts +314 -0
  62. package/src/middleware/policy-injector.ts +66 -0
  63. package/src/openclaw-plugin-sdk.d.ts +81 -10
  64. package/src/outbound/cron-scheduler.ts +130 -0
  65. package/src/outbound/debounce.ts +102 -0
  66. package/src/outbound/deliver-pipeline.ts +235 -0
  67. package/src/outbound/image-size.ts +123 -0
  68. package/src/outbound/index.ts +3 -0
  69. package/src/outbound/local-file-router.ts +145 -0
  70. package/src/outbound/media-send.ts +377 -0
  71. package/src/outbound/outbound-service.ts +222 -0
  72. package/src/outbound/reply-limiter.ts +128 -0
  73. package/src/outbound/sanitize.ts +32 -0
  74. package/src/outbound/streaming-controller.ts +228 -0
  75. package/src/outbound/target.ts +63 -0
  76. package/src/outbound/tts-provider.ts +84 -0
  77. package/src/request-context.ts +9 -8
  78. package/src/runtime.ts +31 -6
  79. package/src/setup/account-key.ts +41 -0
  80. package/src/setup/finalize.ts +110 -0
  81. package/src/setup/login.ts +197 -0
  82. package/src/setup/surface.ts +40 -0
  83. package/src/tools/platform.ts +149 -0
  84. package/src/tools/remind.ts +2 -0
  85. package/src/types-augment.d.ts +54 -0
  86. package/src/types.ts +51 -9
  87. package/src/utils/mention.ts +52 -0
  88. package/src/utils/pkg-version.ts +84 -47
  89. package/src/utils/platform.ts +31 -7
  90. package/src/utils/plugin-logger.ts +104 -0
  91. package/src/utils/ssrf-guard.ts +32 -2
  92. package/src/utils/stt.ts +137 -0
  93. package/src/utils/voice-text.ts +61 -0
  94. package/tsconfig.json +2 -1
  95. package/tsup.config.ts +57 -0
  96. package/dist/index.d.ts +0 -18
  97. package/dist/index.js +0 -27
  98. package/dist/src/admin-resolver.d.ts +0 -33
  99. package/dist/src/admin-resolver.js +0 -157
  100. package/dist/src/api.d.ts +0 -309
  101. package/dist/src/api.js +0 -923
  102. package/dist/src/approval-handler.d.ts +0 -47
  103. package/dist/src/approval-handler.js +0 -372
  104. package/dist/src/channel.d.ts +0 -29
  105. package/dist/src/channel.js +0 -528
  106. package/dist/src/config.d.ts +0 -60
  107. package/dist/src/config.js +0 -288
  108. package/dist/src/credential-backup.d.ts +0 -31
  109. package/dist/src/credential-backup.js +0 -66
  110. package/dist/src/deliver-debounce.d.ts +0 -74
  111. package/dist/src/deliver-debounce.js +0 -174
  112. package/dist/src/gateway.d.ts +0 -18
  113. package/dist/src/gateway.js +0 -2025
  114. package/dist/src/group-history.d.ts +0 -136
  115. package/dist/src/group-history.js +0 -226
  116. package/dist/src/image-server.d.ts +0 -87
  117. package/dist/src/image-server.js +0 -570
  118. package/dist/src/inbound-attachments.d.ts +0 -60
  119. package/dist/src/inbound-attachments.js +0 -248
  120. package/dist/src/known-users.d.ts +0 -100
  121. package/dist/src/known-users.js +0 -263
  122. package/dist/src/message-gating.d.ts +0 -53
  123. package/dist/src/message-gating.js +0 -107
  124. package/dist/src/message-queue.d.ts +0 -91
  125. package/dist/src/message-queue.js +0 -257
  126. package/dist/src/onboarding.d.ts +0 -10
  127. package/dist/src/onboarding.js +0 -203
  128. package/dist/src/outbound-deliver.d.ts +0 -48
  129. package/dist/src/outbound-deliver.js +0 -392
  130. package/dist/src/outbound.d.ts +0 -220
  131. package/dist/src/outbound.js +0 -975
  132. package/dist/src/proactive.d.ts +0 -170
  133. package/dist/src/proactive.js +0 -399
  134. package/dist/src/ref-index-store.d.ts +0 -101
  135. package/dist/src/ref-index-store.js +0 -298
  136. package/dist/src/reply-dispatcher.d.ts +0 -35
  137. package/dist/src/reply-dispatcher.js +0 -311
  138. package/dist/src/request-context.d.ts +0 -25
  139. package/dist/src/request-context.js +0 -37
  140. package/dist/src/runtime.d.ts +0 -3
  141. package/dist/src/runtime.js +0 -13
  142. package/dist/src/session-store.d.ts +0 -52
  143. package/dist/src/session-store.js +0 -254
  144. package/dist/src/slash-commands.d.ts +0 -82
  145. package/dist/src/slash-commands.js +0 -2233
  146. package/dist/src/startup-greeting.d.ts +0 -30
  147. package/dist/src/startup-greeting.js +0 -97
  148. package/dist/src/streaming.d.ts +0 -247
  149. package/dist/src/streaming.js +0 -899
  150. package/dist/src/stt.d.ts +0 -21
  151. package/dist/src/stt.js +0 -70
  152. package/dist/src/tools/channel.d.ts +0 -16
  153. package/dist/src/tools/channel.js +0 -231
  154. package/dist/src/tools/remind.d.ts +0 -2
  155. package/dist/src/tools/remind.js +0 -255
  156. package/dist/src/transport/index.d.ts +0 -10
  157. package/dist/src/transport/index.js +0 -9
  158. package/dist/src/transport/webhook-transport.d.ts +0 -67
  159. package/dist/src/transport/webhook-transport.js +0 -245
  160. package/dist/src/transport/webhook-verify.d.ts +0 -48
  161. package/dist/src/transport/webhook-verify.js +0 -98
  162. package/dist/src/types.d.ts +0 -462
  163. package/dist/src/types.js +0 -22
  164. package/dist/src/typing-keepalive.d.ts +0 -27
  165. package/dist/src/typing-keepalive.js +0 -64
  166. package/dist/src/update-checker.d.ts +0 -36
  167. package/dist/src/update-checker.js +0 -171
  168. package/dist/src/utils/audio-convert.d.ts +0 -98
  169. package/dist/src/utils/audio-convert.js +0 -783
  170. package/dist/src/utils/chunked-upload.d.ts +0 -68
  171. package/dist/src/utils/chunked-upload.js +0 -341
  172. package/dist/src/utils/file-utils.d.ts +0 -61
  173. package/dist/src/utils/file-utils.js +0 -172
  174. package/dist/src/utils/image-size.d.ts +0 -51
  175. package/dist/src/utils/image-size.js +0 -234
  176. package/dist/src/utils/media-send.d.ts +0 -158
  177. package/dist/src/utils/media-send.js +0 -502
  178. package/dist/src/utils/media-tags.d.ts +0 -14
  179. package/dist/src/utils/media-tags.js +0 -165
  180. package/dist/src/utils/payload.d.ts +0 -112
  181. package/dist/src/utils/payload.js +0 -186
  182. package/dist/src/utils/pkg-version.d.ts +0 -5
  183. package/dist/src/utils/pkg-version.js +0 -61
  184. package/dist/src/utils/platform.d.ts +0 -137
  185. package/dist/src/utils/platform.js +0 -390
  186. package/dist/src/utils/ssrf-guard.d.ts +0 -25
  187. package/dist/src/utils/ssrf-guard.js +0 -91
  188. package/dist/src/utils/text-parsing.d.ts +0 -31
  189. package/dist/src/utils/text-parsing.js +0 -74
  190. package/dist/src/utils/upload-cache.d.ts +0 -34
  191. package/dist/src/utils/upload-cache.js +0 -93
  192. package/node_modules/@eshaz/web-worker/LICENSE +0 -201
  193. package/node_modules/@eshaz/web-worker/README.md +0 -134
  194. package/node_modules/@eshaz/web-worker/browser.js +0 -17
  195. package/node_modules/@eshaz/web-worker/cjs/browser.js +0 -16
  196. package/node_modules/@eshaz/web-worker/cjs/node.js +0 -219
  197. package/node_modules/@eshaz/web-worker/index.d.ts +0 -4
  198. package/node_modules/@eshaz/web-worker/node.js +0 -223
  199. package/node_modules/@eshaz/web-worker/package.json +0 -54
  200. package/node_modules/@wasm-audio-decoders/common/index.js +0 -5
  201. package/node_modules/@wasm-audio-decoders/common/package.json +0 -36
  202. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +0 -231
  203. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +0 -129
  204. package/node_modules/@wasm-audio-decoders/common/src/puff/README +0 -67
  205. package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +0 -31
  206. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +0 -863
  207. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +0 -35
  208. package/node_modules/@wasm-audio-decoders/common/src/utilities.js +0 -3
  209. package/node_modules/@wasm-audio-decoders/common/types.d.ts +0 -7
  210. package/node_modules/mpg123-decoder/README.md +0 -265
  211. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +0 -185
  212. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +0 -1
  213. package/node_modules/mpg123-decoder/index.js +0 -8
  214. package/node_modules/mpg123-decoder/package.json +0 -58
  215. package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +0 -464
  216. package/node_modules/mpg123-decoder/src/MPEGDecoder.js +0 -200
  217. package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +0 -21
  218. package/node_modules/mpg123-decoder/types.d.ts +0 -30
  219. package/node_modules/silk-wasm/LICENSE +0 -21
  220. package/node_modules/silk-wasm/README.md +0 -85
  221. package/node_modules/silk-wasm/lib/index.cjs +0 -16
  222. package/node_modules/silk-wasm/lib/index.d.ts +0 -70
  223. package/node_modules/silk-wasm/lib/index.mjs +0 -16
  224. package/node_modules/silk-wasm/lib/silk.wasm +0 -0
  225. package/node_modules/silk-wasm/lib/utils.d.ts +0 -4
  226. package/node_modules/silk-wasm/package.json +0 -39
  227. package/node_modules/simple-yenc/.github/FUNDING.yml +0 -1
  228. package/node_modules/simple-yenc/.prettierignore +0 -1
  229. package/node_modules/simple-yenc/LICENSE +0 -7
  230. package/node_modules/simple-yenc/README.md +0 -163
  231. package/node_modules/simple-yenc/dist/esm.js +0 -1
  232. package/node_modules/simple-yenc/dist/index.js +0 -1
  233. package/node_modules/simple-yenc/package.json +0 -50
  234. package/node_modules/simple-yenc/rollup.config.js +0 -27
  235. package/node_modules/simple-yenc/src/simple-yenc.js +0 -302
  236. package/node_modules/ws/LICENSE +0 -20
  237. package/node_modules/ws/README.md +0 -548
  238. package/node_modules/ws/browser.js +0 -8
  239. package/node_modules/ws/index.js +0 -13
  240. package/node_modules/ws/lib/buffer-util.js +0 -131
  241. package/node_modules/ws/lib/constants.js +0 -19
  242. package/node_modules/ws/lib/event-target.js +0 -292
  243. package/node_modules/ws/lib/extension.js +0 -203
  244. package/node_modules/ws/lib/limiter.js +0 -55
  245. package/node_modules/ws/lib/permessage-deflate.js +0 -528
  246. package/node_modules/ws/lib/receiver.js +0 -706
  247. package/node_modules/ws/lib/sender.js +0 -602
  248. package/node_modules/ws/lib/stream.js +0 -161
  249. package/node_modules/ws/lib/subprotocol.js +0 -62
  250. package/node_modules/ws/lib/validation.js +0 -152
  251. package/node_modules/ws/lib/websocket-server.js +0 -554
  252. package/node_modules/ws/lib/websocket.js +0 -1393
  253. package/node_modules/ws/package.json +0 -69
  254. package/node_modules/ws/wrapper.mjs +0 -8
  255. package/scripts/postinstall-link-sdk.js +0 -170
  256. package/skills/qqbot-media/SKILL.md +0 -60
  257. package/src/admin-resolver.ts +0 -181
  258. package/src/api.ts +0 -1333
  259. package/src/credential-backup.ts +0 -72
  260. package/src/deliver-debounce.ts +0 -229
  261. package/src/gateway.ts +0 -2272
  262. package/src/group-history.ts +0 -328
  263. package/src/image-server.ts +0 -675
  264. package/src/inbound-attachments.ts +0 -321
  265. package/src/known-users.ts +0 -353
  266. package/src/message-gating.ts +0 -190
  267. package/src/message-queue.ts +0 -354
  268. package/src/onboarding.ts +0 -282
  269. package/src/outbound-deliver.ts +0 -486
  270. package/src/outbound.ts +0 -1179
  271. package/src/ref-index-store.ts +0 -412
  272. package/src/reply-dispatcher.ts +0 -334
  273. package/src/session-store.ts +0 -303
  274. package/src/slash-commands.ts +0 -2431
  275. package/src/startup-greeting.ts +0 -120
  276. package/src/streaming.ts +0 -1077
  277. package/src/stt.ts +0 -86
  278. package/src/tools/channel.ts +0 -275
  279. package/src/transport/index.ts +0 -11
  280. package/src/transport/webhook-transport.ts +0 -332
  281. package/src/transport/webhook-verify.ts +0 -119
  282. package/src/typing-keepalive.ts +0 -59
  283. package/src/utils/audio-convert.ts +0 -887
  284. package/src/utils/chunked-upload.ts +0 -483
  285. package/src/utils/file-utils.ts +0 -193
  286. package/src/utils/image-size.ts +0 -266
  287. package/src/utils/media-send.ts +0 -636
  288. package/src/utils/media-tags.ts +0 -183
  289. package/src/utils/payload.ts +0 -265
  290. package/src/utils/text-parsing.ts +0 -85
  291. package/src/utils/upload-cache.ts +0 -128
@@ -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
+ }
@@ -303,4 +303,6 @@ export function registerRemindTool(api: OpenClawPluginApi): void {
303
303
  },
304
304
  { name: "qqbot_remind" },
305
305
  );
306
+
306
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,12 +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;
32
- /** User-Agent 追加后缀(从通道级配置 channels.qqbot.userAgentSuffix 解析) */
33
- userAgentSuffix?: string;
30
+ /** User-Agent 尾部追加内容 */
31
+ userAgentSuffix: string;
32
+ /**
33
+ * 单条消息最大处理时间(ms)。超时后 concurrencyGuard 会 abort 处理链,
34
+ * 释放锁并排空缓冲消息。0 表示不限制。默认 0(不限制)。
35
+ */
36
+ processingTimeoutMs: number;
34
37
  config: QQBotAccountConfig;
35
38
  }
36
39
 
@@ -77,7 +80,7 @@ export interface QQBotAccountConfig {
77
80
  appId?: string;
78
81
  clientSecret?: string;
79
82
  clientSecretFile?: string;
80
- dmPolicy?: "open" | "pairing" | "allowlist";
83
+ dmPolicy?: "open" | "pairing" | "allowlist" | "disabled";
81
84
  allowFrom?: string[];
82
85
  /** 消息接收传输方式:websocket(默认)| webhook */
83
86
  transport?: TransportMode;
@@ -91,8 +94,6 @@ export interface QQBotAccountConfig {
91
94
  groups?: Record<string, GroupConfig>;
92
95
  /** 系统提示词,会添加在用户消息前面 */
93
96
  systemPrompt?: string;
94
- /** 图床服务器公网地址,用于发送图片,例如 http://your-ip:18765 */
95
- imageServerBaseUrl?: string;
96
97
  /** 是否支持 markdown 消息(默认 true,设为 false 可禁用) */
97
98
  markdownSupport?: boolean;
98
99
  /**
@@ -144,11 +145,36 @@ export interface QQBotAccountConfig {
144
145
  * 是否启用流式消息(默认 false)
145
146
  * 启用后,AI 的回复会以流式形式逐步显示在 QQ 聊天中,
146
147
  * 用户可以看到文字逐字出现的打字机效果。
147
- * 设置为 true 可开启流式消息。
148
+ *
149
+ * 兼容布尔值和对象格式,对齐框架 schema:
150
+ * - true / false 旧版布尔格式(自动转换为对象)
151
+ * - { mode: "partial" } 开启(对齐 StreamingMode.partial)
152
+ * - { mode: "off" } 关闭
148
153
  *
149
154
  * 注意:仅 C2C(私聊)支持流式消息 API。
150
155
  */
151
- 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;
152
178
  }
153
179
 
154
180
  /**
@@ -202,6 +228,22 @@ export interface AudioFormatPolicy {
202
228
  transcodeEnabled?: boolean;
203
229
  }
204
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
+
205
247
  /**
206
248
  * 富媒体附件
207
249
  */
@@ -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;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * PluginLogger — 统一日志接口
3
+ *
4
+ * 默认后端为 OpenClaw 框架 logger(`runtime.logging.getChildLogger`),
5
+ * 运行时不可用时临时降级 console,就绪后自动切换缓存。
6
+ */
7
+
8
+ import { getRequestContext } from "../request-context.js";
9
+ import { tryGetQQBotRuntime } from "../runtime.js";
10
+
11
+ export interface PluginLogger {
12
+ info(msg: string, meta?: Record<string, unknown>): void;
13
+ warn(msg: string, meta?: Record<string, unknown>): void;
14
+ error(msg: string, meta?: Record<string, unknown>): void;
15
+ debug(msg: string, meta?: Record<string, unknown>): void;
16
+ child(tag: string): PluginLogger;
17
+ }
18
+
19
+ export interface PluginLoggerOpts {
20
+ prefix?: string;
21
+ output?: Pick<PluginLogger, 'info' | 'warn' | 'error' | 'debug'>;
22
+ /** 强制 console 输出,忽略 runtime.logging(register 阶段使用) */
23
+ forceConsole?: boolean;
24
+ }
25
+
26
+ // ─── Console fallback ───────────────────────────────────────────────────────
27
+
28
+ function consoleSink(): Pick<PluginLogger, 'info' | 'warn' | 'error' | 'debug'> {
29
+ const C = '\x1b[36m', Y = '\x1b[33m', R = '\x1b[31m', G = '\x1b[90m', X = '\x1b[0m';
30
+ return {
31
+ debug: (m) => console.debug(`${G}[qqbot]${X}`, m),
32
+ info: (m) => console.log(`${C}[qqbot]${X}`, m),
33
+ warn: (m) => console.warn(`${Y}[qqbot]${X}`, m),
34
+ error: (m) => console.error(`${R}[qqbot]${X}`, m),
35
+ };
36
+ }
37
+
38
+ // ─── Trace metadata ─────────────────────────────────────────────────────────
39
+
40
+ function enrichMeta(meta?: Record<string, unknown>): Record<string, unknown> | undefined {
41
+ const ctx = getRequestContext();
42
+ if (!ctx) return meta;
43
+ const trace: Record<string, unknown> = {};
44
+ if (ctx.accountId) trace.accountId = ctx.accountId;
45
+ if (ctx.messageId) trace.messageId = ctx.messageId;
46
+ if (ctx.openId) trace.openId = ctx.openId;
47
+ if (Object.keys(trace).length === 0) return meta;
48
+ return meta ? { ...trace, ...meta } : trace;
49
+ }
50
+
51
+ // ─── Framework bridge ───────────────────────────────────────────────────────
52
+
53
+ type Sink = Pick<PluginLogger, 'info' | 'warn' | 'error' | 'debug'>;
54
+
55
+ function frameworkSink(): Sink {
56
+ // 不缓存——register() 阶段 getChildLogger 可行但输出未接好。
57
+ // 每次使用时重新 resolve,gateway 启动后自然拿到正确 logger。
58
+ const resolve = (): Sink => {
59
+ try {
60
+ const r = tryGetQQBotRuntime();
61
+ if (r?.logging) {
62
+ const child = r.logging.getChildLogger({ subsystem: 'qqbot/core' }) as any;
63
+ return {
64
+ debug: child.debug?.bind(child),
65
+ info: child.info.bind(child),
66
+ warn: child.warn.bind(child),
67
+ error: child.error.bind(child),
68
+ };
69
+ }
70
+ } catch {}
71
+ return consoleSink();
72
+ };
73
+
74
+ return {
75
+ debug: (msg, meta) => resolve().debug?.(msg, meta),
76
+ info: (msg, meta) => resolve().info(msg, meta),
77
+ warn: (msg, meta) => resolve().warn(msg, meta),
78
+ error: (msg, meta) => resolve().error(msg, meta),
79
+ };
80
+ }
81
+
82
+ // ─── Factory ────────────────────────────────────────────────────────────────
83
+
84
+ export function createPluginLogger(opts: PluginLoggerOpts = {}): PluginLogger {
85
+ const output = opts.output ?? (opts.forceConsole ? consoleSink() : frameworkSink());
86
+ const prefix = opts.prefix ?? '';
87
+
88
+ const fmt = (msg: string): string =>
89
+ prefix ? `${prefix} ${msg}` : msg;
90
+
91
+ const buildChild = (parentPrefix: string, tag: string): PluginLogger =>
92
+ createPluginLogger({
93
+ output,
94
+ prefix: parentPrefix ? `${parentPrefix}[${tag}]` : `[${tag}]`,
95
+ });
96
+
97
+ return {
98
+ info: (msg, meta) => output.info(fmt(msg), enrichMeta(meta)),
99
+ warn: (msg, meta) => output.warn(fmt(msg), enrichMeta(meta)),
100
+ error: (msg, meta) => output.error(fmt(msg), enrichMeta(meta)),
101
+ debug: (msg, meta) => output.debug(fmt(msg), enrichMeta(meta)),
102
+ child: (tag: string) => buildChild(prefix, tag),
103
+ };
104
+ }