@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,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;