@zhin.js/adapter-icqq 2.0.6 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/adapter.d.ts +7 -2
  3. package/lib/adapter.d.ts.map +1 -1
  4. package/lib/adapter.js +78 -33
  5. package/lib/adapter.js.map +1 -1
  6. package/lib/bot.d.ts +29 -29
  7. package/lib/bot.d.ts.map +1 -1
  8. package/lib/bot.js +226 -405
  9. package/lib/bot.js.map +1 -1
  10. package/lib/commands/index.d.ts +7 -0
  11. package/lib/commands/index.d.ts.map +1 -0
  12. package/lib/commands/index.js +30 -0
  13. package/lib/commands/index.js.map +1 -0
  14. package/lib/index.d.ts +1 -1
  15. package/lib/index.d.ts.map +1 -1
  16. package/lib/index.js +13 -297
  17. package/lib/index.js.map +1 -1
  18. package/lib/ipc-client.d.ts +60 -0
  19. package/lib/ipc-client.d.ts.map +1 -0
  20. package/lib/ipc-client.js +272 -0
  21. package/lib/ipc-client.js.map +1 -0
  22. package/lib/protocol.d.ts +174 -0
  23. package/lib/protocol.d.ts.map +1 -0
  24. package/lib/protocol.js +162 -0
  25. package/lib/protocol.js.map +1 -0
  26. package/lib/routes.d.ts +8 -0
  27. package/lib/routes.d.ts.map +1 -0
  28. package/lib/routes.js +67 -0
  29. package/lib/routes.js.map +1 -0
  30. package/lib/tools/index.d.ts +10 -0
  31. package/lib/tools/index.d.ts.map +1 -0
  32. package/lib/tools/index.js +336 -0
  33. package/lib/tools/index.js.map +1 -0
  34. package/lib/types.d.ts +45 -7
  35. package/lib/types.d.ts.map +1 -1
  36. package/lib/types.js +5 -0
  37. package/lib/types.js.map +1 -1
  38. package/package.json +3 -5
  39. package/plugin.yml +1 -1
  40. package/skills/icqq/SKILL.md +31 -64
  41. package/skills/icqq/references/friends.md +54 -0
  42. package/skills/icqq/references/general.md +145 -0
  43. package/skills/icqq/references/gfs.md +49 -0
  44. package/skills/icqq/references/groups.md +71 -0
  45. package/skills/icqq/references/messaging.md +66 -0
  46. package/skills/icqq/references/requests.md +27 -0
  47. package/skills/icqq/references/settings.md +38 -0
  48. package/src/adapter.ts +73 -35
  49. package/src/bot.ts +272 -443
  50. package/src/commands/index.ts +32 -0
  51. package/src/index.ts +14 -305
  52. package/src/ipc-client.ts +326 -0
  53. package/src/protocol.ts +242 -0
  54. package/src/routes.ts +83 -0
  55. package/src/tools/index.ts +407 -0
  56. package/src/types.ts +47 -7
package/lib/bot.js CHANGED
@@ -1,459 +1,280 @@
1
1
  /**
2
- * ICQQ Bot:继承 icqq Client,实现 zhin Bot 接口
2
+ * ICQQ Bot:通过 @icqqjs/cli 守护进程 IPC 通信,实现 zhin Bot 接口
3
+ *
4
+ * 不再直接依赖 @icqqjs/icqq 协议库。
5
+ * 登录由 `icqq login` 完成,本 Bot 只负责连接守护进程并收发消息。
3
6
  */
