@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,41 @@
1
+ /**
2
+ * 审批相关工具函数
3
+ *
4
+ * 审批 payload 判断逻辑,供 channel.ts 和 features/approval-handler 使用。
5
+ */
6
+
7
+ /** 检查 payload 是否为审批消息(execApproval / plugin approval) */
8
+ export function isApprovalPayload(payload: unknown): boolean {
9
+ if (!payload || typeof payload !== 'object') return false;
10
+ const p = payload as Record<string, unknown>;
11
+ const cd = p.channelData;
12
+ if (cd && typeof cd === 'object' && !Array.isArray(cd)) {
13
+ const execApproval = (cd as Record<string, unknown>).execApproval;
14
+ if (execApproval && typeof execApproval === 'object' && !Array.isArray(execApproval)) {
15
+ return true;
16
+ }
17
+ }
18
+ const text = typeof p.text === 'string' ? p.text : '';
19
+ return /(?:Plugin|Exec) approval (?:required|allowed|denied|expired)/i.test(text);
20
+ }
21
+
22
+ /** 审批 ChannelPlugin stub — 空壳实现(实际审批由 features/approval-handler 处理) */
23
+ export const approvalStubs = {
24
+ execApprovals: {
25
+ getInitiatingSurfaceState: () => ({ kind: 'enabled' as const }),
26
+ shouldSuppressForwardingFallback: () => true,
27
+ shouldSuppressLocalPrompt: ({ payload }: { payload: unknown }) => isApprovalPayload(payload),
28
+ buildPendingPayload: () => null,
29
+ buildResolvedPayload: () => null,
30
+ },
31
+ approvals: {
32
+ delivery: {
33
+ hasConfiguredDmRoute: () => true,
34
+ shouldSuppressForwardingFallback: () => true,
35
+ },
36
+ render: {
37
+ exec: { buildPendingPayload: () => null, buildResolvedPayload: () => null },
38
+ plugin: { buildPendingPayload: () => null, buildResolvedPayload: () => null },
39
+ },
40
+ },
41
+ } as const;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 凭证暂存与恢复
3
+ *
4
+ * 解决热更新被打断时 openclaw.json 中 appId/secret 丢失的问题。
5
+ *
6
+ * 存储路径:~/.openclaw/qqbot/data/credential-backup/current.json
7
+ * 使用子目录避免被框架 SQLite 迁移扫描(迁移只匹配 data/credential-backup.json)。
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { getQQBotDataDir } from '../utils/platform.js';
12
+
13
+ const BACKUP_DIR = 'credential-backup';
14
+ const BACKUP_FILENAME = 'current.json';
15
+ const LEGACY_FILENAME = 'credential-backup.json';
16
+
17
+ interface CredentialBackup {
18
+ accountId: string;
19
+ appId: string;
20
+ clientSecret: string;
21
+ savedAt: string;
22
+ }
23
+
24
+ function getBackupPath(): string {
25
+ return path.join(getQQBotDataDir('data'), BACKUP_DIR, BACKUP_FILENAME);
26
+ }
27
+
28
+ /**
29
+ * 保存凭证快照到暂存文件(gateway 成功启动后调用)
30
+ */
31
+ export function saveCredentialBackup(accountId: string, appId: string, clientSecret: string): void {
32
+ if (!appId || !clientSecret) return;
33
+ try {
34
+ const backupPath = getBackupPath();
35
+ const dir = path.dirname(backupPath);
36
+ if (!fs.existsSync(dir)) {
37
+ fs.mkdirSync(dir, { recursive: true });
38
+ }
39
+ const data: CredentialBackup = {
40
+ accountId,
41
+ appId,
42
+ clientSecret,
43
+ savedAt: new Date().toISOString(),
44
+ };
45
+ const tmpPath = backupPath + '.tmp';
46
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
47
+ fs.renameSync(tmpPath, backupPath);
48
+ } catch {
49
+ // 非关键操作,静默忽略
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 从暂存文件读取凭证(仅在配置为空时调用)
55
+ * 返回 null 表示无可用备份
56
+ */
57
+ export function loadCredentialBackup(accountId?: string): CredentialBackup | null {
58
+ try {
59
+ // 优先读新路径
60
+ const backupPath = getBackupPath();
61
+ if (fs.existsSync(backupPath)) {
62
+ const data = readBackupFile(backupPath, accountId);
63
+ if (data) return data;
64
+ }
65
+ // 兼容旧路径(升级过渡)
66
+ const legacyPath = path.join(getQQBotDataDir('data'), LEGACY_FILENAME);
67
+ if (fs.existsSync(legacyPath)) {
68
+ return readBackupFile(legacyPath, accountId);
69
+ }
70
+ return null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function readBackupFile(filePath: string, accountId?: string): CredentialBackup | null {
77
+ const raw = fs.readFileSync(filePath, 'utf8');
78
+ const data: CredentialBackup = JSON.parse(raw);
79
+ if (!data.appId || !data.clientSecret) return null;
80
+ if (accountId && data.accountId !== accountId) return null;
81
+ return data;
82
+ }
@@ -0,0 +1,19 @@
1
+ import { MemoryHistoryStore } from '@tencent-connect/qqbot-nodejs';
2
+ import type { HistoryStore } from '@tencent-connect/qqbot-nodejs';
3
+
4
+ let _store: HistoryStore | null = null;
5
+
6
+ export function getHistoryStore(): HistoryStore {
7
+ if (!_store) _store = new MemoryHistoryStore();
8
+ return _store;
9
+ }
10
+
11
+ /** 用 accountId 前缀隔离多账号,避免同群历史串用 */
12
+ export function historyGroupKey(accountId: string, groupId: string): string {
13
+ return `${accountId}:${groupId}`;
14
+ }
15
+
16
+ /** 清空群历史(dispatch 完成后调用) */
17
+ export function clearGroupHistory(accountId: string, groupId: string): void {
18
+ _store?.clear?.(historyGroupKey(accountId, groupId));
19
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 消息 ID 缓存 — 记录最近处理的 msgId,供被动回复使用。
3
+ *
4
+ * 场景:外部调用发送消息时没有上下文,
5
+ * 从缓存获取最近一条未过期的 msgId 作为被动回复目标。
6
+ */
7
+
8
+ interface CachedMsgId {
9
+ msgId: string;
10
+ timestamp: number;
11
+ }
12
+
13
+ const MAX_PER_TARGET = 10;
14
+ const TTL_GROUP = 5 * 60 * 1000; // 5 分钟
15
+ const TTL_C2C = 30 * 60 * 1000; // 30 分钟
16
+ const MAX_TARGETS = 200;
17
+
18
+ const cache = new Map<string, CachedMsgId[]>();
19
+
20
+ export function cacheMsgId(scope: string, targetId: string, msgId: string): void {
21
+ if (!scope || !targetId || !msgId) return;
22
+ const key = `${scope}:${targetId}`;
23
+ const existing = cache.get(key);
24
+ if (existing) {
25
+ // LRU: 删除再重新插入
26
+ cache.delete(key);
27
+ existing.push({ msgId, timestamp: Date.now() });
28
+ if (existing.length > MAX_PER_TARGET) existing.shift();
29
+ cache.set(key, existing);
30
+ } else {
31
+ cache.set(key, [{ msgId, timestamp: Date.now() }]);
32
+ if (cache.size > MAX_TARGETS) {
33
+ const oldest = cache.keys().next().value;
34
+ if (oldest) cache.delete(oldest);
35
+ }
36
+ }
37
+ }
38
+
39
+ export function getCachedMsgId(scope: string, targetId: string): string | undefined {
40
+ const key = `${scope}:${targetId}`;
41
+ const list = cache.get(key);
42
+ if (!list || list.length === 0) return undefined;
43
+ const now = Date.now();
44
+ const ttl = scope === 'group' ? TTL_GROUP : TTL_C2C;
45
+ for (let i = list.length - 1; i >= 0; i--) {
46
+ if (now - list[i].timestamp < ttl) {
47
+ return list[i].msgId;
48
+ }
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ export function clearMsgIdCache(scope: string, targetId: string): void {
54
+ cache.delete(`${scope}:${targetId}`);
55
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * QQBot CLI Onboarding Adapter
3
+ *
4
+ * 提供 openclaw onboard 命令的交互式配置支持。
5
+ * 从原 src/onboarding.ts 迁移,保持接口兼容。
6
+ */
7
+ import type {
8
+ ChannelOnboardingAdapter,
9
+ OpenClawConfig,
10
+ } from 'openclaw/plugin-sdk';
11
+ import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount } from '../config.js';
12
+
13
+ /**
14
+ * Onboarding adapter — 导出给 ChannelPlugin 使用
15
+ *
16
+ * 注:完整 onboarding 交互逻辑较复杂(约 300 行),
17
+ * 此处仅保留 adapter 骨架,完整实现从原文件迁移。
18
+ */
19
+ export const qqbotOnboardingAdapter: ChannelOnboardingAdapter = {
20
+ getStatus: (ctx) => {
21
+ const cfg = ctx.config as OpenClawConfig;
22
+ const accountIds = listQQBotAccountIds(cfg);
23
+ if (accountIds.length === 0) {
24
+ return { configured: false, accountCount: 0 };
25
+ }
26
+ const firstAccount = resolveQQBotAccount(cfg, accountIds[0]);
27
+ return {
28
+ configured: Boolean(firstAccount.appId && firstAccount.clientSecret),
29
+ accountCount: accountIds.length,
30
+ defaultAccountId: accountIds[0],
31
+ };
32
+ },
33
+ configure: async (ctx) => {
34
+ // 完整实现需要从原 onboarding.ts 迁移交互逻辑
35
+ // 此处返回默认结果
36
+ return { success: false, message: 'Onboarding not yet migrated to new architecture' };
37
+ },
38
+ };
@@ -1,3 +1,7 @@
1
+ import { createPluginLogger } from "../utils/plugin-logger.js";
2
+
3
+ const log = createPluginLogger({ prefix: '[proactive]' });
4
+
1
5
  /**
2
6
  * QQ Bot 主动发送消息模块
3
7
  *
@@ -9,7 +13,7 @@
9
13
 
10
14
  import * as fs from "node:fs";
11
15
  import * as path from "node:path";
12
- import type { ResolvedQQBotAccount } from "./types.js";
16
+ import type { ResolvedQQBotAccount } from "../types.js";
13
17
 
14
18
  // ============ 类型定义(本地) ============
15
19
 
@@ -56,14 +60,9 @@ export interface ListKnownUsersOptions {
56
60
  limit?: number;
57
61
  }
58
62
  import {
59
- getAccessToken,
60
- sendProactiveC2CMessage,
61
- sendProactiveGroupMessage,
62
- sendChannelMessage,
63
- sendC2CImageMessage,
64
- sendGroupImageMessage,
65
- } from "./api.js";
66
- import { resolveQQBotAccount } from "./config.js";
63
+ getBotForAccount,
64
+ } from "../bot-instance.js";
65
+ import { resolveQQBotAccount } from "../config.js";
67
66
  import type { OpenClawConfig } from "openclaw/plugin-sdk";
68
67
 
69
68
  // ============ 用户存储管理 ============
@@ -72,7 +71,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
72
71
  * 已知用户存储
73
72
  * 使用简单的 JSON 文件存储,保存在 .openclaw/qqbot 目录下
74
73
  */
75
- import { getQQBotDataDir } from "./utils/platform.js";
74
+ import { getQQBotDataDir } from "../utils/platform.js";
76
75
 
77
76
  const STORAGE_DIR = getQQBotDataDir("data");
78
77
  const KNOWN_USERS_FILE = path.join(STORAGE_DIR, "known-users.json");
@@ -127,7 +126,7 @@ function loadKnownUsers(): Map<string, KnownUser> {
127
126
  cacheLastModified = fs.statSync(KNOWN_USERS_FILE).mtimeMs;
128
127
  }
129
128
  } catch (err) {
130
- console.error(`[qqbot:proactive] Failed to load known users: ${err}`);
129
+ log.error(`Failed to load known users: ${err}`);
131
130
  }
132
131
 
133
132
  knownUsersCache = users;
@@ -145,7 +144,7 @@ function saveKnownUsers(users: Map<string, KnownUser>): void {
145
144
  cacheLastModified = Date.now();
146
145
  knownUsersCache = users;
147
146
  } catch (err) {
148
- console.error(`[qqbot:proactive] Failed to save known users: ${err}`);
147
+ log.error(`Failed to save known users: ${err}`);
149
148
  }
150
149
  }
151
150
 
@@ -170,7 +169,7 @@ export function recordKnownUser(user: Omit<KnownUser, "firstInteractionAt">): vo
170
169
  });
171
170
 
172
171
  saveKnownUsers(users);
173
- console.log(`[qqbot:proactive] Recorded user: ${key}`);
172
+ log.info(`Recorded user: ${key}`);
174
173
  }
175
174
 
176
175
  /**
@@ -314,44 +313,29 @@ export async function sendProactive(
314
313
  }
315
314
 
316
315
  try {
317
- const accessToken = await getAccessToken(account.appId, account.clientSecret);
318
-
316
+ const bot = getBotForAccount(account.accountId);
317
+ const target = { scope: type as "c2c" | "group", targetId: to };
318
+
319
319
  // 如果有图片,先发送图片
320
320
  if (imageUrl) {
321
321
  try {
322
- if (type === "c2c") {
323
- await sendC2CImageMessage(accessToken, to, imageUrl, undefined, undefined);
324
- } else if (type === "group") {
325
- await sendGroupImageMessage(accessToken, to, imageUrl, undefined, undefined);
326
- }
327
- console.log(`[qqbot:proactive] Sent image to ${type}:${to}`);
322
+ await bot.sendImage(target, { url: imageUrl });
323
+ log.info(`Sent image to ${type}:${to}`);
328
324
  } catch (err) {
329
- console.error(`[qqbot:proactive] Failed to send image: ${err}`);
330
- // 图片发送失败不影响文本发送
325
+ log.error(`Failed to send image: ${err}`);
331
326
  }
332
327
  }
333
328
 
334
- // 发送文本消息
335
- let result: { id: string; timestamp: number | string };
336
-
337
- if (type === "c2c") {
338
- result = await sendProactiveC2CMessage(accessToken, to, text);
339
- } else if (type === "group") {
340
- result = await sendProactiveGroupMessage(accessToken, to, text);
341
- } else if (type === "channel") {
342
- // 频道消息需要 channel_id,这里暂时不支持主动发送
329
+ // 发送文本消息(主动消息:无 msgId)
330
+ if (type === "channel") {
343
331
  return {
344
332
  success: false,
345
333
  error: "Channel proactive messages are not supported. Please use group or c2c.",
346
334
  };
347
- } else {
348
- return {
349
- success: false,
350
- error: `Unknown message type: ${type}`,
351
- };
352
335
  }
353
-
354
- console.log(`[qqbot:proactive] Sent message to ${type}:${to}, id: ${result.id}`);
336
+
337
+ const result = await bot.sendText(target, text);
338
+ log.info(`Sent message to ${type}:${to}, id: ${result.id}`);
355
339
 
356
340
  return {
357
341
  success: true,
@@ -360,7 +344,7 @@ export async function sendProactive(
360
344
  };
361
345
  } catch (err) {
362
346
  const message = err instanceof Error ? err.message : String(err);
363
- console.error(`[qqbot:proactive] Failed to send message: ${message}`);
347
+ log.error(`Failed to send message: ${message}`);
364
348
 
365
349
  return {
366
350
  success: false,
@@ -487,15 +471,9 @@ export async function sendProactiveMessageDirect(
487
471
  }
488
472
 
489
473
  try {
490
- const accessToken = await getAccessToken(account.appId, account.clientSecret);
491
-
492
- let result: { id: string; timestamp: number | string };
493
-
494
- if (type === "c2c") {
495
- result = await sendProactiveC2CMessage(accessToken, to, text);
496
- } else {
497
- result = await sendProactiveGroupMessage(accessToken, to, text);
498
- }
474
+ const bot = getBotForAccount(account.accountId);
475
+ const target = { scope: type, targetId: to };
476
+ const result = await bot.sendText(target, text);
499
477
 
500
478
  return {
501
479
  success: true,
@@ -528,3 +506,44 @@ export function getKnownUsersStats(accountId?: string): {
528
506
  channel: users.filter(u => u.type === "channel").length,
529
507
  };
530
508
  }
509
+
510
+ /**
511
+ * 获取用户的所有群组(某用户在哪些群里交互过)
512
+ *
513
+ * @param accountId 机器人账户 ID
514
+ * @param openid 用户 openid
515
+ * @returns 群组 openid 列表
516
+ */
517
+ export function getUserGroups(accountId: string, openid: string): string[] {
518
+ const users = loadKnownUsers();
519
+ const groups: string[] = [];
520
+ for (const [, user] of users) {
521
+ if (user.accountId !== accountId) continue;
522
+ if (user.type !== 'group') continue;
523
+ // group 类型的 openid 是 groupOpenid,检查该群中是否有此用户交互记录
524
+ // 注:当前数据模型 group 记录的 openid 是 groupOpenid 本身
525
+ // 要实现精确的"用户在哪些群",需要在 recordKnownUser 时同时记录 senderOpenid
526
+ // 这里降级为:列出该账户的所有群 openid
527
+ groups.push(user.openid);
528
+ }
529
+ return [...new Set(groups)];
530
+ }
531
+
532
+ /**
533
+ * 获取群组的所有已知成员
534
+ *
535
+ * @param accountId 机器人账户 ID
536
+ * @param groupOpenid 群组 openid
537
+ * @returns 在该群中交互过的用户列表
538
+ */
539
+ export function getGroupMembers(accountId: string, groupOpenid: string): KnownUser[] {
540
+ const users = loadKnownUsers();
541
+ const members: KnownUser[] = [];
542
+ for (const [, user] of users) {
543
+ if (user.accountId !== accountId) continue;
544
+ if (user.type === 'group' && user.openid === groupOpenid) {
545
+ members.push(user);
546
+ }
547
+ }
548
+ return members;
549
+ }