@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,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
+ }