@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
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 已知用户存储
|
|
3
|
+
* 记录与机器人交互过的所有用户
|
|
4
|
+
* 支持主动消息和批量通知功能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
// 已知用户信息接口
|
|
11
|
+
export interface KnownUser {
|
|
12
|
+
/** 用户 openid(唯一标识) */
|
|
13
|
+
openid: string;
|
|
14
|
+
/** 消息类型:私聊用户 / 群组 */
|
|
15
|
+
type: "c2c" | "group";
|
|
16
|
+
/** 用户昵称(如有) */
|
|
17
|
+
nickname?: string;
|
|
18
|
+
/** 群组 openid(如果是群消息) */
|
|
19
|
+
groupOpenid?: string;
|
|
20
|
+
/** 关联的机器人账户 ID */
|
|
21
|
+
accountId: string;
|
|
22
|
+
/** 首次交互时间戳 */
|
|
23
|
+
firstSeenAt: number;
|
|
24
|
+
/** 最后交互时间戳 */
|
|
25
|
+
lastSeenAt: number;
|
|
26
|
+
/** 交互次数 */
|
|
27
|
+
interactionCount: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 存储文件路径
|
|
31
|
+
const KNOWN_USERS_DIR = path.join(
|
|
32
|
+
process.env.HOME || "/tmp",
|
|
33
|
+
"clawd",
|
|
34
|
+
"qqbot-data"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const KNOWN_USERS_FILE = path.join(KNOWN_USERS_DIR, "known-users.json");
|
|
38
|
+
|
|
39
|
+
// 内存缓存
|
|
40
|
+
let usersCache: Map<string, KnownUser> | null = null;
|
|
41
|
+
|
|
42
|
+
// 写入节流配置
|
|
43
|
+
const SAVE_THROTTLE_MS = 5000; // 5秒写入一次
|
|
44
|
+
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
45
|
+
let isDirty = false;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 确保目录存在
|
|
49
|
+
*/
|
|
50
|
+
function ensureDir(): void {
|
|
51
|
+
if (!fs.existsSync(KNOWN_USERS_DIR)) {
|
|
52
|
+
fs.mkdirSync(KNOWN_USERS_DIR, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 从文件加载用户数据到缓存
|
|
58
|
+
*/
|
|
59
|
+
function loadUsersFromFile(): Map<string, KnownUser> {
|
|
60
|
+
if (usersCache !== null) {
|
|
61
|
+
return usersCache;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
usersCache = new Map();
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
if (fs.existsSync(KNOWN_USERS_FILE)) {
|
|
68
|
+
const data = fs.readFileSync(KNOWN_USERS_FILE, "utf-8");
|
|
69
|
+
const users = JSON.parse(data) as KnownUser[];
|
|
70
|
+
|
|
71
|
+
for (const user of users) {
|
|
72
|
+
// 使用复合键:accountId + type + openid(群组还要加 groupOpenid)
|
|
73
|
+
const key = makeUserKey(user);
|
|
74
|
+
usersCache.set(key, user);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`[known-users] Loaded ${usersCache.size} users from file`);
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error(`[known-users] Failed to load users: ${err}`);
|
|
81
|
+
usersCache = new Map();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return usersCache;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 保存用户数据到文件(节流版本)
|
|
89
|
+
*/
|
|
90
|
+
function saveUsersToFile(): void {
|
|
91
|
+
if (!isDirty) return;
|
|
92
|
+
|
|
93
|
+
if (saveTimer) {
|
|
94
|
+
return; // 已有定时器在等待
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
saveTimer = setTimeout(() => {
|
|
98
|
+
saveTimer = null;
|
|
99
|
+
doSaveUsersToFile();
|
|
100
|
+
}, SAVE_THROTTLE_MS);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 实际执行保存
|
|
105
|
+
*/
|
|
106
|
+
function doSaveUsersToFile(): void {
|
|
107
|
+
if (!usersCache || !isDirty) return;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
ensureDir();
|
|
111
|
+
const users = Array.from(usersCache.values());
|
|
112
|
+
fs.writeFileSync(KNOWN_USERS_FILE, JSON.stringify(users, null, 2), "utf-8");
|
|
113
|
+
isDirty = false;
|
|
114
|
+
console.log(`[known-users] Saved ${users.length} users to file`);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(`[known-users] Failed to save users: ${err}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 强制立即保存(用于进程退出前)
|
|
122
|
+
*/
|
|
123
|
+
export function flushKnownUsers(): void {
|
|
124
|
+
if (saveTimer) {
|
|
125
|
+
clearTimeout(saveTimer);
|
|
126
|
+
saveTimer = null;
|
|
127
|
+
}
|
|
128
|
+
doSaveUsersToFile();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 生成用户唯一键
|
|
133
|
+
*/
|
|
134
|
+
function makeUserKey(user: Partial<KnownUser>): string {
|
|
135
|
+
const base = `${user.accountId}:${user.type}:${user.openid}`;
|
|
136
|
+
if (user.type === "group" && user.groupOpenid) {
|
|
137
|
+
return `${base}:${user.groupOpenid}`;
|
|
138
|
+
}
|
|
139
|
+
return base;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 记录已知用户(收到消息时调用)
|
|
144
|
+
* @param user 用户信息(部分字段)
|
|
145
|
+
*/
|
|
146
|
+
export function recordKnownUser(user: {
|
|
147
|
+
openid: string;
|
|
148
|
+
type: "c2c" | "group";
|
|
149
|
+
nickname?: string;
|
|
150
|
+
groupOpenid?: string;
|
|
151
|
+
accountId: string;
|
|
152
|
+
}): void {
|
|
153
|
+
const cache = loadUsersFromFile();
|
|
154
|
+
const key = makeUserKey(user);
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
|
|
157
|
+
const existing = cache.get(key);
|
|
158
|
+
|
|
159
|
+
if (existing) {
|
|
160
|
+
// 更新已存在的用户
|
|
161
|
+
existing.lastSeenAt = now;
|
|
162
|
+
existing.interactionCount++;
|
|
163
|
+
if (user.nickname && user.nickname !== existing.nickname) {
|
|
164
|
+
existing.nickname = user.nickname;
|
|
165
|
+
}
|
|
166
|
+
console.log(`[known-users] Updated user ${user.openid}, interactions: ${existing.interactionCount}`);
|
|
167
|
+
} else {
|
|
168
|
+
// 新用户
|
|
169
|
+
const newUser: KnownUser = {
|
|
170
|
+
openid: user.openid,
|
|
171
|
+
type: user.type,
|
|
172
|
+
nickname: user.nickname,
|
|
173
|
+
groupOpenid: user.groupOpenid,
|
|
174
|
+
accountId: user.accountId,
|
|
175
|
+
firstSeenAt: now,
|
|
176
|
+
lastSeenAt: now,
|
|
177
|
+
interactionCount: 1,
|
|
178
|
+
};
|
|
179
|
+
cache.set(key, newUser);
|
|
180
|
+
console.log(`[known-users] New user recorded: ${user.openid} (${user.type})`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
isDirty = true;
|
|
184
|
+
saveUsersToFile();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 获取单个用户信息
|
|
189
|
+
* @param accountId 机器人账户 ID
|
|
190
|
+
* @param openid 用户 openid
|
|
191
|
+
* @param type 消息类型
|
|
192
|
+
* @param groupOpenid 群组 openid(可选)
|
|
193
|
+
*/
|
|
194
|
+
export function getKnownUser(
|
|
195
|
+
accountId: string,
|
|
196
|
+
openid: string,
|
|
197
|
+
type: "c2c" | "group" = "c2c",
|
|
198
|
+
groupOpenid?: string
|
|
199
|
+
): KnownUser | undefined {
|
|
200
|
+
const cache = loadUsersFromFile();
|
|
201
|
+
const key = makeUserKey({ accountId, openid, type, groupOpenid });
|
|
202
|
+
return cache.get(key);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 列出所有已知用户
|
|
207
|
+
* @param options 筛选选项
|
|
208
|
+
*/
|
|
209
|
+
export function listKnownUsers(options?: {
|
|
210
|
+
/** 筛选特定机器人账户的用户 */
|
|
211
|
+
accountId?: string;
|
|
212
|
+
/** 筛选消息类型 */
|
|
213
|
+
type?: "c2c" | "group";
|
|
214
|
+
/** 最近活跃时间(毫秒,如 86400000 表示最近 24 小时) */
|
|
215
|
+
activeWithin?: number;
|
|
216
|
+
/** 返回数量限制 */
|
|
217
|
+
limit?: number;
|
|
218
|
+
/** 排序方式 */
|
|
219
|
+
sortBy?: "lastSeenAt" | "firstSeenAt" | "interactionCount";
|
|
220
|
+
/** 排序方向 */
|
|
221
|
+
sortOrder?: "asc" | "desc";
|
|
222
|
+
}): KnownUser[] {
|
|
223
|
+
const cache = loadUsersFromFile();
|
|
224
|
+
let users = Array.from(cache.values());
|
|
225
|
+
|
|
226
|
+
// 筛选
|
|
227
|
+
if (options?.accountId) {
|
|
228
|
+
users = users.filter(u => u.accountId === options.accountId);
|
|
229
|
+
}
|
|
230
|
+
if (options?.type) {
|
|
231
|
+
users = users.filter(u => u.type === options.type);
|
|
232
|
+
}
|
|
233
|
+
if (options?.activeWithin) {
|
|
234
|
+
const cutoff = Date.now() - options.activeWithin;
|
|
235
|
+
users = users.filter(u => u.lastSeenAt >= cutoff);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 排序
|
|
239
|
+
const sortBy = options?.sortBy ?? "lastSeenAt";
|
|
240
|
+
const sortOrder = options?.sortOrder ?? "desc";
|
|
241
|
+
users.sort((a, b) => {
|
|
242
|
+
const aVal = a[sortBy] ?? 0;
|
|
243
|
+
const bVal = b[sortBy] ?? 0;
|
|
244
|
+
return sortOrder === "asc" ? aVal - bVal : bVal - aVal;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 限制数量
|
|
248
|
+
if (options?.limit && options.limit > 0) {
|
|
249
|
+
users = users.slice(0, options.limit);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return users;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 获取用户统计信息
|
|
257
|
+
* @param accountId 机器人账户 ID(可选,不传则返回所有账户的统计)
|
|
258
|
+
*/
|
|
259
|
+
export function getKnownUsersStats(accountId?: string): {
|
|
260
|
+
totalUsers: number;
|
|
261
|
+
c2cUsers: number;
|
|
262
|
+
groupUsers: number;
|
|
263
|
+
activeIn24h: number;
|
|
264
|
+
activeIn7d: number;
|
|
265
|
+
} {
|
|
266
|
+
let users = listKnownUsers({ accountId });
|
|
267
|
+
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const day = 24 * 60 * 60 * 1000;
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
totalUsers: users.length,
|
|
273
|
+
c2cUsers: users.filter(u => u.type === "c2c").length,
|
|
274
|
+
groupUsers: users.filter(u => u.type === "group").length,
|
|
275
|
+
activeIn24h: users.filter(u => now - u.lastSeenAt < day).length,
|
|
276
|
+
activeIn7d: users.filter(u => now - u.lastSeenAt < 7 * day).length,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 删除用户记录
|
|
282
|
+
* @param accountId 机器人账户 ID
|
|
283
|
+
* @param openid 用户 openid
|
|
284
|
+
* @param type 消息类型
|
|
285
|
+
* @param groupOpenid 群组 openid(可选)
|
|
286
|
+
*/
|
|
287
|
+
export function removeKnownUser(
|
|
288
|
+
accountId: string,
|
|
289
|
+
openid: string,
|
|
290
|
+
type: "c2c" | "group" = "c2c",
|
|
291
|
+
groupOpenid?: string
|
|
292
|
+
): boolean {
|
|
293
|
+
const cache = loadUsersFromFile();
|
|
294
|
+
const key = makeUserKey({ accountId, openid, type, groupOpenid });
|
|
295
|
+
|
|
296
|
+
if (cache.has(key)) {
|
|
297
|
+
cache.delete(key);
|
|
298
|
+
isDirty = true;
|
|
299
|
+
saveUsersToFile();
|
|
300
|
+
console.log(`[known-users] Removed user ${openid}`);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 清除所有用户记录
|
|
309
|
+
* @param accountId 机器人账户 ID(可选,不传则清除所有)
|
|
310
|
+
*/
|
|
311
|
+
export function clearKnownUsers(accountId?: string): number {
|
|
312
|
+
const cache = loadUsersFromFile();
|
|
313
|
+
let count = 0;
|
|
314
|
+
|
|
315
|
+
if (accountId) {
|
|
316
|
+
// 只清除指定账户的用户
|
|
317
|
+
for (const [key, user] of cache.entries()) {
|
|
318
|
+
if (user.accountId === accountId) {
|
|
319
|
+
cache.delete(key);
|
|
320
|
+
count++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
// 清除所有
|
|
325
|
+
count = cache.size;
|
|
326
|
+
cache.clear();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (count > 0) {
|
|
330
|
+
isDirty = true;
|
|
331
|
+
doSaveUsersToFile(); // 立即保存
|
|
332
|
+
console.log(`[known-users] Cleared ${count} users`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return count;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 获取用户的所有群组(某用户在哪些群里交互过)
|
|
340
|
+
* @param accountId 机器人账户 ID
|
|
341
|
+
* @param openid 用户 openid
|
|
342
|
+
*/
|
|
343
|
+
export function getUserGroups(accountId: string, openid: string): string[] {
|
|
344
|
+
const users = listKnownUsers({ accountId, type: "group" });
|
|
345
|
+
return users
|
|
346
|
+
.filter(u => u.openid === openid && u.groupOpenid)
|
|
347
|
+
.map(u => u.groupOpenid!);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 获取群组的所有成员
|
|
352
|
+
* @param accountId 机器人账户 ID
|
|
353
|
+
* @param groupOpenid 群组 openid
|
|
354
|
+
*/
|
|
355
|
+
export function getGroupMembers(accountId: string, groupOpenid: string): KnownUser[] {
|
|
356
|
+
return listKnownUsers({ accountId, type: "group" })
|
|
357
|
+
.filter(u => u.groupOpenid === groupOpenid);
|
|
358
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQBot CLI Onboarding Adapter
|
|
3
|
+
*
|
|
4
|
+
* 提供 openclaw onboard 命令的交互式配置支持
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
ChannelOnboardingAdapter,
|
|
8
|
+
ChannelOnboardingStatus,
|
|
9
|
+
ChannelOnboardingStatusContext,
|
|
10
|
+
ChannelOnboardingConfigureContext,
|
|
11
|
+
ChannelOnboardingResult,
|
|
12
|
+
OpenClawConfig,
|
|
13
|
+
} from "openclaw/plugin-sdk";
|
|
14
|
+
import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount } from "./config.js";
|
|
15
|
+
|
|
16
|
+
// 内部类型(用于类型安全)
|
|
17
|
+
interface QQBotChannelConfig {
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
appId?: string;
|
|
20
|
+
clientSecret?: string;
|
|
21
|
+
clientSecretFile?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
imageServerBaseUrl?: string;
|
|
24
|
+
accounts?: Record<string, {
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
appId?: string;
|
|
27
|
+
clientSecret?: string;
|
|
28
|
+
clientSecretFile?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
imageServerBaseUrl?: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Prompter 类型定义
|
|
35
|
+
interface Prompter {
|
|
36
|
+
note: (message: string, title?: string) => Promise<void>;
|
|
37
|
+
confirm: (opts: { message: string; initialValue?: boolean }) => Promise<boolean>;
|
|
38
|
+
text: (opts: { message: string; placeholder?: string; initialValue?: string; validate?: (value: string) => string | undefined }) => Promise<string>;
|
|
39
|
+
select: <T>(opts: { message: string; options: Array<{ value: T; label: string }>; initialValue?: T }) => Promise<T>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 解析默认账户 ID
|
|
44
|
+
*/
|
|
45
|
+
function resolveDefaultQQBotAccountId(cfg: OpenClawConfig): string {
|
|
46
|
+
const ids = listQQBotAccountIds(cfg);
|
|
47
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* QQBot Onboarding Adapter
|
|
52
|
+
*/
|
|
53
|
+
export const qqbotOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
54
|
+
channel: "qqbot" as any,
|
|
55
|
+
|
|
56
|
+
getStatus: async (ctx: ChannelOnboardingStatusContext): Promise<ChannelOnboardingStatus> => {
|
|
57
|
+
const cfg = ctx.cfg as OpenClawConfig;
|
|
58
|
+
const configured = listQQBotAccountIds(cfg).some((accountId) => {
|
|
59
|
+
const account = resolveQQBotAccount(cfg, accountId);
|
|
60
|
+
return Boolean(account.appId && account.clientSecret);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
channel: "qqbot" as any,
|
|
65
|
+
configured,
|
|
66
|
+
statusLines: [`QQ Bot: ${configured ? "已配置" : "需要 AppID 和 ClientSecret"}`],
|
|
67
|
+
selectionHint: configured ? "已配置" : "支持 QQ 群聊和私聊(流式消息)",
|
|
68
|
+
quickstartScore: configured ? 1 : 20,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
configure: async (ctx: ChannelOnboardingConfigureContext): Promise<ChannelOnboardingResult> => {
|
|
73
|
+
const cfg = ctx.cfg as OpenClawConfig;
|
|
74
|
+
const prompter = ctx.prompter as Prompter;
|
|
75
|
+
const accountOverrides = ctx.accountOverrides as Record<string, string> | undefined;
|
|
76
|
+
const shouldPromptAccountIds = ctx.shouldPromptAccountIds;
|
|
77
|
+
|
|
78
|
+
const qqbotOverride = accountOverrides?.qqbot?.trim();
|
|
79
|
+
const defaultAccountId = resolveDefaultQQBotAccountId(cfg);
|
|
80
|
+
let accountId = qqbotOverride ?? defaultAccountId;
|
|
81
|
+
|
|
82
|
+
// 是否需要提示选择账户
|
|
83
|
+
if (shouldPromptAccountIds && !qqbotOverride) {
|
|
84
|
+
const existingIds = listQQBotAccountIds(cfg);
|
|
85
|
+
if (existingIds.length > 1) {
|
|
86
|
+
accountId = await prompter.select({
|
|
87
|
+
message: "选择 QQBot 账户",
|
|
88
|
+
options: existingIds.map((id) => ({
|
|
89
|
+
value: id,
|
|
90
|
+
label: id === DEFAULT_ACCOUNT_ID ? "默认账户" : id,
|
|
91
|
+
})),
|
|
92
|
+
initialValue: accountId,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let next: OpenClawConfig = cfg;
|
|
98
|
+
const resolvedAccount = resolveQQBotAccount(next, accountId);
|
|
99
|
+
const accountConfigured = Boolean(resolvedAccount.appId && resolvedAccount.clientSecret);
|
|
100
|
+
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
|
101
|
+
const envAppId = typeof process !== "undefined" ? process.env?.QQBOT_APP_ID?.trim() : undefined;
|
|
102
|
+
const envSecret = typeof process !== "undefined" ? process.env?.QQBOT_CLIENT_SECRET?.trim() : undefined;
|
|
103
|
+
const canUseEnv = allowEnv && Boolean(envAppId && envSecret);
|
|
104
|
+
const hasConfigCredentials = Boolean(resolvedAccount.config.appId && resolvedAccount.config.clientSecret);
|
|
105
|
+
|
|
106
|
+
let appId: string | null = null;
|
|
107
|
+
let clientSecret: string | null = null;
|
|
108
|
+
|
|
109
|
+
// 显示帮助
|
|
110
|
+
if (!accountConfigured) {
|
|
111
|
+
await prompter.note(
|
|
112
|
+
[
|
|
113
|
+
"1) 打开 QQ 开放平台: https://q.qq.com/",
|
|
114
|
+
"2) 创建机器人应用,获取 AppID 和 ClientSecret",
|
|
115
|
+
"3) 在「开发设置」中添加沙箱成员(测试阶段)",
|
|
116
|
+
"4) 你也可以设置环境变量 QQBOT_APP_ID 和 QQBOT_CLIENT_SECRET",
|
|
117
|
+
"",
|
|
118
|
+
"文档: https://bot.q.qq.com/wiki/",
|
|
119
|
+
"",
|
|
120
|
+
"此版本支持流式消息发送!",
|
|
121
|
+
].join("\n"),
|
|
122
|
+
"QQ Bot 配置",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 检测环境变量
|
|
127
|
+
if (canUseEnv && !hasConfigCredentials) {
|
|
128
|
+
const keepEnv = await prompter.confirm({
|
|
129
|
+
message: "检测到环境变量 QQBOT_APP_ID 和 QQBOT_CLIENT_SECRET,是否使用?",
|
|
130
|
+
initialValue: true,
|
|
131
|
+
});
|
|
132
|
+
if (keepEnv) {
|
|
133
|
+
next = {
|
|
134
|
+
...next,
|
|
135
|
+
channels: {
|
|
136
|
+
...next.channels,
|
|
137
|
+
qqbot: {
|
|
138
|
+
...(next.channels?.qqbot as Record<string, unknown> || {}),
|
|
139
|
+
enabled: true,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
} else {
|
|
144
|
+
// 手动输入
|
|
145
|
+
appId = String(
|
|
146
|
+
await prompter.text({
|
|
147
|
+
message: "请输入 QQ Bot AppID",
|
|
148
|
+
placeholder: "例如: 102146862",
|
|
149
|
+
initialValue: resolvedAccount.appId || undefined,
|
|
150
|
+
validate: (value: string) => (value?.trim() ? undefined : "AppID 不能为空"),
|
|
151
|
+
}),
|
|
152
|
+
).trim();
|
|
153
|
+
clientSecret = String(
|
|
154
|
+
await prompter.text({
|
|
155
|
+
message: "请输入 QQ Bot ClientSecret",
|
|
156
|
+
placeholder: "你的 ClientSecret",
|
|
157
|
+
validate: (value: string) => (value?.trim() ? undefined : "ClientSecret 不能为空"),
|
|
158
|
+
}),
|
|
159
|
+
).trim();
|
|
160
|
+
}
|
|
161
|
+
} else if (hasConfigCredentials) {
|
|
162
|
+
// 已有配置
|
|
163
|
+
const keep = await prompter.confirm({
|
|
164
|
+
message: "QQ Bot 已配置,是否保留当前配置?",
|
|
165
|
+
initialValue: true,
|
|
166
|
+
});
|
|
167
|
+
if (!keep) {
|
|
168
|
+
appId = String(
|
|
169
|
+
await prompter.text({
|
|
170
|
+
message: "请输入 QQ Bot AppID",
|
|
171
|
+
placeholder: "例如: 102146862",
|
|
172
|
+
initialValue: resolvedAccount.appId || undefined,
|
|
173
|
+
validate: (value: string) => (value?.trim() ? undefined : "AppID 不能为空"),
|
|
174
|
+
}),
|
|
175
|
+
).trim();
|
|
176
|
+
clientSecret = String(
|
|
177
|
+
await prompter.text({
|
|
178
|
+
message: "请输入 QQ Bot ClientSecret",
|
|
179
|
+
placeholder: "你的 ClientSecret",
|
|
180
|
+
validate: (value: string) => (value?.trim() ? undefined : "ClientSecret 不能为空"),
|
|
181
|
+
}),
|
|
182
|
+
).trim();
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
// 没有配置,需要输入
|
|
186
|
+
appId = String(
|
|
187
|
+
await prompter.text({
|
|
188
|
+
message: "请输入 QQ Bot AppID",
|
|
189
|
+
placeholder: "例如: 102146862",
|
|
190
|
+
initialValue: resolvedAccount.appId || undefined,
|
|
191
|
+
validate: (value: string) => (value?.trim() ? undefined : "AppID 不能为空"),
|
|
192
|
+
}),
|
|
193
|
+
).trim();
|
|
194
|
+
clientSecret = String(
|
|
195
|
+
await prompter.text({
|
|
196
|
+
message: "请输入 QQ Bot ClientSecret",
|
|
197
|
+
placeholder: "你的 ClientSecret",
|
|
198
|
+
validate: (value: string) => (value?.trim() ? undefined : "ClientSecret 不能为空"),
|
|
199
|
+
}),
|
|
200
|
+
).trim();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 应用配置
|
|
204
|
+
if (appId && clientSecret) {
|
|
205
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
206
|
+
next = {
|
|
207
|
+
...next,
|
|
208
|
+
channels: {
|
|
209
|
+
...next.channels,
|
|
210
|
+
qqbot: {
|
|
211
|
+
...(next.channels?.qqbot as Record<string, unknown> || {}),
|
|
212
|
+
enabled: true,
|
|
213
|
+
appId,
|
|
214
|
+
clientSecret,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
} else {
|
|
219
|
+
next = {
|
|
220
|
+
...next,
|
|
221
|
+
channels: {
|
|
222
|
+
...next.channels,
|
|
223
|
+
qqbot: {
|
|
224
|
+
...(next.channels?.qqbot as Record<string, unknown> || {}),
|
|
225
|
+
enabled: true,
|
|
226
|
+
accounts: {
|
|
227
|
+
...((next.channels?.qqbot as QQBotChannelConfig)?.accounts || {}),
|
|
228
|
+
[accountId]: {
|
|
229
|
+
...((next.channels?.qqbot as QQBotChannelConfig)?.accounts?.[accountId] || {}),
|
|
230
|
+
enabled: true,
|
|
231
|
+
appId,
|
|
232
|
+
clientSecret,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { success: true, cfg: next as any, accountId };
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
disable: (cfg: unknown) => {
|
|
245
|
+
const config = cfg as OpenClawConfig;
|
|
246
|
+
return {
|
|
247
|
+
...config,
|
|
248
|
+
channels: {
|
|
249
|
+
...config.channels,
|
|
250
|
+
qqbot: { ...(config.channels?.qqbot as Record<string, unknown> || {}), enabled: false },
|
|
251
|
+
},
|
|
252
|
+
} as any;
|
|
253
|
+
},
|
|
254
|
+
};
|