@zhin.js/adapter-icqq 1.0.61 → 1.0.63

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/lib/index.js CHANGED
@@ -1,898 +1,15 @@
1
- import { Client, } from "@icqqjs/icqq";
1
+ /**
2
+ * ICQQ 适配器入口:类型扩展、导出、注册
3
+ */
2
4
  import path from "path";
3
- import { usePlugin, Adapter, Message, segment, MessageCommand, Notice, Request, createGroupManagementTools, GROUP_MANAGEMENT_SKILL_KEYWORDS, GROUP_MANAGEMENT_SKILL_TAGS, } from "zhin.js";
5
+ import { usePlugin } from "zhin.js";
6
+ import { MessageCommand } from "zhin.js";
7
+ import { IcqqAdapter } from "./adapter.js";
8
+ export * from "./types.js";
9
+ export { IcqqBot } from "./bot.js";
10
+ export { IcqqAdapter } from "./adapter.js";
4
11
  const plugin = usePlugin();
5
- const { useContext, addCommand } = plugin;
6
- export class IcqqBot extends Client {
7
- adapter;
8
- $connected = false;
9
- get $id() {
10
- return this.$config.name;
11
- }
12
- constructor(adapter, config) {
13
- if (!config.scope)
14
- config.scope = "icqqjs";
15
- if (!config.data_dir)
16
- config.data_dir = path.join(process.cwd(), "data");
17
- if (config.scope.startsWith("@"))
18
- config.scope = config.scope.slice(1);
19
- super(config);
20
- this.adapter = adapter;
21
- this.$config = config;
22
- }
23
- handleIcqqMessage(msg) {
24
- const message = this.$formatMessage(msg);
25
- this.adapter.emit("message.receive", message);
26
- plugin.logger.debug(`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`);
27
- }
28
- async $connect() {
29
- this.on("message", this.handleIcqqMessage.bind(this));
30
- // 监听通知事件
31
- this.on("notice.group.increase", (e) => this.handleGroupNotice(e, 'group_member_increase', 'increase'));
32
- this.on("notice.group.decrease", (e) => this.handleGroupNotice(e, 'group_member_decrease', 'decrease'));
33
- this.on("notice.group.recall", (e) => this.handleGroupNotice(e, 'group_recall', 'recall'));
34
- this.on("notice.group.admin", (e) => this.handleGroupNotice(e, 'group_admin_change', 'admin'));
35
- this.on("notice.group.ban", (e) => this.handleGroupNotice(e, 'group_ban', 'ban'));
36
- this.on("notice.group.transfer", (e) => this.handleGroupNotice(e, 'group_transfer', 'transfer'));
37
- this.on("notice.group.poke", (e) => this.handleGroupNotice(e, 'group_poke', 'poke'));
38
- this.on("notice.friend.increase", (e) => this.handleFriendNotice(e, 'friend_add', 'increase'));
39
- this.on("notice.friend.recall", (e) => this.handleFriendNotice(e, 'friend_recall', 'recall'));
40
- this.on("notice.friend.poke", (e) => this.handleFriendNotice(e, 'friend_poke', 'poke'));
41
- // 监听请求事件
42
- this.on("request.friend.add", (e) => this.handleFriendRequest(e));
43
- this.on("request.group.add", (e) => this.handleGroupRequest(e, 'group_add'));
44
- this.on("request.group.invite", (e) => this.handleGroupRequest(e, 'group_invite'));
45
- this.on("system.login.device", async (e) => {
46
- await this.sendSmsCode();
47
- plugin.logger.info("请输入短信验证码:");
48
- process.stdin.once("data", (data) => {
49
- this.submitSmsCode(data.toString().trim());
50
- });
51
- });
52
- this.on("system.login.qrcode", (e) => {
53
- plugin.logger.info(`取码地址:${e.image}\n请扫码完成后回车继续:`);
54
- process.stdin.once("data", () => {
55
- this.login();
56
- });
57
- });
58
- this.on("system.login.slider", (e) => {
59
- plugin.logger.info(`取码地址:${e.url}\n请输入滑块验证ticket:`);
60
- process.stdin.once("data", (e) => {
61
- this.submitSlider(e.toString().trim());
62
- });
63
- });
64
- return new Promise((resolve) => {
65
- this.once("system.online", () => {
66
- this.$connected = true;
67
- resolve();
68
- });
69
- this.login(Number(this.$config.name), this.$config.password);
70
- });
71
- }
72
- async $disconnect() {
73
- await this.logout();
74
- this.$connected = false;
75
- }
76
- $formatMessage(msg) {
77
- // 获取发送者的权限信息
78
- const senderInfo = this.getSenderInfo(msg);
79
- const result = Message.from(msg, {
80
- $id: msg.message_id.toString(),
81
- $adapter: "icqq",
82
- $bot: `${this.$config.name}`,
83
- $sender: senderInfo,
84
- $channel: {
85
- id: msg.message_type === "group"
86
- ? msg.group_id.toString()
87
- : msg.from_id.toString(),
88
- type: msg.message_type,
89
- },
90
- $content: IcqqBot.toSegments(msg.message),
91
- $raw: msg.raw_message,
92
- $timestamp: msg.time,
93
- $recall: async () => {
94
- await this.$recallMessage(result.$id);
95
- },
96
- $reply: async (content, quote) => {
97
- if (!Array.isArray(content))
98
- content = [content];
99
- if (quote)
100
- content.unshift({
101
- type: "reply",
102
- data: { id: typeof quote === "boolean" ? result.$id : quote },
103
- });
104
- return await this.adapter.sendMessage({
105
- ...result.$channel,
106
- context: "icqq",
107
- bot: `${this.uin}`,
108
- content,
109
- });
110
- },
111
- });
112
- return result;
113
- }
114
- /**
115
- * 获取发送者的详细权限信息
116
- */
117
- getSenderInfo(msg) {
118
- const senderInfo = {
119
- id: msg.sender.user_id.toString(),
120
- name: msg.sender.nickname.toString(),
121
- };
122
- // 群消息才有权限信息
123
- if (msg.message_type === "group") {
124
- const groupMsg = msg;
125
- const sender = groupMsg.sender;
126
- if (sender.role) {
127
- senderInfo.role = sender.role;
128
- senderInfo.isOwner = sender.role === "owner";
129
- senderInfo.isAdmin = sender.role === "admin" || sender.role === "owner";
130
- // 设置 permissions 数组,供 inferSenderPermissions 使用
131
- const perms = [];
132
- if (sender.role === "owner")
133
- perms.push("owner", "admin");
134
- else if (sender.role === "admin")
135
- perms.push("admin");
136
- senderInfo.permissions = perms;
137
- }
138
- if (sender.card) {
139
- senderInfo.card = sender.card;
140
- }
141
- if (sender.title) {
142
- senderInfo.title = sender.title;
143
- }
144
- }
145
- return senderInfo;
146
- }
147
- // ==================== 通知/请求事件处理 ====================
148
- handleGroupNotice(event, type, subType) {
149
- const notice = Notice.from(event, {
150
- $id: `${event.time || Date.now()}_${type}_${event.group_id}`,
151
- $adapter: 'icqq',
152
- $bot: this.$config.name,
153
- $type: type,
154
- $subType: subType,
155
- $channel: { id: event.group_id?.toString() || '', type: 'group' },
156
- $operator: event.operator_id ? { id: event.operator_id.toString(), name: event.operator_id.toString() } : undefined,
157
- $target: event.user_id ? { id: event.user_id.toString(), name: event.user_id.toString() } : (event.target_id ? { id: event.target_id.toString(), name: event.target_id.toString() } : undefined),
158
- $timestamp: event.time || Math.floor(Date.now() / 1000),
159
- });
160
- this.adapter.emit('notice.receive', notice);
161
- }
162
- handleFriendNotice(event, type, subType) {
163
- const notice = Notice.from(event, {
164
- $id: `${event.time || Date.now()}_${type}_${event.user_id}`,
165
- $adapter: 'icqq',
166
- $bot: this.$config.name,
167
- $type: type,
168
- $subType: subType,
169
- $channel: { id: event.user_id?.toString() || '', type: 'private' },
170
- $operator: event.operator_id ? { id: event.operator_id.toString(), name: event.operator_id.toString() } : undefined,
171
- $target: event.user_id ? { id: event.user_id.toString(), name: event.user_id.toString() } : undefined,
172
- $timestamp: event.time || Math.floor(Date.now() / 1000),
173
- });
174
- this.adapter.emit('notice.receive', notice);
175
- }
176
- handleFriendRequest(event) {
177
- const request = Request.from(event, {
178
- $id: event.flag || `${event.time}_friend_add_${event.user_id}`,
179
- $adapter: 'icqq',
180
- $bot: this.$config.name,
181
- $type: 'friend_add',
182
- $subType: event.sub_type,
183
- $channel: { id: event.user_id.toString(), type: 'private' },
184
- $sender: { id: event.user_id.toString(), name: event.nickname || event.user_id.toString() },
185
- $comment: event.comment,
186
- $timestamp: event.time || Math.floor(Date.now() / 1000),
187
- $approve: async () => { await event.approve(true); },
188
- $reject: async () => { await event.approve(false); },
189
- });
190
- this.adapter.emit('request.receive', request);
191
- }
192
- handleGroupRequest(event, type) {
193
- const request = Request.from(event, {
194
- $id: event.flag || `${event.time}_${type}_${event.user_id}`,
195
- $adapter: 'icqq',
196
- $bot: this.$config.name,
197
- $type: type,
198
- $subType: event.sub_type,
199
- $channel: { id: event.group_id.toString(), type: 'group' },
200
- $sender: { id: event.user_id.toString(), name: event.nickname || event.user_id.toString() },
201
- $comment: 'comment' in event ? event.comment : undefined,
202
- $timestamp: event.time || Math.floor(Date.now() / 1000),
203
- $approve: async () => { await event.approve(true); },
204
- $reject: async () => { await event.approve(false); },
205
- });
206
- this.adapter.emit('request.receive', request);
207
- }
208
- // ==================== 群管理 API ====================
209
- /**
210
- * 踢出群成员
211
- * @param groupId 群号
212
- * @param userId 用户QQ号
213
- * @param block 是否拉黑(加入黑名单)
214
- */
215
- async kickMember(groupId, userId, block) {
216
- try {
217
- const group = this.pickGroup(groupId);
218
- const result = await group.kickMember(userId, undefined, block);
219
- plugin.logger.info(`ICQQ Bot ${this.$id} 踢出成员 ${userId} 从群 ${groupId}${block ? "(已拉黑)" : ""}`);
220
- return result;
221
- }
222
- catch (error) {
223
- plugin.logger.error(`ICQQ Bot ${this.$id} 踢出成员失败:`, error);
224
- throw error;
225
- }
226
- }
227
- /**
228
- * 禁言群成员
229
- * @param groupId 群号
230
- * @param userId 用户QQ号
231
- * @param duration 禁言时长(秒),0 表示解除禁言
232
- */
233
- async muteMember(groupId, userId, duration = 600) {
234
- try {
235
- const group = this.pickGroup(groupId);
236
- const result = await group.muteMember(userId, duration);
237
- plugin.logger.info(`ICQQ Bot ${this.$id} ${duration > 0
238
- ? `禁言成员 ${userId} ${duration}秒`
239
- : `解除成员 ${userId} 禁言`}(群 ${groupId})`);
240
- return result;
241
- }
242
- catch (error) {
243
- plugin.logger.error(`ICQQ Bot ${this.$id} 禁言操作失败:`, error);
244
- throw error;
245
- }
246
- }
247
- /**
248
- * 全员禁言
249
- * @param groupId 群号
250
- * @param enable 是否开启全员禁言
251
- */
252
- async muteAll(groupId, enable = true) {
253
- try {
254
- const group = this.pickGroup(groupId);
255
- const result = await group.muteAll(enable);
256
- plugin.logger.info(`ICQQ Bot ${this.$id} ${enable ? "开启" : "关闭"}全员禁言(群 ${groupId})`);
257
- return result;
258
- }
259
- catch (error) {
260
- plugin.logger.error(`ICQQ Bot ${this.$id} 全员禁言操作失败:`, error);
261
- throw error;
262
- }
263
- }
264
- /**
265
- * 设置管理员
266
- * @param groupId 群号
267
- * @param userId 用户QQ号
268
- * @param enable 是否设为管理员
269
- */
270
- async setAdmin(groupId, userId, enable = true) {
271
- try {
272
- const group = this.pickGroup(groupId);
273
- const result = await group.setAdmin(userId, enable);
274
- plugin.logger.info(`ICQQ Bot ${this.$id} ${enable ? "设置" : "取消"}管理员 ${userId}(群 ${groupId})`);
275
- return result;
276
- }
277
- catch (error) {
278
- plugin.logger.error(`ICQQ Bot ${this.$id} 设置管理员失败:`, error);
279
- throw error;
280
- }
281
- }
282
- /**
283
- * 设置群名片
284
- * @param groupId 群号
285
- * @param userId 用户QQ号
286
- * @param card 群名片
287
- */
288
- async setCard(groupId, userId, card) {
289
- try {
290
- const group = this.pickGroup(groupId);
291
- const result = await group.setCard(userId, card);
292
- plugin.logger.info(`ICQQ Bot ${this.$id} 设置成员 ${userId} 群名片为 "${card}"(群 ${groupId})`);
293
- return result;
294
- }
295
- catch (error) {
296
- plugin.logger.error(`ICQQ Bot ${this.$id} 设置群名片失败:`, error);
297
- throw error;
298
- }
299
- }
300
- /**
301
- * 设置群头衔
302
- * @param groupId 群号
303
- * @param userId 用户QQ号
304
- * @param title 头衔
305
- * @param duration 持续时间(秒),-1 表示永久
306
- */
307
- async setTitle(groupId, userId, title, duration = -1) {
308
- try {
309
- const group = this.pickGroup(groupId);
310
- const result = await group.setTitle(userId, title, duration);
311
- plugin.logger.info(`ICQQ Bot ${this.$id} 设置成员 ${userId} 头衔为 "${title}"(群 ${groupId})`);
312
- return result;
313
- }
314
- catch (error) {
315
- plugin.logger.error(`ICQQ Bot ${this.$id} 设置头衔失败:`, error);
316
- throw error;
317
- }
318
- }
319
- /**
320
- * 设置群名
321
- * @param groupId 群号
322
- * @param name 群名
323
- */
324
- async setGroupName(groupId, name) {
325
- try {
326
- const group = this.pickGroup(groupId);
327
- const result = await group.setName(name);
328
- plugin.logger.info(`ICQQ Bot ${this.$id} 设置群名为 "${name}"(群 ${groupId})`);
329
- return result;
330
- }
331
- catch (error) {
332
- plugin.logger.error(`ICQQ Bot ${this.$id} 设置群名失败:`, error);
333
- throw error;
334
- }
335
- }
336
- /**
337
- * 发送群公告
338
- * @param groupId 群号
339
- * @param content 公告内容
340
- */
341
- async sendAnnounce(groupId, content) {
342
- try {
343
- const group = this.pickGroup(groupId);
344
- const result = await group.announce(content);
345
- plugin.logger.info(`ICQQ Bot ${this.$id} 发送群公告(群 ${groupId})`);
346
- return result;
347
- }
348
- catch (error) {
349
- plugin.logger.error(`ICQQ Bot ${this.$id} 发送群公告失败:`, error);
350
- throw error;
351
- }
352
- }
353
- /**
354
- * 戳一戳
355
- * @param groupId 群号
356
- * @param userId 用户QQ号
357
- */
358
- async pokeMember(groupId, userId) {
359
- try {
360
- const group = this.pickGroup(groupId);
361
- const result = await group.pokeMember(userId);
362
- plugin.logger.info(`ICQQ Bot ${this.$id} 戳了戳 ${userId}(群 ${groupId})`);
363
- return result;
364
- }
365
- catch (error) {
366
- plugin.logger.error(`ICQQ Bot ${this.$id} 戳一戳失败:`, error);
367
- throw error;
368
- }
369
- }
370
- /**
371
- * 获取群成员列表
372
- * @param groupId 群号
373
- */
374
- async getMemberList(groupId) {
375
- try {
376
- const group = this.pickGroup(groupId);
377
- return await group.getMemberMap();
378
- }
379
- catch (error) {
380
- plugin.logger.error(`ICQQ Bot ${this.$id} 获取群成员列表失败:`, error);
381
- throw error;
382
- }
383
- }
384
- /**
385
- * 获取被禁言成员列表
386
- * @param groupId 群号
387
- */
388
- async getMutedMembers(groupId) {
389
- try {
390
- const group = this.pickGroup(groupId);
391
- return await group.getMuteMemberList();
392
- }
393
- catch (error) {
394
- plugin.logger.error(`ICQQ Bot ${this.$id} 获取禁言列表失败:`, error);
395
- throw error;
396
- }
397
- }
398
- /**
399
- * 允许/禁止匿名
400
- * @param groupId 群号
401
- * @param enable 是否允许匿名
402
- */
403
- async setAnonymous(groupId, enable = true) {
404
- try {
405
- const group = this.pickGroup(groupId);
406
- const result = await group.allowAnony(enable);
407
- plugin.logger.info(`ICQQ Bot ${this.$id} ${enable ? "开启" : "关闭"}匿名(群 ${groupId})`);
408
- return result;
409
- }
410
- catch (error) {
411
- plugin.logger.error(`ICQQ Bot ${this.$id} 设置匿名失败:`, error);
412
- throw error;
413
- }
414
- }
415
- async getGroupFiles(groupId) {
416
- try {
417
- const group = this.pickGroup(groupId);
418
- return await group.fs.ls();
419
- }
420
- catch (error) {
421
- plugin.logger.error(`ICQQ Bot ${this.$id} 获取群文件列表失败:`, error);
422
- throw error;
423
- }
424
- }
425
- async $recallMessage(id) {
426
- await this.deleteMsg(id);
427
- }
428
- async $sendMessage(options) {
429
- switch (options.type) {
430
- case "private": {
431
- const result = await this.sendPrivateMsg(Number(options.id), IcqqBot.toSendable(options.content));
432
- plugin.logger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
433
- return result.message_id.toString();
434
- }
435
- case "group": {
436
- const result = await this.sendGroupMsg(Number(options.id), IcqqBot.toSendable(options.content));
437
- plugin.logger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
438
- return result.message_id.toString();
439
- }
440
- default:
441
- throw new Error(`unsupported channel type ${options.type}`);
442
- }
443
- }
444
- }
445
- (function (IcqqBot) {
446
- const allowTypes = [
447
- "text",
448
- "face",
449
- "image",
450
- "record",
451
- "audio",
452
- "dice",
453
- "rps",
454
- "video",
455
- "file",
456
- "location",
457
- "share",
458
- "json",
459
- "at",
460
- "reply",
461
- "long_msg",
462
- "button",
463
- "markdown",
464
- "xml",
465
- ];
466
- function toSegments(message) {
467
- if (!Array.isArray(message))
468
- message = [message];
469
- return message
470
- .filter((item, index) => {
471
- return (typeof item === "string" || item.type !== "long_msg" || index !== 0);
472
- })
473
- .map((item) => {
474
- if (typeof item === "string")
475
- return { type: "text", data: { text: item } };
476
- const { type, ...data } = item;
477
- return { type, data };
478
- });
479
- }
480
- IcqqBot.toSegments = toSegments;
481
- function toSendable(content) {
482
- if (!Array.isArray(content))
483
- content = [content];
484
- return content.map((seg) => {
485
- if (typeof seg === "string")
486
- return { type: "text", text: seg };
487
- let { type, data } = seg;
488
- if (typeof type === "function")
489
- type = type.name;
490
- if (!allowTypes.includes(type))
491
- return { type: "text", text: segment.toString(seg) };
492
- return { type, ...data };
493
- });
494
- }
495
- IcqqBot.toSendable = toSendable;
496
- })(IcqqBot || (IcqqBot = {}));
497
- class IcqqAdapter extends Adapter {
498
- constructor(plugin) {
499
- super(plugin, "icqq", []);
500
- }
501
- createBot(config) {
502
- return new IcqqBot(this, config);
503
- }
504
- // ── IGroupManagement 标准群管方法 ──────────────────────────────────
505
- async kickMember(botId, sceneId, userId) {
506
- const bot = this.bots.get(botId);
507
- if (!bot)
508
- throw new Error(`Bot ${botId} 不存在`);
509
- return bot.kickMember(Number(sceneId), Number(userId), false);
510
- }
511
- async muteMember(botId, sceneId, userId, duration = 600) {
512
- const bot = this.bots.get(botId);
513
- if (!bot)
514
- throw new Error(`Bot ${botId} 不存在`);
515
- return bot.muteMember(Number(sceneId), Number(userId), duration);
516
- }
517
- async muteAll(botId, sceneId, enable = true) {
518
- const bot = this.bots.get(botId);
519
- if (!bot)
520
- throw new Error(`Bot ${botId} 不存在`);
521
- return bot.muteAll(Number(sceneId), enable);
522
- }
523
- async setAdmin(botId, sceneId, userId, enable = true) {
524
- const bot = this.bots.get(botId);
525
- if (!bot)
526
- throw new Error(`Bot ${botId} 不存在`);
527
- return bot.setAdmin(Number(sceneId), Number(userId), enable);
528
- }
529
- async setMemberNickname(botId, sceneId, userId, nickname) {
530
- const bot = this.bots.get(botId);
531
- if (!bot)
532
- throw new Error(`Bot ${botId} 不存在`);
533
- return bot.setCard(Number(sceneId), Number(userId), nickname);
534
- }
535
- async setGroupName(botId, sceneId, name) {
536
- const bot = this.bots.get(botId);
537
- if (!bot)
538
- throw new Error(`Bot ${botId} 不存在`);
539
- return bot.setGroupName(Number(sceneId), name);
540
- }
541
- async listMembers(botId, sceneId) {
542
- const bot = this.bots.get(botId);
543
- if (!bot)
544
- throw new Error(`Bot ${botId} 不存在`);
545
- const memberMap = await bot.getMemberList(Number(sceneId));
546
- const members = Array.from(memberMap.values()).map((m) => ({
547
- user_id: m.user_id,
548
- nickname: m.nickname,
549
- card: m.card,
550
- role: m.role,
551
- title: m.title,
552
- }));
553
- return { members, count: members.length };
554
- }
555
- // ── 生命周期 ───────────────────────────────────────────────────────
556
- async start() {
557
- this.registerIcqqPlatformTools();
558
- const groupTools = createGroupManagementTools(this, this.name);
559
- groupTools.forEach((t) => this.addTool(t));
560
- this.declareSkill({
561
- description: 'ICQQ(QQ 协议)群管理:踢人、禁言、设管理员、改名片、头衔、群公告等。只有昵称时请先调用 list_members 查 QQ 号再操作。',
562
- keywords: GROUP_MANAGEMENT_SKILL_KEYWORDS,
563
- tags: GROUP_MANAGEMENT_SKILL_TAGS,
564
- });
565
- await super.start();
566
- }
567
- /**
568
- * 注册 ICQQ 平台特有工具(头衔、戳一戳、群公告等)
569
- */
570
- registerIcqqPlatformTools() {
571
- const CTX_BOT = {
572
- type: "string",
573
- description: "执行操作的 Bot QQ号",
574
- contextKey: "botId",
575
- };
576
- const CTX_GROUP = {
577
- type: "number",
578
- description: "目标群号",
579
- contextKey: "sceneId",
580
- };
581
- // 设置头衔工具
582
- this.addTool({
583
- name: "icqq_set_title",
584
- description: "设置 QQ 群成员的专属头衔(显示在群昵称旁边的标签)。只有群主才能操作。可设置持续时间或永久。如果只有昵称没有 QQ号,请先调用 list_members 查询。",
585
- tags: ["群管理", "成员管理", "头衔"],
586
- keywords: ["头衔", "专属头衔", "设置头衔", "给头衔", "加头衔", "称号"],
587
- parameters: {
588
- type: "object",
589
- properties: {
590
- bot: CTX_BOT,
591
- group_id: CTX_GROUP,
592
- user_id: {
593
- type: "number",
594
- description: "目标成员 QQ号",
595
- },
596
- title: {
597
- type: "string",
598
- description: "头衔文字内容",
599
- },
600
- duration: {
601
- type: "number",
602
- description: "持续时间(秒),-1 表示永久,默认永久",
603
- },
604
- },
605
- required: ["bot", "group_id", "user_id", "title"],
606
- },
607
- platforms: ["icqq"],
608
- scopes: ["group"],
609
- permissionLevel: "group_owner",
610
- execute: async (args, context) => {
611
- const { bot: botId, group_id, user_id, title, duration = -1 } = args;
612
- const bot = this.bots.get(botId);
613
- if (!bot)
614
- throw new Error(`Bot ${botId} 不存在`);
615
- this.checkPermission(context, "group_owner");
616
- const success = await bot.setTitle(group_id, user_id, title, duration);
617
- return {
618
- success,
619
- message: success
620
- ? `已将 ${user_id} 的头衔设为 "${title}"`
621
- : "设置失败",
622
- };
623
- },
624
- });
625
- // 发送群公告工具
626
- this.addTool({
627
- name: "icqq_announce",
628
- description: "发送 QQ 群公告(需要管理员权限)",
629
- parameters: {
630
- type: "object",
631
- properties: {
632
- bot: CTX_BOT,
633
- group_id: CTX_GROUP,
634
- content: {
635
- type: "string",
636
- description: "公告内容",
637
- },
638
- },
639
- required: ["bot", "group_id", "content"],
640
- },
641
- platforms: ["icqq"],
642
- scopes: ["group"],
643
- permissionLevel: "group_admin",
644
- execute: async (args, context) => {
645
- const { bot: botId, group_id, content } = args;
646
- const bot = this.bots.get(botId);
647
- if (!bot)
648
- throw new Error(`Bot ${botId} 不存在`);
649
- this.checkPermission(context, "group_admin");
650
- const success = await bot.sendAnnounce(group_id, content);
651
- return { success, message: success ? "群公告已发送" : "发送失败" };
652
- },
653
- });
654
- // 戳一戳工具
655
- this.addTool({
656
- name: "icqq_poke",
657
- description: '在 QQ 群中对某个成员执行"戳一戳"互动操作(类似拍一拍)。任何人都可以使用。注意:每次请求只戳一次,不要重复调用此工具。如果只有昵称没有 QQ号,请先调用 list_members 查询。',
658
- tags: ["互动", "趣味", "戳一戳"],
659
- keywords: ["戳", "戳一戳", "拍一拍", "拍", "碰一碰", "poke"],
660
- parameters: {
661
- type: "object",
662
- properties: {
663
- bot: CTX_BOT,
664
- group_id: CTX_GROUP,
665
- user_id: {
666
- type: "number",
667
- description: "要戳的目标成员 QQ号",
668
- },
669
- },
670
- required: ["bot", "group_id", "user_id"],
671
- },
672
- platforms: ["icqq"],
673
- scopes: ["group"],
674
- permissionLevel: "user",
675
- execute: async (args) => {
676
- const { bot: botId, group_id, user_id } = args;
677
- const bot = this.bots.get(botId);
678
- if (!bot)
679
- throw new Error(`Bot ${botId} 不存在`);
680
- const success = await bot.pokeMember(group_id, user_id);
681
- return {
682
- success,
683
- message: success ? `已戳了戳 ${user_id}` : "戳一戳失败",
684
- };
685
- },
686
- });
687
- // 获取被禁言列表工具
688
- this.addTool({
689
- name: "icqq_list_muted",
690
- description: "查询 QQ 群中当前被禁言的成员列表,返回被禁言成员的 QQ号和剩余禁言时间。此工具仅用于查询,不会执行禁言操作。如需禁言或解除禁言,请使用 icqq_mute_member 工具。",
691
- tags: ["群查询", "禁言查询", "列表"],
692
- keywords: ["禁言列表", "被禁言", "谁被禁言", "查看禁言", "禁言名单"],
693
- parameters: {
694
- type: "object",
695
- properties: {
696
- bot: CTX_BOT,
697
- group_id: CTX_GROUP,
698
- },
699
- required: ["bot", "group_id"],
700
- },
701
- platforms: ["icqq"],
702
- scopes: ["group"],
703
- permissionLevel: "user",
704
- execute: async (args) => {
705
- const { bot: botId, group_id } = args;
706
- const bot = this.bots.get(botId);
707
- if (!bot)
708
- throw new Error(`Bot ${botId} 不存在`);
709
- const mutedList = await bot.getMutedMembers(group_id);
710
- return {
711
- muted_members: mutedList.filter((m) => m !== null),
712
- count: mutedList.filter((m) => m !== null).length,
713
- };
714
- },
715
- });
716
- this.addTool({
717
- name: "icqq_send_user_like",
718
- description: "给用户点赞(竖大拇指)。每人每天最多点赞 20 次,超出无效。如果只有昵称没有 QQ号,请先调用 list_members 查询。",
719
- tags: ["互动", "趣味", "点赞"],
720
- keywords: ["点赞", "赞我", "赞一下", "大拇指"],
721
- parameters: {
722
- type: "object",
723
- properties: {
724
- bot: CTX_BOT,
725
- user_id: {
726
- type: "number",
727
- description: "要点赞的目标用户 QQ号(如果只有昵称,先用 list_members 查询获取)",
728
- },
729
- times: {
730
- type: "number",
731
- description: "点赞次数(1-20),默认 1 次,每人每天上限 20 次",
732
- default: 1,
733
- },
734
- },
735
- required: ["bot", "user_id"],
736
- },
737
- platforms: ["icqq"],
738
- scopes: ["group", "private"],
739
- permissionLevel: "user",
740
- execute: async (args) => {
741
- const { bot: botId, user_id, times = 1 } = args;
742
- const bot = this.bots.get(botId);
743
- if (!bot)
744
- throw new Error(`Bot ${botId} 不存在`);
745
- const success = await bot.sendLike(user_id, Math.min(times, 20));
746
- return {
747
- success,
748
- message: success ? `已发送用户点赞消息给 ${user_id}` : "发送失败",
749
- };
750
- },
751
- });
752
- // 设置匿名状态工具
753
- this.addTool({
754
- name: "icqq_set_anonymous",
755
- description: "开启或关闭 QQ 群的匿名聊天功能。开启后群成员可以匿名发言。需要 Bot 拥有管理员权限。",
756
- tags: ["群管理", "群设置", "匿名"],
757
- keywords: ["匿名", "匿名聊天", "开启匿名", "关闭匿名", "允许匿名"],
758
- parameters: {
759
- type: "object",
760
- properties: {
761
- bot: CTX_BOT,
762
- group_id: CTX_GROUP,
763
- enable: {
764
- type: "boolean",
765
- description: "true=开启匿名聊天,false=关闭匿名聊天,默认 true",
766
- },
767
- },
768
- required: ["bot", "group_id"],
769
- },
770
- platforms: ["icqq"],
771
- scopes: ["group"],
772
- permissionLevel: "group_admin",
773
- execute: async (args, context) => {
774
- const { bot: botId, group_id, enable = true } = args;
775
- const bot = this.bots.get(botId);
776
- if (!bot)
777
- throw new Error(`Bot ${botId} 不存在`);
778
- this.checkPermission(context, "group_admin");
779
- const success = await bot.setAnonymous(group_id, enable);
780
- return {
781
- success,
782
- message: success
783
- ? enable
784
- ? "已开启匿名聊天"
785
- : "已关闭匿名聊天"
786
- : "操作失败",
787
- };
788
- },
789
- });
790
- // 群文件列表
791
- this.addTool({
792
- name: "icqq_group_files",
793
- description: "获取 QQ 群的群文件列表",
794
- parameters: {
795
- type: "object",
796
- properties: {
797
- bot: { type: "string", description: "Bot 名称" },
798
- group_id: { type: "number", description: "群号" },
799
- },
800
- required: ["bot", "group_id"],
801
- },
802
- platforms: ["icqq"],
803
- scopes: ["group"],
804
- permissionLevel: "user",
805
- execute: async (args) => {
806
- const { bot: botId, group_id } = args;
807
- const bot = this.bots.get(botId);
808
- if (!bot)
809
- throw new Error(`Bot ${botId} 不存在`);
810
- const files = await bot.getGroupFiles(group_id);
811
- if (!files?.length)
812
- return { files: [], message: "群文件为空" };
813
- return {
814
- files: files.slice(0, 30).map((f) => ({
815
- name: f.name,
816
- size: f.size,
817
- uploader: f.uploader_uin,
818
- upload_time: f.upload_time,
819
- })),
820
- count: files.length,
821
- };
822
- },
823
- });
824
- // 好友列表
825
- this.addTool({
826
- name: "icqq_friend_list",
827
- description: "获取 QQ 好友列表",
828
- parameters: {
829
- type: "object",
830
- properties: {
831
- bot: { type: "string", description: "Bot 名称" },
832
- },
833
- required: ["bot"],
834
- },
835
- platforms: ["icqq"],
836
- scopes: ["group", "private"],
837
- permissionLevel: "user",
838
- execute: async (args) => {
839
- const { bot: botId } = args;
840
- const bot = this.bots.get(botId);
841
- if (!bot)
842
- throw new Error(`Bot ${botId} 不存在`);
843
- const fl = bot.fl;
844
- const friends = Array.from(fl.values()).map((f) => ({
845
- user_id: f.user_id,
846
- nickname: f.nickname,
847
- remark: f.remark,
848
- }));
849
- return { friends: friends.slice(0, 50), count: fl.size };
850
- },
851
- });
852
- plugin.logger.debug("已注册 ICQQ 平台群管理工具");
853
- }
854
- /**
855
- * 检查执行上下文中的权限
856
- * 双重验证:优先从 ToolContext 获取权限级别,其次从消息发送者信息获取
857
- */
858
- checkPermission(context, required) {
859
- const permissionLevels = {
860
- user: 0,
861
- group_admin: 1,
862
- group_owner: 2,
863
- bot_admin: 3,
864
- owner: 4,
865
- };
866
- const requiredLevel = permissionLevels[required] ?? 0;
867
- if (requiredLevel === 0)
868
- return; // user 级别无需检查
869
- // 1. 优先从 ToolContext.senderPermissionLevel 获取(AI Agent 路径注入)
870
- if (context?.senderPermissionLevel) {
871
- const ctxLevel = permissionLevels[context.senderPermissionLevel] ?? 0;
872
- if (ctxLevel < requiredLevel) {
873
- throw new Error(`权限不足:需要 ${required} 权限,当前为 ${context.senderPermissionLevel}`);
874
- }
875
- return; // 检查通过
876
- }
877
- // 2. 从消息的 $sender 获取权限(命令行/中间件路径)
878
- const sender = context?.message?.$sender;
879
- if (!sender) {
880
- // 无上下文且无发送者信息 → 拒绝高权限操作
881
- throw new Error(`权限不足:无法验证身份,拒绝执行需要 ${required} 权限的操作`);
882
- }
883
- let senderLevel = "user";
884
- if (sender.isOwner || sender.role === "owner") {
885
- senderLevel = "group_owner";
886
- }
887
- else if (sender.isAdmin || sender.role === "admin") {
888
- senderLevel = "group_admin";
889
- }
890
- if (permissionLevels[senderLevel] < requiredLevel) {
891
- throw new Error(`权限不足:需要 ${required} 权限,当前为 ${senderLevel}`);
892
- }
893
- }
894
- }
895
- const { provide } = usePlugin();
12
+ const { provide, useContext, addCommand } = plugin;
896
13
  provide({
897
14
  name: "icqq",
898
15
  description: "ICQQ Adapter",
@@ -921,25 +38,19 @@ useContext("icqq", (icqq) => {
921
38
  times += 20;
922
39
  if (send3Result)
923
40
  times += 10;
924
- return `给你咱好啦,你已经获得了${times}个赞`;
41
+ return `给你赞好啦,你已经获得了${times}个赞`;
925
42
  }));
926
43
  });
