@zhin.js/adapter-icqq 1.1.17 → 1.1.19
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 +83 -0
- package/README.md +55 -10
- package/adapters/icqq/index.js +3 -19
- package/adapters/icqq/index.ts +2 -25
- package/commands/icqq/endpoint/definition.js +3 -25
- package/commands/icqq/endpoint/definition.ts +3 -24
- package/commands/icqq/login/code/index.js +28 -0
- package/commands/icqq/login/code/index.ts +30 -0
- package/commands/icqq/login/confirm/index.js +20 -0
- package/commands/icqq/login/confirm/index.ts +21 -0
- package/commands/icqq/login/definition.js +65 -0
- package/commands/icqq/login/definition.ts +72 -0
- package/commands/icqq/login/qr/index.js +53 -0
- package/commands/icqq/login/qr/index.ts +55 -0
- package/commands/icqq/login/retry/index.js +21 -0
- package/commands/icqq/login/retry/index.ts +23 -0
- package/commands/icqq/login/status/index.js +31 -0
- package/commands/icqq/login/status/index.ts +34 -0
- package/lib/adapter.d.ts +13 -0
- package/lib/adapter.js +27 -0
- package/lib/client.d.ts +6 -9
- package/lib/client.js +1 -3
- package/lib/console-spec.d.ts +17 -0
- package/lib/console-spec.js +21 -0
- package/lib/console.d.ts +2 -0
- package/lib/console.js +13 -0
- package/lib/endpoint.d.ts +12 -15
- package/lib/endpoint.js +373 -164
- package/lib/group-moderation.d.ts +6 -3
- package/lib/group-moderation.js +9 -8
- package/lib/group-scene.d.ts +3 -3
- package/lib/group-scene.js +8 -7
- package/lib/icqq-login-assist.d.ts +3 -1
- package/lib/icqq-login-assist.js +3 -1
- package/lib/icqq-login-control.d.ts +24 -0
- package/lib/icqq-login-control.js +25 -0
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -2
- package/lib/protocol.d.ts +13 -0
- package/lib/protocol.js +24 -9
- package/package.json +25 -17
- package/pages/index/index.js +15 -0
- package/pages/index/index.ts +16 -0
- package/plugin.js +3 -5
- package/schema.json +174 -0
- package/src/adapter.ts +29 -0
- package/src/client.ts +6 -11
- package/src/console-spec.ts +26 -0
- package/src/console.ts +17 -0
- package/src/endpoint.ts +381 -178
- package/src/group-moderation.ts +10 -10
- package/src/group-scene.ts +8 -8
- package/src/icqq-login-assist.ts +6 -2
- package/src/icqq-login-control.ts +42 -0
- package/src/index.ts +4 -1
- package/src/protocol.ts +35 -8
- package/lib/icqq-runtime-state.d.ts +0 -1
- package/lib/icqq-runtime-state.js +0 -6
- package/src/icqq-runtime-state.ts +0 -7
package/lib/endpoint.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseGroupMessageId, } from '@icqqjs/icqq';
|
|
2
2
|
import { inspect } from 'node:util';
|
|
3
3
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
|
-
import {
|
|
4
|
+
import { createEndpointSpecification, endpointSpec, createEndpointOperation, } from 'zhin.js/adapter';
|
|
5
5
|
import { receiveOneBotLikeSideEvent, buildSystem, toCanonicalSegments } from '@zhin.js/core';
|
|
6
6
|
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
7
7
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
@@ -11,7 +11,7 @@ import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned,
|
|
|
11
11
|
import { icqqNoticeDedupeKey, isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
|
|
12
12
|
import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
|
|
13
13
|
import { prepareIcqqOutboundMedia, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
|
|
14
|
-
import { formatInboundContent,
|
|
14
|
+
import { formatInboundContent, formatOutboundMessageParts, normalizeIcqqImageMedia, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
|
|
15
15
|
import { ManagedIcqqClient } from './managed-client.js';
|
|
16
16
|
import { inspectGroupMember, moderateGroupMember } from './group-moderation.js';
|
|
17
17
|
import { inspectGroupMemberProfile, inspectGroupScene } from './group-scene.js';
|
|
@@ -33,8 +33,8 @@ const BOUND_EVENTS = [
|
|
|
33
33
|
'system.login.error',
|
|
34
34
|
'system.login.auth',
|
|
35
35
|
];
|
|
36
|
-
export class IcqqEndpoint extends
|
|
37
|
-
|
|
36
|
+
export class IcqqEndpoint extends ManagedIcqqClient {
|
|
37
|
+
[endpointSpec];
|
|
38
38
|
#logger;
|
|
39
39
|
#options;
|
|
40
40
|
endpointName;
|
|
@@ -55,63 +55,79 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
55
55
|
#inflightInbound = new Set();
|
|
56
56
|
#inboundOwner = new AsyncLocalStorage();
|
|
57
57
|
#contentResolver;
|
|
58
|
-
content;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
#content;
|
|
59
|
+
loginControl;
|
|
60
|
+
#reloginTimer;
|
|
61
|
+
#reloginAttempts = 0;
|
|
62
|
+
#reloginInFlight;
|
|
63
|
+
#offlineNoticeAt = 0;
|
|
64
|
+
#challengeNoticeAt = 0;
|
|
65
|
+
#management = Object.freeze({
|
|
66
|
+
inspectGroupMember: request => inspectGroupMember(this, request),
|
|
67
|
+
inspectGroupScene: request => inspectGroupScene(this, request),
|
|
68
|
+
inspectGroupMemberProfile: request => inspectGroupMemberProfile(this, request),
|
|
69
|
+
moderateGroupMember: request => moderateGroupMember({ client: this, isActive: () => this.#open && !this.#retiring && !this.#stopped }, request),
|
|
70
|
+
listFriends: async (request = {}) => createEndpointOperation(request).run(() => Array.from(this.fl.values()).map((f) => ({
|
|
65
71
|
user_id: f.user_id,
|
|
66
72
|
nickname: f.nickname,
|
|
67
73
|
remark: f.remark ?? '',
|
|
68
|
-
})),
|
|
69
|
-
listGroups: async () => Array.from(this.
|
|
74
|
+
}))),
|
|
75
|
+
listGroups: async (request = {}) => createEndpointOperation(request).run(() => Array.from(this.gl.values()).map((g) => ({
|
|
70
76
|
group_id: g.group_id,
|
|
71
77
|
name: g.group_name,
|
|
72
|
-
})),
|
|
73
|
-
listChannels: async () => {
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
}))),
|
|
79
|
+
listChannels: async (request = {}) => {
|
|
80
|
+
const operation = createEndpointOperation(request);
|
|
81
|
+
await operation.run(() => this.#guildCatalog.syncAll(this));
|
|
82
|
+
return operation.run(() => this.#guildCatalog.getGuildChannelList());
|
|
76
83
|
},
|
|
77
|
-
listGroupMembers: async (groupId) => Array.from((await this.
|
|
78
|
-
listRequests: () => this.#listPendingRequests(),
|
|
79
|
-
approveRequest:
|
|
80
|
-
rejectRequest:
|
|
81
|
-
kickGroupMember: async (groupId, userId) => {
|
|
82
|
-
|
|
84
|
+
listGroupMembers: async ({ groupId, signal }) => Array.from((await createEndpointOperation({ signal }).run(() => this.getGroupMemberList(Number(groupId)))).values()),
|
|
85
|
+
listRequests: (request = {}) => createEndpointOperation(request).run(() => this.#listPendingRequests()),
|
|
86
|
+
approveRequest: request => this.#approveRequest(request),
|
|
87
|
+
rejectRequest: request => this.#rejectRequest(request),
|
|
88
|
+
kickGroupMember: async ({ groupId, userId, signal }) => {
|
|
89
|
+
const operation = createEndpointOperation({ signal });
|
|
90
|
+
await operation.run(() => this.setGroupKick(Number(groupId), Number(userId)));
|
|
83
91
|
},
|
|
84
|
-
muteGroupMember: async (groupId, userId, duration) => {
|
|
85
|
-
|
|
92
|
+
muteGroupMember: async ({ groupId, userId, durationSeconds: duration, signal }) => {
|
|
93
|
+
const operation = createEndpointOperation({ signal });
|
|
94
|
+
await operation.run(() => this.setGroupBan(Number(groupId), Number(userId), duration));
|
|
86
95
|
},
|
|
87
|
-
setGroupAdmin: async (groupId, userId, enabled) => {
|
|
88
|
-
|
|
96
|
+
setGroupAdmin: async ({ groupId, userId, enabled, signal }) => {
|
|
97
|
+
const operation = createEndpointOperation({ signal });
|
|
98
|
+
await operation.run(() => this.setGroupAdmin(Number(groupId), Number(userId), enabled));
|
|
89
99
|
},
|
|
90
|
-
deleteFriend: async (userId) => {
|
|
91
|
-
|
|
100
|
+
deleteFriend: async ({ userId, signal }) => {
|
|
101
|
+
const operation = createEndpointOperation({ signal });
|
|
102
|
+
await operation.run(() => this.deleteFriend(Number(userId)));
|
|
92
103
|
},
|
|
93
|
-
sendGroupNotice: async (groupId, content) => {
|
|
94
|
-
|
|
104
|
+
sendGroupNotice: async ({ groupId, content, signal }) => {
|
|
105
|
+
const operation = createEndpointOperation({ signal });
|
|
106
|
+
if (await operation.run(() => this.sendGroupNotice(Number(groupId), content)) === false) {
|
|
95
107
|
throw new Error('QQ did not confirm the group notice');
|
|
96
108
|
}
|
|
97
109
|
},
|
|
98
110
|
});
|
|
99
|
-
control = Object.freeze({
|
|
100
|
-
recall: async (message) => {
|
|
111
|
+
#control = Object.freeze({
|
|
112
|
+
recall: async ({ message, signal }) => {
|
|
101
113
|
if (!message.id || message.id.startsWith('outbound:'))
|
|
102
114
|
return;
|
|
103
|
-
if (!await this.
|
|
115
|
+
if (!await createEndpointOperation({ signal }).run(() => this.deleteMsg(message.id)))
|
|
104
116
|
throw new Error('QQ did not confirm message recall');
|
|
105
117
|
},
|
|
106
|
-
addReaction: async (message, emoji) => {
|
|
118
|
+
addReaction: async ({ message, emoji, signal }) => {
|
|
107
119
|
const target = resolveIcqqGroupReactionTarget(message);
|
|
108
120
|
if (!target)
|
|
109
121
|
return null;
|
|
110
122
|
try {
|
|
111
123
|
// Do not await protocol ACK — packet timeout must not stall the AI reply.
|
|
112
|
-
|
|
124
|
+
await createEndpointOperation({ signal }).run(() => {
|
|
125
|
+
this.#ignoreReactionAck('add', this.pickGroup(target.groupId).setReaction(target.seq, emoji));
|
|
126
|
+
});
|
|
113
127
|
}
|
|
114
128
|
catch (error) {
|
|
129
|
+
if (error instanceof EndpointDeliveryError && error.code === 'endpoint_inactive')
|
|
130
|
+
throw error;
|
|
115
131
|
this.#logger.debug(formatCompact({
|
|
116
132
|
op: 'reaction_add_failed',
|
|
117
133
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -119,14 +135,18 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
119
135
|
}
|
|
120
136
|
return emoji;
|
|
121
137
|
},
|
|
122
|
-
removeReaction: async (message, reactionId) => {
|
|
138
|
+
removeReaction: async ({ message, reactionId, signal }) => {
|
|
123
139
|
const target = resolveIcqqGroupReactionTarget(message);
|
|
124
140
|
if (!target)
|
|
125
141
|
return;
|
|
126
142
|
try {
|
|
127
|
-
|
|
143
|
+
await createEndpointOperation({ signal }).run(() => {
|
|
144
|
+
this.#ignoreReactionAck('remove', this.pickGroup(target.groupId).delReaction(target.seq, reactionId));
|
|
145
|
+
});
|
|
128
146
|
}
|
|
129
147
|
catch (error) {
|
|
148
|
+
if (error instanceof EndpointDeliveryError && error.code === 'endpoint_inactive')
|
|
149
|
+
throw error;
|
|
130
150
|
this.#logger.debug(formatCompact({
|
|
131
151
|
op: 'reaction_remove_failed',
|
|
132
152
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -135,14 +155,35 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
135
155
|
},
|
|
136
156
|
});
|
|
137
157
|
constructor(options) {
|
|
138
|
-
super();
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this
|
|
158
|
+
super(Number(options.config.id), resolveNativeClientConfig(options.config), (operation, error) => this.#handleBackgroundFailure(operation, error));
|
|
159
|
+
const name = () => this.endpointName;
|
|
160
|
+
const state = () => this.#transportState;
|
|
161
|
+
const content = () => this.#content;
|
|
162
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
163
|
+
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(), stop: () => this.#stopFramework(),
|
|
164
|
+
send: request => this.#sendFramework(request), get name() { return name(); },
|
|
165
|
+
get transportState() { return state(); },
|
|
166
|
+
management: this.#management, control: this.#control,
|
|
167
|
+
get content() { return content(); },
|
|
168
|
+
});
|
|
169
|
+
this.#contentResolver = new IcqqContentResolver(this);
|
|
170
|
+
this.#content = this.#contentResolver.port;
|
|
143
171
|
this.#logger = getAdapterLogger('icqq', options.config.id);
|
|
144
172
|
this.#options = options;
|
|
145
173
|
this.endpointName = options.config.id;
|
|
174
|
+
this.loginControl = Object.freeze({
|
|
175
|
+
endpointId: this.endpointName,
|
|
176
|
+
state: () => ({
|
|
177
|
+
endpointId: this.endpointName,
|
|
178
|
+
transportState: this.#transportState,
|
|
179
|
+
online: this.isOnline(),
|
|
180
|
+
pending: this.#pendingLoginTasks(),
|
|
181
|
+
}),
|
|
182
|
+
pending: () => this.#pendingLoginTasks(),
|
|
183
|
+
relogin: async () => { this.#options.signal?.throwIfAborted(); await this.relogin(); },
|
|
184
|
+
submit: (taskId, value) => !this.#options.signal?.aborted && this.#options.loginAssist.submit(taskId, value, this.#options.endpointIdentity),
|
|
185
|
+
cancel: (taskId, reason) => !this.#options.signal?.aborted && this.#options.loginAssist.cancel(taskId, reason, this.#options.endpointIdentity),
|
|
186
|
+
});
|
|
146
187
|
}
|
|
147
188
|
#handleBackgroundFailure(operation, error) {
|
|
148
189
|
if (!this.#started || this.#stopped || this.#retiring)
|
|
@@ -154,13 +195,162 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
154
195
|
: typeof error === 'string' ? error : 'Unknown ICQQ SDK background failure',
|
|
155
196
|
...(error && typeof error === 'object' && 'code' in error && typeof error.code === 'number' ? { code: error.code } : {}),
|
|
156
197
|
}));
|
|
157
|
-
if (operation === 'sso-heartbeat' && !this.
|
|
198
|
+
if (operation === 'sso-heartbeat' && !this.isOnline())
|
|
158
199
|
return;
|
|
159
200
|
if (operation === 'login' && !this.#everOnline)
|
|
160
201
|
return;
|
|
161
202
|
// Delegate to the SDK's existing transport termination, offline event and
|
|
162
203
|
// reconn_interval policy. No account-level logout or second retry timer.
|
|
163
|
-
this.
|
|
204
|
+
this.emit('internal.error.network', -2, `ICQQ ${operation} failed`);
|
|
205
|
+
}
|
|
206
|
+
/** Force a login attempt (manual recovery command). */
|
|
207
|
+
relogin() {
|
|
208
|
+
return this.#startRelogin(true);
|
|
209
|
+
}
|
|
210
|
+
#startRelogin(resetAttempts) {
|
|
211
|
+
if (this.#stopped || this.#retiring) {
|
|
212
|
+
return Promise.reject(new Error('ICQQ endpoint 已停止'));
|
|
213
|
+
}
|
|
214
|
+
if (this.#reloginInFlight)
|
|
215
|
+
return this.#reloginInFlight;
|
|
216
|
+
if (resetAttempts)
|
|
217
|
+
this.#reloginAttempts = 0;
|
|
218
|
+
const task = this.#performRelogin();
|
|
219
|
+
this.#reloginInFlight = task;
|
|
220
|
+
void task.catch(() => undefined).finally(() => {
|
|
221
|
+
if (this.#reloginInFlight === task)
|
|
222
|
+
this.#reloginInFlight = undefined;
|
|
223
|
+
if (this.#started && !this.#stopped && !this.#retiring && !this.isOnline()) {
|
|
224
|
+
this.#scheduleRelogin('relogin_failed');
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
return task;
|
|
228
|
+
}
|
|
229
|
+
async #performRelogin() {
|
|
230
|
+
this.#clearReloginTimer();
|
|
231
|
+
this.cancelReconnect();
|
|
232
|
+
this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName }));
|
|
233
|
+
try {
|
|
234
|
+
await this.login(this.#options.config.password);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
this.#logger.warn(formatCompact({
|
|
238
|
+
op: 'icqq_relogin_failed',
|
|
239
|
+
endpoint: this.endpointName,
|
|
240
|
+
error: error instanceof Error ? error.message : String(error),
|
|
241
|
+
}));
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
#scheduleRelogin(reason) {
|
|
246
|
+
if (!this.#options.config.autoReconnect || this.#stopped || this.#retiring)
|
|
247
|
+
return;
|
|
248
|
+
if (this.#reloginTimer || this.#reloginInFlight)
|
|
249
|
+
return;
|
|
250
|
+
if (this.#reloginAttempts >= 8)
|
|
251
|
+
return;
|
|
252
|
+
const attempt = this.#reloginAttempts++;
|
|
253
|
+
const delayMs = Math.min(300_000, 5_000 * (2 ** Math.min(attempt, 5)));
|
|
254
|
+
this.#logger.info(formatCompact({
|
|
255
|
+
op: 'icqq_relogin_scheduled',
|
|
256
|
+
endpoint: this.endpointName,
|
|
257
|
+
reason,
|
|
258
|
+
attempt: attempt + 1,
|
|
259
|
+
delay_ms: delayMs,
|
|
260
|
+
}));
|
|
261
|
+
this.#reloginTimer = setTimeout(() => {
|
|
262
|
+
this.#reloginTimer = undefined;
|
|
263
|
+
void this.#startRelogin(false).catch(() => {
|
|
264
|
+
// #performRelogin already scheduled the next attempt.
|
|
265
|
+
});
|
|
266
|
+
}, delayMs);
|
|
267
|
+
this.#reloginTimer.unref?.();
|
|
268
|
+
}
|
|
269
|
+
#clearReloginTimer() {
|
|
270
|
+
if (!this.#reloginTimer)
|
|
271
|
+
return;
|
|
272
|
+
clearTimeout(this.#reloginTimer);
|
|
273
|
+
this.#reloginTimer = undefined;
|
|
274
|
+
}
|
|
275
|
+
#pendingLoginTasks() {
|
|
276
|
+
if (this.#options.signal?.aborted)
|
|
277
|
+
return [];
|
|
278
|
+
return this.#options.loginAssist.listPending()
|
|
279
|
+
.filter((task) => task.adapter === 'icqq' && (this.#options.endpointIdentity
|
|
280
|
+
? task.endpointIdentity?.canonicalId === this.#options.endpointIdentity.canonicalId
|
|
281
|
+
&& task.endpointIdentity.instanceId === this.#options.endpointIdentity.instanceId
|
|
282
|
+
&& task.endpointIdentity.epoch === this.#options.endpointIdentity.epoch
|
|
283
|
+
&& task.endpointIdentity.incarnationId === this.#options.endpointIdentity.incarnationId
|
|
284
|
+
: task.endpointKey === this.endpointName));
|
|
285
|
+
}
|
|
286
|
+
#notifyOffline(reason, willRetry) {
|
|
287
|
+
const notifier = this.#options.notifier;
|
|
288
|
+
if (!notifier?.configured)
|
|
289
|
+
return;
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
if (now - this.#offlineNoticeAt < 60_000)
|
|
292
|
+
return;
|
|
293
|
+
this.#offlineNoticeAt = now;
|
|
294
|
+
const recovery = willRetry
|
|
295
|
+
? '已开始自动重新登录。'
|
|
296
|
+
: '请手动重新登录。';
|
|
297
|
+
const qrCommand = `icqq login qr ${this.endpointName}`;
|
|
298
|
+
const confirmCommand = `icqq login confirm ${this.endpointName}`;
|
|
299
|
+
const codeCommand = `icqq login code ${this.endpointName} <内容>`;
|
|
300
|
+
const text = `[Zhin] ICQQ ${this.endpointName} 掉线(${reason})。${recovery}\n`
|
|
301
|
+
+ `恢复:在 QQ 回复 \`${qrCommand}\` 获取二维码,扫码后回复 \`${confirmCommand}\`;`
|
|
302
|
+
+ `滑块/短信验证请回复 \`${codeCommand}\`。也可以打开 Console 的 ICQQ 页面完成登录。`;
|
|
303
|
+
void notifier.send(text).then((sent) => {
|
|
304
|
+
if (sent)
|
|
305
|
+
return;
|
|
306
|
+
this.#logger.warn(formatCompact({
|
|
307
|
+
op: 'icqq_notify_failed',
|
|
308
|
+
endpoint: this.endpointName,
|
|
309
|
+
reason,
|
|
310
|
+
}));
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
#notifyChallenge(step) {
|
|
314
|
+
const notifier = this.#options.notifier;
|
|
315
|
+
if (!notifier?.configured)
|
|
316
|
+
return;
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
if (now - this.#challengeNoticeAt < 30_000)
|
|
319
|
+
return;
|
|
320
|
+
this.#challengeNoticeAt = now;
|
|
321
|
+
const label = step === 'qrcode' ? '二维码验证'
|
|
322
|
+
: step === 'slider' ? '滑块验证'
|
|
323
|
+
: step === 'device' ? '设备确认'
|
|
324
|
+
: '登录确认';
|
|
325
|
+
const qrCommand = `icqq login qr ${this.endpointName}`;
|
|
326
|
+
const confirmCommand = `icqq login confirm ${this.endpointName}`;
|
|
327
|
+
const codeCommand = `icqq login code ${this.endpointName} <内容>`;
|
|
328
|
+
const text = `[Zhin] ICQQ ${this.endpointName} 需要${label}。\n`
|
|
329
|
+
+ (step === 'qrcode' || step === 'auth'
|
|
330
|
+
? `在 QQ 回复 \`${qrCommand}\` 获取二维码,完成后回复 \`${confirmCommand}\`。`
|
|
331
|
+
: `在 QQ 回复 \`icqq login status ${this.endpointName}\` 查看验证链接,再用 \`${codeCommand}\` 提交验证码;也可以打开 Console 的 ICQQ 页面。`);
|
|
332
|
+
void notifier.send(text).then((sent) => {
|
|
333
|
+
if (sent)
|
|
334
|
+
return;
|
|
335
|
+
this.#logger.warn(formatCompact({
|
|
336
|
+
op: 'icqq_notify_failed',
|
|
337
|
+
endpoint: this.endpointName,
|
|
338
|
+
reason: `login_${step}`,
|
|
339
|
+
}));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
#notifyRecovered() {
|
|
343
|
+
const notifier = this.#options.notifier;
|
|
344
|
+
const hadOfflineNotice = this.#offlineNoticeAt > 0;
|
|
345
|
+
this.#offlineNoticeAt = 0;
|
|
346
|
+
this.#challengeNoticeAt = 0;
|
|
347
|
+
if (!notifier?.configured || !hadOfflineNotice)
|
|
348
|
+
return;
|
|
349
|
+
void notifier.send(`[Zhin] ICQQ ${this.endpointName} 已重新上线。`).then((sent) => {
|
|
350
|
+
if (sent)
|
|
351
|
+
return;
|
|
352
|
+
this.#logger.warn(formatCompact({ op: 'icqq_notify_failed', endpoint: this.endpointName, reason: 'recovered' }));
|
|
353
|
+
});
|
|
164
354
|
}
|
|
165
355
|
#ignoreReactionAck(action, task) {
|
|
166
356
|
void Promise.resolve(task).catch((error) => {
|
|
@@ -170,18 +360,18 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
170
360
|
}));
|
|
171
361
|
});
|
|
172
362
|
}
|
|
173
|
-
get transportState() {
|
|
363
|
+
get #transportState() {
|
|
174
364
|
if (this.#stopped || this.#retiring)
|
|
175
365
|
return 'stopped';
|
|
176
366
|
if (!this.#started)
|
|
177
367
|
return 'idle';
|
|
178
|
-
if (this.
|
|
368
|
+
if (this.isOnline())
|
|
179
369
|
return 'open';
|
|
180
370
|
if (!this.#everOnline)
|
|
181
371
|
return 'connecting';
|
|
182
372
|
return this.#options.config.autoReconnect ? 'reconnecting' : 'closed';
|
|
183
373
|
}
|
|
184
|
-
async
|
|
374
|
+
async #startFramework(signal) {
|
|
185
375
|
if (this.#started)
|
|
186
376
|
return;
|
|
187
377
|
signal.throwIfAborted();
|
|
@@ -191,7 +381,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
191
381
|
this.#retirementEpoch += 1;
|
|
192
382
|
this.#retirementArmed = false;
|
|
193
383
|
this.#retiring = false;
|
|
194
|
-
this.
|
|
384
|
+
this.off('system.online');
|
|
195
385
|
this.#started = true;
|
|
196
386
|
let resolveOnline;
|
|
197
387
|
const onlineReady = new Promise((resolve) => {
|
|
@@ -202,15 +392,16 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
202
392
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
203
393
|
try {
|
|
204
394
|
this.#bindClientEvents();
|
|
205
|
-
await raceAbort(Promise.resolve(this.
|
|
395
|
+
await raceAbort(Promise.resolve(this.login(this.#options.config.password)), signal);
|
|
206
396
|
await onlineReady;
|
|
207
397
|
signal.throwIfAborted();
|
|
208
398
|
await this.#pullPendingSystemMessages(signal);
|
|
209
399
|
signal.throwIfAborted();
|
|
210
|
-
this.#logger.info(`connected (direct) | friends: ${this.
|
|
400
|
+
this.#logger.info(`connected (direct) | friends: ${this.fl.size} | groups: ${this.gl.size}`);
|
|
211
401
|
}
|
|
212
402
|
catch (error) {
|
|
213
|
-
|
|
403
|
+
this.#notifyOffline('start_failed', false);
|
|
404
|
+
await this.#stopFramework();
|
|
214
405
|
this.#logger.debug(`Failed to connect ICQQ client: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
215
406
|
throw error;
|
|
216
407
|
}
|
|
@@ -220,11 +411,11 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
220
411
|
this.#resolveStartOnline = undefined;
|
|
221
412
|
}
|
|
222
413
|
}
|
|
223
|
-
|
|
414
|
+
#openFramework() {
|
|
224
415
|
if (this.#retirementArmed) {
|
|
225
416
|
this.#retirementEpoch += 1;
|
|
226
417
|
this.#retirementArmed = false;
|
|
227
|
-
this.
|
|
418
|
+
this.off('system.online');
|
|
228
419
|
this.#bindSystemOnlineEvent();
|
|
229
420
|
}
|
|
230
421
|
this.#retiring = false;
|
|
@@ -236,14 +427,15 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
236
427
|
});
|
|
237
428
|
}
|
|
238
429
|
}
|
|
239
|
-
async
|
|
430
|
+
async #closeFramework() {
|
|
240
431
|
this.#assertExternalLifecycle('close');
|
|
241
432
|
this.#armRetirementFence();
|
|
242
433
|
await Promise.allSettled([...this.#inflightInbound]);
|
|
243
434
|
}
|
|
244
|
-
async
|
|
245
|
-
this.#stopped = true;
|
|
435
|
+
async #stopFramework() {
|
|
246
436
|
this.#assertExternalLifecycle('stop');
|
|
437
|
+
this.#stopped = true;
|
|
438
|
+
this.#clearReloginTimer();
|
|
247
439
|
this.#armRetirementFence();
|
|
248
440
|
await Promise.allSettled([...this.#inflightInbound]);
|
|
249
441
|
this.#resolveStartOnline?.();
|
|
@@ -255,7 +447,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
255
447
|
// The retirement guard owns this matcher until a late login handshake
|
|
256
448
|
// has finished. start()/open() replace it with the normal listener.
|
|
257
449
|
if (event !== 'system.online')
|
|
258
|
-
this.
|
|
450
|
+
this.off(event);
|
|
259
451
|
}
|
|
260
452
|
// `Client.logout()` sends an account-level unregister packet before it
|
|
261
453
|
// closes this transport. During config HMR the replacement endpoint for
|
|
@@ -263,7 +455,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
263
455
|
// also closes the replacement session. Endpoint ownership is transport
|
|
264
456
|
// scoped: retire only this Client's TCP connection.
|
|
265
457
|
try {
|
|
266
|
-
this.
|
|
458
|
+
this.terminate();
|
|
267
459
|
}
|
|
268
460
|
catch { /* ignore */ }
|
|
269
461
|
this.#inboundDeduper.clear();
|
|
@@ -281,38 +473,49 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
281
473
|
const retirementEpoch = ++this.#retirementEpoch;
|
|
282
474
|
// ICQQ owns reconnect through this per-Client timer. Clear it before any
|
|
283
475
|
// asynchronous drain so a queued callback cannot enter while close waits.
|
|
284
|
-
this.
|
|
476
|
+
this.cancelReconnect();
|
|
285
477
|
// A login callback may already be inside the SDK handshake, which has no
|
|
286
478
|
// abort API. Replace the normal online listener with an epoch-owned guard
|
|
287
479
|
// before draining inbound work. Transport termination is account-local;
|
|
288
480
|
// logout would also unregister an HMR replacement using the same UIN.
|
|
289
|
-
this.
|
|
290
|
-
this.
|
|
481
|
+
this.off('system.online');
|
|
482
|
+
this.on('system.online', () => {
|
|
291
483
|
if (this.#retirementEpoch !== retirementEpoch || !this.#retiring)
|
|
292
484
|
return;
|
|
293
485
|
try {
|
|
294
|
-
this.
|
|
486
|
+
this.terminate();
|
|
295
487
|
}
|
|
296
488
|
catch { /* already closed */ }
|
|
297
489
|
});
|
|
298
490
|
}
|
|
299
|
-
async
|
|
491
|
+
async #sendFramework({ conversation, payload, signal }) {
|
|
300
492
|
if (!this.#started)
|
|
301
493
|
throw new EndpointDeliveryError('endpoint_disconnected', 'icqq endpoint 未连接', 'not_sent');
|
|
494
|
+
const operation = createEndpointOperation({ signal });
|
|
302
495
|
const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
|
|
303
|
-
const prepared = isIcqqSegmentPayload(payload)
|
|
496
|
+
const prepared = await operation.prepare(() => isIcqqSegmentPayload(payload)
|
|
304
497
|
? prepareIcqqOutboundMedia(normalizeIcqqImageMedia(payload), mediaMode)
|
|
305
|
-
: { content: payload, dispose() { } };
|
|
498
|
+
: { content: payload, dispose() { } });
|
|
499
|
+
// Cancellation ends our wait, not the SDK's use of temporary media. The actual native
|
|
500
|
+
// promises own that resource until they settle, including a late rejection after cancellation.
|
|
501
|
+
const pendingSends = new Set();
|
|
502
|
+
const sendNative = (target, message) => {
|
|
503
|
+
const task = this.#sendNative(target, message);
|
|
504
|
+
pendingSends.add(task);
|
|
505
|
+
const settled = () => { pendingSends.delete(task); };
|
|
506
|
+
void task.then(settled, settled);
|
|
507
|
+
return task;
|
|
508
|
+
};
|
|
306
509
|
try {
|
|
307
510
|
// Standalone elements (share / file / …) are split into ordered messages.
|
|
308
|
-
const messages =
|
|
511
|
+
const messages = formatOutboundMessageParts(prepared.content);
|
|
309
512
|
const parsed = icqqOutboundTarget(conversation);
|
|
310
|
-
|
|
311
|
-
for (const [index, message] of messages.entries()) {
|
|
513
|
+
const parts = [];
|
|
514
|
+
for (const [index, { message, sourceIndices }] of messages.entries()) {
|
|
312
515
|
const preview = formatIcqqSendablePreview(message);
|
|
313
516
|
let result;
|
|
314
517
|
try {
|
|
315
|
-
result = await
|
|
518
|
+
result = await operation.run(() => sendNative(parsed, message));
|
|
316
519
|
}
|
|
317
520
|
catch (error) {
|
|
318
521
|
this.#logger.warn(formatCompact({
|
|
@@ -324,66 +527,55 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
324
527
|
}));
|
|
325
528
|
if (index === 0)
|
|
326
529
|
throw error;
|
|
327
|
-
// Earlier
|
|
328
|
-
throw new EndpointDeliveryError('partial_delivery', `ICQQ
|
|
530
|
+
// Earlier sends completed; retrying can duplicate accepted messages, even without ids.
|
|
531
|
+
throw new EndpointDeliveryError('partial_delivery', `ICQQ completed ${index} of ${messages.length} message sends`, 'unknown', { cause: error, parts });
|
|
329
532
|
}
|
|
330
533
|
const rawId = result?.message_id;
|
|
331
534
|
const id = (typeof rawId === 'string' || (typeof rawId === 'number' && Number.isFinite(rawId))) && String(rawId).trim() !== ''
|
|
332
535
|
? String(rawId) : undefined;
|
|
333
|
-
messageId
|
|
536
|
+
parts.push({ ...(id ? { messageId: id } : {}), ...(sourceIndices ? { sourceIndices } : {}) });
|
|
334
537
|
this.#logger.info(`send ${conversation.kind}:${conversation.id} | id: ${id ?? '-'} | ${truncatePreview(preview, 80)}`);
|
|
335
538
|
}
|
|
336
|
-
//
|
|
337
|
-
if (!messageId) {
|
|
338
|
-
throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: ICQQ returned no message_id', 'unknown');
|
|
539
|
+
// A resolved share/file call has no message identity; retain that unknown slot in the batch.
|
|
540
|
+
if (!parts.some(part => part.messageId)) {
|
|
541
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: ICQQ returned no message_id', 'unknown', { parts });
|
|
339
542
|
}
|
|
340
|
-
return
|
|
543
|
+
return { parts };
|
|
341
544
|
}
|
|
342
545
|
finally {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
await new Promise(resolve => setTimeout(resolve, wait));
|
|
357
|
-
// A missing or throwing history API must not turn an accepted share into a send failure.
|
|
358
|
-
const messages = await Promise.resolve().then(history).catch(() => []);
|
|
359
|
-
const card = [...messages].reverse().find(item => Number(item.sender?.user_id ?? item.user_id) === self
|
|
360
|
-
&& (item.time ?? 0) >= sentAt - 2
|
|
361
|
-
&& (item.message ?? []).some(element => element.type === 'json' || element.type === 'xml' || element.type === 'share'));
|
|
362
|
-
if (card && card.message_id != null && String(card.message_id).trim())
|
|
363
|
-
return String(card.message_id);
|
|
546
|
+
const disposeMedia = () => {
|
|
547
|
+
try {
|
|
548
|
+
prepared.dispose();
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
// Temporary-file cleanup cannot change an observed send result.
|
|
552
|
+
this.#logger.warn(formatCompact({ op: 'icqq_media_cleanup_failed', error: describeUnknownError(error) }));
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
if (pendingSends.size)
|
|
556
|
+
void Promise.allSettled([...pendingSends]).then(disposeMedia);
|
|
557
|
+
else
|
|
558
|
+
disposeMedia();
|
|
364
559
|
}
|
|
365
|
-
return undefined;
|
|
366
560
|
}
|
|
367
561
|
async #sendNative(target, message) {
|
|
368
562
|
if (isIcqqShareElement(message)) {
|
|
369
563
|
switch (target.kind) {
|
|
370
564
|
case 'private': {
|
|
371
|
-
const
|
|
372
|
-
const friend = this.client.pickFriend(target.userId);
|
|
565
|
+
const friend = this.pickFriend(target.userId);
|
|
373
566
|
await friend.share(message);
|
|
374
|
-
return {
|
|
567
|
+
return {};
|
|
375
568
|
}
|
|
376
569
|
case 'group': {
|
|
377
|
-
const
|
|
378
|
-
const group = this.client.pickGroup(target.groupId);
|
|
570
|
+
const group = this.pickGroup(target.groupId);
|
|
379
571
|
await group.share(message);
|
|
380
|
-
return {
|
|
572
|
+
return {};
|
|
381
573
|
}
|
|
382
574
|
case 'temp':
|
|
383
|
-
await this.
|
|
575
|
+
await this.pickMember(target.groupId, target.userId).share(message);
|
|
384
576
|
return {};
|
|
385
577
|
case 'channel': {
|
|
386
|
-
const channel = this.
|
|
578
|
+
const channel = this.pickGuild(target.guildId).channels.get(target.channelId);
|
|
387
579
|
if (!channel) {
|
|
388
580
|
throw new TypeError(`ICQQ guild channel is unavailable: ${target.guildId}/${target.channelId}`);
|
|
389
581
|
}
|
|
@@ -395,11 +587,11 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
395
587
|
if (isIcqqFileElement(message)) {
|
|
396
588
|
switch (target.kind) {
|
|
397
589
|
case 'private': {
|
|
398
|
-
await this.
|
|
590
|
+
await this.pickFriend(target.userId).sendFile(message.file, message.name);
|
|
399
591
|
return {};
|
|
400
592
|
}
|
|
401
593
|
case 'group': {
|
|
402
|
-
await this.
|
|
594
|
+
await this.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
|
|
403
595
|
return {};
|
|
404
596
|
}
|
|
405
597
|
case 'temp':
|
|
@@ -409,13 +601,13 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
409
601
|
}
|
|
410
602
|
switch (target.kind) {
|
|
411
603
|
case 'private':
|
|
412
|
-
return this.
|
|
604
|
+
return this.sendPrivateMsg(target.userId, message);
|
|
413
605
|
case 'group':
|
|
414
|
-
return this.
|
|
606
|
+
return this.sendGroupMsg(target.groupId, message);
|
|
415
607
|
case 'temp':
|
|
416
|
-
return this.
|
|
608
|
+
return this.sendTempMsg(target.groupId, target.userId, message);
|
|
417
609
|
case 'channel':
|
|
418
|
-
return this.
|
|
610
|
+
return this.sendGuildMsg(target.guildId, target.channelId, message);
|
|
419
611
|
}
|
|
420
612
|
}
|
|
421
613
|
admit(msg) {
|
|
@@ -428,7 +620,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
428
620
|
this.#inflightInbound.add(settlement);
|
|
429
621
|
let operation;
|
|
430
622
|
try {
|
|
431
|
-
operation = this.#inboundOwner.run(owner, run);
|
|
623
|
+
operation = this.#inboundOwner.run(owner, () => this[endpointSpec].events.run(run));
|
|
432
624
|
}
|
|
433
625
|
catch (error) {
|
|
434
626
|
this.#inflightInbound.delete(settlement);
|
|
@@ -477,7 +669,9 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
477
669
|
+ (msg.sender ? ` from ${msg.sender.name ?? msg.sender.id}` : '')
|
|
478
670
|
+ (mentioned ? ' (mentioned)' : '')
|
|
479
671
|
+ ` | ${truncatePreview(msg.content, 80)}`);
|
|
480
|
-
await this.emit('message.receive', {
|
|
672
|
+
await this[endpointSpec].events.emit('message.receive', {
|
|
673
|
+
event: msg.event,
|
|
674
|
+
platformSentAt: Number.isFinite(msg.platformSentAt) ? msg.platformSentAt : undefined,
|
|
481
675
|
conversation,
|
|
482
676
|
message: { conversation, id: msg.id },
|
|
483
677
|
content: msg.content,
|
|
@@ -503,80 +697,81 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
503
697
|
#bindClientEvents() {
|
|
504
698
|
this.#bindNativeEvents();
|
|
505
699
|
const safe = (label, fn) => {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
catch (error) {
|
|
510
|
-
this.#logger.warn(formatCompact({
|
|
511
|
-
op: 'icqq_event_handler',
|
|
512
|
-
endpoint: this.endpointName,
|
|
513
|
-
event: label,
|
|
514
|
-
error: error instanceof Error ? error.message : String(error),
|
|
515
|
-
}));
|
|
516
|
-
}
|
|
700
|
+
void this[endpointSpec].events.run(fn).catch(() => {
|
|
701
|
+
this.#logger.debug(formatCompact({ op: 'icqq_event_not_admitted', endpoint: this.endpointName, event: label }));
|
|
702
|
+
});
|
|
517
703
|
};
|
|
518
|
-
this.
|
|
704
|
+
this.on('message.private', (event) => {
|
|
519
705
|
this.#startMessageEvent('message.private', event);
|
|
520
706
|
});
|
|
521
|
-
this.
|
|
707
|
+
this.on('message.group', (event) => {
|
|
522
708
|
this.#startMessageEvent('message.group', event);
|
|
523
709
|
});
|
|
524
|
-
this.
|
|
525
|
-
void this.#handleGuildEvent(event).catch((error) => {
|
|
710
|
+
this.on('message.guild', (event) => {
|
|
711
|
+
void this.#trackInbound(() => this.#handleGuildEvent(event)).catch((error) => {
|
|
526
712
|
this.#logInboundHandlerFailure('message.guild', error);
|
|
527
713
|
});
|
|
528
714
|
});
|
|
529
|
-
this.
|
|
715
|
+
this.on('request.friend', (event) => safe('request.friend', () => {
|
|
530
716
|
this.#onRequestEvent(serializeIcqqEvent(event));
|
|
531
717
|
}));
|
|
532
|
-
this.
|
|
718
|
+
this.on('request.group', (event) => safe('request.group', () => {
|
|
533
719
|
this.#onRequestEvent(serializeIcqqEvent(event));
|
|
534
720
|
}));
|
|
535
|
-
this.
|
|
721
|
+
this.on('notice.friend', (event) => safe('notice.friend', () => {
|
|
536
722
|
this.#onNoticeEvent(serializeIcqqEvent(event));
|
|
537
723
|
}));
|
|
538
|
-
this.
|
|
724
|
+
this.on('notice.group', (event) => safe('notice.group', () => {
|
|
539
725
|
this.#onNoticeEvent(serializeIcqqEvent(event));
|
|
540
726
|
}));
|
|
541
727
|
this.#bindSystemOnlineEvent();
|
|
542
|
-
this.
|
|
728
|
+
this.on('system.offline', (event) => {
|
|
543
729
|
this.#logSystemEvent('offline', event);
|
|
730
|
+
this.#notifyOffline('offline', this.#options.config.autoReconnect);
|
|
544
731
|
});
|
|
545
|
-
this.
|
|
732
|
+
this.on('system.offline.network', (event) => {
|
|
546
733
|
this.#logSystemEvent('offline.network', event);
|
|
734
|
+
this.#notifyOffline('network', this.#options.config.autoReconnect);
|
|
547
735
|
});
|
|
548
|
-
this.
|
|
736
|
+
this.on('system.offline.kickoff', (event) => {
|
|
549
737
|
this.#logSystemEvent('offline.kickoff', event);
|
|
738
|
+
this.#notifyOffline('kickoff', this.#options.config.autoReconnect);
|
|
739
|
+
this.#scheduleRelogin('kickoff');
|
|
550
740
|
});
|
|
551
|
-
this.
|
|
741
|
+
this.on('system.login.qrcode', (event) => {
|
|
552
742
|
this.#logger.info('icqq 收到二维码登录事件,等待 LoginAssist / 终端确认后继续');
|
|
553
743
|
this.#handleLoginChallenge('qrcode', event);
|
|
554
744
|
});
|
|
555
|
-
this.
|
|
745
|
+
this.on('system.login.device', (event) => {
|
|
556
746
|
this.#handleLoginChallenge('device', event);
|
|
557
747
|
});
|
|
558
|
-
this.
|
|
748
|
+
this.on('system.login.slider', (event) => {
|
|
559
749
|
this.#handleLoginChallenge('slider', event);
|
|
560
750
|
});
|
|
561
|
-
this.
|
|
751
|
+
this.on('system.login.error', (event) => {
|
|
562
752
|
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'login_error');
|
|
563
753
|
this.#logSystemEvent('login.error', event);
|
|
754
|
+
this.#notifyOffline('login_error', this.#options.config.autoReconnect);
|
|
755
|
+
this.#scheduleRelogin('login_error');
|
|
564
756
|
});
|
|
565
|
-
this.
|
|
757
|
+
this.on('system.login.auth', (event) => {
|
|
566
758
|
this.#handleLoginChallenge('auth', event);
|
|
567
759
|
});
|
|
568
760
|
}
|
|
569
761
|
#bindSystemOnlineEvent() {
|
|
570
|
-
this.
|
|
762
|
+
this.on('system.online', (event) => {
|
|
571
763
|
if (this.#retiring)
|
|
572
764
|
return;
|
|
573
765
|
this.#everOnline = true;
|
|
766
|
+
this.#clearReloginTimer();
|
|
767
|
+
this.#reloginAttempts = 0;
|
|
768
|
+
this.#notifyRecovered();
|
|
574
769
|
try {
|
|
575
770
|
this.#resolveStartOnline?.();
|
|
576
771
|
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
|
|
577
772
|
try {
|
|
578
773
|
// transport 重连不清除(账号不变);stop/retire 由基类撤销登记。上报失败不影响上线流程。
|
|
579
|
-
this.reportSelfUserId(resolveIcqqSelfUserId(this
|
|
774
|
+
this[endpointSpec].reportSelfUserId(resolveIcqqSelfUserId(this));
|
|
580
775
|
}
|
|
581
776
|
catch (error) {
|
|
582
777
|
this.#logger.warn(formatCompact({
|
|
@@ -601,7 +796,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
601
796
|
#bindNativeEvents() {
|
|
602
797
|
this.#disposeNativeEvents?.();
|
|
603
798
|
let matchedName;
|
|
604
|
-
const registration = this.
|
|
799
|
+
const registration = this.on((eventName) => {
|
|
605
800
|
matchedName = typeof eventName === 'string' && !eventName.startsWith('internal.')
|
|
606
801
|
? eventName
|
|
607
802
|
: undefined;
|
|
@@ -614,7 +809,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
614
809
|
this.#disposeNativeEvents = () => registration();
|
|
615
810
|
}
|
|
616
811
|
#emitNativeEvent(name, event) {
|
|
617
|
-
void this.
|
|
812
|
+
void this[endpointSpec].events.platform(name, event).catch((error) => {
|
|
618
813
|
this.#logger.warn(formatCompact({
|
|
619
814
|
op: 'icqq_platform_event_failed',
|
|
620
815
|
endpoint: this.endpointName,
|
|
@@ -624,17 +819,22 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
624
819
|
});
|
|
625
820
|
}
|
|
626
821
|
#handleLoginChallenge(step, event) {
|
|
822
|
+
if (this.#retiring || this.#stopped || this.#options.signal?.aborted)
|
|
823
|
+
return;
|
|
627
824
|
const payload = serializeIcqqEvent(event) ?? (event && typeof event === 'object'
|
|
628
825
|
? event
|
|
629
826
|
: null);
|
|
630
827
|
this.#logSystemEvent(`login.${step}`, event);
|
|
828
|
+
this.#clearReloginTimer();
|
|
829
|
+
this.#notifyChallenge(step);
|
|
631
830
|
void runIcqqLoginAssistStep({
|
|
632
831
|
assist: this.#options.loginAssist,
|
|
633
832
|
locale: this.#options.locale,
|
|
634
|
-
client: this
|
|
833
|
+
client: this,
|
|
635
834
|
adapter: 'icqq',
|
|
636
835
|
endpointKey: this.endpointName,
|
|
637
836
|
owner: this.#loginAssistOwner,
|
|
837
|
+
signal: this.#options.signal, endpointIdentity: this.#options.endpointIdentity,
|
|
638
838
|
logger: this.#logger,
|
|
639
839
|
step,
|
|
640
840
|
event: payload ?? event,
|
|
@@ -653,14 +853,14 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
653
853
|
const postType = String(payload.post_type ?? '');
|
|
654
854
|
const isRequest = postType === 'request' || postType.startsWith('request.');
|
|
655
855
|
const flag = payload.flag != null ? String(payload.flag) : '';
|
|
656
|
-
void receiveOneBotLikeSideEvent((name, payload) => this.emit(name, payload), {
|
|
856
|
+
void receiveOneBotLikeSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), {
|
|
657
857
|
adapter: 'icqq',
|
|
658
858
|
endpointKey: this.endpointName,
|
|
659
859
|
platform: 'icqq',
|
|
660
860
|
raw: payload,
|
|
661
861
|
...(isRequest ? {
|
|
662
|
-
approve: (id, remark) => this.#approveRequest(flag || id, remark),
|
|
663
|
-
reject: (id, reason) => this.#rejectRequest(flag || id, reason),
|
|
862
|
+
approve: (id, remark) => this.#approveRequest({ requestId: flag || id, remark }),
|
|
863
|
+
reject: (id, reason) => this.#rejectRequest({ requestId: flag || id, reason }),
|
|
664
864
|
} : {}),
|
|
665
865
|
}).catch((err) => {
|
|
666
866
|
this.#logger.warn(formatCompact({
|
|
@@ -672,7 +872,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
672
872
|
});
|
|
673
873
|
}
|
|
674
874
|
#dispatchIcqqSystemSideEvent(type, payload) {
|
|
675
|
-
void this.emit('system.receive', buildSystem(payload ?? {}, {
|
|
875
|
+
void this[endpointSpec].events.emit('system.receive', buildSystem(payload ?? {}, {
|
|
676
876
|
id: `system:${this.endpointName}:${type}:${Date.now()}`,
|
|
677
877
|
clientAdapter: 'icqq',
|
|
678
878
|
endpointId: this.endpointName,
|
|
@@ -764,6 +964,8 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
764
964
|
}));
|
|
765
965
|
}
|
|
766
966
|
await this.#deliverInbound({
|
|
967
|
+
event,
|
|
968
|
+
platformSentAt: normalized.timestampMs,
|
|
767
969
|
id: normalized.messageId,
|
|
768
970
|
conversation,
|
|
769
971
|
content: formatInboundContent(normalized.rawMessage),
|
|
@@ -794,13 +996,13 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
794
996
|
return null;
|
|
795
997
|
if (data.message_type === 'group' || data.group_id != null) {
|
|
796
998
|
const groupId = Number(data.group_id);
|
|
797
|
-
return Number.isFinite(groupId) && groupId > 0 ? this.
|
|
999
|
+
return Number.isFinite(groupId) && groupId > 0 ? this.pickGroup(groupId) : null;
|
|
798
1000
|
}
|
|
799
1001
|
const sourceGroupId = Number(data.sender?.group_id);
|
|
800
1002
|
if (Number.isFinite(sourceGroupId) && sourceGroupId > 0) {
|
|
801
|
-
return this.
|
|
1003
|
+
return this.pickMember(sourceGroupId, userId);
|
|
802
1004
|
}
|
|
803
|
-
return this.
|
|
1005
|
+
return this.pickFriend(userId);
|
|
804
1006
|
}
|
|
805
1007
|
#logInboundHandlerFailure(event, error) {
|
|
806
1008
|
this.#logger.warn(formatCompact({
|
|
@@ -834,6 +1036,9 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
834
1036
|
return;
|
|
835
1037
|
}
|
|
836
1038
|
await this.admit({
|
|
1039
|
+
event,
|
|
1040
|
+
platformSentAt: typeof normalized.raw.time === 'number' && Number.isFinite(normalized.raw.time)
|
|
1041
|
+
? normalized.timestampMs : undefined,
|
|
837
1042
|
id: normalized.messageId,
|
|
838
1043
|
conversation: icqqInboundConversation(String(this.#options.id), {
|
|
839
1044
|
channelType: 'channel',
|
|
@@ -866,7 +1071,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
866
1071
|
this.#dispatchIcqqSideEvent(payload);
|
|
867
1072
|
}
|
|
868
1073
|
async #listPendingRequests() {
|
|
869
|
-
const messages = await this.
|
|
1074
|
+
const messages = await this.getSystemMsg();
|
|
870
1075
|
const rows = [];
|
|
871
1076
|
for (const raw of messages) {
|
|
872
1077
|
const msg = raw;
|
|
@@ -894,7 +1099,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
894
1099
|
}
|
|
895
1100
|
async #pullPendingSystemMessages(signal) {
|
|
896
1101
|
try {
|
|
897
|
-
const messages = await raceAbort(Promise.resolve(this.
|
|
1102
|
+
const messages = await raceAbort(Promise.resolve(this.getSystemMsg()), signal);
|
|
898
1103
|
for (const raw of messages) {
|
|
899
1104
|
signal.throwIfAborted();
|
|
900
1105
|
const msg = raw;
|
|
@@ -924,8 +1129,9 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
924
1129
|
}));
|
|
925
1130
|
}
|
|
926
1131
|
}
|
|
927
|
-
async #approveRequest(id, remark) {
|
|
928
|
-
const
|
|
1132
|
+
async #approveRequest({ requestId: id, remark, signal }) {
|
|
1133
|
+
const operation = createEndpointOperation({ signal });
|
|
1134
|
+
const messages = await operation.run(() => this.getSystemMsg());
|
|
929
1135
|
const target = messages.find((m) => {
|
|
930
1136
|
const msg = m;
|
|
931
1137
|
return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
|
|
@@ -933,15 +1139,17 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
933
1139
|
if (!target?.flag) {
|
|
934
1140
|
throw new Error(`icqq 未找到待处理请求: ${id}`);
|
|
935
1141
|
}
|
|
1142
|
+
const flag = target.flag;
|
|
936
1143
|
if (target.request_type === 'friend') {
|
|
937
|
-
await this.
|
|
1144
|
+
await operation.run(() => this.setFriendAddRequest(flag, true, remark));
|
|
938
1145
|
}
|
|
939
1146
|
else {
|
|
940
|
-
await this.
|
|
1147
|
+
await operation.run(() => this.setGroupAddRequest(flag, true));
|
|
941
1148
|
}
|
|
942
1149
|
}
|
|
943
|
-
async #rejectRequest(id, reason) {
|
|
944
|
-
const
|
|
1150
|
+
async #rejectRequest({ requestId: id, reason, signal }) {
|
|
1151
|
+
const operation = createEndpointOperation({ signal });
|
|
1152
|
+
const messages = await operation.run(() => this.getSystemMsg());
|
|
945
1153
|
const target = messages.find((m) => {
|
|
946
1154
|
const msg = m;
|
|
947
1155
|
return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
|
|
@@ -949,11 +1157,12 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
949
1157
|
if (!target?.flag) {
|
|
950
1158
|
throw new Error(`icqq 未找到待处理请求: ${id}`);
|
|
951
1159
|
}
|
|
1160
|
+
const flag = target.flag;
|
|
952
1161
|
if (target.request_type === 'friend') {
|
|
953
|
-
await this.
|
|
1162
|
+
await operation.run(() => this.setFriendAddRequest(flag, false));
|
|
954
1163
|
}
|
|
955
1164
|
else {
|
|
956
|
-
await this.
|
|
1165
|
+
await operation.run(() => this.setGroupAddRequest(flag, false, reason));
|
|
957
1166
|
}
|
|
958
1167
|
}
|
|
959
1168
|
}
|