@zhin.js/adapter-icqq 8.0.9 → 8.0.10

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 (87) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +36 -28
  3. package/adapters/icqq.js +4 -9
  4. package/adapters/icqq.ts +5 -14
  5. package/agent/PERMITS.md +2 -0
  6. package/lib/agent-prompt.js +17 -22
  7. package/lib/cq-message.d.ts +2 -2
  8. package/lib/cq-message.js +2 -2
  9. package/lib/endpoint.d.ts +4 -74
  10. package/lib/endpoint.js +348 -507
  11. package/lib/forward-msg.d.ts +7 -8
  12. package/lib/forward-msg.js +11 -24
  13. package/lib/icqq-agent-deps.d.ts +3 -15
  14. package/lib/icqq-agent-deps.js +0 -5
  15. package/lib/icqq-endpoint-commands.js +4 -13
  16. package/lib/icqq-guild.d.ts +15 -11
  17. package/lib/icqq-guild.js +9 -17
  18. package/lib/icqq-inbound.d.ts +20 -20
  19. package/lib/icqq-inbound.js +9 -9
  20. package/lib/icqq-inbox.d.ts +2 -10
  21. package/lib/index.d.ts +2 -3
  22. package/lib/index.js +2 -3
  23. package/lib/outbound-media.d.ts +3 -8
  24. package/lib/outbound-media.js +3 -8
  25. package/lib/protocol.d.ts +38 -57
  26. package/lib/protocol.js +24 -21
  27. package/lib/types.d.ts +4 -40
  28. package/lib/types.js +0 -5
  29. package/package.json +25 -13
  30. package/plugin.js +6 -1
  31. package/schema.json +119 -61
  32. package/src/agent-prompt.ts +17 -22
  33. package/src/cq-message.ts +2 -2
  34. package/src/endpoint.ts +358 -600
  35. package/src/forward-msg.ts +12 -26
  36. package/src/icqq-agent-deps.ts +3 -17
  37. package/src/icqq-endpoint-commands.ts +4 -13
  38. package/src/icqq-guild.ts +16 -25
  39. package/src/icqq-inbound.ts +26 -26
  40. package/src/icqq-inbox.ts +2 -10
  41. package/src/icqqjs-icqq.d.ts +88 -0
  42. package/src/index.ts +2 -9
  43. package/src/outbound-media.ts +4 -9
  44. package/src/protocol.ts +60 -66
  45. package/src/types.ts +4 -41
  46. package/tools/announce.js +19 -0
  47. package/tools/announce.ts +18 -0
  48. package/tools/essence.js +24 -0
  49. package/{agent/tools → tools}/essence.ts +8 -8
  50. package/tools/friend_list.js +20 -0
  51. package/{agent/tools → tools}/friend_list.ts +7 -7
  52. package/tools/group_files.js +25 -0
  53. package/tools/group_files.ts +23 -0
  54. package/tools/group_invite.js +25 -0
  55. package/tools/group_invite.ts +22 -0
  56. package/tools/group_kick.js +28 -0
  57. package/tools/group_kick.ts +24 -0
  58. package/tools/group_list.js +21 -0
  59. package/{agent/tools → tools}/group_list.ts +7 -7
  60. package/tools/group_sign.js +21 -0
  61. package/tools/group_sign.ts +19 -0
  62. package/tools/list_muted.js +20 -0
  63. package/tools/list_muted.ts +19 -0
  64. package/tools/poke.js +19 -0
  65. package/tools/poke.ts +18 -0
  66. package/tools/send_user_like.js +28 -0
  67. package/tools/send_user_like.ts +25 -0
  68. package/tools/set_anonymous.js +23 -0
  69. package/tools/set_anonymous.ts +21 -0
  70. package/tools/set_mute.js +26 -0
  71. package/tools/set_mute.ts +23 -0
  72. package/tools/set_title.js +27 -0
  73. package/tools/set_title.ts +24 -0
  74. package/tools/user_info.js +17 -0
  75. package/tools/user_info.ts +16 -0
  76. package/agent/tools/announce.ts +0 -21
  77. package/agent/tools/group_files.ts +0 -27
  78. package/agent/tools/group_invite.ts +0 -20
  79. package/agent/tools/group_sign.ts +0 -19
  80. package/agent/tools/list_muted.ts +0 -22
  81. package/agent/tools/poke.ts +0 -20
  82. package/agent/tools/send_user_like.ts +0 -22
  83. package/agent/tools/set_anonymous.ts +0 -22
  84. package/agent/tools/set_title.ts +0 -25
  85. package/lib/ipc-client.d.ts +0 -73
  86. package/lib/ipc-client.js +0 -358
  87. package/src/ipc-client.ts +0 -417
