@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,356 @@
1
+ /**
2
+ * Body 组装器(消息入站 body 组装)
3
+ *
4
+ * SDK 中间件链已经完成了所有预处理:
5
+ * - ctx.message.content ← contentSanitizer(face 解析 + mention 清洗)
6
+ * - ctx.state.quote ← quoteRef
7
+ * - ctx.state.history ← historyBuffer
8
+ * - ctx.state.mention ← mentionGate
9
+ * - ctx.state.processedAttachments ← attachmentProcessor(语音 STT、图片 URL)
10
+ *
11
+ * 本模块仅负责"按框架协议把上述上下文拼成最终字符串",是纯函数。
12
+ *
13
+ * 输出字段语义(与框架 buildCtxPayload 语义一致):
14
+ * - webBody → ctxPayload.Body (Web UI 展示)
15
+ * - agentBody → ctxPayload.BodyForAgent (AI 看到的)
16
+ * - rawBody → ctxPayload.RawBody / CommandBody(命令解析、审计)
17
+ * - systemPrompt → ctxPayload.GroupSystemPrompt
18
+ */
19
+ import type {
20
+ MiddlewareContext,
21
+ QQBotInboundMessage,
22
+ ResolvedQuote,
23
+ HistoryEntry,
24
+ } from '@tencent-connect/qqbot-nodejs';
25
+ import type { ResolvedQQBotAccount } from '../types.js';
26
+ import type { ProcessedAttachments } from '../middleware/attachment.js';
27
+ import { getAdapters } from '../adapter/resolve.js';
28
+
29
+ // ── 协议常量 ─────────────────────────────
30
+ const QUOTE_BEGIN = '[Quoted message begins]';
31
+ const QUOTE_END = '[Quoted message ends]';
32
+ const REF_BEGIN = '[Reference message begins]';
33
+ const REF_END = '[Reference message ends]';
34
+ const HISTORY_BEGIN = '[Chat history begins]';
35
+ const HISTORY_END = '[Chat history ends]';
36
+ const MERGE_CTX_BEGIN = '[Merged messages begins]';
37
+ const MERGE_CTX_END = '[Merged messages ends]';
38
+ const CURRENT_MSG = '[Current message]';
39
+
40
+ export interface AssembledBody {
41
+ /** Web UI 展示用 body */
42
+ webBody: string;
43
+ /** AI 实际接收的 body(dynamicCtx + userMessage [+ history 前缀]) */
44
+ agentBody: string;
45
+ /** 原始 content(命令解析 / 审计用) */
46
+ rawBody: string;
47
+ /** 群系统提示(account.systemPrompt + 群级提示拼接) */
48
+ systemPrompt?: string;
49
+ }
50
+
51
+ /**
52
+ * 把 SDK 中间件链产出的所有上下文组装为框架规约的 body。
53
+ */
54
+ export function assembleBody(
55
+ ctx: MiddlewareContext,
56
+ msg: QQBotInboundMessage,
57
+ account: ResolvedQQBotAccount,
58
+ getRuntime?: () => any,
59
+ ): AssembledBody {
60
+ const rawBody = msg.content ?? '';
61
+ const isGroup = msg.kind === 'group';
62
+
63
+ const mentionState = ctx.state.mention as { wasMentioned?: boolean } | undefined;
64
+ const wasMentioned = mentionState?.wasMentioned ?? false;
65
+ const processed = ctx.state.processedAttachments as ProcessedAttachments | undefined;
66
+ const quote = ctx.state.quote as ResolvedQuote | undefined;
67
+ const history = ctx.state.history as HistoryEntry[] | undefined;
68
+ const mergedMessages = ctx.state.mergedMessages as MiddlewareContext[] | undefined;
69
+
70
+ // ── Layer 1: userContent(清洗后的文本 + 语音转录 + 附件描述) ──
71
+ const userContent = buildUserContent(ctx.message.content ?? '', processed);
72
+
73
+ // ── Layer 2: quotePart ──
74
+ const quotePart = buildQuotePart(quote);
75
+
76
+ // ── Layer 3: userMessage(群带 [sender] 前缀 + (@you),合并消息特殊处理) ──
77
+ const userMessage = mergedMessages && mergedMessages.length > 0
78
+ ? buildMergedUserMessage({ messages: mergedMessages, quotePart, isGroup, wasMentioned, getRuntime })
79
+ : buildUserMessage({ msg, userContent, quotePart, isGroup, wasMentioned });
80
+
81
+ // ── Layer 4: dynamicCtx(媒体元数据块 + msg_elements 上下文) ──
82
+ const dynamicCtx = buildDynamicCtx(processed, msg, quote);
83
+
84
+ // ── Layer 5: agentBody(命令直通 / 群被@时前置历史) ──
85
+ const agentBody = buildAgentBody({
86
+ userContent,
87
+ base: dynamicCtx + userMessage,
88
+ isGroup,
89
+ wasMentioned,
90
+ history,
91
+ });
92
+
93
+ // ── webBody(合并消息包含全部前置 + 最后一条 + quotePart,对齐 agentBody) ──
94
+ const bodyContent = mergedMessages && mergedMessages.length > 0
95
+ ? userMessage
96
+ : `${quotePart}${userContent}`;
97
+
98
+ // ── webBody 外层 envelope 渲染(用 formatInboundEnvelope 包装) ──
99
+ const webBody = getRuntime
100
+ ? renderWebBody(getRuntime, bodyContent, msg, isGroup)
101
+ : bodyContent;
102
+
103
+ const systemPrompt = account.systemPrompt?.trim() || undefined;
104
+
105
+ return { webBody, agentBody, rawBody, systemPrompt };
106
+ }
107
+
108
+ // ── 局部组装函数 ─────────────────────────────────────────────
109
+
110
+ /** Layer 1:sanitized + 语音转录 + 附件描述 */
111
+ function buildUserContent(sanitizedRaw: string, processed: ProcessedAttachments | undefined): string {
112
+ const sanitized = sanitizedRaw.trim();
113
+ const voiceText = processed?.voiceText ?? '';
114
+ const attachmentInfo = processed?.otherInfo ? `\n${processed.otherInfo}` : '';
115
+
116
+ if (voiceText) {
117
+ return (sanitized ? `${sanitized}\n${voiceText}` : voiceText) + attachmentInfo;
118
+ }
119
+ return sanitized + attachmentInfo;
120
+ }
121
+
122
+ /** Layer 2:[Quoted message begins]…[Quoted message ends] */
123
+ function buildQuotePart(quote: ResolvedQuote | undefined): string {
124
+ if (!quote) return '';
125
+ const text = quote.text || 'Original content unavailable';
126
+ return `${QUOTE_BEGIN}\n${text}\n${QUOTE_END}\n${CURRENT_MSG}\n`;
127
+ }
128
+
129
+ /** Layer 3:quote + [Sender] {content}{(@you)?} */
130
+ function buildUserMessage(input: {
131
+ msg: QQBotInboundMessage;
132
+ userContent: string;
133
+ quotePart: string;
134
+ isGroup: boolean;
135
+ wasMentioned: boolean;
136
+ }): string {
137
+ const { msg, userContent, quotePart, isGroup, wasMentioned } = input;
138
+ const atYouTag = isGroup && wasMentioned ? ' (@you)' : '';
139
+
140
+ if (isGroup) {
141
+ const senderLabel = formatSenderLabel(msg.senderName, msg.senderId);
142
+ return `${quotePart}[${senderLabel}] ${userContent}${atYouTag}`;
143
+ }
144
+ return `${quotePart}${userContent}`;
145
+ }
146
+
147
+ /** Layer 3(合并版):concurrencyGuard merge 透传的多条消息 */
148
+ function buildMergedUserMessage(input: {
149
+ messages: MiddlewareContext[];
150
+ quotePart: string;
151
+ isGroup: boolean;
152
+ wasMentioned: boolean;
153
+ getRuntime?: () => any;
154
+ }): string {
155
+ const { messages, quotePart, isGroup, wasMentioned, getRuntime } = input;
156
+ if (messages.length <= 1) {
157
+ const single = messages[0]!;
158
+ return buildUserMessage({
159
+ msg: single.message,
160
+ userContent: single.message.content ?? '',
161
+ quotePart,
162
+ isGroup,
163
+ wasMentioned,
164
+ });
165
+ }
166
+
167
+ const formatEnvelope = getRuntime
168
+ ? getAdapters(getRuntime()).formatEnvelope
169
+ : null;
170
+
171
+ const lines = messages.map((ctx, i) => {
172
+ const isLast = i === messages.length - 1;
173
+ const line = formatMergedLine(ctx, { isGroup, isLast, wasMentioned, formatEnvelope });
174
+ return line && isLast ? `${quotePart}${line}` : line;
175
+ }).filter(Boolean);
176
+
177
+ if (!isGroup || allSameSender(messages)) {
178
+ return lines.join('\n');
179
+ }
180
+
181
+ const last = lines.pop()!;
182
+ return [MERGE_CTX_BEGIN, ...lines, MERGE_CTX_END, CURRENT_MSG, last].join('\n');
183
+ }
184
+
185
+ function formatMergedLine(
186
+ ctx: MiddlewareContext,
187
+ opts: { isGroup: boolean; isLast: boolean; wasMentioned: boolean; formatEnvelope: ((p: Record<string, unknown>) => string) | null },
188
+ ): string {
189
+ const m = ctx.message;
190
+ const content = (m.content ?? '').trim();
191
+
192
+ if (opts.formatEnvelope && opts.isGroup) {
193
+ const atYouTag = opts.isLast && opts.wasMentioned ? ' (@you)' : '';
194
+ return opts.formatEnvelope({
195
+ channel: 'qqbot',
196
+ from: formatSenderLabel(m.senderName, m.senderId),
197
+ timestamp: normalizeTimestamp(m.timestamp),
198
+ body: content + atYouTag,
199
+ chatType: 'group',
200
+ });
201
+ }
202
+ return opts.isGroup
203
+ ? `${formatSenderLabel(m.senderName, m.senderId)}: ${content}`
204
+ : content;
205
+ }
206
+
207
+ function allSameSender(messages: MiddlewareContext[]): boolean {
208
+ const first = messages[0]?.message.senderId;
209
+ return messages.every((c) => c.message.senderId === first);
210
+ }
211
+
212
+ /** Layer 4:- Images / - Voice / - ASR 元数据块 + msg_elements 引用上下文 */
213
+ function buildDynamicCtx(
214
+ processed: ProcessedAttachments | undefined,
215
+ msg: QQBotInboundMessage,
216
+ quote: ResolvedQuote | undefined,
217
+ ): string {
218
+ const lines: string[] = [];
219
+
220
+ // Images
221
+ if (processed?.imageUrls.length) {
222
+ lines.push(`- Images: ${processed.imageUrls.join(', ')}`);
223
+ }
224
+
225
+ // Voice:从 transcripts 行存投影出 paths + urls,去重后拼接
226
+ const transcripts = processed?.transcripts ?? [];
227
+ const voiceRefs = unique([
228
+ ...transcripts.map((t) => t.localPath).filter(isNonEmpty),
229
+ ...transcripts.map((t) => t.remoteUrl).filter(isNonEmpty),
230
+ ]);
231
+ if (voiceRefs.length > 0) {
232
+ lines.push(`- Voice: ${voiceRefs.join(', ')}`);
233
+ }
234
+
235
+ // ASR:source==='asr' 的 text,或任意 transcript 上的 asrReferText
236
+ const asrTexts = unique(
237
+ transcripts
238
+ .map((t) => (t.source === 'asr' ? t.text : t.asrReferText))
239
+ .filter(isNonEmpty),
240
+ );
241
+ if (asrTexts.length > 0) {
242
+ lines.push(`- ASR: ${asrTexts.join(' | ')}`);
243
+ }
244
+
245
+ // msg_elements 上下文(仅当非引用消息时解析,避免与 quotePart 重复)
246
+ if (!quote) {
247
+ const elementsCtx = buildMsgElementsContext(msg);
248
+ if (elementsCtx.length > 0) {
249
+ lines.push(REF_BEGIN, ...elementsCtx, REF_END);
250
+ }
251
+ }
252
+
253
+ return lines.length > 0 ? `${lines.join('\n')}\n\n` : '';
254
+ }
255
+
256
+ /** 从 msg_elements 提取上下文(非引用消息也解析,如被回复的 bot 消息等) */
257
+ function buildMsgElementsContext(msg: QQBotInboundMessage): string[] {
258
+ const elements = msg.msgElements;
259
+ if (!elements || elements.length === 0) return [];
260
+
261
+ const lines: string[] = [];
262
+ let index = 0;
263
+ for (const el of elements) {
264
+ const content = el.content?.trim();
265
+ if (!content) continue;
266
+ index += 1;
267
+
268
+ const author = (el as Record<string, unknown>).author as
269
+ | { username?: string }
270
+ | undefined;
271
+ const sender = author?.username ?? '未知';
272
+
273
+ lines.push(
274
+ `=== 消息 ${index} ===`,
275
+ `[消息内容] ${content}`,
276
+ `[发送者] ${sender}`,
277
+ );
278
+ }
279
+
280
+ return lines;
281
+ }
282
+
283
+ function unique<T>(arr: T[]): T[] {
284
+ return [...new Set(arr)];
285
+ }
286
+
287
+ function isNonEmpty(x: string | undefined): x is string {
288
+ return typeof x === 'string' && x.length > 0;
289
+ }
290
+
291
+ /** Layer 5:base = dynamicCtx+userMessage;命令直通;群被@叠历史前缀 */
292
+ function buildAgentBody(input: {
293
+ userContent: string;
294
+ base: string;
295
+ isGroup: boolean;
296
+ wasMentioned: boolean;
297
+ history: HistoryEntry[] | undefined;
298
+ }): string {
299
+ const { userContent, base, isGroup, wasMentioned, history } = input;
300
+
301
+ // 斜杠命令直通:去除一切装饰及首尾空白
302
+ if (userContent.trim().startsWith('/')) {
303
+ return userContent;
304
+ }
305
+
306
+ // 群被@ + 有历史 → 叠历史前缀
307
+ if (isGroup && wasMentioned && history && history.length > 0) {
308
+ const historyText = history
309
+ .map((h) => {
310
+ const label = formatSenderLabel(h.senderName, h.senderId);
311
+ return `[${label}] ${h.content}`;
312
+ })
313
+ .join('\n');
314
+ return [HISTORY_BEGIN, historyText, '', HISTORY_END, CURRENT_MSG, base].join('\n');
315
+ }
316
+
317
+ return base;
318
+ }
319
+
320
+ /** "Nick (openid)" 标签;当 name 已含 id 时避免双重包裹 */
321
+ function formatSenderLabel(name: string | undefined, id: string): string {
322
+ if (!name) return id;
323
+ return name.includes(id) ? name : `${name} (${id})`;
324
+ }
325
+
326
+ /** webBody 外层 envelope 渲染 */
327
+ function renderWebBody(
328
+ getRuntime: () => any,
329
+ bodyContent: string,
330
+ msg: QQBotInboundMessage,
331
+ isGroup: boolean,
332
+ ): string {
333
+ try {
334
+ const { formatEnvelope } = getAdapters(getRuntime());
335
+ if (!formatEnvelope) return bodyContent;
336
+ return formatEnvelope({
337
+ channel: 'qqbot',
338
+ from: msg.senderName ?? msg.senderId,
339
+ timestamp: normalizeTimestamp(msg.timestamp),
340
+ body: bodyContent,
341
+ chatType: isGroup ? 'group' : 'direct',
342
+ });
343
+ } catch {
344
+ return bodyContent;
345
+ }
346
+ }
347
+
348
+ /** timestamp 归一化到 epoch ms */
349
+ function normalizeTimestamp(ts: string | number | undefined): number {
350
+ if (typeof ts === 'number') return ts;
351
+ if (typeof ts === 'string') {
352
+ const d = new Date(ts).getTime();
353
+ if (!Number.isNaN(d)) return d;
354
+ }
355
+ return Date.now();
356
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * ctxPayload 构建器
3
+ *
4
+ * 与内置版 outbound-dispatch.ts:buildCtxPayload 对齐,
5
+ * 将 SDK 消息数据转换为 OpenClaw 框架标准的入站上下文。
6
+ */
7
+ import type { MiddlewareContext, QQBotInboundMessage } from '@tencent-connect/qqbot-nodejs';
8
+ import type { ResolvedQQBotAccount } from '../types.js';
9
+ import type { AssembledBody } from './body-assembler.js';
10
+ import type { OpenClawInboundMessage } from './envelope-builder.js';
11
+ import type { RuntimeAdapters } from '../adapter/resolve.js';
12
+
13
+ export interface CtxPayloadParams {
14
+ assembled: AssembledBody;
15
+ envelope: OpenClawInboundMessage;
16
+ route: { sessionKey: string; accountId: string; agentId?: string };
17
+ msg: QQBotInboundMessage;
18
+ ctx: MiddlewareContext;
19
+ adapters: RuntimeAdapters;
20
+ }
21
+
22
+ export function buildCtxPayload(params: CtxPayloadParams): any {
23
+ const { assembled, envelope, route, msg, ctx, adapters } = params;
24
+
25
+ const isSlashCommand = /^\//.test(assembled.rawBody ?? '');
26
+ const convKind = envelope.chatScope === 'group' ? 'group' : 'direct';
27
+ const peerId = convKind === 'group'
28
+ ? (envelope.groupId ?? envelope.senderId)
29
+ : envelope.senderId;
30
+ const groupId = convKind === 'group' ? envelope.groupId : undefined;
31
+
32
+ const processed = ctx.state.processedAttachments as any;
33
+ const voicePaths = processed?.localMediaPaths?.filter((_: string, i: number) =>
34
+ processed.localMediaTypes?.[i]?.startsWith('audio/')) ?? [];
35
+ const voiceUrls = processed?.remoteMediaUrls?.filter((_: string, i: number) =>
36
+ processed.remoteMediaUrls?.[i]?.startsWith?.('audio/')) ?? [];
37
+
38
+ const msgTimestamp = (msg as any).timestamp ?? (msg as any).Timestamp;
39
+
40
+ return adapters.buildInboundContext?.({
41
+ channel: 'qqbot',
42
+ accountId: route.accountId,
43
+ provider: 'qqbot',
44
+ surface: 'qqbot',
45
+ messageId: envelope.messageId,
46
+ timestamp: msgTimestamp ? new Date(msgTimestamp).getTime() : Date.now(),
47
+ from: envelope.targetId,
48
+ sender: { id: envelope.senderId, name: envelope.senderName },
49
+ conversation: {
50
+ kind: convKind,
51
+ id: peerId,
52
+ label: assembled.systemPrompt,
53
+ },
54
+ message: {
55
+ body: assembled.webBody,
56
+ bodyForAgent: assembled.agentBody,
57
+ rawBody: assembled.rawBody,
58
+ commandBody: assembled.rawBody,
59
+ },
60
+ route: {
61
+ agentId: route.agentId ?? 'default',
62
+ routeSessionKey: route.sessionKey,
63
+ accountId: route.accountId,
64
+ },
65
+ reply: {
66
+ to: envelope.targetId,
67
+ replyToId: envelope.messageId,
68
+ originatingTo: envelope.targetId,
69
+ },
70
+ access: {
71
+ commands: { authorized: isSlashCommand },
72
+ },
73
+ command: isSlashCommand
74
+ ? { kind: 'text-slash' as const, body: assembled.rawBody!, authorized: true }
75
+ : undefined,
76
+ media: voicePaths.length > 0
77
+ ? voicePaths.map((p: string, i: number) => ({
78
+ contentType: processed?.localMediaTypes?.[i] ?? 'audio/silk',
79
+ localPath: p,
80
+ url: voiceUrls[i],
81
+ }))
82
+ : voiceUrls.length > 0
83
+ ? voiceUrls.map((u: string) => ({ contentType: 'audio/wav', url: u }))
84
+ : undefined,
85
+ supplemental: {
86
+ quote: envelope.quote
87
+ ? { id: envelope.messageId, body: envelope.quote.content, sender: envelope.quote.senderId }
88
+ : undefined,
89
+ groupSystemPrompt: envelope.systemPrompt,
90
+ },
91
+ extra: {
92
+ ...(isSlashCommand ? { CommandSource: 'text' } : {}),
93
+ ...(groupId ? { QQGroupOpenid: groupId } : {}),
94
+ ...(processed?.localMediaPaths?.length
95
+ ? {
96
+ MediaPaths: processed.localMediaPaths,
97
+ MediaPath: processed.localMediaPaths[0],
98
+ MediaTypes: processed.localMediaTypes,
99
+ MediaType: processed.localMediaTypes?.[0],
100
+ }
101
+ : {}),
102
+ ...(processed?.remoteMediaUrls?.length
103
+ ? {
104
+ MediaUrls: processed.remoteMediaUrls,
105
+ MediaUrl: processed.remoteMediaUrls[0],
106
+ }
107
+ : {}),
108
+ },
109
+ });
110
+ }