@zhin.js/adapter-icqq 2.0.6 → 2.0.8
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/CHANGELOG.md +15 -0
- package/lib/adapter.d.ts +7 -2
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +78 -33
- package/lib/adapter.js.map +1 -1
- package/lib/bot.d.ts +29 -29
- package/lib/bot.d.ts.map +1 -1
- package/lib/bot.js +226 -405
- package/lib/bot.js.map +1 -1
- package/lib/commands/index.d.ts +7 -0
- package/lib/commands/index.d.ts.map +1 -0
- package/lib/commands/index.js +30 -0
- package/lib/commands/index.js.map +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +13 -297
- package/lib/index.js.map +1 -1
- package/lib/ipc-client.d.ts +60 -0
- package/lib/ipc-client.d.ts.map +1 -0
- package/lib/ipc-client.js +272 -0
- package/lib/ipc-client.js.map +1 -0
- package/lib/protocol.d.ts +174 -0
- package/lib/protocol.d.ts.map +1 -0
- package/lib/protocol.js +162 -0
- package/lib/protocol.js.map +1 -0
- package/lib/routes.d.ts +8 -0
- package/lib/routes.d.ts.map +1 -0
- package/lib/routes.js +67 -0
- package/lib/routes.js.map +1 -0
- package/lib/tools/index.d.ts +10 -0
- package/lib/tools/index.d.ts.map +1 -0
- package/lib/tools/index.js +336 -0
- package/lib/tools/index.js.map +1 -0
- package/lib/types.d.ts +45 -7
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +5 -0
- package/lib/types.js.map +1 -1
- package/package.json +8 -10
- package/plugin.yml +1 -1
- package/skills/icqq/SKILL.md +31 -64
- package/skills/icqq/references/friends.md +54 -0
- package/skills/icqq/references/general.md +145 -0
- package/skills/icqq/references/gfs.md +49 -0
- package/skills/icqq/references/groups.md +71 -0
- package/skills/icqq/references/messaging.md +66 -0
- package/skills/icqq/references/requests.md +27 -0
- package/skills/icqq/references/settings.md +38 -0
- package/src/adapter.ts +73 -35
- package/src/bot.ts +272 -443
- package/src/commands/index.ts +32 -0
- package/src/index.ts +14 -305
- package/src/ipc-client.ts +326 -0
- package/src/protocol.ts +242 -0
- package/src/routes.ts +83 -0
- package/src/tools/index.ts +407 -0
- package/src/types.ts +47 -7
package/lib/bot.js
CHANGED
|
@@ -1,459 +1,280 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ICQQ Bot
|
|
2
|
+
* ICQQ Bot:通过 @icqqjs/cli 守护进程 IPC 通信,实现 zhin Bot 接口
|
|
3
|
+
*
|
|
4
|
+
* 不再直接依赖 @icqqjs/icqq 协议库。
|
|
5
|
+
* 登录由 `icqq login` 完成,本 Bot 只负责连接守护进程并收发消息。
|
|
3
6
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
export class IcqqBot
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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(
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
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:
|
|
110
|
+
$bot: this.$config.name,
|
|
128
111
|
$sender: senderInfo,
|
|
129
|
-
$channel: {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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:
|
|
132
|
+
bot: this.$config.name,
|
|
153
133
|
content,
|
|
154
134
|
});
|
|
155
135
|
},
|
|
156
136
|
});
|
|
157
137
|
return result;
|
|
158
138
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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(
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
247
|
+
return content
|
|
248
|
+
.map((seg) => {
|
|
445
249
|
if (typeof seg === "string")
|
|
446
|
-
return
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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.
|
|
278
|
+
IcqqBot.toCqString = toCqString;
|
|
458
279
|
})(IcqqBot || (IcqqBot = {}));
|
|
459
280
|
//# sourceMappingURL=bot.js.map
|