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