4
- import { Client, } from "@icqqjs/icqq";
5
- import path from "path";
6
- import { Message, segment, Notice, Request, } from "zhin.js";
7
- export class IcqqBot extends Client {
7
+ import { Message, segment, } from "zhin.js";
8
+ import { IpcClient } from "./ipc-client.js";
9
+ import { Actions, } from "./protocol.js";
10
+ export class IcqqBot {
8
11
  adapter;
9
12
  $connected = false;
10
13
  $config;
11
- get pluginLogger() {
12
- return this.adapter.plugin.logger;
13
- }
14
+ ipc;
15
+ /** 缓存的好友列表 */
16
+ friends = new Map();
17
+ /** 缓存的群列表 */
18
+ groups = new Map();
19
+ subscriptions = [];
14
20
  get $id() {
15
21
  return this.$config.name;
16
22
  }
23
+ get logger() {
24
+ return this.adapter.plugin.logger;
25
+ }
17
26
  constructor(adapter, config) {
18
- if (!config.scope)
19
- config.scope = "icqqjs";
20
- if (!config.data_dir)
21
- config.data_dir = path.join(process.cwd(), "data");
22
- if (config.scope.startsWith("@"))
23
- config.scope = config.scope.slice(1);
24
- super(config);
25
27
  this.adapter = adapter;
26
28
  this.$config = config;
27
29
  }
28
- handleIcqqMessage(msg) {
29
- const message = this.$formatMessage(msg);
30
- this.adapter.emit("message.receive", message);
31
- this.pluginLogger.debug(`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`);
32
- }
30
+ // ── 连接 ───────────────────────────────────────────────────────────
33
31
  async $connect() {
34
- this.on("message", this.handleIcqqMessage.bind(this));
35
- this.on("notice.group.increase", (e) => this.handleGroupNotice(e, 'group_member_increase', 'increase'));
36
- this.on("notice.group.decrease", (e) => this.handleGroupNotice(e, 'group_member_decrease', 'decrease'));
37
- this.on("notice.group.recall", (e) => this.handleGroupNotice(e, 'group_recall', 'recall'));
38
- this.on("notice.group.admin", (e) => this.handleGroupNotice(e, 'group_admin_change', 'admin'));
39
- this.on("notice.group.ban", (e) => this.handleGroupNotice(e, 'group_ban', 'ban'));
40
- this.on("notice.group.transfer", (e) => this.handleGroupNotice(e, 'group_transfer', 'transfer'));
41
- this.on("notice.group.poke", (e) => this.handleGroupNotice(e, 'group_poke', 'poke'));
42
- this.on("notice.friend.increase", (e) => this.handleFriendNotice(e, 'friend_add', 'increase'));
43
- this.on("notice.friend.recall", (e) => this.handleFriendNotice(e, 'friend_recall', 'recall'));
44
- this.on("notice.friend.poke", (e) => this.handleFriendNotice(e, 'friend_poke', 'poke'));
45
- this.on("request.friend.add", (e) => this.handleFriendRequest(e));
46
- this.on("request.group.add", (e) => this.handleGroupRequest(e, 'group_add'));
47
- this.on("request.group.invite", (e) => this.handleGroupRequest(e, 'group_invite'));
48
- this.on("system.login.device", async (e) => {
49
- this.pluginLogger.info(`[${this.$config.name}] 触发设备验证,正在发送短信验证码...`);
50
- await this.sendSmsCode();
51
- this.pluginLogger.info(`[${this.$config.name}] 短信验证码已发送,请查收手机短信`);
52
- this.pluginLogger.info(`[${this.$config.name}] 请在终端输入短信验证码:`);
53
- process.stdin.once("data", (data) => {
54
- this.pluginLogger.info(`[${this.$config.name}] 正在提交短信验证码...`);
55
- this.submitSmsCode(data.toString().trim());
56
- });
57
- });
58
- this.on("system.login.qrcode", async (e) => {
59
- this.pluginLogger.info(`[${this.$config.name}] 触发扫码登录,请使用手机 QQ 扫描二维码`);
60
- this.pluginLogger.info(`[${this.$config.name}] 扫码完成后按回车继续`);
61
- process.stdin.once("data", () => {
62
- this.pluginLogger.info(`[${this.$config.name}] 扫码已确认,正在登录...`);
63
- this.login();
64
- });
65
- });
66
- this.on("system.login.slider", async (e) => {
67
- this.pluginLogger.info(`[${this.$config.name}] 触发滑块验证`);
68
- this.pluginLogger.info(`[${this.$config.name}] 验证地址: ${e.url}`);
69
- this.pluginLogger.info(`[${this.$config.name}] 请在浏览器中完成滑块验证后,在终端输入 ticket:`);
70
- process.stdin.once("data", (data) => {
71
- this.pluginLogger.info(`[${this.$config.name}] 正在提交滑块 ticket...`);
72
- this.submitSlider(data.toString().trim());
73
- });
74
- });
75
- const LOGIN_TIMEOUT = 120_000; // 2 分钟登录超时
76
- const loginMode = this.$config.password ? '密码' : '扫码';
77
- this.pluginLogger.info(`[${this.$config.name}] 正在尝试${loginMode}登录...`);
78
- return new Promise((resolve, reject) => {
79
- const timer = setTimeout(() => {
80
- cleanup();
81
- const msg = `[${this.$config.name}] 登录超时(${LOGIN_TIMEOUT / 1000}s),请检查网络或验证流程是否完成`;
82
- this.pluginLogger.error(msg);
83
- reject(new Error(msg));
84
- }, LOGIN_TIMEOUT);
85
- const cleanup = () => {
86
- clearTimeout(timer);
87
- // once 注册的监听器在触发后会自动移除,这里只清理未触发的
88
- this.off("system.online");
89
- this.off("system.login.error");
90
- this.off("system.offline");
91
- };
92
- const onOnline = () => {
93
- cleanup();
94
- this.$connected = true;
95
- this.pluginLogger.info(`[${this.$config.name}] 登录成功,已上线`);
96
- resolve();
97
- };
98
- const onError = (e) => {
99
- cleanup();
100
- const msg = `[${this.$config.name}] 登录失败 (code=${e.code}): ${e.message}`;
101
- this.pluginLogger.error(msg);
102
- reject(new Error(msg));
103
- };
104
- const onOffline = (e) => {
105
- cleanup();
106
- const msg = `[${this.$config.name}] 登录过程中掉线: ${e?.message ?? '未知原因'}`;
107
- this.pluginLogger.error(msg);
108
- reject(new Error(msg));
109
- };
110
- this.once("system.online", onOnline);
111
- this.once("system.login.error", onError);
112
- this.once("system.offline", onOffline);
113
- this.login(Number(this.$config.name), this.$config.password);
114
- });
32
+ const uin = Number(this.$config.name);
33
+ const rpc = this.$config.rpc;
34
+ if (rpc) {
35
+ this.logger.info(`[${this.$id}] 正在通过 RPC 连接 icqq 守护进程 (${rpc.host}:${rpc.port})...`);
36
+ this.ipc = await IpcClient.connectRpc(rpc);
37
+ }
38
+ else {
39
+ this.logger.info(`[${this.$id}] 正在连接 icqq 守护进程...`);
40
+ this.ipc = await IpcClient.connect(uin);
41
+ }
42
+ this.logger.info(`[${this.$id}] 已连接守护进程${rpc ? " (RPC)" : ""}`);
43
+ // 拉取好友/群列表并缓存
44
+ await this.refreshLists();
45
+ // 订阅所有好友消息
46
+ for (const [uid] of this.friends) {
47
+ const sub = this.ipc.subscribe(Actions.SUBSCRIBE, { type: "private", id: uid }, (event) => this.handleEvent(event));
48
+ this.subscriptions.push(sub);
49
+ }
50
+ // 订阅所有群消息
51
+ for (const [gid] of this.groups) {
52
+ const sub = this.ipc.subscribe(Actions.SUBSCRIBE, { type: "group", id: gid }, (event) => this.handleEvent(event));
53
+ this.subscriptions.push(sub);
54
+ }
55
+ this.$connected = true;
56
+ this.logger.info(`[${this.$id}] 登录成功,好友 ${this.friends.size},群 ${this.groups.size}`);
57
+ }
58
+ /** 刷新好友/群列表缓存 */
59
+ async refreshLists() {
60
+ const [flResp, glResp] = await Promise.all([
61
+ this.ipc.request(Actions.LIST_FRIENDS),
62
+ this.ipc.request(Actions.LIST_GROUPS),
63
+ ]);
64
+ this.friends.clear();
65
+ if (flResp.ok && Array.isArray(flResp.data)) {
66
+ for (const f of flResp.data) {
67
+ this.friends.set(f.user_id, f);
68
+ }
69
+ }
70
+ this.groups.clear();
71
+ if (glResp.ok && Array.isArray(glResp.data)) {
72
+ for (const g of glResp.data) {
73
+ this.groups.set(g.group_id, g);
74
+ }
75
+ }
115
76
  }
77
+ // ── 断开 ───────────────────────────────────────────────────────────
116
78
  async $disconnect() {
117
- // Client 继承自 EventEmitter,清除 $connect() 注册的所有监听器
118
- this.removeAllListeners();
119
- await this.logout();
79
+ for (const sub of this.subscriptions) {
80
+ await sub.unsubscribe().catch(() => { });
81
+ }
82
+ this.subscriptions = [];
83
+ this.ipc?.close();
120
84
  this.$connected = false;
85
+ this.logger.info(`[${this.$id}] 已断开连接`);
86
+ }
87
+ // ── 消息处理 ───────────────────────────────────────────────────────
88
+ handleEvent(event) {
89
+ const data = event.data;
90
+ if (!data || !data.raw_message)
91
+ return;
92
+ const message = this.$formatMessage(data);
93
+ this.adapter.emit("message.receive", message);
94
+ this.logger.debug(`${this.$id} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`);
121
95
  }
122
- $formatMessage(msg) {
123
- const senderInfo = this.getSenderInfo(msg);
124
- const result = Message.from(msg, {
125
- $id: msg.message_id.toString(),
96
+ $formatMessage(raw) {
97
+ const channelId = raw.type === "group"
98
+ ? String(raw.group_id ?? raw.from_id)
99
+ : String(raw.from_id);
100
+ const channelType = raw.type === "group" ? "group" : "private";
101
+ // 守护进程推送无 message_id,合成一个
102
+ const syntheticId = `${raw.time}_${raw.user_id}_${channelId}`;
103
+ const senderInfo = {
104
+ id: String(raw.user_id),
105
+ name: raw.nickname,
106
+ };
107
+ const result = Message.from(raw, {
108
+ $id: syntheticId,
126
109
  $adapter: "icqq",
127
- $bot: `${this.$config.name}`,
110
+ $bot: this.$config.name,
128
111
  $sender: senderInfo,
129
- $channel: {
130
- id: msg.message_type === "group"
131
- ? msg.group_id.toString()
132
- : msg.from_id.toString(),
133
- type: msg.message_type,
134
- },
135
- $content: IcqqBot.toSegments(msg.message),
136
- $raw: msg.raw_message,
137
- $timestamp: msg.time * 1000,
112
+ $channel: { id: channelId, type: channelType },
113
+ $content: IcqqBot.parseCqMessage(raw.raw_message),
114
+ $raw: raw.raw_message,
115
+ $timestamp: raw.time * 1000,
138
116
  $recall: async () => {
139
- await this.$recallMessage(result.$id);
117
+ // 合成 id 无法撤回
118
+ this.logger.warn(`[${this.$id}] 收到的消息无法撤回(守护进程推送不含 message_id)`);
140
119
  },
141
120
  $reply: async (content, quote) => {
142
121
  if (!Array.isArray(content))
143
122
  content = [content];
144
- if (quote)
123
+ if (quote) {
145
124
  content.unshift({
146
125
  type: "reply",
147
126
  data: { id: typeof quote === "boolean" ? result.$id : quote },
148
127
  });
128
+ }
149
129
  return await this.adapter.sendMessage({
150
130
  ...result.$channel,
151
131
  context: "icqq",
152
- bot: `${this.uin}`,
132
+ bot: this.$config.name,
153
133
  content,
154
134
  });
155
135
  },
156
136
  });
157
137
  return result;
158
138
  }
159
- getSenderInfo(msg) {
160
- const senderInfo = {
161
- id: msg.sender.user_id.toString(),
162
- name: msg.sender.nickname.toString(),
163
- };
164
- if (msg.message_type === "group") {
165
- const groupMsg = msg;
166
- const sender = groupMsg.sender;
167
- if (sender.role) {
168
- senderInfo.role = sender.role;
169
- senderInfo.isOwner = sender.role === "owner";
170
- senderInfo.isAdmin = sender.role === "admin" || sender.role === "owner";
171
- const perms = [];
172
- if (sender.role === "owner")
173
- perms.push("owner", "admin");
174
- else if (sender.role === "admin")
175
- perms.push("admin");
176
- senderInfo.permissions = perms;
177
- }
178
- if (sender.card)
179
- senderInfo.card = sender.card;
180
- if (sender.title)
181
- senderInfo.title = sender.title;
182
- }
183
- return senderInfo;
184
- }
185
- handleGroupNotice(event, type, subType) {
186
- const notice = Notice.from(event, {
187
- $id: `${event.time || Date.now()}_${type}_${event.group_id}`,
188
- $adapter: 'icqq',
189
- $bot: this.$config.name,
190
- $type: type,
191
- $subType: subType,
192
- $channel: { id: event.group_id?.toString() || '', type: 'group' },
193
- $operator: event.operator_id ? { id: event.operator_id.toString(), name: event.operator_id.toString() } : undefined,
194
- $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),
195
- $timestamp: event.time * 1000 || Date.now(),
196
- });
197
- this.adapter.emit('notice.receive', notice);
198
- }
199
- handleFriendNotice(event, type, subType) {
200
- const notice = Notice.from(event, {
201
- $id: `${event.time || Date.now()}_${type}_${event.user_id}`,
202
- $adapter: 'icqq',
203
- $bot: this.$config.name,
204
- $type: type,
205
- $subType: subType,
206
- $channel: { id: event.user_id?.toString() || '', type: 'private' },
207
- $operator: event.operator_id ? { id: event.operator_id.toString(), name: event.operator_id.toString() } : undefined,
208
- $target: event.user_id ? { id: event.user_id.toString(), name: event.user_id.toString() } : undefined,
209
- $timestamp: event.time * 1000 || Date.now(),
210
- });
211
- this.adapter.emit('notice.receive', notice);
212
- }
213
- handleFriendRequest(event) {
214
- const request = Request.from(event, {
215
- $id: event.flag || `${event.time}_friend_add_${event.user_id}`,
216
- $adapter: 'icqq',
217
- $bot: this.$config.name,
218
- $type: 'friend_add',
219
- $subType: event.sub_type,
220
- $channel: { id: event.user_id.toString(), type: 'private' },
221
- $sender: { id: event.user_id.toString(), name: event.nickname || event.user_id.toString() },
222
- $comment: event.comment,
223
- $timestamp: event.time * 1000 || Date.now(),
224
- $approve: async () => { await event.approve(true); },
225
- $reject: async () => { await event.approve(false); },
226
- });
227
- this.adapter.emit('request.receive', request);
228
- }
229
- handleGroupRequest(event, type) {
230
- const request = Request.from(event, {
231
- $id: event.flag || `${event.time}_${type}_${event.user_id}`,
232
- $adapter: 'icqq',
233
- $bot: this.$config.name,
234
- $type: type,
235
- $subType: event.sub_type,
236
- $channel: { id: event.group_id.toString(), type: 'group' },
237
- $sender: { id: event.user_id.toString(), name: event.nickname || event.user_id.toString() },
238
- $comment: 'comment' in event ? event.comment : undefined,
239
- $timestamp: event.time * 1000 || Date.now(),
240
- $approve: async () => { await event.approve(true); },
241
- $reject: async () => { await event.approve(false); },
139
+ // ── 撤回 ───────────────────────────────────────────────────────────
140
+ async $recallMessage(id) {
141
+ const resp = await this.ipc.request(Actions.RECALL_MSG, {
142
+ message_id: id,
242
143
  });
243
- this.adapter.emit('request.receive', request);
244
- }
245
- async kickMember(groupId, userId, block) {
246
- try {
247
- const group = this.pickGroup(groupId);
248
- const result = await group.kickMember(userId, undefined, block);
249
- this.pluginLogger.info(`ICQQ Bot ${this.$id} 踢出成员 ${userId} 从群 ${groupId}${block ? "(已拉黑)" : ""}`);
250
- return result;
251
- }
252
- catch (error) {
253
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 踢出成员失败:`, error);
254
- throw error;
255
- }
256
- }
257
- async muteMember(groupId, userId, duration = 600) {
258
- try {
259
- const group = this.pickGroup(groupId);
260
- const result = await group.muteMember(userId, duration);
261
- this.pluginLogger.info(`ICQQ Bot ${this.$id} ${duration > 0 ? `禁言成员 ${userId} ${duration}秒` : `解除成员 ${userId} 禁言`}(群 ${groupId})`);
262
- return result;
263
- }
264
- catch (error) {
265
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 禁言操作失败:`, error);
266
- throw error;
144
+ if (!resp.ok) {
145
+ this.logger.warn(`[${this.$id}] 撤回消息失败: ${resp.error}`);
267
146
  }
268
147
  }
269
- async muteAll(groupId, enable = true) {
270
- try {
271
- const group = this.pickGroup(groupId);
272
- const result = await group.muteAll(enable);
273
- this.pluginLogger.info(`ICQQ Bot ${this.$id} ${enable ? "开启" : "关闭"}全员禁言(群 ${groupId})`);
274
- return result;
275
- }
276
- catch (error) {
277
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 全员禁言操作失败:`, error);
278
- throw error;
279
- }
280
- }
281
- async setAdmin(groupId, userId, enable = true) {
282
- try {
283
- const group = this.pickGroup(groupId);
284
- const result = await group.setAdmin(userId, enable);
285
- this.pluginLogger.info(`ICQQ Bot ${this.$id} ${enable ? "设置" : "取消"}管理员 ${userId}(群 ${groupId})`);
286
- return result;
287
- }
288
- catch (error) {
289
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 设置管理员失败:`, error);
290
- throw error;
291
- }
292
- }
293
- async setCard(groupId, userId, card) {
294
- try {
295
- const group = this.pickGroup(groupId);
296
- const result = await group.setCard(userId, card);
297
- this.pluginLogger.info(`ICQQ Bot ${this.$id} 设置成员 ${userId} 群名片为 "${card}"(群 ${groupId})`);
298
- return result;
299
- }
300
- catch (error) {
301
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 设置群名片失败:`, error);
302
- throw error;
303
- }
304
- }
305
- async setTitle(groupId, userId, title, duration = -1) {
306
- try {
307
- const group = this.pickGroup(groupId);
308
- const result = await group.setTitle(userId, title, duration);
309
- this.pluginLogger.info(`ICQQ Bot ${this.$id} 设置成员 ${userId} 头衔为 "${title}"(群 ${groupId})`);
310
- return result;
311
- }
312
- catch (error) {
313
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 设置头衔失败:`, error);
314
- throw error;
315
- }
316
- }
317
- async setGroupName(groupId, name) {
318
- try {
319
- const group = this.pickGroup(groupId);
320
- const result = await group.setName(name);
321
- this.pluginLogger.info(`ICQQ Bot ${this.$id} 设置群名为 "${name}"(群 ${groupId})`);
322
- return result;
323
- }
324
- catch (error) {
325
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 设置群名失败:`, error);
326
- throw error;
327
- }
328
- }
329
- async sendAnnounce(groupId, content) {
330
- try {
331
- const group = this.pickGroup(groupId);
332
- const result = await group.announce(content);
333
- this.pluginLogger.info(`ICQQ Bot ${this.$id} 发送群公告(群 ${groupId})`);
334
- return result;
335
- }
336
- catch (error) {
337
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 发送群公告失败:`, error);
338
- throw error;
339
- }
340
- }
341
- async pokeMember(groupId, userId) {
342
- try {
343
- const group = this.pickGroup(groupId);
344
- const result = await group.pokeMember(userId);
345
- this.pluginLogger.info(`ICQQ Bot ${this.$id} 戳了戳 ${userId}(群 ${groupId})`);
346
- return result;
347
- }
348
- catch (error) {
349
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 戳一戳失败:`, error);
350
- throw error;
351
- }
352
- }
353
- async getMemberList(groupId) {
354
- try {
355
- const group = this.pickGroup(groupId);
356
- return await group.getMemberMap();
357
- }
358
- catch (error) {
359
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 获取群成员列表失败:`, error);
360
- throw error;
361
- }
362
- }
363
- async getMutedMembers(groupId) {
364
- try {
365
- const group = this.pickGroup(groupId);
366
- return await group.getMuteMemberList();
367
- }
368
- catch (error) {
369
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 获取禁言列表失败:`, error);
370
- throw error;
371
- }
372
- }
373
- async setAnonymous(groupId, enable = true) {
374
- try {
375
- const group = this.pickGroup(groupId);
376
- const result = await group.allowAnony(enable);
377
- this.pluginLogger.info(`ICQQ Bot ${this.$id} ${enable ? "开启" : "关闭"}匿名(群 ${groupId})`);
378
- return result;
379
- }
380
- catch (error) {
381
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 设置匿名失败:`, error);
382
- throw error;
383
- }
384
- }
385
- async getGroupFiles(groupId) {
386
- try {
387
- const group = this.pickGroup(groupId);
388
- return await group.fs.ls();
389
- }
390
- catch (error) {
391
- this.pluginLogger.error(`ICQQ Bot ${this.$id} 获取群文件列表失败:`, error);
392
- throw error;
393
- }
394
- }
395
- async $recallMessage(id) {
396
- await this.deleteMsg(id);
397
- }
148
+ // ── 发送消息 ───────────────────────────────────────────────────────
398
149
  async $sendMessage(options) {
150
+ const content = IcqqBot.toCqString(options.content);
151
+ let action;
152
+ let params;
399
153
  switch (options.type) {
400
- case "private": {
401
- const result = await this.sendPrivateMsg(Number(options.id), IcqqBot.toSendable(options.content));
402
- this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
403
- return result.message_id.toString();
404
- }
405
- case "group": {
406
- const result = await this.sendGroupMsg(Number(options.id), IcqqBot.toSendable(options.content));
407
- this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
408
- return result.message_id.toString();
409
- }
154
+ case "private":
155
+ action = Actions.SEND_PRIVATE_MSG;
156
+ params = { user_id: Number(options.id), message: content };
157
+ break;
158
+ case "group":
159
+ action = Actions.SEND_GROUP_MSG;
160
+ params = { group_id: Number(options.id), message: content };
161
+ break;
410
162
  default:
411
- throw new Error(`unsupported channel type ${options.type}`);
163
+ throw new Error(`不支持的频道类型: ${options.type}`);
164
+ }
165
+ const resp = await this.ipc.request(action, params);
166
+ if (!resp.ok) {
167
+ throw new Error(`发送消息失败: ${resp.error}`);
412
168
  }
169
+ const messageId = String(resp.data?.message_id ?? `sent_${Date.now()}`);
170
+ this.logger.debug(`${this.$id} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
171
+ return messageId;
413
172
  }
414
173
  }
174
+ // ── CQ 码解析工具 ──────────────────────────────────────────────────
415
175
  (function (IcqqBot) {
416
- const allowTypes = [
417
- "text", "face", "image", "record", "audio", "dice", "rps", "video",
418
- "file", "location", "share", "json", "at", "reply", "long_msg",
419
- "button", "markdown", "xml",
420
- ];
421
- function toSegments(message) {
422
- if (!Array.isArray(message))
423
- message = [message];
424
- return message
425
- .filter((item, index) => typeof item === "string" || item.type !== "long_msg" || index !== 0)
426
- .map((item) => {
427
- if (typeof item === "string")
428
- return { type: "text", data: { text: item } };
429
- const { type, ...data } = item;
430
- return { type, data };
431
- });
176
+ /**
177
+ * CQ 码原始消息字符串解析为 MessageSegment 数组。
178
+ * 格式: `[type:value]` 或纯文本
179
+ */
180
+ function parseCqMessage(raw) {
181
+ const segments = [];
182
+ // 匹配 [type:arg] 或 [type:arg1,arg2=val] 等 CQ 码
183
+ const cqRegex = /\[([a-z_]+)(?::([^\]]*))?\]/g;
184
+ let lastIndex = 0;
185
+ for (const match of raw.matchAll(cqRegex)) {
186
+ // 前面的纯文本
187
+ if (match.index > lastIndex) {
188
+ const text = raw.slice(lastIndex, match.index);
189
+ if (text)
190
+ segments.push({ type: "text", data: { text } });
191
+ }
192
+ const type = match[1];
193
+ const arg = match[2] ?? "";
194
+ switch (type) {
195
+ case "face":
196
+ segments.push({ type: "face", data: { id: Number(arg) } });
197
+ break;
198
+ case "image":
199
+ segments.push({ type: "image", data: { url: arg, file: arg } });
200
+ break;
201
+ case "at":
202
+ if (arg === "all") {
203
+ segments.push({ type: "at", data: { qq: "all" } });
204
+ }
205
+ else {
206
+ segments.push({ type: "at", data: { qq: arg } });
207
+ }
208
+ break;
209
+ case "dice":
210
+ segments.push({ type: "dice", data: {} });
211
+ break;
212
+ case "rps":
213
+ segments.push({ type: "rps", data: {} });
214
+ break;
215
+ case "record":
216
+ case "audio":
217
+ segments.push({ type: "record", data: { file: arg } });
218
+ break;
219
+ case "video":
220
+ segments.push({ type: "video", data: { file: arg } });
221
+ break;
222
+ case "reply":
223
+ segments.push({ type: "reply", data: { id: arg } });
224
+ break;
225
+ default:
226
+ segments.push({ type, data: { text: `[${type}:${arg}]` } });
227
+ break;
228
+ }
229
+ lastIndex = match.index + match[0].length;
230
+ }
231
+ // 尾部文本
232
+ if (lastIndex < raw.length) {
233
+ const text = raw.slice(lastIndex);
234
+ if (text)
235
+ segments.push({ type: "text", data: { text } });
236
+ }
237
+ return segments.length ? segments : [{ type: "text", data: { text: raw } }];
432
238
  }
433
- IcqqBot.toSegments = toSegments;
434
- })(IcqqBot || (IcqqBot = {}));
435
- (function (IcqqBot) {
436
- const allowTypes = [
437
- "text", "face", "image", "record", "audio", "dice", "rps", "video",
438
- "file", "location", "share", "json", "at", "reply", "long_msg",
439
- "button", "markdown", "xml",
440
- ];
441
- function toSendable(content) {
239
+ IcqqBot.parseCqMessage = parseCqMessage;
240
+ /**
241
+ * SendContent(MessageSegment[] 或字符串)转为 CQ 码字符串。
242
+ * 守护进程使用 CQ 码字符串格式收发消息。
243
+ */
244
+ function toCqString(content) {
442
245
  if (!Array.isArray(content))
443
246
  content = [content];
444
- return content.map((seg) => {
247
+ return content
248
+ .map((seg) => {
445
249
  if (typeof seg === "string")
446
- return { type: "text", text: seg };
447
- let { type, data } = seg;
448
- if (typeof type === "function")
449
- type = type.name;
450
- if (['image', 'video', 'audio'].includes(type))
451
- data.file = data.file || data.url || data.src;
452
- if (!allowTypes.includes(type))
453
- return { type: "text", text: segment.toString(seg) };
454
- return { type, ...data };
455
- });
250
+ return seg;
251
+ const { type, data } = seg;
252
+ switch (type) {
253
+ case "text":
254
+ return data.text ?? "";
255
+ case "face":
256
+ return `[face:${data.id}]`;
257
+ case "image":
258
+ return `[image:${data.file || data.url || data.src}]`;
259
+ case "at":
260
+ return `[at:${data.qq ?? data.id}]`;
261
+ case "dice":
262
+ return "[dice]";
263
+ case "rps":
264
+ return "[rps]";
265
+ case "record":
266
+ case "audio":
267
+ return `[record:${data.file || data.url}]`;
268
+ case "video":
269
+ return `[video:${data.file || data.url}]`;
270
+ case "reply":
271
+ return `[reply:${data.id}]`;
272
+ default:
273
+ return segment.toString(seg);
274
+ }
275
+ })
276
+ .join("");
456
277
  }
457
- IcqqBot.toSendable = toSendable;
278
+ IcqqBot.toCqString = toCqString;
458
279
  })(IcqqBot || (IcqqBot = {}));
459
280
  //# sourceMappingURL=bot.js.map