927
44
  useContext("web", (web) => {
928
- // 注册ICQQ适配器的客户端入口文件
929
45
  const dispose = web.addEntry(path.resolve(import.meta.dirname, "../client/index.tsx"));
930
46
  return dispose;
931
47
  });
932
- useContext("router", async (router) => {
933
- const icqq = plugin.root.inject("icqq");
48
+ useContext("router", 'icqq', async (router, icqq) => {
934
49
  router.get("/api/icqq/bots", async (ctx) => {
935
50
  try {
936
51
  const bots = Array.from(icqq.bots.values());
937
52
  if (bots.length === 0) {
938
- ctx.body = {
939
- success: true,
940
- data: [],
941
- message: "暂无ICQQ机器人实例",
942
- };
53
+ ctx.body = { success: true, data: [], message: "暂无ICQQ机器人实例" };
943
54
  return;
944
55
  }
945
56
  const result = bots.map((bot) => {
@@ -956,8 +67,7 @@ useContext("router", async (router) => {
956
67
  lastActivity: new Date().toISOString(),
957
68
  };
958
69
  }
959
- catch (botError) {
960
- // 单个机器人数据获取失败时的处理
70
+ catch {
961
71
  return {
962
72
  name: bot.$config.name,
963
73
  connected: false,
@@ -971,11 +81,7 @@ useContext("router", async (router) => {
971
81
  };
972
82
  }
973
83
  });
974
- ctx.body = {
975
- success: true,
976
- data: result,
977
- timestamp: new Date().toISOString(),
978
- };
84
+ ctx.body = { success: true, data: result, timestamp: new Date().toISOString() };
979
85
  }
980
86
  catch (error) {
981
87
  ctx.status = 500;
@@ -983,9 +89,7 @@ useContext("router", async (router) => {
983
89
  success: false,
984
90
  error: "ICQQ_API_ERROR",
985
91
  message: "获取机器人数据失败",
986
- details: process.env.NODE_ENV === "development"
987
- ? error.message
988
- : undefined,
92
+ details: process.env.NODE_ENV === "development" ? error.message : undefined,
989
93
  timestamp: new Date().toISOString(),
990
94
  };
991
95
  }