@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,144 @@
1
+ /**
2
+ * Lint: 检测 adapter/ 外部直接访问 runtime.channel.* 的代码
3
+ *
4
+ * 运行方式:
5
+ * npx tsx src/adapter/lint-runtime-access.ts
6
+ *
7
+ * 退出码:
8
+ * 0 = 无违规
9
+ * 1 = 存在未加白的直接 runtime 访问
10
+ *
11
+ * 加白方式(行内注释):
12
+ * // @adapter-bypass: <理由>
13
+ *
14
+ * 示例:
15
+ * const tts = (runtime as any)?.tts; // @adapter-bypass: TTS 非 channel API,无需经过适配层
16
+ */
17
+
18
+ import * as fs from 'node:fs';
19
+ import * as path from 'node:path';
20
+
21
+ // ── 配置 ──
22
+
23
+ const SRC_ROOT = path.resolve(import.meta.dirname, '..');
24
+ const ADAPTER_DIR = path.resolve(SRC_ROOT, 'adapter');
25
+
26
+ /** 匹配直接使用 runtime.channel / runtime.config / (runtime as any) 的模式 */
27
+ const VIOLATION_PATTERNS: RegExp[] = [
28
+ /runtime\.channel\b/,
29
+ /runtime\.config\b/,
30
+ /\(runtime\s+as\s+any\)/,
31
+ /channel\??\.(inbound|turn|reply|session|routing|text|media|runtimeContexts)\b/,
32
+ /config\s+as\s*\{.*writeConfigFile/,
33
+ ];
34
+
35
+ /** 加白注释标记 */
36
+ const BYPASS_MARKER = '@adapter-bypass';
37
+
38
+ /** 排除的目录/文件 */
39
+ const EXCLUDE_DIRS = ['adapter', 'node_modules', 'dist', 'tests'];
40
+ const EXCLUDE_FILES = ['types.ts', 'types-augment.d.ts', 'openclaw-plugin-sdk.d.ts'];
41
+
42
+ // ── 扫描逻辑 ──
43
+
44
+ interface Violation {
45
+ file: string;
46
+ line: number;
47
+ content: string;
48
+ pattern: string;
49
+ }
50
+
51
+ function shouldExclude(filePath: string): boolean {
52
+ const relative = path.relative(SRC_ROOT, filePath);
53
+ // 排除 adapter 自身
54
+ if (filePath.startsWith(ADAPTER_DIR)) return true;
55
+ // 排除配置的目录
56
+ for (const dir of EXCLUDE_DIRS) {
57
+ if (relative.startsWith(dir + '/') || relative === dir) return true;
58
+ }
59
+ // 排除特定文件
60
+ const fileName = path.basename(filePath);
61
+ if (EXCLUDE_FILES.includes(fileName)) return true;
62
+ return false;
63
+ }
64
+
65
+ function scanFile(filePath: string): Violation[] {
66
+ const violations: Violation[] = [];
67
+ const content = fs.readFileSync(filePath, 'utf8');
68
+ const lines = content.split('\n');
69
+
70
+ for (let i = 0; i < lines.length; i++) {
71
+ const line = lines[i];
72
+ const lineNum = i + 1;
73
+
74
+ // 跳过注释行(单行注释、JSDoc 行、多行注释体)
75
+ const trimmed = line.trimStart();
76
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/**') || trimmed.startsWith('*/')) continue;
77
+
78
+ // 检查加白标记
79
+ if (line.includes(BYPASS_MARKER)) continue;
80
+
81
+ // 检查违规模式
82
+ for (const pattern of VIOLATION_PATTERNS) {
83
+ if (pattern.test(line)) {
84
+ violations.push({
85
+ file: path.relative(SRC_ROOT, filePath),
86
+ line: lineNum,
87
+ content: line.trim(),
88
+ pattern: pattern.source,
89
+ });
90
+ break; // 同一行只报一次
91
+ }
92
+ }
93
+ }
94
+
95
+ return violations;
96
+ }
97
+
98
+ function walkDir(dir: string): string[] {
99
+ const results: string[] = [];
100
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
101
+ for (const entry of entries) {
102
+ const fullPath = path.join(dir, entry.name);
103
+ if (entry.isDirectory()) {
104
+ if (!EXCLUDE_DIRS.includes(entry.name)) {
105
+ results.push(...walkDir(fullPath));
106
+ }
107
+ } else if (entry.isFile() && entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
108
+ results.push(fullPath);
109
+ }
110
+ }
111
+ return results;
112
+ }
113
+
114
+ // ── 主流程 ──
115
+
116
+ function main(): void {
117
+ const files = walkDir(SRC_ROOT).filter((f) => !shouldExclude(f));
118
+ const allViolations: Violation[] = [];
119
+
120
+ for (const file of files) {
121
+ const violations = scanFile(file);
122
+ allViolations.push(...violations);
123
+ }
124
+
125
+ if (allViolations.length === 0) {
126
+ console.log('✅ No direct runtime access found outside adapter/');
127
+ process.exit(0);
128
+ }
129
+
130
+ console.error(`❌ Found ${allViolations.length} direct runtime access violation(s):\n`);
131
+ for (const v of allViolations) {
132
+ console.error(` ${v.file}:${v.line}`);
133
+ console.error(` ${v.content}`);
134
+ console.error(` → matched: ${v.pattern}`);
135
+ console.error(` → fix: use adapter or add "// @adapter-bypass: <reason>"\n`);
136
+ }
137
+ console.error(
138
+ `\nTo bypass a specific line, add an inline comment:\n` +
139
+ ` // @adapter-bypass: <reason why this cannot go through the adapter>\n`,
140
+ );
141
+ process.exit(1);
142
+ }
143
+
144
+ main();
@@ -0,0 +1,112 @@
1
+ /**
2
+ * 远程媒体下载(动态加载 plugin-sdk/media-runtime,降级到 fetch)
3
+ *
4
+ * 新版本 openclaw 有 openclaw/plugin-sdk/media-runtime(含 SSRF 防护/重试/大小限制),
5
+ * 旧版本不可用时降级到原生 fetch 直连。
6
+ */
7
+ import { createRequire } from 'node:module';
8
+ import * as dns from 'node:dns';
9
+ import * as path from 'node:path';
10
+ import * as fs from 'node:fs';
11
+ import * as crypto from 'node:crypto';
12
+ import { getQQBotMediaDir } from '../utils/platform.js';
13
+
14
+ const req = createRequire(__filename);
15
+
16
+ type SaveRemoteMedia = (opts: {
17
+ url: string;
18
+ subdir?: string;
19
+ originalFilename?: string;
20
+ maxBytes?: number;
21
+ timeoutMs?: number;
22
+ }) => Promise<{ path: string }>;
23
+
24
+ let _save: SaveRemoteMedia | null | undefined;
25
+
26
+ export function downloadRemoteMedia(opts: {
27
+ url: string;
28
+ subdir?: string;
29
+ originalFilename?: string;
30
+ maxBytes?: number;
31
+ timeoutMs?: number;
32
+ }): Promise<{ path: string }> {
33
+ if (_save === undefined) {
34
+ try {
35
+ const mod = req('openclaw/plugin-sdk/media-runtime') as { saveRemoteMedia: SaveRemoteMedia };
36
+ _save = mod.saveRemoteMedia;
37
+ } catch {
38
+ _save = null;
39
+ }
40
+ }
41
+ return _save ? _save(opts) : downloadViaFetch(opts);
42
+ }
43
+
44
+ // ── SSRF 防护 ──
45
+
46
+ const PRIVATE_RANGES: Array<[netmask: bigint, prefix: number]> = [
47
+ [0x0A000000n, 8], // 10.0.0.0/8
48
+ [0xAC100000n, 12], // 172.16.0.0/12
49
+ [0xC0A80000n, 16], // 192.168.0.0/16
50
+ [0x7F000000n, 8], // 127.0.0.0/8
51
+ [0xA9FE0000n, 16], // 169.254.0.0/16
52
+ [0xE0000000n, 4], // 224.0.0.0/4 (multicast)
53
+ ];
54
+
55
+ function ipToBigInt(ip: string): bigint {
56
+ return ip.split('.').reduce((acc, octet) => (acc << 8n) | BigInt(Number(octet)), 0n);
57
+ }
58
+
59
+ function isPrivateIP(ip: string): boolean {
60
+ const val = ipToBigInt(ip);
61
+ return PRIVATE_RANGES.some(([mask, prefix]) => (val >> (32n - BigInt(prefix))) === (mask >> (32n - BigInt(prefix))));
62
+ }
63
+
64
+ async function assertSafeHostname(hostname: string): Promise<void> {
65
+ const addresses = await dns.promises.resolve4(hostname).catch(() => []);
66
+ if (addresses.length === 0) throw new Error(`DNS resolution failed: ${hostname}`);
67
+ for (const addr of addresses) {
68
+ if (isPrivateIP(addr)) {
69
+ throw new Error(`SSRF blocked: ${hostname} resolves to private IP ${addr}`);
70
+ }
71
+ }
72
+ }
73
+
74
+ // ── 降级 fetch ──
75
+
76
+ /** 降级:原生 fetch 直连(含 SSRF 防护、大小限制) */
77
+ async function downloadViaFetch(opts: {
78
+ url: string;
79
+ subdir?: string;
80
+ originalFilename?: string;
81
+ maxBytes?: number;
82
+ timeoutMs?: number;
83
+ }): Promise<{ path: string }> {
84
+ const parsed = new URL(opts.url);
85
+ if (parsed.protocol !== 'https:') {
86
+ throw new Error(`Only HTTPS allowed: ${parsed.protocol}`);
87
+ }
88
+ await assertSafeHostname(parsed.hostname);
89
+
90
+ const dir = getQQBotMediaDir(opts.subdir ?? 'downloads');
91
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
92
+
93
+ const resp = await fetch(opts.url, {
94
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 120_000),
95
+ });
96
+ if (!resp.ok) throw new Error(`Download HTTP ${resp.status}`);
97
+
98
+ const maxBytes = opts.maxBytes ?? 500 * 1024 * 1024;
99
+ const buf = Buffer.from(await resp.arrayBuffer());
100
+ if (buf.length > maxBytes) throw new Error(`Download exceeds ${(maxBytes / 1024 / 1024).toFixed(0)}MB`);
101
+
102
+ const ext = opts.originalFilename
103
+ ? path.extname(opts.originalFilename) || '.bin'
104
+ : '.bin';
105
+ const name = opts.originalFilename
106
+ ? path.basename(opts.originalFilename, path.extname(opts.originalFilename))
107
+ : 'download';
108
+ const rand = crypto.randomBytes(4).toString('hex');
109
+ const filePath = path.join(dir, `${name}_${Date.now()}_${rand}${ext}`);
110
+ fs.writeFileSync(filePath, buf);
111
+ return { path: filePath };
112
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Pairing Runtime 动态加载(兼容旧版框架)
3
+ *
4
+ * 从 OpenClaw 安装目录动态加载 conversation-runtime 模块中的配对函数。
5
+ * 不支持时返回 null,配对功能降级为不可用。
6
+ */
7
+ import { createRequire } from 'node:module';
8
+ import path from 'node:path';
9
+
10
+ export interface PairingApi {
11
+ readAllowFromStore: (params: { channel: string; accountId: string }) => Promise<string[]>;
12
+ issueChallenge: (params: { channel: string; id: string; accountId: string }) => Promise<{ code: string }>;
13
+ buildReply: (params: { code: string; channel: string }) => string;
14
+ approveCode: (params: { channel: string; code: string; accountId?: string }) => Promise<{ id: string } | null>;
15
+ }
16
+
17
+ let _api: PairingApi | null | undefined;
18
+
19
+ /** 获取 Pairing API,首次调用触发加载并缓存 */
20
+ export function getPairingApi(): PairingApi | null {
21
+ if (_api !== undefined) return _api;
22
+ _api = loadPairingApi();
23
+ return _api;
24
+ }
25
+
26
+ function loadPairingApi(): PairingApi | null {
27
+ const currentFile = __filename;
28
+ const req = createRequire(currentFile);
29
+ const pluginRoot = path.resolve(path.dirname(currentFile), '..', '..');
30
+ const fs = req('node:fs') as typeof import('node:fs');
31
+
32
+ const tryLoad = (root: string) => {
33
+ for (const rel of ['dist/plugin-sdk/conversation-runtime.js', 'plugin-sdk/conversation-runtime.js']) {
34
+ const p = path.join(root, rel);
35
+ try {
36
+ if (fs.existsSync(p)) return req(p);
37
+ } catch { /* try next */ }
38
+ }
39
+ return null;
40
+ };
41
+
42
+ let mod: any = null;
43
+ try {
44
+ const { findOpenclawRoot } = req(path.join(pluginRoot, 'scripts', 'link-sdk-core.cjs')) as {
45
+ findOpenclawRoot: (root: string) => string | null;
46
+ };
47
+ const root = findOpenclawRoot(pluginRoot);
48
+ if (root) mod = tryLoad(root);
49
+ } catch { /* fallback */ }
50
+
51
+ if (!mod) {
52
+ try {
53
+ const entry = process.argv[1];
54
+ if (entry) {
55
+ const realEntry = fs.realpathSync(entry);
56
+ let dir = path.dirname(realEntry);
57
+ for (let i = 0; i < 6; i++) {
58
+ mod = tryLoad(dir);
59
+ if (mod) break;
60
+ const parent = path.dirname(dir);
61
+ if (parent === dir) break;
62
+ dir = parent;
63
+ }
64
+ }
65
+ } catch { /* fallback */ }
66
+ }
67
+
68
+ if (!mod?.readChannelAllowFromStore) return null;
69
+
70
+ return {
71
+ // readChannelAllowFromStore(channel, env?, accountId?) — 位置参数
72
+ readAllowFromStore: (params) =>
73
+ mod.readChannelAllowFromStore(params.channel, undefined, params.accountId),
74
+
75
+ // upsertChannelPairingRequest({ channel, id, accountId }) — 对象参数
76
+ issueChallenge: (params) =>
77
+ mod.upsertChannelPairingRequest({
78
+ channel: params.channel,
79
+ id: params.id,
80
+ accountId: params.accountId,
81
+ }).then((r: any) => ({ code: r.code as string })),
82
+
83
+ // buildPairingReply({ channel, idLine, code }) — 对象参数
84
+ buildReply: (params) =>
85
+ mod.buildPairingReply({
86
+ channel: params.channel,
87
+ idLine: '', // qqbot 无额外 ID 信息,留空即可
88
+ code: params.code,
89
+ }),
90
+
91
+ // approveChannelPairingCode({ channel, code, accountId? }) — 对象参数
92
+ approveCode: (params) =>
93
+ mod.approveChannelPairingCode({
94
+ channel: params.channel,
95
+ code: params.code,
96
+ accountId: params.accountId,
97
+ }),
98
+ };
99
+ }
@@ -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
+