@jerryliang122/openclaw-qqbot 1.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 (120) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +967 -0
  3. package/README.zh.md +790 -0
  4. package/dist/index.cjs +18072 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +1222 -0
  7. package/index.ts +92 -0
  8. package/openclaw.plugin.json +38 -0
  9. package/package.json +69 -0
  10. package/preload.cjs +19 -0
  11. package/scripts/link-sdk-core.cjs +268 -0
  12. package/scripts/proactive-api-server.ts +369 -0
  13. package/scripts/send-proactive.ts +293 -0
  14. package/scripts/test-sendmedia.ts +116 -0
  15. package/skills/qqbot-channel/SKILL.md +285 -0
  16. package/skills/qqbot-channel/references/api_references.md +521 -0
  17. package/skills/qqbot-remind/SKILL.md +159 -0
  18. package/skills/qqbot-upgrade/SKILL.md +56 -0
  19. package/src/adapter/contract.ts +63 -0
  20. package/src/adapter/lint.ts +144 -0
  21. package/src/adapter/media.ts +40 -0
  22. package/src/adapter/pairing.ts +95 -0
  23. package/src/adapter/resolve.ts +255 -0
  24. package/src/adapter/setup.ts +13 -0
  25. package/src/adapter/webhook.ts +248 -0
  26. package/src/adapter/workspace.ts +21 -0
  27. package/src/agent-prompt-adapter.ts +26 -0
  28. package/src/bot-instance.ts +60 -0
  29. package/src/channel.ts +230 -0
  30. package/src/commands/bot-approve.ts +143 -0
  31. package/src/commands/bot-clear-storage.ts +114 -0
  32. package/src/commands/bot-group-always.ts +62 -0
  33. package/src/commands/bot-group-info.ts +48 -0
  34. package/src/commands/bot-help.ts +40 -0
  35. package/src/commands/bot-logs.ts +248 -0
  36. package/src/commands/bot-me.ts +18 -0
  37. package/src/commands/bot-pairing.ts +50 -0
  38. package/src/commands/bot-ping.ts +33 -0
  39. package/src/commands/bot-streaming.ts +55 -0
  40. package/src/commands/bot-upgrade.ts +56 -0
  41. package/src/commands/bot-version.ts +41 -0
  42. package/src/commands/config-util.ts +96 -0
  43. package/src/commands/index.ts +51 -0
  44. package/src/config.ts +403 -0
  45. package/src/constants.ts +6 -0
  46. package/src/dispatch/body-assembler.ts +308 -0
  47. package/src/dispatch/ctx-builder.ts +127 -0
  48. package/src/dispatch/dispatch.ts +667 -0
  49. package/src/dispatch/envelope-builder.ts +112 -0
  50. package/src/dispatch/index.ts +2 -0
  51. package/src/features/approval-capability.ts +302 -0
  52. package/src/features/approval-helpers.ts +271 -0
  53. package/src/features/approval-utils.ts +21 -0
  54. package/src/features/command-panel.ts +301 -0
  55. package/src/features/credential-backup.ts +74 -0
  56. package/src/features/group-mode-store.ts +79 -0
  57. package/src/features/history-store.ts +75 -0
  58. package/src/features/msgid-cache.ts +55 -0
  59. package/src/features/outbound-echo-store.ts +46 -0
  60. package/src/features/proactive-budget.ts +57 -0
  61. package/src/features/proactive.ts +549 -0
  62. package/src/features/question-helpers.ts +771 -0
  63. package/src/features/quota-manager.ts +173 -0
  64. package/src/features/ref-index-store.ts +289 -0
  65. package/src/features/secret-input-store.ts +118 -0
  66. package/src/features/secret-store-cli.ts +324 -0
  67. package/src/features/typing-refresh.ts +51 -0
  68. package/src/features/update-checker.ts +166 -0
  69. package/src/gateway/event-handlers.ts +456 -0
  70. package/src/gateway/index.ts +3 -0
  71. package/src/gateway/lifecycle.ts +236 -0
  72. package/src/gateway/middleware-setup.ts +173 -0
  73. package/src/gateway/qqbot-gateway.ts +458 -0
  74. package/src/gateway-adapter.ts +44 -0
  75. package/src/heartbeat-adapter.ts +57 -0
  76. package/src/message-adapter.ts +40 -0
  77. package/src/messaging-adapter.ts +78 -0
  78. package/src/middleware/access-control.ts +125 -0
  79. package/src/middleware/attachment.ts +373 -0
  80. package/src/middleware/inbound-guard.ts +102 -0
  81. package/src/middleware/policy-injector.ts +71 -0
  82. package/src/middleware/secret-capture.ts +161 -0
  83. package/src/middleware/typing.ts +110 -0
  84. package/src/openclaw-plugin-sdk.d.ts +543 -0
  85. package/src/outbound/chunker.ts +80 -0
  86. package/src/outbound/debounce.ts +102 -0
  87. package/src/outbound/deliver-pipeline.ts +235 -0
  88. package/src/outbound/index.ts +3 -0
  89. package/src/outbound/local-file-router.ts +145 -0
  90. package/src/outbound/media-send.ts +408 -0
  91. package/src/outbound/outbound-service.ts +298 -0
  92. package/src/outbound/reply-limiter.ts +139 -0
  93. package/src/outbound/sanitize.ts +32 -0
  94. package/src/outbound/streaming-controller.ts +332 -0
  95. package/src/outbound/target.ts +109 -0
  96. package/src/outbound-adapter.ts +323 -0
  97. package/src/plugin-base.ts +42 -0
  98. package/src/request-context.ts +50 -0
  99. package/src/runtime.ts +42 -0
  100. package/src/setup/account-key.ts +41 -0
  101. package/src/setup/finalize.ts +110 -0
  102. package/src/setup/login.ts +197 -0
  103. package/src/setup/surface.ts +40 -0
  104. package/src/status-adapter.ts +56 -0
  105. package/src/tools/platform.ts +149 -0
  106. package/src/tools/remind.ts +308 -0
  107. package/src/tools/secret-input.ts +185 -0
  108. package/src/types-augment.d.ts +54 -0
  109. package/src/types-plugin.ts +82 -0
  110. package/src/types.ts +620 -0
  111. package/src/typing-lifecycle.ts +182 -0
  112. package/src/utils/mention.ts +52 -0
  113. package/src/utils/pkg-version.ts +23 -0
  114. package/src/utils/platform.ts +459 -0
  115. package/src/utils/plugin-logger.ts +104 -0
  116. package/src/utils/ssrf-guard.ts +132 -0
  117. package/src/utils/stt.ts +150 -0
  118. package/src/utils/voice-text.ts +61 -0
  119. package/tsconfig.json +17 -0
  120. package/tsup.config.ts +64 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 主动消息每日预算观测
