@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,352 @@
1
+ /**
2
+ * 消息转发 — 入站消息 → OpenClaw AI
3
+ *
4
+ * 核心职责:
5
+ * 1. 从 SDK MiddlewareContext 构建 OpenClaw 标准信封
6
+ * 2. 通过 runtime-adapter 将消息交给 AI 处理
7
+ *
8
+ * 架构说明:
9
+ * - 所有 runtime.channel.* 访问均通过 runtime-adapter 隔离
10
+ * - log: 前缀由 PluginLogger + 框架自动注入,消息体不重复 accountId
11
+ */
12
+ import type { MiddlewareContext, QQBotInboundMessage } from '@tencent-connect/qqbot-nodejs';
13
+ import type { PluginRuntime } from 'openclaw/plugin-sdk';
14
+ import type { ResolvedQQBotAccount } from '../types.js';
15
+ import type { PluginLogger } from '../utils/plugin-logger.js';
16
+ import { buildEnvelope } from './envelope-builder.js';
17
+ import { assembleBody, type AssembledBody } from './body-assembler.js';
18
+ import { sendText, getGateway } from '../outbound/outbound-service.js';
19
+ import { sendMedia } from '../outbound/media-send.js';
20
+ import { deliverReply, type DeliverPayload, type DeliverInfo, type DeliverContext } from '../outbound/deliver-pipeline.js';
21
+ import { buildCtxPayload } from './ctx-builder.js';
22
+
23
+ import { DeliverDebouncer } from '../outbound/debounce.js';
24
+ import { StreamingController, shouldUseStreaming } from '../outbound/streaming-controller.js';
25
+ import { getAdapters } from '../adapter/resolve.js';
26
+ import { clearGroupHistory } from '../features/history-store.js';
27
+
28
+
29
+ /**
30
+ * 将经过中间件处理的入站消息转发给 OpenClaw AI
31
+ */
32
+ export async function dispatchToOpenClaw(
33
+ ctx: MiddlewareContext,
34
+ msg: QQBotInboundMessage,
35
+ account: ResolvedQQBotAccount,
36
+ runtime: PluginRuntime,
37
+ log?: PluginLogger,
38
+ ): Promise<void> {
39
+ const dlog = log?.child('dispatch');
40
+ const adapters = getAdapters(runtime, dlog);
41
+ const envelope = buildEnvelope(ctx, msg, account);
42
+
43
+ dlog?.debug(`received sender=${envelope.senderId} scope=${envelope.chatScope} msgId=${envelope.messageId}`);
44
+
45
+ if (!adapters.dispatchReply) {
46
+ dlog?.error(`runtime adapter dispatchReply not available (openclaw=${adapters.version})`);
47
+ return;
48
+ }
49
+
50
+ const assembled: AssembledBody =
51
+ ((ctx.state as Record<string, unknown>).assembledBody as AssembledBody | undefined) ??
52
+ assembleBody(ctx, msg, account);
53
+
54
+ const cfg = adapters.getConfig?.() ?? {};
55
+
56
+ const route = adapters.resolveAgentRoute?.({
57
+ cfg,
58
+ channel: 'qqbot',
59
+ accountId: account.accountId,
60
+ peer: {
61
+ kind: envelope.chatScope === 'group' ? 'group' : 'direct',
62
+ id: envelope.chatScope === 'group' ? (envelope.groupId ?? envelope.senderId) : envelope.senderId,
63
+ },
64
+ }) ?? { sessionKey: `qqbot:${account.accountId}:${envelope.senderId}`, accountId: account.accountId };
65
+
66
+ const qualifiedTarget = envelope.targetId;
67
+ const agentId = route.agentId ?? 'default';
68
+ const storePath = adapters.resolveStorePath?.((cfg as any)?.session?.store, { agentId }) ?? '';
69
+
70
+ const ctxPayload = buildCtxPayload({ assembled, envelope, route, msg, ctx, adapters });
71
+
72
+ const ttsRuntime = (runtime as any)?.tts ?? (runtime as any)?.channel?.runtimeContexts?.get?.('tts');
73
+
74
+ const debounceConfig = account.config?.deliverDebounce;
75
+ const debouncer = debounceConfig?.enabled !== false
76
+ ? new DeliverDebouncer(debounceConfig, (targetId, mergedText) =>
77
+ sendText({ to: targetId, text: mergedText, accountId: account.accountId, replyToId: envelope.messageId, account }).then(() => {}),
78
+ )
79
+ : undefined;
80
+
81
+ const deliverCtx: DeliverContext = {
82
+ qualifiedTarget,
83
+ accountId: account.accountId,
84
+ replyToId: envelope.messageId,
85
+ chatScope: envelope.chatScope === 'group' ? 'group' : 'direct',
86
+ cfg,
87
+ debouncer: debouncer?.enabled ? debouncer : undefined,
88
+ sendText: (to, text) => sendText({ to, text, accountId: account.accountId, replyToId: envelope.messageId, account }),
89
+ sendMedia: (to, source, opts) => sendMedia({
90
+ to,
91
+ source,
92
+ text: opts?.text ?? '',
93
+ replyToId: envelope.messageId,
94
+ accountId: account.accountId,
95
+ agentId: route.agentId,
96
+ log: deliverCtx.log,
97
+ }),
98
+ textToSpeech: ttsRuntime?.textToSpeech
99
+ ? (params) => ttsRuntime.textToSpeech(params)
100
+ : undefined,
101
+ audioFileToSilkBase64: ttsRuntime?.audioFileToSilkBase64
102
+ ? (audioPath: string) => ttsRuntime.audioFileToSilkBase64(audioPath)
103
+ : undefined,
104
+ log: log?.child('deliver'),
105
+ agentId: route.agentId ?? 'default',
106
+ };
107
+
108
+ const streamingEnabled = shouldUseStreaming(
109
+ account,
110
+ envelope.chatScope === 'group' ? 'group' : 'c2c',
111
+ );
112
+
113
+ const streamingController = streamingEnabled
114
+ ? createStreamingController(envelope, account, log?.child('streaming'))
115
+ : null;
116
+
117
+ if (streamingController) {
118
+ dlog?.debug(`streaming enabled for ${envelope.senderId}`);
119
+ }
120
+
121
+ const deliveredMediaUrls = new Set<string>();
122
+ const deliveredTexts = new Set<string>();
123
+
124
+ if (!adapters.inboundRun) {
125
+ // 低版本:手动 session + dispatchReply 直调
126
+ if (adapters.recordInboundSession) {
127
+ try {
128
+ await adapters.recordInboundSession({
129
+ storePath,
130
+ sessionKey: route.sessionKey,
131
+ ctx: ctxPayload,
132
+ });
133
+ } catch { /* best-effort */ }
134
+ }
135
+ await adapters.dispatchReply!({
136
+ ctx: ctxPayload,
137
+ cfg,
138
+ dispatcherOptions: {
139
+ deliver: async (payload: DeliverPayload, info?: DeliverInfo) => {
140
+ const text = payload.text?.trim() ?? '';
141
+ // 低版本无 block/final 协议,流式启动后 final 仍需跳过(降级除外)
142
+ if (!payload.mediaUrl && !payload.mediaUrls?.length
143
+ && text
144
+ && (deliveredTexts.has(text)
145
+ || (streamingController?.hasStarted && !streamingController?.shouldFallbackToStatic))
146
+ ) {
147
+ return;
148
+ }
149
+ const filteredPayload = deliveredMediaUrls.size > 0
150
+ ? {
151
+ ...payload,
152
+ mediaUrl: payload.mediaUrl && !deliveredMediaUrls.has(payload.mediaUrl)
153
+ ? payload.mediaUrl : undefined,
154
+ mediaUrls: payload.mediaUrls?.filter((u) => !deliveredMediaUrls.has(u)),
155
+ }
156
+ : payload;
157
+ await deliverReply(filteredPayload, info, deliverCtx);
158
+ if (text) deliveredTexts.add(text);
159
+ // 记录已投递媒体(流式路径可能已先发送)
160
+ for (const u of filteredPayload.mediaUrls ?? []) deliveredMediaUrls.add(u);
161
+ if (filteredPayload.mediaUrl) deliveredMediaUrls.add(filteredPayload.mediaUrl);
162
+ },
163
+ },
164
+ replyOptions: {
165
+ abortSignal: ctx.signal,
166
+ runId: envelope.messageId,
167
+ ...(streamingController
168
+ ? {
169
+ onPartialReply: async (p: { text?: string }) => {
170
+ if (p.text) await streamingController.onPartialReply(p.text);
171
+ },
172
+ }
173
+ : {}),
174
+ },
175
+ });
176
+ if (streamingController && !streamingController.isTerminal) {
177
+ await streamingController.finalize();
178
+ }
179
+ if (debouncer) await debouncer.flushAll();
180
+ } else {
181
+ await adapters.inboundRun!({
182
+ channel: 'qqbot',
183
+ accountId: route.accountId,
184
+ raw: envelope,
185
+ adapter: {
186
+ ingest: (raw: any) => ({
187
+ id: envelope.messageId,
188
+ rawText: assembled.rawBody,
189
+ textForAgent: assembled.agentBody,
190
+ textForCommands: assembled.rawBody,
191
+ raw,
192
+ }),
193
+ resolveTurn: (_input: unknown, _eventClass: unknown, _preflight: unknown) => ({
194
+ channel: 'qqbot',
195
+ accountId: route.accountId,
196
+ routeSessionKey: route.sessionKey,
197
+ storePath,
198
+ ctxPayload,
199
+ recordInboundSession: adapters.recordInboundSession,
200
+ record: {
201
+ onRecordError: (err: unknown) => {
202
+ dlog?.error(`Session record error: ${err}`);
203
+ },
204
+ },
205
+ runDispatch: () => {
206
+ return adapters.dispatchReply!({
207
+ ctx: ctxPayload,
208
+ cfg,
209
+ dispatcherOptions: {
210
+ deliver: async (payload: DeliverPayload, info?: DeliverInfo) => {
211
+ try {
212
+ const kind = (info as any)?.kind as string | undefined;
213
+ const text = payload.text?.trim() ?? '';
214
+ const hasMedia = !!(payload.mediaUrl || payload.mediaUrls?.length);
215
+ dlog?.debug(`deliver kind=${kind ?? 'none'} textLen=${text.length} voice=${!!payload.audioAsVoice} media=${hasMedia}`);
216
+
217
+ // ── 1. block: 媒体/语音立即发送,文本留给流式 ──
218
+ if (kind === 'block') {
219
+ if (payload.audioAsVoice) {
220
+ await deliverReply(payload, info, deliverCtx);
221
+ } else {
222
+ await forwardMediaUrls(payload, deliverCtx, deliveredMediaUrls, dlog);
223
+ }
224
+ }
225
+
226
+ // ── 2. 流式路径:流式已启动且未降级 → 跳过静态发送 ──
227
+ if (streamingController?.hasStarted && !streamingController.shouldFallbackToStatic) {
228
+ if (kind !== 'block') await streamingController.finalize();
229
+ if (!streamingController.shouldFallbackToStatic) return;
230
+ dlog?.warn(`streaming fallback to static`);
231
+ }
232
+
233
+ // ── 3. 文本去重:同文本已发过 → 跳过 ──
234
+ if (kind === 'final' && !hasMedia && text && deliveredTexts.has(text)) {
235
+ return;
236
+ }
237
+
238
+ // ── 4. tool 媒体:立即转发 ──
239
+ if (kind === 'tool') {
240
+ await forwardMediaUrls(payload, deliverCtx, deliveredMediaUrls, dlog);
241
+ return;
242
+ }
243
+
244
+ // ── 5. 默认路径:过滤已发媒体 + 发送 ──
245
+ const filteredPayload = filterDeliveredMedia(payload, deliveredMediaUrls);
246
+ await deliverReply(filteredPayload, info, deliverCtx);
247
+ if (text) deliveredTexts.add(text);
248
+ } catch (err) {
249
+ dlog?.error(`deliver error: ${err instanceof Error ? err.message : String(err)}`);
250
+ }
251
+ },
252
+ },
253
+ replyOptions: {
254
+ abortSignal: ctx.signal,
255
+ runId: envelope.messageId,
256
+ ...(streamingController
257
+ ? {
258
+ onPartialReply: async (p: { text?: string }) => {
259
+ if (p.text) await streamingController.onPartialReply(p.text);
260
+ },
261
+ }
262
+ : {}),
263
+ },
264
+ });
265
+ },
266
+ }),
267
+ },
268
+ });
269
+ }
270
+
271
+ dlog?.debug(`inboundRun completed sessionKey=${route.sessionKey}`);
272
+
273
+ // 群消息回复后清空历史缓存(避免下次 @ 时重复组包)
274
+ if (envelope.chatScope === 'group') {
275
+ clearGroupHistory(account.accountId, envelope.groupId ?? envelope.senderId);
276
+ }
277
+
278
+ if (streamingController && !streamingController.isTerminal) {
279
+ await streamingController.finalize();
280
+ }
281
+
282
+ if (debouncer) {
283
+ await debouncer.flushAll();
284
+ }
285
+ }
286
+
287
+ function createStreamingController(
288
+ envelope: ReturnType<typeof buildEnvelope>,
289
+ account: ResolvedQQBotAccount,
290
+ log?: PluginLogger,
291
+ ): StreamingController | null {
292
+ const gw = getGateway(account.accountId);
293
+ if (!gw) {
294
+ log?.error(`cannot enable streaming — gateway not running`);
295
+ return null;
296
+ }
297
+ return new StreamingController({
298
+ gateway: gw,
299
+ target: {
300
+ scope: 'c2c',
301
+ targetId: envelope.senderId,
302
+ msgId: envelope.messageId,
303
+ },
304
+ accountId: account.accountId,
305
+ replyToId: envelope.messageId,
306
+ log,
307
+ });
308
+ }
309
+
310
+ // ── 辅助函数 ──
311
+
312
+ /** 提取 payload 中的媒体 URL 并逐个发送(去重) */
313
+ async function forwardMediaUrls(
314
+ payload: DeliverPayload,
315
+ ctx: DeliverContext,
316
+ delivered: Set<string>,
317
+ log?: PluginLogger,
318
+ ): Promise<void> {
319
+ const urls: string[] = [];
320
+ if (payload.mediaUrls?.length) urls.push(...payload.mediaUrls);
321
+ if (payload.mediaUrl && !urls.includes(payload.mediaUrl)) urls.push(payload.mediaUrl);
322
+ const newUrls = urls.filter((u) => !delivered.has(u));
323
+ for (const url of newUrls) {
324
+ try {
325
+ await sendMedia({
326
+ to: ctx.qualifiedTarget,
327
+ source: url,
328
+ text: '',
329
+ replyToId: ctx.replyToId,
330
+ accountId: ctx.accountId,
331
+ log: ctx.log,
332
+ agentId: ctx.agentId,
333
+ });
334
+ delivered.add(url);
335
+ } catch (err) {
336
+ log?.error(`media forward failed: ${err instanceof Error ? err.message : String(err)}`);
337
+ }
338
+ }
339
+ }
340
+
341
+ /** 过滤已发送的媒体 URL */
342
+ function filterDeliveredMedia(
343
+ payload: DeliverPayload,
344
+ delivered: Set<string>,
345
+ ): DeliverPayload {
346
+ if (delivered.size === 0) return payload;
347
+ return {
348
+ ...payload,
349
+ mediaUrl: payload.mediaUrl && !delivered.has(payload.mediaUrl) ? payload.mediaUrl : undefined,
350
+ mediaUrls: payload.mediaUrls?.filter((u) => !delivered.has(u)),
351
+ };
352
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * 信封构建器
3
+ *
4
+ * 将 SDK MiddlewareContext 中由各中间件填充的 state 数据
5
+ * 转换为 OpenClaw 框架标准的 InboundMessage 结构。
6
+ */
7
+ import type { MiddlewareContext, QQBotInboundMessage, HistoryEntry, ResolvedQuote } from '@tencent-connect/qqbot-nodejs';
8
+ import type { ResolvedQQBotAccount } from '../types.js';
9
+
10
+ /**
11
+ * OpenClaw 入站消息结构(传递给 runtime.channel.dispatch)
12
+ */
13
+ export interface OpenClawInboundMessage {
14
+ channelId: string;
15
+ accountId: string;
16
+ targetId: string;
17
+ chatScope: 'direct' | 'group';
18
+ senderId: string;
19
+ senderName?: string;
20
+ messageId: string;
21
+ content: string;
22
+ history?: Array<{ role: string; content: string; senderId?: string; senderName?: string }>;
23
+ quote?: { content: string; senderId: string; attachments?: unknown[] };
24
+ attachments?: unknown[];
25
+ /** 入站图片 URL 列表(从附件中提取) */
26
+ imageUrls?: string[];
27
+ groupId?: string;
28
+ systemPrompt?: string;
29
+ }
30
+
31
+ /**
32
+ * 将 HistoryEntry 转换为 OpenClaw 历史格式
33
+ */
34
+ function mapHistory(
35
+ entries: HistoryEntry[] | undefined,
36
+ ): OpenClawInboundMessage['history'] {
37
+ if (!entries || entries.length === 0) return undefined;
38
+ return entries.map((e) => ({
39
+ role: 'user',
40
+ content: e.content,
41
+ senderId: e.senderId,
42
+ senderName: e.senderName,
43
+ }));
44
+ }
45
+
46
+ /**
47
+ * 将 ResolvedQuote 转换为 OpenClaw quote 格式
48
+ */
49
+ function mapQuote(
50
+ quote: ResolvedQuote | undefined,
51
+ ): OpenClawInboundMessage['quote'] {
52
+ if (!quote) return undefined;
53
+ return {
54
+ content: quote.text,
55
+ senderId: quote.entry?.senderId ?? '',
56
+ attachments: quote.attachments as unknown[],
57
+ };
58
+ }
59
+
60
+ /**
61
+ * 从 SDK 中间件上下文构建 OpenClaw InboundMessage
62
+ */
63
+ export function buildEnvelope(
64
+ ctx: MiddlewareContext,
65
+ msg: QQBotInboundMessage,
66
+ account: ResolvedQQBotAccount,
67
+ ): OpenClawInboundMessage {
68
+ const scope = msg.replyTarget.scope;
69
+ const targetId = scope === 'group'
70
+ ? `qqbot:group:${msg.replyTarget.targetId}`
71
+ : `qqbot:c2c:${msg.replyTarget.targetId}`;
72
+
73
+ // envelope 是 string 类型,直接作为 content 使用;回退到原始消息内容
74
+ const envelope = ctx.state.envelope as string | undefined;
75
+ let content = envelope ?? msg.content;
76
+
77
+ // 将语音转录文本和附件信息注入到 content 中
78
+ const processed = ctx.state.processedAttachments as
79
+ | { voiceText?: string; imageUrls?: string[]; otherInfo?: string }
80
+ | undefined;
81
+
82
+ if (processed) {
83
+ const parts: string[] = [];
84
+ if (processed.voiceText) {
85
+ parts.push(processed.voiceText);
86
+ }
87
+ if (content) {
88
+ parts.push(content);
89
+ }
90
+ if (processed.otherInfo) {
91
+ parts.push(processed.otherInfo);
92
+ }
93
+ content = parts.join('\n');
94
+ }
95
+
96
+ return {
97
+ channelId: 'qqbot',
98
+ accountId: account.accountId,
99
+ targetId,
100
+ chatScope: scope === 'group' ? 'group' : 'direct',
101
+ senderId: msg.senderId,
102
+ senderName: msg.senderName,
103
+ messageId: msg.messageId,
104
+ content,
105
+ history: mapHistory(ctx.state.history as HistoryEntry[] | undefined),
106
+ quote: mapQuote(ctx.state.quote as ResolvedQuote | undefined),
107
+ attachments: msg.attachments,
108
+ imageUrls: processed?.imageUrls,
109
+ groupId: scope === 'group' ? msg.replyTarget.targetId : undefined,
110
+ systemPrompt: account.systemPrompt,
111
+ };
112
+ }
@@ -0,0 +1,2 @@
1
+ export { dispatchToOpenClaw } from './dispatch.js';
2
+ export { buildEnvelope } from './envelope-builder.js';
@@ -10,75 +10,11 @@
10
10
  */
11
11
 
12
12
  import type { OpenClawConfig } from "openclaw/plugin-sdk";
13
- import { createRequire } from "node:module";
14
- import path from "node:path";
15
- import { fileURLToPath } from "node:url";
16
- import {
17
- getAccessToken,
18
- sendC2CMessageWithInlineKeyboard,
19
- sendGroupMessageWithInlineKeyboard,
20
- } from "./api.js";
21
- import type { InlineKeyboard, KeyboardButton } from "./types.js";
22
-
23
- // ─── 动态加载 gateway-runtime(兼容不同安装环境) ────────
24
-
25
- function loadGatewayRuntime(): { createOperatorApprovalsGatewayClient: (...args: any[]) => Promise<GatewayClient> } {
26
- const req = createRequire(import.meta.url);
27
- const currentFile = fileURLToPath(import.meta.url);
28
- const pluginRoot = path.resolve(path.dirname(currentFile), "..", "..");
29
- const fs = req("node:fs") as typeof import("node:fs");
30
-
31
- // 尝试从找到的 openclaw 根目录加载 gateway-runtime.js
32
- const tryLoadFromRoot = (root: string) => {
33
- for (const rel of ["dist/plugin-sdk/gateway-runtime.js", "plugin-sdk/gateway-runtime.js"]) {
34
- const p = path.join(root, rel);
35
- try {
36
- if (fs.existsSync(p)) return req(p);
37
- } catch { /* try next */ }
38
- }
39
- return null;
40
- };
41
-
42
- // 策略 1: link-sdk-core.cjs findOpenclawRoot
43
- try {
44
- const { findOpenclawRoot } = req(path.join(pluginRoot, "scripts", "link-sdk-core.cjs")) as {
45
- findOpenclawRoot: (root: string) => string | null;
46
- };
47
- const root = findOpenclawRoot(pluginRoot);
48
- if (root) {
49
- const mod = tryLoadFromRoot(root);
50
- if (mod) return mod;
51
- }
52
- } catch { /* fallback */ }
53
-
54
- // 策略 2: process.argv[1] 反推(当前进程就是 openclaw)
55
- try {
56
- const entry = process.argv[1];
57
- if (entry) {
58
- const realEntry = fs.realpathSync(entry);
59
- let dir = path.dirname(realEntry);
60
- for (let i = 0; i < 6; i++) {
61
- const mod = tryLoadFromRoot(dir);
62
- if (mod) return mod;
63
- const parent = path.dirname(dir);
64
- if (parent === dir) break;
65
- dir = parent;
66
- }
67
- }
68
- } catch { /* fallback */ }
69
-
70
- throw new Error("Cannot find openclaw/plugin-sdk/gateway-runtime (all strategies failed)");
71
- }
72
-
73
- // ─── 动态加载的模块类型(兼容旧版框架) ───────────────────────
74
-
75
- /** gateway-runtime 模块的接口(动态 import) */
76
- type GatewayClient = {
77
- start: () => void | Promise<void>;
78
- stop: () => void | Promise<void>;
79
- request: (method: string, params: unknown) => Promise<unknown>;
80
- };
13
+ import { getBotForAccount } from "../bot-instance.js";
14
+ import type { PluginLogger } from '../utils/plugin-logger.js';
15
+ import type { InlineKeyboard, KeyboardButton } from "../types.js";
81
16
 
17
+ import { loadApprovalGatewayRuntime, type ApprovalGatewayClient } from '../adapter/gateway.js';
82
18
  type EventFrame = {
83
19
  event: string;
84
20
  payload: unknown;
@@ -139,11 +75,7 @@ export interface QQBotApprovalHandlerOpts {
139
75
  clientSecret: string;
140
76
  cfg: OpenClawConfig;
141
77
  gatewayUrl?: string;
142
- log?: {
143
- info: (msg: string) => void;
144
- error: (msg: string) => void;
145
- debug?: (msg: string) => void;
146
- };
78
+ log?: PluginLogger;
147
79
  }
148
80
 
149
81
  type ApprovalKind = "exec" | "plugin";
@@ -255,11 +187,12 @@ function resolveTarget(
255
187
  // ─── Handler 类 ──────────────────────────────────────────────
256
188
 
257
189
  export class QQBotApprovalHandler {
258
- private gatewayClient: GatewayClient | null = null;
190
+ private gatewayClient: ApprovalGatewayClient | null = null;
259
191
  private pending = new Map<string, PendingEntry>();
260
192
  private requestCache = new Map<string, CachedApprovalRequest>();
261
193
  private opts: QQBotApprovalHandlerOpts;
262
194
  private started = false;
195
+ private connected = false;
263
196
 
264
197
  constructor(opts: QQBotApprovalHandlerOpts) {
265
198
  this.opts = opts;
@@ -269,14 +202,12 @@ export class QQBotApprovalHandler {
269
202
  if (this.started) return;
270
203
  this.started = true;
271
204
  const { log } = this.opts;
272
- log?.info(`[qqbot:${this.opts.accountId}] approval-handler: starting`);
205
+ log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: starting`);
273
206
 
274
207
  // 动态加载 gateway-runtime(兼容旧版框架 / pnpm 环境)
275
- let gatewayRuntime: { createOperatorApprovalsGatewayClient: (...args: any[]) => Promise<GatewayClient> };
276
- try {
277
- gatewayRuntime = loadGatewayRuntime();
278
- } catch (err) {
279
- log?.error(`[qqbot:${this.opts.accountId}] approval-handler: gateway-runtime module not available, approval feature disabled. Error: ${err}`);
208
+ const gatewayRuntime = loadApprovalGatewayRuntime();
209
+ if (!gatewayRuntime) {
210
+ log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: gateway-runtime not available, approval disabled`);
280
211
  this.started = false;
281
212
  return;
282
213
  }
@@ -287,9 +218,15 @@ export class QQBotApprovalHandler {
287
218
  gatewayUrl: this.opts.gatewayUrl,
288
219
  clientDisplayName: "QQBot Approval Handler",
289
220
  onEvent: (evt: EventFrame) => this.handleGatewayEvent(evt),
290
- onHelloOk: () => log?.info(`[qqbot:${this.opts.accountId}] approval-handler: connected to gateway`),
221
+ onHelloOk: () => {
222
+ this.connected = true;
223
+ log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: connected to gateway`);
224
+ },
291
225
  onConnectError: (err: { message: string }) => log?.error(`[qqbot:${this.opts.accountId}] approval-handler: connect error: ${err.message}`),
292
- onClose: (code: number, reason: string) => log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: gateway closed: ${code} ${reason}`),
226
+ onClose: (code: number, reason: string) => {
227
+ this.connected = false;
228
+ log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: gateway closed: ${code} ${reason}`);
229
+ },
293
230
  });
294
231
  this.gatewayClient.start();
295
232
  setApprovalFeatureAvailable(true);
@@ -305,11 +242,14 @@ export class QQBotApprovalHandler {
305
242
  for (const entry of this.pending.values()) clearTimeout(entry.timeoutId);
306
243
  this.pending.clear();
307
244
  this.requestCache.clear();
308
- this.gatewayClient?.stop();
245
+ await this.gatewayClient?.stop();
309
246
  this.gatewayClient = null;
310
- this.opts.log?.info(`[qqbot:${this.opts.accountId}] approval-handler: stopped`);
247
+ this.opts.log?.debug(`[qqbot:${this.opts.accountId}] approval-handler: stopped`);
311
248
  }
312
249
 
250
+ /** gateway 是否已建立连接 */
251
+ get isConnected(): boolean { return this.connected; }
252
+
313
253
  /** 检查是否有指定 shortId 对应的 pending 审批 */
314
254
  hasShortId(shortId: string): boolean {
315
255
  for (const id of this.pending.keys()) {
@@ -323,7 +263,10 @@ export class QQBotApprovalHandler {
323
263
  approvalId: string,
324
264
  decision: "allow-once" | "allow-always" | "deny"
325
265
  ): Promise<boolean> {
326
- if (!this.gatewayClient) return false;
266
+ if (!this.gatewayClient) {
267
+ this.opts.log?.warn(`[qqbot:${this.opts.accountId}] approval-handler: resolve ignored ${approvalId} → gatewayClient not ready`);
268
+ return false;
269
+ }
327
270
 
328
271
  // 查找完整 ID:支持完整 ID(exec:uuid / plugin:uuid)、纯 UUID、或 shortId(8位)
329
272
  let fullId = approvalId;
@@ -351,11 +294,11 @@ export class QQBotApprovalHandler {
351
294
  const isPending = this.pending.has(fullId);
352
295
  const isCached = this.requestCache.has(fullId);
353
296
 
354
- this.opts.log?.info(`[qqbot:${this.opts.accountId}] approval-handler: resolving ${fullId} (input=${approvalId}) kind=${kind} → ${decision}, pending=${isPending}, cached=${isCached}`);
297
+ this.opts.log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: resolving ${fullId} (input=${approvalId}) kind=${kind} → ${decision}, pending=${isPending}, cached=${isCached}`);
355
298
 
356
299
  try {
357
300
  await this.gatewayClient.request(method, { id: fullId, decision });
358
- this.opts.log?.info(`[qqbot:${this.opts.accountId}] approval-handler: RPC success ${toShortId(fullId)} → ${decision} (method=${method})`);
301
+ this.opts.log?.debug(`[qqbot:${this.opts.accountId}] approval-handler: RPC success ${toShortId(fullId)} → ${decision} (method=${method})`);
359
302
  return true;
360
303
  } catch (err) {
361
304
  this.opts.log?.error(`[qqbot:${this.opts.accountId}] approval-handler: resolve failed: ${err}`);
@@ -384,7 +327,10 @@ export class QQBotApprovalHandler {
384
327
 
385
328
  // 只处理本账号的请求
386
329
  const reqAccountId = (request.request as any).turnSourceAccountId?.trim();
387
- if (reqAccountId && reqAccountId !== accountId) return;
330
+ if (reqAccountId && reqAccountId !== accountId) {
331
+ log?.debug?.(`[qqbot:${accountId}] approval-handler: ${kind} ${shortId} ignored → account mismatch (req=${reqAccountId})`);
332
+ return;
333
+ }
388
334
 
389
335
  // 解析投递目标
390
336
  const sessionKey = (request.request as any).sessionKey;
@@ -419,12 +365,12 @@ export class QQBotApprovalHandler {
419
365
  await new Promise((r) => setTimeout(r, 2000));
420
366
 
421
367
  try {
422
- const token = await getAccessToken(appId, clientSecret);
423
- if (target.type === "c2c") {
424
- await sendC2CMessageWithInlineKeyboard(token, target.id, text, keyboard);
425
- } else {
426
- await sendGroupMessageWithInlineKeyboard(token, target.id, text, keyboard);
427
- }
368
+ const bot = getBotForAccount(accountId);
369
+ const target_reply = {
370
+ scope: target.type as "c2c" | "group",
371
+ targetId: target.id,
372
+ };
373
+ await bot.sendTextWithKeyboard(target_reply, text, keyboard as any);
428
374
  log?.info(`[qqbot:${accountId}] approval-handler: sent ${kind} approval ${shortId}`);
429
375
 
430
376
  const timeoutId = setTimeout(() => {