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