@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.
- package/CHANGELOG.md +6 -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 +3 -5
- 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
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC/RPC 客户端 — 与 icqq 守护进程通信 (ported from @icqqjs/cli v1.4.4)
|
|
3
|
+
*
|
|
4
|
+
* IPC 模式(本地 Unix Socket):
|
|
5
|
+
* const client = await IpcClient.connect(uin);
|
|
6
|
+
*
|
|
7
|
+
* RPC 模式(远程 TCP):
|
|
8
|
+
* const client = await IpcClient.connectRpc({ host, port, token });
|
|
9
|
+
*
|
|
10
|
+
* 通信协议:JSON + 换行符。
|
|
11
|
+
* IPC 连接使用 Token 直传认证;RPC 使用 HMAC-SHA256 挑战-响应认证。
|
|
12
|
+
*/
|
|
13
|
+
import net from "node:net";
|
|
14
|
+
import { createHmac, randomUUID } from "node:crypto";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import type {
|
|
19
|
+
IpcRequest,
|
|
20
|
+
IpcResponse,
|
|
21
|
+
IpcEvent,
|
|
22
|
+
IpcMessage,
|
|
23
|
+
} from "./protocol.js";
|
|
24
|
+
|
|
25
|
+
function getIcqqHome(): string {
|
|
26
|
+
return path.join(os.homedir(), ".icqq");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getSocketPath(uin: number): string {
|
|
30
|
+
return path.join(getIcqqHome(), String(uin), "daemon.sock");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getTokenPath(uin: number): string {
|
|
34
|
+
return path.join(getIcqqHome(), String(uin), "daemon.token");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getRpcPortPath(uin: number): string {
|
|
38
|
+
return path.join(getIcqqHome(), String(uin), "daemon.rpc");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class IpcClient {
|
|
42
|
+
private socket: net.Socket;
|
|
43
|
+
private buffer = "";
|
|
44
|
+
private pending = new Map<
|
|
45
|
+
string,
|
|
46
|
+
{ resolve: (v: IpcResponse) => void; reject: (e: Error) => void }
|
|
47
|
+
>();
|
|
48
|
+
private eventHandlers = new Map<string, (event: IpcEvent) => void>();
|
|
49
|
+
private _closed = false;
|
|
50
|
+
|
|
51
|
+
private constructor(socket: net.Socket, skipDataHandler = false) {
|
|
52
|
+
this.socket = socket;
|
|
53
|
+
if (!skipDataHandler) {
|
|
54
|
+
this.attachDataHandler();
|
|
55
|
+
}
|
|
56
|
+
this.socket.on("error", (err) => {
|
|
57
|
+
for (const { reject } of this.pending.values()) {
|
|
58
|
+
reject(err);
|
|
59
|
+
}
|
|
60
|
+
this.pending.clear();
|
|
61
|
+
});
|
|
62
|
+
this.socket.on("close", () => {
|
|
63
|
+
this._closed = true;
|
|
64
|
+
for (const { reject } of this.pending.values()) {
|
|
65
|
+
reject(new Error("连接已关闭"));
|
|
66
|
+
}
|
|
67
|
+
this.pending.clear();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get closed(): boolean {
|
|
72
|
+
return this._closed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 注册数据处理 handler(RPC 模式延迟到 challenge 完成后调用) */
|
|
76
|
+
private attachDataHandler() {
|
|
77
|
+
this.socket.on("data", (chunk) => this.onData(chunk.toString()));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 通过 IPC(Unix Socket)连接守护进程并完成认证。
|
|
82
|
+
* @param uin - 目标账号的 QQ 号
|
|
83
|
+
* @returns 已认证的 IpcClient 实例
|
|
84
|
+
* @throws 守护进程未运行或认证失败时抛出错误
|
|
85
|
+
*/
|
|
86
|
+
static async connect(uin: number): Promise<IpcClient> {
|
|
87
|
+
let token: string;
|
|
88
|
+
try {
|
|
89
|
+
token = (await readFile(getTokenPath(uin), "utf-8")).trim();
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`无法读取认证 token,icqq 守护进程可能未运行。请先执行: icqq login`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const client = await new Promise<IpcClient>((resolve, reject) => {
|
|
97
|
+
const sock = net.connect(getSocketPath(uin));
|
|
98
|
+
sock.on("connect", () => resolve(new IpcClient(sock)));
|
|
99
|
+
sock.on("error", (err) =>
|
|
100
|
+
reject(
|
|
101
|
+
new Error(
|
|
102
|
+
`无法连接 icqq 守护进程 (${getSocketPath(uin)}): ${err.message}。请先执行: icqq login`,
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const authResp = await client.request("auth", { token });
|
|
109
|
+
if (!authResp.ok) {
|
|
110
|
+
client.close();
|
|
111
|
+
throw new Error("IPC 认证失败");
|
|
112
|
+
}
|
|
113
|
+
return client;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 通过 RPC(TCP)连接守护进程并完成 HMAC 挑战-响应认证。
|
|
118
|
+
*
|
|
119
|
+
* 流程:
|
|
120
|
+
* 1. 服务端连接后发送 { challenge: "<hex>" }
|
|
121
|
+
* 2. 客户端用 HMAC-SHA256(token, challenge) 生成 digest
|
|
122
|
+
* 3. 客户端发送 { action: "auth", params: { digest: "<hex>" } }
|
|
123
|
+
* 4. 服务端验证 digest,通过则认证成功
|
|
124
|
+
*
|
|
125
|
+
* token 不会明文传输,防止中间人嗅探。
|
|
126
|
+
*
|
|
127
|
+
* @param options.host - 远程主机地址
|
|
128
|
+
* @param options.port - 远程端口
|
|
129
|
+
* @param options.token - 认证 token(用于 HMAC 计算,不会明文传输)
|
|
130
|
+
*/
|
|
131
|
+
static async connectRpc(options: {
|
|
132
|
+
host: string;
|
|
133
|
+
port: number;
|
|
134
|
+
token: string;
|
|
135
|
+
}): Promise<IpcClient> {
|
|
136
|
+
const { host, port, token } = options;
|
|
137
|
+
|
|
138
|
+
const client = await new Promise<IpcClient>((resolve, reject) => {
|
|
139
|
+
const sock = net.connect(port, host);
|
|
140
|
+
// skipDataHandler=true: 延迟注册 onData,避免与 challenge 读取冲突
|
|
141
|
+
sock.on("connect", () => resolve(new IpcClient(sock, true)));
|
|
142
|
+
sock.on("error", (err) =>
|
|
143
|
+
reject(
|
|
144
|
+
new Error(`无法连接 icqq RPC 服务 (${host}:${port}): ${err.message}`),
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Wait for challenge from server, with proper buffering for TCP fragmentation
|
|
150
|
+
const challenge = await new Promise<string>((resolve, reject) => {
|
|
151
|
+
let challengeBuffer = "";
|
|
152
|
+
const timeout = setTimeout(() => {
|
|
153
|
+
client.socket.removeListener("data", onData);
|
|
154
|
+
client.close();
|
|
155
|
+
reject(new Error("RPC 挑战超时"));
|
|
156
|
+
}, 10000);
|
|
157
|
+
|
|
158
|
+
const onData = (chunk: Buffer) => {
|
|
159
|
+
challengeBuffer += chunk.toString();
|
|
160
|
+
const nlIdx = challengeBuffer.indexOf("\n");
|
|
161
|
+
if (nlIdx === -1) return; // 等待更多数据
|
|
162
|
+
|
|
163
|
+
clearTimeout(timeout);
|
|
164
|
+
client.socket.removeListener("data", onData);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const msg = JSON.parse(challengeBuffer.slice(0, nlIdx)) as {
|
|
168
|
+
challenge?: string;
|
|
169
|
+
};
|
|
170
|
+
if (!msg.challenge) {
|
|
171
|
+
reject(new Error("RPC 服务端未发送挑战"));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
resolve(msg.challenge);
|
|
175
|
+
} catch {
|
|
176
|
+
reject(new Error("RPC 挑战解析失败"));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
client.socket.on("data", onData);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Challenge 阶段结束,注册正式的数据处理 handler
|
|
183
|
+
client.attachDataHandler();
|
|
184
|
+
|
|
185
|
+
// Compute HMAC digest and authenticate
|
|
186
|
+
const digest = createHmac("sha256", token)
|
|
187
|
+
.update(challenge)
|
|
188
|
+
.digest("hex");
|
|
189
|
+
const authResp = await client.request("auth", { digest });
|
|
190
|
+
if (!authResp.ok) {
|
|
191
|
+
client.close();
|
|
192
|
+
throw new Error(authResp.error ?? "RPC 认证失败");
|
|
193
|
+
}
|
|
194
|
+
return client;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 通过 RPC 连接守护进程(自动从 daemon.rpc 文件读取地址)。
|
|
199
|
+
* @param uin - 目标账号的 QQ 号
|
|
200
|
+
*/
|
|
201
|
+
static async connectRpcByUin(uin: number): Promise<IpcClient> {
|
|
202
|
+
let token: string;
|
|
203
|
+
try {
|
|
204
|
+
token = (await readFile(getTokenPath(uin), "utf-8")).trim();
|
|
205
|
+
} catch {
|
|
206
|
+
throw new Error("无法读取认证 token,守护进程可能未运行");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let rpcInfo: { host: string; port: number };
|
|
210
|
+
try {
|
|
211
|
+
const raw = await readFile(getRpcPortPath(uin), "utf-8");
|
|
212
|
+
rpcInfo = JSON.parse(raw);
|
|
213
|
+
} catch {
|
|
214
|
+
throw new Error("RPC 未启用或守护进程未运行");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return IpcClient.connectRpc({
|
|
218
|
+
host: rpcInfo.host,
|
|
219
|
+
port: rpcInfo.port,
|
|
220
|
+
token,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private onData(data: string) {
|
|
225
|
+
this.buffer += data;
|
|
226
|
+
const lines = this.buffer.split("\n");
|
|
227
|
+
this.buffer = lines.pop()!;
|
|
228
|
+
|
|
229
|
+
for (const line of lines) {
|
|
230
|
+
if (!line.trim()) continue;
|
|
231
|
+
try {
|
|
232
|
+
const msg = JSON.parse(line) as IpcMessage;
|
|
233
|
+
if ("event" in msg) {
|
|
234
|
+
const handler = this.eventHandlers.get(msg.id);
|
|
235
|
+
handler?.(msg as IpcEvent);
|
|
236
|
+
} else {
|
|
237
|
+
const p = this.pending.get(msg.id);
|
|
238
|
+
if (p) {
|
|
239
|
+
this.pending.delete(msg.id);
|
|
240
|
+
p.resolve(msg as IpcResponse);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
// ignore malformed JSON
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 发送 IPC 请求并等待响应。
|
|
251
|
+
*/
|
|
252
|
+
async request(
|
|
253
|
+
action: string,
|
|
254
|
+
params: Record<string, unknown> = {},
|
|
255
|
+
timeoutMs = 30000,
|
|
256
|
+
): Promise<IpcResponse> {
|
|
257
|
+
if (this._closed) throw new Error("IPC 连接已关闭");
|
|
258
|
+
const id = randomUUID();
|
|
259
|
+
const req: IpcRequest = { id, action, params };
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
const timer = setTimeout(() => {
|
|
262
|
+
this.pending.delete(id);
|
|
263
|
+
reject(new Error(`IPC 请求超时 (${timeoutMs}ms): ${action}`));
|
|
264
|
+
}, timeoutMs);
|
|
265
|
+
|
|
266
|
+
this.pending.set(id, {
|
|
267
|
+
resolve: (v) => {
|
|
268
|
+
clearTimeout(timer);
|
|
269
|
+
resolve(v);
|
|
270
|
+
},
|
|
271
|
+
reject: (e) => {
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
reject(e);
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
this.socket.write(JSON.stringify(req) + "\n", (err) => {
|
|
277
|
+
if (err) {
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
this.pending.delete(id);
|
|
280
|
+
reject(err);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 订阅消息推送。
|
|
288
|
+
* @returns 订阅句柄,包含 unsubscribe() 方法
|
|
289
|
+
*/
|
|
290
|
+
subscribe(
|
|
291
|
+
action: string,
|
|
292
|
+
params: Record<string, unknown>,
|
|
293
|
+
onEvent: (event: IpcEvent) => void,
|
|
294
|
+
): { id: string; unsubscribe: () => Promise<void> } {
|
|
295
|
+
const id = randomUUID();
|
|
296
|
+
this.eventHandlers.set(id, onEvent);
|
|
297
|
+
const req: IpcRequest = { id, action, params };
|
|
298
|
+
this.socket.write(JSON.stringify(req) + "\n");
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
id,
|
|
302
|
+
unsubscribe: async () => {
|
|
303
|
+
this.eventHandlers.delete(id);
|
|
304
|
+
if (!this._closed) {
|
|
305
|
+
const unsub: IpcRequest = {
|
|
306
|
+
id: randomUUID(),
|
|
307
|
+
action: "unsubscribe",
|
|
308
|
+
params,
|
|
309
|
+
};
|
|
310
|
+
this.socket.write(JSON.stringify(unsub) + "\n");
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** 关闭连接 */
|
|
317
|
+
close() {
|
|
318
|
+
this._closed = true;
|
|
319
|
+
this.eventHandlers.clear();
|
|
320
|
+
for (const { reject } of this.pending.values()) {
|
|
321
|
+
reject(new Error("连接已关闭"));
|
|
322
|
+
}
|
|
323
|
+
this.pending.clear();
|
|
324
|
+
this.socket.destroy();
|
|
325
|
+
}
|
|
326
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC 协议类型定义与 Action 常量 (ported from @icqqjs/cli)
|
|
3
|
+
*
|
|
4
|
+
* CLI 与守护进程通过 Unix Domain Socket 通信,
|
|
5
|
+
* 使用 JSON + 换行符(\n)分隔的文本协议。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** CLI → Daemon 请求 */
|
|
9
|
+
export type IpcRequest = {
|
|
10
|
+
id: string;
|
|
11
|
+
action: string;
|
|
12
|
+
params: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Daemon → CLI 响应 */
|
|
16
|
+
export type IpcResponse = {
|
|
17
|
+
id: string;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
data?: unknown;
|
|
20
|
+
error?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Daemon → CLI 事件推送 */
|
|
24
|
+
export type IpcEvent = {
|
|
25
|
+
id: string;
|
|
26
|
+
event: string;
|
|
27
|
+
data: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type IpcMessage = IpcResponse | IpcEvent;
|
|
31
|
+
|
|
32
|
+
/** 收到的消息事件数据 */
|
|
33
|
+
export interface IpcMessageEventData {
|
|
34
|
+
type: "group" | "private";
|
|
35
|
+
from_id: number;
|
|
36
|
+
user_id: number;
|
|
37
|
+
nickname: string;
|
|
38
|
+
raw_message: string;
|
|
39
|
+
time: number;
|
|
40
|
+
group_id?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Guild 消息事件数据 */
|
|
44
|
+
export interface IpcGuildMessageEventData {
|
|
45
|
+
type: "guild";
|
|
46
|
+
guild_id: string;
|
|
47
|
+
guild_name: string;
|
|
48
|
+
channel_id: string;
|
|
49
|
+
channel_name: string;
|
|
50
|
+
nickname: string;
|
|
51
|
+
tiny_id: string;
|
|
52
|
+
raw_message: string;
|
|
53
|
+
time: number;
|
|
54
|
+
seq: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 所有 IPC 操作常量 (from @icqqjs/cli)
|
|
59
|
+
*/
|
|
60
|
+
export const Actions = {
|
|
61
|
+
PING: "ping",
|
|
62
|
+
|
|
63
|
+
// ── 列表查询 ──
|
|
64
|
+
LIST_FRIENDS: "list_friends",
|
|
65
|
+
LIST_GROUPS: "list_groups",
|
|
66
|
+
LIST_GROUP_MEMBERS: "list_group_members",
|
|
67
|
+
LIST_BLACKLIST: "list_blacklist",
|
|
68
|
+
LIST_FRIEND_CLASSES: "list_friend_classes",
|
|
69
|
+
|
|
70
|
+
// ── 信息查询 ──
|
|
71
|
+
GET_FRIEND_INFO: "get_friend_info",
|
|
72
|
+
GET_GROUP_INFO: "get_group_info",
|
|
73
|
+
GET_GROUP_MEMBER_INFO: "get_group_member_info",
|
|
74
|
+
GET_STRANGER_INFO: "get_stranger_info",
|
|
75
|
+
GET_STATUS: "get_status",
|
|
76
|
+
GET_SELF_PROFILE: "get_self_profile",
|
|
77
|
+
|
|
78
|
+
// ── 消息发送 ──
|
|
79
|
+
SEND_PRIVATE_MSG: "send_private_msg",
|
|
80
|
+
SEND_GROUP_MSG: "send_group_msg",
|
|
81
|
+
|
|
82
|
+
// ── 消息操作 ──
|
|
83
|
+
RECALL_MSG: "recall_msg",
|
|
84
|
+
GET_MSG: "get_msg",
|
|
85
|
+
HISTORY_PRIVATE: "history_private",
|
|
86
|
+
HISTORY_GROUP: "history_group",
|
|
87
|
+
MARK_READ: "mark_read",
|
|
88
|
+
DELETE_MSG: "delete_msg",
|
|
89
|
+
|
|
90
|
+
// ── 个人设置 ──
|
|
91
|
+
SET_NICKNAME: "set_nickname",
|
|
92
|
+
SET_GENDER: "set_gender",
|
|
93
|
+
SET_BIRTHDAY: "set_birthday",
|
|
94
|
+
SET_SIGNATURE: "set_signature",
|
|
95
|
+
SET_DESCRIPTION: "set_description",
|
|
96
|
+
SET_AVATAR: "set_avatar",
|
|
97
|
+
SET_ONLINE_STATUS: "set_online_status",
|
|
98
|
+
|
|
99
|
+
// ── 群设置 ──
|
|
100
|
+
SET_GROUP_NAME: "set_group_name",
|
|
101
|
+
SET_GROUP_AVATAR: "set_group_avatar",
|
|
102
|
+
SET_GROUP_CARD: "set_group_card",
|
|
103
|
+
SET_GROUP_TITLE: "set_group_title",
|
|
104
|
+
SET_GROUP_ADMIN: "set_group_admin",
|
|
105
|
+
SET_GROUP_REMARK: "set_group_remark",
|
|
106
|
+
|
|
107
|
+
// ── 群管理 ──
|
|
108
|
+
GROUP_MUTE: "group_mute",
|
|
109
|
+
GROUP_MUTE_ALL: "group_mute_all",
|
|
110
|
+
GROUP_KICK: "group_kick",
|
|
111
|
+
GROUP_QUIT: "group_quit",
|
|
112
|
+
GROUP_INVITE: "group_invite",
|
|
113
|
+
GROUP_POKE: "group_poke",
|
|
114
|
+
GROUP_ANNOUNCE: "group_announce",
|
|
115
|
+
GROUP_SIGN: "group_sign",
|
|
116
|
+
GROUP_ESSENCE_ADD: "group_essence_add",
|
|
117
|
+
GROUP_ESSENCE_REMOVE: "group_essence_remove",
|
|
118
|
+
GROUP_ALLOW_ANONY: "group_allow_anony",
|
|
119
|
+
GROUP_MUTED_LIST: "group_muted_list",
|
|
120
|
+
GROUP_AT_ALL_REMAIN: "group_at_all_remain",
|
|
121
|
+
|
|
122
|
+
// ── 好友操作 ──
|
|
123
|
+
FRIEND_POKE: "friend_poke",
|
|
124
|
+
FRIEND_LIKE: "friend_like",
|
|
125
|
+
FRIEND_DELETE: "friend_delete",
|
|
126
|
+
FRIEND_REMARK: "friend_remark",
|
|
127
|
+
FRIEND_CLASS: "friend_class",
|
|
128
|
+
|
|
129
|
+
// ── 系统消息/请求 ──
|
|
130
|
+
GET_SYSTEM_MSG: "get_system_msg",
|
|
131
|
+
HANDLE_FRIEND_REQUEST: "handle_friend_request",
|
|
132
|
+
HANDLE_GROUP_REQUEST: "handle_group_request",
|
|
133
|
+
|
|
134
|
+
// ── 好友分组 ──
|
|
135
|
+
ADD_FRIEND_CLASS: "add_friend_class",
|
|
136
|
+
DELETE_FRIEND_CLASS: "delete_friend_class",
|
|
137
|
+
RENAME_FRIEND_CLASS: "rename_friend_class",
|
|
138
|
+
|
|
139
|
+
// ── 群文件系统 ──
|
|
140
|
+
GFS_LIST: "gfs_list",
|
|
141
|
+
GFS_INFO: "gfs_info",
|
|
142
|
+
GFS_MKDIR: "gfs_mkdir",
|
|
143
|
+
GFS_DELETE: "gfs_delete",
|
|
144
|
+
GFS_RENAME: "gfs_rename",
|
|
145
|
+
GFS_STAT: "gfs_stat",
|
|
146
|
+
GFS_MOVE: "gfs_move",
|
|
147
|
+
GFS_DOWNLOAD: "gfs_download",
|
|
148
|
+
|
|
149
|
+
// ── 其他功能 ──
|
|
150
|
+
IMAGE_OCR: "image_ocr",
|
|
151
|
+
RELOAD_FRIEND_LIST: "reload_friend_list",
|
|
152
|
+
RELOAD_GROUP_LIST: "reload_group_list",
|
|
153
|
+
CLEAN_CACHE: "clean_cache",
|
|
154
|
+
GET_GROUP_SHARE: "get_group_share",
|
|
155
|
+
|
|
156
|
+
// ── 群管理扩展 ──
|
|
157
|
+
GROUP_SET_JOIN_TYPE: "group_set_join_type",
|
|
158
|
+
GROUP_SET_RATE_LIMIT: "group_set_rate_limit",
|
|
159
|
+
GROUP_MUTE_ANONY: "group_mute_anony",
|
|
160
|
+
GROUP_ANON_INFO: "group_anon_info",
|
|
161
|
+
|
|
162
|
+
// ── 好友操作扩展 ──
|
|
163
|
+
ADD_FRIEND: "add_friend",
|
|
164
|
+
SEND_TEMP_MSG: "send_temp_msg",
|
|
165
|
+
|
|
166
|
+
// ── 漫游表情 ──
|
|
167
|
+
GET_ROAMING_STAMP: "get_roaming_stamp",
|
|
168
|
+
DELETE_STAMP: "delete_stamp",
|
|
169
|
+
|
|
170
|
+
// ── 文件传输 ──
|
|
171
|
+
SEND_PRIVATE_FILE: "send_private_file",
|
|
172
|
+
SEND_GROUP_FILE: "send_group_file",
|
|
173
|
+
FRIEND_RECALL_FILE: "friend_recall_file",
|
|
174
|
+
GFS_UPLOAD: "gfs_upload",
|
|
175
|
+
|
|
176
|
+
// ── 群消息表态 ──
|
|
177
|
+
GROUP_SET_REACTION: "group_set_reaction",
|
|
178
|
+
GROUP_DEL_REACTION: "group_del_reaction",
|
|
179
|
+
|
|
180
|
+
// ── 转发消息 ──
|
|
181
|
+
GET_FORWARD_MSG: "get_forward_msg",
|
|
182
|
+
MAKE_FORWARD_MSG: "make_forward_msg",
|
|
183
|
+
|
|
184
|
+
// ── 频道系统 ──
|
|
185
|
+
GUILD_LIST: "guild_list",
|
|
186
|
+
GUILD_INFO: "guild_info",
|
|
187
|
+
GUILD_CHANNELS: "guild_channels",
|
|
188
|
+
GUILD_MEMBERS: "guild_members",
|
|
189
|
+
GUILD_SEND_MSG: "guild_send_msg",
|
|
190
|
+
GUILD_RECALL_MSG: "guild_recall_msg",
|
|
191
|
+
|
|
192
|
+
// ── 用户文件操作 ──
|
|
193
|
+
GET_FILE_INFO: "get_file_info",
|
|
194
|
+
GET_FILE_URL: "get_file_url",
|
|
195
|
+
GET_AVATAR_URL: "get_avatar_url",
|
|
196
|
+
GET_GROUP_AVATAR_URL: "get_group_avatar_url",
|
|
197
|
+
|
|
198
|
+
// ── 屏蔽群成员消息 ──
|
|
199
|
+
SET_SCREEN_MEMBER_MSG: "set_screen_member_msg",
|
|
200
|
+
|
|
201
|
+
// ── 群文件转发 ──
|
|
202
|
+
GFS_FORWARD: "gfs_forward",
|
|
203
|
+
GFS_FORWARD_OFFLINE: "gfs_forward_offline",
|
|
204
|
+
|
|
205
|
+
// ── 重载列表 ──
|
|
206
|
+
RELOAD_BLACKLIST: "reload_blacklist",
|
|
207
|
+
RELOAD_STRANGER_LIST: "reload_stranger_list",
|
|
208
|
+
RELOAD_GUILDS: "reload_guilds",
|
|
209
|
+
|
|
210
|
+
// ── 在线状态查询 ──
|
|
211
|
+
GET_STATUS_INFO: "get_status_info",
|
|
212
|
+
|
|
213
|
+
// ── 密钥/工具 ──
|
|
214
|
+
GET_CLIENT_KEY: "get_client_key",
|
|
215
|
+
GET_PSKEY: "get_pskey",
|
|
216
|
+
UID2UIN: "uid2uin",
|
|
217
|
+
UIN2UID: "uin2uid",
|
|
218
|
+
|
|
219
|
+
// ── 视频/加好友设置 ──
|
|
220
|
+
GET_VIDEO_URL: "get_video_url",
|
|
221
|
+
GET_ADD_FRIEND_SETTING: "get_add_friend_setting",
|
|
222
|
+
|
|
223
|
+
// ── 频道扩展 ──
|
|
224
|
+
GET_FORUM_URL: "get_forum_url",
|
|
225
|
+
GUILD_CHANNEL_SHARE: "guild_channel_share",
|
|
226
|
+
|
|
227
|
+
// ── 获取图片/语音 URL ──
|
|
228
|
+
GET_PIC_URL: "get_pic_url",
|
|
229
|
+
GET_PTT_URL: "get_ptt_url",
|
|
230
|
+
|
|
231
|
+
// ── 消息订阅 ──
|
|
232
|
+
SUBSCRIBE: "subscribe",
|
|
233
|
+
UNSUBSCRIBE: "unsubscribe",
|
|
234
|
+
|
|
235
|
+
// ── Webhook ──
|
|
236
|
+
SET_WEBHOOK: "set_webhook",
|
|
237
|
+
GET_WEBHOOK: "get_webhook",
|
|
238
|
+
|
|
239
|
+
// ── 系统通知 ──
|
|
240
|
+
SET_NOTIFY: "set_notify",
|
|
241
|
+
GET_NOTIFY: "get_notify",
|
|
242
|
+
} as const;
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICQQ HTTP 路由注册 — 通过 IPC 查询守护进程数据
|
|
3
|
+
*/
|
|
4
|
+
import type { Router } from "@zhin.js/http";
|
|
5
|
+
import type { Plugin } from "zhin.js";
|
|
6
|
+
import type { IcqqAdapter } from "./adapter.js";
|
|
7
|
+
import { Actions } from "./protocol.js";
|
|
8
|
+
|
|
9
|
+
export function registerRoutes(
|
|
10
|
+
router: Router,
|
|
11
|
+
icqq: IcqqAdapter,
|
|
12
|
+
_root: Plugin,
|
|
13
|
+
) {
|
|
14
|
+
router.get("/api/icqq/bots", async (ctx) => {
|
|
15
|
+
try {
|
|
16
|
+
const bots = Array.from(icqq.bots.values());
|
|
17
|
+
if (bots.length === 0) {
|
|
18
|
+
ctx.body = { success: true, data: [], message: "暂无ICQQ机器人实例" };
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const result = await Promise.all(
|
|
22
|
+
bots.map(async (bot) => {
|
|
23
|
+
try {
|
|
24
|
+
const base: Record<string, unknown> = {
|
|
25
|
+
name: bot.$config.name,
|
|
26
|
+
connected: bot.$connected || false,
|
|
27
|
+
groupCount: bot.groups.size,
|
|
28
|
+
friendCount: bot.friends.size,
|
|
29
|
+
status: bot.$connected ? "online" : "offline",
|
|
30
|
+
lastActivity: new Date().toISOString(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// 尝试从守护进程获取详细状态
|
|
34
|
+
if (bot.$connected && bot.ipc && !bot.ipc.closed) {
|
|
35
|
+
try {
|
|
36
|
+
const statusResp = await bot.ipc.request(
|
|
37
|
+
Actions.GET_STATUS,
|
|
38
|
+
{},
|
|
39
|
+
5000,
|
|
40
|
+
);
|
|
41
|
+
if (statusResp.ok && statusResp.data) {
|
|
42
|
+
const stat = statusResp.data as Record<string, unknown>;
|
|
43
|
+
base.receiveCount = stat.recv_msg_cnt ?? 0;
|
|
44
|
+
base.sendCount = stat.sent_msg_cnt ?? 0;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// status 查询超时,忽略
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return base;
|
|
52
|
+
} catch {
|
|
53
|
+
return {
|
|
54
|
+
name: bot.$config.name,
|
|
55
|
+
connected: false,
|
|
56
|
+
groupCount: 0,
|
|
57
|
+
friendCount: 0,
|
|
58
|
+
status: "error",
|
|
59
|
+
error: "数据获取失败",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
ctx.body = {
|
|
65
|
+
success: true,
|
|
66
|
+
data: result,
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
ctx.status = 500;
|
|
71
|
+
ctx.body = {
|
|
72
|
+
success: false,
|
|
73
|
+
error: "ICQQ_API_ERROR",
|
|
74
|
+
message: "获取机器人数据失败",
|
|
75
|
+
details:
|
|
76
|
+
process.env.NODE_ENV === "development"
|
|
77
|
+
? (error as Error).message
|
|
78
|
+
: undefined,
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|