package/src/ipc-client.ts DELETED
@@ -1,417 +0,0 @@
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
- function asRecord(value: unknown): Record<string, unknown> | null {
42
- return value && typeof value === "object"
43
- ? (value as Record<string, unknown>)
44
- : null;
45
- }
46
-
47
- function isChatMessageEvent(
48
- event: IpcEvent,
49
- type: "private" | "group",
50
- id: number,
51
- ): boolean {
52
- if (!event.event.startsWith("message.")) return false;
53
- const data = asRecord(event.data);
54
- if (!data) return false;
55
-
56
- const msgType = String(data.message_type ?? "");
57
- if (msgType !== type) return false;
58
-
59
- if (type === "private") {
60
- const from = Number(data.from_id ?? data.user_id);
61
- return Number.isFinite(from) && from === id;
62
- }
63
-
64
- const groupId = Number(data.group_id);
65
- return Number.isFinite(groupId) && groupId === id;
66
- }
67
-
68
- function isGuildChannelMessageEvent(event: IpcEvent, channelId: string): boolean {
69
- if (!event.event.startsWith("message.guild")) return false;
70
- const data = asRecord(event.data);
71
- if (!data) return false;
72
- return String(data.channel_id ?? "") === channelId;
73
- }
74
-
75
- export class IpcClient {
76
- private socket: net.Socket;
77
- private buffer = "";
78
- private pending = new Map<
79
- string,
80
- { resolve: (v: IpcResponse) => void; reject: (e: Error) => void }
81
- >();
82
- private eventHandlers = new Map<string, (event: IpcEvent) => void>();
83
- private _closed = false;
84
- /** 是否为客户端主动 close()(与对端断线区分) */
85
- private _userClosed = false;
86
- private _remoteDisconnectEmitted = false;
87
- private onRemoteDisconnect: (() => void) | null = null;
88
-
89
- private constructor(socket: net.Socket, skipDataHandler = false) {
90
- this.socket = socket;
91
- if (!skipDataHandler) {
92
- this.attachDataHandler();
93
- }
94
- this.socket.on("error", (err) => {
95
- for (const { reject } of this.pending.values()) {
96
- reject(err);
97
- }
98
- this.pending.clear();
99
- });
100
- this.socket.on("close", () => {
101
- this._closed = true;
102
- for (const { reject } of this.pending.values()) {
103
- reject(new Error("连接已关闭"));
104
- }
105
- this.pending.clear();
106
- const unexpected = !this._userClosed;
107
- if (
108
- unexpected &&
109
- this.onRemoteDisconnect &&
110
- !this._remoteDisconnectEmitted
111
- ) {
112
- this._remoteDisconnectEmitted = true;
113
- const fn = this.onRemoteDisconnect;
114
- this.onRemoteDisconnect = null;
115
- queueMicrotask(() => {
116
- try {
117
- fn();
118
- } catch {
119
- /* 忽略上层重连回调异常 */
120
- }
121
- });
122
- }
123
- });
124
- }
125
-
126
- get closed(): boolean {
127
- return this._closed;
128
- }
129
-
130
- /**
131
- * 注册「对端断开 / 连接异常关闭」回调(不含本端 close())。
132
- * 传入 null 可清除回调。
133
- */
134
- setOnRemoteDisconnect(handler: (() => void) | null): void {
135
- this.onRemoteDisconnect = handler;
136
- }
137
-
138
- /** 注册数据处理 handler(RPC 模式延迟到 challenge 完成后调用) */
139
- private attachDataHandler() {
140
- this.socket.on("data", (chunk) => this.onData(chunk.toString()));
141
- }
142
-
143
- /**
144
- * 通过 IPC(Unix Socket)连接守护进程并完成认证。
145
- * @param uin - 目标账号的 QQ 号
146
- * @returns 已认证的 IpcClient 实例
147
- * @throws 守护进程未运行或认证失败时抛出错误
148
- */
149
- static async connect(uin: number): Promise<IpcClient> {
150
- let token: string;
151
- try {
152
- token = (await readFile(getTokenPath(uin), "utf-8")).trim();
153
- } catch {
154
- throw new Error(
155
- `无法读取认证 token,icqq 守护进程可能未运行。请先执行: icqq login`,
156
- );
157
- }
158
-
159
- const client = await new Promise<IpcClient>((resolve, reject) => {
160
- const sock = net.connect(getSocketPath(uin));
161
- sock.on("connect", () => resolve(new IpcClient(sock)));
162
- sock.on("error", (err) =>
163
- reject(
164
- new Error(
165
- `无法连接 icqq 守护进程 (${getSocketPath(uin)}): ${err instanceof Error ? err.message : String(err)}。请先执行: icqq login`,
166
- ),
167
- ),
168
- );
169
- });
170
-
171
- const authResp = await client.request("auth", { token });
172
- if (!authResp.ok) {
173
- client.close();
174
- throw new Error("IPC 认证失败");
175
- }
176
- return client;
177
- }
178
-
179
- /**
180
- * 通过 RPC(TCP)连接守护进程并完成 HMAC 挑战-响应认证。
181
- *
182
- * 流程:
183
- * 1. 服务端连接后发送 { challenge: "<hex>" }
184
- * 2. 客户端用 HMAC-SHA256(token, challenge) 生成 digest
185
- * 3. 客户端发送 { action: "auth", params: { digest: "<hex>" } }
186
- * 4. 服务端验证 digest,通过则认证成功
187
- *
188
- * token 不会明文传输,防止中间人嗅探。
189
- *
190
- * @param options.host - 远程主机地址
191
- * @param options.port - 远程端口
192
- * @param options.token - 认证 token(用于 HMAC 计算,不会明文传输)
193
- */
194
- static async connectRpc(options: {
195
- host: string;
196
- port: number;
197
- token: string;
198
- }): Promise<IpcClient> {
199
- const { host, port, token } = options;
200
-
201
- const client = await new Promise<IpcClient>((resolve, reject) => {
202
- const sock = net.connect(port, host);
203
- // skipDataHandler=true: 延迟注册 onData,避免与 challenge 读取冲突
204
- sock.on("connect", () => resolve(new IpcClient(sock, true)));
205
- sock.on("error", (err) =>
206
- reject(
207
- new Error(`无法连接 icqq RPC 服务 (${host}:${port}): ${err instanceof Error ? err.message : String(err)}`),
208
- ),
209
- );
210
- });
211
-
212
- // Wait for challenge from server, with proper buffering for TCP fragmentation
213
- const challenge = await new Promise<string>((resolve, reject) => {
214
- let challengeBuffer = "";
215
- const timeout = setTimeout(() => {
216
- client.socket.removeListener("data", onData);
217
- client.close();
218
- reject(new Error("RPC 挑战超时"));
219
- }, 10000);
220
-
221
- const onData = (chunk: Buffer) => {
222
- challengeBuffer += chunk.toString();
223
- const nlIdx = challengeBuffer.indexOf("\n");
224
- if (nlIdx === -1) return; // 等待更多数据
225
-
226
- clearTimeout(timeout);
227
- client.socket.removeListener("data", onData);
228
-
229
- try {
230
- const msg = JSON.parse(challengeBuffer.slice(0, nlIdx)) as {
231
- challenge?: string;
232
- };
233
- if (!msg.challenge) {
234
- reject(new Error("RPC 服务端未发送挑战"));
235
- return;
236
- }
237
- resolve(msg.challenge);
238
- } catch {
239
- reject(new Error("RPC 挑战解析失败"));
240
- }
241
- };
242
- client.socket.on("data", onData);
243
- });
244
-
245
- // Challenge 阶段结束,注册正式的数据处理 handler
246
- client.attachDataHandler();
247
-
248
- // Compute HMAC digest and authenticate
249
- const digest = createHmac("sha256", token)
250
- .update(challenge)
251
- .digest("hex");
252
- const authResp = await client.request("auth", { digest });
253
- if (!authResp.ok) {
254
- client.close();
255
- throw new Error(authResp.error ?? "RPC 认证失败");
256
- }
257
- return client;
258
- }
259
-
260
- /**
261
- * 通过 RPC 连接守护进程(自动从 daemon.rpc 文件读取地址)。
262
- * @param uin - 目标账号的 QQ 号
263
- */
264
- static async connectRpcByUin(uin: number): Promise<IpcClient> {
265
- let token: string;
266
- try {
267
- token = (await readFile(getTokenPath(uin), "utf-8")).trim();
268
- } catch {
269
- throw new Error(`无法读取认证 token,守护进程可能未运行:请先执行 icqq login ${uin} 完成登录并启动守护进程`);
270
- }
271
-
272
- let rpcInfo: { host: string; port: number };
273
- try {
274
- const raw = await readFile(getRpcPortPath(uin), "utf-8");
275
- rpcInfo = JSON.parse(raw);
276
- } catch {
277
- throw new Error(`RPC 未启用或守护进程未运行:请先执行 icqq login ${uin},并确认守护进程已启用 RPC`);
278
- }
279
-
280
- return IpcClient.connectRpc({
281
- host: rpcInfo.host,
282
- port: rpcInfo.port,
283
- token,
284
- });
285
- }
286
-
287
- private onData(data: string) {
288
- this.buffer += data;
289
- const lines = this.buffer.split("\n");
290
- this.buffer = lines.pop()!;
291
-
292
- for (const line of lines) {
293
- if (!line.trim()) continue;
294
- try {
295
- const msg = JSON.parse(line) as IpcMessage;
296
- if ("event" in msg) {
297
- const event = msg as IpcEvent;
298
- if (event.id === "*") {
299
- for (const handler of this.eventHandlers.values()) {
300
- handler(event);
301
- }
302
- } else {
303
- const handler = this.eventHandlers.get(event.id);
304
- handler?.(event);
305
- }
306
- } else {
307
- const p = this.pending.get(msg.id);
308
- if (p) {
309
- this.pending.delete(msg.id);
310
- p.resolve(msg as IpcResponse);
311
- }
312
- }
313
- } catch {
314
- // ignore malformed JSON
315
- }
316
- }
317
- }
318
-
319
- /**
320
- * 注册 icqq 事件回调。
321
- * 新版守护进程在认证后会自动广播事件(event.id = "*")。
322
- */
323
- onEvent(onEvent: (event: IpcEvent) => void): () => void {
324
- const id = randomUUID();
325
- this.eventHandlers.set(id, onEvent);
326
- return () => {
327
- this.eventHandlers.delete(id);
328
- };
329
- }
330
-
331
- /**
332
- * 发送 IPC 请求并等待响应。
333
- */
334
- async request(
335
- action: string,
336
- params: Record<string, unknown> = {},
337
- timeoutMs = 30000,
338
- ): Promise<IpcResponse> {
339
- if (this._closed) throw new Error("IPC 连接已关闭");
340
- const id = randomUUID();
341
- const req: IpcRequest = { id, action, params };
342
- return new Promise((resolve, reject) => {
343
- const timer = setTimeout(() => {
344
- this.pending.delete(id);
345
- reject(new Error(`IPC 请求超时 (${timeoutMs}ms): ${action}`));
346
- }, timeoutMs);
347
-
348
- this.pending.set(id, {
349
- resolve: (v) => {
350
- clearTimeout(timer);
351
- resolve(v);
352
- },
353
- reject: (e) => {
354
- clearTimeout(timer);
355
- reject(e);
356
- },
357
- });
358
- this.socket.write(JSON.stringify(req) + "\n", (err) => {
359
- if (err) {
360
- clearTimeout(timer);
361
- this.pending.delete(id);
362
- reject(err);
363
- }
364
- });
365
- });
366
- }
367
-
368
- /**
369
- * 订阅 daemon 推送事件:服务端自动推送全部事件,type/id 过滤在客户端完成。
370
- * @returns 订阅句柄,包含 unsubscribe() 方法
371
- */
372
- subscribe(
373
- _action: string,
374
- params: Record<string, unknown>,
375
- onEvent: (event: IpcEvent) => void,
376
- ): { id: string; unsubscribe: () => Promise<void> } {
377
- let off: () => void;
378
- if (params.type === "private" || params.type === "group") {
379
- const chatType: "private" | "group" = params.type;
380
- const sessionId = Number(params.id);
381
- off = Number.isFinite(sessionId) && sessionId > 0
382
- ? this.onEvent((event) => {
383
- if (isChatMessageEvent(event, chatType, sessionId)) onEvent(event);
384
- })
385
- : this.onEvent(onEvent);
386
- } else if (params.type === "guild" && params.id != null && params.id !== "") {
387
- const channelId = String(params.id);
388
- off = this.onEvent((event) => {
389
- if (isGuildChannelMessageEvent(event, channelId)) onEvent(event);
390
- });
391
- } else {
392
- off = this.onEvent(onEvent);
393
- }
394
-
395
- const id = randomUUID();
396
-
397
- return {
398
- id,
399
- unsubscribe: async () => {
400
- off();
401
- },
402
- };
403
- }
404
-
405
- /** 关闭连接 */
406
- close() {
407
- this._userClosed = true;
408
- this._closed = true;
409
- this.onRemoteDisconnect = null;
410
- this.eventHandlers.clear();
411
- for (const { reject } of this.pending.values()) {
412
- reject(new Error("连接已关闭"));
413
- }
414
- this.pending.clear();
415
- this.socket.destroy();
416
- }
417
- }