@zhin.js/adapter-icqq 8.0.9 → 8.0.11
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 +27 -0
- package/README.md +36 -28
- package/adapters/icqq.js +6 -11
- package/adapters/icqq.ts +7 -16
- package/agent/PERMITS.md +2 -0
- package/lib/agent-prompt.js +17 -22
- package/lib/cq-message.d.ts +2 -2
- package/lib/cq-message.js +2 -2
- package/lib/endpoint.d.ts +6 -76
- package/lib/endpoint.js +348 -507
- package/lib/forward-msg.d.ts +7 -8
- package/lib/forward-msg.js +11 -24
- package/lib/icqq-agent-deps.d.ts +3 -15
- package/lib/icqq-agent-deps.js +0 -5
- package/lib/icqq-endpoint-commands.js +6 -15
- package/lib/icqq-guild.d.ts +15 -11
- package/lib/icqq-guild.js +9 -17
- package/lib/icqq-inbound.d.ts +20 -20
- package/lib/icqq-inbound.js +9 -9
- package/lib/icqq-inbox.d.ts +2 -10
- package/lib/icqq-runtime-state.js +1 -1
- package/lib/index.d.ts +2 -3
- package/lib/index.js +2 -3
- package/lib/outbound-media.d.ts +3 -8
- package/lib/outbound-media.js +3 -8
- package/lib/protocol.d.ts +38 -57
- package/lib/protocol.js +25 -22
- package/lib/types.d.ts +4 -40
- package/lib/types.js +0 -5
- package/package.json +22 -13
- package/plugin.js +8 -3
- package/schema.json +119 -61
- package/src/agent-prompt.ts +17 -22
- package/src/cq-message.ts +2 -2
- package/src/endpoint.ts +360 -602
- package/src/forward-msg.ts +12 -26
- package/src/icqq-agent-deps.ts +3 -17
- package/src/icqq-endpoint-commands.ts +6 -15
- package/src/icqq-guild.ts +16 -25
- package/src/icqq-inbound.ts +26 -26
- package/src/icqq-inbox.ts +2 -10
- package/src/icqq-runtime-state.ts +1 -1
- package/src/index.ts +2 -9
- package/src/outbound-media.ts +4 -9
- package/src/protocol.ts +61 -67
- package/src/types.ts +4 -41
- package/tools/announce.js +19 -0
- package/tools/announce.ts +18 -0
- package/tools/essence.js +24 -0
- package/{agent/tools → tools}/essence.ts +8 -8
- package/tools/friend_list.js +20 -0
- package/{agent/tools → tools}/friend_list.ts +7 -7
- package/tools/group_files.js +25 -0
- package/tools/group_files.ts +23 -0
- package/tools/group_invite.js +25 -0
- package/tools/group_invite.ts +22 -0
- package/tools/group_kick.js +28 -0
- package/tools/group_kick.ts +24 -0
- package/tools/group_list.js +21 -0
- package/{agent/tools → tools}/group_list.ts +7 -7
- package/tools/group_sign.js +21 -0
- package/tools/group_sign.ts +19 -0
- package/tools/list_muted.js +20 -0
- package/tools/list_muted.ts +19 -0
- package/tools/poke.js +19 -0
- package/tools/poke.ts +18 -0
- package/tools/send_user_like.js +28 -0
- package/tools/send_user_like.ts +25 -0
- package/tools/set_anonymous.js +23 -0
- package/tools/set_anonymous.ts +21 -0
- package/tools/set_mute.js +26 -0
- package/tools/set_mute.ts +23 -0
- package/tools/set_title.js +27 -0
- package/tools/set_title.ts +24 -0
- package/tools/user_info.js +17 -0
- package/tools/user_info.ts +16 -0
- package/agent/tools/announce.ts +0 -21
- package/agent/tools/group_files.ts +0 -27
- package/agent/tools/group_invite.ts +0 -20
- package/agent/tools/group_sign.ts +0 -19
- package/agent/tools/list_muted.ts +0 -22
- package/agent/tools/poke.ts +0 -20
- package/agent/tools/send_user_like.ts +0 -22
- package/agent/tools/set_anonymous.ts +0 -22
- package/agent/tools/set_title.ts +0 -25
- package/lib/ipc-client.d.ts +0 -73
- package/lib/ipc-client.js +0 -358
- package/src/ipc-client.ts +0 -417
package/lib/endpoint.js
CHANGED
|
@@ -1,502 +1,236 @@
|
|
|
1
|
+
import { Client, parseGroupMessageId, } from '@icqqjs/icqq';
|
|
2
|
+
import { inspect } from 'node:util';
|
|
1
3
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
2
4
|
import { registerIcqqAgentEndpoint } from './icqq-agent-deps.js';
|
|
3
|
-
import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, quotedPayloadFromIcqqSource, resolveIcqqQuoteIdFromEvent, shouldSkipSelfInboundMessage,
|
|
5
|
+
import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, quotedPayloadFromIcqqSource, resolveIcqqQuoteIdFromEvent, shouldSkipSelfInboundMessage, } from './icqq-inbound.js';
|
|
4
6
|
import { buildIcqqInboxNoticeRow, buildIcqqInboxRequestRow, buildIcqqSystemRequestRow, isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
|
|
5
|
-
import { IcqqGuildCatalog,
|
|
6
|
-
import { IpcClient } from './ipc-client.js';
|
|
7
|
+
import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
|
|
7
8
|
import { materializeOutboundBase64, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
9
|
+
import { formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
|
|
10
|
+
const BOUND_EVENTS = [
|
|
11
|
+
'message.private',
|
|
12
|
+
'message.group',
|
|
13
|
+
'message.guild',
|
|
14
|
+
'request.friend',
|
|
15
|
+
'request.group',
|
|
16
|
+
'notice.friend',
|
|
17
|
+
'notice.group',
|
|
18
|
+
'system.online',
|
|
19
|
+
'system.offline',
|
|
20
|
+
'system.offline.network',
|
|
21
|
+
'system.offline.kickoff',
|
|
22
|
+
'system.login.qrcode',
|
|
23
|
+
'system.login.device',
|
|
24
|
+
'system.login.slider',
|
|
25
|
+
'system.login.error',
|
|
26
|
+
'system.login.auth',
|
|
27
|
+
];
|
|
28
|
+
export class IcqqEndpoint extends Client {
|
|
10
29
|
#logger;
|
|
11
30
|
#options;
|
|
12
|
-
|
|
13
|
-
name;
|
|
14
|
-
/** Populated after start(); agent tools read this. */
|
|
15
|
-
ipc;
|
|
16
|
-
friends = new Map();
|
|
17
|
-
groups = new Map();
|
|
31
|
+
endpointName;
|
|
18
32
|
#guildCatalog = new IcqqGuildCatalog();
|
|
33
|
+
#inboundDeduper = new InboundMessageDeduper();
|
|
34
|
+
#inboxDeduper = new InboundMessageDeduper();
|
|
35
|
+
#open = false;
|
|
36
|
+
#started = false;
|
|
37
|
+
#heldInbound = [];
|
|
38
|
+
#unregisterAgent;
|
|
19
39
|
management = Object.freeze({
|
|
20
|
-
listFriends: () => this.
|
|
21
|
-
|
|
40
|
+
listFriends: async () => Array.from(this.fl.values()).map((f) => ({
|
|
41
|
+
user_id: f.user_id,
|
|
42
|
+
nickname: f.nickname,
|
|
43
|
+
remark: f.remark ?? '',
|
|
44
|
+
})),
|
|
45
|
+
listGroups: async () => Array.from(this.gl.values()).map((g) => ({
|
|
46
|
+
group_id: g.group_id,
|
|
47
|
+
name: g.group_name,
|
|
48
|
+
})),
|
|
22
49
|
listChannels: async () => {
|
|
23
|
-
await this.#guildCatalog.syncAll(this
|
|
50
|
+
await this.#guildCatalog.syncAll(this);
|
|
24
51
|
return this.#guildCatalog.getGuildChannelList();
|
|
25
52
|
},
|
|
26
|
-
listGroupMembers: (groupId) => this.getGroupMemberList(groupId),
|
|
27
|
-
approveRequest: (requestId, remark) => this
|
|
28
|
-
rejectRequest: (requestId, reason) => this
|
|
29
|
-
kickGroupMember: (groupId, userId) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
listGroupMembers: async (groupId) => Array.from((await this.getGroupMemberList(Number(groupId))).values()),
|
|
54
|
+
approveRequest: (requestId, remark) => this.#approveRequest(requestId, remark),
|
|
55
|
+
rejectRequest: (requestId, reason) => this.#rejectRequest(requestId, reason),
|
|
56
|
+
kickGroupMember: async (groupId, userId) => {
|
|
57
|
+
await this.setGroupKick(Number(groupId), Number(userId));
|
|
58
|
+
},
|
|
59
|
+
muteGroupMember: async (groupId, userId, duration) => {
|
|
60
|
+
await this.setGroupBan(Number(groupId), Number(userId), duration);
|
|
61
|
+
},
|
|
62
|
+
setGroupAdmin: async (groupId, userId, enabled) => {
|
|
63
|
+
await this.setGroupAdmin(Number(groupId), Number(userId), enabled);
|
|
64
|
+
},
|
|
65
|
+
deleteFriend: async (userId) => {
|
|
66
|
+
await this.deleteFriend(Number(userId));
|
|
67
|
+
},
|
|
33
68
|
});
|
|
34
69
|
control = Object.freeze({
|
|
35
|
-
recall: (message) =>
|
|
36
|
-
|
|
37
|
-
|
|
70
|
+
recall: async (message) => {
|
|
71
|
+
if (!message.id || message.id.startsWith('outbound:'))
|
|
72
|
+
return;
|
|
73
|
+
await this.deleteMsg(message.id);
|
|
74
|
+
},
|
|
75
|
+
addReaction: async (message, emoji) => {
|
|
76
|
+
const target = resolveIcqqGroupReactionTarget(message);
|
|
77
|
+
if (!target)
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
// Do not await protocol ACK — packet timeout must not stall the AI reply.
|
|
81
|
+
this.#ignoreReactionAck('add', this.pickGroup(target.groupId).setReaction(target.seq, emoji));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.#logger.debug(formatCompact({
|
|
85
|
+
op: 'reaction_add_failed',
|
|
86
|
+
error: error instanceof Error ? error.message : String(error),
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
return emoji;
|
|
90
|
+
},
|
|
91
|
+
removeReaction: async (message, reactionId) => {
|
|
92
|
+
const target = resolveIcqqGroupReactionTarget(message);
|
|
93
|
+
if (!target)
|
|
94
|
+
return;
|
|
95
|
+
try {
|
|
96
|
+
this.#ignoreReactionAck('remove', this.pickGroup(target.groupId).delReaction(target.seq, reactionId));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
this.#logger.debug(formatCompact({
|
|
100
|
+
op: 'reaction_remove_failed',
|
|
101
|
+
error: error instanceof Error ? error.message : String(error),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
},
|
|
38
105
|
});
|
|
39
|
-
#open = false;
|
|
40
|
-
#started = false;
|
|
41
|
-
#subscriptions = [];
|
|
42
|
-
#inboundDeduper = new InboundMessageDeduper();
|
|
43
|
-
/** request/notice 去重(推送事件与 GET_SYSTEM_MSG 首拉可能重复同一 flag)。 */
|
|
44
|
-
#inboxDeduper = new InboundMessageDeduper();
|
|
45
|
-
#unregisterAgent;
|
|
46
|
-
/** 用户主动 stop 时为 true,阻止自动重连 */
|
|
47
|
-
#intentionalDisconnect = false;
|
|
48
|
-
/** 是否已有重连循环在跑(避免多次 schedule 叠套) */
|
|
49
|
-
#reconnectRunning = false;
|
|
50
106
|
constructor(options) {
|
|
107
|
+
const nativeConfig = resolveNativeClientConfig(options.config);
|
|
108
|
+
super(Number(options.config.id), nativeConfig);
|
|
51
109
|
this.#logger = getAdapterLogger('icqq', options.config.id);
|
|
52
110
|
this.#options = options;
|
|
53
|
-
this.
|
|
54
|
-
this.#createIpc = options.createIpc ?? defaultCreateIpc;
|
|
111
|
+
this.endpointName = options.config.id;
|
|
55
112
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
113
|
+
#ignoreReactionAck(action, task) {
|
|
114
|
+
void Promise.resolve(task).catch((error) => {
|
|
115
|
+
this.#logger.debug(formatCompact({
|
|
116
|
+
op: `reaction_${action}_failed`,
|
|
117
|
+
error: error instanceof Error ? error.message : String(error),
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
61
120
|
}
|
|
62
121
|
async start() {
|
|
63
122
|
if (this.#started)
|
|
64
123
|
return;
|
|
65
124
|
this.#started = true;
|
|
66
|
-
this.#intentionalDisconnect = false;
|
|
67
125
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
126
|
+
const onlineReady = new Promise((resolve) => {
|
|
127
|
+
this.on('system.online', () => resolve());
|
|
128
|
+
});
|
|
129
|
+
this.#bindClientEvents();
|
|
130
|
+
await this.login(this.#options.config.password);
|
|
131
|
+
await onlineReady;
|
|
132
|
+
await this.#pullPendingSystemMessages();
|
|
133
|
+
this.#unregisterAgent = registerIcqqAgentEndpoint(this.endpointName, this);
|
|
134
|
+
this.#logger.info(`connected (direct) | friends: ${this.fl.size} | groups: ${this.gl.size}`);
|
|
71
135
|
}
|
|
72
136
|
catch (error) {
|
|
73
137
|
await this.stop();
|
|
74
|
-
|
|
75
|
-
// keep this at debug to avoid a duplicate error-level log.
|
|
76
|
-
this.#logger.debug(`Failed to connect ICQQ IPC: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
138
|
+
this.#logger.debug(`Failed to connect ICQQ client: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
77
139
|
throw error;
|
|
78
140
|
}
|
|
79
141
|
}
|
|
80
|
-
/** 建立或恢复与守护进程的 IPC/RPC 会话(订阅、缓存列表) */
|
|
81
|
-
async #bindIpcSession() {
|
|
82
|
-
// 旧连接上的订阅句柄已失效,不再对死 socket 发 unsubscribe
|
|
83
|
-
this.#subscriptions = [];
|
|
84
|
-
this.ipc = await this.#createIpc(this.#options.config);
|
|
85
|
-
if (this.#options.config.autoReconnect !== false) {
|
|
86
|
-
this.ipc.setOnRemoteDisconnect(() => this.#scheduleReconnect());
|
|
87
|
-
}
|
|
88
|
-
await this.#refreshLists();
|
|
89
|
-
const sub = this.ipc.subscribe(Actions.SUBSCRIBE, {}, (event) => {
|
|
90
|
-
this.#handleEvent(event);
|
|
91
|
-
});
|
|
92
|
-
this.#subscriptions.push(sub);
|
|
93
|
-
// daemon 推送 client.em 全部事件,request/notice 实时到达;
|
|
94
|
-
// 启动时补一次 GET_SYSTEM_MSG,捞离线期间积存的待处理请求。
|
|
95
|
-
void this.#pullPendingSystemMessages();
|
|
96
|
-
}
|
|
97
|
-
/** IPC/RPC 意外断开时调度重连(指数退避) */
|
|
98
|
-
#scheduleReconnect() {
|
|
99
|
-
if (this.#options.config.autoReconnect === false)
|
|
100
|
-
return;
|
|
101
|
-
if (this.#intentionalDisconnect)
|
|
102
|
-
return;
|
|
103
|
-
if (this.#reconnectRunning)
|
|
104
|
-
return;
|
|
105
|
-
this.#reconnectRunning = true;
|
|
106
|
-
void this.#runReconnectLoop();
|
|
107
|
-
}
|
|
108
|
-
async #runReconnectLoop() {
|
|
109
|
-
try {
|
|
110
|
-
for (let attempt = 0; !this.#intentionalDisconnect; attempt++) {
|
|
111
|
-
const base = Math.min(30_000, 500 * 2 ** Math.min(attempt, 6));
|
|
112
|
-
const jitter = Math.floor(Math.random() * 400);
|
|
113
|
-
const delayMs = base + jitter;
|
|
114
|
-
// 首次断开 WARN,后续重试静默为 DEBUG,避免刷屏
|
|
115
|
-
const disconnectLog = attempt === 0 ? this.#logger.warn.bind(this.#logger) : this.#logger.debug.bind(this.#logger);
|
|
116
|
-
disconnectLog(formatCompact({
|
|
117
|
-
op: 'disconnect',
|
|
118
|
-
ok: false,
|
|
119
|
-
delay_ms: delayMs,
|
|
120
|
-
attempt: attempt + 1,
|
|
121
|
-
}));
|
|
122
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
123
|
-
if (this.#intentionalDisconnect)
|
|
124
|
-
break;
|
|
125
|
-
try {
|
|
126
|
-
await this.#bindIpcSession();
|
|
127
|
-
this.#logger.info(`reconnected after ${attempt + 1} attempt(s)`
|
|
128
|
-
+ ` | friends: ${this.friends.size} | groups: ${this.groups.size}`);
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
const retryLog = attempt === 0 ? this.#logger.warn.bind(this.#logger) : this.#logger.debug.bind(this.#logger);
|
|
133
|
-
retryLog(formatCompact({
|
|
134
|
-
op: 'reconnect',
|
|
135
|
-
endpoint: this.name,
|
|
136
|
-
ok: false,
|
|
137
|
-
attempt: attempt + 1,
|
|
138
|
-
error: error instanceof Error ? error.message : String(error),
|
|
139
|
-
}));
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
finally {
|
|
144
|
-
this.#reconnectRunning = false;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
142
|
open() {
|
|
148
143
|
this.#open = true;
|
|
144
|
+
const held = this.#heldInbound.splice(0);
|
|
145
|
+
for (const msg of held)
|
|
146
|
+
this.admit(msg);
|
|
149
147
|
}
|
|
150
148
|
close() {
|
|
151
149
|
this.#open = false;
|
|
152
150
|
}
|
|
153
151
|
async stop() {
|
|
154
|
-
this.#intentionalDisconnect = true;
|
|
155
152
|
this.#open = false;
|
|
156
|
-
this.
|
|
157
|
-
for (const sub of this.#subscriptions.splice(0)) {
|
|
158
|
-
await sub.unsubscribe().catch(() => { });
|
|
159
|
-
}
|
|
153
|
+
this.#heldInbound.length = 0;
|
|
160
154
|
this.#unregisterAgent?.();
|
|
161
155
|
this.#unregisterAgent = undefined;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
this.friends.clear();
|
|
165
|
-
this.groups.clear();
|
|
166
|
-
this.#guildCatalog.clear();
|
|
156
|
+
for (const event of BOUND_EVENTS)
|
|
157
|
+
this.off(event);
|
|
167
158
|
try {
|
|
168
|
-
this.
|
|
159
|
+
await this.logout?.();
|
|
169
160
|
}
|
|
170
|
-
catch {
|
|
171
|
-
|
|
161
|
+
catch { /* ignore */ }
|
|
162
|
+
try {
|
|
163
|
+
this.terminate();
|
|
172
164
|
}
|
|
173
|
-
|
|
174
|
-
this.
|
|
165
|
+
catch { /* ignore */ }
|
|
166
|
+
this.#inboundDeduper.clear();
|
|
167
|
+
this.#inboxDeduper.clear();
|
|
168
|
+
this.#guildCatalog.clear();
|
|
175
169
|
this.#started = false;
|
|
176
170
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
177
171
|
}
|
|
178
172
|
async send({ conversation, payload }) {
|
|
179
|
-
if (!this
|
|
180
|
-
throw new Error(
|
|
181
|
-
}
|
|
173
|
+
if (!this.#started)
|
|
174
|
+
throw new Error('icqq endpoint 未连接');
|
|
182
175
|
const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
|
|
183
176
|
const content = Array.isArray(payload)
|
|
184
177
|
? materializeOutboundBase64(payload, mediaMode)
|
|
185
178
|
: payload;
|
|
186
179
|
const message = formatOutboundBody(content);
|
|
187
180
|
const parsed = icqqOutboundTarget(conversation);
|
|
188
|
-
let
|
|
189
|
-
let params;
|
|
190
|
-
switch (parsed.kind) {
|
|
191
|
-
case 'private':
|
|
192
|
-
action = Actions.SEND_PRIVATE_MSG;
|
|
193
|
-
params = { user_id: parsed.userId, message };
|
|
194
|
-
break;
|
|
195
|
-
case 'group':
|
|
196
|
-
action = Actions.SEND_GROUP_MSG;
|
|
197
|
-
params = { group_id: parsed.groupId, message };
|
|
198
|
-
break;
|
|
199
|
-
case 'temp':
|
|
200
|
-
action = Actions.SEND_TEMP_MSG;
|
|
201
|
-
params = {
|
|
202
|
-
group_id: parsed.groupId,
|
|
203
|
-
user_id: parsed.userId,
|
|
204
|
-
message,
|
|
205
|
-
};
|
|
206
|
-
break;
|
|
207
|
-
case 'channel':
|
|
208
|
-
action = Actions.GUILD_SEND_MSG;
|
|
209
|
-
params = {
|
|
210
|
-
guild_id: parsed.guildId,
|
|
211
|
-
channel_id: parsed.channelId,
|
|
212
|
-
message,
|
|
213
|
-
};
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
const resp = await this.ipc.request(action, params);
|
|
217
|
-
if (!resp.ok) {
|
|
218
|
-
this.#logger.warn(`send failed ${conversation.kind}:${conversation.id} | ${resp.error} | ${truncatePreview(typeof message === 'string' ? message : String(message), 80)}`);
|
|
219
|
-
throw new Error(`发送消息失败: ${resp.error}`);
|
|
220
|
-
}
|
|
221
|
-
const messageId = String(resp.data?.message_id ?? `sent_${Date.now()}`);
|
|
222
|
-
this.#logger.info(`send ${conversation.kind}:${conversation.id} | id: ${messageId} | ${truncatePreview(typeof message === 'string' ? message : String(message), 80)}`);
|
|
223
|
-
return messageId;
|
|
224
|
-
}
|
|
225
|
-
// ── 消息回应(activity-feedback / typing reaction)────────────────────
|
|
226
|
-
static EMOJI_MAP = {
|
|
227
|
-
'⏳': '1468368274',
|
|
228
|
-
'👍': '128077',
|
|
229
|
-
'❤️': '10084',
|
|
230
|
-
'😊': '128522',
|
|
231
|
-
'🎉': '127881',
|
|
232
|
-
'🔥': '128293',
|
|
233
|
-
'✅': '9989',
|
|
234
|
-
'❌': '10060',
|
|
235
|
-
'⭐': '11088',
|
|
236
|
-
'💯': '128175',
|
|
237
|
-
};
|
|
238
|
-
getEmojiId(emoji) {
|
|
239
|
-
if (/^\d+$/u.test(emoji))
|
|
240
|
-
return emoji;
|
|
241
|
-
return IcqqIpcEndpoint.EMOJI_MAP[emoji] ?? String(emoji.codePointAt(0) || 0);
|
|
242
|
-
}
|
|
243
|
-
/** Activity-feedback / OutboundHost reaction surface. */
|
|
244
|
-
async addReaction(messageId, emoji) {
|
|
181
|
+
let result;
|
|
245
182
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
error: resp.error,
|
|
260
|
-
}));
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
this.#logger.debug(formatCompact({
|
|
264
|
-
op: 'add_reaction',
|
|
265
|
-
endpoint: this.name,
|
|
266
|
-
message_id: messageId,
|
|
267
|
-
emoji,
|
|
268
|
-
id: emojiId,
|
|
269
|
-
ok: true,
|
|
270
|
-
}));
|
|
271
|
-
return emojiId;
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
this.#logger.warn(formatCompact({
|
|
275
|
-
op: 'add_reaction',
|
|
276
|
-
endpoint: this.name,
|
|
277
|
-
message_id: messageId,
|
|
278
|
-
emoji,
|
|
279
|
-
ok: false,
|
|
280
|
-
error: error instanceof Error ? error.message : String(error),
|
|
281
|
-
}));
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
async removeReaction(messageId, reactionId) {
|
|
286
|
-
try {
|
|
287
|
-
const parts = reactionId.split('_');
|
|
288
|
-
const emojiId = parts.length >= 2 ? parts[1] : reactionId;
|
|
289
|
-
const resp = await this.request(Actions.GROUP_DEL_REACTION, {
|
|
290
|
-
message_id: messageId,
|
|
291
|
-
id: emojiId,
|
|
292
|
-
});
|
|
293
|
-
if (!resp.ok) {
|
|
294
|
-
this.#logger.warn(formatCompact({
|
|
295
|
-
op: 'remove_reaction',
|
|
296
|
-
endpoint: this.name,
|
|
297
|
-
message_id: messageId,
|
|
298
|
-
reaction_id: reactionId,
|
|
299
|
-
id: emojiId,
|
|
300
|
-
ok: false,
|
|
301
|
-
error: resp.error,
|
|
302
|
-
}));
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
this.#logger.debug(formatCompact({
|
|
306
|
-
op: 'remove_reaction',
|
|
307
|
-
endpoint: this.name,
|
|
308
|
-
message_id: messageId,
|
|
309
|
-
reaction_id: reactionId,
|
|
310
|
-
id: emojiId,
|
|
311
|
-
ok: true,
|
|
312
|
-
}));
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
this.#logger.warn(formatCompact({
|
|
316
|
-
op: 'remove_reaction',
|
|
317
|
-
endpoint: this.name,
|
|
318
|
-
message_id: messageId,
|
|
319
|
-
reaction_id: reactionId,
|
|
320
|
-
ok: false,
|
|
321
|
-
error: error instanceof Error ? error.message : String(error),
|
|
322
|
-
}));
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
/** Activity-feedback autoRemove / Console recall. */
|
|
326
|
-
async recallMessage(messageId) {
|
|
327
|
-
if (!messageId || messageId.startsWith('outbound:'))
|
|
328
|
-
return;
|
|
329
|
-
try {
|
|
330
|
-
const resp = await this.request(Actions.RECALL_MSG, { message_id: messageId });
|
|
331
|
-
if (!resp.ok) {
|
|
332
|
-
this.#logger.warn(formatCompact({
|
|
333
|
-
op: 'recall_message',
|
|
334
|
-
endpoint: this.name,
|
|
335
|
-
message_id: messageId,
|
|
336
|
-
ok: false,
|
|
337
|
-
error: resp.error,
|
|
338
|
-
}));
|
|
339
|
-
return;
|
|
183
|
+
switch (parsed.kind) {
|
|
184
|
+
case 'private':
|
|
185
|
+
result = await this.sendPrivateMsg(parsed.userId, message);
|
|
186
|
+
break;
|
|
187
|
+
case 'group':
|
|
188
|
+
result = await this.sendGroupMsg(parsed.groupId, message);
|
|
189
|
+
break;
|
|
190
|
+
case 'temp':
|
|
191
|
+
result = await this.sendTempMsg(parsed.groupId, parsed.userId, message);
|
|
192
|
+
break;
|
|
193
|
+
case 'channel':
|
|
194
|
+
result = await this.sendGuildMsg(parsed.guildId, parsed.channelId, message);
|
|
195
|
+
break;
|
|
340
196
|
}
|
|
341
|
-
this.#logger.debug(formatCompact({
|
|
342
|
-
op: 'recall_message',
|
|
343
|
-
endpoint: this.name,
|
|
344
|
-
message_id: messageId,
|
|
345
|
-
ok: true,
|
|
346
|
-
}));
|
|
347
197
|
}
|
|
348
198
|
catch (error) {
|
|
349
199
|
this.#logger.warn(formatCompact({
|
|
350
|
-
op: '
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
error: error instanceof Error ? error.message : String(error),
|
|
200
|
+
op: 'icqq_send_failed',
|
|
201
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
202
|
+
error: describeUnknownError(error),
|
|
203
|
+
preview: truncatePreview(typeof message === 'string' ? message : String(message), 120),
|
|
355
204
|
}));
|
|
205
|
+
throw error;
|
|
356
206
|
}
|
|
207
|
+
const messageId = String(result?.message_id ?? `sent_${Date.now()}`);
|
|
208
|
+
this.#logger.info(`send ${conversation.kind}:${conversation.id} | id: ${messageId} | ${truncatePreview(typeof message === 'string' ? message : String(message), 80)}`);
|
|
209
|
+
return messageId;
|
|
357
210
|
}
|
|
358
|
-
// ── 社交/群管(Console endpoint RPC 探测面)────────────────────────
|
|
359
|
-
// console-rpc-extended.ts 按方法名探测这些接口;全部薄封装走 IPC daemon。
|
|
360
|
-
/** EndpointManagement.listFriends —— 好友列表(LIST_FRIENDS,归一为 {user_id, nickname, remark})。 */
|
|
361
|
-
async getFriendList() {
|
|
362
|
-
const resp = await this.#mustRequest(Actions.LIST_FRIENDS, undefined, '获取好友列表');
|
|
363
|
-
const list = Array.isArray(resp.data) ? resp.data : [];
|
|
364
|
-
return list.map((f) => ({
|
|
365
|
-
user_id: f.user_id,
|
|
366
|
-
nickname: f.nickname,
|
|
367
|
-
remark: f.remark ?? '',
|
|
368
|
-
}));
|
|
369
|
-
}
|
|
370
|
-
/** EndpointManagement.listGroups —— 群列表(LIST_GROUPS,归一为 {group_id, name})。 */
|
|
371
|
-
async getGroupList() {
|
|
372
|
-
const resp = await this.#mustRequest(Actions.LIST_GROUPS, undefined, '获取群列表');
|
|
373
|
-
const list = Array.isArray(resp.data) ? resp.data : [];
|
|
374
|
-
return list.map((g) => ({
|
|
375
|
-
group_id: g.group_id,
|
|
376
|
-
name: g.group_name,
|
|
377
|
-
}));
|
|
378
|
-
}
|
|
379
|
-
/** EndpointManagement.listGroupMembers —— 群成员列表(字段对齐 daemon 返回)。 */
|
|
380
|
-
async getGroupMemberList(groupId) {
|
|
381
|
-
const resp = await this.#mustRequest(Actions.LIST_GROUP_MEMBERS, { group_id: toNumericId(groupId, 'group_id') }, '获取群成员列表');
|
|
382
|
-
return Array.isArray(resp.data) ? resp.data : [];
|
|
383
|
-
}
|
|
384
|
-
/** getGroupMemberList 别名(console 探测 listMembers / getMemberList)。 */
|
|
385
|
-
listMembers(groupId) {
|
|
386
|
-
return this.getGroupMemberList(groupId);
|
|
387
|
-
}
|
|
388
|
-
getMemberList(groupId) {
|
|
389
|
-
return this.getGroupMemberList(groupId);
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* EndpointManagement.resolveRequest —— id 为 inbox 行的 platform_request_id。
|
|
393
|
-
* 先 GET_SYSTEM_MSG 按 flag(回退 seq)定位请求,再按类型路由 handle_friend/group_request。
|
|
394
|
-
*/
|
|
395
|
-
async approveRequest(id, remark) {
|
|
396
|
-
await this.#handleSystemRequest(id, true, { remark });
|
|
397
|
-
}
|
|
398
|
-
async rejectRequest(id, reason) {
|
|
399
|
-
await this.#handleSystemRequest(id, false, { reason });
|
|
400
|
-
}
|
|
401
|
-
/** EndpointManagement.deleteFriend —— FRIEND_DELETE。 */
|
|
402
|
-
async deleteFriend(userId) {
|
|
403
|
-
await this.#mustRequest(Actions.FRIEND_DELETE, { user_id: toNumericId(userId, 'user_id') }, '删除好友');
|
|
404
|
-
}
|
|
405
|
-
/** deleteFriend 别名(console 探测 delete_friend)。 */
|
|
406
|
-
delete_friend(userId) {
|
|
407
|
-
return this.deleteFriend(userId);
|
|
408
|
-
}
|
|
409
|
-
/** EndpointManagement.kickGroupMember —— GROUP_KICK。 */
|
|
410
|
-
async removeMember(groupId, userId) {
|
|
411
|
-
await this.#mustRequest(Actions.GROUP_KICK, { group_id: toNumericId(groupId, 'group_id'), user_id: toNumericId(userId, 'user_id') }, '踢出群成员');
|
|
412
|
-
}
|
|
413
|
-
kickMember(groupId, userId) {
|
|
414
|
-
return this.removeMember(groupId, userId);
|
|
415
|
-
}
|
|
416
|
-
setGroupKick(groupId, userId) {
|
|
417
|
-
return this.removeMember(groupId, userId);
|
|
418
|
-
}
|
|
419
|
-
/** EndpointManagement.muteGroupMember —— GROUP_MUTE(duration 秒,默认 600)。 */
|
|
420
|
-
async muteMember(groupId, userId, duration = 600) {
|
|
421
|
-
await this.#mustRequest(Actions.GROUP_MUTE, {
|
|
422
|
-
group_id: toNumericId(groupId, 'group_id'),
|
|
423
|
-
user_id: toNumericId(userId, 'user_id'),
|
|
424
|
-
duration,
|
|
425
|
-
}, '禁言群成员');
|
|
426
|
-
}
|
|
427
|
-
banMember(groupId, userId, duration = 600) {
|
|
428
|
-
return this.muteMember(groupId, userId, duration);
|
|
429
|
-
}
|
|
430
|
-
setGroupMute(groupId, userId, duration = 600) {
|
|
431
|
-
return this.muteMember(groupId, userId, duration);
|
|
432
|
-
}
|
|
433
|
-
/** EndpointManagement.setGroupAdmin —— SET_GROUP_ADMIN(enable 默认 true)。 */
|
|
434
|
-
async setModerator(groupId, userId, enable = true) {
|
|
435
|
-
await this.#mustRequest(Actions.SET_GROUP_ADMIN, {
|
|
436
|
-
group_id: toNumericId(groupId, 'group_id'),
|
|
437
|
-
user_id: toNumericId(userId, 'user_id'),
|
|
438
|
-
enable,
|
|
439
|
-
}, '设置群管理员');
|
|
440
|
-
}
|
|
441
|
-
setAdmin(groupId, userId, enable = true) {
|
|
442
|
-
return this.setModerator(groupId, userId, enable);
|
|
443
|
-
}
|
|
444
|
-
setGroupAdmin(groupId, userId, enable = true) {
|
|
445
|
-
return this.setModerator(groupId, userId, enable);
|
|
446
|
-
}
|
|
447
|
-
/** IPC 请求并统一错误上下文:daemon 返回 ok=false 时抛出带操作名的错误。 */
|
|
448
|
-
async #mustRequest(action, params, label) {
|
|
449
|
-
const resp = await this.request(action, params);
|
|
450
|
-
if (!resp.ok) {
|
|
451
|
-
throw new Error(`icqq ${label}失败: ${resp.error ?? 'daemon 未返回错误详情'}`);
|
|
452
|
-
}
|
|
453
|
-
return resp;
|
|
454
|
-
}
|
|
455
|
-
/** 按 flag/seq 在 GET_SYSTEM_MSG 中定位请求,路由到好友/群请求处理 action。 */
|
|
456
|
-
async #handleSystemRequest(id, approve, extra) {
|
|
457
|
-
const resp = await this.#mustRequest(Actions.GET_SYSTEM_MSG, undefined, '获取待处理请求');
|
|
458
|
-
const data = resp.data;
|
|
459
|
-
const friendRequests = Array.isArray(data?.friendRequests)
|
|
460
|
-
? data.friendRequests
|
|
461
|
-
: [];
|
|
462
|
-
const groupRequests = Array.isArray(data?.groupRequests)
|
|
463
|
-
? data.groupRequests
|
|
464
|
-
: [];
|
|
465
|
-
const matches = (m) => m.flag === id || (m.seq != null && String(m.seq) === id);
|
|
466
|
-
const friend = friendRequests.find(matches);
|
|
467
|
-
const group = friend ? undefined : groupRequests.find(matches);
|
|
468
|
-
const target = friend ?? group;
|
|
469
|
-
if (!target?.flag) {
|
|
470
|
-
throw new Error(`icqq 未找到待处理请求: ${id}(GET_SYSTEM_MSG 中无匹配 flag/seq)`);
|
|
471
|
-
}
|
|
472
|
-
const params = { flag: target.flag, approve };
|
|
473
|
-
if (friend) {
|
|
474
|
-
if (extra.remark)
|
|
475
|
-
params.remark = extra.remark;
|
|
476
|
-
await this.#mustRequest(Actions.HANDLE_FRIEND_REQUEST, params, '处理好友请求');
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
if (extra.reason)
|
|
480
|
-
params.reason = extra.reason;
|
|
481
|
-
await this.#mustRequest(Actions.HANDLE_GROUP_REQUEST, params, '处理群请求');
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
/** Test / internal: admit when open. */
|
|
485
211
|
admit(msg) {
|
|
486
212
|
const conversation = msg.conversation;
|
|
487
213
|
if (!this.#open) {
|
|
488
|
-
this.#
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
214
|
+
if (this.#heldInbound.length >= INBOUND_HOLD_LIMIT) {
|
|
215
|
+
const dropped = this.#heldInbound.shift();
|
|
216
|
+
this.#logger.warn(formatCompact({
|
|
217
|
+
op: 'icqq_inbound_hold_drop',
|
|
218
|
+
endpoint: this.endpointName,
|
|
219
|
+
id: dropped?.id,
|
|
220
|
+
target: dropped
|
|
221
|
+
? `${dropped.conversation.kind}:${dropped.conversation.id}`
|
|
222
|
+
: undefined,
|
|
223
|
+
limit: INBOUND_HOLD_LIMIT,
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
this.#heldInbound.push(msg);
|
|
227
|
+
this.#logger.info(`hold ${conversation.kind}:${conversation.id}`
|
|
228
|
+
+ (msg.sender ? ` from ${msg.sender.name ?? msg.sender.id}` : '')
|
|
229
|
+
+ ` until open | ${truncatePreview(msg.content, 80)}`);
|
|
495
230
|
return;
|
|
496
231
|
}
|
|
497
|
-
// 新 Runtime Message.content 为纯文本:@ 本机(uin = name)经结构化段 / metadata 传递
|
|
498
232
|
const mentioned = isIcqqBotMentioned({
|
|
499
|
-
uin: this.
|
|
233
|
+
uin: this.endpointName,
|
|
500
234
|
content: msg.segments,
|
|
501
235
|
rawMessage: msg.content,
|
|
502
236
|
});
|
|
@@ -512,7 +246,7 @@ export class IcqqIpcEndpoint {
|
|
|
512
246
|
content: msg.content,
|
|
513
247
|
...(msg.segments ? { segments: msg.segments } : {}),
|
|
514
248
|
sender: msg.sender,
|
|
515
|
-
endpointId: this.
|
|
249
|
+
endpointId: this.endpointName,
|
|
516
250
|
...(mentioned ? { mentioned: true } : {}),
|
|
517
251
|
...(quoteId ? { replyTo: { id: String(quoteId) } } : {}),
|
|
518
252
|
metadata: Object.freeze({
|
|
@@ -522,55 +256,70 @@ export class IcqqIpcEndpoint {
|
|
|
522
256
|
}).catch((err) => {
|
|
523
257
|
this.#logger.warn(formatCompact({
|
|
524
258
|
op: 'recv',
|
|
525
|
-
endpoint: this.
|
|
259
|
+
endpoint: this.endpointName,
|
|
526
260
|
target: `${conversation.kind}:${conversation.id}`,
|
|
527
261
|
ok: false,
|
|
528
262
|
error: err instanceof Error ? err.message : String(err),
|
|
529
263
|
}));
|
|
530
264
|
});
|
|
531
265
|
}
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
]);
|
|
537
|
-
this.friends.clear();
|
|
538
|
-
if (flResp.ok && Array.isArray(flResp.data)) {
|
|
539
|
-
for (const f of flResp.data) {
|
|
540
|
-
this.friends.set(f.user_id, f);
|
|
266
|
+
#bindClientEvents() {
|
|
267
|
+
const safe = (label, fn) => {
|
|
268
|
+
try {
|
|
269
|
+
fn();
|
|
541
270
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
271
|
+
catch (error) {
|
|
272
|
+
this.#logger.warn(formatCompact({
|
|
273
|
+
op: 'icqq_event_handler',
|
|
274
|
+
endpoint: this.endpointName,
|
|
275
|
+
event: label,
|
|
276
|
+
error: error instanceof Error ? error.message : String(error),
|
|
277
|
+
}));
|
|
547
278
|
}
|
|
548
|
-
}
|
|
279
|
+
};
|
|
280
|
+
this.on('message.private', (event) => safe('message.private', () => this.#handleMessageEvent('message.private', event)));
|
|
281
|
+
this.on('message.group', (event) => safe('message.group', () => this.#handleMessageEvent('message.group', event)));
|
|
282
|
+
this.on('message.guild', (event) => safe('message.guild', () => this.#handleGuildEvent(event)));
|
|
283
|
+
this.on('request.friend', (event) => safe('request.friend', () => this.#recordInboxRequest(serializeIcqqEvent(event))));
|
|
284
|
+
this.on('request.group', (event) => safe('request.group', () => this.#recordInboxRequest(serializeIcqqEvent(event))));
|
|
285
|
+
this.on('notice.friend', (event) => safe('notice.friend', () => this.#recordInboxNotice(serializeIcqqEvent(event))));
|
|
286
|
+
this.on('notice.group', (event) => safe('notice.group', () => this.#recordInboxNotice(serializeIcqqEvent(event))));
|
|
287
|
+
this.on('system.offline', (event) => this.#logSystemEvent('offline', event));
|
|
288
|
+
this.on('system.offline.network', (event) => this.#logSystemEvent('offline.network', event));
|
|
289
|
+
this.on('system.offline.kickoff', (event) => this.#logSystemEvent('offline.kickoff', event));
|
|
290
|
+
this.on('system.login.qrcode', () => {
|
|
291
|
+
this.#logger.info('icqq 收到二维码登录事件,请按 icqq 默认流程扫码后继续登录');
|
|
292
|
+
});
|
|
293
|
+
this.on('system.login.device', (event) => this.#logSystemEvent('login.device', event));
|
|
294
|
+
this.on('system.login.slider', (event) => this.#logSystemEvent('login.slider', event));
|
|
295
|
+
this.on('system.login.error', (event) => this.#logSystemEvent('login.error', event));
|
|
296
|
+
this.on('system.login.auth', (event) => this.#logSystemEvent('login.auth', event));
|
|
297
|
+
}
|
|
298
|
+
#logSystemEvent(type, event) {
|
|
299
|
+
const payload = serializeIcqqEvent(event);
|
|
300
|
+
this.#logger.warn(formatCompact({
|
|
301
|
+
op: `system.${type}`,
|
|
302
|
+
endpoint: this.endpointName,
|
|
303
|
+
...(payload ? { event: JSON.stringify(payload) } : {}),
|
|
304
|
+
}));
|
|
549
305
|
}
|
|
550
|
-
#
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
this.#recordInboxRequest(payload);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (isIcqqNoticePayload(payload)) {
|
|
563
|
-
this.#recordInboxNotice(payload);
|
|
306
|
+
#handleMessageEvent(eventName, event) {
|
|
307
|
+
const payload = serializeIcqqEvent(event);
|
|
308
|
+
if (!payload || !isIcqqMessagePostType(payload)) {
|
|
309
|
+
this.#logger.debug(formatCompact({
|
|
310
|
+
op: 'icqq_inbound_skip',
|
|
311
|
+
reason: payload ? 'not_message_post' : 'serialize_failed',
|
|
312
|
+
endpoint: this.endpointName,
|
|
313
|
+
event: eventName,
|
|
314
|
+
}));
|
|
564
315
|
return;
|
|
565
316
|
}
|
|
566
|
-
if (!isIcqqMessagePostType(payload))
|
|
567
|
-
return;
|
|
568
317
|
const data = payload;
|
|
569
318
|
if (shouldSkipSelfInboundMessage(data)) {
|
|
570
319
|
this.#logger.debug(formatCompact({
|
|
571
320
|
op: 'icqq_inbound_skip',
|
|
572
321
|
reason: 'self',
|
|
573
|
-
endpoint: this.
|
|
322
|
+
endpoint: this.endpointName,
|
|
574
323
|
user_id: data.user_id,
|
|
575
324
|
self_id: data.self_id,
|
|
576
325
|
}));
|
|
@@ -581,9 +330,9 @@ export class IcqqIpcEndpoint {
|
|
|
581
330
|
this.#logger.debug(formatCompact({
|
|
582
331
|
op: 'icqq_inbound_skip',
|
|
583
332
|
reason: 'normalize_failed',
|
|
584
|
-
endpoint: this.
|
|
333
|
+
endpoint: this.endpointName,
|
|
334
|
+
event: eventName,
|
|
585
335
|
message_id: data.message_id,
|
|
586
|
-
message_type: data.message_type ?? data.type,
|
|
587
336
|
}));
|
|
588
337
|
return;
|
|
589
338
|
}
|
|
@@ -591,7 +340,7 @@ export class IcqqIpcEndpoint {
|
|
|
591
340
|
this.#logger.debug(formatCompact({
|
|
592
341
|
op: 'icqq_inbound_skip',
|
|
593
342
|
reason: 'dedupe',
|
|
594
|
-
endpoint: this.
|
|
343
|
+
endpoint: this.endpointName,
|
|
595
344
|
id: normalized.messageId,
|
|
596
345
|
}));
|
|
597
346
|
return;
|
|
@@ -618,7 +367,7 @@ export class IcqqIpcEndpoint {
|
|
|
618
367
|
});
|
|
619
368
|
}
|
|
620
369
|
#handleGuildEvent(event) {
|
|
621
|
-
const payload = event
|
|
370
|
+
const payload = serializeIcqqEvent(event);
|
|
622
371
|
if (!payload || typeof payload !== 'object')
|
|
623
372
|
return;
|
|
624
373
|
const normalized = normalizeIcqqGuildInboundMessage(payload);
|
|
@@ -626,7 +375,7 @@ export class IcqqIpcEndpoint {
|
|
|
626
375
|
this.#logger.debug(formatCompact({
|
|
627
376
|
op: 'icqq_inbound_skip',
|
|
628
377
|
reason: 'guild_normalize_failed',
|
|
629
|
-
endpoint: this.
|
|
378
|
+
endpoint: this.endpointName,
|
|
630
379
|
}));
|
|
631
380
|
return;
|
|
632
381
|
}
|
|
@@ -635,7 +384,7 @@ export class IcqqIpcEndpoint {
|
|
|
635
384
|
this.#logger.debug(formatCompact({
|
|
636
385
|
op: 'icqq_inbound_skip',
|
|
637
386
|
reason: 'dedupe',
|
|
638
|
-
endpoint: this.
|
|
387
|
+
endpoint: this.endpointName,
|
|
639
388
|
id: `guild:${normalized.messageId}`,
|
|
640
389
|
}));
|
|
641
390
|
return;
|
|
@@ -657,12 +406,9 @@ export class IcqqIpcEndpoint {
|
|
|
657
406
|
},
|
|
658
407
|
});
|
|
659
408
|
}
|
|
660
|
-
/** 收件箱行公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
|
|
661
|
-
#inboxBase() {
|
|
662
|
-
const id = String(this.#options.id);
|
|
663
|
-
return { adapter: id.split('\0').pop() ?? id, endpointKey: this.name };
|
|
664
|
-
}
|
|
665
409
|
#recordInboxRequest(payload) {
|
|
410
|
+
if (!payload || !isIcqqRequestPayload(payload))
|
|
411
|
+
return;
|
|
666
412
|
const hooks = this.#options.inbox;
|
|
667
413
|
if (!hooks)
|
|
668
414
|
return;
|
|
@@ -675,6 +421,8 @@ export class IcqqIpcEndpoint {
|
|
|
675
421
|
hooks.publish?.('endpoint:request', row);
|
|
676
422
|
}
|
|
677
423
|
#recordInboxNotice(payload) {
|
|
424
|
+
if (!payload || !isIcqqNoticePayload(payload))
|
|
425
|
+
return;
|
|
678
426
|
const hooks = this.#options.inbox;
|
|
679
427
|
if (!hooks)
|
|
680
428
|
return;
|
|
@@ -686,24 +434,30 @@ export class IcqqIpcEndpoint {
|
|
|
686
434
|
void hooks.recordNotice(row);
|
|
687
435
|
hooks.publish?.('endpoint:notice', row);
|
|
688
436
|
}
|
|
689
|
-
|
|
437
|
+
#inboxBase() {
|
|
438
|
+
const id = String(this.#options.id);
|
|
439
|
+
return { adapter: id.split('\0').pop() ?? id, endpointKey: this.endpointName };
|
|
440
|
+
}
|
|
690
441
|
async #pullPendingSystemMessages() {
|
|
691
442
|
const hooks = this.#options.inbox;
|
|
692
|
-
if (!hooks
|
|
443
|
+
if (!hooks)
|
|
693
444
|
return;
|
|
694
445
|
try {
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const
|
|
446
|
+
const messages = await this.getSystemMsg();
|
|
447
|
+
const friendRequests = [];
|
|
448
|
+
const groupRequests = [];
|
|
449
|
+
for (const msg of messages) {
|
|
450
|
+
if (msg.request_type === 'friend') {
|
|
451
|
+
friendRequests.push(msg);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
groupRequests.push(msg);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
699
457
|
const base = this.#inboxBase();
|
|
700
458
|
const rows = [
|
|
701
|
-
...
|
|
702
|
-
|
|
703
|
-
: []),
|
|
704
|
-
...(Array.isArray(data?.groupRequests)
|
|
705
|
-
? data.groupRequests.map((m) => buildIcqqSystemRequestRow(m, 'group', base))
|
|
706
|
-
: []),
|
|
459
|
+
...friendRequests.map((m) => buildIcqqSystemRequestRow(m, 'friend', base)),
|
|
460
|
+
...groupRequests.map((m) => buildIcqqSystemRequestRow(m, 'group', base)),
|
|
707
461
|
];
|
|
708
462
|
for (const row of rows) {
|
|
709
463
|
if (!row)
|
|
@@ -717,17 +471,94 @@ export class IcqqIpcEndpoint {
|
|
|
717
471
|
catch (error) {
|
|
718
472
|
this.#logger.debug(formatCompact({
|
|
719
473
|
op: 'inbox_pull_system_msg',
|
|
720
|
-
endpoint: this.
|
|
474
|
+
endpoint: this.endpointName,
|
|
721
475
|
error: error instanceof Error ? error.message : String(error),
|
|
722
476
|
}));
|
|
723
477
|
}
|
|
724
478
|
}
|
|
479
|
+
async #approveRequest(id, remark) {
|
|
480
|
+
const messages = await this.getSystemMsg();
|
|
481
|
+
const target = messages.find((m) => {
|
|
482
|
+
const msg = m;
|
|
483
|
+
return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
|
|
484
|
+
});
|
|
485
|
+
if (!target?.flag) {
|
|
486
|
+
throw new Error(`icqq 未找到待处理请求: ${id}`);
|
|
487
|
+
}
|
|
488
|
+
if (target.request_type === 'friend') {
|
|
489
|
+
await this.setFriendAddRequest(target.flag, true, remark);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
await this.setGroupAddRequest(target.flag, true);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async #rejectRequest(id, reason) {
|
|
496
|
+
const messages = await this.getSystemMsg();
|
|
497
|
+
const target = messages.find((m) => {
|
|
498
|
+
const msg = m;
|
|
499
|
+
return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
|
|
500
|
+
});
|
|
501
|
+
if (!target?.flag) {
|
|
502
|
+
throw new Error(`icqq 未找到待处理请求: ${id}`);
|
|
503
|
+
}
|
|
504
|
+
if (target.request_type === 'friend') {
|
|
505
|
+
await this.setFriendAddRequest(target.flag, false);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
await this.setGroupAddRequest(target.flag, false, reason);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function describeUnknownError(error) {
|
|
513
|
+
if (error instanceof Error)
|
|
514
|
+
return error.stack ?? error.message;
|
|
515
|
+
if (error && typeof error === 'object') {
|
|
516
|
+
try {
|
|
517
|
+
return JSON.stringify(error);
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return inspect(error, { depth: 4, breakLength: 120 });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return String(error);
|
|
524
|
+
}
|
|
525
|
+
function resolveIcqqGroupReactionTarget(message) {
|
|
526
|
+
if (message.conversation.kind !== 'group')
|
|
527
|
+
return null;
|
|
528
|
+
const groupId = Number(message.conversation.id);
|
|
529
|
+
if (!Number.isFinite(groupId) || groupId <= 0)
|
|
530
|
+
return null;
|
|
531
|
+
const raw = message.id?.trim();
|
|
532
|
+
if (!raw || raw.startsWith('outbound:'))
|
|
533
|
+
return null;
|
|
534
|
+
if (/^[1-9]\d*$/.test(raw))
|
|
535
|
+
return { groupId, seq: Number(raw) };
|
|
536
|
+
try {
|
|
537
|
+
const parsed = parseGroupMessageId(raw);
|
|
538
|
+
if (parsed.seq > 0)
|
|
539
|
+
return { groupId: parsed.group_id || groupId, seq: parsed.seq };
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// not a cqhttp group message id
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
function resolveNativeClientConfig(config) {
|
|
547
|
+
return {
|
|
548
|
+
log_level: 'info',
|
|
549
|
+
platform: config.platform,
|
|
550
|
+
...(config.ver ? { ver: config.ver } : {}),
|
|
551
|
+
...(config.dataDir ? { data_dir: config.dataDir } : {}),
|
|
552
|
+
...(config.signApiAddr ? { sign_api_addr: config.signApiAddr } : {}),
|
|
553
|
+
...(config.ignoreSelf != null ? { ignore_self: config.ignoreSelf } : {}),
|
|
554
|
+
...(config.resend != null ? { resend: config.resend } : {}),
|
|
555
|
+
...(config.cacheGroupMember != null ? { cache_group_member: config.cacheGroupMember } : {}),
|
|
556
|
+
...(config.autoServer != null ? { auto_server: config.autoServer } : {}),
|
|
557
|
+
...(config.qqnt != null ? { QQNT: config.qqnt } : {}),
|
|
558
|
+
...(config.ntLogin != null ? { NTLogin: config.ntLogin } : {}),
|
|
559
|
+
reconn_interval: config.autoReconnect ? 5 : 0,
|
|
560
|
+
};
|
|
725
561
|
}
|
|
726
|
-
/**
|
|
727
|
-
* 组装入站 metadata:在基础字段上补充 quote 链路信息。
|
|
728
|
-
* - quote_id:被引用消息 id(resolveIcqqQuoteIdFromEvent,无则不写)
|
|
729
|
-
* - quote_sender_id / quote_sender_name / quote_content:source 可用时的平铺摘要
|
|
730
|
-
*/
|
|
731
562
|
function buildIcqqQuoteMetadata(data, base) {
|
|
732
563
|
const metadata = { ...base };
|
|
733
564
|
const quoteId = resolveIcqqQuoteIdFromEvent(data);
|
|
@@ -751,18 +582,28 @@ function buildIcqqQuoteMetadata(data, base) {
|
|
|
751
582
|
metadata.quote_content = quoteText;
|
|
752
583
|
return metadata;
|
|
753
584
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (!
|
|
758
|
-
|
|
585
|
+
const INBOUND_HOLD_LIMIT = 32;
|
|
586
|
+
const ICQQ_SERIALIZE_SKIP = ['group', 'member', 'friend', 'discuss', 'client'];
|
|
587
|
+
function serializeIcqqEvent(event) {
|
|
588
|
+
if (!event || typeof event !== 'object')
|
|
589
|
+
return null;
|
|
590
|
+
const nativeToJSON = event.toJSON;
|
|
591
|
+
if (typeof nativeToJSON === 'function') {
|
|
592
|
+
try {
|
|
593
|
+
const json = nativeToJSON.call(event, [...ICQQ_SERIALIZE_SKIP]);
|
|
594
|
+
if (json && typeof json === 'object' && !Array.isArray(json)) {
|
|
595
|
+
return json;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
/* JSON.stringify 也会无参调 toJSON,不能当回退 */
|
|
600
|
+
}
|
|
759
601
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
return IpcClient.connectRpc(config.rpc);
|
|
602
|
+
const out = {};
|
|
603
|
+
for (const [key, value] of Object.entries(event)) {
|
|
604
|
+
if (typeof value === 'function' || ICQQ_SERIALIZE_SKIP.includes(key))
|
|
605
|
+
continue;
|
|
606
|
+
out[key] = value;
|
|
766
607
|
}
|
|
767
|
-
return
|
|
608
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
768
609
|
}
|