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