@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,333 @@
1
+ /**
2
+ * Runtime Adapters — 一次 resolve,全程复用。
3
+ *
4
+ * Capability Probe 模式:按候选 API 路径探测,选出第一个可用函数。
5
+ * 对每个 runtime API 维护一个"候选列表"(最新在前),未来 API 改名时只需 +1 行。
6
+ *
7
+ * 调用方通过 `resolveRuntimeAdapters(runtime)` 获取适配层对象,
8
+ * 后续所有 dispatch / channel 代码只使用 adapters 上的方法。
9
+ */
10
+
11
+ import type { PluginRuntime } from 'openclaw/plugin-sdk';
12
+ import type { PluginLogger } from '../utils/plugin-logger.js';
13
+
14
+ // ── 类型 ──
15
+
16
+ export interface RuntimeAdapters {
17
+ /** 入站事件处理(自动适配 inbound.run / turn.run) */
18
+ inboundRun: ((params: any) => Promise<any>) | null;
19
+ /** 回复分发(带 block buffer) */
20
+ dispatchReply: ((params: any) => Promise<any>) | null;
21
+ /** Agent 路由解析 */
22
+ resolveAgentRoute: ((params: any) => any) | null;
23
+ /** 构建入站上下文(自动适配 inbound.buildContext / reply.finalizeInboundContext) */
24
+ buildInboundContext: ((params: any) => any) | null;
25
+ /** Session store path 解析 */
26
+ resolveStorePath: ((storeConfig: any, opts: any) => string) | null;
27
+ /** Session 记录 */
28
+ recordInboundSession: ((params: any) => Promise<void>) | null;
29
+ /** 格式化 envelope(自动适配 formatAgentEnvelope / formatInboundEnvelope) */
30
+ formatEnvelope: ((params: any) => string) | null;
31
+ /** 解析 envelope format options */
32
+ resolveEnvelopeFormatOptions: ((cfg: any) => any) | null;
33
+ /** Markdown 文本分块 */
34
+ chunkMarkdownText: ((text: string, limit: number) => string[]) | null;
35
+ /** 远程媒体保存(图片/语音下载) */
36
+ saveRemoteMedia: ((opts: { url: string; subdir?: string; originalFilename?: string }) => Promise<{ path: string } | null>) | null;
37
+ /** 获取当前配置快照 */
38
+ getConfig: (() => any) | null;
39
+ /**
40
+ * 持久化配置变更(高低版本兼容)。
41
+ *
42
+ * 内部优先级:
43
+ * 1. mutateConfigFile(新版 API,原子操作,支持 afterWrite 策略)
44
+ * 2. writeConfigFile(旧版 API,整体覆盖)
45
+ *
46
+ * 调用方只需传入完整 config 对象,不必关心框架版本差异。
47
+ */
48
+ /**
49
+ * 持久化配置变更(高低版本兼容)。
50
+ *
51
+ * 接受一个 mutator 回调,接收当前 config 对象(可变),
52
+ * 可直接修改或返回新对象。适配器负责确保写入和热重载。
53
+ */
54
+ persistConfig: ((mutator: (cfg: any) => any) => Promise<void>) | null;
55
+ /** openclaw 版本 */
56
+ version: string;
57
+ }
58
+
59
+ // ── Probe 辅助 ──
60
+
61
+ type ProbePath = string[];
62
+
63
+ /**
64
+ * 从 runtime 对象沿路径探测函数。返回 bound function 或 null。
65
+ */
66
+ function probeFunction(rt: any, paths: ProbePath[]): ((...args: any[]) => any) | null {
67
+ for (const path of paths) {
68
+ let target = rt;
69
+ let parent = rt;
70
+ for (let i = 0; i < path.length; i++) {
71
+ parent = target;
72
+ target = target?.[path[i]];
73
+ if (target === undefined || target === null) break;
74
+ }
75
+ if (typeof target === 'function') {
76
+ // bind 到 parent(倒数第二层),确保 this 上下文正确
77
+ return target.bind(parent);
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+
83
+ /**
84
+ * 从 runtime 对象沿路径探测值(非函数也可)。返回值或 null。
85
+ */
86
+ function probeValue(rt: any, paths: ProbePath[]): any {
87
+ for (const path of paths) {
88
+ let target = rt;
89
+ for (const key of path) {
90
+ target = target?.[key];
91
+ if (target === undefined || target === null) break;
92
+ }
93
+ if (target !== undefined && target !== null) return target;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ // ── 主入口 ──
99
+
100
+ /**
101
+ * 一次性 resolve 所有 runtime adapter。
102
+ *
103
+ * 通常在 gateway ready 后(首条消息到来前)调用一次,结果缓存复用。
104
+ * 返回 null 的字段表示该能力不可用,调用方按需降级或走 fallback。
105
+ */
106
+ export function resolveRuntimeAdapters(
107
+ rt: PluginRuntime,
108
+ log?: PluginLogger,
109
+ ): RuntimeAdapters {
110
+ const version = (rt as any).version ?? 'unknown';
111
+
112
+ const inboundRun = probeFunction(rt, [
113
+ ['channel', 'inbound', 'run'], // current (2026-05+)
114
+ ['channel', 'turn', 'run'], // legacy (removed 2026-05-27)
115
+ ]);
116
+
117
+ const dispatchReply = probeFunction(rt, [
118
+ ['channel', 'reply', 'dispatchReplyWithBufferedBlockDispatcher'],
119
+ ]);
120
+
121
+ const resolveAgentRoute = probeFunction(rt, [
122
+ ['channel', 'routing', 'resolveAgentRoute'],
123
+ ]);
124
+
125
+ // 构建入站上下文:新 API 优先,低版本 fallback 到 deprecated finalizeInboundContext
126
+ // 两个 API 签名不同,通过 wrapper 统一为 buildInboundContext(params) 接口
127
+ const rawBuildContext = probeFunction(rt, [
128
+ ['channel', 'inbound', 'buildContext'],
129
+ ]);
130
+ const rawFinalizeContext = !rawBuildContext
131
+ ? probeFunction(rt, [['channel', 'reply', 'finalizeInboundContext']])
132
+ : null;
133
+
134
+ const buildInboundContext: RuntimeAdapters['buildInboundContext'] = rawBuildContext
135
+ ? (params) => rawBuildContext(params)
136
+ : rawFinalizeContext
137
+ ? (params) => {
138
+ // 将统一参数转换为旧 API 的 rawCtxPayload 格式
139
+ const isCommand = params.access?.commands?.authorized ?? false;
140
+ const rawCtx = {
141
+ Body: params.message.body,
142
+ BodyForAgent: params.message.bodyForAgent,
143
+ RawBody: params.message.rawBody,
144
+ CommandBody: params.message.commandBody ?? params.message.rawBody,
145
+ CommandSource: isCommand ? 'text' : undefined,
146
+ CommandTurn: params.command ?? undefined,
147
+ CommandAuthorized: isCommand,
148
+ From: params.from,
149
+ To: params.reply.to,
150
+ SessionKey: params.route.routeSessionKey,
151
+ AccountId: params.route.accountId ?? params.accountId,
152
+ ChatType: params.conversation.kind,
153
+ GroupSystemPrompt: params.conversation.label,
154
+ SenderId: params.sender.id,
155
+ SenderName: params.sender.name,
156
+ Provider: params.provider ?? params.channel,
157
+ Surface: params.surface ?? params.channel,
158
+ MessageSid: params.messageId,
159
+ Timestamp: params.timestamp ?? Date.now(),
160
+ OriginatingChannel: params.channel,
161
+ OriginatingTo: params.reply.originatingTo ?? params.reply.to,
162
+ ...params.extra,
163
+ };
164
+ return rawFinalizeContext(rawCtx);
165
+ }
166
+ : null;
167
+
168
+ const resolveStorePath = probeFunction(rt, [
169
+ ['channel', 'session', 'resolveStorePath'],
170
+ ]);
171
+
172
+ const recordInboundSession = probeFunction(rt, [
173
+ ['channel', 'session', 'recordInboundSession'],
174
+ ]);
175
+
176
+ // 格式化 envelope:新 API 优先,低版本 fallback 到 deprecated formatInboundEnvelope
177
+ const formatEnvelope = probeFunction(rt, [
178
+ ['channel', 'reply', 'formatAgentEnvelope'], // current (2026-06+)
179
+ ['channel', 'reply', 'formatInboundEnvelope'], // deprecated,低版本兼容
180
+ ]);
181
+
182
+ const resolveEnvelopeFormatOptions = probeFunction(rt, [
183
+ ['channel', 'reply', 'resolveEnvelopeFormatOptions'],
184
+ ]);
185
+
186
+ const chunkMarkdownText = probeFunction(rt, [
187
+ ['channel', 'text', 'chunkMarkdownText'],
188
+ ]);
189
+
190
+ const saveRemoteMedia = probeFunction(rt, [
191
+ ['channel', 'media', 'saveRemoteMedia'],
192
+ ]);
193
+
194
+ const getConfig = probeFunction(rt, [
195
+ ['config', 'current'],
196
+ ]) ?? probeFunction(rt, [
197
+ ['getConfig'],
198
+ ]) ?? probeFunction(rt, [
199
+ ['config', 'loadConfig'],
200
+ ]);
201
+
202
+ // config 持久化:优先 mutateConfigFile(新版,原子操作),fallback writeConfigFile(旧版)
203
+ const rawMutateConfig = probeFunction(rt, [['config', 'mutateConfigFile']]);
204
+ const rawWriteConfig = probeFunction(rt, [['config', 'writeConfigFile']]);
205
+
206
+ const persistConfig: RuntimeAdapters['persistConfig'] = rawMutateConfig
207
+ ? async (mutator: (cfg: any) => any) => {
208
+ // 新版 API:mutate 回调接收当前 config,可直接修改或返回新对象
209
+ await rawMutateConfig({
210
+ afterWrite: 'hot-reload',
211
+ mutate: mutator,
212
+ });
213
+ }
214
+ : rawWriteConfig && getConfig
215
+ ? async (mutator: (cfg: any) => any) => {
216
+ // 旧版 API:先读取当前 config → 应用 mutator → 整体写入
217
+ const current = JSON.parse(JSON.stringify(getConfig()));
218
+ mutator(current);
219
+ await rawWriteConfig(current);
220
+ }
221
+ : null;
222
+
223
+ // 日志汇总
224
+ const resolved = [
225
+ inboundRun && 'inboundRun',
226
+ dispatchReply && 'dispatchReply',
227
+ resolveAgentRoute && 'resolveAgentRoute',
228
+ buildInboundContext && 'buildInboundContext',
229
+ resolveStorePath && 'resolveStorePath',
230
+ recordInboundSession && 'recordInboundSession',
231
+ formatEnvelope && 'formatEnvelope',
232
+ chunkMarkdownText && 'chunkMarkdownText',
233
+ saveRemoteMedia && 'saveRemoteMedia',
234
+ getConfig && 'getConfig',
235
+ persistConfig && `persistConfig(${rawMutateConfig ? 'mutate' : 'write'})`,
236
+ ].filter(Boolean);
237
+
238
+ log?.info(
239
+ `[qqbot:adapter] openclaw=${version} resolved ${resolved.length} adapters: ${resolved.join(', ')}`,
240
+ );
241
+
242
+ return {
243
+ inboundRun,
244
+ dispatchReply,
245
+ resolveAgentRoute,
246
+ buildInboundContext,
247
+ resolveStorePath,
248
+ recordInboundSession,
249
+ formatEnvelope,
250
+ resolveEnvelopeFormatOptions,
251
+ chunkMarkdownText,
252
+ saveRemoteMedia,
253
+ getConfig,
254
+ persistConfig,
255
+ version,
256
+ };
257
+ }
258
+
259
+ // ── 全局缓存(所有消费者共享同一份 adapters) ──
260
+
261
+ let _cachedAdapters: RuntimeAdapters | null = null;
262
+ let _cachedRuntimeRef: WeakRef<PluginRuntime> | null = null;
263
+
264
+ /**
265
+ * 获取 RuntimeAdapters 的全局缓存版本。
266
+ *
267
+ * - 自动基于 runtime 实例的 WeakRef 判断是否需要重新 resolve
268
+ * - 如果 runtime 被替换(框架热更新、二次 register),自动刷新
269
+ * - 所有消费者(dispatch / channel / middleware)共用一份,避免重复 probe
270
+ *
271
+ * @param rt 当前 PluginRuntime 实例
272
+ * @param log 可选日志(仅在首次 resolve 或 runtime 变更时输出)
273
+ */
274
+ export function getAdapters(
275
+ rt: PluginRuntime,
276
+ log?: PluginLogger,
277
+ ): RuntimeAdapters {
278
+ const cached = _cachedRuntimeRef?.deref();
279
+ if (cached === rt && _cachedAdapters) {
280
+ return _cachedAdapters;
281
+ }
282
+ // runtime 引用变化(首次 / 热更新)→ 重新 resolve
283
+ _cachedAdapters = resolveRuntimeAdapters(rt, log);
284
+ _cachedRuntimeRef = new WeakRef(rt);
285
+ return _cachedAdapters;
286
+ }
287
+
288
+ /**
289
+ * 持久化配置(auth.login 场景,兼容高低版本)。
290
+ *
291
+ * 探测顺序:
292
+ * 1. config.mutateConfigFile — 新版原子操作
293
+ * 2. config.writeConfigFile — 旧版整体覆盖
294
+ * 3. writeConfigFile — 更旧的顶层 API
295
+ */
296
+ export async function persistAuthConfig(
297
+ runtime: Record<string, unknown>,
298
+ cfg: Record<string, unknown>,
299
+ afterWrite = 'restart',
300
+ ): Promise<void> {
301
+ const config: Record<string, unknown> | undefined = runtime.config as any;
302
+
303
+ if (typeof config?.mutateConfigFile === 'function') {
304
+ await (config.mutateConfigFile as Function)({
305
+ mutate: () => cfg,
306
+ afterWrite,
307
+ });
308
+ return;
309
+ }
310
+ if (typeof config?.writeConfigFile === 'function') {
311
+ await (config.writeConfigFile as Function)(cfg);
312
+ return;
313
+ }
314
+ if (typeof runtime.writeConfigFile === 'function') {
315
+ await (runtime.writeConfigFile as Function)(cfg);
316
+ return;
317
+ }
318
+
319
+ // 最后兜底:裸写文件
320
+ const { homedir } = await import('node:os');
321
+ const { join } = await import('node:path');
322
+ const { writeFileSync } = await import('node:fs');
323
+ writeFileSync(join(homedir(), '.openclaw', 'openclaw.json'), JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
324
+ }
325
+
326
+ /**
327
+ * 强制清除 adapter 缓存(仅测试用)
328
+ */
329
+ export function _resetAdaptersCache(): void {
330
+ _cachedAdapters = null;
331
+ _cachedRuntimeRef = null;
332
+ }
333
+
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Setup 工具动态加载(plugin-sdk/setup + plugin-sdk/setup-tools)
3
+ *
4
+ * 新版本 openclaw 有这些导出,旧版本可能缺失。
5
+ * 不可用时提供最小降级实现。
6
+ */
7
+ import { createRequire } from 'node:module';
8
+
9
+ const req = createRequire(__filename);
10
+
11
+ let _setup: typeof import('openclaw/plugin-sdk/setup') | null | undefined;
12
+ let _tools: typeof import('openclaw/plugin-sdk/setup-tools') | null | undefined;
13
+
14
+ function loadSetup() {
15
+ if (_setup !== undefined) return _setup;
16
+ try { _setup = req('openclaw/plugin-sdk/setup'); } catch { _setup = null; }
17
+ return _setup;
18
+ }
19
+
20
+ function loadTools() {
21
+ if (_tools !== undefined) return _tools;
22
+ try { _tools = req('openclaw/plugin-sdk/setup-tools'); } catch { _tools = null; }
23
+ return _tools;
24
+ }
25
+
26
+ export type { ChannelSetupWizard } from 'openclaw/plugin-sdk/setup';
27
+
28
+ export const DEFAULT_ACCOUNT_ID = 'default';
29
+
30
+ export function createStandardChannelSetupStatus(
31
+ ...args: Parameters<NonNullable<typeof _setup>['createStandardChannelSetupStatus']>
32
+ ): ReturnType<NonNullable<typeof _setup>['createStandardChannelSetupStatus']> {
33
+ const mod = loadSetup();
34
+ if (mod) return mod.createStandardChannelSetupStatus(...args);
35
+ return {
36
+ channelLabel: args[0]?.channelLabel ?? 'QQ Bot',
37
+ configuredLabel: 'Configured',
38
+ unconfiguredLabel: 'Not configured',
39
+ resolveConfigured: () => false,
40
+ } as unknown as ReturnType<NonNullable<typeof _setup>['createStandardChannelSetupStatus']>;
41
+ }
42
+
43
+ export function setSetupChannelEnabled(
44
+ ...args: Parameters<NonNullable<typeof _setup>['setSetupChannelEnabled']>
45
+ ): void {
46
+ loadSetup()?.setSetupChannelEnabled?.(...args);
47
+ }
48
+
49
+ export function formatDocsLink(
50
+ ...args: Parameters<NonNullable<typeof _tools>['formatDocsLink']>
51
+ ): ReturnType<NonNullable<typeof _tools>['formatDocsLink']> {
52
+ const mod = loadTools();
53
+ if (mod) return mod.formatDocsLink(...args);
54
+ return (args[1] ? `${args[1]}: ${args[0]}` : args[0]) as ReturnType<NonNullable<typeof _tools>['formatDocsLink']>;
55
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * PluginWebhookAdapter — 将 OpenClaw Gateway HTTP 路由适配为 SDK WebhookServerAdapter。
3
+ *
4
+ * 复用 openclaw/plugin-sdk/webhook-ingress 的 target registry、rate limiting 和
5
+ * resolveWebhookTargetWithAuthOrRejectSync 签名匹配,对齐旧版本插件行为。
6
+ *
7
+ * 多账号同路径:通过 resolveWebhookTargetWithAuthOrRejectSync 遍历 targets 做
8
+ * Ed25519 签名匹配,找到正确的 target 后调用其 SDK handler 处理事件。
9
+ */
10
+ import type { ResolvedQQBotAccount } from '../types.js';
11
+ import type { PluginLogger } from '../utils/plugin-logger.js';
12
+ import type { WebhookServerAdapter, WebhookRequestHandler } from '@tencent-connect/qqbot-nodejs';
13
+ import { verifyWebhookSignature } from '@tencent-connect/qqbot-nodejs/protocol';
14
+
15
+ // ── 模块级共享状态 ──
16
+ interface WebhookTarget {
17
+ path: string;
18
+ accountId: string;
19
+ appId: string;
20
+ clientSecret: string;
21
+ handler: WebhookRequestHandler;
22
+ }
23
+ const sharedTargets = new Map<string, WebhookTarget[]>();
24
+ const _routeUnregisters = new Map<string, () => void>();
25
+
26
+ export function createPluginWebhookAdapter(params: {
27
+ account: ResolvedQQBotAccount;
28
+ log: PluginLogger;
29
+ }): WebhookServerAdapter {
30
+ let unregistered = false;
31
+ let storedPath = '';
32
+ const accountId = params.account.accountId;
33
+
34
+ return {
35
+ async listen(_port: number, path: string, handler: WebhookRequestHandler): Promise<void> {
36
+ const ingress = await import('openclaw/plugin-sdk/webhook-ingress').catch(() => null);
37
+ if (!ingress?.registerWebhookTargetWithPluginRoute) {
38
+ params.log.error('Webhook ingress not available');
39
+ return;
40
+ }
41
+
42
+ const webhookPath = storedPath = path && path !== '/' ? path : (params.account.config.webhook?.path ?? '/qqbot/webhook');
43
+
44
+ // 去重:reload 时避免同一 accountId 重复注册
45
+ const existing = sharedTargets.get(webhookPath);
46
+ if (existing) {
47
+ const dupIdx = existing.findIndex((t) => t.accountId === accountId);
48
+ if (dupIdx >= 0) {
49
+ existing[dupIdx] = { path: webhookPath, accountId, appId: params.account.appId, clientSecret: params.account.clientSecret, handler };
50
+ params.log.info(`Webhook target refreshed for ${accountId} on ${webhookPath}`);
51
+ return;
52
+ }
53
+ }
54
+
55
+ // 注册 target(框架管理 targetsByPath + 首次触发路由注册)
56
+ const target: WebhookTarget = {
57
+ path: webhookPath,
58
+ accountId,
59
+ appId: params.account.appId,
60
+ clientSecret: params.account.clientSecret,
61
+ handler,
62
+ };
63
+
64
+ const result = ingress.registerWebhookTargetWithPluginRoute({
65
+ targetsByPath: sharedTargets as any,
66
+ target,
67
+ route: {
68
+ auth: 'plugin',
69
+ match: 'exact',
70
+ pluginId: 'openclaw-qqbot',
71
+ source: 'qqbot-webhook',
72
+ accountId,
73
+ replaceExisting: true,
74
+ log: (msg: string) => params.log.info(msg),
75
+ handler: createSharedHandler(webhookPath, params.log),
76
+ },
77
+ onLastPathTargetRemoved: () => {
78
+ params.log.info(`Last webhook target removed from ${webhookPath}`);
79
+ },
80
+ });
81
+ _routeUnregisters.set(webhookPath, result.unregister);
82
+
83
+ params.log.info(`Webhook target added on ${webhookPath} (${sharedTargets.get(webhookPath)!.length} account(s))`);
84
+ },
85
+
86
+ close(): void {
87
+ if (unregistered) return;
88
+ unregistered = true;
89
+ if (storedPath) {
90
+ _routeUnregisters.get(storedPath)?.();
91
+ _routeUnregisters.delete(storedPath);
92
+ }
93
+ },
94
+ };
95
+ }
96
+
97
+ function createSharedHandler(path: string, log: PluginLogger) {
98
+ return async (req: any, res: any) => {
99
+ try {
100
+ const ingress = await import('openclaw/plugin-sdk/webhook-ingress').catch(() => null);
101
+
102
+ if (!ingress?.withResolvedWebhookRequestPipeline) {
103
+ await handleSimple(req, res, path, log);
104
+ return;
105
+ }
106
+
107
+ await ingress.withResolvedWebhookRequestPipeline({
108
+ req,
109
+ res,
110
+ targetsByPath: sharedTargets,
111
+ rateLimiter: ingress.createFixedWindowRateLimiter?.({
112
+ windowMs: 60_000, maxRequests: 600, maxTrackedKeys: 4096,
113
+ }),
114
+ inFlightLimiter: ingress.createWebhookInFlightLimiter?.({
115
+ maxInFlightPerKey: 8, maxTrackedKeys: 4096,
116
+ }),
117
+ requireJsonContentType: true,
118
+ handle: async ({ targets }) => {
119
+ const bodyResult = await ingress.readWebhookBodyOrReject({
120
+ req, res, maxBytes: 1_048_576, timeoutMs: 30_000,
121
+ });
122
+ if (!bodyResult.ok) {
123
+ log.warn?.(`[webhook] body rejected`);
124
+ return;
125
+ }
126
+
127
+ const rawBody = Buffer.from(bodyResult.value, 'utf-8');
128
+
129
+ let payload: any;
130
+ try { payload = JSON.parse(bodyResult.value); } catch (err) {
131
+ log.warn?.(`[webhook] invalid json: ${(err as Error).message}`);
132
+ res.statusCode = 400; res.end(JSON.stringify({ error: 'invalid json' })); return;
133
+ }
134
+
135
+ // op:13 → 用第一个 target 的 SDK handler(无需签名)
136
+ if (payload.op === 13) {
137
+ const t = targets[0];
138
+ if (!t) {
139
+ log.warn?.(`[webhook] op:13 no target on ${path}`);
140
+ res.statusCode = 500; res.end(JSON.stringify({ error: 'no target' })); return;
141
+ }
142
+ const h = resolveTargetHandler(path, t.accountId);
143
+ if (!h) {
144
+ log.warn?.(`[webhook] op:13 no handler for ${t.accountId}`);
145
+ res.statusCode = 500; res.end(JSON.stringify({ error: 'no handler' })); return;
146
+ }
147
+ await delegateToHandler(h, req, res, rawBody);
148
+ return;
149
+ }
150
+
151
+ // op:0 → 签名匹配 target → SDK handler 处理事件
152
+ const timestamp = getHeader(req, 'x-signature-timestamp') ?? '';
153
+ const signature = getHeader(req, 'x-signature-ed25519') ?? '';
154
+ if (!timestamp || !signature) {
155
+ log.warn?.(`[webhook] missing signature headers on ${req.url}`);
156
+ res.statusCode = 401; res.end(JSON.stringify({ error: 'missing signature' })); return;
157
+ }
158
+
159
+ const matched = ingress.resolveWebhookTargetWithAuthOrRejectSync({
160
+ targets, res,
161
+ isMatch: (t: any) => verifyWebhookSignature({
162
+ body: rawBody, timestamp, signature, botSecret: t.clientSecret as string,
163
+ }),
164
+ unauthorizedStatusCode: 401,
165
+ unauthorizedMessage: JSON.stringify({ error: 'invalid signature' }),
166
+ });
167
+ if (!matched) {
168
+ log.warn?.(`[webhook] signature mismatch on ${path} (${targets.length} target(s))`);
169
+ return;
170
+ }
171
+
172
+ const h = resolveTargetHandler(path, matched.accountId);
173
+ if (!h) {
174
+ log.error?.(`[webhook] no handler for matched target ${matched.accountId}`);
175
+ res.statusCode = 500; res.end(JSON.stringify({ error: 'no handler' })); return;
176
+ }
177
+ await delegateToHandler(h, req, res, rawBody);
178
+ },
179
+ });
180
+ } catch (err) {
181
+ log.error(`Webhook handler error: ${(err as Error).message}`);
182
+ if (!res.headersSent) {
183
+ res.statusCode = 500;
184
+ res.setHeader('Content-Type', 'application/json');
185
+ res.end(JSON.stringify({ error: 'internal error' }));
186
+ }
187
+ }
188
+ };
189
+ }
190
+
191
+ function resolveTargetHandler(path: string, accountId: string): WebhookRequestHandler | undefined {
192
+ return sharedTargets.get(path)?.find((t) => t.accountId === accountId)?.handler;
193
+ }
194
+
195
+ async function delegateToHandler(handler: WebhookRequestHandler, req: any, res: any, rawBody: Buffer) {
196
+ const headers: Record<string, string | string[]> = {};
197
+ for (const [k, v] of Object.entries(req.headers)) {
198
+ headers[k.toLowerCase()] = v as string | string[];
199
+ }
200
+ const resp = await handler({ body: rawBody, headers });
201
+ res.statusCode = resp.status;
202
+ if (resp.headers) {
203
+ for (const [k, v] of Object.entries(resp.headers)) res.setHeader(k, v as string);
204
+ }
205
+ res.end(resp.body);
206
+ }
207
+
208
+ async function handleSimple(req: any, res: any, path: string, log: PluginLogger) {
209
+ try {
210
+ const ct = String(req.headers['content-type'] ?? '');
211
+ if (!ct.includes('application/json')) { res.statusCode = 400; res.end(JSON.stringify({ error: 'unsupported content type' })); return; }
212
+
213
+ const chunks: Buffer[] = [];
214
+ let total = 0;
215
+ for await (const chunk of req) {
216
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
217
+ total += buf.length;
218
+ if (total > 1_048_576) { res.statusCode = 413; res.end(JSON.stringify({ error: 'too large' })); return; }
219
+ chunks.push(buf);
220
+ }
221
+ const rawBody = Buffer.concat(chunks);
222
+ const entries = sharedTargets.get(path) ?? [];
223
+ for (const entry of entries) {
224
+ const resp = await entry.handler({ body: rawBody, headers: mapHeaders(req) });
225
+ if (resp.status < 400) {
226
+ res.statusCode = resp.status;
227
+ if (resp.headers) for (const [k, v] of Object.entries(resp.headers)) res.setHeader(k, v as string);
228
+ res.end(resp.body);
229
+ return;
230
+ }
231
+ }
232
+ res.statusCode = 401; res.end(JSON.stringify({ error: 'invalid signature' }));
233
+ } catch (err) {
234
+ log.error(`Webhook simple handler error: ${(err as Error).message}`);
235
+ if (!res.headersSent) { res.statusCode = 500; res.end(JSON.stringify({ error: 'internal error' })); }
236
+ }
237
+ }
238
+
239
+ function mapHeaders(req: any): Record<string, string | string[]> {
240
+ const h: Record<string, string | string[]> = {};
241
+ for (const [k, v] of Object.entries(req.headers ?? {})) h[k.toLowerCase()] = v as string | string[];
242
+ return h;
243
+ }
244
+
245
+ function getHeader(req: any, key: string): string | undefined {
246
+ const val = req.headers[key];
247
+ return Array.isArray(val) ? val[0] : val;
248
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Agent workspace 目录解析(动态加载 plugin-sdk/health)
3
+ *
4
+ * 新版本 openclaw 有 openclaw/plugin-sdk/health 导出,
5
+ * 旧版本不可用时 fallback 到 QQ Bot media 目录。
6
+ */
7
+ import { createRequire } from 'node:module';
8
+ import { getQQBotMediaDir } from '../utils/platform.js';
9
+
10
+ const req = createRequire(__filename);
11
+
12
+ let health: {
13
+ resolveAgentWorkspaceDir: (cfg: any, agentId: string) => string;
14
+ resolveDefaultAgentId: (cfg: any) => string;
15
+ } | null = null;
16
+
17
+ export function resolveAgentWorkspace(cfg: any, agentId?: string): string {
18
+ if (!health) {
19
+ try {
20
+ health = req('openclaw/plugin-sdk/health') as typeof health;
21
+ } catch {
22
+ health = null;
23
+ }
24
+ }
25
+ if (health) {
26
+ return health.resolveAgentWorkspaceDir(cfg, agentId ?? health.resolveDefaultAgentId(cfg));
27
+ }
28
+ return getQQBotMediaDir();
29
+ }