3
+ *
4
+ * QQ Bot 平台主动消息(不带 msg_id)有每日额度(接收方维度约 1000 条/天),
5
+ * 被动回复(带 msg_id)不占此预算。插件全链路被动优先(msgid-cache 挂接 +
6
+ * 配额预检),主动仅在「静群超 TTL」或「被动配额耗尽」时触发——本模块对
7
+ * 这些残余的主动发送按账号计数,接近阈值告警,供排障与预算复核。
8
+ */
9
+ import { createPluginLogger } from '../utils/plugin-logger.js';
10
+
11
+ const log = createPluginLogger({ prefix: '[proactive-budget]' });
12
+
13
+ /** 平台每日主动消息额度(保守取 1000) */
14
+ export const DAILY_PROACTIVE_LIMIT = 1000;
15
+
16
+ /** 告警阈值比例(80%)与之后的步进 */
17
+ const WARN_RATIO = 0.8;
18
+ const WARN_EVERY = 100;
19
+
20
+ const counters = new Map<string, { date: string; count: number }>();
21
+
22
+ function today(): string {
23
+ return new Date().toISOString().slice(0, 10);
24
+ }
25
+
26
+ /** 记录一次主动发送(无 msg_id 的出站);跨天自动重置 */
27
+ export function recordProactiveSend(accountId: string): void {
28
+ let counter = counters.get(accountId);
29
+ if (!counter || counter.date !== today()) {
30
+ counter = { date: today(), count: 0 };
31
+ counters.set(accountId, counter);
32
+ }
33
+ counter.count += 1;
34
+
35
+ const warnAt = Math.floor(DAILY_PROACTIVE_LIMIT * WARN_RATIO);
36
+ if (counter.count === warnAt || (counter.count > warnAt && counter.count % WARN_EVERY === 0)) {
37
+ log.warn(
38
+ `proactive daily budget ${counter.count}/${DAILY_PROACTIVE_LIMIT} used (accountId=${accountId}); ` +
39
+ `passive replies (msg_id) don't count — check msgid-cache TTL / passive quota if unexpected`,
40
+ );
41
+ }
42
+ }
43
+
44
+ /** 查询当日用量(诊断/命令展示用) */
45
+ export function getProactiveUsage(accountId: string): { date: string; count: number; limit: number } {
46
+ const counter = counters.get(accountId);
47
+ return {
48
+ date: counter?.date ?? today(),
49
+ count: counter?.date === today() ? counter.count : 0,
50
+ limit: DAILY_PROACTIVE_LIMIT,
51
+ };
52
+ }
53
+
54
+ /** 测试用:清空计数 */
55
+ export function _resetProactiveBudget(): void {
56
+ counters.clear();
57
+ }
@@ -0,0 +1,549 @@
1
+ import { createPluginLogger } from "../utils/plugin-logger.js";
2
+
3
+ const log = createPluginLogger({ prefix: '[proactive]' });
4
+
5
+ /**
6
+ * QQ Bot 主动发送消息模块
7
+ *
8
+ * 该模块提供以下能力:
9
+ * 1. 记录已知用户(曾与机器人交互过的用户)
10
+ * 2. 主动发送消息给用户或群组
11
+ * 3. 查询已知用户列表
12
+ */
13
+
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+ import type { ResolvedQQBotAccount } from "../types.js";
17
+
18
+ // ============ 类型定义(本地) ============
19
+
20
+ /**
21
+ * 已知用户信息
22
+ */
23
+ export interface KnownUser {
24
+ type: "c2c" | "group" | "channel";
25
+ openid: string;
26
+ accountId: string;
27
+ nickname?: string;
28
+ firstInteractionAt: number;
29
+ lastInteractionAt: number;
30
+ }
31
+
32
+ /**
33
+ * 主动发送消息选项
34
+ */
35
+ export interface ProactiveSendOptions {
36
+ to: string;
37
+ text: string;
38
+ type?: "c2c" | "group" | "channel";
39
+ imageUrl?: string;
40
+ accountId?: string;
41
+ }
42
+
43
+ /**
44
+ * 主动发送消息结果
45
+ */
46
+ export interface ProactiveSendResult {
47
+ success: boolean;
48
+ messageId?: string;
49
+ timestamp?: number | string;
50
+ error?: string;
51
+ }
52
+
53
+ /**
54
+ * 列出已知用户选项
55
+ */
56
+ export interface ListKnownUsersOptions {
57
+ type?: "c2c" | "group" | "channel";
58
+ accountId?: string;
59
+ sortByLastInteraction?: boolean;
60
+ limit?: number;
61
+ }
62
+ import {
63
+ getBotForAccount,
64
+ } from "../bot-instance.js";
65
+ import { resolveQQBotAccount } from "../config.js";
66
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
67
+
68
+ // ============ 用户存储管理 ============
69
+
70
+ /**
71
+ * 已知用户存储
72
+ * 使用简单的 JSON 文件存储,保存在 .openclaw/qqbot 目录下
73
+ */
74
+ import { getQQBotDataDir } from "../utils/platform.js";
75
+
76
+ const STORAGE_DIR = getQQBotDataDir("data");
77
+ const KNOWN_USERS_FILE = path.join(STORAGE_DIR, "known-users.json");
78
+
79
+ // 内存缓存
80
+ let knownUsersCache: Map<string, KnownUser> | null = null;
81
+ let cacheLastModified = 0;
82
+
83
+ /**
84
+ * 确保存储目录存在
85
+ */
86
+ function ensureStorageDir(): void {
87
+ if (!fs.existsSync(STORAGE_DIR)) {
88
+ fs.mkdirSync(STORAGE_DIR, { recursive: true });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 生成用户唯一键
94
+ */
95
+ function getUserKey(type: string, openid: string, accountId: string): string {
96
+ return `${accountId}:${type}:${openid}`;
97
+ }
98
+
99
+ /**
100
+ * 从文件加载已知用户
101
+ */
102
+ function loadKnownUsers(): Map<string, KnownUser> {
103
+ if (knownUsersCache !== null) {
104
+ // 检查文件是否被修改
105
+ try {
106
+ const stat = fs.statSync(KNOWN_USERS_FILE);
107
+ if (stat.mtimeMs <= cacheLastModified) {
108
+ return knownUsersCache;
109
+ }
110
+ } catch {
111
+ // 文件不存在,使用缓存
112
+ return knownUsersCache;
113
+ }
114
+ }
115
+
116
+ const users = new Map<string, KnownUser>();
117
+
118
+ try {
119
+ if (fs.existsSync(KNOWN_USERS_FILE)) {
120
+ const data = fs.readFileSync(KNOWN_USERS_FILE, "utf-8");
121
+ const parsed = JSON.parse(data) as KnownUser[];
122
+ for (const user of parsed) {
123
+ const key = getUserKey(user.type, user.openid, user.accountId);
124
+ users.set(key, user);
125
+ }
126
+ cacheLastModified = fs.statSync(KNOWN_USERS_FILE).mtimeMs;
127
+ }
128
+ } catch (err) {
129
+ log.error(`Failed to load known users: ${err}`);
130
+ }
131
+
132
+ knownUsersCache = users;
133
+ return users;
134
+ }
135
+
136
+ /**
137
+ * 保存已知用户到文件
138
+ */
139
+ function saveKnownUsers(users: Map<string, KnownUser>): void {
140
+ try {
141
+ ensureStorageDir();
142
+ const data = Array.from(users.values());
143
+ fs.writeFileSync(KNOWN_USERS_FILE, JSON.stringify(data, null, 2), "utf-8");
144
+ cacheLastModified = Date.now();
145
+ knownUsersCache = users;
146
+ } catch (err) {
147
+ log.error(`Failed to save known users: ${err}`);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * 记录一个已知用户(当收到用户消息时调用)
153
+ *
154
+ * @param user - 用户信息
155
+ */
156
+ export function recordKnownUser(user: Omit<KnownUser, "firstInteractionAt">): void {
157
+ const users = loadKnownUsers();
158
+ const key = getUserKey(user.type, user.openid, user.accountId);
159
+
160
+ const existing = users.get(key);
161
+ const now = user.lastInteractionAt || Date.now();
162
+
163
+ users.set(key, {
164
+ ...user,
165
+ lastInteractionAt: now,
166
+ firstInteractionAt: existing?.firstInteractionAt ?? now,
167
+ // 更新昵称(如果有新的)
168
+ nickname: user.nickname || existing?.nickname,
169
+ });
170
+
171
+ saveKnownUsers(users);
172
+ log.info(`Recorded user: ${key}`);
173
+ }
174
+
175
+ /**
176
+ * 获取一个已知用户
177
+ *
178
+ * @param type - 用户类型
179
+ * @param openid - 用户 openid
180
+ * @param accountId - 账户 ID
181
+ */
182
+ export function getKnownUser(type: string, openid: string, accountId: string): KnownUser | undefined {
183
+ const users = loadKnownUsers();
184
+ const key = getUserKey(type, openid, accountId);
185
+ return users.get(key);
186
+ }
187
+
188
+ /**
189
+ * 列出已知用户
190
+ *
191
+ * @param options - 过滤选项
192
+ */
193
+ export function listKnownUsers(options?: ListKnownUsersOptions): KnownUser[] {
194
+ const users = loadKnownUsers();
195
+ let result = Array.from(users.values());
196
+
197
+ // 过滤类型
198
+ if (options?.type) {
199
+ result = result.filter(u => u.type === options.type);
200
+ }
201
+
202
+ // 过滤账户
203
+ if (options?.accountId) {
204
+ result = result.filter(u => u.accountId === options.accountId);
205
+ }
206
+
207
+ // 排序
208
+ if (options?.sortByLastInteraction !== false) {
209
+ result.sort((a, b) => b.lastInteractionAt - a.lastInteractionAt);
210
+ }
211
+
212
+ // 限制数量
213
+ if (options?.limit && options.limit > 0) {
214
+ result = result.slice(0, options.limit);
215
+ }
216
+
217
+ return result;
218
+ }
219
+
220
+ /**
221
+ * 删除一个已知用户
222
+ *
223
+ * @param type - 用户类型
224
+ * @param openid - 用户 openid
225
+ * @param accountId - 账户 ID
226
+ */
227
+ export function removeKnownUser(type: string, openid: string, accountId: string): boolean {
228
+ const users = loadKnownUsers();
229
+ const key = getUserKey(type, openid, accountId);
230
+ const deleted = users.delete(key);
231
+ if (deleted) {
232
+ saveKnownUsers(users);
233
+ }
234
+ return deleted;
235
+ }
236
+
237
+ /**
238
+ * 清除所有已知用户
239
+ *
240
+ * @param accountId - 可选,只清除指定账户的用户
241
+ */
242
+ export function clearKnownUsers(accountId?: string): number {
243
+ const users = loadKnownUsers();
244
+ let count = 0;
245
+
246
+ if (accountId) {
247
+ for (const [key, user] of users) {
248
+ if (user.accountId === accountId) {
249
+ users.delete(key);
250
+ count++;
251
+ }
252
+ }
253
+ } else {
254
+ count = users.size;
255
+ users.clear();
256
+ }
257
+
258
+ if (count > 0) {
259
+ saveKnownUsers(users);
260
+ }
261
+ return count;
262
+ }
263
+
264
+ // ============ 主动发送消息 ============
265
+
266
+ /**
267
+ * 主动发送消息(带配置解析)
268
+ * 注意:与 outbound.ts 中的 sendProactiveMessage 不同,这个函数接受 OpenClawConfig 并自动解析账户
269
+ *
270
+ * @param options - 发送选项
271
+ * @param cfg - OpenClaw 配置
272
+ * @returns 发送结果
273
+ *
274
+ * @example
275
+ * ```typescript
276
+ * // 发送私聊消息
277
+ * const result = await sendProactive({
278
+ * to: "E7A8F3B2C1D4E5F6A7B8C9D0E1F2A3B4", // 用户 openid
279
+ * text: "你好!这是一条主动消息",
280
+ * type: "c2c",
281
+ * }, cfg);
282
+ *
283
+ * // 发送群聊消息
284
+ * const result = await sendProactive({
285
+ * to: "A1B2C3D4E5F6A7B8", // 群组 openid
286
+ * text: "群公告:今天有活动",
287
+ * type: "group",
288
+ * }, cfg);
289
+ *
290
+ * // 发送带图片的消息
291
+ * const result = await sendProactive({
292
+ * to: "E7A8F3B2C1D4E5F6A7B8C9D0E1F2A3B4",
293
+ * text: "看看这张图片",
294
+ * imageUrl: "https://example.com/image.png",
295
+ * type: "c2c",
296
+ * }, cfg);
297
+ * ```
298
+ */
299
+ export async function sendProactive(
300
+ options: ProactiveSendOptions,
301
+ cfg: OpenClawConfig
302
+ ): Promise<ProactiveSendResult> {
303
+ const { to, text, type = "c2c", imageUrl, accountId = "default" } = options;
304
+
305
+ // 解析账户配置
306
+ const account = resolveQQBotAccount(cfg, accountId);
307
+
308
+ if (!account.appId || !account.clientSecret) {
309
+ return {
310
+ success: false,
311
+ error: "QQBot not configured (missing appId or clientSecret)",
312
+ };
313
+ }
314
+
315
+ try {
316
+ const bot = getBotForAccount(account.accountId);
317
+ const target = { scope: type as "c2c" | "group", targetId: to };
318
+
319
+ // 如果有图片,先发送图片
320
+ if (imageUrl) {
321
+ try {
322
+ await bot.sendImage(target, { url: imageUrl });
323
+ log.info(`Sent image to ${type}:${to}`);
324
+ } catch (err) {
325
+ log.error(`Failed to send image: ${err}`);
326
+ }
327
+ }
328
+
329
+ // 发送文本消息(主动消息:无 msgId)
330
+ if (type === "channel") {
331
+ return {
332
+ success: false,
333
+ error: "Channel proactive messages are not supported. Please use group or c2c.",
334
+ };
335
+ }
336
+
337
+ const result = await bot.sendText(target, text);
338
+ log.info(`Sent message to ${type}:${to}, id: ${result.id}`);
339
+
340
+ return {
341
+ success: true,
342
+ messageId: result.id,
343
+ timestamp: result.timestamp,
344
+ };
345
+ } catch (err) {
346
+ const message = err instanceof Error ? err.message : String(err);
347
+ log.error(`Failed to send message: ${message}`);
348
+
349
+ return {
350
+ success: false,
351
+ error: message,
352
+ };
353
+ }
354
+ }
355
+
356
+ /**
357
+ * 批量发送主动消息
358
+ *
359
+ * @param recipients - 接收者列表(openid 数组)
360
+ * @param text - 消息内容
361
+ * @param type - 消息类型
362
+ * @param cfg - OpenClaw 配置
363
+ * @param accountId - 账户 ID
364
+ * @returns 发送结果列表
365
+ */
366
+ export async function sendBulkProactiveMessage(
367
+ recipients: string[],
368
+ text: string,
369
+ type: "c2c" | "group",
370
+ cfg: OpenClawConfig,
371
+ accountId = "default"
372
+ ): Promise<Array<{ to: string; result: ProactiveSendResult }>> {
373
+ const results: Array<{ to: string; result: ProactiveSendResult }> = [];
374
+
375
+ for (const to of recipients) {
376
+ const result = await sendProactive({ to, text, type, accountId }, cfg);
377
+ results.push({ to, result });
378
+
379
+ // 添加延迟,避免频率限制
380
+ await new Promise(resolve => setTimeout(resolve, 500));
381
+ }
382
+
383
+ return results;
384
+ }
385
+
386
+ /**
387
+ * 发送消息给所有已知用户
388
+ *
389
+ * @param text - 消息内容
390
+ * @param cfg - OpenClaw 配置
391
+ * @param options - 过滤选项
392
+ * @returns 发送结果统计
393
+ */
394
+ export async function broadcastMessage(
395
+ text: string,
396
+ cfg: OpenClawConfig,
397
+ options?: {
398
+ type?: "c2c" | "group";
399
+ accountId?: string;
400
+ limit?: number;
401
+ }
402
+ ): Promise<{
403
+ total: number;
404
+ success: number;
405
+ failed: number;
406
+ results: Array<{ to: string; result: ProactiveSendResult }>;
407
+ }> {
408
+ const users = listKnownUsers({
409
+ type: options?.type,
410
+ accountId: options?.accountId,
411
+ limit: options?.limit,
412
+ sortByLastInteraction: true,
413
+ });
414
+
415
+ // 过滤掉频道用户(不支持主动发送)
416
+ const validUsers = users.filter(u => u.type === "c2c" || u.type === "group");
417
+
418
+ const results: Array<{ to: string; result: ProactiveSendResult }> = [];
419
+ let success = 0;
420
+ let failed = 0;
421
+
422
+ for (const user of validUsers) {
423
+ const result = await sendProactive({
424
+ to: user.openid,
425
+ text,
426
+ type: user.type as "c2c" | "group",
427
+ accountId: user.accountId,
428
+ }, cfg);
429
+
430
+ results.push({ to: user.openid, result });
431
+
432
+ if (result.success) {
433
+ success++;
434
+ } else {
435
+ failed++;
436
+ }
437
+
438
+ // 添加延迟,避免频率限制
439
+ await new Promise(resolve => setTimeout(resolve, 500));
440
+ }
441
+
442
+ return {
443
+ total: validUsers.length,
444
+ success,
445
+ failed,
446
+ results,
447
+ };
448
+ }
449
+
450
+ // ============ 辅助函数 ============
451
+
452
+ /**
453
+ * 根据账户配置直接发送主动消息(不需要 cfg)
454
+ *
455
+ * @param account - 已解析的账户配置
456
+ * @param to - 目标 openid
457
+ * @param text - 消息内容
458
+ * @param type - 消息类型
459
+ */
460
+ export async function sendProactiveMessageDirect(
461
+ account: ResolvedQQBotAccount,
462
+ to: string,
463
+ text: string,
464
+ type: "c2c" | "group" = "c2c"
465
+ ): Promise<ProactiveSendResult> {
466
+ if (!account.appId || !account.clientSecret) {
467
+ return {
468
+ success: false,
469
+ error: "QQBot not configured (missing appId or clientSecret)",
470
+ };
471
+ }
472
+
473
+ try {
474
+ const bot = getBotForAccount(account.accountId);
475
+ const target = { scope: type, targetId: to };
476
+ const result = await bot.sendText(target, text);
477
+
478
+ return {
479
+ success: true,
480
+ messageId: result.id,
481
+ timestamp: result.timestamp,
482
+ };
483
+ } catch (err) {
484
+ return {
485
+ success: false,
486
+ error: err instanceof Error ? err.message : String(err),
487
+ };
488
+ }
489
+ }
490
+
491
+ /**
492
+ * 获取已知用户统计
493
+ */
494
+ export function getKnownUsersStats(accountId?: string): {
495
+ total: number;
496
+ c2c: number;
497
+ group: number;
498
+ channel: number;
499
+ } {
500
+ const users = listKnownUsers({ accountId });
501
+
502
+ return {
503
+ total: users.length,
504
+ c2c: users.filter(u => u.type === "c2c").length,
505
+ group: users.filter(u => u.type === "group").length,
506
+ channel: users.filter(u => u.type === "channel").length,
507
+ };
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
+ }