@sliverp/qqbot 1.3.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.
- package/README.md +231 -0
- package/clawdbot.plugin.json +16 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +22 -0
- package/dist/src/api.d.ts +194 -0
- package/dist/src/api.js +555 -0
- package/dist/src/channel.d.ts +3 -0
- package/dist/src/channel.js +146 -0
- package/dist/src/config.d.ts +25 -0
- package/dist/src/config.js +148 -0
- package/dist/src/gateway.d.ts +17 -0
- package/dist/src/gateway.js +722 -0
- package/dist/src/image-server.d.ts +62 -0
- package/dist/src/image-server.js +401 -0
- package/dist/src/known-users.d.ts +100 -0
- package/dist/src/known-users.js +264 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +190 -0
- package/dist/src/outbound.d.ts +149 -0
- package/dist/src/outbound.js +476 -0
- package/dist/src/proactive.d.ts +170 -0
- package/dist/src/proactive.js +398 -0
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/session-store.d.ts +49 -0
- package/dist/src/session-store.js +242 -0
- package/dist/src/types.d.ts +116 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils/image-size.d.ts +51 -0
- package/dist/src/utils/image-size.js +234 -0
- package/dist/src/utils/payload.d.ts +112 -0
- package/dist/src/utils/payload.js +186 -0
- package/index.ts +27 -0
- package/moltbot.plugin.json +16 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +13 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/node_modules/ws/lib/receiver.js +706 -0
- package/node_modules/ws/lib/sender.js +602 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +554 -0
- package/node_modules/ws/lib/websocket.js +1393 -0
- package/node_modules/ws/package.json +69 -0
- package/node_modules/ws/wrapper.mjs +8 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +38 -0
- package/qqbot-1.3.0.tgz +0 -0
- package/scripts/proactive-api-server.ts +346 -0
- package/scripts/send-proactive.ts +273 -0
- package/scripts/upgrade.sh +106 -0
- package/skills/qqbot-cron/SKILL.md +490 -0
- package/skills/qqbot-media/SKILL.md +138 -0
- package/src/api.ts +752 -0
- package/src/channel.ts +303 -0
- package/src/config.ts +172 -0
- package/src/gateway.ts +1588 -0
- package/src/image-server.ts +474 -0
- package/src/known-users.ts +358 -0
- package/src/onboarding.ts +254 -0
- package/src/openclaw-plugin-sdk.d.ts +483 -0
- package/src/outbound.ts +571 -0
- package/src/proactive.ts +528 -0
- package/src/runtime.ts +14 -0
- package/src/session-store.ts +292 -0
- package/src/types.ts +123 -0
- package/src/utils/image-size.ts +266 -0
- package/src/utils/payload.ts +265 -0
- package/tsconfig.json +16 -0
- package/upgrade-and-run.sh +89 -0
package/src/proactive.ts
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Bot 主动发送消息模块
|
|
3
|
+
*
|
|
4
|
+
* 该模块提供以下能力:
|
|
5
|
+
* 1. 记录已知用户(曾与机器人交互过的用户)
|
|
6
|
+
* 2. 主动发送消息给用户或群组
|
|
7
|
+
* 3. 查询已知用户列表
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import type { ResolvedQQBotAccount } from "./types.js";
|
|
13
|
+
|
|
14
|
+
// ============ 类型定义(本地) ============
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 已知用户信息
|
|
18
|
+
*/
|
|
19
|
+
export interface KnownUser {
|
|
20
|
+
type: "c2c" | "group" | "channel";
|
|
21
|
+
openid: string;
|
|
22
|
+
accountId: string;
|
|
23
|
+
nickname?: string;
|
|
24
|
+
firstInteractionAt: number;
|
|
25
|
+
lastInteractionAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 主动发送消息选项
|
|
30
|
+
*/
|
|
31
|
+
export interface ProactiveSendOptions {
|
|
32
|
+
to: string;
|
|
33
|
+
text: string;
|
|
34
|
+
type?: "c2c" | "group" | "channel";
|
|
35
|
+
imageUrl?: string;
|
|
36
|
+
accountId?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 主动发送消息结果
|
|
41
|
+
*/
|
|
42
|
+
export interface ProactiveSendResult {
|
|
43
|
+
success: boolean;
|
|
44
|
+
messageId?: string;
|
|
45
|
+
timestamp?: number | string;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 列出已知用户选项
|
|
51
|
+
*/
|
|
52
|
+
export interface ListKnownUsersOptions {
|
|
53
|
+
type?: "c2c" | "group" | "channel";
|
|
54
|
+
accountId?: string;
|
|
55
|
+
sortByLastInteraction?: boolean;
|
|
56
|
+
limit?: number;
|
|
57
|
+
}
|
|
58
|
+
import {
|
|
59
|
+
getAccessToken,
|
|
60
|
+
sendProactiveC2CMessage,
|
|
61
|
+
sendProactiveGroupMessage,
|
|
62
|
+
sendChannelMessage,
|
|
63
|
+
sendC2CImageMessage,
|
|
64
|
+
sendGroupImageMessage,
|
|
65
|
+
} from "./api.js";
|
|
66
|
+
import { resolveQQBotAccount } from "./config.js";
|
|
67
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
68
|
+
|
|
69
|
+
// ============ 用户存储管理 ============
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 已知用户存储
|
|
73
|
+
* 使用简单的 JSON 文件存储,保存在 clawd 目录下
|
|
74
|
+
*/
|
|
75
|
+
const STORAGE_DIR = path.join(process.env.HOME || "/home/ubuntu", "clawd", "qqbot-data");
|
|
76
|
+
const KNOWN_USERS_FILE = path.join(STORAGE_DIR, "known-users.json");
|
|
77
|
+
|
|
78
|
+
// 内存缓存
|
|
79
|
+
let knownUsersCache: Map<string, KnownUser> | null = null;
|
|
80
|
+
let cacheLastModified = 0;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 确保存储目录存在
|
|
84
|
+
*/
|
|
85
|
+
function ensureStorageDir(): void {
|
|
86
|
+
if (!fs.existsSync(STORAGE_DIR)) {
|
|
87
|
+
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 生成用户唯一键
|
|
93
|
+
*/
|
|
94
|
+
function getUserKey(type: string, openid: string, accountId: string): string {
|
|
95
|
+
return `${accountId}:${type}:${openid}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 从文件加载已知用户
|
|
100
|
+
*/
|
|
101
|
+
function loadKnownUsers(): Map<string, KnownUser> {
|
|
102
|
+
if (knownUsersCache !== null) {
|
|
103
|
+
// 检查文件是否被修改
|
|
104
|
+
try {
|
|
105
|
+
const stat = fs.statSync(KNOWN_USERS_FILE);
|
|
106
|
+
if (stat.mtimeMs <= cacheLastModified) {
|
|
107
|
+
return knownUsersCache;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// 文件不存在,使用缓存
|
|
111
|
+
return knownUsersCache;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const users = new Map<string, KnownUser>();
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (fs.existsSync(KNOWN_USERS_FILE)) {
|
|
119
|
+
const data = fs.readFileSync(KNOWN_USERS_FILE, "utf-8");
|
|
120
|
+
const parsed = JSON.parse(data) as KnownUser[];
|
|
121
|
+
for (const user of parsed) {
|
|
122
|
+
const key = getUserKey(user.type, user.openid, user.accountId);
|
|
123
|
+
users.set(key, user);
|
|
124
|
+
}
|
|
125
|
+
cacheLastModified = fs.statSync(KNOWN_USERS_FILE).mtimeMs;
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error(`[qqbot:proactive] Failed to load known users: ${err}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
knownUsersCache = users;
|
|
132
|
+
return users;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 保存已知用户到文件
|
|
137
|
+
*/
|
|
138
|
+
function saveKnownUsers(users: Map<string, KnownUser>): void {
|
|
139
|
+
try {
|
|
140
|
+
ensureStorageDir();
|
|
141
|
+
const data = Array.from(users.values());
|
|
142
|
+
fs.writeFileSync(KNOWN_USERS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
143
|
+
cacheLastModified = Date.now();
|
|
144
|
+
knownUsersCache = users;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(`[qqbot:proactive] Failed to save known users: ${err}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 记录一个已知用户(当收到用户消息时调用)
|
|
152
|
+
*
|
|
153
|
+
* @param user - 用户信息
|
|
154
|
+
*/
|
|
155
|
+
export function recordKnownUser(user: Omit<KnownUser, "firstInteractionAt">): void {
|
|
156
|
+
const users = loadKnownUsers();
|
|
157
|
+
const key = getUserKey(user.type, user.openid, user.accountId);
|
|
158
|
+
|
|
159
|
+
const existing = users.get(key);
|
|
160
|
+
const now = user.lastInteractionAt || Date.now();
|
|
161
|
+
|
|
162
|
+
users.set(key, {
|
|
163
|
+
...user,
|
|
164
|
+
lastInteractionAt: now,
|
|
165
|
+
firstInteractionAt: existing?.firstInteractionAt ?? now,
|
|
166
|
+
// 更新昵称(如果有新的)
|
|
167
|
+
nickname: user.nickname || existing?.nickname,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
saveKnownUsers(users);
|
|
171
|
+
console.log(`[qqbot:proactive] Recorded user: ${key}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 获取一个已知用户
|
|
176
|
+
*
|
|
177
|
+
* @param type - 用户类型
|
|
178
|
+
* @param openid - 用户 openid
|
|
179
|
+
* @param accountId - 账户 ID
|
|
180
|
+
*/
|
|
181
|
+
export function getKnownUser(type: string, openid: string, accountId: string): KnownUser | undefined {
|
|
182
|
+
const users = loadKnownUsers();
|
|
183
|
+
const key = getUserKey(type, openid, accountId);
|
|
184
|
+
return users.get(key);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 列出已知用户
|
|
189
|
+
*
|
|
190
|
+
* @param options - 过滤选项
|
|
191
|
+
*/
|
|
192
|
+
export function listKnownUsers(options?: ListKnownUsersOptions): KnownUser[] {
|
|
193
|
+
const users = loadKnownUsers();
|
|
194
|
+
let result = Array.from(users.values());
|
|
195
|
+
|
|
196
|
+
// 过滤类型
|
|
197
|
+
if (options?.type) {
|
|
198
|
+
result = result.filter(u => u.type === options.type);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 过滤账户
|
|
202
|
+
if (options?.accountId) {
|
|
203
|
+
result = result.filter(u => u.accountId === options.accountId);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 排序
|
|
207
|
+
if (options?.sortByLastInteraction !== false) {
|
|
208
|
+
result.sort((a, b) => b.lastInteractionAt - a.lastInteractionAt);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 限制数量
|
|
212
|
+
if (options?.limit && options.limit > 0) {
|
|
213
|
+
result = result.slice(0, options.limit);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 删除一个已知用户
|
|
221
|
+
*
|
|
222
|
+
* @param type - 用户类型
|
|
223
|
+
* @param openid - 用户 openid
|
|
224
|
+
* @param accountId - 账户 ID
|
|
225
|
+
*/
|
|
226
|
+
export function removeKnownUser(type: string, openid: string, accountId: string): boolean {
|
|
227
|
+
const users = loadKnownUsers();
|
|
228
|
+
const key = getUserKey(type, openid, accountId);
|
|
229
|
+
const deleted = users.delete(key);
|
|
230
|
+
if (deleted) {
|
|
231
|
+
saveKnownUsers(users);
|
|
232
|
+
}
|
|
233
|
+
return deleted;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 清除所有已知用户
|
|
238
|
+
*
|
|
239
|
+
* @param accountId - 可选,只清除指定账户的用户
|
|
240
|
+
*/
|
|
241
|
+
export function clearKnownUsers(accountId?: string): number {
|
|
242
|
+
const users = loadKnownUsers();
|
|
243
|
+
let count = 0;
|
|
244
|
+
|
|
245
|
+
if (accountId) {
|
|
246
|
+
for (const [key, user] of users) {
|
|
247
|
+
if (user.accountId === accountId) {
|
|
248
|
+
users.delete(key);
|
|
249
|
+
count++;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
count = users.size;
|
|
254
|
+
users.clear();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (count > 0) {
|
|
258
|
+
saveKnownUsers(users);
|
|
259
|
+
}
|
|
260
|
+
return count;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============ 主动发送消息 ============
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 主动发送消息(带配置解析)
|
|
267
|
+
* 注意:与 outbound.ts 中的 sendProactiveMessage 不同,这个函数接受 OpenClawConfig 并自动解析账户
|
|
268
|
+
*
|
|
269
|
+
* @param options - 发送选项
|
|
270
|
+
* @param cfg - OpenClaw 配置
|
|
271
|
+
* @returns 发送结果
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* // 发送私聊消息
|
|
276
|
+
* const result = await sendProactive({
|
|
277
|
+
* to: "E7A8F3B2C1D4E5F6A7B8C9D0E1F2A3B4", // 用户 openid
|
|
278
|
+
* text: "你好!这是一条主动消息",
|
|
279
|
+
* type: "c2c",
|
|
280
|
+
* }, cfg);
|
|
281
|
+
*
|
|
282
|
+
* // 发送群聊消息
|
|
283
|
+
* const result = await sendProactive({
|
|
284
|
+
* to: "A1B2C3D4E5F6A7B8", // 群组 openid
|
|
285
|
+
* text: "群公告:今天有活动",
|
|
286
|
+
* type: "group",
|
|
287
|
+
* }, cfg);
|
|
288
|
+
*
|
|
289
|
+
* // 发送带图片的消息
|
|
290
|
+
* const result = await sendProactive({
|
|
291
|
+
* to: "E7A8F3B2C1D4E5F6A7B8C9D0E1F2A3B4",
|
|
292
|
+
* text: "看看这张图片",
|
|
293
|
+
* imageUrl: "https://example.com/image.png",
|
|
294
|
+
* type: "c2c",
|
|
295
|
+
* }, cfg);
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export async function sendProactive(
|
|
299
|
+
options: ProactiveSendOptions,
|
|
300
|
+
cfg: OpenClawConfig
|
|
301
|
+
): Promise<ProactiveSendResult> {
|
|
302
|
+
const { to, text, type = "c2c", imageUrl, accountId = "default" } = options;
|
|
303
|
+
|
|
304
|
+
// 解析账户配置
|
|
305
|
+
const account = resolveQQBotAccount(cfg, accountId);
|
|
306
|
+
|
|
307
|
+
if (!account.appId || !account.clientSecret) {
|
|
308
|
+
return {
|
|
309
|
+
success: false,
|
|
310
|
+
error: "QQBot not configured (missing appId or clientSecret)",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const accessToken = await getAccessToken(account.appId, account.clientSecret);
|
|
316
|
+
|
|
317
|
+
// 如果有图片,先发送图片
|
|
318
|
+
if (imageUrl) {
|
|
319
|
+
try {
|
|
320
|
+
if (type === "c2c") {
|
|
321
|
+
await sendC2CImageMessage(accessToken, to, imageUrl, undefined, undefined);
|
|
322
|
+
} else if (type === "group") {
|
|
323
|
+
await sendGroupImageMessage(accessToken, to, imageUrl, undefined, undefined);
|
|
324
|
+
}
|
|
325
|
+
console.log(`[qqbot:proactive] Sent image to ${type}:${to}`);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
console.error(`[qqbot:proactive] Failed to send image: ${err}`);
|
|
328
|
+
// 图片发送失败不影响文本发送
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 发送文本消息
|
|
333
|
+
let result: { id: string; timestamp: number | string };
|
|
334
|
+
|
|
335
|
+
if (type === "c2c") {
|
|
336
|
+
result = await sendProactiveC2CMessage(accessToken, to, text);
|
|
337
|
+
} else if (type === "group") {
|
|
338
|
+
result = await sendProactiveGroupMessage(accessToken, to, text);
|
|
339
|
+
} else if (type === "channel") {
|
|
340
|
+
// 频道消息需要 channel_id,这里暂时不支持主动发送
|
|
341
|
+
return {
|
|
342
|
+
success: false,
|
|
343
|
+
error: "Channel proactive messages are not supported. Please use group or c2c.",
|
|
344
|
+
};
|
|
345
|
+
} else {
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
error: `Unknown message type: ${type}`,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log(`[qqbot:proactive] Sent message to ${type}:${to}, id: ${result.id}`);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
success: true,
|
|
356
|
+
messageId: result.id,
|
|
357
|
+
timestamp: result.timestamp,
|
|
358
|
+
};
|
|
359
|
+
} catch (err) {
|
|
360
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
361
|
+
console.error(`[qqbot:proactive] Failed to send message: ${message}`);
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
error: message,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 批量发送主动消息
|
|
372
|
+
*
|
|
373
|
+
* @param recipients - 接收者列表(openid 数组)
|
|
374
|
+
* @param text - 消息内容
|
|
375
|
+
* @param type - 消息类型
|
|
376
|
+
* @param cfg - OpenClaw 配置
|
|
377
|
+
* @param accountId - 账户 ID
|
|
378
|
+
* @returns 发送结果列表
|
|
379
|
+
*/
|
|
380
|
+
export async function sendBulkProactiveMessage(
|
|
381
|
+
recipients: string[],
|
|
382
|
+
text: string,
|
|
383
|
+
type: "c2c" | "group",
|
|
384
|
+
cfg: OpenClawConfig,
|
|
385
|
+
accountId = "default"
|
|
386
|
+
): Promise<Array<{ to: string; result: ProactiveSendResult }>> {
|
|
387
|
+
const results: Array<{ to: string; result: ProactiveSendResult }> = [];
|
|
388
|
+
|
|
389
|
+
for (const to of recipients) {
|
|
390
|
+
const result = await sendProactive({ to, text, type, accountId }, cfg);
|
|
391
|
+
results.push({ to, result });
|
|
392
|
+
|
|
393
|
+
// 添加延迟,避免频率限制
|
|
394
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return results;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 发送消息给所有已知用户
|
|
402
|
+
*
|
|
403
|
+
* @param text - 消息内容
|
|
404
|
+
* @param cfg - OpenClaw 配置
|
|
405
|
+
* @param options - 过滤选项
|
|
406
|
+
* @returns 发送结果统计
|
|
407
|
+
*/
|
|
408
|
+
export async function broadcastMessage(
|
|
409
|
+
text: string,
|
|
410
|
+
cfg: OpenClawConfig,
|
|
411
|
+
options?: {
|
|
412
|
+
type?: "c2c" | "group";
|
|
413
|
+
accountId?: string;
|
|
414
|
+
limit?: number;
|
|
415
|
+
}
|
|
416
|
+
): Promise<{
|
|
417
|
+
total: number;
|
|
418
|
+
success: number;
|
|
419
|
+
failed: number;
|
|
420
|
+
results: Array<{ to: string; result: ProactiveSendResult }>;
|
|
421
|
+
}> {
|
|
422
|
+
const users = listKnownUsers({
|
|
423
|
+
type: options?.type,
|
|
424
|
+
accountId: options?.accountId,
|
|
425
|
+
limit: options?.limit,
|
|
426
|
+
sortByLastInteraction: true,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// 过滤掉频道用户(不支持主动发送)
|
|
430
|
+
const validUsers = users.filter(u => u.type === "c2c" || u.type === "group");
|
|
431
|
+
|
|
432
|
+
const results: Array<{ to: string; result: ProactiveSendResult }> = [];
|
|
433
|
+
let success = 0;
|
|
434
|
+
let failed = 0;
|
|
435
|
+
|
|
436
|
+
for (const user of validUsers) {
|
|
437
|
+
const result = await sendProactive({
|
|
438
|
+
to: user.openid,
|
|
439
|
+
text,
|
|
440
|
+
type: user.type as "c2c" | "group",
|
|
441
|
+
accountId: user.accountId,
|
|
442
|
+
}, cfg);
|
|
443
|
+
|
|
444
|
+
results.push({ to: user.openid, result });
|
|
445
|
+
|
|
446
|
+
if (result.success) {
|
|
447
|
+
success++;
|
|
448
|
+
} else {
|
|
449
|
+
failed++;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 添加延迟,避免频率限制
|
|
453
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
total: validUsers.length,
|
|
458
|
+
success,
|
|
459
|
+
failed,
|
|
460
|
+
results,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ============ 辅助函数 ============
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* 根据账户配置直接发送主动消息(不需要 cfg)
|
|
468
|
+
*
|
|
469
|
+
* @param account - 已解析的账户配置
|
|
470
|
+
* @param to - 目标 openid
|
|
471
|
+
* @param text - 消息内容
|
|
472
|
+
* @param type - 消息类型
|
|
473
|
+
*/
|
|
474
|
+
export async function sendProactiveMessageDirect(
|
|
475
|
+
account: ResolvedQQBotAccount,
|
|
476
|
+
to: string,
|
|
477
|
+
text: string,
|
|
478
|
+
type: "c2c" | "group" = "c2c"
|
|
479
|
+
): Promise<ProactiveSendResult> {
|
|
480
|
+
if (!account.appId || !account.clientSecret) {
|
|
481
|
+
return {
|
|
482
|
+
success: false,
|
|
483
|
+
error: "QQBot not configured (missing appId or clientSecret)",
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
const accessToken = await getAccessToken(account.appId, account.clientSecret);
|
|
489
|
+
|
|
490
|
+
let result: { id: string; timestamp: number | string };
|
|
491
|
+
|
|
492
|
+
if (type === "c2c") {
|
|
493
|
+
result = await sendProactiveC2CMessage(accessToken, to, text);
|
|
494
|
+
} else {
|
|
495
|
+
result = await sendProactiveGroupMessage(accessToken, to, text);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
success: true,
|
|
500
|
+
messageId: result.id,
|
|
501
|
+
timestamp: result.timestamp,
|
|
502
|
+
};
|
|
503
|
+
} catch (err) {
|
|
504
|
+
return {
|
|
505
|
+
success: false,
|
|
506
|
+
error: err instanceof Error ? err.message : String(err),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 获取已知用户统计
|
|
513
|
+
*/
|
|
514
|
+
export function getKnownUsersStats(accountId?: string): {
|
|
515
|
+
total: number;
|
|
516
|
+
c2c: number;
|
|
517
|
+
group: number;
|
|
518
|
+
channel: number;
|
|
519
|
+
} {
|
|
520
|
+
const users = listKnownUsers({ accountId });
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
total: users.length,
|
|
524
|
+
c2c: users.filter(u => u.type === "c2c").length,
|
|
525
|
+
group: users.filter(u => u.type === "group").length,
|
|
526
|
+
channel: users.filter(u => u.type === "channel").length,
|
|
527
|
+
};
|
|
528
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
let runtime: PluginRuntime | null = null;
|
|
4
|
+
|
|
5
|
+
export function setQQBotRuntime(next: PluginRuntime) {
|
|
6
|
+
runtime = next;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getQQBotRuntime(): PluginRuntime {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
throw new Error("QQBot runtime not initialized");
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|