@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,130 @@
1
+ /**
2
+ * 动态访问控制中间件。
3
+ *
4
+ * 从 ctx.state.policy(由 policy-injector 注入)动态读取策略,支持配置热更新。
5
+ * 模式:disabled | open | allowlist | pairing(仅 c2c)
6
+ */
7
+ import type { Middleware } from '@tencent-connect/qqbot-nodejs';
8
+ import { getPairingApi } from '../adapter/pairing.js';
9
+
10
+
11
+ /**
12
+ * 创建动态访问控制中间件。
13
+ *
14
+ * 决策优先级:
15
+ * 1. disabled → 拒绝
16
+ * 2. open → 放行
17
+ * 3. allowlist → allowFrom 匹配(默认模式)
18
+ * 4. pairing → allowFrom + pairing store,未配对发起挑战
19
+ */
20
+ export function dynamicAccessControl(params: {
21
+ accountId: string;
22
+ getRuntime: () => any;
23
+ }): Middleware {
24
+ const { accountId, getRuntime } = params;
25
+
26
+ return async (ctx, next) => {
27
+ const p = ctx.state.policy as Record<string, unknown> | undefined;
28
+ const isGroup = ctx.message.kind === 'group';
29
+ const mode: string = isGroup
30
+ ? (p?.groupMode as string) ?? 'open'
31
+ : (p?.c2cMode as string) ?? 'allowlist';
32
+
33
+ if (mode === 'disabled') {
34
+ ctx.log?.info?.(`[access] blocked ${isGroup ? 'group' : 'c2c'} from ${ctx.message.senderId}: policy disabled`);
35
+ ctx.stop('access:policy_disabled');
36
+ return;
37
+ }
38
+ if (mode === 'open') {
39
+ await next();
40
+ return;
41
+ }
42
+
43
+ // allowlist / pairing:c2c 用 allowFrom,group 用 groupAllowFrom
44
+ const allowList = isGroup
45
+ ? ((p?.groupAllowFrom as string[]) ?? [])
46
+ : ((p?.allowFrom as string[]) ?? []);
47
+ if (!allowList.length || allowList.includes('*')) {
48
+ await next();
49
+ return;
50
+ }
51
+
52
+ const id = isGroup
53
+ ? (ctx.message.groupOpenid ?? '')
54
+ : (ctx.message.senderId as string);
55
+ if (allowList.includes(id)) {
56
+ await next();
57
+ return;
58
+ }
59
+
60
+ // allowlist 未匹配 → pairing 模式尝试 pairing store(仅 c2c)
61
+ if (mode === 'pairing' && !isGroup) {
62
+ await checkPairingMode(ctx, next, {
63
+ accountId,
64
+ getRuntime,
65
+ senderId: ctx.message.senderId as string,
66
+ });
67
+ return;
68
+ }
69
+
70
+ // 未匹配 → 拒绝
71
+ const listLabel = isGroup ? 'groupAllowFrom' : 'allowFrom';
72
+ ctx.log?.info?.(
73
+ `[access] blocked ${isGroup ? 'group' : 'c2c'} from ${id}: not in ${listLabel}`,
74
+ );
75
+ ctx.stop('access:not_allowlisted');
76
+ };
77
+ }
78
+
79
+ /**
80
+ * pairing 模式:检查 pairing store + 发起配对挑战。
81
+ */
82
+ async function checkPairingMode(
83
+ ctx: any,
84
+ next: () => Promise<void>,
85
+ opts: { accountId: string; getRuntime: () => any; senderId: string },
86
+ ): Promise<void> {
87
+ const api = getPairingApi();
88
+ if (!api) {
89
+ ctx.log?.info?.(`[access] pairing unavailable for ${opts.senderId}`);
90
+ ctx.stop('access:pairing_unavailable');
91
+ return;
92
+ }
93
+
94
+ try {
95
+ const storeIds = await api.readAllowFromStore({
96
+ channel: 'qqbot',
97
+ accountId: opts.accountId,
98
+ });
99
+ if (storeIds.includes(opts.senderId) || storeIds.includes('*')) {
100
+ await next();
101
+ return;
102
+ }
103
+
104
+ const challenge = await api.issueChallenge({
105
+ channel: 'qqbot',
106
+ id: opts.senderId,
107
+ accountId: opts.accountId,
108
+ });
109
+ const frameworkReply = api.buildReply({
110
+ code: challenge.code,
111
+ channel: 'qqbot',
112
+ });
113
+ const reply = [
114
+ frameworkReply,
115
+ '',
116
+ 'QQ 管理员可直接执行:',
117
+ '',
118
+ '```',
119
+ `/bot-pairing approve ${challenge.code}`,
120
+ '```',
121
+ ].join('\n');
122
+
123
+ ctx.log?.info?.(`[access] pairing required for ${opts.senderId}`);
124
+ await ctx.bot.sendText(ctx.replyTarget, reply).catch(() => {/* ignore */});
125
+ ctx.stop('access:pairing_required');
126
+ } catch (err) {
127
+ ctx.log?.error?.(`[access] pairing error: ${(err as Error).message}`);
128
+ ctx.stop(`access:pairing_error: ${(err as Error).message}`);
129
+ }
130
+ }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * 入站附件处理中间件
3
+ *
4
+ * 处理入站消息中的语音/图片/视频附件:
5
+ * - 语音:下载 → SILK转WAV → STT转文字 → 写入 ctx.state.processedAttachments
6
+ * - 图片:提取 URL 列表
7
+ * - 其他:标记为附件描述
8
+ *
9
+ * 插入位置:envelopeFormatter 之前
10
+ */
11
+ import * as path from 'node:path';
12
+ import type { MiddlewareContext } from '@tencent-connect/qqbot-nodejs';
13
+ import {
14
+ convertSilkToWav,
15
+ isVoiceAttachment,
16
+ } from '@tencent-connect/qqbot-nodejs/protocol';
17
+ import type { MessageAttachment } from '../types.js';
18
+ import { transcribeAudio, resolveSTTConfig } from '../utils/stt.js';
19
+ import { formatVoiceText, formatDuration, type VoiceTranscript, type TranscriptSource } from '../utils/voice-text.js';
20
+ import { downloadRemoteMedia } from '../adapter/media.js';
21
+ import { getAdapters } from '../adapter/resolve.js';
22
+
23
+ export { formatVoiceText, formatDuration };
24
+ export type { VoiceTranscript, TranscriptSource };
25
+
26
+ /** 处理后的附件结果(写入 ctx.state.processedAttachments) */
27
+ export interface ProcessedAttachments {
28
+ voiceText: string;
29
+ imageUrls: string[];
30
+ otherInfo: string;
31
+ transcripts: VoiceTranscript[];
32
+ /** 下载到本地的媒体路径(图片 + 语音,供 AI 引用) */
33
+ localMediaPaths: string[];
34
+ /** 对应 localMediaPaths 的 MIME type */
35
+ localMediaTypes: string[];
36
+ /** 远端 URL 列表(下载失败时的回退) */
37
+ remoteMediaUrls: string[];
38
+ }
39
+
40
+ interface AttachmentMiddlewareOptions {
41
+ /** 获取 runtime */
42
+ getRuntime: () => any;
43
+ }
44
+
45
+ /**
46
+ * 附件处理中间件
47
+ *
48
+ * SDK ctx 已提供 log 和 accountId,只需传入配置获取函数。
49
+ */
50
+ export function attachmentProcessor(opts: AttachmentMiddlewareOptions) {
51
+ return async (ctx: MiddlewareContext, next: () => Promise<void>) => {
52
+ const msg = ctx.message;
53
+ const attachments = msg.attachments as MessageAttachment[] | undefined;
54
+
55
+ if (attachments?.length) {
56
+ const runtime = opts.getRuntime();
57
+ const adapters = getAdapters(runtime);
58
+ const cfg = (adapters.getConfig?.() ?? {}) as Record<string, unknown>;
59
+ const log = ctx.log;
60
+ const result = await processAttachments(attachments, cfg, log);
61
+
62
+ if (result.voiceText || result.imageUrls.length > 0 || result.otherInfo || result.localMediaPaths.length > 0) {
63
+ ctx.state.processedAttachments = result;
64
+ }
65
+ }
66
+
67
+ await next();
68
+ };
69
+ }
70
+
71
+ // ── 核心处理逻辑 ──
72
+
73
+ type Log = { info: (m: string) => void; error: (m: string) => void; debug?: (m: string) => void };
74
+
75
+ async function processAttachments(
76
+ attachments: MessageAttachment[],
77
+ cfg: Record<string, unknown>,
78
+ log?: Log,
79
+ ): Promise<ProcessedAttachments> {
80
+ const sttCfg = resolveSTTConfig(cfg);
81
+ const audioPolicy = resolveAudioPolicy(cfg);
82
+
83
+ const imageUrls: string[] = [];
84
+ const otherParts: string[] = [];
85
+ const transcripts: VoiceTranscript[] = [];
86
+ const localMediaPaths: string[] = [];
87
+ const localMediaTypes: string[] = [];
88
+ const remoteMediaUrls: string[] = [];
89
+
90
+ // 并行下载所有附件
91
+ const tasks = attachments.map(async (att) => {
92
+ const isVoice = isVoiceAttachment(att);
93
+ const isImage = att.content_type?.startsWith('image/');
94
+ const url = normalizeUrl(att.url);
95
+
96
+ if (isImage && url) {
97
+ const localPath = await downloadMediaFile(url, att.filename, log);
98
+ return { type: 'image' as const, localPath, url, contentType: att.content_type ?? 'image/png' };
99
+ }
100
+
101
+ if (isVoice) {
102
+ const transcript = await processVoiceAttachment(att, sttCfg, audioPolicy, log);
103
+ return { type: 'voice' as const, transcript };
104
+ }
105
+
106
+ // other 类型也尝试下载
107
+ if (url) {
108
+ const localPath = await downloadMediaFile(url, att.filename, log);
109
+ return { type: 'other' as const, localPath, url, filename: att.filename ?? att.content_type };
110
+ }
111
+ return { type: 'other' as const, localPath: null, url: '', filename: att.filename ?? att.content_type };
112
+ });
113
+
114
+ const results = await Promise.all(tasks);
115
+
116
+ // 按原始顺序收集结果
117
+ for (const result of results) {
118
+ if (result.type === 'image') {
119
+ if (result.localPath) {
120
+ imageUrls.push(result.localPath);
121
+ localMediaPaths.push(result.localPath);
122
+ localMediaTypes.push(result.contentType);
123
+ } else {
124
+ imageUrls.push(result.url);
125
+ remoteMediaUrls.push(result.url);
126
+ }
127
+ } else if (result.type === 'voice') {
128
+ transcripts.push(result.transcript);
129
+ if (result.transcript.localPath) {
130
+ localMediaPaths.push(result.transcript.localPath);
131
+ localMediaTypes.push('audio/wav');
132
+ } else if (result.transcript.remoteUrl) {
133
+ remoteMediaUrls.push(result.transcript.remoteUrl);
134
+ }
135
+ } else if (result.type === 'other') {
136
+ if (result.localPath) {
137
+ otherParts.push(`[Attachment: ${result.localPath}]`);
138
+ localMediaPaths.push(result.localPath);
139
+ localMediaTypes.push('application/octet-stream');
140
+ } else {
141
+ otherParts.push(`[Attachment: ${result.filename}]`);
142
+ }
143
+ }
144
+ }
145
+
146
+ return {
147
+ voiceText: formatVoiceText(transcripts),
148
+ imageUrls,
149
+ otherInfo: otherParts.join('\n'),
150
+ transcripts,
151
+ localMediaPaths,
152
+ localMediaTypes,
153
+ remoteMediaUrls,
154
+ };
155
+ }
156
+
157
+ // ── 语音处理 ──
158
+
159
+ async function processVoiceAttachment(
160
+ att: MessageAttachment,
161
+ sttCfg: ReturnType<typeof resolveSTTConfig>,
162
+ audioPolicy: AudioPolicyResolved,
163
+ log?: Log,
164
+ ): Promise<VoiceTranscript> {
165
+ const asrReferText = att.asr_refer_text?.trim() || undefined;
166
+ // 远端 URL 兜底:优先 wav_url,其次原始 url
167
+ const remoteUrl = normalizeUrl(att.voice_wav_url) || normalizeUrl(att.url) || undefined;
168
+
169
+ // STT 未配置:直接走 ASR / fallback
170
+ if (!sttCfg) {
171
+ if (asrReferText) {
172
+ log?.debug?.(`Voice: using asr_refer_text (STT not configured)`);
173
+ return { text: asrReferText, source: 'asr', asrReferText, remoteUrl };
174
+ }
175
+ return {
176
+ text: '[Voice message - transcription unavailable]',
177
+ source: 'fallback',
178
+ asrReferText,
179
+ remoteUrl,
180
+ };
181
+ }
182
+
183
+ let localPath: string | undefined;
184
+ let duration: number | undefined;
185
+
186
+ try {
187
+ const wavUrl = normalizeUrl(att.voice_wav_url);
188
+ if (wavUrl) {
189
+ const downloaded = await downloadMediaFile(wavUrl, undefined, log);
190
+ if (downloaded) {
191
+ localPath = downloaded;
192
+ log?.debug?.(`Voice: downloaded WAV from voice_wav_url`);
193
+ }
194
+ }
195
+
196
+ if (!localPath) {
197
+ const silkUrl = normalizeUrl(att.url);
198
+ if (silkUrl) {
199
+ const silkPath = await downloadMediaFile(silkUrl, att.filename, log);
200
+ if (silkPath) {
201
+ const ext = path.extname(silkPath).toLowerCase();
202
+ if (audioPolicy.sttDirectFormats.includes(ext)) {
203
+ localPath = silkPath;
204
+ } else {
205
+ const wavResult = await convertSilkToWav(silkPath);
206
+ if (wavResult) {
207
+ localPath = wavResult.wavPath;
208
+ duration = wavResult.duration / 1000;
209
+ log?.debug?.(`Voice: SILK→WAV (${formatDuration(duration)})`);
210
+ } else {
211
+ localPath = silkPath;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ } catch (err) {
218
+ log?.error(`Voice download/convert failed: ${err instanceof Error ? err.message : String(err)}`);
219
+ }
220
+
221
+ if (localPath) {
222
+ try {
223
+ const transcript = await transcribeAudio(localPath, cfg2stt(sttCfg));
224
+ if (transcript) {
225
+ log?.debug?.(`Voice STT: ${transcript.slice(0, 80)}...`);
226
+ return { text: transcript, source: 'stt', duration, localPath, remoteUrl, asrReferText };
227
+ }
228
+ } catch (err) {
229
+ log?.error(`Voice STT failed: ${err instanceof Error ? err.message : String(err)}`);
230
+ }
231
+ }
232
+
233
+ if (asrReferText) {
234
+ return { text: asrReferText, source: 'asr', duration, localPath, remoteUrl, asrReferText };
235
+ }
236
+
237
+ return {
238
+ text: '[Voice message - transcription failed]',
239
+ source: 'fallback',
240
+ duration,
241
+ localPath,
242
+ remoteUrl,
243
+ asrReferText,
244
+ };
245
+ }
246
+
247
+ // ── 配置 ──
248
+
249
+ interface AudioPolicyResolved {
250
+ sttDirectFormats: string[];
251
+ uploadDirectFormats: string[];
252
+ transcodeEnabled: boolean;
253
+ }
254
+
255
+ function resolveAudioPolicy(cfg: Record<string, unknown>): AudioPolicyResolved {
256
+ const channels = cfg.channels as Record<string, unknown> | undefined;
257
+ const qqbot = channels?.qqbot as Record<string, unknown> | undefined;
258
+ const policy = qqbot?.audioFormatPolicy as Record<string, unknown> | undefined;
259
+
260
+ return {
261
+ sttDirectFormats: normalizeFormats((policy?.sttDirectFormats as string[]) ?? []),
262
+ uploadDirectFormats: normalizeFormats(
263
+ (policy?.uploadDirectFormats as string[]) ??
264
+ (qqbot?.voiceDirectUploadFormats as string[]) ??
265
+ ['.wav', '.mp3', '.silk'],
266
+ ),
267
+ transcodeEnabled: (policy?.transcodeEnabled as boolean) !== false,
268
+ };
269
+ }
270
+
271
+ function normalizeFormats(formats: string[]): string[] {
272
+ return formats.map((f) => {
273
+ const lower = f.toLowerCase().trim();
274
+ return lower.startsWith('.') ? lower : `.${lower}`;
275
+ });
276
+ }
277
+
278
+ function cfg2stt(sttCfg: NonNullable<ReturnType<typeof resolveSTTConfig>>): Record<string, unknown> {
279
+ return { channels: { qqbot: { stt: sttCfg } } };
280
+ }
281
+
282
+ // ── 文件工具 ──
283
+
284
+ function normalizeUrl(url: string | undefined): string {
285
+ if (!url) return '';
286
+ return url.startsWith('//') ? `https:${url}` : url;
287
+ }
288
+
289
+ async function downloadMediaFile(
290
+ url: string,
291
+ filename?: string,
292
+ log?: Log,
293
+ ): Promise<string | null> {
294
+ // 仅允许 HTTPS(安全策略)
295
+ if (!url.startsWith('https://')) {
296
+ log?.debug?.(`Skipping non-HTTPS URL: ${url.slice(0, 80)}`);
297
+ return null;
298
+ }
299
+
300
+ try {
301
+ const result = await downloadRemoteMedia({
302
+ url,
303
+ subdir: 'qqbot/downloads',
304
+ originalFilename: filename,
305
+ maxBytes: 500 * 1024 * 1024,
306
+ timeoutMs: 120_000,
307
+ });
308
+ log?.debug?.(`Downloaded: ${result.path}`);
309
+ return result.path;
310
+ } catch (err) {
311
+ log?.error(`Download failed: ${url.slice(0, 80)} — ${err instanceof Error ? err.message : String(err)}`);
312
+ return null;
313
+ }
314
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 动态策略注入中间件
3
+ *
4
+ * 每条消息到达时,按 groupId 解析群配置并注入 `ctx.state.policy`。
5
+ * SDK 内置中间件(mentionGate、historyBuffer 等)自动从 `ctx.state.policy`
6
+ * 读取动态策略作为 fallback,无需各自注册 `resolveConfig`。
7
+ *
8
+ * 优先级链(与旧版一致):
9
+ * 具体群配置 > 通配符 "*" > defaultRequireMention > 硬编码默认值
10
+ */
11
+ import type { Middleware } from '@tencent-connect/qqbot-nodejs';
12
+ import type { ResolvedQQBotAccount } from '../types.js';
13
+ import { resolveGroupConfigFromAccount } from '../config.js';
14
+
15
+ /**
16
+ * 创建 policy injector 中间件。
17
+ *
18
+ * 注入的 `ctx.state.policy` 结构:
19
+ * ```
20
+ * {
21
+ * scope: "c2c" | "group",
22
+ * group: {
23
+ * requireMention: boolean,
24
+ * ignoreOtherMentions: boolean,
25
+ * historyLimit: number,
26
+ * // ... 可扩展任意字段
27
+ * }
28
+ * }
29
+ * ```
30
+ *
31
+ * 使用者可在 `policy.group` 上添加自定义字段,如:
32
+ * ```
33
+ * ctx.state.policy.toolPolicy = "full";
34
+ * ```
35
+ * 自定义中间件通过 `ctx.state.policy` 读取即可。
36
+ */
37
+ export function createPolicyInjector(account: ResolvedQQBotAccount): Middleware {
38
+ return async (ctx, next) => {
39
+ const msg = ctx.message as any;
40
+ const scope = msg.kind as 'c2c' | 'group' | 'dm' | 'channel';
41
+
42
+ const policy: Record<string, unknown> = {
43
+ scope,
44
+ accountId: account.accountId,
45
+ // 访问控制:dmPolicy(c2c) / groupPolicy(group),默认 allowlist
46
+ c2cMode: account.config?.dmPolicy ?? 'allowlist',
47
+ groupMode: account.config?.groupPolicy ?? 'allowlist',
48
+ allowFrom: account.config?.allowFrom ?? [],
49
+ groupAllowFrom: account.config?.groupAllowFrom ?? [],
50
+ };
51
+
52
+ if (scope === 'group') {
53
+ const groupOpenid = msg.groupOpenid ?? '';
54
+ const groupCfg = resolveGroupConfigFromAccount(account, groupOpenid);
55
+ policy.group = {
56
+ requireMention: groupCfg.requireMention,
57
+ ignoreOtherMentions: groupCfg.ignoreOtherMentions,
58
+ historyLimit: groupCfg.historyLimit,
59
+ prompt: groupCfg.prompt,
60
+ };
61
+ }
62
+
63
+ ctx.state.policy = policy;
64
+ await next();
65
+ };
66
+ }
@@ -78,26 +78,41 @@ declare module "openclaw/plugin-sdk" {
78
78
  export interface PluginRuntime {
79
79
  /** OpenClaw 框架版本号,如 "2026.3.31" */
80
80
  version: string;
81
- /** 获取当前配置 */
82
- getConfig(): OpenClawConfig;
83
- /** 更新配置 */
84
- setConfig(config: OpenClawConfig): void;
85
- /** 获取数据目录路径 */
86
- getDataDir(): string;
87
- /** Channel 接口 - 使用 any 类型以兼容 SDK 内部复杂类型 */
81
+ /** 配置访问(运行时 config 在 config.current(),非顶层 getConfig()) */
82
+ config?: {
83
+ /** 获取当前运行时配置快照(只读) */
84
+ current(): OpenClawConfig;
85
+ };
86
+ /** Channel 接口 */
88
87
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
88
  channel?: any;
90
- /** 日志函数 */
89
+ /** 日志函数(旧版,建议使用 logging.getChildLogger) */
91
90
  log: {
92
91
  info: (message: string, ...args: unknown[]) => void;
93
92
  warn: (message: string, ...args: unknown[]) => void;
94
93
  error: (message: string, ...args: unknown[]) => void;
95
94
  debug: (message: string, ...args: unknown[]) => void;
96
95
  };
97
- /** 其他运行时方法 */
96
+ /** 结构化日志(框架自动追加 channel 前缀) */
97
+ logging: {
98
+ shouldLogVerbose: () => boolean;
99
+ getChildLogger: (
100
+ bindings?: Record<string, unknown>,
101
+ opts?: { level?: string },
102
+ ) => RuntimeLogger;
103
+ };
104
+ /** 其他运行时方法(不要假设 getConfig/getDataDir/setConfig 可用) */
98
105
  [key: string]: unknown;
99
106
  }
100
107
 
108
+ /** 结构化日志器 */
109
+ export interface RuntimeLogger {
110
+ debug?: (message: string, meta?: Record<string, unknown>) => void;
111
+ info: (message: string, meta?: Record<string, unknown>) => void;
112
+ warn: (message: string, meta?: Record<string, unknown>) => void;
113
+ error: (message: string, meta?: Record<string, unknown>) => void;
114
+ }
115
+
101
116
  // ============ 插件 API ============
102
117
 
103
118
  /**
@@ -219,7 +234,6 @@ declare module "openclaw/plugin-sdk" {
219
234
  tokenFile?: string;
220
235
  useEnv?: boolean;
221
236
  name?: string;
222
- imageServerBaseUrl?: string;
223
237
  [key: string]: unknown;
224
238
  }
225
239
 
@@ -661,6 +675,63 @@ declare module "openclaw/plugin-sdk" {
661
675
  export function normalizeAccountId(accountId: string | undefined | null): string;
662
676
  }
663
677
 
678
+ declare module "openclaw/plugin-sdk/setup" {
679
+ export interface ChannelSetupWizardStatus {
680
+ channelLabel: string;
681
+ configuredLabel: string;
682
+ unconfiguredLabel: string;
683
+ configuredHint?: string;
684
+ unconfiguredHint?: string;
685
+ configuredScore?: number;
686
+ unconfiguredScore?: number;
687
+ includeStatusLine?: boolean;
688
+ resolveConfigured: (params: { cfg: unknown; accountId: string }) => boolean;
689
+ }
690
+
691
+ export interface ChannelSetupWizard {
692
+ channel: string;
693
+ status: ChannelSetupWizardStatus;
694
+ /** 解析配置时使用哪个账户 ID:无账户时默认 'default' */
695
+ resolveAccountIdForConfigure?: (params: {
696
+ cfg: unknown;
697
+ prompter: unknown;
698
+ options?: unknown;
699
+ accountOverride?: string;
700
+ shouldPromptAccountIds: boolean;
701
+ listAccountIds: (cfg: unknown) => string[];
702
+ defaultAccountId: string;
703
+ }) => string | Promise<string>;
704
+ resolveShouldPromptAccountIds?: (params: {
705
+ cfg: unknown;
706
+ options?: unknown;
707
+ shouldPromptAccountIds: boolean;
708
+ }) => boolean;
709
+ credentials?: Array<{ id: string; label: string }>;
710
+ textInputs?: Array<{ id: string; label: string; placeholder?: string }>;
711
+ onStatusChange?: (params: { cfg: unknown }) => void | Promise<void>;
712
+ configure?: (params: { cfg: unknown; accountId: string; prompter: unknown; runtime: unknown }) => Promise<unknown>;
713
+ finalize: (params: { cfg: unknown; accountId: string; prompter: unknown; runtime: unknown }) => Promise<unknown>;
714
+ enable?: (cfg: unknown) => unknown;
715
+ disable?: (cfg: unknown) => unknown;
716
+ }
717
+
718
+ export function createStandardChannelSetupStatus(
719
+ opts: ChannelSetupWizardStatus,
720
+ ): ChannelSetupWizardStatus;
721
+
722
+ export function setSetupChannelEnabled(
723
+ cfg: unknown,
724
+ channel: string,
725
+ enabled: boolean,
726
+ ): void;
727
+
728
+ export const DEFAULT_ACCOUNT_ID: string;
729
+ }
730
+
731
+ declare module "openclaw/plugin-sdk/setup-tools" {
732
+ export function formatDocsLink(href: string, text?: string): string;
733
+ }
734
+
664
735
  declare module "openclaw/plugin-sdk/approval-runtime" {
665
736
  export interface ExecApprovalReplyMetadata {
666
737
  approvalId: string;